Let me introduce you to my new library called Yukata. It is a light weight Ruby attribute library that is configurable and extendable.
Virtus has been a huge inspiration for this library. I enjoyed the DSL it offered, while allowing me to have a quick way to make data objects.
Here is an example on how to utilize Yukata:
class Person < Yukata::Base
attribute :first_name, String
attribute :last_name, String
attribute :born_on, DateTime
attribute :married, Boolean, default: -> { false }
end
The #attribute
method is straight forward with its meaning. It is dynamically
creating both getter and setter methods for the object. It can be thought of as
a fancy attr_accessor
but with a few extra features. It provides a fast way to
discover what data type can be expected for that attribute.
Example Usage
When using Yukata, the the initializer expects a hash to be provided or a class
that behaves like a Hash
.
john = Person.new({
:first_name => 'John',
'last_name' => 'Doe',
:born_on => '1969-01-16T00:00:00+00:00'
})
Yukata will take the hash and assign the values to their respective attribute keys. If a setter method is defined, then a corresponding value can be passed as well.
class Foo < Yukata::Base
attr_accessor :bar
attribute :qux, String
attribute :baz, String, writer: false
def baz=(value)
@baz = value.to_s
end
end
foo = Foo.new({
bar: 'woot',
qux: 'herp',
baz: 'derp'
})
foo.bar # => 'woot'
foo.qux # => 'herp'
foo.bas # => 'derp'
foo.attributes # => { bar: 'woot', qux: 'herp' }
If a :coerce => false
is passed, then Yukata will not attempt to coerce that
attribute and leave it as is. This can be handy if a custom coercion is desired
for the specific model. Here is an example:
class Episode < Yukata::Base
attribute :season, Integer
attribute :number, Integer
attribute :name, String, coerce: false
# @override overides the yukata definition
def name=(value)
@name = '%sx%s - %s' % [@season, @number, value]
end
end
episode = Episode.new({ season: 1, number: 1 })
episode.name = 'Foo Bar'
episode.name # => '1x1 - Foo Bar'
Now, remember just because there is access directly to instance variables does not mean it is okay to abuse them. With great power comes great responsibility, this means I am not responsible for your mistakes.
Setting Attribute Defaults
Sometimes the objects need default values if it is not set. Defaults are lazily loaded. They will only be set once the getter method is called.
class Book < Yukata::Base
attribute :name, String
attribute :created_at, DateTime, default: -> { DateTime.now }
end
Registering Custom Coercions
This library only comes with basic coercers. I tried to make as little assumptions about the data coming in as I could. I believe that the consumer of the library should be the one who defines the coercions.
If the value can not be coerced, it is simply passed through and left alone.
Yukata.coercer.register(String, Array) do |string, target|
string.split(' ')
end
Optional Readers and Writers
When declaring an attribute, both the reader and writer can be skipped. There is a use case where this would be handy.
class Book < Yukata::Base
attribute :title, String, writer: false, reader: false
def title=(value)
@title = value.to_s
end
def title
@title
end
end
This is a bit contrived, but it demonstrates the following:
- The expected return data type for
#title
is aString
. - Custom coercer is defined.
- The attribute will be included when
#attributes
is called onBook
.
If :writer => false
is provided, there would be no need to include
:coerce => false
since the coercion only takes place when the value is being
set on the object.
Conclusion
I wrote this library becaues I wanted to see how Virtus accomplished this task and how I could go about doing it differently. This is a highly configurable library that can be used to put your fat models on a diet.