Open-source project
barsoom/attr_extras avatar
barsoom/attr_extras

attr_extras: Ruby shortcuts for initializers, readers and value objects

Takes some boilerplate out of Ruby with methods like attr_initialize.

562 stars28 forksRubyMIT

At a glance

What is it?
attr_extras is a Ruby gem that removes the boilerplate around attribute declaration, offering initializers, private and public readers, value object equality, method objects, static facades and query methods in one place. Its central trade is that the initializers it generates are less explicit than a hand written one, and it is MIT licensed.
Who is it for?
attr_extras fits a Ruby codebase that favours small focused classes and is tired of writing the same initializer and reader block for each of them, and it is most useful when the argument list is short and positional. It does not fit a codebase that wants its constructor contract visible at a glance, since the generated signature hides which arguments are required, and the default value evaluation happens when the class is loaded rather than per instance.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 125 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the gem is for and where it sits against Struct

attr_extras takes some boilerplate out of Ruby, with the stated aim of lowering the barrier to extracting small focused classes, and specifically without the downsides of using Struct.

That last clause is the design brief. Struct is the obvious way to get a class with an initializer and readers in one line, and the problems with it are well known, so a gem that offers the convenience without the inheritance and equality surprises is filling a real gap.

The methods come in two levels. There are lower level ones such as attr_private and attr_value, which nicely complement Ruby's built-in attr_accessor, attr_reader and attr_writer. Then there are the higher level ones, pattr_initialize and its alias attr_private_initialize, and method_object, which are the shortcuts that really cut down on the boilerplate.

The list of what is provided is longer than that, though. Alongside the initializers and the readers there are conveniences for creating value objects, method objects, query methods and abstract methods, and the usage list in the documentation names seventeen entry points: attr_initialize, attr_private, attr_value, pattr_initialize and attr_private_initialize, vattr_initialize and attr_value_initialize, rattr_initialize and attr_reader_initialize, aattr_initialize and attr_accessor_initialize, static_facade, method_object, attr_implement, cattr_implement, attr_query and attr_id_query.

The audience is Ruby developers who write small objects, especially in a style where each class does one thing, and who find the repeated initialize-plus-attr_reader block tedious.

The project is a single gem, attr_extras, with a Gem Version badge and a Ruby CI badge, and the repository is compact: lib/, spec/, script/, a gemspec, a Rakefile, a Gemfile, a.rubocop.yml, a CHANGELOG.md and LICENSE.txt.

The example the documentation leads with

The README opens with a before and after, and the before is instructive.

``` ruby class InvoiceBuilder pattr_initialize :invoice, [:company!]

def payable? some_logic(invoice, company) end end ```

The class it replaces had an initialize that took an invoice positionally and a company keyword, a payable? method, a private section and an attr_reader for both attributes. The replacement is one line of declaration and no private section, because pattr_initialize defines private readers as well as the initializer.

The second example replaces a class that had a class method call, an initialize, a call method, a private helper and a reader block. The replacement declares method_object :invoice, :amount, and the call method uses them.

The overall shape of the gem is a set of declaration macros that a class body calls instead of writing the corresponding methods by hand. That is the whole idea, and everything else in the documentation is a variation on it.

The gem also supports positional arguments as well as optional and required keyword arguments, which is what makes the notation in the examples a bit denser than a plain attr_reader list.

attr_initialize and the notation you have to learn

attr_initialize is the primitive the rest is built on, and the documentation defines it in three forms.

`attr_initialize :foo, :bar` defines an initializer that takes two arguments and assigns @foo and @bar. `attr_initialize :foo, [:bar, :baz!]` defines an initializer that takes one regular argument, assigning @foo, and two keyword arguments, assigning @bar as optional and @baz as required. And `attr_initialize [:bar, :baz!]` defines an initializer that takes two keyword arguments, assigning @bar as optional and @baz as required.

So a bare symbol is a positional argument, and a bracketed list is a keyword argument list where a trailing exclamation mark means required.

The error behaviour is defined. If you pass unknown keyword arguments you get an ArgumentError, and if you do not pass required arguments and have not defined a default value for them, you get a KeyError.

attr_initialize can also accept a block which is invoked after initialization, which the documentation says is useful for example for initializing private data as necessary.

Then there is the default value rule, and it is the sharpest edge in the gem. Keyword arguments can have default values, so `attr_initialize [:bar, baz: "default value"]` takes two keyword arguments, assigning @bar as optional and @baz as optional with the default value. But the documentation warns that default values are evaluated when the class is loaded and not on every instantiation, so `attr_initialize [time: Time.now]` might not do what you expect.

The value object case and the equality it gives you

attr_value is the method with a semantic payload, not just less typing.

`attr_value :foo, :bar` defines public readers for @foo and @bar and also defines object equality, so two value objects of the same class with the same values are considered equal, with == and eql?, in Sets, as Hash keys and so on.

It deliberately does not define writers, because value objects are typically immutable. That is a good default, since a writer on a value object lets you break the invariant the equality depends on.

The combined macro vattr_initialize, aliased as attr_value_initialize for those who prefer the longer name, defines the initializer, public readers and the value object identity in one call. The example in the documentation is short:

``` ruby class Country vattr_initialize :code end

Country.new("SE") == Country.new("SE") # => true Country.new("SE").code # => "SE" ```

Two separately constructed Country objects with the same code are equal, and the code is readable. That is the whole case for the method, and it is the kind of behaviour people usually end up writing by hand or reaching for a data class library for.

The keyword argument notation is supported here too, with vattr_initialize :foo, [:bar, :baz!].

The other combinations and the query and implement helpers

The remaining initializers are combinations of the same primitives, and the documentation gives the shortcut each one expands to.

pattr_initialize, aliased as attr_private_initialize, defines both the initializer and private readers. rattr_initialize, aliased as attr_reader_initialize, defines the initializer and public readers. aattr_initialize, aliased as attr_accessor_initialize, defines the initializer, public readers and public writers. The example for the last one creates a client with a username and an access token, reads the username, and then assigns a new access token, which is the case where you want a writer.

Alongside them are the helpers that are not about initializers at all. static_facade is a convenience for a class whose whole interface is one delegating method, method_object turns a set of readers into an object you can pass around, attr_implement and cattr_implement provide the implement and abstract method conveniences, and attr_query and attr_id_query give the query method forms.

The documentation's own summary of the higher level set is that it also provides conveniences for creating value objects, method objects, query methods and abstract methods.

The naming is regular enough to predict: the v prefix means value object, the r prefix means reader, the p prefix means private, and the a prefix means accessor. That is a small vocabulary, and it is the difference between a gem you learn in five minutes and one you look up every time.

Where attr_extras is the wrong choice

The first limit is that the generated signature is invisible. A reader of the class sees `pattr_initialize:invoice, [:company!]` and has to know the notation to know that company is a required keyword argument and invoice is positional. In a hand written initialize, that information is right there in the method body, and in an API that other people call, that visibility has value. The trade is that the notation is somewhat denser than a plain list of symbols.

The second is the default value evaluation, which is a genuine bug source rather than a style choice. Default values are evaluated when the class is loaded, not on every instantiation, so `attr_initialize [time: Time.now]` gives every instance the same timestamp from load time. The documentation says it might not do what you expect and shows the manual alternative, a private reader that falls back to Time.now, so the fix is known, but the trap is on by default.

The third is that a macro-heavy class body is not obvious to a reader who does not know the gem. Every line in a class built this way is a declaration, and a newcomer reading the file has to hold the whole vocabulary in their head. A codebase that uses attr_extras consistently is fine with this, and a codebase that uses it in one place and hand written initializers elsewhere is harder to read than either style alone.

Compare that with writing the initializers by hand. The difference in approach is that the gem moves the boilerplate into the class body in exchange for a notation that has to be learned, while a hand written initializer costs six lines per class and needs no vocabulary at all. For a project with a strong convention and a shared style guide, the gem removes a lot of repetition. For a project with mixed styles or a team that is learning the codebase, the repetition is cheaper than the notation.

Licence, project state and the checklist before you adopt it

attr_extras is released under the MIT licence, with the text in the LICENSE.txt file at the repository root, and the project carries a Gem Version badge and a Ruby CI workflow badge. The MIT terms are permissive, with no copyleft obligation, and you should read the LICENSE file yourself for the exact wording.

The release history shows a stable 7.x line. v6.2.5, v7.0.0 and v7.1.0 were all published on 2025-10-15, which suggests the version jumps are driven by Ruby compatibility rather than by feature work, and the CHANGELOG.md in the tree is where you would confirm that. The repository is not archived, and the last push was on 2026-06-20, so the tree is about four months old relative to today.

What to check before you adopt it is a short list. Read the attr_initialize section on default values first, because that is the one place where the gem's behaviour will surprise someone. Then decide per class whether the positional form is enough, and reserve the keyword form for arguments that are genuinely required, since that is where the error behaviour, the ArgumentError for unknown keywords and the KeyError for missing required ones, is doing real work for you.

And check the interaction with your test suite, because value object equality changes how your specs compare instances. If you switch a class to attr_value, any spec that was comparing objects by identity will now pass for the right reason, and any spec that relied on two equal objects being distinct will now fail.

Editorial conclusion

attr_extras fits a Ruby codebase that favours small focused classes and is tired of writing the same initializer and reader block for each of them, and it is most useful when the argument list is short and positional. It does not fit a codebase that wants its constructor contract visible at a glance, since the generated signature hides which arguments are required, and the default value evaluation happens when the class is loaded rather than per instance. Before you adopt it, read the attr_initialize section on default values, keep the positional form for the common case and the keyword form for anything with a required argument, and read the licence in LICENSE.txt yourself.

Frequently asked questions

What does the attr_extras Ruby gem provide?

It takes boilerplate out of Ruby with methods like attr_initialize, attr_private, attr_value, pattr_initialize, vattr_initialize, rattr_initialize, aattr_initialize, method_object, static_facade, attr_implement, cattr_implement, attr_query and attr_id_query, so a class can declare its initializer and readers in one line.

How does attr_initialize handle required and optional keyword arguments?

A bare symbol is a positional argument, while a bracketed list is a keyword argument list where a trailing exclamation mark means required. Unknown keyword arguments raise an ArgumentError, and missing required arguments without a default raise a KeyError.

What does attr_value do differently from attr_reader?

attr_value defines public readers and also defines object equality, so two value objects of the same class with the same values are considered equal with == and eql?, in Sets and as Hash keys. It does not define writers, because value objects are typically immutable.

What is the gotcha with default values in attr_initialize?

Default values are evaluated when the class is loaded and not on every instantiation, so an expression like attr_initialize [time: Time.now] might not do what you expect. The documentation shows the alternative, a private reader that falls back to Time.now, or a regular initializer with default values.

What licence does attr_extras use?

attr_extras is released under the MIT licence, with the terms in the LICENSE.txt file at the repository root. The recent releases are v7.0.0 and v7.1.0, both published on 2025-10-15, and the last push to the repository was on 2026-06-20.

Official sources

  1. barsoom/attr_extras on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/barsoom-attr-extras.svg)](https://hysenlabs.com/projects/barsoom-attr-extras)

Was this useful?