.. -*- coding: utf-8; mode: rst; indent-tabs-mode: nil -*-

========
 Schema
========

Grundideen
==========

- Angelehnt an JSON Schema
- Deklarativ in YAML
- Verwendung von erweiterten YAML-Features:

  + laden von (beliebigen) Python-Objekten
  + Benutzung von YAML-Referenzen

- Möglichkeit der direkten Verwendung von Python-Callables
  Diese müssen Iteratoren sein und jedes Problem `yield`en.
- Rückgabe einer Liste von Problemen: Versuch möglichst viele Probleme auf
  einen Schlag zu melden (soweit möglich und sinnvoll)

.. seealso:: - https://json-schema.org/understanding-json-schema/index.html
             - http://rx.codesimply.com/coretypes.html


Extra Keywords für :py:func:`validate`
======================================

- ``skip_keys``

  Eine Liste Strings oder von compilierten REs

  Ein String-Item wird auf Gleichheit getestet, die RE per :py:meth:`search`
  -- und zwar auf den Dict-key

  Bei Treffer wird dieser Key komplett ignoriert. Das ist also eine globale
  Ignore-Liste für Dict-Keys.

  Default: `None`

- ``break_on_keynames_problems``

  Wenn ``True`` und Validierungs-Fehler beim Prüfen von Keys gegen
  ihre Schemata auftreten, brich frühzeitig ab.

  Default: ``True``

- ``data_stream_loader``

  Default: `None` (i.e. use the builtin
           :func:`data_schema.util.get_data_stream`)

  A callable with an `uri` argument that returns a file-alike and a
  context manager to be feed into the schema loader.

- ``schema_loader``

  Default: configmix.yaml.load (if available) or ``None``

  A callable that returns a schema dict from a file object. This is
  used to load a schema from a data stream (YAML, et al.)


Typen
=====

Durch ``$type`` (required) gekennzeichnet


Alle Schemata außer den `Schema-Kombinatoren`_ haben auch ein optionales
Attribut ``index-constraint``.

  Dessen Wert ist eine Liste von Indizes, an denen das Element in
  seinem Parent-Container (Liste, sorted dict) vorkommen darf.


dict / map / object
-------------------

- ``nullable``

  bool (Default: False): instead of an empty dict allow also a None/null/nil

- ``keys``

  `dict` mit Keys und den Values als zugeordnete Schemata für die Values
  des Dicts

- ``key-names``

  Wenn vorhanden: ein Schema, dem die *Keys* -- auch die `additional-keys` --
  folgen müssen.

  Default: entspricht ``{"$type": "string"}``

- ``additional-keys``

  * bool

      `False`
         nicht erlaubt (default)

         Globales ``skip_keys`` wird aber zusätzlich noch in Betracht
         gezogen.

      `True`
         erlaubt -- keine weitergehende Schema-Prüfung der Inhalte

         Globales ``skip_keys`` ist offensichtlich irrelevant.

  * Schema

    Prüfung erfolgt nach gegebenem Schema

    Globales ``skip_keys`` wird aber zusätzlich noch in Betracht gezogen.

- ``required``

  Liste von Strings mit Key-Namen, die vorkommen müssen

- ``forbidden``

  Liste von Strings mit Key-Namen, die **nicht** vorkommen dürfen

- ``max-length``
- ``min-length``


list / array
------------

- ``nullable``

  bool (Default: False): instead of an empty list allow also a None/null/nil

- ``items``

  Ein Schema für *alle* Items.

- ``max-length``
- ``min-length``


set / frozenset
---------------

- ``nullable``

  bool (Default: False): instead of an empty set allow also a None/null/nil

- ``items``

  Ein Schema für *alle* Items

- ``max-length``
- ``min-length``


tuple / record
--------------

- ``nullable``

  bool (Default: False): instead of an empty list or tuple allow also
  a None/null/nil

- ``items``

  Eine Liste: je ein spezielles Schema *pro Item*

- ``additional-items``

  * bool

      `False`
         nicht erlaubt (default)

      `True`
         erlaubt -- keine weitergehende Schema-Prüfung der Inhalte

  * Schema

    Prüfung der "zusätzlichen" Items erfolgt nach gegebenem Schema

- ``max-length``
- ``min-length``


string / str
------------

- ``nullable``

  bool (Default: False): instead of an empty string allow also a None/null/nil

- ``enum``

  Eine Liste von Strings, von denen genau einer dem String entspricht

  Achtung: Alle anderen Prüfungen (siehe unten) werden trotzdem auch
           durchgeführt.

- ``is-contained-in-ref``

  The string's value must be contained in (Python ``in``) in the referenced
  object (see `Referenzen`_).

- ``max-length``
- ``min-length``
- ``pattern``

  * string

    RE of the accepted pattern

  * compiled RE

    compiled RE of the accepted pattern

  * Callable


binary
------

- ``max-length``
- ``min-length``
- ``pattern``

  * string

    RE of the accepted pattern. The YAML unicode string value will be
    converted to a byte-string with :func:`ast.literal_eval` as if it
    is surrounded by ``b'''<re>'''`` or ``b"""<re>"""``. If the pattern
    contains both a ``'''`` or ``"""`` substring the conversion will fail.

  * bytes, bytearray

    RE of the accepted pattern

  * compiled RE

    compiled RE of the accepted pattern

  * Callable


bool / boolean
--------------

Only **real** boolean values: ``true`` and ``false``

- ``value``

  The accepted value or a validating callable

- ``nullable``

  bool (Default: False): instead of a boolean allow also a None/null/nil


timestamp / datetime
--------------------

Only :py:class:`datetime.datetime` allowed

- ``value``

  Callable that validates the value of a timestamp


Callable
--------

Iterator (e.g. ``yield``) mit Signatur: :py:func:`callable(object, schema, context)`


accept
------

Validates successfully always: accept everything


deny
----

Does not validate successfully: always yield the error code 10010


:py:obj:`None` / none / null / nil
----------------------------------

Only the `None` object validates


empty
-----

Erlaubt sind: None, leeres Dict, leere Liste, leeres Set/Frozenset

.. note:: Leere Strings sind **nicht** erlaubt.


integer / int
-------------

- ``nullable``

  bool (Default: False): allow also a None/null/nil

- ``min-value``
- ``max-value``
- ``value``

  A callable to validate the integer value

- ``enum``

  Eine Liste von ganzen Zahlen, von denen genau einer dem vorhandenen
  Wert entsprechen muß.

  Achtung: Alle anderen Prüfungen (`min-value`, `max-value`, `value`)
           werden trotzdem auch durchgeführt.


real / double / float
---------------------

- ``nullable``

  bool (Default: False): allow also a None/null/nil

- ``min-value``
- ``max-value``
- ``value``

  A callable to validate the float value


number / num
------------

- ``nullable``

  bool (Default: False): allow also a None/null/nil

Any numeric value (int or float)

- ``min-value``
- ``max-value``
- ``value``

  A callable to validate the number

- ``enum``

  Eine Liste von Zahlen, von denen genau einer dem vorhandenen
  Wert entsprechen muß.

  Achtung: Alle anderen Prüfungen (`min-value`, `max-value`, `value`)
           werden trotzdem auch durchgeführt.


scalar
------

Any scalar value: no `None`, no `dict`, no `tuple`, no `list`, no `set`,
no `frozenset`.

But if

- ``nullable``

  bool (Default: False): None/null/nil is allowed also


Schema-Kombinatoren
-------------------

- ``all-of``

  alle in der gegebenen Liste müssen validieren

- ``any-of``

  mindestens einer muß validieren

    Nach den ersten erfolgreichen Test werden alle weiteren Sub-Tests
    abgebrochen (aka. short-circuit Verhalten).

- ``one-of``

  **genau einer** aus der Liste muß validieren (aka. xor)

- ``not``

  das folgende Schema darf nicht successful validieren


Bedingungen
===========

``cond``-Key im Schema:

  Lisp-like `cond`:

  - eine Liste von Wenn-Dann-Paaren

    Bedingung: ``when``, ``when-ref-true``, ``when-ref-false``, 
    ``when-ref-exists``, ``when-ref-not-exists``

    Dann: ``then``, ``then-merge``

    Für ``when``:

      Logische Operatoren:

        ``not``

        ``all-of`` (aka `and`)

        ``any-of`` (aka `or`)

        ``one-of`` (aka `xor`)

      Prädikate:

        ``ref-true``, ``ref-false``, ``ref-exists``, ``ref-not-exists``
        ein Objekt im boolschen Kontext

      Vergleichs-Operator:

        ``equals`` gefolgt von einer Liste der Länge zwei als Gleichheits-
        Operator:

            Mögliche Keys:

              ``ref``: eine Referenz

              ``value`` oder ``val`` ein Wert

        z.B. in YAML::

            equals:
              - ref: object:#my.key
              - value: "a string value"

    ``when-ref-true`` und ``when-ref-exists`` sind einfache Abkürzungen für::

         when:
           ref-true:   ...

    bzw::

         when:
           ref-exists:  ...

    Analoges gilt für ``when-ref-false`` und ``when-ref-not-exists``.

  - die *erste* zutreffende Bedingung bestimmt via seinem "Dann" ein Schema

      ``then``

         Keys im Then-Schema *ersetzen* korrespondierende Keys im Parent-Schema

      ``then-merge``

         Then-Merge-Schema wird in das Parent-Schema *eingemischt*

  - das ganze erfolgt rekursiv

  - falls keine der Bedingungen zutrifft wird nichts ausgeführt/geändert

  - ``when`` -- direkt gefolgt von einer Liste -- ist eine Abkürzung für
    ``all-of` und eben dieser Liste::

        cond:
          when:
            - test1
            - test2
            - test3

    ist äquivalent zu::

        cond:
          when:
            all-of:
              - test1
              - test2
              - test3

.. important:: Schema-Referenzen werden **vor** dem Replace/Merge jeweils
               aufgelöst!

``match`` entspricht ``cond`` -- mit dem Unterschied, daß statt der *ersten*
wahren Bedingung **alle** wahren Bedingungen ausgeführt werden;

   erst werden alle Schemata, die aus wahren Bedingungen kommen gesammelt,
   danach werden die Schemata ersetzt bzw. gemerged.

Beispiel::

  required:
    - a
    - b
  cond:
    - when:
        all-of:
          - not:
              ref-true: 'object:#p1.p2.p3'
          - ref-exists: '#p4.p5'
      then:
        required: ["foo", "bar"]    # replace existing `required'
    - when:
        ref-true: 'object:#p6.p7'
      then:
        new-key: "new-val"          # a new key to the containing dict
      then-merge:
        required: ["c", "d"]     # add `c' and `d' to `a' and `b'
    - when: true            # als letzer Fall: "else"
      then-replace:
        required: ["something", "else"]    # replace existing `required'


Referenzen
==========

URI-Syntax

  Angepaßte und simplifizierte JSON-Pointer-Syntax (:rfc:`6901`)

Beispiele:

  - ``object:#wsgi.china_detector.enabled``

    ist (weil `object` das Default-URI-Schema ist) äquivalent zu:

    ``#wsgi.china_detector.enabled``

    Das ist eine **absolute** Referenz.

    ``.`` ist also -- wie in :py:mod:`configmix` -- der Hierarchie-Separator
    für URI-Fragmente in Objekt-Referenzen

  - ``object:#`` ist das Root-Objekt

  - ``object:#.`` ist das current Kontext-Object (aka "Base")

  - ``object:`` ist *ungültig*

    Ein Fragment **muß** also formal vorhanden sein -- auch wenn es leer ist.

  - Relative Referenzen *starten* mit einen Punkt (analog Python-Imports)

    Mehrere führende Punkte sind -- wie bei Python-Imports -- relative
    Referenzen zu Parent-Objekten. Der Versuch, den Parent des Root-Objektes
    anzusprechen, liefert einen :py:exc:`TypeError`.

Wo ein Schema erlaubt ist, ist auch ein dict mit dem einzigen Key ``$ref``
erlaubt. Dies ist eine Referenz auf ein anderes Schema mit dem URI-Schema
``schema:``. Dieses andere Schema kann auch aus einer anderen Datei kommen:

  - ``schema:$root#/``

    Das Root-Element des Root-Schemas

  - ``schema:$self#/``

    Das Root-Element des gerade aktiven Schemas.

  - ``schema:data:schemalib:file.schema.yml#/foo``

    Das ``foo``-Element des via Packagedata von `schemalib` geladenen Schemas
    `file.schema.yml`. Das ist dann auch das neue aktive Schema.
