Skip to content
Gem for handling JSON-backed attributes as ActiveRecord models
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
bin
gemfiles
lib Bump version to 0.1.2 Mar 14, 2019
spec Fix typo in class name: CombileErrorsStrategies -> CombineErrorsStrat… Mar 7, 2019
.coveralls.yml Initial implementation Feb 20, 2019
.gitignore
.rubocop.yml Initial implementation Feb 20, 2019
.travis.yml
CHANGELOG.md Bump version to 0.1.2 Mar 14, 2019
Gemfile
MIT-LICENSE Initial implementation Feb 20, 2019
README.md fix: typo in README.md Mar 14, 2019
Rakefile Initial implementation Feb 20, 2019
store_model.gemspec Initial implementation Feb 20, 2019

README.md

Gem Version Build Status Coverage Status

StoreModel

Sponsored by Evil Martians

StoreModel allows to work with JSON-backed database columns in a similar way we work with ActiveRecord models. Supports Ruby >= 2.3 and Rails >= 5.2.

For instance, imagine that you have a model Product with a jsonb column called configuration. Your usual workflow probably looks like:

product = Product.find(params[:id])
if product.configuration["model"] == "spaceship"
  product.configuration["color"] = "red"
end
product.save

This approach works fine when you don't have a lot of keys with logic around them and just read the data. However, when you start working with that data more intensively (for instance, adding some validations around it) - you may find the code a bit verbose and error-prone. With this gem, the snipped above could be rewritten this way:

product = Product.find(params[:id])
if product.configuration.model == "spaceship"
  product.configuration.color = "red"
end
product.save

Installation

Add this line to your application's Gemfile:

gem 'store_model'

And then execute:

$ bundle

Or install it yourself as:

$ gem install store_model

How to register stored model

Start with creating a class for representing the hash as an object:

class Configuration
  include StoreModel::Model

  attribute :model, :string
  attribute :color, :string
end

Attributes should be defined using Rails Attributes API. There is a number of types available out of the box, and you can always extend the type system with your own ones.

Register the field in the ActiveRecord model class:

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type
end

Validations

StoreModel supports all the validations shipped with ActiveModel. Start with defining validation for the store model:

class Configuration
  include StoreModel::Model

  attribute :model, :string
  attribute :color, :string

  validates :color, presence: true
end

Then, configure your ActiveRecord model to validates this field as a store model:

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type

  validates :configuration, store_model: true
end

When attribute is invalid, errors are not copied to the parent model by default:

product = Product.new
puts product.valid? # => false
puts product.errors.messages # => { configuration: ["is invalid"] }
puts product.configuration.errors.messages # => { color: ["can't be blank"] }

You can change this behavior to have these errors on the root level (instead of ["is invalid"]):

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type

  validates :configuration, store_model: { merge_errors: true }
end

In this case errors look this way:

product = Product.new
puts product.valid? # => false
puts product.errors.messages # => { color: ["can't be blank"] }

You can change the global behavior using StoreModel.config:

StoreModel.config.merge_errors = true

You can also add your own custom strategies to handle errors. All you need to do is to provide a callable object to StoreModel.config.merge_errors or as value of :merge_errors. It should accept three arguments - attribute, base_errors and store_model_errors:

StoreModel.config.merge_errors = lambda do |attribute, base_errors, _store_model_errors| do
  base_errors.add(attribute, "cthulhu fhtagn")
end

If the logic is complex enough - it worth defining a separate class with a #call method:

class FhtagnErrorStrategy
  def call(attribute, base_errors, _store_model_errors)
    base_errors.add(attribute, "cthulhu fhtagn")
  end
end

You can provide its instance or snake-cased name when configuring global merge_errors:

StoreModel.config.merge_errors = :fhtagn_error_strategy

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type

  validates :configuration, store_model: { merge_errors: :fhtagn_error_strategy }
end

or when calling validates method on a class level:

StoreModel.config.merge_errors = FhtagnErrorStrategy.new

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type

  validates :configuration, store_model: { merge_errors: FhtagnErrorStrategy.new }
end

Note: :store_model validator does not allow nils by default, if you want to change this behavior - configure the validation with allow_nil: true:

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type

  validates :configuration, store_model: true, allow_nil: true
end

Alternatives

  • store_attribute - work with JSON fields as an attributes, defined on the ActiveRecord model (not in the separate class)
  • jsonb_accessor - same thing, but with built-in queries
  • attr_json - works like previous one, but using ActiveModel::Type

License

The gem is available as open source under the terms of the MIT License.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.