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, untrustedfrom_json/from_yaml, HTTP params) (defaulttrue)persistence:— whenfalse, the field isn't serialised or persisted (defaulttrue)show:— serialise the field even whenpersistence: false(defaultfalse)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, … plusconverter:
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,URIandTime(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.