Attributes DSL

Lightweight DSL to define PORO attributes.

Uses immutable (deeply frozen) instances via ice_nine gem.

Synopsis

require "attributes_dsl"

class User
  extend AttributesDSL

  attribute :name, required: true, coerce: -> v { v.to_s }
  attribute :sex,  default: :male, only: /male|female/
  attribute :age,  only: 18..25
  attribute :city, reader: false, except: %w(Moscow)
end

user = User.new(name: :Jane, sex: :female, age: 24, city: "Kiev")
user.attributes
# => { name: :Jane, sex: :female, age: 26, city: "Kiev" }

# Aliases for attributes[:some_attribute]
user.name # => "Jane"
user.sex  # => :female
user.age  # => 26
user.city # => #<NoMethodError ...>

Additional Details

Attribute declaration

The attribute class method takes the name and 3 options:

It is also takes the block, used to coerce a value. The coercer is applied to the default value too.

Instance methods

Instance methods (like #name) are just aliases for the corresponding value of the #attributes hash. Instance variables aren’t defined for them (to ensure syncronization between #name and #attributes[:name]):

user = User.new(name: "John")
user.attributes # => { name: :John, sex: :male, age: nil, city: nil }

user.name # => :John
user.instance_variable_get :@name # => nil

Inheritance

Subclasses inherits attributes of the superclass:

class UserWithRole < User
  attribute :role, default: :user
end

user = UserWithRole.new(name: "Sam")
user.attributes
user.attributes
# => { name: :John, sex: :male, age: nil, city: nil, role: :user }

Undefining Attributes

This feature is not available (and it won’t be).

The reason is that a subclass should satisfy a contract of its superclass, including the existence of attributes, declared by the superclass. All you can do is reload attribute definition in a subclass:

class Person < User
  attribute :name, &:to_s
end

user = Person.new(name: :Sam)
user.attributes
# => { name: "Sam", sex: :male, age: 0, position: nil }

Freezing

You’re free to redefine attributes (class settings are used by the initializer only):

user.attributes[:name] = "Jim"
user.attributes # => { name: "Jim", sex: :male, age: 0, position: nil }
user.name # => "Jim"

But if you (like me) prefer instance immutability, you can deeply freeze instances safely:

require "ice_nine"

class User
  # ... staff like before

  def initializer(attributes)
    super
    IceNine.deep_freeze(self)
  end
end

args = { user: "Joe" }

user = User.new(args)
user.frozen?            # => true
user.attributes.frozen? # => true

# "Safely" means:
args.frozen? # => false

Freezing instances to exclude side effects is a part of my coding style. That’s why the gem doesn’t (and won’t do) care about changing attributes after initialization.

Benchmarks

The list of gems to compare has been taken from Idiosyncratic Ruby #18 by Jan Lelis. I’ve selected only those gems that support initialization from hash.

Look at the benchmark source for details.

The results are following:

-------------------------------------------------
              kwattr:   183416.9 i/s
               anima:   169647.3 i/s - 1.08x slower
     fast_attributes:   156036.2 i/s - 1.18x slower
      attributes_dsl:    74495.9 i/s - 2.46x slower
         active_attr:    74469.4 i/s - 2.46x slower
              virtus:    46587.0 i/s - 3.94x slower

Results above are pretty reasonable.

The gem is faster than virtus that has many additional features.

It is as fast as active_attrs (but has more options).

It is 2 times slower than fast_attributes that has no coercer and default values. And it is 2.5 times slower than anima and kwattr that provide only simple attribute’s declaration.

Installation

Add this line to your application’s Gemfile:

# Gemfile
gem "attributes_dsl"

Then execute:

bundle

Or add it manually:

gem install attributes_dsl

Compatibility

Tested under rubies compatible to MRI 2.0+.

Uses RSpec 3.0+ for testing and hexx-suit for dev/test tools collection.

Contributing

License

See the MIT LICENSE.