From 0ab0055dea8d42bab9825f7885b5403e4e758d6e Mon Sep 17 00:00:00 2001 From: Robb Shecter Date: Wed, 27 Aug 2025 20:54:01 -0600 Subject: [PATCH 1/6] docs --- lib/validated_object/simplified_api.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/validated_object/simplified_api.rb b/lib/validated_object/simplified_api.rb index 41560d7..a89484d 100644 --- a/lib/validated_object/simplified_api.rb +++ b/lib/validated_object/simplified_api.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'active_support/concern' -# Enable a simplified API for the common case of -# read-only ValidatedObjects. module ValidatedObject + # Enable a simplified API for the common case of + # read-only ValidatedObjects. module SimplifiedApi extend ActiveSupport::Concern @@ -19,7 +21,6 @@ def validated(*args, **kwargs, &block) validates(*args, **kwargs, &block) end - # Alias for validated_attr for compatibility with test usage. def validates_attr(attribute, *options, **kwargs) attr_reader attribute From a48fc55c6255b0aa2fbd58fb1bf9489bc1b4fdd1 Mon Sep 17 00:00:00 2001 From: Robb Shecter Date: Wed, 27 Aug 2025 20:54:59 -0600 Subject: [PATCH 2/6] first pass at failing specs --- spec/validated_object_spec.rb | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/spec/validated_object_spec.rb b/spec/validated_object_spec.rb index f2f89b2..8306123 100644 --- a/spec/validated_object_spec.rb +++ b/spec/validated_object_spec.rb @@ -229,5 +229,86 @@ class SpecStreamlinedPost < ValidatedObject::Base expect(SpecStreamlinedPost.new(comments: nil, tags: nil, id: 13)).to be_valid end end + + context 'when using union types' do + let(:multi_type_class) do + Class.new(ValidatedObject::Base) do + attr_accessor :id, :status, :data + + validates :id, type: union(String, Integer) + validates :status, type: union(:active, :inactive, :pending) + validates :data, type: union(Hash, [Hash]), allow_nil: true + end + end + + it 'accepts values matching first union type (String)' do + obj = multi_type_class.new(id: 'abc123', status: :active, data: { key: 'value' }) + expect(obj).to be_valid + end + + it 'accepts values matching second union type (Integer)' do + obj = multi_type_class.new(id: 42, status: :inactive, data: nil) + expect(obj).to be_valid + end + + it 'accepts values matching array element type in union' do + obj = multi_type_class.new(id: 'test', status: :pending, data: [{ a: 1 }, { b: 2 }]) + expect(obj).to be_valid + end + + it 'rejects values not matching any union type' do + expect do + multi_type_class.new(id: 3.14, status: :active, data: {}) + end.to raise_error(ArgumentError, /is a Float, not one of String or Integer/) + end + + it 'rejects invalid symbol values in union' do + expect do + multi_type_class.new(id: 'test', status: :invalid, data: {}) + end.to raise_error(ArgumentError, /is a Symbol.*not one of.*active.*inactive.*pending/) + end + + it 'works with validates_attr syntax' do + union_attr_class = Class.new(ValidatedObject::Base) do + validates_attr :mixed, type: union(String, Integer, [String]) + end + + expect(union_attr_class.new(mixed: 'text')).to be_valid + expect(union_attr_class.new(mixed: 42)).to be_valid + expect(union_attr_class.new(mixed: %w[a b c])).to be_valid + + expect do + union_attr_class.new(mixed: 3.14) + end.to raise_error(ArgumentError, /is a Float.*not one of.*String.*Integer.*Array of String/) + end + + it 'handles complex union with multiple array types' do + complex_class = Class.new(ValidatedObject::Base) do + attr_accessor :flexible + validates :flexible, type: union(String, Integer, [String], [Hash]) + end + + expect(complex_class.new(flexible: 'text')).to be_valid + expect(complex_class.new(flexible: 123)).to be_valid + expect(complex_class.new(flexible: %w[a b])).to be_valid + expect(complex_class.new(flexible: [{ a: 1 }, { b: 2 }])).to be_valid + + expect do + complex_class.new(flexible: [1, 2, 3]) + end.to raise_error(ArgumentError, /Array.*contains non-String elements/) + end + + it 'works with single type union (equivalent to regular type validation)' do + single_union_class = Class.new(ValidatedObject::Base) do + attr_accessor :name + validates :name, type: union(String) + end + + expect(single_union_class.new(name: 'test')).to be_valid + expect do + single_union_class.new(name: 123) + end.to raise_error(ArgumentError, /is a Integer, not one of String/) + end + end end end From f090c1093633c17c92ba3e897d3d2649a1a9e310 Mon Sep 17 00:00:00 2001 From: Robb Shecter Date: Wed, 27 Aug 2025 20:58:05 -0600 Subject: [PATCH 3/6] refactor: extract specs --- spec/union_types_spec.rb | 93 +++++++++++++++++++++++++++++++++++ spec/validated_object_spec.rb | 81 ------------------------------ 2 files changed, 93 insertions(+), 81 deletions(-) create mode 100644 spec/union_types_spec.rb diff --git a/spec/union_types_spec.rb b/spec/union_types_spec.rb new file mode 100644 index 0000000..2f68075 --- /dev/null +++ b/spec/union_types_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'validated_object' + +# +# I needed to create actual classes for these specs to work. +# Therefore I namespaced them with Spec. +# +class SpecUnionComment; end + +describe 'ValidatedObject Union Types' do + context 'when using union types' do + let(:multi_type_class) do + Class.new(ValidatedObject::Base) do + attr_accessor :id, :status, :data + + validates :id, type: union(String, Integer) + validates :status, type: union(:active, :inactive, :pending) + validates :data, type: union(Hash, [Hash]), allow_nil: true + end + end + + it 'accepts values matching first union type (String)' do + obj = multi_type_class.new(id: 'abc123', status: :active, data: { key: 'value' }) + expect(obj).to be_valid + end + + it 'accepts values matching second union type (Integer)' do + obj = multi_type_class.new(id: 42, status: :inactive, data: nil) + expect(obj).to be_valid + end + + it 'accepts values matching array element type in union' do + obj = multi_type_class.new(id: 'test', status: :pending, data: [{ a: 1 }, { b: 2 }]) + expect(obj).to be_valid + end + + it 'rejects values not matching any union type' do + expect do + multi_type_class.new(id: 3.14, status: :active, data: {}) + end.to raise_error(ArgumentError, /is a Float, not one of String or Integer/) + end + + it 'rejects invalid symbol values in union' do + expect do + multi_type_class.new(id: 'test', status: :invalid, data: {}) + end.to raise_error(ArgumentError, /is a Symbol.*not one of.*active.*inactive.*pending/) + end + + it 'works with validates_attr syntax' do + union_attr_class = Class.new(ValidatedObject::Base) do + validates_attr :mixed, type: union(String, Integer, [String]) + end + + expect(union_attr_class.new(mixed: 'text')).to be_valid + expect(union_attr_class.new(mixed: 42)).to be_valid + expect(union_attr_class.new(mixed: %w[a b c])).to be_valid + + expect do + union_attr_class.new(mixed: 3.14) + end.to raise_error(ArgumentError, /is a Float.*not one of.*String.*Integer.*Array of String/) + end + + it 'handles complex union with multiple array types' do + complex_class = Class.new(ValidatedObject::Base) do + attr_accessor :flexible + validates :flexible, type: union(String, Integer, [String], [Hash]) + end + + expect(complex_class.new(flexible: 'text')).to be_valid + expect(complex_class.new(flexible: 123)).to be_valid + expect(complex_class.new(flexible: %w[a b])).to be_valid + expect(complex_class.new(flexible: [{ a: 1 }, { b: 2 }])).to be_valid + + expect do + complex_class.new(flexible: [1, 2, 3]) + end.to raise_error(ArgumentError, /Array.*contains non-String elements/) + end + + it 'works with single type union (equivalent to regular type validation)' do + single_union_class = Class.new(ValidatedObject::Base) do + attr_accessor :name + validates :name, type: union(String) + end + + expect(single_union_class.new(name: 'test')).to be_valid + expect do + single_union_class.new(name: 123) + end.to raise_error(ArgumentError, /is a Integer, not one of String/) + end + end +end \ No newline at end of file diff --git a/spec/validated_object_spec.rb b/spec/validated_object_spec.rb index 8306123..f2f89b2 100644 --- a/spec/validated_object_spec.rb +++ b/spec/validated_object_spec.rb @@ -229,86 +229,5 @@ class SpecStreamlinedPost < ValidatedObject::Base expect(SpecStreamlinedPost.new(comments: nil, tags: nil, id: 13)).to be_valid end end - - context 'when using union types' do - let(:multi_type_class) do - Class.new(ValidatedObject::Base) do - attr_accessor :id, :status, :data - - validates :id, type: union(String, Integer) - validates :status, type: union(:active, :inactive, :pending) - validates :data, type: union(Hash, [Hash]), allow_nil: true - end - end - - it 'accepts values matching first union type (String)' do - obj = multi_type_class.new(id: 'abc123', status: :active, data: { key: 'value' }) - expect(obj).to be_valid - end - - it 'accepts values matching second union type (Integer)' do - obj = multi_type_class.new(id: 42, status: :inactive, data: nil) - expect(obj).to be_valid - end - - it 'accepts values matching array element type in union' do - obj = multi_type_class.new(id: 'test', status: :pending, data: [{ a: 1 }, { b: 2 }]) - expect(obj).to be_valid - end - - it 'rejects values not matching any union type' do - expect do - multi_type_class.new(id: 3.14, status: :active, data: {}) - end.to raise_error(ArgumentError, /is a Float, not one of String or Integer/) - end - - it 'rejects invalid symbol values in union' do - expect do - multi_type_class.new(id: 'test', status: :invalid, data: {}) - end.to raise_error(ArgumentError, /is a Symbol.*not one of.*active.*inactive.*pending/) - end - - it 'works with validates_attr syntax' do - union_attr_class = Class.new(ValidatedObject::Base) do - validates_attr :mixed, type: union(String, Integer, [String]) - end - - expect(union_attr_class.new(mixed: 'text')).to be_valid - expect(union_attr_class.new(mixed: 42)).to be_valid - expect(union_attr_class.new(mixed: %w[a b c])).to be_valid - - expect do - union_attr_class.new(mixed: 3.14) - end.to raise_error(ArgumentError, /is a Float.*not one of.*String.*Integer.*Array of String/) - end - - it 'handles complex union with multiple array types' do - complex_class = Class.new(ValidatedObject::Base) do - attr_accessor :flexible - validates :flexible, type: union(String, Integer, [String], [Hash]) - end - - expect(complex_class.new(flexible: 'text')).to be_valid - expect(complex_class.new(flexible: 123)).to be_valid - expect(complex_class.new(flexible: %w[a b])).to be_valid - expect(complex_class.new(flexible: [{ a: 1 }, { b: 2 }])).to be_valid - - expect do - complex_class.new(flexible: [1, 2, 3]) - end.to raise_error(ArgumentError, /Array.*contains non-String elements/) - end - - it 'works with single type union (equivalent to regular type validation)' do - single_union_class = Class.new(ValidatedObject::Base) do - attr_accessor :name - validates :name, type: union(String) - end - - expect(single_union_class.new(name: 'test')).to be_valid - expect do - single_union_class.new(name: 123) - end.to raise_error(ArgumentError, /is a Integer, not one of String/) - end - end end end From 366891563361a1dba32ff13be352ebd34758cd15 Mon Sep 17 00:00:00 2001 From: Robb Shecter Date: Wed, 27 Aug 2025 21:28:28 -0600 Subject: [PATCH 4/6] all specs pass --- lib/validated_object.rb | 77 ++++++++++++++++++++++++ lib/validated_object/simplified_api.rb | 19 ++++-- spec/union_types_spec.rb | 81 +++++++++++++------------- 3 files changed, 130 insertions(+), 47 deletions(-) diff --git a/lib/validated_object.rb b/lib/validated_object.rb index 4705c89..546be22 100644 --- a/lib/validated_object.rb +++ b/lib/validated_object.rb @@ -54,6 +54,21 @@ class Base class Boolean end + # A private class definition for union types. + # Stores multiple allowed types for validation. + # Created via ValidatedObject::Base.union(*types) + # + # @example + # validates :id, type: union(String, Integer) + # validates :data, type: union(Hash, [Hash]) + class Union + attr_reader :types + + def initialize(*types) + @types = types + end + end + # Instantiate and validate a new object. # @example # maru = Dog.new(birthday: Date.today, name: 'Maru') @@ -99,6 +114,14 @@ def validate_each(record, attribute, value) validation_options = options expected_class = validation_options[:with] + # Support union types + if expected_class.is_a?(Union) + return if validate_union_type(record, attribute, value, expected_class, validation_options) + + save_union_error(record, attribute, value, expected_class, validation_options) + return + end + # Support type: Array, element_type: ElementType if expected_class == Array && validation_options[:element_type] return save_error(record, attribute, value, validation_options) unless value.is_a?(Array) @@ -134,6 +157,50 @@ def save_error(record, attribute, value, validation_options) record.errors.add attribute, validation_options[:message] || "is a #{value.class}, not a #{validation_options[:with]}" end + + def validate_union_type(record, attribute, value, union, validation_options) + union.types.any? do |type_spec| + if type_spec.is_a?(Array) && type_spec.length == 1 + # Handle [ElementType] syntax within union + validate_array_element_type(value, type_spec[0]) + elsif type_spec.is_a?(Class) || type_spec == Boolean + # Handle class types (String, Integer, etc.) and pseudo-boolean + pseudo_boolean?(type_spec, value) || expected_class?(type_spec, value) + else + # Handle literal values (symbols, strings, numbers, etc.) + value == type_spec + end + end + end + + def validate_array_element_type(value, element_type) + return false unless value.is_a?(Array) + + value.all? { |el| el.is_a?(element_type) } + end + + def save_union_error(record, attribute, value, union, validation_options) + return if validation_options[:message] + + type_names = union.types.map do |type_spec| + if type_spec.is_a?(Array) && type_spec.length == 1 + "Array of #{type_spec[0]}" + elsif type_spec.is_a?(Class) || type_spec == Boolean + type_spec.to_s + else + # For literal values like :active, show them as-is + type_spec.inspect + end + end + + message = if type_names.length == 1 + "is a #{value.class}, not one of #{type_names.first}" + else + "is a #{value.class}, not one of #{type_names.join(', ')}" + end + + record.errors.add attribute, message + end end # Register the TypeValidator with ActiveModel so `type:` validation option works @@ -146,6 +213,16 @@ def self.validated(*args, **kwargs, &block) validates(*args, **kwargs, &block) end + # Create a union type specification for validation + # @param types [Array] The types that are allowed + # @return [Union] A union type specification + # @example + # validates :id, type: union(String, Integer) + # validates :data, type: union(Hash, [Hash]) + def self.union(*types) + Union.new(*types) + end + private def set_instance_variables(from_hash:) diff --git a/lib/validated_object/simplified_api.rb b/lib/validated_object/simplified_api.rb index a89484d..e0f1bb5 100644 --- a/lib/validated_object/simplified_api.rb +++ b/lib/validated_object/simplified_api.rb @@ -28,16 +28,23 @@ def validates_attr(attribute, *options, **kwargs) type_val = kwargs.delete(:type) element_type = kwargs.delete(:element_type) - # Parse Array[ElementType] syntax - if type_val.is_a?(Array) && type_val.length == 1 && type_val[0].is_a?(Class) + # Handle Union types - pass them through directly + if type_val.is_a?(ValidatedObject::Base::Union) + opts = { type: { with: type_val } } + validates attribute, opts.merge(kwargs) + # Parse Array[ElementType] syntax + elsif type_val.is_a?(Array) && type_val.length == 1 && type_val[0].is_a?(Class) # This handles Array[Comment] syntax element_type = type_val[0] type_val = Array + opts = { type: { with: type_val } } + opts[:type][:element_type] = element_type if element_type + validates attribute, opts.merge(kwargs) + else + opts = { type: { with: type_val } } + opts[:type][:element_type] = element_type if element_type + validates attribute, opts.merge(kwargs) end - - opts = { type: { with: type_val } } - opts[:type][:element_type] = element_type if element_type - validates attribute, opts.merge(kwargs) else validates attribute, *options, **kwargs end diff --git a/spec/union_types_spec.rb b/spec/union_types_spec.rb index 2f68075..28e0c39 100644 --- a/spec/union_types_spec.rb +++ b/spec/union_types_spec.rb @@ -9,84 +9,83 @@ # class SpecUnionComment; end +class SpecMultiType < ValidatedObject::Base + attr_accessor :id, :status, :data + + validates :id, type: union(String, Integer) + validates :status, type: union(:active, :inactive, :pending) + validates :data, type: union(Hash, [Hash]), allow_nil: true +end + +class SpecUnionAttr < ValidatedObject::Base + validates_attr :mixed, type: union(String, Integer, [String]) +end + +class SpecComplexUnion < ValidatedObject::Base + attr_accessor :flexible + validates :flexible, type: union(String, Integer, [String], [Hash]) +end + +class SpecSingleUnion < ValidatedObject::Base + attr_accessor :name + validates :name, type: union(String) +end + describe 'ValidatedObject Union Types' do context 'when using union types' do - let(:multi_type_class) do - Class.new(ValidatedObject::Base) do - attr_accessor :id, :status, :data - - validates :id, type: union(String, Integer) - validates :status, type: union(:active, :inactive, :pending) - validates :data, type: union(Hash, [Hash]), allow_nil: true - end - end it 'accepts values matching first union type (String)' do - obj = multi_type_class.new(id: 'abc123', status: :active, data: { key: 'value' }) + obj = SpecMultiType.new(id: 'abc123', status: :active, data: { key: 'value' }) expect(obj).to be_valid end it 'accepts values matching second union type (Integer)' do - obj = multi_type_class.new(id: 42, status: :inactive, data: nil) + obj = SpecMultiType.new(id: 42, status: :inactive, data: nil) expect(obj).to be_valid end it 'accepts values matching array element type in union' do - obj = multi_type_class.new(id: 'test', status: :pending, data: [{ a: 1 }, { b: 2 }]) + obj = SpecMultiType.new(id: 'test', status: :pending, data: [{ a: 1 }, { b: 2 }]) expect(obj).to be_valid end it 'rejects values not matching any union type' do expect do - multi_type_class.new(id: 3.14, status: :active, data: {}) - end.to raise_error(ArgumentError, /is a Float, not one of String or Integer/) + SpecMultiType.new(id: 3.14, status: :active, data: {}) + end.to raise_error(ArgumentError, /is a Float, not one of String, Integer/) end it 'rejects invalid symbol values in union' do expect do - multi_type_class.new(id: 'test', status: :invalid, data: {}) + SpecMultiType.new(id: 'test', status: :invalid, data: {}) end.to raise_error(ArgumentError, /is a Symbol.*not one of.*active.*inactive.*pending/) end it 'works with validates_attr syntax' do - union_attr_class = Class.new(ValidatedObject::Base) do - validates_attr :mixed, type: union(String, Integer, [String]) - end - - expect(union_attr_class.new(mixed: 'text')).to be_valid - expect(union_attr_class.new(mixed: 42)).to be_valid - expect(union_attr_class.new(mixed: %w[a b c])).to be_valid + expect(SpecUnionAttr.new(mixed: 'text')).to be_valid + expect(SpecUnionAttr.new(mixed: 42)).to be_valid + expect(SpecUnionAttr.new(mixed: %w[a b c])).to be_valid expect do - union_attr_class.new(mixed: 3.14) + SpecUnionAttr.new(mixed: 3.14) end.to raise_error(ArgumentError, /is a Float.*not one of.*String.*Integer.*Array of String/) end it 'handles complex union with multiple array types' do - complex_class = Class.new(ValidatedObject::Base) do - attr_accessor :flexible - validates :flexible, type: union(String, Integer, [String], [Hash]) - end - - expect(complex_class.new(flexible: 'text')).to be_valid - expect(complex_class.new(flexible: 123)).to be_valid - expect(complex_class.new(flexible: %w[a b])).to be_valid - expect(complex_class.new(flexible: [{ a: 1 }, { b: 2 }])).to be_valid + expect(SpecComplexUnion.new(flexible: 'text')).to be_valid + expect(SpecComplexUnion.new(flexible: 123)).to be_valid + expect(SpecComplexUnion.new(flexible: %w[a b])).to be_valid + expect(SpecComplexUnion.new(flexible: [{ a: 1 }, { b: 2 }])).to be_valid expect do - complex_class.new(flexible: [1, 2, 3]) - end.to raise_error(ArgumentError, /Array.*contains non-String elements/) + SpecComplexUnion.new(flexible: [1, 2, 3]) + end.to raise_error(ArgumentError, /is a Array, not one of String, Integer, Array of String, Array of Hash/) end it 'works with single type union (equivalent to regular type validation)' do - single_union_class = Class.new(ValidatedObject::Base) do - attr_accessor :name - validates :name, type: union(String) - end - - expect(single_union_class.new(name: 'test')).to be_valid + expect(SpecSingleUnion.new(name: 'test')).to be_valid expect do - single_union_class.new(name: 123) + SpecSingleUnion.new(name: 123) end.to raise_error(ArgumentError, /is a Integer, not one of String/) end end From a49dcea04cee6f3358f9435ea3c3abca5cd7db64 Mon Sep 17 00:00:00 2001 From: Robb Shecter Date: Wed, 27 Aug 2025 21:29:52 -0600 Subject: [PATCH 5/6] rubocop fix --- lib/validated_object.rb | 20 ++++++++++---------- lib/validated_object/simplified_api.rb | 2 +- spec/union_types_spec.rb | 9 +++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/validated_object.rb b/lib/validated_object.rb index 546be22..611715d 100644 --- a/lib/validated_object.rb +++ b/lib/validated_object.rb @@ -117,7 +117,7 @@ def validate_each(record, attribute, value) # Support union types if expected_class.is_a?(Union) return if validate_union_type(record, attribute, value, expected_class, validation_options) - + save_union_error(record, attribute, value, expected_class, validation_options) return end @@ -158,7 +158,7 @@ def save_error(record, attribute, value, validation_options) validation_options[:message] || "is a #{value.class}, not a #{validation_options[:with]}" end - def validate_union_type(record, attribute, value, union, validation_options) + def validate_union_type(_record, _attribute, value, union, _validation_options) union.types.any? do |type_spec| if type_spec.is_a?(Array) && type_spec.length == 1 # Handle [ElementType] syntax within union @@ -175,13 +175,13 @@ def validate_union_type(record, attribute, value, union, validation_options) def validate_array_element_type(value, element_type) return false unless value.is_a?(Array) - + value.all? { |el| el.is_a?(element_type) } end def save_union_error(record, attribute, value, union, validation_options) return if validation_options[:message] - + type_names = union.types.map do |type_spec| if type_spec.is_a?(Array) && type_spec.length == 1 "Array of #{type_spec[0]}" @@ -192,13 +192,13 @@ def save_union_error(record, attribute, value, union, validation_options) type_spec.inspect end end - + message = if type_names.length == 1 - "is a #{value.class}, not one of #{type_names.first}" - else - "is a #{value.class}, not one of #{type_names.join(', ')}" - end - + "is a #{value.class}, not one of #{type_names.first}" + else + "is a #{value.class}, not one of #{type_names.join(', ')}" + end + record.errors.add attribute, message end end diff --git a/lib/validated_object/simplified_api.rb b/lib/validated_object/simplified_api.rb index e0f1bb5..12ac72d 100644 --- a/lib/validated_object/simplified_api.rb +++ b/lib/validated_object/simplified_api.rb @@ -32,7 +32,7 @@ def validates_attr(attribute, *options, **kwargs) if type_val.is_a?(ValidatedObject::Base::Union) opts = { type: { with: type_val } } validates attribute, opts.merge(kwargs) - # Parse Array[ElementType] syntax + # Parse Array[ElementType] syntax elsif type_val.is_a?(Array) && type_val.length == 1 && type_val[0].is_a?(Class) # This handles Array[Comment] syntax element_type = type_val[0] diff --git a/spec/union_types_spec.rb b/spec/union_types_spec.rb index 28e0c39..19b4594 100644 --- a/spec/union_types_spec.rb +++ b/spec/union_types_spec.rb @@ -11,7 +11,7 @@ class SpecUnionComment; end class SpecMultiType < ValidatedObject::Base attr_accessor :id, :status, :data - + validates :id, type: union(String, Integer) validates :status, type: union(:active, :inactive, :pending) validates :data, type: union(Hash, [Hash]), allow_nil: true @@ -23,17 +23,18 @@ class SpecUnionAttr < ValidatedObject::Base class SpecComplexUnion < ValidatedObject::Base attr_accessor :flexible + validates :flexible, type: union(String, Integer, [String], [Hash]) end class SpecSingleUnion < ValidatedObject::Base attr_accessor :name + validates :name, type: union(String) end describe 'ValidatedObject Union Types' do context 'when using union types' do - it 'accepts values matching first union type (String)' do obj = SpecMultiType.new(id: 'abc123', status: :active, data: { key: 'value' }) expect(obj).to be_valid @@ -65,7 +66,7 @@ class SpecSingleUnion < ValidatedObject::Base expect(SpecUnionAttr.new(mixed: 'text')).to be_valid expect(SpecUnionAttr.new(mixed: 42)).to be_valid expect(SpecUnionAttr.new(mixed: %w[a b c])).to be_valid - + expect do SpecUnionAttr.new(mixed: 3.14) end.to raise_error(ArgumentError, /is a Float.*not one of.*String.*Integer.*Array of String/) @@ -89,4 +90,4 @@ class SpecSingleUnion < ValidatedObject::Base end.to raise_error(ArgumentError, /is a Integer, not one of String/) end end -end \ No newline at end of file +end From da932e5978ca7044ecf68a59663558b2637eef9f Mon Sep 17 00:00:00 2001 From: Robb Shecter Date: Wed, 27 Aug 2025 21:30:20 -0600 Subject: [PATCH 6/6] bump minor version number --- lib/validated_object/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validated_object/version.rb b/lib/validated_object/version.rb index 788837c..809d346 100644 --- a/lib/validated_object/version.rb +++ b/lib/validated_object/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ValidatedObject - VERSION = '2.3.3' + VERSION = '2.3.4' end