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
# `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
Attribute declaration
The attribute class method takes the name and 2 options:
:defaultfor the default value (otherwisenil);:requiredto 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.
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: 0, position: nil }
user.name # => :John
# but
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
# => { name: :Sam, sex: :male, age: 0, position: 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:
-------------------------------------------------
anima 211.638k (
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 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 mutantorrake exhortto 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.