StatifierDatamodel

CI Hex.pm Version Hex Downloads Hex Docs License

The shared type registry the family's documents, blocks and expressions agree on. It reads a datamodel document - a host's typed description of the data an author writes conditions against - and answers what the document alone can settle: which paths it declares, what type each one holds, whether a value read at a path satisfies the type a step expects, and whether a redefined type still serves the readers of the one it replaces.

Why this package

A host describes its data once, but more than one tool reads that description: the block editor checks that a step reads what an earlier step wrote, and the expression editor offers the paths a condition may name and the values each may take. When each tool reads the document its own way, they drift apart on what a path holds and on whether a read is satisfied, and an author sees one answer in one place and another answer somewhere else. This package is the one reader both take. Every function is pure and total, each returns a fact rather than a verdict, and the package depends on nothing else in the family, so either tool can take it without taking the other, and both give the same answer about the same document.

Install

Add statifier_datamodel to the dependencies in your mix.exs:

def deps do
[
{:statifier_datamodel, "~> 0.5.0"}
]
end

Basic usage

A library loan: a copy of a book lent to a patron, due on a date, renewed, returned or lost. The host declares a library.loan record and a Renewable shape a renewal step reads, and types the loan entry by the record. Index the document, read the type the index holds at a path, and hand it to the read check. An undeclared path answers nil, which the caller turns into :unknown: unknown, not wrong.

iex> alias StatifierDatamodel.{Declarations, Index, Types}
iex> document = %{
...> "scopes" => [
...> %{"scope" => "local", "label" => "Chart-local", "entries" => [
...> %{"name" => "loan", "path" => "loan", "type" => "library.loan", "label" => "Loan"},
...> %{"name" => "status", "path" => "status", "type" => "string", "label" => "Status",
...> "one_of" => ["on_loan", "returned", "lost"]}]}],
...> "types" => [
...> %{"name" => "library.loan", "kind" => "record", "label" => "Loan", "fields" => [
...> %{"name" => "patron_id", "type" => "string", "required?" => true},
...> %{"name" => "due_on", "type" => "date", "required?" => true},
...> %{"name" => "renewals", "type" => "integer"}]},
...> %{"name" => "Renewable", "kind" => "shape", "label" => "Renewable", "fields" => [
...> %{"name" => "due_on", "type" => "date", "required?" => true},
...> %{"name" => "renewals", "type" => "integer", "required?" => true}]}]}
iex> index = Index.index(document)
iex> index.order
["loan", "loan.patron_id", "loan.due_on", "loan.renewals", "status"]
iex> Index.path_types(index)["status"]
{:one_of, ["on_loan", "returned", "lost"]}
iex> declarations = Declarations.from_document(document)
iex> held = Index.type(index, "loan") || :unknown
iex> Types.satisfies(declarations, held, {:declared, "Renewable"})
{:missing, ["renewals"]}
iex> Types.satisfies(declarations, Index.type(index, "loan.fine") || :unknown, :integer)
:unknown

The loan record leaves renewals optional, so it has not promised the value a renewal step reads, and the read check names that field. Marking it required in the record is the one-key fix.

Documentation

Compatibility

The package needs Elixir 1.18 or later (elixir: "~> 1.18" in mix.exs) and has no runtime dependencies.

Until 1.0, the public surface may change between minor releases: a release may rename modules, functions or error vocabulary with no compatibility shim. Every such change is recorded in the changelog under a bold Breaking heading that says what to do about it, and pinning to an exact minor, ~> X.Y.0, is the recommended way to take the package until then.

License

MIT - see LICENSE.