Skip to content

Comments

Add custom type support for object attributes via type: option#47

Open
afuno wants to merge 9 commits intomainfrom
feature/SRV-358/any_models
Open

Add custom type support for object attributes via type: option#47
afuno wants to merge 9 commits intomainfrom
feature/SRV-358/any_models

Conversation

@afuno
Copy link
Member

@afuno afuno commented Jan 12, 2026

No description provided.

afuno added 2 commits January 13, 2026 02:30
- Create ValueExtractor class for polymorphic Hash/PORO access
- Add type: option to OptionNormalizer for normalization
- Add custom_type and custom_type? methods to Attribute::Base
- Extend TypeValidator with custom type validation and schema checks
- Add I18n keys for class type mismatch errors
- Update NestedObjectValidator to use ValueExtractor
- Extend NestedArrayValidator with object :_self pattern support
- Extend NestedTransformer for three array types (simple, self-object, complex)
- Renamed `self_attr` to `self_object_attribute` for clearer intent.
- Updated methods in `NestedTransformer` and `NestedArrayValidator` to use the new naming convention.
- Improved code readability by aligning variables with self-object context.
@afuno afuno added this to the v1.0.0 milestone Jan 12, 2026
@afuno afuno self-assigned this Jan 12, 2026
@afuno afuno added the feature label Jan 12, 2026
afuno added 7 commits January 13, 2026 18:44
- Added comprehensive RSpec tests for `ValueExtractor` to verify support for Hash, Struct, OpenStruct, custom PORO, and Data objects.
- Expanded `TypeValidator` test suite with custom type validation, including error handling, custom error messages, and lambda-based error message generation.
- Ensured compatibility with anonymous classes, Struct, and Data objects.
- Covered edge cases for nil, Hash, and non-object attribute types in `TypeValidator`.
- Marked edge case spec with `:aggregate_failures` in `TypeValidator` to allow grouped assertions.
- Updated comments in `TypeValidator` specs for clarity on handling anonymous classes.
- Removed tests for `OpenStruct` in `ValueExtractor` to simplify the test suite and avoid reliance on deprecated structures.
- Renamed context description in `ValueExtractor` tests for edge cases.
- Introduced `self_object_attribute` method to cache and optimize access to the self-object attribute.
- Updated `validate_regular_array_item!` to use the new method for better clarity and consistency.
- Improved edge case handling by falling back to `inspect` if `expected_class.name` is nil in `TypeValidator`.
- Introduced `ConditionalProcessing` module in `lib/treaty/entity/attribute/validation/concerns` to handle `:if` and `:unless` options for nested transformers.
- Moved duplicated conditional logic from `ObjectTransformer` and `ArrayTransformer` into the new concern.
- Updated `ObjectTransformer` and `ArrayTransformer` to include `ConditionalProcessing` for shared functionality.
- Added comprehensive RSpec tests for `ConditionalProcessing`, ensuring coverage for mutual exclusivity, caching, and conditional evaluation.
- Updated documentation to introduce the `type:` option for object attributes, allowing values to be instances of a custom class.
- Added examples for defining and validating custom types in nested structures.
- Expanded troubleshooting section with error scenarios and solutions for improper `type:` usage.
- Improved i18n messages to include `type:`-related error handling.
- Enhanced cheatsheet and API reference with concise `type:` usage examples.
- Added detailed explanations and examples for `:_self` pattern in arrays.
- Expanded documentation on using `type:` option for validating array items as specific class instances or hashes.
- Updated guides, references, and the cheatsheet with relevant examples, covering validation, use cases, and polymorphic acceptance details.
- Expanded documentation on the `type:` option for class instance validation.
- Added detailed examples combining `type:` with `use_entity` for reusable, type-safe nested attributes.
- Clarified behavior when `type:` is specified (only instances accepted, not hashes) across guides, API reference, and cheatsheet.
- Improved explanations for scenarios involving typed objects and array handling.
@afuno afuno marked this pull request as ready for review January 14, 2026 13:22
@afuno afuno added the claude label Jan 14, 2026
@claude
Copy link

claude bot commented Jan 14, 2026

🔍 Code Review - PR #47: Add custom type support for object attributes via type: option

📝 Summary

This PR introduces the ability to validate object attributes against custom class instances (POROs, ActiveRecord models, Structs, Data classes) instead of just Hashes. The implementation includes:

Core Changes:

  • ✅ Added type: option for object attributes to accept custom class instances
  • ✅ Extended TypeValidator with custom type validation logic
  • ✅ Introduced ValueExtractor for polymorphic value extraction (Hash vs PORO)
  • ✅ Extracted ConditionalProcessing concern to reduce duplication
  • ✅ Updated NestedTransformer to use ValueExtractor and ConditionalProcessing
  • ✅ Comprehensive test coverage (645 lines across 3 new test files)
  • ✅ Extensive documentation updates (11 doc files)

Scope: 1540 additions, 186 deletions across 21 files


🔒 Type Safety & Validation

Type System:

  • Excellent: Custom type validation properly integrated into existing type system
  • Correct: Schema validation enforces type: only works with object attribute type (lines 81-92 in type_validator.rb)
  • Safe: Value must be Class constant, not String/Symbol (lines 94-104 in type_validator.rb)
  • Robust: Custom type validation uses is_a? check (line 238 in type_validator.rb)
  • Backward Compatible: Without type:, Hash validation remains default (line 215 in type_validator.rb)

Validation Logic:

  • Required/optional: Works correctly with custom types (nil handled by RequiredValidator)
  • Presence checks: Proper delegation to RequiredValidator
  • Custom validators: Type validation integrated before nested validation
  • Error messages: Clear, I18n-enabled messages with interpolation support

Type Extraction:

  • Consistent: extract_custom_type in Base (lines 138-143) and extract_custom_type_from_schema in TypeValidator (lines 224-229) follow same pattern
  • Safe: Returns nil for non-Class values, preventing type confusion

Issues:
None found. Type safety implementation is excellent.


🎯 DSL Consistency

API Design:

  • Naming consistency: type: follows existing pattern of options (required:, format:, etc.)
  • Options format: Supports both simple and advanced modes correctly
    # Simple mode
    object :author, type: User do ... end
    
    # Advanced mode
    object :author, type: { is: User, message: "..." } do ... end
  • Helper shortcuts: No new helpers needed, works with existing :required, :optional

Breaking Changes:

  • None: This is a pure addition, no changes to existing behavior
  • Backward Compatible: Without type:, objects still expect Hash (default behavior preserved)
  • Deprecation: N/A - no deprecated features

Validation:

  • ⚠️ Important Note: When type: SomeClass is specified, only instances of that class are accepted. Hash is rejected. This is correct behavior but should be emphasized in docs.
    • ✓ Already documented in docs/attributes.md:234 and docs/nested-structures.md:203

🔄 Version Management

Version Handling:

  • ✅ N/A - No changes to version management system

🔀 Transformation Pipeline

Request Processing:

  • Validation before transform: Correct order maintained (type check → required check → nested transform)
  • Attribute renaming: as: option still works with custom types
  • Default values: Compatible with custom types
  • Object merging: :_self works correctly (not affected by this change)

Response Processing:

  • Output validation: Custom type instances validated in responses
  • Transformation correctness: ValueExtractor properly extracts from PORO objects
  • Error handling: Nested errors properly wrapped with context

Data Flow Analysis:

Flow for object with custom type:

1. TypeValidator.validate_object! (line 209)
   ↓
2. extract_custom_type_from_schema (line 224)
   ↓
3. validate_custom_type! (line 237) - is_a? check
   ↓
4. NestedTransformer.transform_object (line 48)
   ↓
5. ObjectTransformer.transform (line 85)
   ↓
6. ValueExtractor.extract (line 35-42) - polymorphic extraction
   ↓
7. Nested attribute validation + transformation

Critical Path:

  • ✅ Custom type validation happens before nested attribute processing
  • ✅ ValueExtractor handles both Hash (fetch) and PORO (public_send) seamlessly
  • ✅ Nil handling: TypeValidator skips nil (line 114), RequiredValidator handles it
  • ✅ Error context preserved through all layers

Potential Issues:

  • No data loss: ValueExtractor returns nil for missing keys/methods (safe fallback)
  • Idempotency: Transformations are pure (no side effects)
  • Private methods: ValueExtractor checks respond_to? before public_send (line 40)

🏗️ Nested Structures

Objects:

  • Hash validation: Default behavior preserved when type: not specified
  • Nested attributes: Custom type objects work correctly with nested validation
  • Deep nesting: No changes to nesting level limits
  • Nil handling: Proper nil handling for optional objects with custom types

Arrays:

  • Array validation: Type validator for arrays unchanged
  • Item validation: object :_self, type: User pattern works correctly (lines 162-222 in nested_transformer.rb)
  • Empty array: No issues (type validation skipped for empty arrays)
  • No default: [] anti-pattern: Not introduced

Use with use_entity:

  • Documented: Combination of type: with use_entity well-documented in docs/entities.md:315-366
  • Correct behavior: Type validation happens first, then entity attributes copied for nested validation
  • ⚠️ Important caveat: When using type: + use_entity, the entity defines structure for extraction, but type validation ensures correct class

🚀 Delegation Safety

Service Integration:

  • Target resolution: No changes to delegation mechanism
  • Error propagation: Custom type validation errors properly raised as Treaty::Exceptions::Validation

Note: No integration tests found using custom types with actual services. Consider adding to spec/treaties/ for real-world validation.


🛤️ Rails 7.1+ Integration

Engine Compatibility:

  • Rails 7.1 APIs: No deprecated APIs used
  • Engine initialization: No changes to engine initialization
  • Dependencies: No new dependencies added

Ruby 3.2+ Compatibility:

  • Data class: Tests include Data class support (Ruby 3.2+ feature) - line 613 in type_validator_spec.rb
  • Struct: Tests include keyword_init Struct support - line 600 in type_validator_spec.rb
  • Anonymous classes: Properly handled in tests - line 581 in type_validator_spec.rb

🔄 Inventory System

No changes to inventory system - ✅ Correct, this PR doesn't affect inventory


✨ Code Quality

Ruby 3.2+ Features:

  • Frozen string literals: All new files have # frozen_string_literal: true
  • Pattern matching: Not used (not needed for this feature)
  • Keyword arguments: Properly used throughout
  • Modern idioms: Clean, readable Ruby code

Design Patterns:

  • SOLID principles:

    • Single Responsibility: ValueExtractor does one thing (extract values)
    • Open/Closed: TypeValidator extended without modifying existing validators
    • Liskov Substitution: Custom types work as drop-in replacements
    • Interface Segregation: Concerns properly extracted
    • Dependency Inversion: Depends on abstractions (is_a? check)
  • DRY compliance:

    • Excellent refactor: ConditionalProcessing concern eliminates duplication between ObjectTransformer and ArrayTransformer
    • ValueExtractor: Single source of truth for value extraction logic
    • Consistent patterns: extract_custom_type used in both Base and TypeValidator
  • Abstraction levels: Clear separation between validation, transformation, and extraction

Performance:

  • No N+1: Conditionals cached in build_conditionals_for_attributes (line 77 in conditional_processing.rb)
  • Efficient data structures: Custom type extracted once and cached via @custom_type ||= (line 121 in base.rb)
  • Lazy evaluation: Conditionals built once, reused for all nested attributes
  • Memory allocations: Minimal - ValueExtractor uses simple case statement

Code Organization:

  • Well-structured: New concern properly namespaced under Validation::Concerns
  • Clear naming: ValueExtractor, ConditionalProcessing - names clearly convey purpose
  • Good documentation: Comprehensive YARD comments in all new files

🧪 Testing Coverage

RSpec Tests:

  • Unit tests: Comprehensive unit tests for new code

    • TypeValidator: 224 lines of tests covering custom type validation
    • ConditionalProcessing: 261 lines testing concern behavior
    • ValueExtractor: 120 lines testing Hash/PORO extraction
  • Integration tests: Existing treaty tests cover integration (no new integration tests found)

  • Edge cases: Excellent edge case coverage

    • Anonymous classes (line 581)
    • Struct with keyword_init (line 600)
    • Data class (Ruby 3.2+) (line 613)
    • Custom error messages (line 556)
    • Lambda error messages (line 566)
    • Private methods (line 82 in value_extractor_spec.rb)
    • Nil source (line 101 in value_extractor_spec.rb)
  • Error scenarios:

    • Type mismatch errors tested
    • Schema validation errors tested
    • Invalid class errors tested
    • Mutual exclusivity errors tested (line 87 in conditional_processing_spec.rb)

Test Quality:

  • Clear test descriptions: Excellent RSpec descriptions
  • Proper setup/teardown: Clean test structure with let blocks
  • No flaky tests: Tests are deterministic
  • Fast execution: Unit tests are fast (no external dependencies)

Coverage Estimate: ~95%+ for new code (all new classes have comprehensive specs)

Missing:

  • ⚠️ Integration tests: No full treaty integration tests found using type: option with actual treaties/services
  • ⚠️ Sandbox examples: No examples in spec/sandbox using the new feature
  • 💡 Recommendation: Add example treaty in spec/sandbox and integration test in spec/treaties demonstrating real-world usage

📚 Documentation

Code Documentation:

  • Public API documented: All public methods have YARD comments
  • Complex logic explained: Clear inline comments and YARD docs
  • Examples provided: Test files serve as usage examples

User Documentation:

  • docs/ updated: 11 documentation files updated

    • api-reference.md (29 additions)
    • attributes.md (46 additions) - comprehensive type: section
    • cheatsheet.md (33 additions)
    • entities.md (67 additions) - use_entity + type: combination
    • nested-structures.md (82 additions) - typed instances in arrays
    • troubleshooting.md (65 additions) - 3 new error scenarios
    • validation.md (94 additions) - custom type validation section
    • internationalization.md (8 additions) - new I18n keys
  • Examples are up-to-date: Documentation includes correct usage examples

  • Breaking changes documented: N/A (no breaking changes)

  • Migration guides: N/A (pure addition)

Documentation Quality:

  • Clear examples: Multiple code examples in every doc
  • Common pitfalls documented: Troubleshooting section covers typical errors
  • Consistency: Documentation style consistent with existing docs

I18n Updates:

  • English locale: 3 new keys added (option_type_mismatch, invalid_class, class)
  • ⚠️ German locale: Translations provided (good!)
  • 💡 Note: Other locales may need updating if they exist

💡 Recommendations

Critical (Must Fix):
None. Code is production-ready.

Important (Should Fix):

  1. Add integration test example (Recommended)

    • Create example treaty in spec/sandbox/app/treaties/ using type: option
    • Add corresponding integration test in spec/treaties/
    • Demonstrates real-world usage with services returning PORO objects

    Example location: spec/treaties/users/create_treaty_spec.rb

    # Define PORO
    class User
      attr_reader :id, :name, :email
      def initialize(id:, name:, email:)
        @id, @name, @email = id, name, email
      end
    end
    
    # Use in response
    response 201 do
      object :user, type: User do
        string :id
        string :name
        string :email
      end
    end
  2. Consider adding performance benchmark (Optional)

    • Test performance impact of ValueExtractor vs direct Hash access
    • Document any performance considerations in README
    • Current implementation looks efficient, but benchmark would provide confidence

Nice to Have (Can Improve):

  1. Enhance error messages for type mismatches (Optional improvement)

    • Current: "Attribute 'author' must be User, got Hash"
    • Could add: "Did you mean to remove type: option? Objects without type: expect Hash."
    • Location: lib/treaty/entity/attribute/option/validators/type_validator.rb:258-264
  2. Add rubocop metrics context (Optional)

    • # rubocop:disable Metrics/ClassLength used in nested_transformer.rb (line 70, 131)
    • # rubocop:disable Metrics/MethodLength used extensively
    • Consider if any methods can be simplified further
    • Current disables are reasonable given complexity
  3. Document performance characteristics (Optional)

    • ValueExtractor uses respond_to? which has small overhead
    • Document in ARCHITECTURE.md if it exists
    • Minimal impact, but worth documenting for contributors

📊 Risk Assessment

  • Type safety: 🟢 LOW - Excellent validation at schema and value levels
  • DSL consistency: 🟢 LOW - Natural extension of existing DSL, backward compatible
  • Version management: 🟢 LOW - No changes
  • Transformation pipeline: 🟢 LOW - Well-integrated, proper error handling
  • Inventory system: 🟢 LOW - No changes
  • Rails integration: 🟢 LOW - No Rails-specific changes, engine unaffected
  • Testing: 🟡 MEDIUM - Unit tests excellent, integration tests missing
  • Documentation: 🟢 LOW - Comprehensive documentation updates

Overall Risk: 🟢 LOW

Risk Factors:

  • ✅ No breaking changes
  • ✅ Extensive test coverage for new code
  • ✅ Backward compatible (Hash still works without type:)
  • ✅ Clear error messages
  • ⚠️ Minimal integration testing (can be addressed post-merge)
  • ✅ Well-documented

Legend:

  • 🟢 LOW: No issues, safe to merge
  • 🟡 MEDIUM: Minor issues, can merge with follow-up
  • 🟠 HIGH: Important issues, should fix before merge
  • 🔴 CRITICAL: Blocking issues, must fix before merge

✅ Final Decision

🟢 APPROVED

Reason: This is an excellent, well-engineered feature addition that enhances Treaty's type safety without compromising backward compatibility. The implementation demonstrates:

  1. Strong software engineering principles: Clean abstractions (ValueExtractor, ConditionalProcessing), proper separation of concerns, DRY compliance
  2. Comprehensive testing: 605 lines of unit tests with excellent edge case coverage
  3. Thorough documentation: 416 lines of documentation updates across 11 files
  4. Zero breaking changes: Existing code continues to work unchanged
  5. Production-ready code: Frozen string literals, proper error handling, I18n support

The only minor gap is lack of integration tests demonstrating real-world usage with services, but this doesn't block the merge as unit tests are comprehensive and the integration follows established patterns.

Must Fix (Blocking):
None.

Should Fix (Important):

  1. Add integration test example in spec/treaties/ (can be follow-up PR)
  2. Add sandbox example demonstrating type: usage (can be follow-up PR)

Can Improve (Optional):

  1. Performance benchmark for ValueExtractor
  2. Enhanced error messages suggesting Hash when type mismatch occurs
  3. Document performance characteristics of respond_to? check

🎯 Code Review Highlights

What this PR does exceptionally well:

  1. Refactoring quality: The extraction of ConditionalProcessing concern is exemplary - removes 65+ lines of duplication
  2. Polymorphic design: ValueExtractor elegantly handles Hash and PORO with single interface
  3. Error handling: Every error path has clear, I18n-enabled messages with context
  4. Test coverage: Anonymous classes, Struct, Data class - nothing left untested
  5. Documentation: Users will know exactly how to use this feature

Technical debt addressed:

  • ✅ Extracted conditional logic duplication
  • ✅ Unified value extraction (no more scattered fetch vs public_send)
  • ✅ Reduced nested_transformer.rb from 432 lines to 367 lines (15% reduction)

Patterns to replicate in future PRs:

  • Clean concern extraction with clear purpose
  • Comprehensive YARD documentation
  • Edge case testing (anonymous classes, Data, Struct)
  • I18n from day one

Review completed by Claude Sonnet 4.5
Analysis performed: January 14, 2026
Treaty v0.20.0 | Ruby 3.2+ | Rails 7.1+

@afuno afuno removed the claude label Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant