StoreModel Documentation

repository·master·Indexed 22 days ago

https://github.com/dmitrytsepelev/store_model

A Ruby gem that allows developers to wrap JSON-backed database columns with ActiveModel-like classes. It integrates with the Rails Attributes API to provide structured data handling, validations, enums, and nested attributes for JSON/JSONB columns, promoting the single responsibility principle by separating logic from the parent ActiveRecord model.

Tokens
11.7K
Snippets
38
Records
59
Agent score
79%

What's inside StoreModel

  1. Use Nested Models as attributes

    master

    You can use a StoreModel class as an attribute type within another StoreModel. To define this, use the .to_type method on the nested class.

    If you are using Rails nested forms and want to support #{attribute_name}_attributes=, you must add accepts_nested_attributes_for :attribute_name to the parent model, similar to standard ActiveRecord behavior.

    class Supplier
      include StoreModel::Model
    
      attribute :title, :string
    end
    
    class Configuration
      include StoreModel::Model
    
      attribute :supplier, Supplier.to_type
    end
  2. Merge StoreModel errors into the parent model

    master

    By default, when a StoreModel attribute is invalid, the parent model only reports that the attribute is invalid. The specific error messages (e.g., color can't be blank) remain nested within the StoreModel instance.

    To surface the specific errors directly on the parent model, use the merge_errors: true option in the validates call.

    If you are using an array type (via .to_array_type), use merge_array_errors: true to merge errors from elements within the array. For array types, errors are prefixed with their index (e.g., [0]).

    Note for Rails >= 6.1: Due to changes in Rails error internals, all merged errors will be placed under the parent attribute name (e.g., { configuration: ["Color can't be blank"] }) rather than the specific sub-attribute name.

    # For a single StoreModel attribute
    class Product < ApplicationRecord
      attribute :configuration, Configuration.to_type
      validates :configuration, store_model: { merge_errors: true }
    end
    
    # For an array of StoreModel attributes
    class Product < ApplicationRecord
      attribute :configurations, Configuration.to_array_type
      validates :configurations, store_model: { merge_array_errors: true }
    end
  3. How to handle changes and dirty tracking in StoreModel

    master

    When working with StoreModel attributes, you must ensure changes are flagged as dirty so ActiveRecord knows to save them.

    There are two ways to ensure changes are tracked:

    1. Reassignment: Reassign the attribute (e.g., self.my_stored_model = my_stored_model.map(&:as_json)).
    2. Manual Dirty Flagging: Use the will_change! method (e.g., self.my_stored_model_will_change!).

    Note: Assigned attributes must be a String, Hash, Array of Hashes, or a StoreModel instance. If receiving data from a controller, ensure you convert ActionController::Parameters as needed.

  4. How Union types work in StoreModel

    master

    A Union type allows you to select a specific model class based on a discriminator field (e.g., a type or kind field) within a JSON column. This is useful for polymorphic-like behavior in JSON data.

    To implement a union:

    1. Define individual models using StoreModel::Model.
    2. Assign each model a unique value using discriminator_attribute.
    3. Create the union using StoreModel.union([ModelA, ModelB, ...]).
    4. Apply the union to an ApplicationRecord using either .to_type (for a single object) or .to_array_type (for a collection of objects).

    Important Requirement: The database field must have a default value of nil or be a value hash. An empty hash {} will cause errors because it lacks the required discriminator field.

    class Dog
      include StoreModel::Model
      discriminator_attribute value: "dog"
      attribute :breed, :string
    end
    
    class Cat
      include StoreModel::Model
      discriminator_attribute value: "cat"
      attribute :color, :string
    end
    
    AnimalType = StoreModel.union([Dog, Cat])
    
    class Pet < ApplicationRecord
      attribute :animal, AnimalType.to_type
    end
  5. How parent tracking works in StoreModel

    master

    By default, models that include StoreModel::Model track their parent object. This allows a store model to know which Active Record object it was assigned to. You can access the parent via the .parent method on the store model instance.

    class Configuration
      include StoreModel::Model
    
      attribute :model, :string
    end
    
    class Product < ApplicationRecord
      attribute :configuration, Configuration.to_type
    end
    
    product = Product.first
    product.configuration.parent # returns the `product` object
  6. How StoreModel types and instantiation differ

    master

    In StoreModel, there is a distinction between a standard class instantiation and a 'type' instantiation:

    1. Standard Instantiation (.new): Uses the class directly. It is strict; passing attributes not explicitly defined in the model will raise errors.
    2. Type Instantiation (.from_value or .from_values): Uses the logic defined by to_type. This is what you typically use when assigning values to an attribute in an ActiveModel (e.g., attribute :configuration, Configuration.to_type). This approach enables specialized behaviors like the Unknown attributes handling.

    Internally, from_value is equivalent to Configuration.to_type.cast_value(value) and from_values is equivalent to Configuration.to_array_type.cast_value(values).

  7. Validate StoreModel attributes in ActiveRecord

    master

    StoreModel integrates with ActiveModel validations. To use them, first define a class including StoreModel::Model with its own attributes and validations. Then, in your ActiveRecord model, use the .to_type or .to_array_type method on the StoreModel class to define the attribute, and use the store_model: true validation option.

    Note that by default, the :store_model validator does not allow nil values. To allow nil, pass allow_nil: true to the validation.

    class Configuration
      include StoreModel::Model
    
      attribute :model, :string
      attribute :color, :string
    
      validates :color, presence: true
    end
    
    class Product < ApplicationRecord
      attribute :configuration, Configuration.to_type
    
      validates :configuration, store_model: true
    end
    class Configuration
      include StoreModel::Model
    
      attribute :model, :string
      attribute :color, :string
    
      validates :color, presence: true
    end
    
    class Product < ApplicationRecord
      attribute :configuration, Configuration.to_type
    
      validates :configuration, store_model: true
    end
  8. Define a custom type for StoreModel

    master

    To create a custom type for use within a StoreModel, inherit from ActiveRecord::Type::Value. You must implement the type method (returning a symbol) and the cast(value) method to handle how data is converted when assigned to an attribute. A common pattern is to handle various input types (like Date or String) and return nil on failure or blank values.

    class Iso8601Type < ActiveRecord::Type::Value
      def type
        :datetime_iso8601
      end
    
      def cast(value)
        return value if value.is_a?(Time)
        return value.to_time if value.is_a?(Date)
        return nil if value.blank?
    
        Time.iso8601(value)
      rescue ArgumentError, TypeError
        nil
      end
    end
  9. Validate hash-type attributes and use `merge_hash_errors`

    master

    When you add validates :attribute, store_model: true, all models within the hash are validated.

    By default, if a value inside the hash is invalid, the error message is generic (e.g., "Configurations is invalid"). To receive detailed error messages that specify which key failed validation, use the merge_hash_errors: true option.

    class Product < ApplicationRecord
      attribute :configurations, Configuration.to_hash_type
      # Use merge_hash_errors to see which specific key failed
      validates :configurations, store_model: { merge_hash_errors: true }
    end
    
    product = Product.new
    product.configurations["primary"] = Configuration.new(color: nil)
    product.valid? # => false
    product.errors.full_messages 
    # => ["Configurations [primary] Color can't be blank"]
    class Product < ApplicationRecord
      attribute :configurations, Configuration.to_hash_type
      validates :configurations, store_model: { merge_hash_errors: true }
    end
  10. Store a keyed collection of models using `to_hash_type`

    master

    When you need to store a collection of models indexed by a key (similar to a dictionary or map) in a JSON column, use the #to_hash_type method on your model class. The resulting attribute will behave like a hash where keys are strings and values are instances of the specified model.

    Important Notes:

    • Keys: Keys are always stored and returned as strings, even if you use symbols when setting values.
    • Overwriting: This attribute is a JSON hash, not an association. Using assign_attributes (or similar methods) will override the entire hash rather than merging with existing values.
    class Product < ApplicationRecord
      attribute :configurations, Configuration.to_hash_type
    end
  11. Get started with StoreModel

    master

    StoreModel allows you to wrap JSON-backed database columns with ActiveModel-like classes. This enables you to use the Rails Attributes API, validations, enums, and nested attributes on data stored in JSON/JSONB columns, keeping the logic separated from the parent ActiveRecord model.

    To use StoreModel:

    1. Define a class that includes StoreModel::Model.
    2. Define attributes using the Rails Attributes API.
    3. Register the field in your ActiveRecord model using .to_type.
    class Configuration
      include StoreModel::Model
    
      attribute :model, :string
      attribute :color, :string
    end
    
    class Product < ApplicationRecord
      attribute :configuration, Configuration.to_type
    end