data Rotor::Changeset[M <: Rotor::Model](data: M, changes: Hash[Symbol, Int | Float | String | Bool | Time | Bytes | Nil], errors: Array[Rotor::FieldError], params: Hash[String, String | Int | Float | Bool | Time | Bytes | Nil], permitted: Array[Symbol], constraints: Array[Rotor::Constraint], assocs: Hash[Symbol, Array[Rotor::Model]], action: Symbol?, cast: Bool)
Changes to a model from untrusted input (a form, JSON), cast to the
columns' types and validated, with the errors meant for a user, as
Ecto's changesets: an immutable value, each validation giving a new
one; Rotor::Repo#insert and Rotor::Repo#update take it and answer with a
Rotor::Outcome.
class CsUser < Rotor::Model
table "users" do
column :id, Int, primary: true
column :email, String
column :age, Int, null: true
end
end
cs = Rotor::Changeset.cast(CsUser, {"email" => "ann@example.com", "age" => "x"}, [:email, :age])
.validate_required([:email])
.validate_format(:email, /@/)
cs.valid? # => false
cs.errors_on(:age) # => ["is invalid"]
cs.changes # => {email: "ann@example.com"}
datais the model it started from (a new one, the class'snew, forcast(Class, ...));changesthe cast values that differ from its own;paramsthe input's scalar values by name.- Validations skip a field without a change, except
validate_required, which looks at the value the model would have, and a fieldvalidate_requiredfound blank.
/home/runner/work/rotor/rotor/src/changeset.qz:75
Class methods
def self.cast[T <: Rotor::Model](model: T, params: Hash[String, Object?], permitted: Array[Symbol]) -> Rotor::Changeset[T]
A changeset of model with the permitted fields of params
(form or JSON input, by name) cast to their columns' types; the
other parameters are ignored. A value that does not cast is an is invalid error (validation: :cast) and no change; a string of
spaces or nothing is nil, except for a String column that admits
no NULL; nil for a column that admits none is can't be blank
(:required). Raises
Rotor::SchemaError for a permitted name that is not a column.
def self.cast[T <: Rotor::Model](model: T, params: Hash[Symbol, Object?], permitted: Array[Symbol]) -> Rotor::Changeset[T]
A changeset of model with the permitted fields of params
(form or JSON input, by name) cast to their columns' types; the
other parameters are ignored. A value that does not cast is an is invalid error (validation: :cast) and no change; a string of
spaces or nothing is nil, except for a String column that admits
no NULL; nil for a column that admits none is can't be blank
(:required). Raises
Rotor::SchemaError for a permitted name that is not a column.
def self.cast[T <: Rotor::Model](model: Type[T], params: Hash[String, Object?], permitted: Array[Symbol]) -> Rotor::Changeset[T]
A changeset of a new model of class model (its new: the
columns' defaults).
def self.cast[T <: Rotor::Model](model: Type[T], params: Hash[Symbol, Object?], permitted: Array[Symbol]) -> Rotor::Changeset[T]
A changeset of a new model of class model (its new: the
columns' defaults).
def self.change[T <: Rotor::Model](model: T) -> Rotor::Changeset[T]
A changeset of model without parameters: for put_change,
put_assoc and validations of code's own values.
def self.cast_value(c: Rotor::Column, raw: Object?) -> [Bool, Int | Float | String | Bool | Time | Bytes | Nil]
[true, value] for raw cast to column c's type, [false, nil]
when it does not cast.
def self.int?(s: String) -> Bool
Whether s is an Int in decimal: ASCII digits, within 64 bits.
def self.with_zone(s: String) -> String
An ISO 8601 time as Time.iso8601 reads it: a space between the
date and the time taken as T, and a time without an offset (an
HTML datetime-local, 2026-11-07T10:00) read as UTC, as Ecto.
def self.same_class?(c: Rotor::Column, v: Int | Float | String | Bool | Time | Bytes) -> Bool
Whether v is of column c's class exactly (a change put by code).
Methods
def data -> M
def changes -> Hash[Symbol, Int | Float | String | Bool | Time | Bytes | Nil]
def errors -> Array[Rotor::FieldError]
def params -> Hash[String, String | Int | Float | Bool | Time | Bytes | Nil]
def permitted -> Array[Symbol]
def constraints -> Array[Rotor::Constraint]
def assocs -> Hash[Symbol, Array[Rotor::Model]]
def action -> Symbol?
def cast -> Bool
def with(*, data: M = …, changes: Hash[Symbol, Int | Float | String | Bool | Time | Bytes | Nil] = …, errors: Array[Rotor::FieldError] = …, params: Hash[String, String | Int | Float | Bool | Time | Bytes | Nil] = …, permitted: Array[Symbol] = …, constraints: Array[Rotor::Constraint] = …, assocs: Hash[Symbol, Array[Rotor::Model]] = …, action: Symbol? = …, cast: Bool = …) -> Rotor::Changeset[M]
A copy with the fields given replaced.
def valid? -> Bool
Whether it has no error.
def errors_on(field: Symbol) -> Array[String]
The messages of the errors on field, their placeholders filled.
def get_change(field: Symbol) -> Int | Float | String | Bool | Time | Bytes | Nil
The change of field, or nil.
def get_field(field: Symbol) -> Int | Float | String | Bool | Time | Bytes | Nil
The value field would have: its change, else the data's (nil when
it has none).
def put_change(field: Symbol, value: Object?) -> Rotor::Changeset[M]
With value as the change of field (no change when it equals the
data's), checked against the column's type.
def add_error(field: Symbol, message: String, *, validation: Symbol = …, meta: Hash[Symbol, Int | Float | String | Symbol | Array[Int | Float | String | Bool | Time | Bytes]] = …) -> Rotor::Changeset[M]
With an error on field (:base for the whole).
def apply -> M?
The model with the changes, or nil when there are errors.
def apply! -> M
The model with the changes; raises Rotor::InvalidChangeset when
there are errors.
def validate_required(fields: Array[Symbol], *, message: String = …) -> Rotor::Changeset[M]
Each of fields must have a value: not nil, not a blank string
(can't be blank, :required). A field with an error already is
skipped.
def validate_length(field: Symbol, *, min: Int? = …, max: Int? = …, is: Int? = …) -> Rotor::Changeset[M]
The change of field, a String, must have between min and
max characters (grapheme clusters, what a reader sees as one,
as Ecto counts), or is (:length, with count and kind).
def validate_format(field: Symbol, pattern: Regexp, *, message: String = …) -> Rotor::Changeset[M]
The change of field, a String, must match pattern (has invalid format, :format).
def validate_number(field: Symbol, *, min: Int | Float | Nil = …, max: Int | Float | Nil = …, greater_than: Int | Float | Nil = …, less_than: Int | Float | Nil = …) -> Rotor::Changeset[M]
The change of field, a number, must be within the bounds given
(:number, with kind and number).
def validate_inclusion(field: Symbol, values: Array[Int | Float | String | Bool | Time | Bytes], *, message: String = …) -> Rotor::Changeset[M]
The change of field must be one of values (is invalid,
:inclusion, with enum).
def validate_exclusion(field: Symbol, values: Array[Int | Float | String | Bool | Time | Bytes], *, message: String = …) -> Rotor::Changeset[M]
The change of field must not be one of values (is reserved,
:exclusion, with enum).
def validate_acceptance(field: Symbol, *, message: String = …) -> Rotor::Changeset[M]
The parameter field (a checkbox, not a column) must be true:
true, "true", "1" or "on" (must be accepted,
:acceptance); a missing one is not. Skipped for a changeset
made by Rotor::Changeset.change (no input), as Ecto.
def validate_confirmation(field: Symbol, *, required: Bool = …, message: String = …) -> Rotor::Changeset[M]
The parameter <field>_confirmation, cast as field is, must equal
the change of field (does not match confirmation, :confirmation, on the
confirmation's field); a missing one is an error with required: true only.
def validate(field: Symbol, &: (M) -> String?) -> Rotor::Changeset[M]
A rule of the change of field: the block gets the model with the
changes and gives a message, or nil when it holds (:custom). Runs
only when field changed and cast.
class CsAccount < Rotor::Model
table "accounts" do
column :id, Int, primary: true
column :email, String
end
end
cs = Rotor::Changeset.cast(CsAccount, {"email" => "root@example.com"}, [:email])
.validate(:email) { |a| a.email.start_with?("root@") ? "is reserved" : nil }
cs.errors_on(:email) # => ["is reserved"]
def validate(&: (Rotor::Changeset[M]) -> Rotor::Changeset[M]) -> Rotor::Changeset[M]
A rule of the whole: the block gives the changeset with the errors
it adds (cs.add_error(...)).
def validate_change(field: Symbol, validation: Symbol, &: (M) -> String?) -> Rotor::Changeset[M]
As validate(field), the error's validation named.
def unsafe_validate_unique(fields: Array[Symbol], repo: Rotor::Repo, *, query: Rotor::Query[M]? = …, message: String = …) -> Rotor::Changeset[M]
Whether a row of the table has the values fields would have, by a
query: an error on the first field (has already been taken,
:unsafe_unique) before the database's unique index refuses the
row. Racy (two saves may pass it at once): no replacement for the
index and its unique_constraint. Checked when one of the fields
changed and none has an error; nil values never match. query
narrows the rows looked at.
def unique_constraint(field: Symbol, *, name: String? = …, message: String = …) -> Rotor::Changeset[M]
On save, a unique violation of the index name (by default
<table>_<field>_index, the name create_index gives) is an error
on field (has already been taken, :unique).
def foreign_key_constraint(field: Symbol, *, name: String? = …, message: String = …) -> Rotor::Changeset[M]
On save, a foreign key violation of name (by default
<table>_<field>_fkey, PostgreSQL's) is an error on field (does not exist, :foreign_key).
def check_constraint(field: Symbol, *, name: String, message: String = …) -> Rotor::Changeset[M]
On save, a violation of the check constraint name is an error on
field (is invalid, :check).
def no_assoc_constraint(assoc: Symbol, *, name: String? = …, message: String = …) -> Rotor::Changeset[M]
On save (a delete), a violation of the foreign key from the
association assoc's table (a has_many; by default
<its table>_<its key>_fkey) is an error on assoc (are still associated with this entry, :no_assoc).
def put_assoc(name: Symbol, models: Array[Rotor::Model]) -> Rotor::Changeset[M]
On save, the has_many ..., through: association name is made
exactly models (persisted ones: their key is read): the join rows
for others deleted, the missing ones inserted, and the saved model
has models preloaded as name. Two saves of one model at once may
both insert a pair: a unique index on the join table's two keys
stops that.
def summary -> String
User is invalid: email can't be blank; age is invalid.
def with_action(a: Symbol) -> Rotor::Changeset[M]
The changeset with action (set by the repository on save).
def constraint_error(e: PG::ServerError) -> Rotor::FieldError?
The error a violation e of a declared constraint is, or nil.
def to_h -> Hash[Symbol, String]
The values by field, a field declared redact: true shown as
**redacted**: the changes, the errors, the action.
def inspect -> String
#<Rotor::Changeset User changes: {...} errors: [...] valid: false>,
a field declared redact: true hidden.
def to_s -> String
As inspect.