Skip to content

Active Model#

ActiveModel provides a known set of interfaces for model classes, and is the foundation for building custom ORMs.

Note

pg-orm is built on top of ActiveModel — everything on this page (attributes, dirty tracking, validations, callbacks, serialisation) is available to a pg-orm model too.

Installation#

Add the shard to your shard.yml, then run shards install.

dependencies:
  active-model:
    github: spider-gazelle/active-model
    version: ~> 4.7

Defining a model#

ActiveModel::Model is an abstract base class. Inherit from it and declare your fields with the attribute macro.

require "active-model"

class Person < ActiveModel::Model
  attribute name : String = "default value"
  attribute age : Int32
end

Instances mix in JSON::Serializable, YAML::Serializable and DB::Serializable, so they serialise and deserialise out of the box.

The attribute macro#

attribute takes a typed field name and an optional default value.

attribute name : String            # nil until assigned; the getter raises NilAssertionError while nil
attribute age : Int32 = 18         # with a default

Common options:

  • mass_assignment: — allow the field to be set by mass assignment (assign_attributes, untrusted from_json/from_yaml, HTTP params) (default true)
  • persistence: — when false, the field isn't serialised or persisted (default true)
  • show: — serialise the field even when persistence: false (default false)
  • sanitize: — strip/clean HTML on write (see Serialisation)
  • serialization_group: — add the field to a named serialiser (see Serialisation)
  • JSON/YAML tags — json_key, json_emit_null, json_root, yaml_key, … plus converter:

Enums#

Enum attributes are supported. By default an enum serialises as a snake_case string (ExtraMedium becomes "extra_medium"). Use Enum::ValueConverter(T) to serialise the backing value instead. Give the converter the enum's fully qualified name, as it's expanded outside the class.

class Order < ActiveModel::Model
  enum Product
    Fries
    Burger
  end

  enum Size
    Medium
    ExtraMedium
  end

  attribute product : Product = Product::Fries
  attribute size : Size = Size::ExtraMedium, converter: Enum::ValueConverter(Order::Size)
end

Order.new.to_json # => {"product":"fries","size":1}

Constructing models#

person = Person.new(name: "Bob Jane", age: 32)
person.attributes # => {:name => "Bob Jane", :age => 32}

# from JSON / YAML
person = Person.from_json(%({"name": "Bob Jane"}))
person.name    # => "Bob Jane"
person.to_json # => {"name":"Bob Jane"}

# from HTTP::Params (also accepts Hash(String, String)) — mass-assignable fields only
person = Person.new(HTTP::Params.parse("name=Bob+Jane"))

Note

from_json / from_yaml treat input as untrusted: fields with mass_assignment: false (and not show: true) are ignored and fall back to their default. Use from_trusted_json / from_trusted_yaml for internal sources. assign_attributes updates an existing instance in place (mass-assignable fields only), and assign_attributes_from_json / assign_attributes_from_yaml do the same from a payload.

HTTP params#

new(params) and assign_attributes(params) take HTTP::Params, a Hash(String, String) or a {key, value} tuple, and parse each value into the attribute's type:

  • Scalars: strings, numbers, Bool, Char, URI and Time (a unix timestamp), and unions of them.
  • Enums: by member name (case-insensitive) or value. An unknown value raises TypeCastError.
  • Arrays and sets of those types: repeat the key, or use key[], for each element (ids=1&ids=2). A hash provides a single element.
  • Nilable attributes: an empty value sets nil.

Attributes that can't be expressed as form values, such as hashes, JSON::Any and other models, are ignored. This needs active-model 4.7.1 or later.

class Search < ActiveModel::Model
  enum Sort
    Newest
    Oldest
  end

  attribute ids : Array(Int64) = [] of Int64
  attribute sort : Sort = Sort::Newest
  attribute query : String?
end

search = Search.new(HTTP::Params.parse("ids=1&ids=2&sort=oldest&query="))
search.ids   # => [1, 2]
search.sort  # => Search::Sort::Oldest
search.query # => nil

Introspection helpers: attributes (Hash), attributes_tuple (NamedTuple), Person.attributes (Array of Symbol keys), and persistent_attributes (only fields without persistence: false).

Dirty tracking#

Changes are tracked over the lifetime of the model, similar to ActiveModel::Dirty.

person = Person.new(name: "JD")
person.changed?           # => true
person.changed_attributes # => {:name => "JD"}
person.name_changed?      # => true
person.name_change        # => {nil, "JD"}
person.name_was           # => nil

person.clear_changes_information
person.changed? # => false

Per-attribute you get <attr>_changed?, <attr>_was, <attr>_change (a {old, new} tuple), and <attr>_will_change! to force-mark a field. Across the model: changed?, changed_attributes, changes (a Hash of {old, new} tuples), changed_json / changed_yaml, changes_json, restore_attributes (revert to previous values), and clear_changes_information (reset tracking).

Models built with from_json / from_yaml start with no changes recorded.

See also#