Storing custom fields for your models as JSON


Goal of this article - feature which can allow you easy extend your AR models with custom fields.

You can set type, value, name and for example label for each field and use them later let’s say on uniq forms for each customer.

Adding Fields class

First let’s add Fields class, and initialize it with array or hash which we passing to constructor.

class Fields
  extend Forwardable
  def_delegators :@collection, *[].public_methods

  def initialize(array_or_hash = [])
    collection = case array_or_hash
      when Hash
        [CustomField.new(array_or_hash)]
      else
        Array(array_or_hash).map do |field|
          field.is_a?(CustomField) ? field : CustomField.new(field)
        end
      end
    @collection = collection.reject(&:empty?)
  end

  def to_a
    @collection
  end
end

Attributable module

Next, Attributable module.

This module responsible for dynamic methods creation, and looks pretty simple and clear, right?

For each custom field we creating setter and getter and method itself.

module Attributable
  def create_method(name, &block)
    self.class.send(:define_method, name.to_sym, &block)
  end

  def create_setter(m)
    create_method("#{m}=".to_sym) { |v| instance_variable_set("@#{m}", v) }
  end

  def create_getter(m)
    create_method(m.to_sym) { instance_variable_get("@#{m}") }
  end

  def set_attr(method, value)
    create_setter method
    send "#{method}=".to_sym, value
    create_getter method
  end
end

Model

Ok, cool - but what’s next?

Let’s check our model file, let say Customer.rb.

First - let’s include our Attributable module and add custom_fields attribute.

Here you can see custom_fields_values method, with this method you can easy get hash of keys and values for your model:

{"location"=>"KZN", "ig"=>"IG", "age"=>"24", "city"=>"KZN", "team"=>"ASR", "size"=>"8", "code"=>"SSS"}

Cool - right?

Next, let’s add custom_fields setter and write_attribute methods.

First one just creates Fields instance and calls super. But write_attribute returns value for each CustomField.

And finally - we should initialize each value (define_default_values method) on model after_initialize callback.

class Customer < ApplicationRecord
  include Attributable

  attribute :custom_fields, FieldsType.new
  COLUMNS = ['first_name', 'last_name', 'email'].freeze

  def custom_fields_values
    data = {}
    custom_fields.each do |attr|
      unless COLUMNS.include?(attr.name)
        data["#{attr.name}"] = attr.value
      end
    end
    data
  end

  def custom_fields=(value)
    value = Fields.new(value)
    super
  end

  def write_attribute(name, value)
    if name == :custom_fields
      value = Fields.new(value)
    end
    super
  end

  after_initialize :define_default_values

  def define_default_values(fields = custom_fields)
    custom_fields.each do |attr|
      unless COLUMNS.include?(attr.name)
        set_attr(attr.name, attr.value)
      end
    end
  end

end

See it in action

Let’s select latest Customer model from database, and see how it works.

[2] pry(main)> cus = Customer.last
  Customer Load (5.2ms)  SELECT "customers".* FROM "customers" ORDER BY "customers"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<Customer:0x00007fa22a72a758
 id: 10000,
 user_id: 3,
 first_name: "Mike",
 last_name: "Heatley",
 email: "mike@outlook.com",
 created_at: Sat, 11 Apr 2019 13:53:26 UTC +00:00,
 updated_at: Sat, 11 Apr 2019 13:53:26 UTC +00:00,
 custom_fields:
  [#<CustomField:0x00007fa22cc4a7e0 @name="location", @value="USA">,
   #<CustomField:0x00007fa22cc4a718 @name="age", @value="24">,
   #<CustomField:0x00007fa22cc4a6a0 @name="city", @value="SFA">,
   #<CustomField:0x00007fa22cc4a600 @name="size", @value="12">]>

And specific size field:

[4] pry(main)> cus = Customer.last.age
  Customer Load (0.5ms)  SELECT "customers".* FROM "customers" ORDER BY "customers"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> "12"