From 664e923a086f84b709e9903f601da6d3e8d2e46e Mon Sep 17 00:00:00 2001 From: Giallombardo Nathan Date: Tue, 3 Feb 2026 13:49:38 +0000 Subject: [PATCH 01/10] add polymorphic --- lib/couchbase-orm/utilities/embeds_one.rb | 103 +++++++++++++++------- spec/embeds_one_spec.rb | 96 ++++++++++++++++++++ 2 files changed, 168 insertions(+), 31 deletions(-) diff --git a/lib/couchbase-orm/utilities/embeds_one.rb b/lib/couchbase-orm/utilities/embeds_one.rb index e42aca64..79c5fee5 100644 --- a/lib/couchbase-orm/utilities/embeds_one.rb +++ b/lib/couchbase-orm/utilities/embeds_one.rb @@ -2,7 +2,7 @@ module CouchbaseOrm module EmbedsOne - def embeds_one(name, class_name: nil, store_as: nil, validate: true) + def embeds_one(name, class_name: nil, store_as: nil, validate: true, polymorphic: false) storage_key = (store_as || name).to_sym attribute storage_key, :hash, default: nil @@ -15,49 +15,90 @@ def embeds_one(name, class_name: nil, store_as: nil, validate: true) key: storage_key, name: name, instance_var: instance_var, + polymorphic: polymorphic, }) validates_embedded(name) if validate - # Helper to lazy-resolve class when needed - define_method("_resolve_embedded_class_for_#{name}") do - @__resolved_classes ||= {} - @__resolved_classes[name] ||= begin - klass_name.constantize - rescue NameError => e - warn "WARNING: #{klass_name} could not be resolved in #{self.class.name}: #{e.message}" - raise + if polymorphic + # Add type attribute for polymorphic associations + type_key = :"#{name}_type" + attribute type_key, :string, default: nil + + # Polymorphic reader + define_method(name) do + return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + + raw = read_attribute(storage_key) + type = read_attribute(type_key) + return instance_variable_set(instance_var, nil) unless raw.present? && type.present? + + klass = type.constantize + obj = klass.new(raw) + obj.embedded = true + instance_variable_set(instance_var, obj) end - end - define_method(name) do - return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + # Polymorphic writer + define_method("#{name}=") do |val| + if val.nil? + write_attribute(storage_key, nil) + write_attribute(type_key, nil) + instance_variable_set(instance_var, nil) + next + end - raw = read_attribute(storage_key) - return instance_variable_set(instance_var, nil) unless raw.present? + obj = val.is_a?(Hash) ? raise(ArgumentError, "Cannot infer type from Hash for polymorphic embeds_one") : val + obj.embedded = true - klass = send("_resolve_embedded_class_for_#{name}") - obj = klass.new(raw) - obj.embedded = true - instance_variable_set(instance_var, obj) - end + raw = obj.serialized_attributes + raw.delete('id') if raw['id'].blank? + + write_attribute(storage_key, raw) + write_attribute(type_key, obj.class.name) + instance_variable_set(instance_var, obj) + end + else + # Helper to lazy-resolve class when needed + define_method("_resolve_embedded_class_for_#{name}") do + @__resolved_classes ||= {} + @__resolved_classes[name] ||= begin + klass_name.constantize + rescue NameError => e + warn "WARNING: #{klass_name} could not be resolved in #{self.class.name}: #{e.message}" + raise + end + end + + define_method(name) do + return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) - define_method("#{name}=") do |val| - if val.nil? - write_attribute(storage_key, nil) - instance_variable_set(instance_var, nil) - next + raw = read_attribute(storage_key) + return instance_variable_set(instance_var, nil) unless raw.present? + + klass = send("_resolve_embedded_class_for_#{name}") + obj = klass.new(raw) + obj.embedded = true + instance_variable_set(instance_var, obj) end - klass = send("_resolve_embedded_class_for_#{name}") - obj = val.is_a?(klass) ? val : klass.new(val) - obj.embedded = true + define_method("#{name}=") do |val| + if val.nil? + write_attribute(storage_key, nil) + instance_variable_set(instance_var, nil) + next + end + + klass = send("_resolve_embedded_class_for_#{name}") + obj = val.is_a?(klass) ? val : klass.new(val) + obj.embedded = true - raw = obj.serialized_attributes - raw.delete('id') if raw['id'].blank? + raw = obj.serialized_attributes + raw.delete('id') if raw['id'].blank? - write_attribute(storage_key, raw) - instance_variable_set(instance_var, obj) + write_attribute(storage_key, raw) + instance_variable_set(instance_var, obj) + end end define_method(:"#{name}_reset") do diff --git a/spec/embeds_one_spec.rb b/spec/embeds_one_spec.rb index a72d08bb..65bd7365 100644 --- a/spec/embeds_one_spec.rb +++ b/spec/embeds_one_spec.rb @@ -66,6 +66,27 @@ class Article < CouchbaseOrm::Base embeds_one :comments_container, class_name: 'CommentsContainer' end +class Attachment < CouchbaseOrm::Base + attribute :filename, :string + attribute :attachable_type, :string + attribute :attachable_id, :string + belongs_to :attachable, polymorphic: true +end + +class Image < CouchbaseOrm::Base + attribute :url, :string + attribute :caption, :string +end + +class Video < CouchbaseOrm::Base + attribute :url, :string + attribute :duration, :integer +end + +class Post < CouchbaseOrm::Base + embeds_one :media, polymorphic: true +end + describe CouchbaseOrm::EmbedsOne do let(:raw_data) { { bio: 'Software Engineer' } } @@ -377,4 +398,79 @@ class Article < CouchbaseOrm::Base article.destroy! if article&.persisted? end end + + describe 'polymorphic embeds_one' do + it 'can embed different types polymorphically with type attribute' do + image = Image.new(url: 'https://example.com/image.jpg', caption: 'A beautiful sunset') + post = Post.new(media: image) + + expect(post.media).to be_a(Image) + expect(post.media.url).to eq('https://example.com/image.jpg') + expect(post.media.caption).to eq('A beautiful sunset') + expect(post.attributes['media_type']).to eq('Image') + end + + it 'can embed a different polymorphic type' do + video = Video.new(url: 'https://example.com/video.mp4', duration: 120) + post = Post.new(media: video) + + expect(post.media).to be_a(Video) + expect(post.media.url).to eq('https://example.com/video.mp4') + expect(post.media.duration).to eq(120) + expect(post.attributes['media_type']).to eq('Video') + end + + it 'persists and retrieves polymorphic embedded objects correctly' do + video = Video.new(url: 'https://example.com/demo.mp4', duration: 90) + post = Post.create!(media: video) + + loaded = Post.find(post.id) + expect(loaded.media).to be_a(Video) + expect(loaded.media.url).to eq('https://example.com/demo.mp4') + expect(loaded.media.duration).to eq(90) + ensure + post.destroy! if post&.persisted? + end + + it 'can switch between different polymorphic types' do + image = Image.new(url: 'https://example.com/pic.jpg', caption: 'Original') + post = Post.create!(media: image) + + post.media = Video.new(url: 'https://example.com/clip.mp4', duration: 45) + post.save! + + post.reload + expect(post.media).to be_a(Video) + expect(post.media.url).to eq('https://example.com/clip.mp4') + ensure + post.destroy! if post&.persisted? + end + + it 'sets embedded flag on polymorphic embedded objects' do + image = Image.new(url: 'https://example.com/test.jpg', caption: 'Test') + post = Post.new(media: image) + + expect(post.media.instance_variable_get(:@_embedded)).to be true + end + + it 'can set polymorphic embedded to nil' do + video = Video.new(url: 'https://example.com/test.mp4', duration: 60) + post = Post.new(media: video) + + post.media = nil + expect(post.media).to be_nil + expect(post.attributes['media']).to be_nil + expect(post.attributes['media_type']).to be_nil + end + + it 'validates polymorphic embedded objects' do + # Assuming Image has validations + image = Image.new(url: nil, caption: 'No URL') + post = Post.new(media: image) + + # Since Image doesn't have validations in our test setup, this would pass + # But demonstrates the structure for validation testing + expect(post.media).to be_a(Image) + end + end end From 02d619d61d7e4bc599f29dcf6d5863a6cf8ba13f Mon Sep 17 00:00:00 2001 From: Giallombardo Nathan Date: Tue, 3 Feb 2026 13:54:43 +0000 Subject: [PATCH 02/10] add embeds many polymorphic --- lib/couchbase-orm/utilities/embeds_many.rb | 123 ++++++++---- spec/embeds_many_spec.rb | 218 +++++++++++++++++++++ 2 files changed, 308 insertions(+), 33 deletions(-) diff --git a/lib/couchbase-orm/utilities/embeds_many.rb b/lib/couchbase-orm/utilities/embeds_many.rb index 2dbb7d41..c81dde9e 100644 --- a/lib/couchbase-orm/utilities/embeds_many.rb +++ b/lib/couchbase-orm/utilities/embeds_many.rb @@ -2,7 +2,7 @@ module CouchbaseOrm module EmbedsMany - def embeds_many(name, class_name: nil, store_as: nil, validate: true) + def embeds_many(name, class_name: nil, store_as: nil, validate: true, polymorphic: false) storage_key = (store_as || name).to_sym attribute storage_key, :array, type: :hash, default: [] @@ -15,51 +15,108 @@ def embeds_many(name, class_name: nil, store_as: nil, validate: true) key: storage_key, name: name, instance_var: instance_var, + polymorphic: polymorphic, }) validates_embedded(name) if validate - # Lazy class resolution method - define_method("_resolve_embedded_class_for_#{name}") do - @__resolved_classes ||= {} - @__resolved_classes[name] ||= begin - klass_name.constantize - rescue NameError => e - warn "WARNING: #{klass_name} could not be resolved in #{self.class.name}: #{e.message}" - raise - end - end + if polymorphic + # Add types attribute for polymorphic associations + types_key = :"#{name}_types" + attribute types_key, :array, type: :string, default: [] + + # Polymorphic reader + define_method(name) do + return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + + raw_array = read_attribute(storage_key) + types_array = read_attribute(types_key) - define_method(name) do - return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + embedded_objects = [] + raw_array.each_with_index do |raw, index| + type = types_array[index] + next unless raw.present? && type.present? - klass = send("_resolve_embedded_class_for_#{name}") - embedded_objects = read_attribute(storage_key).map do |raw| - obj = klass.new(raw) - obj.embedded = true - obj + klass = type.constantize + obj = klass.new(raw) + obj.embedded = true + embedded_objects << obj + end + + instance_variable_set(instance_var, embedded_objects) end - instance_variable_set(instance_var, embedded_objects) - end + # Polymorphic writer + define_method("#{name}=") do |val| + embedded_objects = [] + serialized = [] + types = [] + + Array(val).each do |v| + if v.nil? + next + elsif v.is_a?(Hash) + raise ArgumentError, "Cannot infer type from Hash for polymorphic embeds_many" + else + obj = v + obj.embedded = true + + raw = obj.serialized_attributes + raw.delete('id') if raw['id'].blank? - define_method("#{name}=") do |val| - klass = send("_resolve_embedded_class_for_#{name}") + embedded_objects << obj + serialized << raw + types << obj.class.name + end + end - embedded_objects = [] - serialized = [] + write_attribute(storage_key, serialized) + write_attribute(types_key, types) + instance_variable_set(instance_var, embedded_objects) + end + else + # Lazy class resolution method + define_method("_resolve_embedded_class_for_#{name}") do + @__resolved_classes ||= {} + @__resolved_classes[name] ||= begin + klass_name.constantize + rescue NameError => e + warn "WARNING: #{klass_name} could not be resolved in #{self.class.name}: #{e.message}" + raise + end + end + + define_method(name) do + return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + + klass = send("_resolve_embedded_class_for_#{name}") + embedded_objects = read_attribute(storage_key).map do |raw| + obj = klass.new(raw) + obj.embedded = true + obj + end - Array(val).each do |v| - obj = v.is_a?(klass) ? v : klass.new(v) - obj.embedded = true - raw = obj.serialized_attributes - raw.delete('id') if raw['id'].blank? - embedded_objects << obj - serialized << raw + instance_variable_set(instance_var, embedded_objects) end - write_attribute(storage_key, serialized) - instance_variable_set(instance_var, embedded_objects) + define_method("#{name}=") do |val| + klass = send("_resolve_embedded_class_for_#{name}") + + embedded_objects = [] + serialized = [] + + Array(val).each do |v| + obj = v.is_a?(klass) ? v : klass.new(v) + obj.embedded = true + raw = obj.serialized_attributes + raw.delete('id') if raw['id'].blank? + embedded_objects << obj + serialized << raw + end + + write_attribute(storage_key, serialized) + instance_variable_set(instance_var, embedded_objects) + end end define_method(:"#{name}_reset") do diff --git a/spec/embeds_many_spec.rb b/spec/embeds_many_spec.rb index d074e163..4dc4bee3 100644 --- a/spec/embeds_many_spec.rb +++ b/spec/embeds_many_spec.rb @@ -67,6 +67,25 @@ class Citizen < CouchbaseOrm::Base embeds_many :addresses, class_name: 'AddressWithCity' end +class ImageAttachment < CouchbaseOrm::Base + attribute :url, :string + attribute :caption, :string +end + +class VideoAttachment < CouchbaseOrm::Base + attribute :url, :string + attribute :duration, :integer +end + +class DocumentAttachment < CouchbaseOrm::Base + attribute :filename, :string + attribute :size, :integer +end + +class Article < CouchbaseOrm::Base + embeds_many :attachments, polymorphic: true +end + describe CouchbaseOrm::EmbedsMany do let(:raw_data) { [{ street: '123 Main St' }, { street: '456 Elm St' }] } @@ -354,4 +373,203 @@ class Citizen < CouchbaseOrm::Base person.destroy! if person&.persisted? end end + + describe 'polymorphic embeds_many' do + it 'can embed different types polymorphically with types attribute' do + image = ImageAttachment.new(url: 'https://example.com/image.jpg', caption: 'A beautiful sunset') + video = VideoAttachment.new(url: 'https://example.com/video.mp4', duration: 120) + article = Article.new(attachments: [image, video]) + + expect(article.attachments.size).to eq(2) + expect(article.attachments.first).to be_a(ImageAttachment) + expect(article.attachments.first.url).to eq('https://example.com/image.jpg') + expect(article.attachments.first.caption).to eq('A beautiful sunset') + expect(article.attachments.last).to be_a(VideoAttachment) + expect(article.attachments.last.url).to eq('https://example.com/video.mp4') + expect(article.attachments.last.duration).to eq(120) + expect(article.attributes['attachments_types']).to eq(['ImageAttachment', 'VideoAttachment']) + end + + it 'can embed multiple items of the same polymorphic type' do + image1 = ImageAttachment.new(url: 'https://example.com/image1.jpg', caption: 'First') + image2 = ImageAttachment.new(url: 'https://example.com/image2.jpg', caption: 'Second') + article = Article.new(attachments: [image1, image2]) + + expect(article.attachments.size).to eq(2) + expect(article.attachments).to all(be_a(ImageAttachment)) + expect(article.attachments.first.caption).to eq('First') + expect(article.attachments.last.caption).to eq('Second') + expect(article.attributes['attachments_types']).to eq(['ImageAttachment', 'ImageAttachment']) + end + + it 'can embed mixed polymorphic types' do + image = ImageAttachment.new(url: 'https://example.com/pic.jpg', caption: 'Photo') + video = VideoAttachment.new(url: 'https://example.com/clip.mp4', duration: 90) + doc = DocumentAttachment.new(filename: 'report.pdf', size: 1024) + article = Article.new(attachments: [image, video, doc]) + + expect(article.attachments.size).to eq(3) + expect(article.attachments[0]).to be_a(ImageAttachment) + expect(article.attachments[1]).to be_a(VideoAttachment) + expect(article.attachments[2]).to be_a(DocumentAttachment) + expect(article.attachments[2].filename).to eq('report.pdf') + expect(article.attachments[2].size).to eq(1024) + end + + it 'persists and retrieves polymorphic embedded collections correctly' do + image = ImageAttachment.new(url: 'https://example.com/demo.jpg', caption: 'Demo') + video = VideoAttachment.new(url: 'https://example.com/demo.mp4', duration: 60) + article = Article.create!(attachments: [image, video]) + + loaded = Article.find(article.id) + expect(loaded.attachments.size).to eq(2) + expect(loaded.attachments.first).to be_a(ImageAttachment) + expect(loaded.attachments.first.url).to eq('https://example.com/demo.jpg') + expect(loaded.attachments.last).to be_a(VideoAttachment) + expect(loaded.attachments.last.duration).to eq(60) + ensure + article.destroy! if article&.persisted? + end + + it 'can update polymorphic embedded collections' do + image = ImageAttachment.new(url: 'https://example.com/original.jpg', caption: 'Original') + article = Article.create!(attachments: [image]) + + video = VideoAttachment.new(url: 'https://example.com/new.mp4', duration: 45) + article.attachments = [video] + article.save! + + article.reload + expect(article.attachments.size).to eq(1) + expect(article.attachments.first).to be_a(VideoAttachment) + expect(article.attachments.first.url).to eq('https://example.com/new.mp4') + ensure + article.destroy! if article&.persisted? + end + + it 'can add to polymorphic embedded collection' do + image = ImageAttachment.new(url: 'https://example.com/first.jpg', caption: 'First') + article = Article.create!(attachments: [image]) + + video = VideoAttachment.new(url: 'https://example.com/second.mp4', duration: 30) + article.attachments = article.attachments + [video] + article.save! + + article.reload + expect(article.attachments.size).to eq(2) + expect(article.attachments.first).to be_a(ImageAttachment) + expect(article.attachments.last).to be_a(VideoAttachment) + ensure + article.destroy! if article&.persisted? + end + + it 'sets embedded flag on polymorphic embedded objects' do + image = ImageAttachment.new(url: 'https://example.com/test.jpg', caption: 'Test') + video = VideoAttachment.new(url: 'https://example.com/test.mp4', duration: 60) + article = Article.new(attachments: [image, video]) + + article.attachments.each do |attachment| + expect(attachment.instance_variable_get(:@_embedded)).to be true + end + end + + it 'can set polymorphic embedded collection to empty array' do + video = VideoAttachment.new(url: 'https://example.com/test.mp4', duration: 60) + article = Article.new(attachments: [video]) + + article.attachments = [] + expect(article.attachments).to eq([]) + expect(article.attributes['attachments']).to eq([]) + expect(article.attributes['attachments_types']).to eq([]) + end + + it 'can set polymorphic embedded collection to nil' do + image = ImageAttachment.new(url: 'https://example.com/test.jpg', caption: 'Test') + article = Article.new(attachments: [image]) + + article.attachments = nil + expect(article.attachments).to eq([]) + expect(article.attributes['attachments']).to eq([]) + expect(article.attributes['attachments_types']).to eq([]) + end + + it 'raises error when trying to assign Hash to polymorphic embeds_many' do + article = Article.new + + expect { + article.attachments = [{ url: 'https://example.com/test.jpg', caption: 'Test' }] + }.to raise_error(ArgumentError, 'Cannot infer type from Hash for polymorphic embeds_many') + end + + it 'skips nil values in polymorphic embedded collection' do + image = ImageAttachment.new(url: 'https://example.com/test.jpg', caption: 'Test') + article = Article.new(attachments: [image, nil]) + + expect(article.attachments.size).to eq(1) + expect(article.attachments.first).to be_a(ImageAttachment) + end + + it 'memoizes polymorphic embedded collection after first access' do + image = ImageAttachment.new(url: 'https://example.com/test.jpg', caption: 'Test') + article = Article.new(attachments: [image]) + + first_call = article.attachments + second_call = article.attachments + + expect(first_call).to equal(second_call) + end + + it 'lazily loads polymorphic embedded collection' do + image = ImageAttachment.new(url: 'https://example.com/lazy.jpg', caption: 'Lazy') + article = Article.create!(attachments: [image]) + article = Article.find(article.id) + + expect(article.instance_variable_defined?(:@__assoc_attachments)).to be false + + _ = article.attachments + + expect(article.instance_variable_defined?(:@__assoc_attachments)).to be true + ensure + article.destroy! if article&.persisted? + end + + it 'supports reset for polymorphic embedded collection' do + image = ImageAttachment.new(url: 'https://example.com/test.jpg', caption: 'Test') + article = Article.new(attachments: [image]) + + original = article.attachments + article.attachments_reset + + new_instance = article.attachments + expect(new_instance).to be_an(Array) + expect(new_instance).not_to equal(original) + end + + it 'does not include id in polymorphic embedded objects if blank' do + image = ImageAttachment.new(url: 'https://example.com/test.jpg', caption: 'Test') + article = Article.new(attachments: [image]) + + expect(article.send(:serialized_attributes)['attachments'].first).not_to include('id') + end + + it 'handles empty polymorphic embedded collection on read' do + article = Article.new + expect(article.attachments).to eq([]) + end + + it 'preserves order of polymorphic embedded objects' do + video = VideoAttachment.new(url: 'https://example.com/first.mp4', duration: 30) + image = ImageAttachment.new(url: 'https://example.com/second.jpg', caption: 'Second') + doc = DocumentAttachment.new(filename: 'third.pdf', size: 2048) + + article = Article.create!(attachments: [video, image, doc]) + article.reload + + expect(article.attachments[0]).to be_a(VideoAttachment) + expect(article.attachments[1]).to be_a(ImageAttachment) + expect(article.attachments[2]).to be_a(DocumentAttachment) + ensure + article.destroy! if article&.persisted? + end + end end From e0403e4bcdd5f774361b6460a33b4078aadb2d49 Mon Sep 17 00:00:00 2001 From: Giallombardo Nathan Date: Tue, 3 Feb 2026 13:58:08 +0000 Subject: [PATCH 03/10] add documentation --- docs/reference/associations.txt | 226 ++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/docs/reference/associations.txt b/docs/reference/associations.txt index b2ca1117..37ab0486 100644 --- a/docs/reference/associations.txt +++ b/docs/reference/associations.txt @@ -12,6 +12,232 @@ Associations :depth: 2 :class: singlecol +Embedded Associations +===================== + +CouchbaseOrm supports embedding documents within a parent document using +``embeds_one`` and ``embeds_many``. Unlike referenced associations, embedded +documents are stored directly within the parent document and do not have their +own separate document ID in the database. This is useful for modeling +has-one and has-many relationships where the child documents don't need to exist +independently. + +Embeds One +---------- + +Use the ``embeds_one`` association to declare that the parent embeds a single +child document: + +.. code-block:: ruby + + class Profile < CouchbaseOrm::Base + attribute :bio, :string + attribute :website, :string + end + + class User < CouchbaseOrm::Base + attribute :name, :string + embeds_one :profile + end + + user = User.create!(name: 'Alice', profile: { bio: 'Software Engineer' }) + # => # + + user.profile + # => # + + user.profile.bio = 'Senior Software Engineer' + user.profile = user.profile # Reassign to track changes + user.save! + +The embedded document is stored as a hash within the parent: + +.. code-block:: ruby + + # In the database, the document looks like: + { + "_id": "user::123", + "name": "Alice", + "profile": { + "bio": "Software Engineer", + "website": null + } + } + +Embedded documents cannot be saved, destroyed, or reloaded independently: + +.. code-block:: ruby + + user.profile.save + # => raises "Cannot save an embedded document!" + +Embeds Many +----------- + +Use the ``embeds_many`` association to declare that the parent embeds multiple +child documents: + +.. code-block:: ruby + + class Address < CouchbaseOrm::Base + attribute :street, :string + attribute :city, :string + end + + class Person < CouchbaseOrm::Base + attribute :name, :string + embeds_many :addresses + end + + person = Person.create!( + name: 'Bob', + addresses: [ + { street: '123 Main St', city: 'New York' }, + { street: '456 Elm St', city: 'Boston' } + ] + ) + + person.addresses + # => [#
, + # #
] + + person.addresses << Address.new(street: '789 Oak Ave', city: 'Chicago') + person.addresses = person.addresses # Reassign to track changes + person.save! + +Polymorphic Embedded Associations +---------------------------------- + +Both ``embeds_one`` and ``embeds_many`` support polymorphic associations, +allowing you to embed different types of documents in the same association: + +.. code-block:: ruby + + class Image < CouchbaseOrm::Base + attribute :url, :string + attribute :caption, :string + end + + class Video < CouchbaseOrm::Base + attribute :url, :string + attribute :duration, :integer + end + + class Post < CouchbaseOrm::Base + embeds_one :media, polymorphic: true + end + + # Embed an image + post = Post.create!(media: Image.new(url: 'photo.jpg', caption: 'Sunset')) + post.media + # => # + + # Switch to a video + post.media = Video.new(url: 'clip.mp4', duration: 120) + post.save! + +For ``embeds_many`` with polymorphism: + +.. code-block:: ruby + + class Article < CouchbaseOrm::Base + embeds_many :attachments, polymorphic: true + end + + article = Article.create!( + attachments: [ + Image.new(url: 'diagram.png', caption: 'Architecture'), + Video.new(url: 'demo.mp4', duration: 90) + ] + ) + + article.attachments + # => [#, #