From 5965fc4a841f81d34507a1070f18fd82f7ad951b Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 22 Sep 2022 14:16:50 +0200 Subject: [PATCH 1/9] Array, Nested, Array of Nested, Nested of Nested --- lib/couchbase-orm/persistence.rb | 2 + lib/couchbase-orm/types.rb | 4 ++ lib/couchbase-orm/types/array.rb | 29 ++++++++++ lib/couchbase-orm/types/nested.rb | 29 ++++++++++ spec/type_array_spec.rb | 52 +++++++++++++++++ spec/type_nested_spec.rb | 93 +++++++++++++++++++++++++++++++ spec/type_spec.rb | 4 ++ 7 files changed, 213 insertions(+) create mode 100644 lib/couchbase-orm/types/array.rb create mode 100644 lib/couchbase-orm/types/nested.rb create mode 100644 spec/type_array_spec.rb create mode 100644 spec/type_nested_spec.rb diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 29f72552..87c85eb8 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -225,6 +225,8 @@ def touch(**options) def serialized_attributes attributes.map { |k, v| + v = id if k == "id" + CouchbaseOrm.logger.debug { "Data - Serialize #{k} #{v} with #{self.class.attribute_types[k]}" } [k, self.class.attribute_types[k].serialize(v)] }.to_h end diff --git a/lib/couchbase-orm/types.rb b/lib/couchbase-orm/types.rb index e6674c0d..95300033 100644 --- a/lib/couchbase-orm/types.rb +++ b/lib/couchbase-orm/types.rb @@ -1,6 +1,8 @@ require "couchbase-orm/types/date" require "couchbase-orm/types/date_time" require "couchbase-orm/types/timestamp" +require "couchbase-orm/types/array" +require "couchbase-orm/types/nested" if ActiveModel::VERSION::MAJOR < 6 # In Rails 5, the type system cannot allow overriding the default types @@ -12,3 +14,5 @@ ActiveModel::Type.register(:date, CouchbaseOrm::Types::Date) ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime) ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp) +ActiveModel::Type.register(:array, CouchbaseOrm::Types::Array) +ActiveModel::Type.register(:nested, CouchbaseOrm::Types::Nested) diff --git a/lib/couchbase-orm/types/array.rb b/lib/couchbase-orm/types/array.rb new file mode 100644 index 00000000..817fbf48 --- /dev/null +++ b/lib/couchbase-orm/types/array.rb @@ -0,0 +1,29 @@ +module CouchbaseOrm + module Types + class Array < ActiveModel::Type::Value + attr_reader :type_class + attr_reader :model_class + + def initialize(type: nil) + if type.is_a?(Class) && type < CouchbaseOrm::Base + @model_class = type + @type_class = CouchbaseOrm::Types::Nested.new(type: @model_class) + else + @type_class = ActiveModel::Type.registry.lookup(type) + end + super() + end + + def cast(values) + return [] if values.nil? + raise ArgumentError, "#{values.inspect} must be an array" unless values.is_a?(::Array) + values.map(&@type_class.method(:cast)) + end + + def serialize(values) + return [] if values.nil? + values.map(&@type_class.method(:serialize)) + end + end + end +end diff --git a/lib/couchbase-orm/types/nested.rb b/lib/couchbase-orm/types/nested.rb new file mode 100644 index 00000000..2b6dedbb --- /dev/null +++ b/lib/couchbase-orm/types/nested.rb @@ -0,0 +1,29 @@ +module CouchbaseOrm + module Types + class Nested < ActiveModel::Type::Value + attr_reader :model_class + + def initialize(type:) + raise ArgumentError, "type is nil" if type.nil? + raise ArgumentError, "type is not a class : #{type.inspect}" unless type.is_a?(Class) + @model_class = type + super() + end + + def cast(value) + return nil if value.nil? + return value if value.is_a?(@model_class) + return @model_class.new(value) if value.is_a?(Hash) + + raise ArgumentError, "Nested: #{value.inspect} is not supported for cast" + end + + def serialize(value) + return nil if value.nil? + return value.send(:serialized_attributes).except("id") if value.is_a?(@model_class) + + raise ArgumentError, "Nested: #{value.inspect} is not supported for serialization" + end + end + end +end diff --git a/spec/type_array_spec.rb b/spec/type_array_spec.rb new file mode 100644 index 00000000..f6bc8c6b --- /dev/null +++ b/spec/type_array_spec.rb @@ -0,0 +1,52 @@ +require File.expand_path("../support", __FILE__) + +require "active_model" + +class TypeArrayTest < CouchbaseOrm::Base + attribute :name + attribute :tags, :array, type: :string + attribute :milestones, :array, type: :date + attribute :flags, :array, type: :boolean + attribute :things +end + +describe CouchbaseOrm::Base do + it "should be able to store and retrieve an array of strings" do + obj = TypeArrayTest.new + obj.tags = ["foo", "bar"] + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.tags).to eq ["foo", "bar"] + end + + it "should be able to store and retrieve an array of date" do + dates = [Date.today, Date.today + 1] + obj = TypeArrayTest.new + obj.milestones = dates + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.milestones).to eq dates + end + + it "should be able to store and retrieve an array of boolean" do + flags = [true, false] + obj = TypeArrayTest.new + obj.flags = flags + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.flags).to eq flags + end + + it "should be able to store and retrieve an array of basic objects" do + things = [1, "1234", {"key" => 4}] + obj = TypeArrayTest.new + obj.things = things + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.things).to eq things + end +end diff --git a/spec/type_nested_spec.rb b/spec/type_nested_spec.rb new file mode 100644 index 00000000..560f70c0 --- /dev/null +++ b/spec/type_nested_spec.rb @@ -0,0 +1,93 @@ +require File.expand_path("../support", __FILE__) + +require "active_model" + +class SubTypeTest < CouchbaseOrm::Base + attribute :name + attribute :tags, :array, type: :string + attribute :milestones, :array, type: :date + attribute :flags, :array, type: :boolean + attribute :things + attribute :child, :nested, type: SubTypeTest +end + +class TypeNestedTest < CouchbaseOrm::Base + attribute :main, :nested, type: SubTypeTest + attribute :others, :array, type: SubTypeTest +end + +describe CouchbaseOrm::Types::Nested do + it "should be able to store and retrieve a nested object" do + obj = TypeNestedTest.new + obj.main = SubTypeTest.new + obj.main.name = "foo" + obj.main.tags = ["foo", "bar"] + obj.main.child = SubTypeTest.new(name: "bar") + obj.save! + + obj = TypeNestedTest.find(obj.id) + expect(obj.main.name).to eq "foo" + expect(obj.main.tags).to eq ["foo", "bar"] + expect(obj.main.child.name).to eq "bar" + end + + it "should be able to store and retrieve an array of nested objects" do + obj = TypeNestedTest.new + obj.others = [SubTypeTest.new, SubTypeTest.new] + obj.others[0].name = "foo" + obj.others[0].tags = ["foo", "bar"] + obj.others[1].name = "bar" + obj.others[1].tags = ["bar", "baz"] + obj.others[1].child = SubTypeTest.new(name: "baz") + obj.save! + + obj = TypeNestedTest.find(obj.id) + expect(obj.others[0].name).to eq "foo" + expect(obj.others[0].tags).to eq ["foo", "bar"] + expect(obj.others[1].name).to eq "bar" + expect(obj.others[1].tags).to eq ["bar", "baz"] + expect(obj.others[1].child.name).to eq "baz" + end + + it "should serialize to JSON" do + obj = TypeNestedTest.new + obj.others = [SubTypeTest.new, SubTypeTest.new] + obj.others[0].name = "foo" + obj.others[0].tags = ["foo", "bar"] + obj.others[1].name = "bar" + obj.others[1].tags = ["bar", "baz"] + obj.others[1].child = SubTypeTest.new(name: "baz") + obj.save! + + obj = TypeNestedTest.find(obj.id) + expect(obj.send(:serialized_attributes)).to eq ({ + "id" => obj.id, + "main" => nil, + "others" => [ + { + "name" => "foo", + "tags" => ["foo", "bar"], + "milestones" => [], + "flags" => [], + "things" => nil, + "child" => nil + }, + { + "name" => "bar", + "tags" => ["bar", "baz"], + "milestones" => [], + "flags" => [], + "things" => nil, + "child" => { + "name" => "baz", + "tags" => [], + "milestones" => [], + "flags" => [], + "things" => nil, + "child" => nil + } + } + ] + }) + end +end diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 935095ee..87c42d38 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -84,6 +84,10 @@ class N1qlTypeTest < CouchbaseOrm::Base N1qlTypeTest.delete_all end + it "should be typed" do + expect(N1qlTypeTest.attribute_types["name"]).to be_a(ActiveModel::Type::String) + end + it "should be createable" do t = TypeTest.create! expect(t).to be_a(TypeTest) From f8379531aa3ccfe4cd4df7de0892bd5c80e409fa Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 22 Sep 2022 14:41:17 +0200 Subject: [PATCH 2/9] Remove debug --- lib/couchbase-orm/persistence.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 87c85eb8..d299f869 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -226,7 +226,6 @@ def touch(**options) def serialized_attributes attributes.map { |k, v| v = id if k == "id" - CouchbaseOrm.logger.debug { "Data - Serialize #{k} #{v} with #{self.class.attribute_types[k]}" } [k, self.class.attribute_types[k].serialize(v)] }.to_h end From 214b2c959968be2e14f9f3ba8b74af82afe20dae Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 22 Sep 2022 14:46:15 +0200 Subject: [PATCH 3/9] 100% coverage --- spec/type_nested_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/type_nested_spec.rb b/spec/type_nested_spec.rb index 560f70c0..e6c88cea 100644 --- a/spec/type_nested_spec.rb +++ b/spec/type_nested_spec.rb @@ -90,4 +90,10 @@ class TypeNestedTest < CouchbaseOrm::Base ] }) end + it "should not cast a list" do + expect{CouchbaseOrm::Types::Nested.new(type: SubTypeTest).cast([1,2,3])}.to raise_error(ArgumentError) + end + it "should not serialize a list" do + expect{CouchbaseOrm::Types::Nested.new(type: SubTypeTest).serialize([1,2,3])}.to raise_error(ArgumentError) + end end From 083ea48fefd5f1683a20fb865eb6d6e6efa742d3 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 22 Sep 2022 16:29:35 +0200 Subject: [PATCH 4/9] Refactor to add NestedDocument class which doesn't have persistence, query, bucket things --- README.md | 36 +++---- lib/couchbase-orm.rb | 6 +- lib/couchbase-orm/base.rb | 120 +++++++++++++----------- lib/couchbase-orm/n1ql.rb | 2 +- lib/couchbase-orm/railtie.rb | 2 +- lib/couchbase-orm/types/array.rb | 1 + lib/couchbase-orm/types/nested.rb | 3 +- lib/couchbase-orm/utilities/has_many.rb | 2 +- lib/couchbase-orm/views.rb | 2 +- spec/associations_spec.rb | 10 +- spec/base_spec.rb | 8 +- spec/enum_spec.rb | 4 +- spec/has_many_spec.rb | 12 +-- spec/id_generator_spec.rb | 2 +- spec/index_spec.rb | 6 +- spec/n1ql_spec.rb | 2 +- spec/persistence_spec.rb | 8 +- spec/relation_spec.rb | 2 +- spec/type_array_spec.rb | 4 +- spec/type_nested_spec.rb | 10 +- spec/type_spec.rb | 6 +- spec/views_spec.rb | 2 +- 22 files changed, 138 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index cc3a050d..580c882e 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,10 @@ This works fine in production however by default in development models are lazy ```ruby require 'couchbase-orm' - class Post < CouchbaseOrm::Base - attribute :title, type: String - attribute :body, type: String - attribute :draft, type: Boolean + class Post < CouchbaseOrm::Document + attribute :title, :string + attribute :body, :string + attribute :draft, :boolean end p = Post.new(id: 'hello-world', @@ -86,10 +86,10 @@ You can also let the library generate the unique identifier for you: You can define connection options on per model basis: ```ruby - class Post < CouchbaseOrm::Base - attribute :title, type: String - attribute :body, type: String - attribute :draft, type: Boolean + class Post < CouchbaseOrm::Document + attribute :title, :string + attribute :body, :string + attribute :draft, :boolean connect bucket: 'blog', password: ENV['BLOG_BUCKET_PASSWORD'] end @@ -115,8 +115,9 @@ Views are defined in the model and typically just emit an attribute that can then be used for filtering results or ordering. ```ruby - class Comment < CouchbaseOrm::Base - attribute :author, :body, type: String + class Comment < CouchbaseOrm::Document + attribute :author :string + attribute :body, :string view :all # => emits :id and will return all comments view :by_author, emit_key: :author @@ -158,8 +159,9 @@ Ex : Compound keys allows to decide the order of the results, and you can revers Like views, it's possible to use N1QL to process some requests used for filtering results or ordering. ```ruby - class Comment < CouchbaseOrm::Base - attribute :author, :body, type: String + class Comment < CouchbaseOrm::Document + attribute :author, :string + attribute :body, :string n1ql :by_author, emit_key: :author # Generates two functions: @@ -188,15 +190,15 @@ Comment.bucket.n1ql.select('RAW meta(ui).id').from('bucket').where('author="my_v There are common active record helpers available for use `belongs_to` and `has_many` ```ruby - class Comment < CouchbaseOrm::Base + class Comment < CouchbaseOrm::Document belongs_to :author end - class Author < CouchbaseOrm::Base + class Author < CouchbaseOrm::Document has_many :comments, dependent: :destroy # You can ensure an attribute is unique for this model - attribute :email, type: String + attribute :email, :string ensure_unique :email end ``` @@ -204,11 +206,11 @@ There are common active record helpers available for use `belongs_to` and `has_m By default, `has_many` uses a view for association, but you can define a `type` option to specify an association using N1QL instead: ```ruby - class Comment < CouchbaseOrm::Base + class Comment < CouchbaseOrm::Document belongs_to :author end - class Author < CouchbaseOrm::Base + class Author < CouchbaseOrm::Document has_many :comments, type: :n1ql, dependent: :destroy end ``` diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index c91f87ec..d27962ff 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -11,6 +11,8 @@ module CouchbaseOrm autoload :Connection, 'couchbase-orm/connection' autoload :IdGenerator, 'couchbase-orm/id_generator' autoload :Base, 'couchbase-orm/base' + autoload :Document, 'couchbase-orm/base' + autoload :NestedDocument, 'couchbase-orm/base' autoload :HasMany, 'couchbase-orm/utilities/has_many' def self.logger @@ -30,7 +32,7 @@ def self.try_load(id) query_id = id end - result = query_id.is_a?(Array) ? CouchbaseOrm::Base.bucket.default_collection.get_multi(query_id) : CouchbaseOrm::Base.bucket.default_collection.get(query_id) + result = query_id.is_a?(Array) ? CouchbaseOrm::Document.bucket.default_collection.get_multi(query_id) : CouchbaseOrm::Document.bucket.default_collection.get(query_id) result = Array.wrap(result) if was_array @@ -46,7 +48,7 @@ def self.try_load(id) def self.try_load_create_model(result, id) ddoc = result&.content["type"] return nil unless ddoc - ::CouchbaseOrm::Base.descendants.each do |model| + ::CouchbaseOrm::Document.descendants.each do |model| if model.design_document == ddoc return model.new(result, id: id) end diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 91e6ed29..9e58667d 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -109,9 +109,76 @@ class Base include ::ActiveRecord::Core include ActiveRecordCompat + extend Enum + define_model_callbacks :initialize, :only => :after define_model_callbacks :create, :destroy, :save, :update + Metadata = Struct.new(:cas) + + class MismatchTypeError < RuntimeError; end + + # Add support for libcouchbase response objects + def initialize(model = nil, ignore_doc_type: false, **attributes) + CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" } + @__metadata__ = Metadata.new + + super() + + if model + case model + when Couchbase::Collection::GetResult + doc = HashWithIndifferentAccess.new(model.content) || raise('empty response provided') + type = doc.delete(:type) + doc.delete(:id) + + if type && !ignore_doc_type && type.to_s != self.class.design_document + raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self) + end + + self.id = attributes[:id] if attributes[:id].present? + @__metadata__.cas = model.cas + + assign_attributes(doc) + when CouchbaseOrm::Base + clear_changes_information + super(model.attributes.except(:id, 'type')) + else + clear_changes_information + assign_attributes(**attributes.merge(Hash(model)).symbolize_keys) + end + else + clear_changes_information + super(attributes) + end + yield self if block_given? + + run_callbacks :initialize + end + + def [](key) + send(key) + end + + def []=(key, value) + send(:"#{key}=", value) + end + + protected + + def serialized_attributes + attributes.map { |k, v| + v = id if k == "id" # Fixme: review comment about that + [k, self.class.attribute_types[k].serialize(v)] + }.to_h + end + end + + class NestedDocument < Base + + end + + class Document < Base include Persistence include ::ActiveRecord::AttributeMethods::Dirty include ::ActiveRecord::Timestamp # must be included after Persistence @@ -127,10 +194,6 @@ class Base extend HasMany extend Index - - Metadata = Struct.new(:key, :cas) - - class << self def connect(**options) @bucket = BucketProxy.new(::MTLibcouchbase::Bucket.new(**options)) @@ -190,47 +253,6 @@ def exists?(id) alias_method :has_key?, :exists? end - class MismatchTypeError < RuntimeError; end - - # Add support for libcouchbase response objects - def initialize(model = nil, ignore_doc_type: false, **attributes) - CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" } - @__metadata__ = Metadata.new - - super() - - if model - case model - when Couchbase::Collection::GetResult - doc = HashWithIndifferentAccess.new(model.content) || raise('empty response provided') - type = doc.delete(:type) - doc.delete(:id) - - if type && !ignore_doc_type && type.to_s != self.class.design_document - raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self) - end - - self.id = attributes[:id] if attributes[:id].present? - @__metadata__.cas = model.cas - - assign_attributes(doc) - when CouchbaseOrm::Base - clear_changes_information - super(model.attributes.except(:id, 'type')) - else - clear_changes_information - assign_attributes(**attributes.merge(Hash(model)).symbolize_keys) - end - else - clear_changes_information - super(attributes) - end - yield self if block_given? - - run_callbacks :initialize - end - - # Document ID is a special case as it is not stored in the document def id @id @@ -242,14 +264,6 @@ def id=(value) @id = value.to_s.presence end - def [](key) - send(key) - end - - def []=(key, value) - send(:"#{key}=", value) - end - # Public: Allows for access to ActiveModel functionality. # # Returns self. diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 29a5640b..7b9927a3 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -26,7 +26,7 @@ module ClassMethods # @param [Hash] options options passed to the {Couchbase::N1QL} # # @example Define some N1QL queries for a model - # class Post < CouchbaseOrm::Base + # class Post < CouchbaseOrm::Document # n1ql :by_rating, emit_key: :rating # end # diff --git a/lib/couchbase-orm/railtie.rb b/lib/couchbase-orm/railtie.rb index 4d6d5706..07310506 100644 --- a/lib/couchbase-orm/railtie.rb +++ b/lib/couchbase-orm/railtie.rb @@ -74,7 +74,7 @@ def self.rescue_responses config.after_initialize do |app| if config.couchbase_orm.ensure_design_documents begin - ::CouchbaseOrm::Base.descendants.each do |model| + ::CouchbaseOrm::Document.descendants.each do |model| model.ensure_design_document! end rescue ::MTLibcouchbase::Error::Timedout, ::MTLibcouchbase::Error::ConnectError, ::MTLibcouchbase::Error::NetworkError diff --git a/lib/couchbase-orm/types/array.rb b/lib/couchbase-orm/types/array.rb index 817fbf48..780f493e 100644 --- a/lib/couchbase-orm/types/array.rb +++ b/lib/couchbase-orm/types/array.rb @@ -17,6 +17,7 @@ def initialize(type: nil) def cast(values) return [] if values.nil? raise ArgumentError, "#{values.inspect} must be an array" unless values.is_a?(::Array) + values.map(&@type_class.method(:cast)) end diff --git a/lib/couchbase-orm/types/nested.rb b/lib/couchbase-orm/types/nested.rb index 2b6dedbb..7d7238bf 100644 --- a/lib/couchbase-orm/types/nested.rb +++ b/lib/couchbase-orm/types/nested.rb @@ -6,6 +6,7 @@ class Nested < ActiveModel::Type::Value def initialize(type:) raise ArgumentError, "type is nil" if type.nil? raise ArgumentError, "type is not a class : #{type.inspect}" unless type.is_a?(Class) + @model_class = type super() end @@ -21,7 +22,7 @@ def cast(value) def serialize(value) return nil if value.nil? return value.send(:serialized_attributes).except("id") if value.is_a?(@model_class) - + raise ArgumentError, "Nested: #{value.inspect} is not supported for serialization" end end diff --git a/lib/couchbase-orm/utilities/has_many.rb b/lib/couchbase-orm/utilities/has_many.rb index 164b75f0..beb25232 100644 --- a/lib/couchbase-orm/utilities/has_many.rb +++ b/lib/couchbase-orm/utilities/has_many.rb @@ -23,7 +23,7 @@ def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_cla # Open the class early - load order will have to be changed to prevent this. # Warning notice required as a misspelling will not raise an error Object.class_eval <<-EKLASS - class #{class_name} < CouchbaseOrm::Base + class #{class_name} < CouchbaseOrm::Document attribute :#{foreign_key} end EKLASS diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index b2e55493..97deb582 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -14,7 +14,7 @@ module ClassMethods # @param [Hash] options options passed to the {Couchbase::View} # # @example Define some views for a model - # class Post < CouchbaseOrm::Base + # class Post < CouchbaseOrm::Document # view :all # view :by_rating, emit_key: :rating # end diff --git a/spec/associations_spec.rb b/spec/associations_spec.rb index ff45ed1d..000a91b5 100644 --- a/spec/associations_spec.rb +++ b/spec/associations_spec.rb @@ -3,27 +3,27 @@ require File.expand_path("../support", __FILE__) -class Parent < CouchbaseOrm::Base +class Parent < CouchbaseOrm::Document attribute :name end -class RandomOtherType < CouchbaseOrm::Base +class RandomOtherType < CouchbaseOrm::Document attribute :name end -class Child < CouchbaseOrm::Base +class Child < CouchbaseOrm::Document attribute :name belongs_to :parent, dependent: :destroy end -class Assembly < CouchbaseOrm::Base +class Assembly < CouchbaseOrm::Document attribute :name has_and_belongs_to_many :parts, autosave: true end -class Part < CouchbaseOrm::Base +class Part < CouchbaseOrm::Document attribute :name has_and_belongs_to_many :assemblies, dependent: :destroy, autosave: true diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 3745b0e6..6b150b62 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -2,20 +2,20 @@ require File.expand_path("../support", __FILE__) -class BaseTest < CouchbaseOrm::Base +class BaseTest < CouchbaseOrm::Document attribute :name, :string attribute :job, :string end -class CompareTest < CouchbaseOrm::Base +class CompareTest < CouchbaseOrm::Document attribute :age, :integer end -class TimestampTest < CouchbaseOrm::Base +class TimestampTest < CouchbaseOrm::Document attribute :created_at, :datetime end -describe CouchbaseOrm::Base do +describe CouchbaseOrm::Document do it "should be comparable to other objects" do base = BaseTest.create!(name: 'joe') base2 = BaseTest.create!(name: 'joe') diff --git a/spec/enum_spec.rb b/spec/enum_spec.rb index 76036183..ccf2bfd3 100644 --- a/spec/enum_spec.rb +++ b/spec/enum_spec.rb @@ -2,12 +2,12 @@ require File.expand_path("../support", __FILE__) -class EnumTest < CouchbaseOrm::Base +class EnumTest < CouchbaseOrm::Document enum rating: [:awesome, :good, :okay, :bad], default: :okay enum color: [:red, :green, :blue] end -describe CouchbaseOrm::Base do +describe CouchbaseOrm::Document do it "should create an attribute" do base = EnumTest.create!(rating: :good, color: :red) expect(base.attribute_names).to eq(["id", "rating", "color"]) diff --git a/spec/has_many_spec.rb b/spec/has_many_spec.rb index 68dbc417..f5e73cfc 100644 --- a/spec/has_many_spec.rb +++ b/spec/has_many_spec.rb @@ -77,12 +77,12 @@ describe CouchbaseOrm::HasMany do context 'with view' do - class ObjectRatingViewTest < CouchbaseOrm::Base + class ObjectRatingViewTest < CouchbaseOrm::Document join :object_view_test, :rating_view_test view :view_all end - class RatingViewTest < CouchbaseOrm::Base + class RatingViewTest < CouchbaseOrm::Document enum rating: [:awesome, :good, :okay, :bad], default: :okay belongs_to :object_view_test @@ -90,7 +90,7 @@ class RatingViewTest < CouchbaseOrm::Base view :view_all end - class ObjectViewTest < CouchbaseOrm::Base + class ObjectViewTest < CouchbaseOrm::Document attribute :name, type: String has_many :rating_view_tests, dependent: :destroy @@ -101,13 +101,13 @@ class ObjectViewTest < CouchbaseOrm::Base end context 'with n1ql' do - class ObjectRatingN1qlTest < CouchbaseOrm::Base + class ObjectRatingN1qlTest < CouchbaseOrm::Document join :object_n1ql_test, :rating_n1ql_test n1ql :n1ql_all end - class RatingN1qlTest < CouchbaseOrm::Base + class RatingN1qlTest < CouchbaseOrm::Document enum rating: [:awesome, :good, :okay, :bad], default: :okay belongs_to :object_n1ql_test @@ -116,7 +116,7 @@ class RatingN1qlTest < CouchbaseOrm::Base n1ql :n1ql_all end - class ObjectN1qlTest < CouchbaseOrm::Base + class ObjectN1qlTest < CouchbaseOrm::Document attribute :name, type: String has_many :rating_n1ql_tests, dependent: :destroy, type: :n1ql diff --git a/spec/id_generator_spec.rb b/spec/id_generator_spec.rb index f1594d6e..3d6a8051 100644 --- a/spec/id_generator_spec.rb +++ b/spec/id_generator_spec.rb @@ -3,7 +3,7 @@ require 'couchbase-orm' require 'thread' -class IdTestModel < CouchbaseOrm::Base; end +class IdTestModel < CouchbaseOrm::Document; end describe CouchbaseOrm::IdGenerator do it "should not generate ID clashes" do diff --git a/spec/index_spec.rb b/spec/index_spec.rb index c9d0a26a..6602cd5a 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -3,20 +3,20 @@ require File.expand_path("../support", __FILE__) -class IndexTest < CouchbaseOrm::Base +class IndexTest < CouchbaseOrm::Document attribute :email, type: String attribute :name, type: String, default: :joe ensure_unique :email, presence: false end -class NoUniqueIndexTest < CouchbaseOrm::Base +class NoUniqueIndexTest < CouchbaseOrm::Document attribute :email, type: String attribute :name, type: String, default: :joe index :email, presence: false end -class IndexEnumTest < CouchbaseOrm::Base +class IndexEnumTest < CouchbaseOrm::Document enum visibility: [:group, :authority, :public], default: :authority enum color: [:red, :green, :blue] end diff --git a/spec/n1ql_spec.rb b/spec/n1ql_spec.rb index f5ba9971..3df8b959 100644 --- a/spec/n1ql_spec.rb +++ b/spec/n1ql_spec.rb @@ -2,7 +2,7 @@ require File.expand_path("../support", __FILE__) -class N1QLTest < CouchbaseOrm::Base +class N1QLTest < CouchbaseOrm::Document attribute :name, type: String attribute :lastname, type: String enum rating: [:awesome, :good, :okay, :bad], default: :okay diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index 6c83212a..37406e04 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -3,19 +3,19 @@ require File.expand_path("../support", __FILE__) -class BasicModel < CouchbaseOrm::Base +class BasicModel < CouchbaseOrm::Document attribute :name attribute :address attribute :age end -class ModelWithDefaults < CouchbaseOrm::Base +class ModelWithDefaults < CouchbaseOrm::Document attribute :name, default: proc { 'bob' } attribute :address attribute :age, default: 23 end -class ModelWithCallbacks < CouchbaseOrm::Base +class ModelWithCallbacks < CouchbaseOrm::Document attribute :name attribute :address attribute :age @@ -35,7 +35,7 @@ def set_address; self.address = '23'; end def set_age; self.age = 30; end end -class ModelWithValidations < CouchbaseOrm::Base +class ModelWithValidations < CouchbaseOrm::Document attribute :name, type: String attribute :address, type: String attribute :age, type: :Integer diff --git a/spec/relation_spec.rb b/spec/relation_spec.rb index 8efbe5a9..fba199a7 100644 --- a/spec/relation_spec.rb +++ b/spec/relation_spec.rb @@ -3,7 +3,7 @@ require File.expand_path("../support", __FILE__) -class RelationModel < CouchbaseOrm::Base +class RelationModel < CouchbaseOrm::Document attribute :name, :string attribute :last_name, :string attribute :active, :boolean diff --git a/spec/type_array_spec.rb b/spec/type_array_spec.rb index f6bc8c6b..35f2ba45 100644 --- a/spec/type_array_spec.rb +++ b/spec/type_array_spec.rb @@ -2,7 +2,7 @@ require "active_model" -class TypeArrayTest < CouchbaseOrm::Base +class TypeArrayTest < CouchbaseOrm::Document attribute :name attribute :tags, :array, type: :string attribute :milestones, :array, type: :date @@ -10,7 +10,7 @@ class TypeArrayTest < CouchbaseOrm::Base attribute :things end -describe CouchbaseOrm::Base do +describe CouchbaseOrm::Document do it "should be able to store and retrieve an array of strings" do obj = TypeArrayTest.new obj.tags = ["foo", "bar"] diff --git a/spec/type_nested_spec.rb b/spec/type_nested_spec.rb index e6c88cea..289d1ded 100644 --- a/spec/type_nested_spec.rb +++ b/spec/type_nested_spec.rb @@ -2,7 +2,7 @@ require "active_model" -class SubTypeTest < CouchbaseOrm::Base +class SubTypeTest < CouchbaseOrm::NestedDocument attribute :name attribute :tags, :array, type: :string attribute :milestones, :array, type: :date @@ -11,7 +11,7 @@ class SubTypeTest < CouchbaseOrm::Base attribute :child, :nested, type: SubTypeTest end -class TypeNestedTest < CouchbaseOrm::Base +class TypeNestedTest < CouchbaseOrm::Document attribute :main, :nested, type: SubTypeTest attribute :others, :array, type: SubTypeTest end @@ -90,9 +90,15 @@ class TypeNestedTest < CouchbaseOrm::Base ] }) end + + it "should not have a save method" do + expect(SubTypeTest.new).to_not respond_to(:save) + end + it "should not cast a list" do expect{CouchbaseOrm::Types::Nested.new(type: SubTypeTest).cast([1,2,3])}.to raise_error(ArgumentError) end + it "should not serialize a list" do expect{CouchbaseOrm::Types::Nested.new(type: SubTypeTest).serialize([1,2,3])}.to raise_error(ArgumentError) end diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 87c42d38..622e71ca 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -11,7 +11,7 @@ def serialize(value) ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal) -class TypeTest < CouchbaseOrm::Base +class TypeTest < CouchbaseOrm::Document attribute :name, :string attribute :age, :integer attribute :size, :float @@ -27,7 +27,7 @@ class TypeTest < CouchbaseOrm::Base index :precision_time, presence: false end -class N1qlTypeTest < CouchbaseOrm::Base +class N1qlTypeTest < CouchbaseOrm::Document attribute :name, :string attribute :age, :integer attribute :size, :float @@ -78,7 +78,7 @@ class N1qlTypeTest < CouchbaseOrm::Base end end -describe CouchbaseOrm::Base do +describe CouchbaseOrm::Document do before(:each) do TypeTest.delete_all N1qlTypeTest.delete_all diff --git a/spec/views_spec.rb b/spec/views_spec.rb index 3ae3f865..c9d15738 100644 --- a/spec/views_spec.rb +++ b/spec/views_spec.rb @@ -3,7 +3,7 @@ require File.expand_path("../support", __FILE__) require 'set' -class ViewTest < CouchbaseOrm::Base +class ViewTest < CouchbaseOrm::Document attribute :name, type: String enum rating: [:awesome, :good, :okay, :bad], default: :okay From f4f054561344f01e69285a1088f573fe4f333460 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 28 Sep 2022 16:41:04 +0200 Subject: [PATCH 5/9] Revert rename swap --- README.md | 16 ++++---- lib/couchbase-orm.rb | 2 +- lib/couchbase-orm/base.rb | 10 +++-- lib/couchbase-orm/n1ql.rb | 2 +- lib/couchbase-orm/railtie.rb | 2 +- lib/couchbase-orm/types/array.rb | 2 +- lib/couchbase-orm/types/nested.rb | 12 ++++++ lib/couchbase-orm/utilities/has_many.rb | 2 +- lib/couchbase-orm/views.rb | 2 +- spec/associations_spec.rb | 10 ++--- spec/base_spec.rb | 8 ++-- spec/enum_spec.rb | 4 +- spec/has_many_spec.rb | 12 +++--- spec/id_generator_spec.rb | 2 +- spec/index_spec.rb | 6 +-- spec/n1ql_spec.rb | 2 +- spec/persistence_spec.rb | 8 ++-- spec/relation_spec.rb | 2 +- spec/type_array_spec.rb | 4 +- spec/type_nested_spec.rb | 53 ++++++++++++++++++++++++- spec/type_spec.rb | 6 +-- spec/views_spec.rb | 2 +- 22 files changed, 116 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 580c882e..030003a8 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This works fine in production however by default in development models are lazy ```ruby require 'couchbase-orm' - class Post < CouchbaseOrm::Document + class Post < CouchbaseOrm::Base attribute :title, :string attribute :body, :string attribute :draft, :boolean @@ -86,7 +86,7 @@ You can also let the library generate the unique identifier for you: You can define connection options on per model basis: ```ruby - class Post < CouchbaseOrm::Document + class Post < CouchbaseOrm::Base attribute :title, :string attribute :body, :string attribute :draft, :boolean @@ -115,7 +115,7 @@ Views are defined in the model and typically just emit an attribute that can then be used for filtering results or ordering. ```ruby - class Comment < CouchbaseOrm::Document + class Comment < CouchbaseOrm::Base attribute :author :string attribute :body, :string view :all # => emits :id and will return all comments @@ -159,7 +159,7 @@ Ex : Compound keys allows to decide the order of the results, and you can revers Like views, it's possible to use N1QL to process some requests used for filtering results or ordering. ```ruby - class Comment < CouchbaseOrm::Document + class Comment < CouchbaseOrm::Base attribute :author, :string attribute :body, :string n1ql :by_author, emit_key: :author @@ -190,11 +190,11 @@ Comment.bucket.n1ql.select('RAW meta(ui).id').from('bucket').where('author="my_v There are common active record helpers available for use `belongs_to` and `has_many` ```ruby - class Comment < CouchbaseOrm::Document + class Comment < CouchbaseOrm::Base belongs_to :author end - class Author < CouchbaseOrm::Document + class Author < CouchbaseOrm::Base has_many :comments, dependent: :destroy # You can ensure an attribute is unique for this model @@ -206,11 +206,11 @@ There are common active record helpers available for use `belongs_to` and `has_m By default, `has_many` uses a view for association, but you can define a `type` option to specify an association using N1QL instead: ```ruby - class Comment < CouchbaseOrm::Document + class Comment < CouchbaseOrm::Base belongs_to :author end - class Author < CouchbaseOrm::Document + class Author < CouchbaseOrm::Base has_many :comments, type: :n1ql, dependent: :destroy end ``` diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index d27962ff..d9c5aa31 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -32,7 +32,7 @@ def self.try_load(id) query_id = id end - result = query_id.is_a?(Array) ? CouchbaseOrm::Document.bucket.default_collection.get_multi(query_id) : CouchbaseOrm::Document.bucket.default_collection.get(query_id) + result = query_id.is_a?(Array) ? CouchbaseOrm::Base.bucket.default_collection.get_multi(query_id) : CouchbaseOrm::Base.bucket.default_collection.get(query_id) result = Array.wrap(result) if was_array diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 9e58667d..4f55d444 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -97,7 +97,7 @@ def _write_attribute(attr_name, value) end end - class Base + class Document include ::ActiveModel::Model include ::ActiveModel::Dirty include ::ActiveModel::Attributes @@ -140,7 +140,7 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) @__metadata__.cas = model.cas assign_attributes(doc) - when CouchbaseOrm::Base + when CouchbaseOrm::Document clear_changes_information super(model.attributes.except(:id, 'type')) else @@ -174,14 +174,16 @@ def serialized_attributes end end - class NestedDocument < Base + class NestedDocument < Document end - class Document < Base + class Base < Document + include ::ActiveRecord::Validations include Persistence include ::ActiveRecord::AttributeMethods::Dirty include ::ActiveRecord::Timestamp # must be included after Persistence + include Associations include Views include QueryHelper diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 7b9927a3..29a5640b 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -26,7 +26,7 @@ module ClassMethods # @param [Hash] options options passed to the {Couchbase::N1QL} # # @example Define some N1QL queries for a model - # class Post < CouchbaseOrm::Document + # class Post < CouchbaseOrm::Base # n1ql :by_rating, emit_key: :rating # end # diff --git a/lib/couchbase-orm/railtie.rb b/lib/couchbase-orm/railtie.rb index 07310506..4d6d5706 100644 --- a/lib/couchbase-orm/railtie.rb +++ b/lib/couchbase-orm/railtie.rb @@ -74,7 +74,7 @@ def self.rescue_responses config.after_initialize do |app| if config.couchbase_orm.ensure_design_documents begin - ::CouchbaseOrm::Document.descendants.each do |model| + ::CouchbaseOrm::Base.descendants.each do |model| model.ensure_design_document! end rescue ::MTLibcouchbase::Error::Timedout, ::MTLibcouchbase::Error::ConnectError, ::MTLibcouchbase::Error::NetworkError diff --git a/lib/couchbase-orm/types/array.rb b/lib/couchbase-orm/types/array.rb index 780f493e..9d7137f0 100644 --- a/lib/couchbase-orm/types/array.rb +++ b/lib/couchbase-orm/types/array.rb @@ -5,7 +5,7 @@ class Array < ActiveModel::Type::Value attr_reader :model_class def initialize(type: nil) - if type.is_a?(Class) && type < CouchbaseOrm::Base + if type.is_a?(Class) && type < CouchbaseOrm::NestedDocument @model_class = type @type_class = CouchbaseOrm::Types::Nested.new(type: @model_class) else diff --git a/lib/couchbase-orm/types/nested.rb b/lib/couchbase-orm/types/nested.rb index 7d7238bf..c0f113d0 100644 --- a/lib/couchbase-orm/types/nested.rb +++ b/lib/couchbase-orm/types/nested.rb @@ -1,3 +1,15 @@ +class NestedValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if value.is_a?(Array) + record.errors.add attribute, (options[:message] || "is invalid") unless value.map(&:valid?).all? + else + record.errors.add attribute, (options[:message] || "is invalid") unless + value.nil? || value.valid? + end + + end +end + module CouchbaseOrm module Types class Nested < ActiveModel::Type::Value diff --git a/lib/couchbase-orm/utilities/has_many.rb b/lib/couchbase-orm/utilities/has_many.rb index beb25232..164b75f0 100644 --- a/lib/couchbase-orm/utilities/has_many.rb +++ b/lib/couchbase-orm/utilities/has_many.rb @@ -23,7 +23,7 @@ def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_cla # Open the class early - load order will have to be changed to prevent this. # Warning notice required as a misspelling will not raise an error Object.class_eval <<-EKLASS - class #{class_name} < CouchbaseOrm::Document + class #{class_name} < CouchbaseOrm::Base attribute :#{foreign_key} end EKLASS diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index 97deb582..b2e55493 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -14,7 +14,7 @@ module ClassMethods # @param [Hash] options options passed to the {Couchbase::View} # # @example Define some views for a model - # class Post < CouchbaseOrm::Document + # class Post < CouchbaseOrm::Base # view :all # view :by_rating, emit_key: :rating # end diff --git a/spec/associations_spec.rb b/spec/associations_spec.rb index 000a91b5..ff45ed1d 100644 --- a/spec/associations_spec.rb +++ b/spec/associations_spec.rb @@ -3,27 +3,27 @@ require File.expand_path("../support", __FILE__) -class Parent < CouchbaseOrm::Document +class Parent < CouchbaseOrm::Base attribute :name end -class RandomOtherType < CouchbaseOrm::Document +class RandomOtherType < CouchbaseOrm::Base attribute :name end -class Child < CouchbaseOrm::Document +class Child < CouchbaseOrm::Base attribute :name belongs_to :parent, dependent: :destroy end -class Assembly < CouchbaseOrm::Document +class Assembly < CouchbaseOrm::Base attribute :name has_and_belongs_to_many :parts, autosave: true end -class Part < CouchbaseOrm::Document +class Part < CouchbaseOrm::Base attribute :name has_and_belongs_to_many :assemblies, dependent: :destroy, autosave: true diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 6b150b62..3745b0e6 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -2,20 +2,20 @@ require File.expand_path("../support", __FILE__) -class BaseTest < CouchbaseOrm::Document +class BaseTest < CouchbaseOrm::Base attribute :name, :string attribute :job, :string end -class CompareTest < CouchbaseOrm::Document +class CompareTest < CouchbaseOrm::Base attribute :age, :integer end -class TimestampTest < CouchbaseOrm::Document +class TimestampTest < CouchbaseOrm::Base attribute :created_at, :datetime end -describe CouchbaseOrm::Document do +describe CouchbaseOrm::Base do it "should be comparable to other objects" do base = BaseTest.create!(name: 'joe') base2 = BaseTest.create!(name: 'joe') diff --git a/spec/enum_spec.rb b/spec/enum_spec.rb index ccf2bfd3..76036183 100644 --- a/spec/enum_spec.rb +++ b/spec/enum_spec.rb @@ -2,12 +2,12 @@ require File.expand_path("../support", __FILE__) -class EnumTest < CouchbaseOrm::Document +class EnumTest < CouchbaseOrm::Base enum rating: [:awesome, :good, :okay, :bad], default: :okay enum color: [:red, :green, :blue] end -describe CouchbaseOrm::Document do +describe CouchbaseOrm::Base do it "should create an attribute" do base = EnumTest.create!(rating: :good, color: :red) expect(base.attribute_names).to eq(["id", "rating", "color"]) diff --git a/spec/has_many_spec.rb b/spec/has_many_spec.rb index f5e73cfc..68dbc417 100644 --- a/spec/has_many_spec.rb +++ b/spec/has_many_spec.rb @@ -77,12 +77,12 @@ describe CouchbaseOrm::HasMany do context 'with view' do - class ObjectRatingViewTest < CouchbaseOrm::Document + class ObjectRatingViewTest < CouchbaseOrm::Base join :object_view_test, :rating_view_test view :view_all end - class RatingViewTest < CouchbaseOrm::Document + class RatingViewTest < CouchbaseOrm::Base enum rating: [:awesome, :good, :okay, :bad], default: :okay belongs_to :object_view_test @@ -90,7 +90,7 @@ class RatingViewTest < CouchbaseOrm::Document view :view_all end - class ObjectViewTest < CouchbaseOrm::Document + class ObjectViewTest < CouchbaseOrm::Base attribute :name, type: String has_many :rating_view_tests, dependent: :destroy @@ -101,13 +101,13 @@ class ObjectViewTest < CouchbaseOrm::Document end context 'with n1ql' do - class ObjectRatingN1qlTest < CouchbaseOrm::Document + class ObjectRatingN1qlTest < CouchbaseOrm::Base join :object_n1ql_test, :rating_n1ql_test n1ql :n1ql_all end - class RatingN1qlTest < CouchbaseOrm::Document + class RatingN1qlTest < CouchbaseOrm::Base enum rating: [:awesome, :good, :okay, :bad], default: :okay belongs_to :object_n1ql_test @@ -116,7 +116,7 @@ class RatingN1qlTest < CouchbaseOrm::Document n1ql :n1ql_all end - class ObjectN1qlTest < CouchbaseOrm::Document + class ObjectN1qlTest < CouchbaseOrm::Base attribute :name, type: String has_many :rating_n1ql_tests, dependent: :destroy, type: :n1ql diff --git a/spec/id_generator_spec.rb b/spec/id_generator_spec.rb index 3d6a8051..f1594d6e 100644 --- a/spec/id_generator_spec.rb +++ b/spec/id_generator_spec.rb @@ -3,7 +3,7 @@ require 'couchbase-orm' require 'thread' -class IdTestModel < CouchbaseOrm::Document; end +class IdTestModel < CouchbaseOrm::Base; end describe CouchbaseOrm::IdGenerator do it "should not generate ID clashes" do diff --git a/spec/index_spec.rb b/spec/index_spec.rb index 6602cd5a..c9d0a26a 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -3,20 +3,20 @@ require File.expand_path("../support", __FILE__) -class IndexTest < CouchbaseOrm::Document +class IndexTest < CouchbaseOrm::Base attribute :email, type: String attribute :name, type: String, default: :joe ensure_unique :email, presence: false end -class NoUniqueIndexTest < CouchbaseOrm::Document +class NoUniqueIndexTest < CouchbaseOrm::Base attribute :email, type: String attribute :name, type: String, default: :joe index :email, presence: false end -class IndexEnumTest < CouchbaseOrm::Document +class IndexEnumTest < CouchbaseOrm::Base enum visibility: [:group, :authority, :public], default: :authority enum color: [:red, :green, :blue] end diff --git a/spec/n1ql_spec.rb b/spec/n1ql_spec.rb index 3df8b959..f5ba9971 100644 --- a/spec/n1ql_spec.rb +++ b/spec/n1ql_spec.rb @@ -2,7 +2,7 @@ require File.expand_path("../support", __FILE__) -class N1QLTest < CouchbaseOrm::Document +class N1QLTest < CouchbaseOrm::Base attribute :name, type: String attribute :lastname, type: String enum rating: [:awesome, :good, :okay, :bad], default: :okay diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index 37406e04..6c83212a 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -3,19 +3,19 @@ require File.expand_path("../support", __FILE__) -class BasicModel < CouchbaseOrm::Document +class BasicModel < CouchbaseOrm::Base attribute :name attribute :address attribute :age end -class ModelWithDefaults < CouchbaseOrm::Document +class ModelWithDefaults < CouchbaseOrm::Base attribute :name, default: proc { 'bob' } attribute :address attribute :age, default: 23 end -class ModelWithCallbacks < CouchbaseOrm::Document +class ModelWithCallbacks < CouchbaseOrm::Base attribute :name attribute :address attribute :age @@ -35,7 +35,7 @@ def set_address; self.address = '23'; end def set_age; self.age = 30; end end -class ModelWithValidations < CouchbaseOrm::Document +class ModelWithValidations < CouchbaseOrm::Base attribute :name, type: String attribute :address, type: String attribute :age, type: :Integer diff --git a/spec/relation_spec.rb b/spec/relation_spec.rb index fba199a7..8efbe5a9 100644 --- a/spec/relation_spec.rb +++ b/spec/relation_spec.rb @@ -3,7 +3,7 @@ require File.expand_path("../support", __FILE__) -class RelationModel < CouchbaseOrm::Document +class RelationModel < CouchbaseOrm::Base attribute :name, :string attribute :last_name, :string attribute :active, :boolean diff --git a/spec/type_array_spec.rb b/spec/type_array_spec.rb index 35f2ba45..f6bc8c6b 100644 --- a/spec/type_array_spec.rb +++ b/spec/type_array_spec.rb @@ -2,7 +2,7 @@ require "active_model" -class TypeArrayTest < CouchbaseOrm::Document +class TypeArrayTest < CouchbaseOrm::Base attribute :name attribute :tags, :array, type: :string attribute :milestones, :array, type: :date @@ -10,7 +10,7 @@ class TypeArrayTest < CouchbaseOrm::Document attribute :things end -describe CouchbaseOrm::Document do +describe CouchbaseOrm::Base do it "should be able to store and retrieve an array of strings" do obj = TypeArrayTest.new obj.tags = ["foo", "bar"] diff --git a/spec/type_nested_spec.rb b/spec/type_nested_spec.rb index 289d1ded..b7be1fd9 100644 --- a/spec/type_nested_spec.rb +++ b/spec/type_nested_spec.rb @@ -3,7 +3,7 @@ require "active_model" class SubTypeTest < CouchbaseOrm::NestedDocument - attribute :name + attribute :name, :string attribute :tags, :array, type: :string attribute :milestones, :array, type: :date attribute :flags, :array, type: :boolean @@ -11,7 +11,7 @@ class SubTypeTest < CouchbaseOrm::NestedDocument attribute :child, :nested, type: SubTypeTest end -class TypeNestedTest < CouchbaseOrm::Document +class TypeNestedTest < CouchbaseOrm::Base attribute :main, :nested, type: SubTypeTest attribute :others, :array, type: SubTypeTest end @@ -102,4 +102,53 @@ class TypeNestedTest < CouchbaseOrm::Document it "should not serialize a list" do expect{CouchbaseOrm::Types::Nested.new(type: SubTypeTest).serialize([1,2,3])}.to raise_error(ArgumentError) end + + describe "Validations" do + + + class SubWithValidation < CouchbaseOrm::NestedDocument + attribute :name + attribute :label + attribute :child, :nested, type: SubWithValidation + validates :name, presence: true + validates :child, nested: true + end + + class WithValidationParent < CouchbaseOrm::Base + attribute :child, :nested, type: SubWithValidation + attribute :children, :array, type: SubWithValidation + validates :child, :children, nested: true + end + + it "should validate the nested object" do + obj = WithValidationParent.new + obj.child = SubWithValidation.new + expect(obj).to_not be_valid + expect(obj.errors[:child]).to eq ["is invalid"] + expect(obj.child.errors[:name]).to eq ["can't be blank"] + + end + + it "should validate the nested objects in an array" do + obj = WithValidationParent.new + obj.children = [SubWithValidation.new(name: "foo"), SubWithValidation.new] + expect(obj).to_not be_valid + expect(obj.errors[:children]).to eq ["is invalid"] + expect(obj.children[1].errors[:name]).to eq ["can't be blank"] + end + + it "should validate the nested in the nested object" do + obj = WithValidationParent.new + obj.child = SubWithValidation.new name: "foo", label: "parent" + obj.child.child = SubWithValidation.new label: "child" + + expect(obj).to_not be_valid + expect(obj.child).to_not be_valid + expect(obj.child.child).to_not be_valid + + expect(obj.errors[:child]).to eq ["is invalid"] + expect(obj.child.errors[:child]).to eq ["is invalid"] + expect(obj.child.child.errors[:name]).to eq ["can't be blank"] + end + end end diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 622e71ca..87c42d38 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -11,7 +11,7 @@ def serialize(value) ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal) -class TypeTest < CouchbaseOrm::Document +class TypeTest < CouchbaseOrm::Base attribute :name, :string attribute :age, :integer attribute :size, :float @@ -27,7 +27,7 @@ class TypeTest < CouchbaseOrm::Document index :precision_time, presence: false end -class N1qlTypeTest < CouchbaseOrm::Document +class N1qlTypeTest < CouchbaseOrm::Base attribute :name, :string attribute :age, :integer attribute :size, :float @@ -78,7 +78,7 @@ class N1qlTypeTest < CouchbaseOrm::Document end end -describe CouchbaseOrm::Document do +describe CouchbaseOrm::Base do before(:each) do TypeTest.delete_all N1qlTypeTest.delete_all diff --git a/spec/views_spec.rb b/spec/views_spec.rb index c9d15738..3ae3f865 100644 --- a/spec/views_spec.rb +++ b/spec/views_spec.rb @@ -3,7 +3,7 @@ require File.expand_path("../support", __FILE__) require 'set' -class ViewTest < CouchbaseOrm::Document +class ViewTest < CouchbaseOrm::Base attribute :name, type: String enum rating: [:awesome, :good, :okay, :bad], default: :okay From de981970b342d92be17a45ca2ec354c12ba6a32e Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 28 Sep 2022 17:03:22 +0200 Subject: [PATCH 6/9] fix tests --- lib/couchbase-orm.rb | 2 +- lib/couchbase-orm/base.rb | 10 +++++++++- lib/couchbase-orm/persistence.rb | 8 -------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index d9c5aa31..b7dbe0d4 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -48,7 +48,7 @@ def self.try_load(id) def self.try_load_create_model(result, id) ddoc = result&.content["type"] return nil unless ddoc - ::CouchbaseOrm::Document.descendants.each do |model| + ::CouchbaseOrm::Base.descendants.each do |model| if model.design_document == ddoc return model.new(result, id: id) end diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 4f55d444..8afcecd0 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -59,6 +59,14 @@ def connected? def table_exists? true end + + def _reflect_on_association(attribute) + false + end + + def type_for_attribute(attribute) + attribute_types[attribute] + end if ActiveModel::VERSION::MAJOR < 6 def attribute_names @@ -140,7 +148,7 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) @__metadata__.cas = model.cas assign_attributes(doc) - when CouchbaseOrm::Document + when CouchbaseOrm::Base clear_changes_information super(model.attributes.except(:id, 'type')) else diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index d299f869..26089ae6 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -221,14 +221,6 @@ def touch(**options) end - protected - - def serialized_attributes - attributes.map { |k, v| - v = id if k == "id" - [k, self.class.attribute_types[k].serialize(v)] - }.to_h - end def _update_record(*_args, with_cas: false, **options) return false unless perform_validations(:update, options) From d7cd4021e968d81666036641a442e04d04a6ee06 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 29 Sep 2022 10:28:00 +0200 Subject: [PATCH 7/9] remove special case for id storing --- lib/couchbase-orm/base.rb | 10 ++-------- lib/couchbase-orm/persistence.rb | 7 +++---- spec/base_spec.rb | 6 ++++++ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 8afcecd0..6ebdc1df 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -176,7 +176,6 @@ def []=(key, value) def serialized_attributes attributes.map { |k, v| - v = id if k == "id" # Fixme: review comment about that [k, self.class.attribute_types[k].serialize(v)] }.to_h end @@ -263,15 +262,10 @@ def exists?(id) alias_method :has_key?, :exists? end - # Document ID is a special case as it is not stored in the document - def id - @id - end - def id=(value) - raise 'ID cannot be changed' if @__metadata__.cas && value + raise RuntimeError, 'ID cannot be changed' if @__metadata__.cas && value attribute_will_change!(:id) - @id = value.to_s.presence + _write_attribute("id", value) end # Public: Allows for access to ActiveModel functionality. diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 26089ae6..8584470a 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -186,7 +186,7 @@ def update_columns(with_cas: false, **hash) else # Fallback to writing the whole document CouchbaseOrm.logger.debug { "Data - Replace #{id} #{attributes.to_s.truncate(200)}" } - self.class.collection.replace(id, attributes.except(:id).merge(type: self.class.design_document), **options) + self.class.collection.replace(id, attributes.except("id").merge(type: self.class.design_document), **options) end # Ensure the model is up to date @@ -230,7 +230,7 @@ def _update_record(*_args, with_cas: false, **options) run_callbacks :save do options[:cas] = @__metadata__.cas if with_cas CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{serialized_attributes.to_s.truncate(200)}" } - resp = self.class.collection.replace(id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options)) + resp = self.class.collection.replace(id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options)) # Ensure the model is up to date @__metadata__.cas = resp.cas @@ -247,8 +247,7 @@ def _create_record(*_args, **options) run_callbacks :save do assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{serialized_attributes.to_s.truncate(200)}" } - - resp = self.class.collection.upsert(self.id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) + resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) # Ensure the model is up to date @__metadata__.cas = resp.cas diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 3745b0e6..c69aa09a 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -148,6 +148,12 @@ class TimestampTest < CouchbaseOrm::Base base.destroy end + it "cannot change the id of a loaded object" do + base = BaseTest.create!(name: 'joe') + expect(base.id).to_not be_nil + expect{base.id = "foo"}.to raise_error(RuntimeError, 'ID cannot be changed') + end + if ActiveModel::VERSION::MAJOR >= 6 it "should have timestamp attributes for create in model" do expect(TimestampTest.timestamp_attributes_for_create_in_model).to eq(["created_at"]) From 7101f56821152e8f330785674202ff3960b54b72 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 29 Sep 2022 14:58:06 +0200 Subject: [PATCH 8/9] Add some doc in README about nested and array --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 030003a8..6e6fa7d3 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ context of rails application. You can also enforce types using ruby ```ruby class Comment < Couchbase::Model - attribute :author, :body, type: String + attribute :author, :string + attribute :body, :string validates_presence_of :author, :body end @@ -215,6 +216,41 @@ By default, `has_many` uses a view for association, but you can define a `type` end ``` +## Nested + +Attributes can be of type nested, they must specify a type of NestedDocument. The NestedValidation triggers nested validation on parent validation. + +```ruby + class Address < CouchbaseOrm::NestedDocument + attribute :road, :string + attribute :city, :string + validates :road, :city, presence: true + end + + class Author < CouchbaseOrm::Base + attribute :address, :nested, type: Address + validates :address, nested: true + end +``` + +## Array + +Attributes can be of type array, they must contain something that can be serialized and deserialized to/from JSON. You can enforce the type of array elements. The type can be a NestedDocument + +```ruby + class Book < CouchbaseOrm::NestedDocument + attribute :name, :string + validates :name, presence: true + end + + class Author < CouchbaseOrm::Base + attribute things, :array + attribute flags, :array, type: :string + attribute books, :array, type: Book + + validates :books, nested: true + end +``` ## Performance Comparison with Couchbase-Ruby-Model From 87868983b69cd7fa9c98a0ffde1a11701f6fded1 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 29 Sep 2022 15:00:01 +0200 Subject: [PATCH 9/9] Cosmetic --- lib/couchbase-orm/types/array.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/couchbase-orm/types/array.rb b/lib/couchbase-orm/types/array.rb index 9d7137f0..a8dd7fe5 100644 --- a/lib/couchbase-orm/types/array.rb +++ b/lib/couchbase-orm/types/array.rb @@ -16,6 +16,7 @@ def initialize(type: nil) def cast(values) return [] if values.nil? + raise ArgumentError, "#{values.inspect} must be an array" unless values.is_a?(::Array) values.map(&@type_class.method(:cast)) @@ -23,6 +24,7 @@ def cast(values) def serialize(values) return [] if values.nil? + values.map(&@type_class.method(:serialize)) end end