{"id":17159622,"url":"https://github.com/jvillarejo/embedded","last_synced_at":"2025-04-13T13:26:29.166Z","repository":{"id":56844540,"uuid":"101303222","full_name":"jvillarejo/embedded","owner":"jvillarejo","description":"Rails/Activerecord plugin to make value objects embedded into active record objects","archived":false,"fork":false,"pushed_at":"2019-06-12T18:21:38.000Z","size":60,"stargazers_count":46,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-27T04:33:12.900Z","etag":null,"topics":["activerecord","embedded","rails","ruby"],"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/jvillarejo.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2017-08-24T14:20:48.000Z","updated_at":"2024-10-07T17:04:47.000Z","dependencies_parsed_at":"2022-09-09T04:11:37.247Z","dependency_job_id":null,"html_url":"https://github.com/jvillarejo/embedded","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jvillarejo%2Fembedded","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jvillarejo%2Fembedded/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jvillarejo%2Fembedded/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jvillarejo%2Fembedded/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jvillarejo","download_url":"https://codeload.github.com/jvillarejo/embedded/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248719529,"owners_count":21150754,"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","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":["activerecord","embedded","rails","ruby"],"created_at":"2024-10-14T22:14:43.904Z","updated_at":"2025-04-13T13:26:29.138Z","avatar_url":"https://github.com/jvillarejo.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Embedded\n\nEmbedded is a small rails engine to correctly persist Value Objects in Active Record Object columns\n\n## Code Status\n[![Build Status](https://travis-ci.org/jvillarejo/embedded.svg?branch=master)](https://travis-ci.org/jvillarejo/embedded)\n\nEmbedded supports and was tested against this Ruby and Rails versions:\n\n* Ruby 2.1.5 and Rails 3.2 (it's a shame but I have a legacy project)\n* Ruby 2.4 and Rails 4.2\n* Ruby 2.4 and Rails 5.1\n* Ruby 2.4 and Rails 5.2\n* Ruby 2.5 and Rails 4.2\n* Ruby 2.5 and Rails 5.1\n* Ruby 2.5 and Rails 5.2\n\n## Motivation\n\nThere are objects in every domain that don't have an identity by themselves but in which their equality depends on the values of their attributes.\n\nExample: prices, any magnitude, a color, a polygon.\n\nDefining a value object lets you extract common behavior from your current bloated active record objects.\n\nEvery time I did this, I had to define a getter and a setter for the value object, and map those to the columns of the object that gets persisted, so I thought that it would be better to define those value object attributes in a declarative way and let the plugin do the magic behind.\n\nFor more info about value objects check this links:\n\n* [Value Object by Martin Fowler](https://martinfowler.com/bliki/ValueObject.html)\n* [Don't forget about Value Objects](https://plainoldobjects.com/2017/03/19/dont-forget-about-value-objects)\n\n### What about ActiveRecord composed_of API ?\n\nSincerly at the time of written this I didn't know about ActiveRecord [composed_of](http://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html) API. \n\nI feel embarrassed about this because I usually read the Rails code, maybe my mind forgot about it. Thanks to a very good samaritan on Reddit that pointed me to it. \n\nAs I take a very good look to composed_of there are some things that make me noise: \n* A lack of conventions, you have to configure everything in it.\n* The mapping is defined as an array of arrays instead of a simple hash as embedded. \n* That Rails dev team tried remove and then reverted it. [PR 6743](https://github.com/rails/rails/pull/6743)\n\nSo I'm thinking to still use and maintain this gem, but I will improve it to use composed_of under the hood as it's not a good practice to duplicate functionalities in a system. And if the Rails dev team remove the composed_of API in a future, I can maintain it with this gem too. \n\n## Features\n\nIt lets you define value objects and map them into the corresponding value object attributes columns\n\nIt lets you query by those value objects in a safe way, without monkeypatching the default activerecord classes\n\n## Installation\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'embedded'\n```\n\nCreate an initializer in your rails project\n\n```ruby\n# config/initializers/embedded_initializer\nActiveRecord::Base.send(:extend, Embedded::Model)\n```\n\nOr you can extend the ApplicationRecord class\n```ruby\nclass ApplicationRecord \u003c ActiveRecord::Base\n  extend Embedded::Model\n  self.abstract_class = true\nend\n```\n\n\n## Usage\n\nLet's say you have a Reservation in your active record model and that it has a start_time and an end_time. And that you want to calculate the duration in hours of the period.\n\n```ruby\n  class Reservation \u003c ApplicationRecord\n\n    def period_in_hours\n      (end_time - start_time).round / 60 / 60\n    end\n  end\n```\n\n```ruby\n  reservation = Reservation.new(start_time: Time.now, end_time: 3.hours.ago)\n  reservation.period_in_hours\n  # =\u003e 3\n```\n\n If you want your model to have cohesion, something is not quite right when a reservation is calculating time intervals of a period, but let's keep that for a while.\n\n You have a new requirement, you need to persist available hours for a shop, and you want to calculate the duration in hours of the available time\n\n ```ruby\n   class Shop \u003c ApplicationRecord\n      def opening_period_in_hours\n        (open_time - closed_time).round / 60 / 60\n      end\n   end\n ```\n\n ```ruby\n  shop = Shop.new(start_time: Time.now, end_time: 3.hours.ago)\n  shop.period_in_hours\n  # =\u003e 3\n```\n\nNow you are starting to see the problem. That behavior belongs to a TimeInterval object that has a start_time an end_time and let's you calculate all the durations and intervals you want.\n\nSo with embedded in hand we can do this.\n\nWe have a reservation that has an attribute scheduled_time of type TimeInterval and will map the start_time and end_time attributes to the ones in TimeInterval\n\n```ruby\nclass Reservation \u003c ApplicationRecord\n  embeds :scheduled_time, attrs: [:start_time, :end_time], class_name: 'TimeInterval'\nend\n```\n\nThe same here with the shop\n\n```ruby\nclass Shop \u003c ApplicationRecord\n  embeds :available_time, attrs: [:start_time, :end_time], class_name: 'TimeInterval'\nend\n```\n\nTimeInterval is a plain PORO, it just need the attributes that you defined in your activerecord objects mapping.\n\n```ruby\n  class TimeInterval\n    attr_reader :start_time, :end_time\n\n    def initialize(values)\n      @start_time = values.fetch(:start_time)\n      @end_time = values.fetch(:end_time)\n\n      # you can validate as you want, here or in a valid? method that you define\n    end\n\n    def hours\n      minutes / 60\n    end\n\n    def minutes\n      seconds / 60\n    end\n\n    def seconds\n      (end_time - start_time).round\n    end\n  end\n```\n\nNow you can pass available time to shop constructor and check the duration directly\n ```ruby\n  t = TimeInterval.new(start_time: Time.now, end_time: 3.hours.ago)\n  shop = Shop.new(available_time: t)\n  shop.available_time.hours\n  # =\u003e 3\n```\nAlso you can persist the reservation, and when fetching it back from the db its scheduled_time will be a TimeInterval\n\n```ruby\n  t = TimeInterval.new(start_time: Time.now, end_time: 3.hours.ago)\n  reservation = Reservation.create(scheduled_time: t)\n\n  reservation.reload\n\n  reservation.scheduled_time.hours\n  # =\u003e 3\n```\n\nEmbedded also supports passing a hash to the setter, so you can still use ```fields_for``` in form views and also use request params to create activerecord model objects.\n\n```ruby\n  reservation = Reservation.create(scheduled_time: {\n    start_time: Time.now,\n    end_time: 3.hours.ago\n  })\n\n  reservation.reload\n\n  reservation.scheduled_time.hours\n  # =\u003e 3\n```\n\n### Database Mapping\n\nThe default convention column mapping is the value object name as prefix and the value object attribute as suffix.\n\nExample:\nIf Reservation attribute name is scheduled_time and its TimeInterval has start_time and end_time attributes, your column names should be defined as followed:\n\n```ruby\nclass CreateReservations \u003c ActiveRecord::Migration\n  def change\n    create_table :reservations do |t|\n      t.timestamp :scheduled_time_start_time\n      t.timestamp :scheduled_time_end_time\n\n      t.timestamps\n    end\n  end\nend\n```\n\nShop attribute name is available time, and its TimeInterval has start_time and end_time attributes. Your column names here must be like this:\n\n```ruby\nclass CreateShops \u003c ActiveRecord::Migration\n  def change\n    create_table :shops do |t|\n      t.timestamp :available_time_start_time\n      t.timestamp :available_time_end_time\n\n      t.timestamps\n    end\n  end\nend\n```\n\nWe can override this convetion if you pass attrs argument as hash where you define the mapping. \n\nExample: \n```ruby\nclass PersonalDocument\n  attr_reader :number, :type\n\n  def initialize(values = {})\n    @number = values.fetch(:number)\n    @type = values.fetch(:type)\n  end\n\n  def ==(other)\n    return false if !other.is_a?(PersonalDocument)\n\n    @number == other.number \u0026\u0026 @type == other.type\n  end\nend\n\nclass Person \u003c ApplicationRecord\n  embeds :identification, attrs: { \n    number: :id_number, \n    type: :id_type\n  }, class_name: 'PersonalDocument'\nend\n\nclass CreatePeople \u003c ActiveRecord::Migration\n  def change\n    create_table :people do |t|\n      t.string :id_number\n      t.string :id_type\n\n      t.timestamps\n    end\n\n    add_index :people, :id_number\n    add_index :people, :id_type\n  end\nend\n```\n\n### Querying\n\nFor example you have now a model that has prices in different currencies. \n\n```ruby\nprice = Price.new(currency: 'BTC', amount: BigDecimal.new('2.5'))\nmy_gamble = BuyOrder.create(price: price, created_at: Time.new(2015,03,17))\n\nbubble_price = Price.new(currency: 'USD', amount: BigDecimal.new('5257'))\nmy_intelligent_investment = SellOrder.create(price: price, created_at: Time.new(2017,10,18))\n```\n\nAnd if we want to check the orders for a specific price we can do it like this:\n\n```ruby\nprice = Price.new(currency: 'BTC', amount: BigDecimal.new('2.5'))\ngambles = BuyOrder.embedded.where(price: price).to_a\n\n# =\u003e [#\u003cOrder id: 1, price_currency: \"BTC\", price_amount: #\u003cBigDecimal:555e61776630,'0.25E1',18(36)\u003e, created_at: \"2017-03-17 17:11:00\", updated_at: \"2017-10-18 17:11:00\"\u003e]\n```\n\nIn order to search with value objects you should use embedded method. This decision was made because I didn't want to monkey patch the activerecord method 'where'.\n\nThis way the embedded method returns another scope in which the method 'where' is overridden. If you want to query by the column attributes you can still use the default 'where' method. \n\n```ruby\njpm_orders = BuyOrder.where(price_currency: 'BTC')\njpm_orders.find_each {|o| o.trader.fire! }\n``` \n\n## Contributing\n\nEveryone is encouraged to help to improve this project. Here are a few ways you can help:\n\n- [Report bugs](https://github.com/jvillarejo/embedded/issues)\n- Fix bugs and [submit pull requests](https://github.com/jvillarejo/embedded/pulls)\n- Suggest or add new features\n\n## Maintainer\n\nJuani Villarejo \u003ccontact@jonvillage.com\u003e\n\n## License\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjvillarejo%2Fembedded","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjvillarejo%2Fembedded","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjvillarejo%2Fembedded/lists"}