model_attribute

by yammer

ModelAttribute gem - attributes for non-ActiveRecord models

124 Stars 5 Forks Last release: Not found Other 135 Commits 14 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

ModelAttribute Gem Version Build Status

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 # => "#"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:

gem 'model_attribute'

And then execute:

$ bundle

Or install it yourself as:

$ gem install model_attribute

Testing

Running specs:

$ rspec

Contributing

  1. Fork it
  2. Create your feature branch (
    git checkout -b my-new-feature
    )
  3. Commit your changes (
    git commit -am 'Add some feature'
    )
  4. Push to the branch (
    git push origin my-new-feature
    )
  5. 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 [email protected] with any additional questions or comments.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.