Plyushkin
Plyushkin provides a way to capture historical data in an ActiveRecord class.
Installation
Add this line to your application's Gemfile:
gem 'plyushkin'
And then execute:
$ bundle
Or install it yourself as:
$ gem install plyushkin
Usage
Configure the backend service that plyushkin will use in your environments/<environment>.rb file.
For example, to configure the stub service for running specs, the following code would go in your
config/environments/test.rb file of a Rails application.
config.before_initialize do |c|
Plyushkin::Service.service = Plyushkin::Service::Stub.new
end
To use plyushkin against a live web service,
config.before_initialize do |c|
Plyushkin::Service.service =
Plyushkin::Service::Web.new(:url => 'http://yourservice.com')
end
Quick start
To add a property, use the hoards
class macro on a class that inherits from ActiveRecord::Base.
class Vehicle < ActiveRecord::Base
hoards :mechanic
end
This will add a mechanic attribute to the Vehicle class. To set the mechanic for the vehicle
v = Vehicle.new
v.mechanic.create(:value => 'Mike')
This will update the in-memory version of the vehicle, but has not yet persisted the change. To save the change to the plyushkin-service
v.save
To get the value of the vehicle's mechanic
v.mechanic.last.value # => 'Mike'
To get the date that the mechanic was last set
v.mechanic.last.date # => Wed, 09 Jul 2014 11:52:12 -0500
Every Plyushkin value has a date
attribute.
Once we change the mechanic on the vehicle, we will now have historical data and can view all the mechanics that worked on the vehicle.
v.mechanic.create(:value => 'Joe')
v.mechanic.all.map(&:value) # => ['Mike', 'Joe']
Setting past values
To store a property using a DateTime other than the current time
v.mechanic.create(:value => 'Sally', :date => 3.days.ago)
v.mechanic.last.date # => Sun, 06 Jul 2014 11:52:12 -0500
Value types
In the example above, we needed to use last
and all
methods to see the mechanic. This is
because the property getter returns a Plyushkin::Property
. A property consists of
instances of classes that derive from Plyushkin::BaseValue
.
Plyushkin::BaseValue
provides basic value functionality. It provides a date
attribute, that is
required for all Plyushkin values. And, it provides four formatters, to_i
, to_f
,
to_date
and to_bool
.
When not specifying a value type when calling hoards
, Plyushkin will use a
Plyushkin::StringValue
as the value type. In addition to the base implementation, this basic
implementation includes one attribute, value
, which uses no formatter.
In most applications, you will not want to use Plyushkin::StringValue
, and instead would want
to create your own value type implementation that derives from Plyushkin::BaseValue
and use custom
value types.
Creating a custom value type
In our vehicle example, if we wanted to capture oil change history, we would start by creating a custom value type.
class OilChangeValue < Plyushkin::BaseValue
persisted_attr :mileage, :formatter => :to_i
persisted_attr :oil_type
end
The OilChangeValue
will have a mileage attribute that uses the to_i
formatter. Formatters attempt to
convert the attribute assignment to a specified format. If no formatter is provided, the attribute will be
stored as a string.
We can now add this to our vehicle
class Vehicle < ActiveRecord::Base
hoards :mechanic
hoards :oil_change, :type => OilChangeValue
end
To set the oil_change
v.oil_change.create(:mileage => '1234', :oil_type => '10W30')
To get the latest oil change details
v.oil_change.last.mileage # => 1234
v.oil_change.last.oil_type # => '10W30'
Specifying a callback after a value is stored
When defining a hoards property, you can set a callback for after the value is persisted. For example,
if the vehicle table contains a next_oil_change_mileage
column, we might want to update it whenever
an oil_change is saved.
class Vehicle < ActiveRecord::Base
hoards :oil_change, :type => OilChangeValue,
:after_create => :calculate_next_oil_change_mileage
def calculate_next_oil_change_mileage
next_oil_change_mileage = oil_change.last.mileage + 3000
end
end
Filters
Sometimes it is useful to filter hoarded values. For example, we may want the ability to soft-delete values from the historical properties (hard deletes is against the phylosophy of plyushkin), but we can obtain similar results by:
class OilChangeValue < Plyushkin::BaseValue
persisted_attr :mileage, :formatter => :to_i
persisted_attr :oil_type
persisted_attr :is_deleted, :formatter => :to_bool
end
class Vehicle < ActiveRecord::Base
hoards :oil_change, :type => OilChangeValue, :filter => :soft_delete_filter
def soft_delete_filter(value)
value.is_deleted != true
end
end
Now all values for the oil_change
property will be filtered to exclude any values where is_deleted == true
.
You can also specify a class level filter for all hoards that a class might implement by using:
class Vehicle < ActiveRecord::Base
filter_hoards_by :soft_delete_filter
end
This would cause all hoards to be filtered using the soft_delete_filter
, unless this behavior was
overridden using a :filter
parameter on the individual hoards.
If at anytime you wish to get unfiltered data from a property, you can still do that using the
:unfiltered
option:
vehicle.oil_change.all(:unfiltered => true) #=> unfiltered data
Ignoring unchanged values
There may be a case where we don't need to track history when a value is set, but is the same as the previous value. For example, if the mechanic for your last maintenance is Mike and Mike again performs the maintenance, we don't need two data points recorded.
In the current configuration, calling create twice would create two values
v.mechanic.create(:value => 'Joe')
v.mechanic.create(:value => 'Mike')
v.mechanic.create(:value => 'Mike')
v.mechanic.all.map(&:value) # => [ 'Joe', 'Mike', 'Mike' ]
By setting the :ignore_unchanged_values
option, we can change this behavior.
class Vehicle < ActiveRecord::Base
hoards :mechanic, :ignore_unchanged_values => true
end
v.mechanic.create(:value => 'Joe')
v.mechanic.create(:value => 'Mike')
v.mechanic.create(:value => 'Mike')
v.mechanic.all.map(&:value) # => [ 'Joe', 'Mike' ]
In this example, the date of the last value will be the date that mechanic
was first assigned 'Mike'.
Validation
Validation is done with ActiveModel::Validations
. This is included in Plyushkin::BaseValue
.
class OilChangeValue < Plyushkin::BaseValue
persisted_attr :oil_type
persisted_attr :mileage
validates :oil_type, :inclusion => { :in => ['10W30', '5W40'] }
validates :mileage, :numericality => { :only_integer => true,
:greater_than_or_equal_to => 0 }
end
Adding behavior
Plyushkin::BaseValue
and it's subclasses are classes and additional behavior can be added to
encapsulate functionality.
class OilChange < Plyushkin::BaseValue
persisted_attr :mileage
def mileage_as_km
mileage / 0.6214
end
end
v.oil_change.create(:mileage => 10000)
v.oil_change.last.mileage # => 10000
v.oil_change.last.mileage_as_km # => 6214
Accessing a property that has not had any values assigned
Plyushkin::NilValue
is a special case used when no value has been assigned to a property yet.
v = Vehicle.new
v.mechanic.last # => Plyushkin::NilValue
v.mechanic.last.value # => nil
v.oil_change.last # => Plyushkin::NilValue
v.oil_change.last.mileage # => nil
v.oil_change.last.oil_type # => nil
This is necessary so that consumers of the property do not need to check if a value is nil before trying to access an attribute of the value.
In addition, Plyushkin::Property
has a property empty?
to indicate whether any values have been assigned
v.mechanic.empty? # => true
v.oil_change.empty? # => true
Testing
Plyushkin provides RSpec matchers for testing class macros. To use these matchers,
add config.include Plyushkin::Test::Matchers
to your RSpec.configure in spec_helper.
Testing custom value types
To test Plyushkin configuration in your custom value type:
describe OilChangeValue do
it { should persist_attribute(:mileage) }
it { should persist_attribute(:mileage).with_format(:to_i) }
it { should_not persist_attribute(:air_filter) }
it { should persist_attribute(:oil_type) }
it { should_not persist_attribute(:oil_type).with_format(:to_f) }
# RSpec Shoulda matchers also work to test validations here.
end
To test Plyushkin configuration in your model:
describe Vehicle do
it { should hoard(:mechanic) }
it { should hoard(:mechanic).and_ignore_unchanged_values }
it { should hoard(:oil_change).of_type(OilChangeValue) }
it { should hoard(:oil_change).of_type(OilChangeValue).
and_after_create_call(:calculate_next_oil_change_mileage) }
end
Contributing
- Fork it
- 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