joetjen.net
EN DE
Datenformat

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.

Format 1.0

Ein Schema-Dokument

customer.dxns
%{
  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

FormPasst auf
:anyjeden Wert
:integer, :string, …ein Keyword pro Typname
Address, Ns/Nameein 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üsselStandardWirkung
closed:falsejedes 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.

Gelesen mit den obigen Schemas
%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