{"id":13483661,"url":"https://github.com/barsoom/attr_extras","last_synced_at":"2025-03-27T14:31:29.439Z","repository":{"id":5087269,"uuid":"6249598","full_name":"barsoom/attr_extras","owner":"barsoom","description":"Takes some boilerplate out of Ruby with methods like attr_initialize.","archived":false,"fork":false,"pushed_at":"2024-06-20T12:07:10.000Z","size":208,"stargazers_count":562,"open_issues_count":3,"forks_count":30,"subscribers_count":18,"default_branch":"master","last_synced_at":"2025-03-27T04:35:21.352Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"Gisto/Gisto","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/barsoom.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"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":"2012-10-16T18:46:18.000Z","updated_at":"2025-03-12T09:44:17.000Z","dependencies_parsed_at":"2024-01-08T17:12:45.272Z","dependency_job_id":"ec810900-859c-459f-ab0f-7a7a583fb2e8","html_url":"https://github.com/barsoom/attr_extras","commit_stats":{"total_commits":250,"total_committers":24,"mean_commits":"10.416666666666666","dds":"0.31200000000000006","last_synced_commit":"4913a40d2cea9cd90fbdc444441e35268cf3c6c3"},"previous_names":[],"tags_count":53,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barsoom%2Fattr_extras","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barsoom%2Fattr_extras/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barsoom%2Fattr_extras/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barsoom%2Fattr_extras/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/barsoom","download_url":"https://codeload.github.com/barsoom/attr_extras/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245863101,"owners_count":20684788,"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":[],"created_at":"2024-07-31T17:01:13.933Z","updated_at":"2025-03-27T14:31:29.177Z","avatar_url":"https://github.com/barsoom.png","language":"Ruby","readme":"[![Gem Version](https://badge.fury.io/rb/attr_extras.svg)](https://badge.fury.io/rb/attr_extras)\n[![Ruby CI](https://github.com/barsoom/attr_extras/actions/workflows/ci.yml/badge.svg)](https://github.com/barsoom/attr_extras/actions/workflows/ci.yml)\n[![Code Climate](https://codeclimate.com/github/barsoom/attr_extras/badges/gpa.svg)](https://codeclimate.com/github/barsoom/attr_extras)\n\n# attr\\_extras\n\nTakes some boilerplate out of Ruby, lowering the barrier to extracting small focused classes, without [the downsides of using `Struct`](http://thepugautomatic.com/2013/08/struct-inheritance-is-overused/).\n\nProvides lower-level methods like `attr_private` and `attr_value` that nicely complement Ruby's built-in `attr_accessor`, `attr_reader` and `attr_writer`.\n\nAlso higher-level ones like `pattr_initialize` (or `attr_private_initialize`) and `method_object` to really cut down on the boilerplate.\n\nInstead of\n\n``` ruby\nclass InvoicePolicy\n  def initialize(invoice, company:)\n    @invoice = invoice\n    @company = company\n  end\n\n  def payable?\n    some_logic(invoice, company)\n  end\n\n  private\n\n  attr_reader :invoice, :company\nend\n```\n\nyou can just do\n\n``` ruby\nclass InvoiceBuilder\n  pattr_initialize :invoice, [:company!]\n\n  def payable?\n    some_logic(invoice, company)\n  end\nend\n```\n\nAnd instead of\n\n``` ruby\nclass PayInvoice\n  def self.call(invoice, amount)\n    new(invoice, amount).call\n  end\n\n  def initialize(invoice, amount)\n    @invoice = invoice\n    @amount = amount\n  end\n\n  def call\n    PaymentGateway.charge(invoice.id, amount_in_cents)\n  end\n\n  private\n\n  def amount_in_cents\n    amount * 100\n  end\n\n  attr_reader :invoice, :amount\nend\n```\n\nyou can just do\n\n``` ruby\nclass PayInvoice\n  method_object :invoice, :amount\n\n  def call\n    PaymentGateway.charge(invoice.id, amount_in_cents)\n  end\n\n  private\n\n  def amount_in_cents\n    amount * 100\n  end\nend\n```\n\nSupports positional arguments as well as optional and required keyword arguments.\n\nAlso provides conveniences for creating value objects, method objects, query methods and abstract methods.\n\n\n## Usage\n\n* [`attr_initialize`](#attr_initialize)\n* [`attr_private`](#attr_private)\n* [`attr_value`](#attr_value)\n* [`pattr_initialize`](#pattr_initialize) / [`attr_private_initialize`](#attr_private_initialize)\n* [`vattr_initialize`](#vattr_initialize) / [`attr_value_initialize`](#attr_value_initialize)\n* [`rattr_initialize`](#rattr_initialize) / [`attr_reader_initialize`](#attr_reader_initialize)\n* [`aattr_initialize`](#aattr_initialize) / [`attr_accessor_initialize`](#attr_accessor_initialize)\n* [`static_facade`](#static_facade)\n* [`method_object`](#method_object)\n* [`attr_implement`](#attr_implement)\n* [`cattr_implement`](#cattr_implement)\n* [`attr_query`](#attr_query)\n* [`attr_id_query`](#attr_id_query)\n\n\n### `attr_initialize`\n\n`attr_initialize :foo, :bar` defines an initializer that takes two arguments and assigns `@foo` and `@bar`.\n\n`attr_initialize :foo, [:bar, :baz!]` defines an initializer that takes one regular argument, assigning `@foo`, and two keyword arguments, assigning `@bar` (optional) and `@baz` (required).\n\n`attr_initialize [:bar, :baz!]` defines an initializer that takes two keyword arguments, assigning `@bar` (optional) and `@baz` (required).\n\nIf you pass unknown keyword arguments, you will get an `ArgumentError`.\nIf you don't pass required arguments and don't define default value for them, you will get a `KeyError`.\n\n`attr_initialize` can also accept a block which will be invoked after initialization. This is useful for e.g. initializing private data as necessary.\n\n#### Default values\n\nKeyword arguments can have default values:\n\n`attr_initialize [:bar, baz: \"default value\"]` defines an initializer that takes two keyword arguments, assigning `@bar` (optional) and `@baz` (optional with default value `\"default value\"`).\n\nNote that default values are evaluated *when the class is loaded* and not on every instantition. So `attr_initialize [time: Time.now]` might not do what you expect.\n\nYou can always use regular Ruby methods to achieve this:\n\n```\nclass Foo\n  attr_initialize [:time]\n\n  private\n\n  def time\n    @time || Time.now\n  end\nend\n```\n\nOr just use a regular initializer with default values.\n\n\n### `attr_private`\n\n`attr_private :foo, :bar` defines private readers for `@foo` and `@bar`.\n\n\n### `attr_value`\n\n`attr_value :foo, :bar` defines public readers for `@foo` and `@bar` and also defines object equality: two value objects of the same class with the same values will be considered equal (with `==` and `eql?`, in `Set`s, as `Hash` keys etc).\n\nIt does not define writers, because [value objects](http://en.wikipedia.org/wiki/Value_object) are typically immutable.\n\n\n### `pattr_initialize`\n### `attr_private_initialize`\n\n`pattr_initialize :foo, :bar` defines both initializer and private readers. Shortcut for:\n\n``` ruby\nattr_initialize :foo, :bar\nattr_private :foo, :bar\n```\n\n`pattr_initialize` is aliased as `attr_private_initialize` if you prefer a longer but clearer name.\n\nExample:\n\n``` ruby\nclass Item\n  pattr_initialize :name, :price\n\n  def price_with_vat\n    price * 1.25\n  end\nend\n\nItem.new(\"Pug\", 100).price_with_vat  # =\u003e 125.0\n```\n\n[The `attr_initialize` notation](#attr_initialize) for keyword arguments is also supported: `pattr_initialize :foo, [:bar, :baz!]`\n\n### `vattr_initialize`\n### `attr_value_initialize`\n\n`vattr_initialize :foo, :bar` defines initializer, public readers and [value object identity](#attr_value). Shortcut for:\n\n``` ruby\nattr_initialize :foo, :bar\nattr_value :foo, :bar\n```\n\n`vattr_initialize` is aliased as `attr_value_initialize` if you prefer a longer but clearer name.\n\nExample:\n\n``` ruby\nclass Country\n  vattr_initialize :code\nend\n\nCountry.new(\"SE\") == Country.new(\"SE\")  # =\u003e true\nCountry.new(\"SE\").code  # =\u003e \"SE\"\n```\n\n[The `attr_initialize` notation](#attr_initialize) for keyword arguments is also supported: `vattr_initialize :foo, [:bar, :baz!]`\n\n\n### `rattr_initialize`\n### `attr_reader_initialize`\n\n`rattr_initialize :foo, :bar` defines both initializer and public readers. Shortcut for:\n\n``` ruby\nattr_initialize :foo, :bar\nattr_reader :foo, :bar\n```\n\n`rattr_initialize` is aliased as `attr_reader_initialize` if you prefer a longer but clearer name.\n\nExample:\n\n``` ruby\nclass PublishBook\n  rattr_initialize :book_name, :publisher_backend\n\n  def call\n    publisher_backend.publish book_name\n  end\nend\n\nservice = PublishBook.new(\"A Novel\", publisher)\nservice.book_name  # =\u003e \"A Novel\"\n```\n\n[The `attr_initialize` notation](#attr_initialize) for keyword arguments is also supported: `rattr_initialize :foo, [:bar, :baz!]`\n\n### `aattr_initialize`\n### `attr_accessor_initialize`\n\n`aattr_initialize :foo, :bar` defines an initializer, public readers, and public writers. It's a shortcut for:\n\n``` ruby\nattr_initialize :foo, :bar\nattr_accessor :foo, :bar\n```\n\n`aattr_initialize` is aliased as `attr_accessor_initialize`, if you prefer a longer but clearer name.\n\nExample:\n\n``` ruby\nclass Client\n  aattr_initialize :username, :access_token\nend\n\nclient = Client.new(\"barsoom\", \"SECRET\")\nclient.username # =\u003e \"barsoom\"\n\nclient.access_token = \"NEW_SECRET\"\nclient.access_token # =\u003e \"NEW_SECRET\"\n```\n\n[The `attr_initialize` notation](#attr_initialize) for keyword arguments and blocks is also supported.\n\n### `static_facade`\n\n`static_facade :allow?, :user` defines an `.allow?` class method that delegates to an instance method by the same name, having first provided `user` as a private reader.\n\nThis is handy when a class-method API makes sense but you still want [the refactorability of instance methods](http://blog.codeclimate.com/blog/2012/11/14/why-ruby-class-methods-resist-refactoring/).\n\nExample:\n\n``` ruby\nclass PublishingPolicy\n  static_facade :allow?, :user\n\n  def allow?\n    user.admin? \u0026\u0026 complicated_extracted_method\n  end\n\n  private\n\n  def complicated_extracted_method\n    # …\n  end\nend\n\nPublishingPolicy.allow?(user)\n```\n\n`static_facade :allow?, :user` is a shortcut for\n\n``` ruby\npattr_initialize :user\n\ndef self.allow?(user)\n  new(user).allow?\nend\n```\n\n[The `attr_initialize` notation](#attr_initialize) for keyword arguments is also supported: `static_facade :allow?, :user, [:user_agent, :ip!]`\n\nYou don't have to specify arguments/readers if you don't want them: just `static_facade :tuesday?` is also valid.\n\nYou can specify multiple method names as long as they can share the same initializer arguments: `static_facade [:allow?, :deny?], :user, [:user_agent, :ip!]`\n\nAny block given to the class method will be passed on to the instance method.\n\n\"Static façade\" is the least bad name for this pattern we've come up with. Suggestions are welcome.\n\n\n### `method_object`\n\n*NOTE: v4.0.0 made a breaking change! [`static_facade`](#static_facade) does exactly what `method_object` used to do; the new `method_object` no longer accepts a method name argument.*\n\n`method_object :foo` defines a `.call` class method that delegates to an instance method by the same name, having first provided `foo` as a private reader.\n\nThis is a special case of [`static_facade`](#static_facade) for when you want a [Method Object](http://refactoring.com/catalog/replaceMethodWithMethodObject.html), and the class name itself will communicate the action it performs.\n\nExample:\n\n``` ruby\nclass CalculatePrice\n  method_object :order\n\n  def call\n    total * factor\n  end\n\n  private\n\n  def total\n    order.items.map(\u0026:price).inject(:+)\n  end\n\n  def factor\n    1 + rand\n  end\nend\n\nclass Order\n  def price\n    CalculatePrice.call(self)\n  end\nend\n```\n\nYou could even do `CalculatePrice.(self)` if you like, since we're using the [`call` convention](http://www.ruby-doc.org/core-2.2.0/Proc.html#method-i-call).\n\n`method_object :foo` is a shortcut for\n\n``` ruby\nstatic_facade :call, :foo\n```\n\nwhich is a shortcut for\n\n``` ruby\npattr_initialize :foo\n\ndef self.call(foo)\n  new(foo).call\nend\n```\n\n[The `attr_initialize` notation](#attr_initialize) for keyword arguments is also supported: `method_object :foo, [:bar, :baz!]`\n\nYou don't have to specify arguments/readers if you don't want them: just `method_object` is also valid.\n\nAny block given to the class method will be passed on to the instance method.\n\n\n### `attr_implement`\n\n`attr_implement :foo, :bar` defines nullary (0-argument) methods `foo` and `bar` that raise e.g. `\"Implement a 'foo()' method\"`.\n\n`attr_implement :foo, [:name, :age]` will define a binary (2-argument) method `foo` that raises `\"Implement a 'foo(name, age)' method\"`.\n\nThis is suitable for [abstract methods](http://en.wikipedia.org/wiki/Abstract_method#Abstract_methods) in base classes, e.g. when using the [template method pattern](http://en.wikipedia.org/wiki/Template_method_pattern).\n\nIt's more or less a shortcut for\n\n``` ruby\ndef my_method\n  raise \"Implement me in a subclass!\"\nend\n```\n\nthough it is shorter, more declarative, gives you a clear message and handles edge cases you might not have thought about (see tests).\n\nNote that you can also use this with modules, to effectively mix in interfaces:\n\n``` ruby\nmodule Bookable\n  attr_implement :book, [:bookable]\n  attr_implement :booked?\nend\n\nclass Invoice\n  include Bookable\nend\n\nclass Payment\n  include Bookable\nend\n```\n\n\n### `cattr_implement`\n\nLike [`attr_implement`](#attr_implement) but for class methods.\n\nExample:\n\n``` ruby\nclass TransportOrder\n  cattr_implement :must_be_tracked?\nend\n```\n\n\n### `attr_query`\n\n`attr_query :foo?, :bar?` defines query methods like `foo?`, which is true if (and only if) `foo` is truthy.\n\n\n### `attr_id_query`\n\n`attr_id_query :foo?, :bar?` defines query methods like `foo?`, which is true if (and only if) `foo_id` is truthy. Goes well with Active Record.\n\n\n## Explicit mode\n\nBy default, attr\\_extras will add methods to every class and module.\n\nThis is not ideal if you're using attr\\_extras in a library: those who depend on your library will get those methods as well.\n\nIt's also not obvious where the methods come from. You can be more explicit about it, and restrict where the methods are added, like this:\n\n``` ruby\nrequire \"attr_extras/explicit\"\n\nclass MyLib\n  extend AttrExtras.mixin\n\n  pattr_initialize :now_this_class_can_use_attr_extras\nend\n```\n\nCrucially, you need to `require \"attr_extras/explicit\"` *instead of* `require \"attr_extras\"`. Some frameworks, like Ruby on Rails, may automatically require everything in your `Gemfile`. You can avoid that with `gem \"attr_extras\", require: \"attr_extras/explicit\"`.\n\nIn explicit mode, you need to call `extend AttrExtras.mixin` *in every class or module* that wants the attr\\_extras methods.\n\n\n## Philosophy\n\nFindability is a core value.\nHence the long name `attr_initialize`, so you see it when scanning for the initializer;\nand the enforced questionmarks with `attr_id_query :foo?`, so you can search for that method.\n\n\n## Q \u0026 A\n\n\n### Why not use `Struct` instead of `pattr_initialize`?\n\nSee: [\"Struct inheritance is overused\"](http://thepugautomatic.com/2013/08/struct-inheritance-is-overused/)\n\n\n### Why not use `private; attr_reader :foo` instead of `attr_private :foo`?\n\nOther than being more to type, declaring `attr_reader` after `private` will actually give you a warning (deserved or not) if you run Ruby with warnings turned on.\n\nIf you don't want the dependency on `attr_extras`, you can get rid of the warnings with `attr_reader :foo; private :foo`. Or just define a regular private method.\n\n### Can I use attr\\_extras in `BasicObject`s?\n\nNo, sorry. It depends on various methods that `BasicObject`s don't have. Use a regular `Object` or make do without attr\\_extras.\n\n\n## Installation\n\nAdd this line to your application's `Gemfile`:\n\n    gem \"attr_extras\"\n\nAnd then execute:\n\n    bundle\n\nOr install it yourself as:\n\n    gem install attr_extras\n\n\n## Running the tests\n\nRun them with:\n\n    rake\n\nOr to see warnings (try not to have any):\n\n    RUBYOPT=-w rake\n\nYou can run an individual test using the [m](https://github.com/qrush/m) gem:\n\n    m spec/attr_extras/attr_initialize_spec.rb:48\n\nThe tests are intentionally split into two test suites for reasons described in `Rakefile`.\n\n\n## License\n\n[MIT](LICENSE.txt)\n","funding_links":[],"categories":["Ruby","Core Extensions"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbarsoom%2Fattr_extras","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbarsoom%2Fattr_extras","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbarsoom%2Fattr_extras/lists"}