{"id":19437388,"url":"https://github.com/reenhanced/plyushkin","last_synced_at":"2026-03-02T11:36:32.059Z","repository":{"id":22353850,"uuid":"25689826","full_name":"reenhanced/plyushkin","owner":"reenhanced","description":null,"archived":false,"fork":false,"pushed_at":"2014-10-24T13:37:39.000Z","size":149,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-01-07T21:17:30.313Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reenhanced.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-10-24T13:28:58.000Z","updated_at":"2020-11-18T15:16:26.000Z","dependencies_parsed_at":"2022-08-05T17:15:27.406Z","dependency_job_id":null,"html_url":"https://github.com/reenhanced/plyushkin","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reenhanced%2Fplyushkin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reenhanced%2Fplyushkin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reenhanced%2Fplyushkin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reenhanced%2Fplyushkin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reenhanced","download_url":"https://codeload.github.com/reenhanced/plyushkin/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240619451,"owners_count":19830206,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-10T15:14:31.076Z","updated_at":"2025-11-19T11:03:08.446Z","avatar_url":"https://github.com/reenhanced.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Plyushkin\n\n[![Code Climate](https://codeclimate.com/github/OnlifeHealth/plyushkin.png)](https://codeclimate.com/github/OnlifeHealth/plyushkin) [![Gem Version](https://badge.fury.io/rb/plyushkin.svg)](http://badge.fury.io/rb/plyushkin)\n[![Build Status](https://travis-ci.org/OnlifeHealth/plyushkin.png)](https://travis-ci.org/OnlifeHealth/plyushkin)\n\nPlyushkin provides a way to capture historical data in an ActiveRecord class.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'plyushkin'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install plyushkin\n\n## Usage\n\nConfigure the backend service that plyushkin will use in your environments/\u0026lt;environment\u0026gt;.rb file.  \nFor example, to configure the stub service for running specs, the following code would go in your \nconfig/environments/test.rb file of a Rails application.\n\n    config.before_initialize do |c|\n      Plyushkin::Service.service = Plyushkin::Service::Stub.new\n    end\n\nTo use plyushkin against a live web service,\n\n    config.before_initialize do |c|\n      Plyushkin::Service.service =\n        Plyushkin::Service::Web.new(:url =\u003e 'http://yourservice.com')\n    end\n\n## Quick start\nTo add a property, use the `hoards` class macro on a class that inherits from ActiveRecord::Base.\n\n    class Vehicle \u003c ActiveRecord::Base\n      hoards :mechanic\n    end\n    \nThis will add a mechanic attribute to the Vehicle class. To set the mechanic for the vehicle\n\n    v = Vehicle.new\n    v.mechanic.create(:value =\u003e 'Mike')\n\nThis will update the in-memory version of the vehicle, but has not yet persisted the change.  To\nsave the change to the plyushkin-service\n\n    v.save\n\nTo get the value of the vehicle's mechanic\n\n    v.mechanic.last.value  # =\u003e 'Mike'\n\nTo get the date that the mechanic was last set\n\n    v.mechanic.last.date   # =\u003e Wed, 09 Jul 2014 11:52:12 -0500\n\nEvery Plyushkin value has a `date` attribute.\n\nOnce we change the mechanic on the vehicle, we will now have historical data and can view all the\nmechanics that worked on the vehicle.\n\n    v.mechanic.create(:value =\u003e 'Joe')\n    v.mechanic.all.map(\u0026:value) # =\u003e ['Mike', 'Joe']\n\n#### Setting past values\n\nTo store a property using a DateTime other than the current time\n\n    v.mechanic.create(:value =\u003e 'Sally', :date =\u003e 3.days.ago)\n    v.mechanic.last.date # =\u003e Sun, 06 Jul 2014 11:52:12 -0500\n\n#### Value types\n\nIn the example above, we needed to use `last` and `all` methods to see the mechanic.  This is\nbecause the property getter returns a `Plyushkin::Property`.  A property consists of\ninstances of classes that derive from `Plyushkin::BaseValue`.\n\n`Plyushkin::BaseValue` provides basic value functionality.  It provides a `date` attribute, that is\nrequired for all Plyushkin values.  And, it provides four formatters, `to_i`, `to_f`,\n`to_date` and `to_bool`.\n\nWhen not specifying a value type when calling `hoards`, Plyushkin will use a \n`Plyushkin::StringValue` as the value type.  In addition to the base implementation, this basic\nimplementation includes one attribute, `value`, which uses no formatter.\n\nIn most applications, you will not want to use `Plyushkin::StringValue`, and instead would want\nto create your own value type implementation that derives from `Plyushkin::BaseValue` and use custom\nvalue types.\n\n##### Creating a custom value type\n\nIn our vehicle example, if we wanted to capture oil change history, we would start by creating a\ncustom value type.\n\n    class OilChangeValue \u003c Plyushkin::BaseValue\n      persisted_attr :mileage, :formatter =\u003e :to_i\n      persisted_attr :oil_type\n    end\n\nThe `OilChangeValue` will have a mileage attribute that uses the `to_i` formatter.  Formatters attempt to\nconvert the attribute assignment to a specified format.  If no formatter is provided, the attribute will be\nstored as a string.\n\nWe can now add this to our vehicle\n\n    class Vehicle \u003c ActiveRecord::Base\n      hoards :mechanic\n      hoards :oil_change, :type =\u003e OilChangeValue\n    end\n\nTo set the oil_change\n\n    v.oil_change.create(:mileage =\u003e '1234', :oil_type =\u003e '10W30')\n\nTo get the latest oil change details\n\n    v.oil_change.last.mileage  # =\u003e 1234\n    v.oil_change.last.oil_type # =\u003e '10W30'\n\n##### Specifiying a callback after a value is stored\n\nWhen defining a hoards property, you can set a callback for after the value is persisted.  For example,\nif the vehicle table contains a `next_oil_change_mileage` column, we might want to update it whenever\nan oil_change is saved.  \n\n    class Vehicle \u003c ActiveRecord::Base\n      hoards :oil_change, :type =\u003e OilChangeValue,\n             :after_create =\u003e :calculate_next_oil_change_mileage\n\n      def calculate_next_oil_change_mileage\n        next_oil_change_mileage = oil_change.last.mileage + 3000\n      end\n    end\n\n##### Ignoring unchanged values\n\nThere may be a case where we don't need to track history when a value is set, but is the same as the\nprevious value.  For example, if the mechanic for your last maintenance is Mike and Mike again\nperforms the maintenance, we don't need two data points recorded.\n\nIn the current configuration, calling create twice would create two values\n    \n    v.mechanic.create(:value =\u003e 'Joe')\n    v.mechanic.create(:value =\u003e 'Mike')\n    v.mechanic.create(:value =\u003e 'Mike')\n    v.mechanic.all.map(\u0026:value) # =\u003e [ 'Joe', 'Mike', 'Mike' ]\n\nBy setting the `:ignore_unchanged_values` option, we can change this behavior.\n\n    class Vehicle \u003c ActiveRecord::Base\n      hoards :mechanic, :ignore_unchanged_values =\u003e true\n    end\n\n    v.mechanic.create(:value =\u003e 'Joe')\n    v.mechanic.create(:value =\u003e 'Mike')\n    v.mechanic.create(:value =\u003e 'Mike')\n    v.mechanic.all.map(\u0026:value) # =\u003e [ 'Joe', 'Mike' ]\n\nIn this example, the date of the last value will be the date that `mechanic` was first assigned 'Mike'.\n\n###### Validation\n\nValidation is done with `ActiveModel::Validations`.  This is included in `Plyushkin::BaseValue`.\n\n    class OilChangeValue \u003c Plyushkin::BaseValue\n      persisted_attr :oil_type\n      persisted_attr :mileage\n\n      validates :oil_type, :inclusion    =\u003e { :in =\u003e ['10W30', '5W40'] }\n      validates :mileage,  :numericality =\u003e { :only_integer             =\u003e true,\n                                              :greater_than_or_equal_to =\u003e 0 }\n    end\n\n###### Adding behavior\n\n`Plyushkin::BaseValue` and it's subclasses are classes and additional behavior can be added to \nencapsulate functionality.\n\n    class OilChange \u003c Plyushkin::BaseValue\n      persisted_attr :mileage\n\n      def mileage_as_km\n        mileage / 0.6214\n      end\n    end\n\n    v.oil_change.create(:mileage =\u003e 10000)\n    v.oil_change.last.mileage       # =\u003e 10000\n    v.oil_change.last.mileage_as_km # =\u003e 6214\n\n##### Accessing a property that has not had any values assigned\n\n`Plyushkin::NilValue` is a special case used when no value has been assigned to a property yet.\n\n    v = Vehicle.new\n    v.mechanic.last            # =\u003e Plyushkin::NilValue\n    v.mechanic.last.value      # =\u003e nil\n    v.oil_change.last          # =\u003e Plyushkin::NilValue\n    v.oil_change.last.mileage  # =\u003e nil\n    v.oil_change.last.oil_type # =\u003e nil\n\nThis is necessary so that consumers of the property do not need to check if a value is nil before trying\nto access an attribute of the value.\n\nIn addition, `Plyushkin::Property` has a property `empty?` to indicate whether any values have been assigned\n\n    v.mechanic.empty?   # =\u003e true\n    v.oil_change.empty? # =\u003e true\n\n## Testing\n\nPlyushkin provides RSpec matchers for testing class macros.  To use these matchers, \nadd `config.include Plyushkin::Test::Matchers` to your RSpec.configure in spec_helper.\n\n#### Testing custom value types\nTo test Plyushkin configuration in your custom value type:\n\n    describe OilChangeValue do\n      it { should persist_attribute(:mileage) }\n      it { should persist_attribute(:mileage).with_format(:to_i) }\n      it { should_not persist_attribute(:air_filter) }\n      it { should persist_attribute(:oil_type) }\n      it { should_not persist_attribute(:oil_type).with_format(:to_f) }\n\n      # RSpec Shoulda matchers also work to test validations here.\n    end\n\nTo test Plyushkin configuration in your model:\n\n    describe Vehicle do\n      it { should hoard(:mechanic) }\n      it { should hoard(:mechanic).and_ignore_unchanged_values } \n      it { should hoard(:oil_change).of_type(OilChangeValue) }\n      it { should hoard(:oil_change).of_type(OilChangeValue).\n        and_after_create_call(:calculate_next_oil_change_mileage) }\n    end\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freenhanced%2Fplyushkin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freenhanced%2Fplyushkin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freenhanced%2Fplyushkin/lists"}