Ruby
Need help with attr_extras?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.
barsoom

Description

Takes some boilerplate out of Ruby with methods like attr_initialize.

479 Stars 26 Forks MIT License 240 Commits 0 Opened issues

Services available

Need anything else?

Gem Version Build status Code Climate

attr_extras

Takes some boilerplate out of Ruby, lowering the barrier to extracting small focused classes, without the downsides of using

Struct
.

Instead of

class InvoiceBuilder
  def initialize(invoice, employee)
    @invoice, @employee = invoice, employee
  end

private

attr_reader :invoice, :employee end

you can just do

class InvoiceBuilder
  pattr_initialize :invoice, :employee
end

This nicely complements Ruby's built-in

attr_accessor
,
attr_reader
and
attr_writer
.

Supports positional arguments as well as optional and required keyword arguments.

Also provides conveniences for creating value objects, method objects, query methods and abstract methods.

Usage

attr_initialize

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
(optional) and
@baz
(required).

attr_initialize [:bar, :baz!]
defines an initializer that takes two keyword arguments, assigning
@bar
(optional) and
@baz
(required).

If you pass unknown keyword arguments, you will get an

ArgumentError
. If you don't pass required arguments and don't define default value for them, you will get a
KeyError
.

attr_initialize
can also accept a block which will be invoked after initialization. This is useful for e.g. initializing private data as necessary.

Default values

Keyword arguments can have default values:

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"
).

Note 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.

You can always use regular Ruby methods to achieve this:

class Foo
  attr_initialize [:time]

private

def time @time || Time.now end end

Or just use a regular initializer with default values.

attr_private

attr_private :foo, :bar
defines private readers for
@foo
and
@bar
.

attr_value

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).

It does not define writers, because value objects are typically immutable.

pattr_initialize

attr_private_initialize

pattr_initialize :foo, :bar
defines both initializer and private readers. Shortcut for:
attr_initialize :foo, :bar
attr_private :foo, :bar

pattr_initialize
is aliased as
attr_private_initialize
if you prefer a longer but clearer name.

Example:

class Item
  pattr_initialize :name, :price

def price_with_vat price * 1.25 end end

Item.new("Pug", 100).price_with_vat # => 125.0

The

attr_initialize
notation for keyword arguments is also supported:

pattr_initialize :foo, [:bar, :baz!]

vattr_initialize

attr_value_initialize

vattr_initialize :foo, :bar
defines initializer, public readers and value object identity. Shortcut for:
attr_initialize :foo, :bar
attr_value :foo, :bar

vattr_initialize
is aliased as
attr_value_initialize
if you prefer a longer but clearer name.

Example:

class Country
  vattr_initialize :code
end

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

The

attr_initialize
notation for keyword arguments is also supported:

vattr_initialize :foo, [:bar, :baz!]

rattr_initialize

attr_reader_initialize

rattr_initialize :foo, :bar
defines both initializer and public readers. Shortcut for:
attr_initialize :foo, :bar
attr_reader :foo, :bar

rattr_initialize
is aliased as
attr_reader_initialize
if you prefer a longer but clearer name.

Example:

class PublishBook
  rattr_initialize :book_name, :publisher_backend

def call publisher_backend.publish book_name end end

service = PublishBook.new("A Novel", publisher) service.book_name # => "A Novel"

The

attr_initialize
notation for keyword arguments is also supported:

rattr_initialize :foo, [:bar, :baz!]

aattr_initialize

attr_accessor_initialize

aattr_initialize :foo, :bar
defines an initializer, public readers, and public writers. It's a shortcut for:
attr_initialize :foo, :bar
attr_accessor :foo, :bar

aattr_initialize
is aliased as
attr_accessor_initialize
, if you prefer a longer but clearer name.

Example:

class Client
  aattr_initialize :username, :access_token
end

client = Client.new("barsoom", "SECRET") client.username # => "barsoom"

client.access_token = "NEW_SECRET" client.access_token # => "NEW_SECRET"

The

attr_initialize
notation for keyword arguments and blocks is also supported.

static_facade

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.

This is handy when a class-method API makes sense but you still want the refactorability of instance methods.

Example:

class PublishingPolicy
  static_facade :allow?, :user

def allow? user.admin? && complicated_extracted_method end

private

def complicated_extracted_method # … end end

PublishingPolicy.allow?(user)

static_facade :allow?, :user
is a shortcut for
pattr_initialize :user

def self.allow?(user) new(user).allow? end

The

attr_initialize
notation for keyword arguments is also supported:

static_facade :allow?, :user, [:user_agent, :ip!]

You don't have to specify arguments/readers if you don't want them: just

static_facade :tuesday?
is also valid.

You can specify multiple method names as long as they can share the same initializer arguments:

static_facade [:allow?, :deny?], :user, [:user_agent, :ip!]

Any block given to the class method will be passed on to the instance method.

"Static façade" is the least bad name for this pattern we've come up with. Suggestions are welcome.

method_object

NOTE: v4.0.0 made a breaking change!

static_facade
does exactly what
method_object
used to do; the new
method_object
no longer accepts a method name argument.

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.

This is a special case of

static_facade
for when you want a Method Object, and the class name itself will communicate the action it performs.

Example:

class CalculatePrice
  method_object :order

def call total * factor end

private

def total order.items.map(&:price).inject(:+) end

def factor 1 + rand end end

class Order def price CalculatePrice.call(self) end end

You could even do

CalculatePrice.(self)
if you like, since we're using the
call
convention
.

method_object :foo
is a shortcut for
static_facade :call, :foo

which is a shortcut for

pattr_initialize :foo

def self.call(foo) new(foo).call end

The

attr_initialize
notation for keyword arguments is also supported:

method_object :foo, [:bar, :baz!]

You don't have to specify arguments/readers if you don't want them: just

method_object
is also valid.

Any block given to the class method will be passed on to the instance method.

attr_implement

attr_implement :foo, :bar
defines nullary (0-argument) methods
foo
and
bar
that raise e.g.
"Implement a 'foo()' method"
.

attr_implement :foo, [:name, :age]
will define a binary (2-argument) method
foo
that raises
"Implement a 'foo(name, age)' method"
.

This is suitable for abstract methods in base classes, e.g. when using the template method pattern.

It's more or less a shortcut for

def my_method
  raise "Implement me in a subclass!"
end

though it is shorter, more declarative, gives you a clear message and handles edge cases you might not have thought about (see tests).

Note that you can also use this with modules, to effectively mix in interfaces:

module Bookable
  attr_implement :book, [:bookable]
  attr_implement :booked?
end

class Invoice include Bookable end

class Payment include Bookable end

cattr_implement

Like

attr_implement
but for class methods.

Example:

class TransportOrder
  cattr_implement :must_be_tracked?
end

attr_query

attr_query :foo?, :bar?
defines query methods like
foo?
, which is true if (and only if)
foo
is truthy.

attr_id_query

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.

Explicit mode

By default, attr_extras will add methods to every class and module.

This is not ideal if you're using attr_extras in a library: those who depend on your library will get those methods as well.

It'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:

require "attr_extras/explicit"

class MyLib extend AttrExtras.mixin

pattr_initialize :now_this_class_can_use_attr_extras end

Crucially, 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"
.

In explicit mode, you need to call

extend AttrExtras.mixin
in every class or module that wants the attr_extras methods.

Philosophy

Findability is a core value. Hence the long name

attr_initialize
, so you see it when scanning for the initializer; and the enforced questionmarks with
attr_id_query :foo?
, so you can search for that method.

Q & A

Why not use
Struct
instead of
pattr_initialize
?

See: "Struct inheritance is overused"

Why not use
private; attr_reader :foo
instead of
attr_private :foo
?

Other 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.

If 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.

Can I use attr_extras in
BasicObject
s?

No, sorry. It depends on various methods that

BasicObject
s don't have. Use a regular
Object
or make do without attr_extras.

Installation

Add this line to your application's

Gemfile
:
gem "attr_extras"

And then execute:

bundle

Or install it yourself as:

gem install attr_extras

Running the tests

Run them with:

rake

Or to see warnings (try not to have any):

RUBYOPT=-w rake

You can run an individual test using the m gem:

m spec/attr_extras/attr_initialize_spec.rb:48

The tests are intentionally split into two test suites for reasons described in

Rakefile
.

Contributors

License

MIT

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.