FastAttributes
Motivation
There are already a lot of good and flexible gems which solve a similar problem, allowing attributes to be defined with their types, for example: virtus or attrio. However, the disadvantage of these gems is performance. So, the goal of fast_attributes
is to provide a simple solution which is fast, understandable and extendable.
This is the performance benchmark of fast_attributes
compared to other popular gems.
Comparison:
FastAttributes: without values : 1528209.4 i/s
FastAttributes: integer values for integer attributes: 88794.2 i/s - 17.21x slower
FastAttributes: string values for integer attributes : 77673.3 i/s - 19.67x slower
Virtus: integer values for integer attributes : 21104.7 i/s - 72.41x slower
Attrio: integer values for integer attributes : 11932.2 i/s - 128.07x slower
Attrio: string values for integer attributes : 11007.2 i/s - 138.84x slower
Virtus: without values : 10151.0 i/s - 150.55x slower
Attrio: without values : 7164.3 i/s - 213.31x slower
Virtus: string values for integer attributes : 3195.6 i/s - 478.22x slower
Installation
Add this line to your application's Gemfile:
gem 'fast_attributes'
And then execute:
$ bundle
Or install it yourself as:
$ gem install fast_attributes
Usage
Define getter/setter methods:
class Book
extend FastAttributes
attribute :title, :name, String
attribute :pages, Integer
attribute :authors, Array
attribute :published, Date
attribute :sold, Time
attribute :finished, DateTime
end
book = Book.new
book.title = 'There and Back Again'
book.name = 'The Hobbit'
book.pages = '200'
book.authors = 'Tolkien'
book.published = '1937-09-21'
book.sold = '2014-06-25 13:45'
book.finished = '1937-08-20 12:35'
#<Book:0x007f9a0110be20
@authors=["Tolkien"],
@finished=
#<DateTime: 1937-08-20T12:35:00+00:00 ((2428766j,45300s,0n),+0s,2299161j)>,
@name="The Hobbit",
@pages=200,
@published=#<Date: 1937-09-21 ((2428798j,0s,0n),+0s,2299161j)>,
@sold=2014-06-25 13:45:00 +0200,
@title="There and Back Again">
To generate initialize
and attributes
methods, attribute definition should be wrapped with define_attributes
:
class Book
extend FastAttributes
define_attributes initialize: true, attributes: true do
attribute :title, :name, String
attribute :pages, Integer
attribute :authors, Array
attribute :published, Date
attribute :sold, Time
attribute :finished, DateTime
end
end
book = Book.new(
title: 'There and Back Again',
name: 'The Hobbit',
pages: '200',
authors: 'Tolkien',
published: '1937-09-21',
sold: '2014-06-25 13:45',
finished: '1937-08-20 12:35'
)
book.attributes
{"title"=>"There and Back Again",
"name"=>"The Hobbit",
"pages"=>200,
"authors"=>["Tolkien"],
"published"=>#<Date: 1937-09-21 ((2428798j,0s,0n),+0s,2299161j)>,
"sold"=>2014-06-25 13:45:00 +0200,
"finished"=>
#<DateTime: 1937-08-20T12:35:00+00:00 ((2428766j,45300s,0n),+0s,2299161j)>}
Default values
Requires define_attributes
to be used with initialize: true
(this is where the default values are set):
class Book
extend FastAttributes
define_attributes initialize: true do
attribute :author, String, default: "Some String"
end
end
book = Book.new
book.attributes
{"author" => "Some String"}
Attributes via accessors
Sometimes you want attributes
to return the value of your accessors, rather than the instance variables.
This is slower than using ivars directly, so use attributes: :accessors
:
class Book
extend FastAttributes
define_attributes attributes: :accessors do
attribute :author, String
end
def
@author || "No author set"
end
end
book = Book.new
book.attributes
{"author" => "No author set"}
Custom Type
It's easy to add a custom attribute type.
FastAttributes.set_type_casting(OpenStruct, 'OpenStruct.new(name: %s)')
class Book
extend FastAttributes
attribute :author, OpenStruct
end
book = Book.new
book. = 'Rowling'
book.
# => #<OpenStruct name="Rowling">
Notice, that second parameter is a string. It's necessary because this code is compiled into a ruby method in runtime. The placeholder %s
represents a value which this method accepts.
It's possible to refer to a placeholder several times.
Size = Class.new(Array)
FastAttributes.set_type_casting Size, <<-EOS
Size[%s, %s]
EOS
class Square
extend FastAttributes
attribute :size, Size
end
square = Square.new
square.size = 5
square.size
# => [5, 5]
Method FastAttributes.set_type_casting
generates the following template:
FastAttributes.set_type_casting String, 'String(%s)'
# begin
# case %s
# when nil then nil
# when String then %s
# else String(%s)
# end
# rescue => e
# raise FastAttributes::TypeCast::InvalidValueError, %(Invalid value "\#{%s}" for attribute "%a" of type "String")
# end
and when the attribute is defined, fast_attributes
generates the following setter method:
class A
extend FastAttributes
attribute :name, String
end
# def name=(value)
# @name = begin
# case value
# when nil then nil
# when String then value
# else String(value)
# end
# rescue => e
# raise FastAttributes::TypeCast::InvalidValueError, %(Invalid value "#{value}" for attribute "name" of type "String")
# end
# end
Notice, placeholder %a
represents method name. Also, set_type_casting
method generates lenient date type. See Lenient Data Types section.
If you need to conrol the whole type casting process, you can use the following DSL:
FastAttributes.type_cast String do # begin
# case String
from 'nil', to: 'nil' # when nil then nil
from 'String', to: '%s' # when String then %s
otherwise 'String(%s)' # else String(%s)
# end
on_error 'TypeError', act: 'nil' # rescue TypeError => e
# nil
on_error 'StandardError', act: '""' # rescue StandardError => e
# ""
end # end
Lenient Data Types
It's also possible to define a lenient data type which doesn't correspond to any of ruby classes:
FastAttributes.type_cast :yes_no do
from '"yes"', to: 'true'
from '"no"', to: 'false'
otherwise 'nil'
end
class Order
extend FastAttributes
attribute :terms_of_service, :yes_no
end
order = Order.new
order.terms_of_service = 'yes'
order.terms_of_service
# => true
order.terms_of_service = 'no'
order.terms_of_service
# => false
order.terms_of_service = 42
order.terms_of_service
# => nil
All default data types have lenient notation:
class Book
extend FastAttributes
attribute :title, :string
attribute :pages, :integer
attribute :price, :big_decimal
attribute :authors, :array
attribute :published, :date
attribute :sold, :time
attribute :finished, :date_time
attribute :rate, :float
attribute :active, :boolean
end
Notice, Boolean
attribute can be defined only via symbol, fast_attribute
doesn't create Boolean
class.
Extensions
- fast_attributes-uuid - adds support of
UUID
tofast_attributes
Contributing
- Fork it ( http://github.com/applift/fast_attributes/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request