{"id":15605695,"url":"https://github.com/gangelo/deco_lite","last_synced_at":"2025-04-30T08:21:28.784Z","repository":{"id":54466422,"uuid":"519741469","full_name":"gangelo/deco_lite","owner":"gangelo","description":"DecoLite is a little gem that allows you to use the provided DecoLite::Model class to dynamically create Decorator class objects. Use the DecoLite::Model class directly, or inherit from the DecoLite::Model class to create your own unique subclasses with custom functionality. DecoLite::Model includes ActiveModel::Model, so validation can be applied using ActiveModel validation helpers you're familiar with; or, you can roll your own - just like any other ActiveModel.  DecoLite::Model allows you to consume a Ruby Hash that you supply via the initializer (DecoLite::Model#new) or via the DecoLite::Model#load! method. Any number of Ruby Hashes can be consumed. Your supplied Ruby Hashes are used to create attr_accessor attributes (or \"fields\") on the model. Each attribute created is then assigned the value from the Hash that was loaded. Again, any number of hashes can be consumed using the DecoLite::Model#load! method.","archived":false,"fork":false,"pushed_at":"2024-11-04T21:08:27.000Z","size":211,"stargazers_count":5,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-30T14:21:49.854Z","etag":null,"topics":["decorator","decorator-pattern","decorators","gem","rails-gem","ruby","ruby-gem","ruby-gems"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/dsu","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/gangelo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-31T10:15:05.000Z","updated_at":"2024-08-10T00:12:32.000Z","dependencies_parsed_at":"2024-01-07T18:21:48.562Z","dependency_job_id":"2e02640c-03ec-434c-9fa9-83cc587f64d5","html_url":"https://github.com/gangelo/deco_lite","commit_stats":{"total_commits":125,"total_committers":1,"mean_commits":125.0,"dds":0.0,"last_synced_commit":"12a0de270b3ad5d98f487d6af5131b373a4c45ab"},"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gangelo%2Fdeco_lite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gangelo%2Fdeco_lite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gangelo%2Fdeco_lite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gangelo%2Fdeco_lite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gangelo","download_url":"https://codeload.github.com/gangelo/deco_lite/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251666679,"owners_count":21624351,"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":["decorator","decorator-pattern","decorators","gem","rails-gem","ruby","ruby-gem","ruby-gems"],"created_at":"2024-10-03T04:11:33.310Z","updated_at":"2025-04-30T08:21:28.763Z","avatar_url":"https://github.com/gangelo.png","language":"Ruby","readme":"# DecoLite\r\n\r\n[![Ruby](https://github.com/gangelo/deco_lite/actions/workflows/ruby.yml/badge.svg)](https://github.com/gangelo/deco_lite/actions/workflows/ruby.yml)\r\n[![GitHub version](http://badge.fury.io/gh/gangelo%2Fdeco_lite.svg?refresh=7)](https://badge.fury.io/gh/gangelo%2Fdeco_lite)\r\n[![Gem Version](https://badge.fury.io/rb/deco_lite.svg?refresh=7)](https://badge.fury.io/rb/deco_lite)\r\n[![](http://ruby-gem-downloads-badge.herokuapp.com/deco_lite?type=total)](http://www.rubydoc.info/gems/deco_lite/)\r\n[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/deco_lite/)\r\n[![Report Issues](https://img.shields.io/badge/report-issues-red.svg)](https://github.com/gangelo/deco_lite/issues)\r\n[![License](http://img.shields.io/badge/license-MIT-yellowgreen.svg)](#license)\r\n\r\n## Introduction\r\n\r\n_DecoLite_ is a little gem that allows you to use the provided `DecoLite::Model` class to dynamically create Decorator class objects. Use the `DecoLite::Model` class directly, or inherit from the `DecoLite::Model` class to create your own unique subclasses with custom functionality. `DecoLite::Model` includes `ActiveModel::Model`, so validation can be applied using [ActiveModel validation helpers](https://api.rubyonrails.org/v6.1.3/classes/ActiveModel/Validations/HelperMethods.html) you're familiar with; or, you can roll your own - just like any other `ActiveModel`.\r\n\r\n`DecoLite::Model` allows you to consume a Ruby `Hash` that you supply via the initializer (`DecoLite::Model#new`) or via the `DecoLite::Model#load!` method. Any number of Ruby `Hashes` can be consumed. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (or _\"fields\"_) on the model. Each attribute created is then assigned the value from the Hash that was loaded. Again, any number of hashes can be consumed using the `DecoLite::Model#load!` method.\r\n\r\n`attr_accessors` created during initialization, or by calling `DecoLite::Model#load!`, are _mangled_ to include namespacing. This allows `DecoLite` to create _unique_ attribute names for nested Hashes that may have non-unique key names. For example:\r\n\r\n```ruby\r\n# NOTE: keys :name and :age are not unique across this Hash.\r\nfamily = {\r\n  # :name and :age are not unique\r\n  name: 'John Doe',\r\n  age: 35,\r\n  wife: {\r\n    # :name and :age are not unique\r\n    name: 'Mary Doe',\r\n    age: 30,\r\n  }\r\n}\r\n```\r\n\r\nGiven the above example, `DecoLite` will produce the following _unique_ `attr_accessors` on the `DecoLite::Model` object, and assign the values:\r\n\r\n```ruby\r\n# Instead of the below, you can also use DecoLite::Model.new.load!(hash: family)\r\nmodel = DecoLite::Model.new(hash: family)\r\n\r\nmodel.name #=\u003e 'John Doe'\r\nmodel.age #=\u003e 35\r\n\r\nmodel.wife_name #=\u003e 'Mary Doe'\r\nmodel.wife_age #=\u003e 30\r\n```\r\n\r\nIn the above example, notice how `DecoLite` _mangles_ attributes `:wife_name` and `:wife_age` using the `:wife` `Hash` key name to make them unique.\r\n\r\n`DecoLite::Model#load!` can be called _multiple times_, on the same model using different `Hashes`. This could potentially cause `attr_accessor` name clashes. In order to ensure unique `attr_accessor` names, a _\"namespace\"_ may be _explicitly_ provided to ensure attribute name uniqueness.\r\n\r\nFor example, **continuing from the previous example,** if we were to call `DecoLite::Model#load!` a _second time_ with the following `Hash`, this would produce `attr_accessor` name clashes which would raise errors, because `:name` and `:age` attributes already exist on the `DecoLite::Model` in question:\r\n\r\n```ruby\r\ngrandpa = {\r\n  name: 'Henry Doe',\r\n  age: 85,\r\n}\r\n```\r\n\r\nTo handle the above scenario, `DecoLite::Model` allows you to pass a `:namespace` option (for example `namespace: :grandpa`) to the `DecoLite::Model#load!` method; this would produce the following `attr_accessors`, ensuring their uniqueness:\r\n\r\n```ruby\r\nmodel.load!(hash: grandpa, options: { namespace: :grandpa })\r\n\r\nmodel.grandpa_name #=\u003e 'Henry Doe'\r\nmodel.grandpa_age #=\u003e 85\r\n\r\nmodel.name #=\u003e 'John Doe'\r\nmodel.age #=\u003e 35\r\n\r\nmodel.wife_name #=\u003e 'Mary Doe'\r\nmodel.wife_age #=\u003e 30\r\n```\r\n\r\n### More examples and usage\r\n\r\nFor more examples and usage, see the [Examples and usage](#examples-and-usage) and [More examples and usage](#more-examples-and-usage) sections; there is also an \"I want to...\" section with examples of things you may want to accomplish when using `DecoLite`.\r\n\r\n## Use cases\r\n\r\n### Generally Speaking\r\n\r\n`DecoLite` would _most likely_ thrive where the structure of the `Hashe(s)` consumed are (of course) known, relatively small to moderate in size, and not _terribly_ deep nested-hash-wise. This is because of the way `DecoLite` mangles loaded Hash key names to create unique `attr_accessors` on the model (see the Introduction section). However, I'm sure there are some geniuses out there that would find other contexts where `DecoLite` may thrive. Assuming the former is the case, `DecoLite` would be ideal to consume Model attributes, Webservice JSON results (converted to Ruby `Hash`), JSON Web Token (JWT) payloads, etc. to create a cohesive data model to be used in any scenario.\r\n\r\n### Rails\r\n\r\nBecause `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a _decorator pattern_ might be used, and decorator methods provided for use in Rails views; for example:\r\n\r\n```ruby\r\nclass ViewModel \u003c DecoLite::Model\r\n  validates :first, :last, presence: true\r\n\r\n  def salutation\r\n    \"\u003cspan class='happy'\u003eHello \u003cem\u003e#{full_name}\u003cem\u003e, welcome back!\u003c/span\u003e\"\r\n  end\r\n\r\n  def full_name\r\n    \"#{first} #{last}\"\r\n  end\r\nend\r\n\r\nview_model = ViewModel.new(hash: { first: 'John', last: 'Doe' })\r\n\r\nview_model.valid?\r\n#=\u003e true\r\n\r\nview_model.full_name\r\n=\u003e \"John Doe\"\r\n\r\nview_model.salutation\r\n=\u003e \"\u003cspan class='happy'\u003eHello \u003cem\u003eJohn Doe\u003cem\u003e, welcome back!\u003c/span\u003e\"\r\n```\r\n\r\n### Etc., etc., etc.\r\n\r\nGet creative. Please pop me an email and let me know how _you're_ using `DecoLite`.\r\n\r\n## Examples and usage\r\n\r\n```ruby\r\nrequire 'deco_lite'\r\n\r\nhusband = {\r\n  name: 'John Doe',\r\n  info: {\r\n    age: 21,\r\n    address: '1 street, boonton, nj 07005',\r\n    salary: 100_000,\r\n  },\r\n}\r\n\r\nwife = {\r\n  name: 'Mary Doe',\r\n  info: {\r\n    age: 20,\r\n    address: '1 street, boonton, nj 07005',\r\n    salary: 90_000,\r\n  },\r\n}\r\n\r\nclass Couple \u003c DecoLite::Model\r\n  def live_together?\r\n    husband_info_address == wife_info_address\r\n  end\r\n\r\n  def bread_winner\r\n    case\r\n    when husband_info_salary \u003e wife_info_salary\r\n      husband_name\r\n    when husband_info_salary \u003c wife_info_salary\r\n      wife_name\r\n    else\r\n      \"#{husband_name} and #{wife_name} make the same amount\"\r\n    end\r\n  end\r\nend\r\n\r\ncouple = Couple.new\r\n\r\ncouple.load!(hash: husband, options: { namespace: :husband })\r\ncouple.load!(hash: wife, options: { namespace: :wife })\r\n\r\n# Will produce the following:\r\nmodel.live_together?        #=\u003e true\r\nmodel.bread_winner          #=\u003e John Doe\r\n\r\nmodel.husband_name          #=\u003e John Doe\r\nmodel.husband_info_age      #=\u003e 21\r\nmodel.husband_info_address  #=\u003e 1 street, boonton, nj 07005\r\n\r\nmodel.wife_name             #=\u003e Amy Doe\r\nmodel.wife_info_age         #=\u003e 20\r\nmodel.wife_info_address     #=\u003e 1 street, boonton, nj 07005\r\n```\r\n\r\n## More examples and usage\r\n\r\n### I want to...\r\n\r\n#### Add validators to my model\r\n\r\nSimply add your `ActiveModel` validators just like you would any other `ActiveModel::Model` validator, with one caveat noted below. It is important to note that any attribute (field) having an _explicit validation_ associated with it, will automatically cause `DecoLite` to create an `attr_accessor` for that field; this is to avoid `NoMethodErrors` when validating the model (e.g. `#valid?`, `#validate`, etc.) _before_ the data is loaded. Why does `DecoLite` need to do this? Typically, `DecoLite` dynamically creates `attr_accessors` using the keys from the `Hash` loaded into the model. If the `Hash` loaded into your `DecoLite` model _does not_ include a `Hash` key for the attribute referenced by any validators on your model, `DecoLite` will not create an `attr_accessor` for it; consequently, calling any validation method (e.g. `#valid?`, `#validate`, etc.) on your model will result in a `NoMethodError` for that attribute.\r\n\r\nOne caveat to note is when using Rails custom validators with `validates_with`. When using Rails custom validators via `validates_with`, you should pass the attribute names being validated to your custom validator via the `#options` `Hash` with a key of either `:attributes` or `:fields`. This is so that `DecoLite` can create dynamic `attr_accessors` for these attributes and avoid the aformentioned `NoMethodError` (see above):\r\n\r\n```ruby\r\nclass Model \u003c DecoLite::Model\r\n  validates :first, :last, :address, presence: true\r\n  validates :age, numericality: true\r\n  # When using Rails custom validators via validates_with,\r\n  # pass the attribute name(s) being validated in an Array\r\n  # via the #options Hash, with a key of either :attributes\r\n  # or :fields. For example:\r\n  validates_with CustomFirstNameValidator, attributes: [:first]\r\n  validates_with CustomAgeValidator, fields: [:age]\r\nend\r\n\r\n# :address is missing\r\nmodel = Model.new(hash: { first: 'John', last: 'Doe', age: 25 })\r\nmodel.respond_to? :address\r\n#=\u003e true\r\n\r\nmodel.valid?\r\n#=\u003e false\r\nmodel.errors.full_messages\r\n#=\u003e [\"Address can't be blank\"]\r\n\r\nmodel.load!(hash: { address: '123 park road, anytown, nj 01234' })\r\nmodel.valid?\r\n#=\u003e true\r\n```\r\n\r\n#### Validate whether or not certain fields were loaded\r\n\r\nTo be clear, this example does not validate the _data_ associated with the fields loaded; rather, this example validates whether or not the _fields themselves_ were loaded into your model, and as a result, `attr_accessors` created _for_ them on the model. If you only want to validate the _data_ loaded into your model, simply use `ActiveModel` validations, just like you would any other `ActiveModel` model (see the [Add validators to my model](#add-validators-to-my-model) section).\r\n\r\nIf you want to validate whether or not particular _fields_ were loaded into your model, as a result of `#load!`ing data into your model, you need to add the required field names to the `DecoLite::Model#required_fields` attribute, or use inheritance:\r\n\r\n- Create a `DecoLite::Model` subclass.\r\n- Override the `DecoLite::Model#required_fields` method.\r\n- Return an Array of `Symbols` that represent the fields you want to validate (e.g. `%i[first last ssn]`).\r\n\r\nFor example:\r\n\r\n```ruby\r\nclass Model \u003c DecoLite::Model\r\n  # :age field is optional and it's value is optional.\r\n  validates :age, numericality: { only_integer: true }, allow_blank: true\r\n\r\n  def required_fields\r\n    # We want to ensure these fields were loaded.\r\n    %i[first last address]\r\n  end\r\nend\r\n\r\nmodel = Model.new\r\n\r\nmodel.validate\r\n#=\u003e false\r\n\r\nmodel.errors.full_messages\r\n#=\u003e [\"First field is missing\", \"Last field is missing\", \"Address field is missing\"]\r\n```\r\n\r\nIf we load data that includes :first, :last, and :address Hash keys, even with nil data, our `\":\u003cfield\u003e field is missing\"` errors would go away; in this scenario, we only wish to validate the _presence of the FIELDS,_ not the data associated with these fields!\r\n\r\n```ruby\r\nmodel.load!(hash: { first: nil, last: nil, address: nil })\r\n\r\nmodel.validate\r\n#=\u003e true\r\n\r\nmodel.errors.full_messages\r\n#=\u003e []\r\n\r\nuser = {\r\n  first: 'John',\r\n  last: 'Doe',\r\n  address: '123 anystreet, anytown, nj 01234',\r\n  age: 'x'\r\n}\r\n\r\nmodel.load!(hash: user)\r\n\r\nmodel.validate\r\n#=\u003e false\r\n\r\nmodel.errors.full_messages\r\n#=\u003e [\"Age is not a number\"]\r\n```\r\n\r\n#### Validate whether or not certain fields were loaded _and_ validate the data associated with these same fields\r\n\r\nIf you simply want to validate the _data_ loaded into your model, simply add `ActiveModel` validation, just like you would any other `ActiveModel` model (see the [Add validators to my model](#add-validators-to-my-model) section).\r\n\r\nIf you want to validate whether or not particular fields were loaded _and_ the field data associated with those same fields, you simply need to return the required fields from the `DecoLite#required_fields` method and add the appropriate validation(s); for example:\r\n\r\n```ruby\r\nclass Model \u003c DecoLite::Model\r\n  validates :first, :last, :address, :age, presence: true\r\n\r\n  def required_fields\r\n    %i[first last address age]\r\n  end\r\nend\r\n\r\nmodel = Model.new\r\n\r\nmodel.validate\r\n#=\u003e false\r\n\r\nmodel.errors.full_messages\r\n#=\u003e [\"First field is missing\",\r\n \"Last field is missing\",\r\n \"Address field is missing\",\r\n \"Age field is missing\",\r\n \"First can't be blank\",\r\n \"Last can't be blank\",\r\n \"Address can't be blank\",\r\n \"Age can't be blank\",\r\n \"Age is not a number\"]\r\n```\r\n\r\n#### Manually define attributes (fields) on my model\r\n\r\nManually defining attributes on your subclass is possible, although there doesn't seem a valid reason to do so, since you can just use `DecoLite::Model#load!` to wire all this up for you automatically. However, if there _were_ a need to do this, you must add your `attr_reader` to the `DecoLite::Model@field_names` array, or an error will be raised _provided_ there are any conflicting field names being loaded using `DecoLite::Model#load!`. Note that the aforementioned error will be raised regardless of whether or not you set `options: { fields: :merge }`. This is because `DecoLite` considers any existing model attributes _not_ added to the model via `load!`_to be native to the model object,_ and therefore will not allow you to create `attr_accessors` and assign values to existing model attributes because this can potentially be dangerous.\r\n\r\nTo avoid errors when manually defining model attributes that could potentially conflict with fields loaded using `DecoLite::Model#load!`, you could do the following:\r\n\r\n```ruby\r\nclass JustBecauseYouCanDoesntMeanYouShould \u003c DecoLite::Model\r\n  attr_accessor :existing_field\r\n\r\n  def initialize(hash: {}, options: {})\r\n    # Make sure we add existing_field to @field_names before we call\r\n    # the base class initializer.\r\n    @field_names = %i[existing_field]\r\n\r\n    super\r\n  end\r\nend\r\n\r\nmodel = JustBecauseYouCanDoesntMeanYouShould.new(hash: { existing_field: :value })\r\n\r\nmodel.existing_field\r\n#=\u003e :value\r\n```\r\n\r\nHowever, the above is unnecessary as this can be easily accomplished by passing a `Hash` to the initializer or by using `DecoLite::Model#load!`:\r\n\r\n```ruby\r\nmodel = Class.new(DecoLite::Model).new(hash:{ existing_field: :value })\r\n\r\nmodel.field_names\r\n#=\u003e [:existing_field]\r\n\r\nmodel.existing_field\r\n#=\u003e :value\r\n\r\nmodel.respond_to? :existing_field=\r\n#=\u003e true\r\n```\r\n\r\n## Installation\r\n\r\nAdd this line to your application's Gemfile:\r\n\r\n```ruby\r\ngem 'deco_lite'\r\n```\r\n\r\nAnd then execute:\r\n\r\n    $ bundle\r\n\r\nOr install it yourself as:\r\n\r\n    $ gem install deco_lite\r\n\r\n## Development\r\n\r\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\r\n\r\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\r\n\r\n## Contributing\r\n\r\nBug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/deco_lite. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\r\n\r\n## License\r\n\r\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\r\n\r\n## Code of Conduct\r\n\r\nEveryone interacting in the DecoLite project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/deco_lite/blob/master/CODE_OF_CONDUCT.md).\r\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgangelo%2Fdeco_lite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgangelo%2Fdeco_lite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgangelo%2Fdeco_lite/lists"}