Schemas
Ein .dxns-Schema ist ein gewöhnliches DXN-Dokument: eine Map von Namen auf Struct-Schemas und benannte Typen. Es sagt einem Leser, wie die Felder eines Structs heißen, in welcher Reihenfolge sie stehen, was sie enthalten dürfen und welche fehlen dürfen.
Ein Schema-Dokument
%{
Percentage: {:refine :float %{min: 0.0, max: 100.0}}
Tag: {:refine :string %{min-length: 1, max-length: 20}}
Address: %schema{
fields: @ordered %{
street: :string
city: :string
zip?: :string
}
}
Customer: %schema{
closed: true
fields: @ordered %{
name: :string
address: Address
loyalty: Percentage
tags?: {:list-of Tag}
}
}
}Address und Customer sind Struct-Schemas. Percentage und Tag sind benannte Typen — wiederverwendbare Namen für einen Typausdruck, auf die man genau wie auf einen Struct-Namen verweist.
Typausdrücke
| Form | Passt auf |
|---|---|
:any | jeden Wert |
:integer, :string, … | ein Keyword pro Typname |
Address, Ns/Name | ein Struct-Schema oder einen benannten Typ, per Name |
{:list-of T}, {:set-of T} | eine Liste oder Menge, deren Elemente alle auf T passen |
{:tuple-of A B C} | ein Tupel aus genau diesen Plätzen |
{:map-of K V} | eine Map mit Schlüsseln K und Werten V |
{:enum :a :b …} | eines der angegebenen Literale |
{:one-of A B}, {:all-of A B} | mindestens eine oder jede Variante |
{:nilable T} | nil oder T |
{:refine T %{…}} | T plus Einschränkungen |
%schema{…} | eine Struct-Form |
Die Einschränkungen von refine sind min, max, exclusive-min, exclusive-max und multiple-of für Zahlen; min-length, max-length und pattern für Zeichenketten, wobei die Länge in Codepoints zählt; und min-count und max-count für Listen, Mengen und Tupel. Ein Wert muss zum Grundtyp und zu jeder Einschränkung passen.
Struct-Schemas
fields: muss eine geordnete Map sein: Ihre Reihenfolge ist die, die positionelle Structs und die Binärform verwenden. Ein Schlüssel mit ? am Ende markiert ein optionales Feld; das ? gehört nicht zum Namen. Ein Feld, das mehr als einen Typ braucht, nutzt %field{type: …, default: …, description: …}.
| Schlüssel | Standard | Wirkung |
|---|---|---|
closed: | false | jedes Feld ablehnen, das nicht in fields: steht |
forbidden: | [] | diese Felder namentlich ablehnen, auch bei offenem Schema |
refine-fn: | — | eine Prüfung des ganzen Werts benennen, die außerhalb des Dokuments liegt |
Money: %schema{
closed: true
forbidden: [legacy_amount_cents]
fields: @ordered %{
amount: :decimal
currency: {:enum :usd :eur :gbp}
}
}forbidden: gibt einem ausgemusterten Feld eine eigene Fehlermeldung statt eines allgemeinen „unbekanntes Feld“.
Benannte Typen und andere Dateien
Jeder Eintrag auf oberster Ebene, der kein %schema{} ist, ist ein benannter Typ. Ob ein benannter Typ auf einen anderen im selben Dokument verweisen darf, bleibt der Implementierung überlassen, denn eine Map hat keine Reihenfolge, nach der sich das auflösen ließe; ein Verweis auf einen Typ aus einem vorher geladenen Dokument funktioniert immer.
Namespace/Name verweist auf ein Schema in einer anderen Datei. Das Format legt nur diese Syntax fest; wie ein Leser die Datei findet — über einen Pfad, eine Registry, einen Download — ist seine Sache.
%{
Customer: %schema{
fields: @ordered %{
name: :string
home: Address/Address
}
}
}Durchsetzung
Ohne Schema ist ein Struct ein undurchsichtiger Wert, nie ein Fehler. Kennt ein Leser das Schema, muss jeder Wert dieses Structs seine Pflichtfelder haben, Feldwerte der deklarierten Typen, bei geschlossenem Schema kein nicht aufgeführtes Feld und kein verbotenes Feld. Ein Verstoß ist ein Fehler eigener Art, verschieden von einem Parse-Fehler.
%Money{amount: 9.99M, currency: :eur}
%Money[9.99M :eur]
%Money{amount: 9.99M, currency: :chf}
%Money{amount: 9.99M, currency: :eur, legacy_amount_cents: 999}
%Money[9.99M]ok ok — the same value struct "Money" violates its schema: field "currency" does not match its declared type struct "Money" violates its schema: field "legacy_amount_cents" is forbidden struct "Money" violates its schema: expected 2 positional field(s), got 1