diff --git a/lib/couchbase-orm/attributes/dynamic.rb b/lib/couchbase-orm/attributes/dynamic.rb index 35863bfa..5b8ab6e3 100644 --- a/lib/couchbase-orm/attributes/dynamic.rb +++ b/lib/couchbase-orm/attributes/dynamic.rb @@ -31,13 +31,42 @@ def _assign_attribute(name, value) if responds public_send(name.writer, value) else - type = value.class.to_s.underscore.to_sym - type = :hash if type == :"active_support/hash_with_indifferent_access" - type = ActiveModel::Type.lookup(type) + type = define_attribute_type(value) + type = if type == :array + item_type = define_attribute_type(value.first) + ActiveModel::Type.lookup(type, type: item_type) + else + ActiveModel::Type.lookup(type) + end @attributes[name] = ActiveModel::Attribute.from_database(name, value, type) end end + # Determines the attribute type based on the value provided. + # + # This method converts the class of the value to a symbol and handles special cases + # for `ActiveSupport::HashWithIndifferentAccess`, booleans, and nil values. + # + # @param [Object] value The value whose type needs to be determined. + # @return [Symbol] The determined type of the attribute. + # + # @example Determining types of various values + # define_attribute_type(123) # => :integer + # define_attribute_type("Hello") # => :string + # define_attribute_type(true) # => :boolean + # define_attribute_type(false) # => :boolean + # define_attribute_type(nil) # => :raw + # define_attribute_type(ActiveSupport::HashWithIndifferentAccess.new) # => :hash + def define_attribute_type(value) + type = value.class.to_s.underscore.to_sym + return :hash if type == :"active_support/hash_with_indifferent_access" + return :boolean if type == :true_class + return :boolean if type == :false_class + return :raw if type == :nil_class + + type + end + # Define a reader method for a dynamic attribute. # # @example Define a reader method. diff --git a/lib/couchbase-orm/types.rb b/lib/couchbase-orm/types.rb index 9909aae6..c0d01714 100644 --- a/lib/couchbase-orm/types.rb +++ b/lib/couchbase-orm/types.rb @@ -7,6 +7,7 @@ require 'couchbase-orm/types/nested' require 'couchbase-orm/types/encrypted' require 'couchbase-orm/types/hash' +require 'couchbase-orm/types/raw' if ActiveModel::VERSION::MAJOR <= 6 # In Rails 5, the type system cannot allow overriding the default types @@ -22,3 +23,4 @@ ActiveModel::Type.register(:nested, CouchbaseOrm::Types::Nested) ActiveModel::Type.register(:encrypted, CouchbaseOrm::Types::Encrypted) ActiveModel::Type.register(:hash, CouchbaseOrm::Types::Hash) +ActiveModel::Type.register(:raw, CouchbaseOrm::Types::Raw) diff --git a/lib/couchbase-orm/types/raw.rb b/lib/couchbase-orm/types/raw.rb new file mode 100644 index 00000000..fe4e15f0 --- /dev/null +++ b/lib/couchbase-orm/types/raw.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module CouchbaseOrm + module Types + class Raw < ActiveModel::Type::Value + def cast(values) + values + end + + def serialize(values) + values + end + end + end +end diff --git a/spec/attribute_dynamic_spec.rb b/spec/attribute_dynamic_spec.rb index e90c6333..01fef1fb 100644 --- a/spec/attribute_dynamic_spec.rb +++ b/spec/attribute_dynamic_spec.rb @@ -24,5 +24,79 @@ class AttributeDynamicTest < CouchbaseOrm::Base expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 2) dynamic.destroy end + + context 'with integer' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 2) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 2) + dynamic.destroy + end + end + + context 'with decimal' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 2.0) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 2.0) + dynamic.destroy + end + end + + context 'with true_class' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: true) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: true) + dynamic.destroy + end + end + + context 'with false_class' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: false) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: false) + dynamic.destroy + end + end + + context 'with string' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 'a string') + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 'a string') + dynamic.destroy + end + end + + context 'with hash' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: { a: 'hash' }) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: { a: 'hash' }) + dynamic.destroy + end + end + + context 'with nil' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: nil) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: nil) + dynamic.destroy + end + end + + context 'with array' do + it 'accepts unknown attribute from Coucbbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: [{ a: 'hash' }]) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: [{ a: 'hash' }]) + dynamic.destroy + end + end + + context 'with raw' do + it 'not accepts to change string to number' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 'an string') + dynamic.new_attribute = 1 + dynamic.save! + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: '1') + dynamic.destroy! + end + end end end