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:
:defaultfor the default value (otherwisenil).:requiredto declare the attribute as required. It will be ignored if a default value is provided!:readerdefines whether the attribute reader should be defined (trueby default).:onlydefines allowed values. You can use procs (-> v { v.to_s }), ranges (18..25), regexps (/male|female/), constants (String), or arrays ([:male, :female]) to define a restriction.:exceptdefines forbidden values.:coercerdefines a procedure to convert assigned value.
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
- Read the STYLEGUIDE
- Fork the project
- Create your feature branch (
git checkout -b my-new-feature) - Add tests for it
- 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.