Simple attributes for a non-ActiveRecord model.
- Stores attributes in instance variables.
- Type casting and checking.
- Dirty tracking.
- List attribute names and values.
- Default values for attributes
- Handles integers, floats, booleans, strings and times - a set of types that are very easy to persist to and parse from JSON.
- Supports efficient serialization of attributes to JSON.
- Mass assignment - handy for initializers.
Why not Virtus? Virtus doesn't provide dirty tracking, and doesn't integrate with ActiveModel::Dirty. So if you're not using ActiveRecord, but you need attributes with dirty tracking, ModelAttribute may be what you're after. For example, it works very well for a model that fronts an HTTP web service, and you want dirty tracking so you can PATCH appropriately.
Also in favor of ModelAttribute:
- It's simple - less than 200 lines of code.
- It supports efficient serialization and deserialization to/from JSON.
Integrating with Rails
If you're using ModelAttribute in a Rails application, you will probably want to
augment your model with other methods to make it behave more like
ActiveRecord. ActiveModel provides a very useful set of mixins,
described in the Rails guide. You can also see an example
of the methods we found useful at Yammer described in this blog
post, with full source in this Gist.
Usage
require 'model_attribute' class User extend ModelAttribute attribute :id, :integer attribute :paid, :boolean attribute :name, :string attribute :created_at, :time attribute :grades, :json def initialize(attributes = {}) set_attributes(attributes) end end User.attributes # => [:id, :paid, :name, :created_at, :grades] user = User.new user.attributes # => {:id=>nil, :paid=>nil, :name=>nil, :created_at=>nil, :grades=>nil} # An integer attribute user.id # => nil user.id = 3 user.id # => 3 # Stores values that convert cleanly to an integer user.id = '5' user.id # => 5 # Protects you against nonsense assignment user.id = '5error' ArgumentError: invalid value for Integer(): "5error" # A boolean attribute user.paid # => nil user.paid = true # Booleans also define a predicate method (ending in '?') user.paid? # => true # Conversion from strings used by databases. user.paid = 'f' user.paid # => false user.paid = 't' user.paid # => true user.paid = 'false' user.paid # => false user.paid = 'true' user.paid # => true # A :time attribute user.created_at = Time.now user.created_at # => 2015-01-08 15:57:05 +0000 # Also converts from other reasonable time formats user.created_at = "2014-12-25 14:00:00 +0100" user.created_at # => 2014-12-25 13:00:00 +0000 user.created_at = Date.parse('2014-01-08') user.created_at # => 2014-01-08 00:00:00 +0000 user.created_at = DateTime.parse("2014-12-25 13:00:45") user.created_at # => 2014-12-25 13:00:45 +0000 # Convert from seconds since the epoch user.created_at = Time.now.to_f user.created_at # => 2015-01-08 16:23:02 +0000 # Or milliseconds since the epoch user.created_at = 1420734182000 user.created_at # => 2015-01-08 16:23:02 +0000 # A :json attribute is schemaless and accepts the basic JSON types - hash, # array, nil, numeric, string and boolean. user.grades = {'maths' => 'A', 'history' => 'C'} user.grades # => {"maths"=>"A", "history"=>"C"} user.grades = ['A', 'A*', 'C'] user.grades # => ["A", "A*", "C"] user.grades = 'AAB' user.grades # => "AAB" user.grades = Time.now # => ArgumentError: JSON only supports nil, numeric, string, boolean and arrays and hashes of those. # read_attribute and write_attribute methods user.read_attribute(:created_at) user.write_attribute(:name, 'Fred') # View attributes user.attributes # => {:id=>5, :paid=>true, :name=>"Fred", :created_at=>2015-01-08 15:57:05 +0000, :grades=>{"maths"=>"A", "history"=>"C"}} user.inspect # => "#<User id: 5, paid: true, name: \"Fred\", created_at: 2015-01-08 15:57:05 +0000, grades: {\"maths\"=>\"A\", \"history\"=>\"C\"}>" # Mass assignment user.set_attributes(name: "Sally", paid: false) user.attributes # => {:id=>5, :paid=>false, :name=>"Sally", :created_at=>2015-01-08 15:57:05 +0000} # Efficient JSON serialization and deserialization. # Attributes with nil values are omitted. user.attributes_for_json # => {"id"=>5, "paid"=>true, "name"=>"Fred", "created_at"=>1421171317762} require 'oj' Oj.dump(user.attributes_for_json, mode: :strict) # => "{\"id\":5,\"paid\":true,\"name\":\"Fred\",\"created_at\":1421171317762}" user2 = User.new(Oj.load(json, strict: true)) # Change tracking. A much smaller set of functions than that provided by # ActiveModel::Dirty. user.changes # => {:id=>[nil, 5], :paid=>[nil, true], :created_at=>[nil, 2015-01-08 15:57:05 +0000], :name=>[nil, "Fred"]} user.name_changed? # => true # If you need the new values to send as a PUT to a web service user.changes_for_json # => {"id"=>5, "paid"=>true, "name"=>"Fred", "created_at"=>1421171317762} # If you're imitating ActiveRecord behaviour, changes are cleared after # after_save callbacks, but before after_commit callbacks. user.changes.clear user.changes # => {} # Equality if all the attribute values match another = User.new another.id = 5 another.paid = true another.created_at = user.created_at another.name = 'Fred' user == another # => true user === another # => true user.eql? another # => true # Making some attributes private class User extend ModelAttribute attribute :events, :string private :events= def initialize(attributes) # Pass flag to set_attributes to allow setting attributes with private writers set_attributes(attributes, true) end def add_event(new_event) events ||= "" events += new_event end end # Supporting default attributes class UserWithDefaults extend ModelAttribute attribute :name, :string, default: 'Charlie' end UserWithDefaults.attribute_defaults # => {:name=>"Charlie"} user = UserWithDefaults.new user.name # => "Charlie" user.read_attribute(:name) # => "Charlie" user.attributes # => {:name=>"Charlie"} # attributes_for_json omits defaults to keep the JSON compact user.attributes_for_json # => {} # You can add them back in if you need them user.attributes_for_json.merge(user.class.attribute_defaults) # => {:name=>"Charlie"} # A default isn't a change user.changes # => {} user.changes_for_json # => {} user.name = 'Bob' user.attributes # => {:name=>"Bob"}
Installation
Add this line to your application's Gemfile:
And then execute:
Or install it yourself as:
$ gem install model_attribute
Testing
Running specs:
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 a new Pull Request
Code of Conduct
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.