{"id":13879934,"url":"https://github.com/davydovanton/shallow_attributes","last_synced_at":"2025-04-05T20:09:01.210Z","repository":{"id":56895255,"uuid":"53887480","full_name":"davydovanton/shallow_attributes","owner":"davydovanton","description":"Simple and lightweight Virtus analog.","archived":false,"fork":false,"pushed_at":"2023-04-18T17:52:34.000Z","size":111,"stargazers_count":100,"open_issues_count":8,"forks_count":18,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-04-25T17:21:42.894Z","etag":null,"topics":["data-object","ruby","virtus"],"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/davydovanton.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}},"created_at":"2016-03-14T19:58:31.000Z","updated_at":"2023-12-12T07:10:29.000Z","dependencies_parsed_at":"2024-01-30T06:01:37.921Z","dependency_job_id":"f01ee784-467a-423e-9659-c271fb455cc8","html_url":"https://github.com/davydovanton/shallow_attributes","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davydovanton%2Fshallow_attributes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davydovanton%2Fshallow_attributes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davydovanton%2Fshallow_attributes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davydovanton%2Fshallow_attributes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/davydovanton","download_url":"https://codeload.github.com/davydovanton/shallow_attributes/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247393572,"owners_count":20931813,"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":["data-object","ruby","virtus"],"created_at":"2024-08-06T08:02:39.705Z","updated_at":"2025-04-05T20:09:01.192Z","avatar_url":"https://github.com/davydovanton.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# ShallowAttributes\n\n[![Build Status](https://travis-ci.org/davydovanton/shallow_attributes.svg?branch=master)](https://travis-ci.org/davydovanton/shallow_attributes)\n[![Code Climate](https://codeclimate.com/github/davydovanton/shallow_attributes/badges/gpa.svg)](https://codeclimate.com/github/davydovanton/shallow_attributes)\n[![Coverage Status](https://coveralls.io/repos/github/davydovanton/shallow_attributes/badge.svg?branch=master)](https://coveralls.io/github/davydovanton/shallow_attributes?branch=master)\n[![Inline docs](http://inch-ci.org/github/davydovanton/shallow_attributes.svg?branch=master)](http://inch-ci.org/github/davydovanton/shallow_attributes)\n\nSimple and lightweight Virtus analog without any dependencies. [Documentation][doc-link].\n\n## Motivation\n\nThere are already a lot of good and flexible gems which solve a similar problem, allowing attributes\nto be defined with their types, for example: [virtus][virtus-link], [fast_attributes][fast-attributes-link]\nor [attrio][attrio-link]. However, the disadvantage of these gems is performance or API. So, the goal\nof `ShallowAttributes` is to provide a simple solution which is similar to the `Virtus` API, simple, fast,\nunderstandable and extendable.\n\n* This is [the performance benchmark][performance-benchmark] of ShallowAttributes compared to Virtus gems.\n* [Default ruby struct, dry-struct, virtus and ShallowAttributes ips and memory benchmarks](https://gist.github.com/IvanShamatov/94e78ca52f04f20c6085651345dbdfda)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n``` ruby\ngem 'shallow_attributes'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install shallow_attributes\n\n## Examples\n\n### Table of contents\n\n* [Using ShallowAttributes with Classes](#using-shallowattributes-with-classes)\n* [Default Values](#default-values)\n* [Mandatory Attributes](#mandatory-attributes)\n* [Embedded Value](#embedded-value)\n* [Custom Coercions](#custom-coercions)\n* [Collection Member Coercions](#collection-member-coercions)\n* [Note about Member Coercions](#important-note-about-member-coercions)\n* [Overriding setters](#overriding-setters)\n* [ActiveModel compatibility](#activemodel-compatibility)\n* [Dry-types](#dry-types)\n\n### Using ShallowAttributes with Classes\n\nYou can create classes extended with Virtus and define attributes:\n\n``` ruby\nclass User\n  include ShallowAttributes\n\n  attribute :name, String\n  attribute :age, Integer\n  attribute :birthday, DateTime\nend\n\nclass SuperUser \u003c User\n  include ShallowAttributes\n\n  attribute :name, String\n  attribute :age, Integer, allow_nil: true\n  attribute :birthday, DateTime\nend\n\nuser = User.new(name: 'Anton', age: 31)\nuser.name       # =\u003e \"Anton\"\n\nuser.age = '31' # =\u003e 31\nuser.age = nil  # =\u003e nil\nuser.age.class  # =\u003e Fixnum\n\nuser.birthday = 'November 18th, 1983' # =\u003e #\u003cDateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)\u003e\n\nuser.attributes # =\u003e { name: \"Anton\", age: 31, birthday: nil }\n\n# mass-assignment\nuser.attributes = { name: 'Jane', age: 21 }\nuser.name       # =\u003e \"Jane\"\nuser.age        # =\u003e 21\n\nsuper_user = SuperUser.new\nuser.age = nil  # =\u003e 0\n```\n\nShallowAttributes doesn't make any assumptions about base classes. There is no need to define\ndefault attributes, or even mix ShallowAttributes into the base class:\n\n``` ruby\nrequire 'active_model'\n\nclass Form\n  extend ActiveModel::Naming\n  extend ActiveModel::Translation\n  include ActiveModel::Conversion\n  include ShallowAttributes\n\n  def persisted?\n    false\n  end\nend\n\nclass SearchForm \u003c Form\n  attribute :name, String\nend\n\nform = SearchForm.new(name: 'Anton')\nform.name # =\u003e \"Anton\"\n```\n\n### Default Values\n\n``` ruby\nclass Page\n  include ShallowAttributes\n\n  attribute :title, String\n\n  # default from a singleton value (integer in this case)\n  attribute :views, Integer, default: 0\n\n  # default from a singleton value (boolean in this case)\n  attribute :published, 'Boolean', default: false\n\n  # default from a callable object (proc in this case)\n  attribute :slug, String, default: lambda { |page, attribute| page.title.downcase.gsub(' ', '-') }\n\n  # default from a method name as symbol\n  attribute :editor_title, String,  default: :default_editor_title\n\n  private\n\n  def default_editor_title\n    published ? title : \"UNPUBLISHED: #{title}\"\n  end\nend\n\npage = Page.new(title: 'Virtus README')\npage.slug         # =\u003e 'virtus-readme'\npage.views        # =\u003e 0\npage.published    # =\u003e false\npage.editor_title # =\u003e \"UNPUBLISHED: Virtus README\"\n\npage.views = 10\npage.views                    # =\u003e 10\npage.reset_attribute(:views)  # =\u003e 0\npage.views                    # =\u003e 0\n```\n\n### Mandatory attributes\nYou can provide `present: true` option for any attribute that will prevent class from initialization\nif this attribute was not provided:\n\n``` ruby\nclass CreditCard\n  include ShallowAttributes\n  attribute :number, Integer, present: true\n  attribute :owner, String, present: true\nend\n\ncard = CreditCard.new(number: 1239342)\n# =\u003e ShallowAttributes::MissingAttributeError: Mandatory attribute \"owner\" was not provided\n```\n\n\n### Embedded Value\n\n``` ruby\nclass City\n  include ShallowAttributes\n\n  attribute :name, String\n  attribute :size, Integer, default: 9000\nend\n\nclass Address\n  include ShallowAttributes\n\n  attribute :street,  String\n  attribute :zipcode, String, default: '111111'\n  attribute :city,    City\nend\n\nclass User\n  include ShallowAttributes\n\n  attribute :name,    String\n  attribute :address, Address\nend\n\nuser = User.new(address: {\n  street: 'Street 1/2',\n  zipcode: '12345',\n  city: {\n    name: 'NYC'\n  }\n})\n\nuser.address.street    # =\u003e \"Street 1/2\"\nuser.address.city.name # =\u003e \"NYC\"\n```\n\n### Custom Coercions\n\n``` ruby\nrequire 'json'\n\nclass Json\n  def coerce(value, options = {})\n    value.is_a?(::Hash) ? value : JSON.parse(value)\n  end\nend\n\nclass User\n  include ShallowAttributes\n\n  attribute :info, Json, default: {}\nend\n\nuser = User.new\nuser.info = '{\"email\":\"john@domain.com\"}' # =\u003e {\"email\"=\u003e\"john@domain.com\"}\nuser.info.class                           # =\u003e Hash\n\n# With a custom attribute encapsulating coercion-specific configuration\nclass NoisyString\n  def coerce(value, options = {})\n    value.to_s.upcase\n  end\nend\n\nclass User\n  include ShallowAttributes\n\n  attribute :scream, NoisyString\nend\n\nuser = User.new(scream: 'hello world!')\nuser.scream # =\u003e \"HELLO WORLD!\"\n```\n\n### Collection Member Coercions\n\n``` ruby\n# Support \"primitive\" classes\nclass Book\n  include ShallowAttributes\n\n  attribute :page_numbers, Array, of: Integer\nend\n\nbook = Book.new(:page_numbers =\u003e %w[1 2 3])\nbook.page_numbers # =\u003e [1, 2, 3]\n\n# Support EmbeddedValues, too!\nclass Address\n  include ShallowAttributes\n\n  attribute :address,     String\n  attribute :locality,    String\n  attribute :region,      String\n  attribute :postal_code, String\nend\n\nclass PhoneNumber\n  include ShallowAttributes\n\n  attribute :number, String\nend\n\nclass User\n  include ShallowAttributes\n\n  attribute :phone_numbers, Array, of: PhoneNumber\n  attribute :addresses,     Array, of: Address\nend\n\nuser = User.new(\n  :phone_numbers =\u003e [\n    { :number =\u003e '212-555-1212' },\n    { :number =\u003e '919-444-3265' } ],\n  :addresses =\u003e [\n    { :address =\u003e '1234 Any St.', :locality =\u003e 'Anytown', :region =\u003e \"DC\", :postal_code =\u003e \"21234\" } ])\n\nuser.phone_numbers # =\u003e [#\u003cPhoneNumber:0x007fdb2d3bef88 @number=\"212-555-1212\"\u003e, #\u003cPhoneNumber:0x007fdb2d3beb00 @number=\"919-444-3265\"\u003e]\nuser.addresses     # =\u003e [#\u003cAddress:0x007fdb2d3be448 @address=\"1234 Any St.\", @locality=\"Anytown\", @region=\"DC\", @postal_code=\"21234\"\u003e]\n\nuser.attributes\n# =\u003e {\n# =\u003e   :phone_numbers =\u003e [\n# =\u003e     { :number =\u003e '212-555-1212' },\n# =\u003e     { :number =\u003e '919-444-3265' }\n# =\u003e   ],\n# =\u003e   :addresses =\u003e [\n# =\u003e     {\n# =\u003e       :address =\u003e '1234 Any St.',\n# =\u003e       :locality =\u003e 'Anytown',\n# =\u003e       :region =\u003e \"DC\",\n# =\u003e       :postal_code =\u003e \"21234\"\n# =\u003e     }\n# =\u003e   ]\n# =\u003e }\n```\n\n### IMPORTANT note about member coercions\n\nShallowAttributes performs coercions only when a value is being assigned. If you mutate the value\nlater on using its own interfaces then coercion won't be triggered.\n\nHere's an example:\n\n``` ruby\nclass Book\n  include ShallowAttributes\n  attribute :title, String\nend\n\nclass Library\n  include ShallowAttributes\n  attribute :books, Array, of: Book\nend\n\nlibrary = Library.new\n\n# This will coerce Hash to a Book instance\nlibrary.books = [ { :title =\u003e 'Introduction' } ]\n\n# This WILL NOT COERCE the value because you mutate the books array with Array#\u003c\u003c\nlibrary.books \u003c\u003c { :title =\u003e 'Another Introduction' }\n```\n\n### Overriding setters\n\n``` ruby\nclass User\n  include ShallowAttributes\n\n  attribute :name, String\n\n  alias_method :_name=, :name=\n  def name=(new_name)\n    custom_name = nil\n    if new_name == \"Godzilla\"\n      custom_name = \"Can't tell\"\n    end\n\n    self._name = custom_name || new_name\n  end\nend\n\nuser = User.new(name: \"Frank\")\nuser.name # =\u003e 'Frank'\n\nuser = User.new(name: \"Godzilla\")\nuser.name # =\u003e 'Can't tell'\n```\n\n### ActiveModel compatibility\n\nShallowAttributes is fully compatible with ActiveModel.\n\n#### Form object\n\n``` ruby\nrequire 'active_model'\n\nclass SearchForm\n  extend ActiveModel::Naming\n  extend ActiveModel::Translation\n  include ActiveModel::Conversion\n  include ShallowAttributes\n\n  attribute :name, String\n  attribute :service_ids, Array, of: Integer\n  attribute :archived, 'Boolean', default: false\n\n  def persisted?\n    false\n  end\n\n  def results\n    # ...\n  end\nend\n\nclass SearchesController \u003c ApplicationController\n  def index\n    search_params = params.require(:search_form).permit(...)\n    @search_form = SearchForm.new(search_params)\n  end\nend\n```\n\n``` erb\n\u003ch1\u003eSearch\u003c/h1\u003e\n\u003c%= form_for @search_form do |f| %\u003e\n  \u003c%= f.text_field :name %\u003e\n  \u003c%= f.collection_check_boxes :service_ids, Service.all, :id, :name %\u003e\n  \u003c%= f.select :archived, [['Archived', true], ['Not Archived', false]] %\u003e\n\u003c% end %\u003e\n```\n\n#### Validations\n\n``` ruby\nrequire 'active_model'\n\nclass Children\n  include ShallowAttributes\n  include ActiveModel::Validations\n\n  attribute :scream, String\n  validates :scream, presence: true\nend\n\nuser = User.new(scream: '')\nuser.valid? # =\u003e false\nuser.scream = 'hello world!'\nuser.valid? # =\u003e true\n```\n\n### Dry-types\nYou can use dry-types objects as a type for your attribute:\n```ruby\nmodule Types\n  include Dry::Types.module\nend\n\nclass User\n  include ShallowAttributes\n\n  attribute :name, Types::Coercible::String\n  attribute :age, Types::Coercible::Int\n  attribute :birthday, DateTime\nend\n\nuser = User.new(name: nil, age: 0)\nuser.name # =\u003e ''\nuser.age # =\u003e 0\n```\n\n## Ruby version support\n\nShallowAttributes is [known to work correctly][travis-link] with the following rubies:\n\n* 2.0\n* 2.1\n* 2.2\n* 2.3\n* 2.4\n* jruby-head\n\nAlso we run rbx-2 buld too.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/davydovanton/shallow_attributes.\nThis project is intended to be a safe, welcoming space for collaboration, and contributors are expected\nto adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\n[doc-link]: http://www.rubydoc.info/github/davydovanton/shallow_attributes/master\n[virtus-link]: https://github.com/solnic/virtus\n[fast-attributes-link]: https://github.com/applift/fast_attributes\n[attrio-link]: https://github.com/jetrockets/attrio\n[performance-benchmark]: https://gist.github.com/davydovanton/d14b51ab63e3fab63ecb\n[travis-link]: https://travis-ci.org/davydovanton/shallow_attributes\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavydovanton%2Fshallow_attributes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavydovanton%2Fshallow_attributes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavydovanton%2Fshallow_attributes/lists"}