Attributes DSL

Gem Version Build Status Dependency Status Code Climate Coverage Inline docs

Lightweight DSL to define PORO attributes.

Uses immutable (deeply frozen) instances via ice_nine gem.

Synopsis

require "attributes_dsl"

class User
  extend AttributesDSL

  # `name` is required should be symbolized
  attribute :name, required: true do |value|
    value.to_s.to_sym
  end

  # `sex` is optional and set to `:male` by default.
  # It can be set to either :male or :female
  attribute :sex, default: :male do |value|
    (value == :male )? :male : :female
  end

  # `age` is optional and set to `nil` by default.
  # Then it is converted to integer
  attribute :age, &:to_i

  # `position` is optional and set to `nil` by default
  attribute :position

  # All other attributes are ignored
end

user = User.new(name: "Jane", sex: :women, age: "26", place: "Moscow")
user.attributes
# => { name: :Jane, sex: :female, age: 26, position: nil }

# Aliases for attributes[:some_attribute]
user.name     # => :Jane
user.sex      # => :female
user.age      # => 26
user.position # => nil

# Required attributes should be assigned:
user = User.new(sex: :women, age: "26", place: "Moscow")
# => #<ArgumentError "Undefined attributes: name">

Additional Details

The attribute class method takes the name and 2 options:

  • :default for the default value (otherwise nil);
  • :required to declare the attribute as required. It will be ignored if a default value is provided!

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

Also notice, that 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: 0, position: nil }
user.name # => :John

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

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:

-------------------------------------------------
               anima    202.863k (± 5.1%) i/s -      1.023M
              kwattr    171.461k (± 5.3%) i/s -    855.680k
     fast_attributes    156.596k (± 2.5%) i/s -    785.792k
      attributes_dsl     58.966k (± 4.6%) i/s -    296.514k
         active_attr     58.322k (± 2.8%) i/s -    293.205k
              virtus     45.734k (± 2.7%) i/s -    228.960k

Comparison:
               anima:   202862.9 i/s
              kwattr:   171460.9 i/s - 1.18x slower
     fast_attributes:   156596.3 i/s - 1.30x slower
      attributes_dsl:    58966.5 i/s - 3.44x slower
         active_attr:    58321.6 i/s - 3.48x slower
              virtus:    45734.3 i/s - 4.44x 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 customizable coercers).

It is 2 times slower than fast_attributes that has no coercer and default values. And it is 3-3.5 times slower than anima and kwattr that provides only the base settings.

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 1.9+.

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

100% mutant-proof covered.

Contributing

  • Read the STYLEGUIDE
  • Fork the project
  • Create your feature branch (git checkout -b my-new-feature)
  • Add tests for it
  • Run rake mutant or rake exhort to ensure 100% mutant coverage
  • Commit your changes (git commit -am '[UPDATE] Add some feature')
  • Push to the branch (git push origin my-new-feature)
  • Create a new Pull Request

License

See the MIT LICENSE.