change
An early, in-progress implementation of Changesets in Crystal. Inspired by Elixir's Ecto Changesets.
Changesets are a powerful way of implementing validations and coercions in a composable, non-destructive way. Changes can be applied and validations can be enforced without ever modifying the underlying instance, making it easy to trust that data manipulation is always done correctly and safely.
What works:
cast
with typecasting of primitive types (nil, booleans, string, ints, floats).validate_*
for making assertions about fields (currently onlyvalidate_required
).valid?
method on the changeset to indicate overall validity.add_error
to add an error to the changeset and mark it as invalid.
get_*
to inspect information about the changeset dynamically.- Schema definitions (but this is definitely going to change a lot soon).
Initial goals for this implementation include:
- Compile Performance: minimal, linear/constant-time overhead with macros and generated code with respect to the number of usages.
- Type safety: safe, automatic casting of data between types from any input type.
- Extensibility: enabling user-defined types to be added without requiring modifications to the core
Installation
-
Add the dependency to your
shard.yml
:dependencies: changeset: github: faultyserver/change
-
Run
shards install
Usage
This library is too early to have a documented API. In the meantime, check example.cr
for an example usage. A short sample is also given here:
require "change"
class User
Change.schema User,
name : String,
age : Int32,
bio : String
# Standard pattern for working with changesets. Define a static method
# that performs casts and validations to abstract that from the callsite.
def self.changeset(instance, changes={} of String => String)
Changeset.new(instance)
.cast(changes, [:name, :age, :bio])
.validate_required(["name", "age"])
end
end
user = User.new
changeset = User.changeset(user, {
name: "Jon",
age: 23,
})
# Changesets abstract validation to a single place, so clients can just query
# their validity before continuing.
changeset.valid? #=> true
# Changesets don't modify the backing instance until explicitly applied.
user #=> #<User @name=nil, @age=nil, @bio=nil>
user = changeset.apply_changes
user #=> #<User @name="Jon, @age=23, @bio=nil>
SQL querying
In addition to changesets, this library also includes an optional SQL querying interface. This lives in src/change/sql
and must be included separately from the changeset module. Change::SQL
is built on top of Changesets, so using them is required to be able to use this. Part of this is due to Crystal's current lack of support for copying instance vars effectively at compile time, but also to provide a cleaner, more powerful interface with validations, constraints, and more.
See example-sql.cr
for an example of what the SQL interface currently looks like.