Coelho.Ash.Type (coelho v0.2.0)

Copy Markdown View Source

A Coelho document as an Ash attribute, stored in a :map column.

Coelho.Ecto.Type targets an Ecto schema, and Ash does not go through Ecto.Type for its own attributes, so an Ash resource declaring attribute :body, :map gets a map and no validation at all.

Why this is a use rather than a ready-made type

Coelho does not depend on Ash — not even optionally. An optional dependency would be the usual way to ship a module that needs another library at compile time, but Ash depends on :stream_data in every environment, and Coelho keeps :stream_data to :dev and :test for its property tests. Reconciling the two means loosening Coelho's own dependencies for every application that will never use Ash.

So the type is a macro that expands in your application, where Ash is present by definition. It costs one module:

defmodule MyApp.RichText.Type do
  use Coelho.Ash.Type
end

and then reads the way any other Ash type does:

attribute :cgv_doc, MyApp.RichText.Type do
  constraints document_schema: MyApp.RichText.cgv_schema()
end

The schema arrives as a constraint rather than as an option on the attribute, because that is where Ash puts per-attribute configuration and where Ash.Resource.Info will show it.

Constraints

  • :document_schema — required, the Coelho.Schema to validate against
  • :sanitize? — when true, a value that fails validation is put through Coelho.Document.sanitize/2 and accepted instead of rejected. Defaults to false. Turn it on for an import path where refusing the whole document is worse than keeping a poorer one; leave it off wherever a person is typing, so they are told rather than silently corrected

What casting accepts

  • a document map, as ProseMirror's toJSON() produces it
  • a JSON string, which is what a form posts back from the editor's hidden input
  • nil and "", which cast to nil

What an invalid document looks like

An Ash.Error.Changes.InvalidAttribute on the attribute, whose vars carry the location in the document tree — which is what lets a LiveView form say more than "is invalid":

%Ash.Error.Changes.InvalidAttribute{
  field: :cgv_doc,
  message: "is not valid rich text (%{location}: %{reason})",
  vars: [location: "content[0].attrs.href", reason: "scheme \"javascript\" is not allowed", ...]
}

vars[:errors] holds every failure, formatted, not only the first.

Loading

Values already in the database are loaded without being re-validated, for the reason Coelho.Ecto.Type gives: a schema that grew stricter after rows were written would otherwise make old rows unreadable, which is a migration to run deliberately. Put a stored document through Coelho.Document.sanitize/2 before rendering it.

Summary

Functions

Casts user supplied input — a document map, or the JSON a form posts back.

Reads a value back out of the column, without re-validating it.

The constraint schema the generated type declares.

Writes a value to the column.

Functions

cast_input(value, constraints)

@spec cast_input(
  term(),
  keyword()
) :: {:ok, map() | nil} | {:error, keyword()}

Casts user supplied input — a document map, or the JSON a form posts back.

cast_stored(value, constraints)

@spec cast_stored(
  term(),
  keyword()
) :: {:ok, map() | nil}

Reads a value back out of the column, without re-validating it.

constraints()

@spec constraints() :: keyword()

The constraint schema the generated type declares.

dump_to_native(value, constraints)

@spec dump_to_native(
  term(),
  keyword()
) :: {:ok, map() | nil} | :error

Writes a value to the column.