{"id":15554157,"url":"https://github.com/mansakondo/activemodel-embedding","last_synced_at":"2025-08-03T22:39:37.697Z","repository":{"id":45847947,"uuid":"405228091","full_name":"mansakondo/activemodel-embedding","owner":"mansakondo","description":"An ActiveModel extension to model your semi-structured data using embedded associations","archived":false,"fork":false,"pushed_at":"2021-10-28T18:22:44.000Z","size":205,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-17T01:13:21.575Z","etag":null,"topics":["activemodel","database","denormalization","document-modelling","json","jsonb","rails","ruby","semi-structured-data","type-casting"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mansakondo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-09-10T22:32:55.000Z","updated_at":"2022-10-29T22:09:12.000Z","dependencies_parsed_at":"2022-09-01T23:01:38.114Z","dependency_job_id":null,"html_url":"https://github.com/mansakondo/activemodel-embedding","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/mansakondo/activemodel-embedding","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mansakondo%2Factivemodel-embedding","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mansakondo%2Factivemodel-embedding/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mansakondo%2Factivemodel-embedding/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mansakondo%2Factivemodel-embedding/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mansakondo","download_url":"https://codeload.github.com/mansakondo/activemodel-embedding/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mansakondo%2Factivemodel-embedding/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267338329,"owners_count":24071329,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-27T02:00:11.917Z","response_time":82,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activemodel","database","denormalization","document-modelling","json","jsonb","rails","ruby","semi-structured-data","type-casting"],"created_at":"2024-10-02T14:50:28.820Z","updated_at":"2025-08-03T22:39:37.675Z","avatar_url":"https://github.com/mansakondo.png","language":"Ruby","readme":"# ActiveModel::Embedding [![Gem Version](https://badge.fury.io/rb/activemodel-embedding.svg)](https://badge.fury.io/rb/activemodel-embedding)\nAn ActiveModel extension to model your [semi-structured data](#semi-structured-data) using\n[embedded associations](#embedded-associations).\n\n- [Features](#features)\n- [Introduction](#introduction)\n- [Usage](#usage)\n- [:warning: Warning](#warning-warning)\n- [Use Case: Dealing with bibliographic data](#use-case%3A-dealing-with-bibliographic-data)\n- [Concepts](#concepts)\n- [Components](#components)\n- [Installation](#installation)\n- [License](#license)\n\n## Features\n- [Embedded associations](#embedded-associations) (powered by the [Attributes API](https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html))\n- Nested attributes support out-of-the-box\n- [Validations](#validations)\n- [Custom collections](#custom-collections)\n- [Custom types](#custom-types)\n- Autosaving\n- Dirty tracking\n\n## Introduction\nRelational databases are very powerful. Their power comes from their ability to...\n- Preserve data integrity with a predefined schema.\n- Make complex relationships through joins.\n\nBut sometimes, we can stumble accross data that don't fit in the [relational\nmodel](https://www.digitalocean.com/community/tutorials/what-is-the-relational-model). We call\nthis kind of data: [semi-structured data](#semi-structured-data). When this happens, the\nthings that makes relational databases powerful are the things that gets in our way, and\ncomplicate our model instead of simplifying it.\n\nThat's why [document databases](https://en.wikipedia.org/wiki/Document-oriented_database)\nexist, to model and store semi-structured data. However, if we choose to use a document\ndatabase, we'll loose all the power of using a relational database.\n\nLuckily for us, relational databases like Postgres and MySQL now has good JSON support. So most\nof us won't need to use a document database like MongoDB, as it would be overkill. Most of the\ntime, we only need to\n[denormalize](https://www.geeksforgeeks.org/denormalization-in-databases/) some parts of our\nmodel. So it makes more sense to use simple JSON columns for those, instead of going all-in,\nand dump your beloved relational database for MongoDB.\n\nCurrently in Rails, we can have full control over how our JSON data is stored and retrieved\nfrom the database, by using the [Attributes\nAPI](https://dev.to/swanson/automatically-cast-params-with-the-rails-attributes-api-446a) to\nserialize and deserialize our data.\n\nThat's what this extension does, in order to provide a convinient way to model semi-structured\ndata in a Rails application.\n\n## Usage\nLet's say that we need to store books in our database. We might want to \"embed\" data such as\nparts, chapters and sections without creating additional tables. By doing so, we can retrieve\nall the embedded data of a book in a single read operation, instead of performing expensive\nmulti-table joins.\n\nWe can then model our data this way:\n```ruby\nclass Book \u003c ApplicationRecord\n  include ActiveModel::Embedding::Associations\n\n  embeds_many :parts\nend\n\nclass Book::Part\n  include ActiveModel::Embedding::Document\n\n  attribute :title, :string\n\n  embeds_many :chapters\nend\n\nclass Book::Part::Chapter\n  include ActiveModel::Embedding::Document\n\n  attribute :title, :string\n\n  embeds_many :sections\nend\n\nclass Book::Part::Chapter::Section\n  include ActiveModel::Embedding::Document\n\n  attribute :title, :string\n  attribute :content, :string\nend\n```\n\nAnd display it like this (with nested attributes support out-of-the-box):\n```erb\n# app/views/books/_form.html.erb\n\u003c%= form_with model: @book do |book_form| %\u003e\n  \u003c%= book_form.fields_for :parts do |part_fields| %\u003e\n\n    \u003c%= part_fields.label :title %\u003e\n    \u003c%= part_fields.text_field :title %\u003e\n\n    \u003c%= part_fields.fields_for :chapters do |chapter_fields| %\u003e\n      \u003c%= chapter_fields.label :title %\u003e\n      \u003c%= chapter_fields.text_field :title %\u003e\n\n      \u003c%= chapter_fields.fields_for :sections do |section_fields| %\u003e\n        \u003c%= section_fields.label :title %\u003e\n        \u003c%= section_fields.text_field :title %\u003e\n        \u003c%= section_fields.text_area :content %\u003e\n      \u003c% end %\u003e\n    \u003c% end %\u003e\n  \u003c% end %\u003e\n\n  \u003c%= book_form.submit %\u003e\n\u003c% end %\u003e\n```\n### Validations\n```ruby\nclass SomeModel \u003c ApplicationRecord\n  include ActiveModel::Embedding::Associations\n\n  embeds_many :things\n\n  validates_associated :things\nend\n\nclass Thing\n  include ActiveModel::Embedding::Document\n\n  embeds_many :other_things\n\n  validates_associated :other_things\nend\n\nclass OtherThing\n  include ActiveModel::Embedding::Document\n\n  attribute :some_attribute, :string\n\n  validates :some_attribute, presence: true\nend\n```\n```ruby\nthings = Array.new(3) { Thing.new(other_things: Array.new(3) { OtherThing.new } }\nrecord = SomeModel.new things: things\n\nrecord.valid? # =\u003e false\nrecord.save # =\u003e false\n\nrecord.things.other_things = Array.new(3) { OtherThing.new(some_attribute: \"present\") }\n\nrecord.valid? # =\u003e true\nrecord.save # =\u003e true\n```\n\n### Custom collections\n```ruby\nclass SomeCollection\n  include ActiveModel::Embedding::Collecting\nend\n\nclass Thing\nend\n\nclass SomeModel\n  include ActiveModel::Embedding::Document\n\n  embeds_many :things, collection: \"SomeCollection\"\nend\n\nsome_model = SomeModel.new things: Array.new(3) { Thing.new }\nsome_model.things.class\n# =\u003e SomeCollection\n```\n### Custom types\n```ruby\n# config/initializers/types.rb\nclass SomeType \u003c ActiveModel::Type::Value\n  def cast(value)\n    value.cast_type = self.class\n    super\n  end\nend\n\nActiveModel::Type.register(:some_type, SomeType)\n\nclass SomeOtherType \u003c ActiveModel::Type::Value\n  attr_reader :context\n\n  def initialize(context:)\n    @context = context\n  end\n\n  def cast(value)\n    value.cast_type = self.class\n    value.context = context\n    super\n  end\nend\n```\n```ruby\nclass Thing\n  attr_accessor :cast_type\n  attr_accessor :context\nend\n\nclass SomeModel\n  include ActiveModel::Embedding::Document\n\n  embeds_many :things, cast_type: :some_type\n  embeds_many :other_things, cast_type: SomeOtherType.new(context: self)\nend\n\n@some_model.things.first.cast_type\n# =\u003e SomeType\n@some_model.other_things.first.cast_type\n# =\u003e SomeOtherType\n@some_model.other_things.first.context\n# =\u003e SomeModel\n```\n\n### Associations\n#### embeds_many\nMaps a JSON array to a [collection](#collection).\n\nOptions:\n- `:class_name`: Specify the class of the [documents](#document) in the collection. Inferred by default.\n- `:collection`: Specify a custom collection class which includes\n    [`ActiveModel::Collecting`](#activemodel%3A%3Acollecting) (`ActiveModel::Collection` by\n    default).\n- `:cast_type`: Specify a custom type that should be used to cast the documents in the\ncollection. (the `:class_name` is ignored if this option is present.)\n#### embed_one\nMaps a JSON object to a [document](#document).\n\nOptions:\n- `:class_name`: Same as above.\n- `:cast_type`: Same as above.\n\n## :warning: Warning\n[Embedded associations](#embedded-associations) should only be used if you're sure that the data you want to embed is\n**encapsulated**. Which means, that embedded associations should only be accessed through the\nparent, and not from the outside. Thus, this should only be used if performing joins isn't a\nviable option.\n\nRead the section below (and [this\narticle](http://www.sarahmei.com/blog/2013/11/11/why-you-should-never-use-mongodb/)) for more\ninsights on the use cases of this feature.\n\n## Use case: Dealing with bibliographic data\nLet's say that we are building an app to help libraries build and manage an online catalog.\nWhen we're browsing through a catalog, we often see item information formatted like this:\n```\nAuthor:        Shakespeare, William, 1564-1616.\nTitle:         Hamlet / William Shakespeare.\nDescription:   xiii, 295 pages : illustrations ; 23 cm.\nSeries:        NTC Shakespeare series.\nLocal Call No: 822.33 S52 S7\nISBN:          0844257443\nSeries Entry:  NTC Shakespeare series.\nControl No.:   ocm30152659\n```\n\nBut in the library world, data is produced and exchanged is this form:\n```\nLDR 00815nam  2200289 a 4500\n001 ocm30152659\n003 OCoLC\n005 19971028235910.0\n008 940909t19941994ilua          000 0 eng\n010   $a92060871\n020   $a0844257443\n040   $aDLC$cDLC$dBKL$dUtOrBLW\n049   $aBKLA\n099   $a822.33$aS52$aS7\n100 1 $aShakespeare, William,$d1564-1616.\n245 10$aHamlet /$cWilliam Shakespeare.\n264  1$aLincolnwood, Ill. :$bNTC Pub. Group,$c[1994]\n264  4$cÂ©1994.\n300   $axiii, 295 pages :$billustrations ;$c23 cm.\n336   $atext$btxt$2rdacontent.\n337   $aunmediated$bn$2rdamedia.\n338   $avolume$bnc$2rdacarrier.\n490 1 $aNTC Shakespeare series.\n830  0$aNTC Shakespeare series.\n907   $a.b108930609\n948   $aLTI 2018-07-09\n948   $aMARS\n```\nThis is what we call a *MARC record*. That's how libraries describes the ressources they own.\n\nAs you can see, that's really verbose! That's because in the library world, ressources are\ndescribed very precisely, in order to be \"machine-readable\" (MARC stands for \"MAchine-Readable\nCataloging\").\n\nFor convinience, developpers usually represent MARC data in JSON:\n```json\n{\n  \"leader\": \"00815nam 2200289 a 4500\",\n  \"fields\": [\n    { \"tag\": \"001\", \"value\": \"ocm30152659\" },\n    { \"tag\": \"003\", \"value\": \"OCoLC\" },\n    { \"tag\": \"005\", \"value\": \"19971028235910.0\" },\n    { \"tag\": \"008\", \"value\": \"940909t19941994ilua 000 0 eng \" },\n    { \"tag\": \"010\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"92060871\" }] },\n    { \"tag\": \"020\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"0844257443\" }] },\n    { \"tag\": \"040\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"DLC\" }, { \"code\": \"c\", \"value\": \"DLC\" }, { \"code\": \"d\", \"value\": \"BKL\" }, { \"code\": \"d\", \"value\": \"UtOrBLW\" } ] },\n    { \"tag\": \"049\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"BKLA\" }] },\n    { \"tag\": \"099\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"822.33\" }, { \"code\": \"a\", \"value\": \"S52\" }, { \"code\": \"a\", \"value\": \"S7\" } ] },\n    { \"tag\": \"100\", \"indicator1\": \"1\", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"Shakespeare, William,\" }, { \"code\": \"d\", \"value\": \"1564-1616.\" } ] },\n    { \"tag\": \"245\", \"indicator1\": \"1\", \"indicator2\": \"0\", \"subfields\": [{ \"code\": \"a\", \"value\": \"Hamlet\" }, { \"code\": \"c\", \"value\": \"William Shakespeare.\" } ] },\n    { \"tag\": \"264\", \"indicator1\": \" \", \"indicator2\": \"1\", \"subfields\": [{ \"code\": \"a\", \"value\": \"Lincolnwood, Ill. :\" }, { \"code\": \"b\", \"value\": \"NTC Pub. Group,\" }, { \"code\": \"c\", \"value\": \"[1994]\" } ] },\n    { \"tag\": \"264\", \"indicator1\": \" \", \"indicator2\": \"4\", \"subfields\": [{ \"code\": \"c\", \"value\": \"©1994.\" }] },\n    { \"tag\": \"300\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"xiii, 295 pages :\" }, { \"code\": \"b\", \"value\": \"illustrations ;\" }, { \"code\": \"c\", \"value\": \"23 cm.\" } ] },\n    { \"tag\": \"336\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"text\" }, { \"code\": \"b\", \"value\": \"txt\" }, { \"code\": \"2\", \"value\": \"rdacontent.\" } ] },\n    { \"tag\": \"337\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"unmediated\" }, { \"code\": \"b\", \"value\": \"n\" }, { \"code\": \"2\", \"value\": \"rdamedia.\" } ] },\n    { \"tag\": \"338\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"volume\" }, { \"code\": \"b\", \"value\": \"nc\" }, { \"code\": \"2\", \"value\": \"rdacarrier.\" } ] },\n    { \"tag\": \"490\", \"indicator1\": \"1\", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"NTC Shakespeare series.\" }] },\n    { \"tag\": \"830\", \"indicator1\": \" \", \"indicator2\": \"0\", \"subfields\": [{ \"code\": \"a\", \"value\": \"NTC Shakespeare series.\" }] },\n    { \"tag\": \"907\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \".b108930609\" }] },\n    { \"tag\": \"948\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"LTI 2018-07-09\" }] },\n    { \"tag\": \"948\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"MARS\" }] }\n  ]\n}\n```\n\nBy looking at this JSON representation, we can see that the data is...\n- **Nested**: A MARC record contains many fields, and most of them contains multiple subfields.\n- **Dynamic**: Some fields are repeatable (\"264\" and \"948\"), and subfields too. The first\n    fields don't have subfields nor indicators (they're called *control fields*).\n- **Encapsulated**: The meaning of subfields depends on the field they're in (take a look at\n    the \"a\" subfield for example).\n\nAll those characteristics can be grouped into what we call: [**semi-structured\ndata**](#semi-structured-data).\n\u003e Semi-structured data is a form of structured data that does not obey the tabular structure of data models associated with relational databases or other forms of data tables, but nonetheless contains tags or other markers to separate semantic elements and enforce hierarchies of records and fields within the data. Therefore, it is also known as self-describing structure. - Wikipedia\n\nA perfect example of that is HTML documents. An HTML document contains different types of tags,\nwhich can nested with one and other. It wouldn't make sense to model HTML documents with tables\nand columns. Imagine having to access nested tags through joins, considering the fact that we\ncould potentially have hundreds of them on a single HTML document. That's why we usually store\nthis kind of data in a text field.\n\nIn our case, we're using JSON to represent MARC data. Luckily for us, we can store JSON\ndata directly in relational databases like Postgres or MySQL:\n\n```ruby\n# config/initializers/inflections.rb\nActiveSupport::Inflector.inflections(:en) do |inflect|\n  inflect.acronym \"MARC\"\nend\n```\n\n```bash\n\u003e rails g model marc/record leader:string fields:json\n\u003e rails db:migrate\n```\n\nWe can then create a MARC record like this:\n```ruby\nMARC::Record.create leader: \"00815nam 2200289 a 4500\", fields: [\n  { \"tag\": \"001\", \"value\": \"ocm30152659\" },\n  { \"tag\": \"003\", \"value\": \"OCoLC\" },\n  { \"tag\": \"005\", \"value\": \"19971028235910.0\" },\n  { \"tag\": \"008\", \"value\": \"940909t19941994ilua 000 0 eng \" },\n  { \"tag\": \"010\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"92060871\" }] },\n  { \"tag\": \"020\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"0844257443\" }] },\n  { \"tag\": \"040\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"DLC\" }, { \"code\": \"c\", \"value\": \"DLC\" }, { \"code\": \"d\", \"value\": \"BKL\" }, { \"code\": \"d\", \"value\": \"UtOrBLW\" } ] },\n  { \"tag\": \"049\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"BKLA\" }] },\n  { \"tag\": \"099\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"822.33\" }, { \"code\": \"a\", \"value\": \"S52\" }, { \"code\": \"a\", \"value\": \"S7\" } ] },\n  { \"tag\": \"100\", \"indicator1\": \"1\", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"Shakespeare, William,\" }, { \"code\": \"d\", \"value\": \"1564-1616.\" } ] },\n  { \"tag\": \"245\", \"indicator1\": \"1\", \"indicator2\": \"0\", \"subfields\": [{ \"code\": \"a\", \"value\": \"Hamlet\" }, { \"code\": \"c\", \"value\": \"William Shakespeare.\" } ] },\n  { \"tag\": \"264\", \"indicator1\": \" \", \"indicator2\": \"1\", \"subfields\": [{ \"code\": \"a\", \"value\": \"Lincolnwood, Ill. :\" }, { \"code\": \"b\", \"value\": \"NTC Pub. Group,\" }, { \"code\": \"c\", \"value\": \"[1994]\" } ] },\n  { \"tag\": \"264\", \"indicator1\": \" \", \"indicator2\": \"4\", \"subfields\": [{ \"code\": \"c\", \"value\": \"©1994.\" }] },\n  { \"tag\": \"300\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"xiii, 295 pages :\" }, { \"code\": \"b\", \"value\": \"illustrations ;\" }, { \"code\": \"c\", \"value\": \"23 cm.\" } ] },\n  { \"tag\": \"336\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"text\" }, { \"code\": \"b\", \"value\": \"txt\" }, { \"code\": \"2\", \"value\": \"rdacontent.\" } ] },\n  { \"tag\": \"337\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"unmediated\" }, { \"code\": \"b\", \"value\": \"n\" }, { \"code\": \"2\", \"value\": \"rdamedia.\" } ] },\n  { \"tag\": \"338\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"volume\" }, { \"code\": \"b\", \"value\": \"nc\" }, { \"code\": \"2\", \"value\": \"rdacarrier.\" } ] },\n  { \"tag\": \"490\", \"indicator1\": \"1\", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"NTC Shakespeare series.\" }] },\n  { \"tag\": \"830\", \"indicator1\": \" \", \"indicator2\": \"0\", \"subfields\": [{ \"code\": \"a\", \"value\": \"NTC Shakespeare series.\" }] },\n  { \"tag\": \"907\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \".b108930609\" }] },\n  { \"tag\": \"948\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"LTI 2018-07-09\" }] },\n  { \"tag\": \"948\", \"indicator1\": \" \", \"indicator2\": \" \", \"subfields\": [{ \"code\": \"a\", \"value\": \"MARS\" }] }\n]\n```\n\nAnd access it this way:\n```ruby\n\u003e record = MARC::Record.first\n\u003e field = record.fields.find { |field| field[\"tag\"] == \"245\" }\n\u003e subfield = field[\"subfields\"].first\n\u003e subfield[\"value\"]\n=\u003e \"Hamlet\"\n```\nIt works, but...\n- It's not very convinient to access nested data this way.\n- We cannot easily attach logic to our JSON data without polluting our model.\n\nWhat if we could interact with our JSON data the same way we do with ActiveRecord associations\n? Enters ActiveModel and the\n[AttributesAPI](https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute)\n!\n\nFirst, we have to define a custom type which...\n- Maps JSON objects to ActiveModel-compliant objects.\n- Handles collections.\n\nTo do that, we'll add the following options to our type:\n- `:class_name`: The class name of an ActiveModel-compliant object.\n- `:collection`: Specify if the attribute is a collection. Default to `false`.\n\n```ruby\nclass DocumentType \u003c ::ActiveModel::Type::Value\n  attr_reader :document_class, :collection\n\n  def initialize(class_name:, collection: false)\n    @document_class = class_name.constantize\n    @collection     = collection\n  end\n\n  def cast(value)\n    if collection\n      value.map { |attributes| process attributes }\n    else\n      process value\n    end\n  end\n\n  def process(value)\n    document_class.new(value)\n  end\n\n  def serialize(value)\n    value.to_json\n  end\n\n  def deserialize(json)\n    value = ActiveSupport::JSON.decode(json)\n\n    cast value\n  end\n\n  # Track changes\n  def changed_in_place?(old_value, new_value)\n    deserialize(old_value) != new_value\n  end\nend\n```\nLet's register our type as we gonna use it multiple times:\n```ruby\n# config/initializers/type.rb\nActiveModel::Type.register(:document, DocumentType)\nActiveRecord::Type.register(:document, DocumentType)\n```\n\nNow we can use it in our models:\n```ruby\nclass MARC::Record \u003c ApplicationRecord\n  attribute :fields, :document,\n    class_name: \"MARC::Record::Field\",\n    collection: true\n\n  # Hash-like reader method\n  def [](tag)\n    occurences = fields.select { |field| field.tag == tag }\n    occurences.first unless occurences.count \u003e 1\n  end\nend\n```\n```ruby\nclass MARC::Record::Field\n  include ActiveModel::Model\n  include ActiveModel::Attributes\n  include ActiveModel::Serializers::JSON\n\n  attribute :tag, :string\n  attribute :indicator1, :string\n  attribute :indicator2, :string\n  attribute :subfields, :document,\n    class_name: \"MARC::Record::Field::Subfield\",\n    collection: true\n\n  attribute :value, :string\n\n  # Some domain logic\n  def value=(value)\n    @value = value if control_field?\n  end\n\n  def control_field?\n    /00\\d/ === tag\n  end\n\n  # Yet another Hash-like reader method\n  def [](code)\n    occurences = subfields.find { |subfield| subfield.code == code }\n    occurences.first unless occurences.count \u003e 1\n  end\n\n  # Used to track changes\n  def ==(other)\n    attributes == other.attributes\n  end\nend\n```\n```ruby\nclass MARC::Record::Field::Subfield\n  include ActiveModel::Model\n  include ActiveModel::Attributes\n  include ActiveModel::Serializers::JSON\n\n  attribute :code, :string\n  attribute :value, :string\n\n  # Used to track changes\n  def ==(other)\n    attributes == other.attributes\n  end\nend\n```\n```ruby\n\u003e record = MARC::Record.first\n\u003e record.fields.first.class\n=\u003e MARC::Record::Field\n\n\u003e record.fields.first.control_field?\n=\u003e true\n\n\u003e record.fields.first.subfields.first.class\n=\u003e MARC::Record::Field::Subfield\n\n\u003e record[\"245\"][\"a\"].value\n=\u003e \"Hamlet\"\n\n\u003e record.changed?\n=\u003e false\n\n\u003e record[\"245\"][\"a\"].value = \"Romeo and Juliet\"\n\u003e record[\"245\"][\"a\"].value\n=\u003e \"Romeo and Juliet\"\n\n\u003e record.changed?\n=\u003e true\n```\n\nEt voilà ! Home-made associations !\n\nIf we want to go further, we can...\n- Create our custom collection class to provide functionalities like ActiveRecord\n    collection proxies.\n- Add support for nested attributes.\n- Emulate persistence to update specific objects.\n- Provide a way to resolve constants, so that we can use the relative name of a constant\n    instead of it's full name. For example, `\"MARC::Record::Field\"` could be referred as\n    `\"Field\"` in our example.\n\nAnd that's what this extension does. (Nothing fancy, in fact the code is quite simple. So don't\nbe afraid to dive into it if you want to know how it was implemented !)\n\nHere's the updated version with the extension:\n```ruby\nclass MARC::Record \u003c ApplicationRecord\n  include ActiveModel::Embedding::Associations\n\n  embeds_many :fields\n\n  # ...\nend\n```\n```ruby\nclass MARC::Record::Field\n  include ActiveModel::Embedding::Document\n\n  # ...\n\n  embeds_many :subfields\n\n  # ...\nend\n```\n```ruby\nclass MARC::Record::Field::Subfield\n  include ActiveModel::Embedding::Document\n\n  # ...\nend\n```\n\nWe can then use our embedded associations in the views as nested attributes:\n```erb\n# app/views/marc/records/_form.html.erb\n\u003c%= form_with model: @record do |record_form| %\u003e\n  \u003c% @record.fields.each do |field| %\u003e\n    \u003c%= record_form.fields_for :fields, field do |field_fields| %\u003e\n\n      \u003c%= field_fields.label :tag %\u003e\n      \u003c%= field_fields.text_field :tag %\u003e\n\n      \u003c% if field.control_field? %\u003e\n        \u003c%= field_fields.text_field :value %\u003e\n      \u003c% else %\u003e\n        \u003c%= field_fields.text_field :indicator1 %\u003e\n        \u003c%= field_fields.text_field :indicator2 %\u003e\n\n        \u003c%= field_fields.fields_for :subfields do |subfield_fields| %\u003e\n          \u003c%= subfield_fields.label :code %\u003e\n          \u003c%= subfield_fields.text_field :code %\u003e\n          \u003c%= subfield_fields.text_field :value %\u003e\n        \u003c% end %\u003e\n      \u003c% end %\u003e\n    \u003c% end %\u003e\n  \u003c% end %\u003e\n\n  \u003c%= record_form.submit %\u003e\n\u003c% end %\u003e\n```\n\n\n## Concepts\n### Document\nA JSON object mapped to a PORO which includes `ActiveModel::Embedding::Document`. Usually part of a\n[collection](#collection).\n\n### Collection\nA JSON array mapped to an `ActiveModel::Embedding::Collection` (or any class that includes\n`ActiveModel::Embedding::Collecting`). Stores collections of\n[documents](#document).\n\n### Embedded associations\nModels structural hierarchies in [semi-structured data](#semi-structured-data), by \"embedding\"\nthe content of children directly in the parent, instead of using references like foreign keys. See\n[Embedded Data\nModels](https://docs.mongodb.com/manual/core/data-model-design/#embedded-data-models) from\nMongoDB's docs.\n\n### Semi-structured data\nData that don't fit in the [relational model](https://www.digitalocean.com/community/tutorials/what-is-the-relational-model).\n\u003e Semi-structured data is a form of structured data that does not obey the tabular structure of data models associated with relational databases or other forms of data tables, but nonetheless contains tags or other markers to separate semantic elements and enforce hierarchies of records and fields within the data. Therefore, it is also known as self-describing structure. - Wikipedia\n\n\n## Components\n### `ActiveModel::Type::Document`\nA polymorphic cast type (registered as `:document`). Maps JSON objects to POROs that includes\n`ActiveModel::Embedding::Document`. Provides support for defining [collections](#collection).\n\n### `ActiveModel::Embedding::Associations`\nAPI for defining [embedded associations](#embedded-associations). Uses the Attributes API with\nthe `:document` type.\n\n### `ActiveModel::Embedding::Document`\nA mixin which includes everything needed to work with the `:document` type\n(`ActiveModel::Model`, `ActiveModel::Attributes`, `ActiveModel::Serializers::JSON`,\n`ActiveModel::Embedding::Associations`). Provides an `id` attribute and implements methods like `#persisted?`\nand `#save` to emulate persistence.\n\n### `ActiveModel::Embedding::Collecting`\nA mixin which provides capabailities similar to ActiveRecord collection proxies. Provides\nsupport for nested attributes.\n\n### `ActiveModel::Embedding::Collection`\nDefault collection class. Includes `ActiveModel::Embedding::Collecting`.\n\n## Installation\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'activemodel-embedding'\n```\n\nAnd then execute:\n```bash\n$ bundle\n```\n\nOr install it yourself as:\n```bash\n$ gem install activemodel-embedding\n```\n\n## License\nThe gem is available as open source under the terms of the [MIT\nLicense](https://opensource.org/licenses/MIT).\n\n## Alternatives\nHere's some alternatives I came accross after I've started working on this gem:\n- [attr_json](https://github.com/jrochkind/attr_json)\n- [store_model](https://github.com/DmitryTsepelev/store_model)\n\nEach one uses a different approach to solve the same problem.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmansakondo%2Factivemodel-embedding","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmansakondo%2Factivemodel-embedding","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmansakondo%2Factivemodel-embedding/lists"}