Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Схемы и валидация

Схемы

type объявляет схему, new — инстанцирует с проверкой:

type Container
  name:  String
  image: String
  port:  Int
end

main: new Container
  name:  "gateway"
  image: "company/gateway:2.4.1"
  port:  8080
end

Короткий экземпляр помещается в одну строку, с запятыми между полями (D38). Проверяется он ровно так же, как блочная форма:

sidecar: new Container name: "proxy", image: "envoy:1.30", port: 9901 end

Проверки при инстанцировании:

СитуацияКодКогда
Отсутствует поле схемыE0511всегда
Неверный тип (Int vs String, Int vs Float)E0512всегда
Лишнее поле, не объявленное в схемеE0513только --strict

Значение по умолчанию проверяется там, где схема объявлена, а не только там, где используется. q: Int = "not an int" — это E0516 прямо на type, потому что поле, которое вызывающие всегда задают явно, иначе никогда не дошло бы до new со своим умолчанием, и объявление продолжало бы утверждать невозможное.

Судятся только литералы: что даст произвольное выражение, до запуска неизвестно, а результат всё равно проверит new.

type Quota
  # `Int? = null` — маркер допускает null, а умолчание делает поле необязательным.
  # Один только маркер по-прежнему требовал бы поле при `new`.
  limit: Int? = null
end

Типы полей: String, Int, Float, Bool, List, Object. Int и Float — раздельные типы: лимиты памяти в байтах и 64-битные идентификаторы не теряют точность (E0304 на переполнении, а не тихий wrap).

Поля, у которых может не быть значения

Поле хранит значение своего типа и ничего больше. Когда значения действительно нет - безлимитная квота, срок, который никто не назначил, - пометьте поле ?:

type Plan
  id:    String
  quota: Int?
end

unlimited: new Plan
  id:    "enterprise"
  quota: null
end

Это нужно, чтобы покончить с сигнальными значениями. Без ? «безлимит» пишут как -1 или 2147483647, и каждому приходится помнить, какое число что значит и где. Сигнальное значение - законное значение своего типа, поэтому неверное никто никогда не поймает.

? отвечает только на один вопрос: может ли значения не быть. Можно ли поле опустить - отдельная задача = default, и они сочетаются:

ОбъявлениеЕсли опуститьЕсли написать null
quota: IntE0511E0512
quota: Int?E0511можно
quota: Int = 100будет 100E0512
quota: Int? = nullбудет nullможно

Nullable-поле по-прежнему проверяется по типу, когда значение в нём есть, а aura types выпускает Option<i64>, number | null и *int64.

Единственное место, где ? может навредить

? расширяет то, что поле принимает, и именно поэтому на него стоит посмотреть ещё раз, когда значение приходит извне:

quota: data.get("quota") # без запасного значения

Без ? отсутствующий ключ - это E0512, и прогон останавливается, так что двое людей с разными входными файлами узнают об этом сразу. С ? та же разница проходит как null, и они молча выкатывают разные конфигурации.

Это сочетание - W0513, а под --strict - ошибка:

[W0513] Warning: 'quota' is nullable and is filled from `get(key)`, so a
        missing value becomes null instead of an error
          Help: name the value that stands for absence: `get(key, fallback)`
                — or drop the `?` so a missing one is reported

env(name) без запасного значения - тот же случай. Предупреждение просит только назвать запасное значение: data.get("quota", 0) говорит, какое значение означает отсутствие, и говорит это в манифесте, а не в чьей-то голове.

Списки с типом элементов

List сам по себе принимает любые элементы. Чтобы сказать, что в нём лежит, пишите [T]:

type Service
  name:      String
  tags:      [String]
  endpoints: [Endpoint]
end

Тип повторяет значение: [1, 2] - список, значит, его тип [Int]. Вложенность получается без нового правила, [[Int]], и enum тоже годится как тип элемента.

Неверный элемент называется с индексом, потому что «один из них неверный» - не ответ в списке из сорока:

[E0512] Error: endpoints[0] of schema Service expects Endpoint, got String

Это доходит и до хоста: aura types выпускает Vec<Endpoint>, Endpoint[] или []Endpoint, а не нетипизированный массив.

Голый List сохраняет прежний смысл, так что ничего написанного раньше менять не нужно.

Поле с типом-схемой хранит то, что принял бы new

Поле с типом Port, [Port] или [[Port]] хранит значения, которые принял бы new Port, откуда бы они ни пришли (D41). Элемент из JSON, YAML или от хоста проверяется по тем же правилам, что и написанный через new: обязательные поля, типы, enum и инварианты.

type Port
  number: Int
  protocol: String = "tcp"

  assert number > 0, "port must be positive"
end

type Service
  ports: [Port]
end

raw = read_file("ports.json").parse_json()
svc: new Service ports: raw end

Ошибка называет путь до неверного значения:

[E0515] Error: Port: port must be positive (in ports[2])
[E0511] Error: missing field 'port' required by schema Endpoint (in routes[1].target)

Как это работает и почему

  • Структурно, а не номинально. Значение не помнит, какая схема его создала, поэтому вопрос звучит «принял бы это new Port?», а не «создано ли это через new Port?». Данные извне проходят, если они правильные, и их не нужно пересобирать поле за полем через .map.
  • Умолчания подставляются. {"number": 80} в поле [Port] становится {"number": 80, "protocol": "tcp"}, в порядке полей схемы, ровно как сделал бы new. Одна схема даёт одну форму вывода, каким бы ни был источник, - на это опираются aura types и хост, читающий результат.
  • Экземпляр, созданный через new, проверяется ещё раз. Проверки чистые, а умолчания уже на месте, так что ничего не меняется; альтернатива - скрытая метка «уже проверено» - была бы состоянием, которого читатель не видит.
  • Лишние поля - как у new: сохраняются, а под --strict дают E0513.
  • Схема может содержать саму себя - children: [Node] - на глубину до 32 уровней; глубже - E0208, а не падение, потому что данные, в отличие от кода, можно построить любой глубины.

До D41 такое поле принимало любой объект: без number или с number, который нарушает инвариант. Манифест, который на это опирался, теперь остановится с ошибкой, называющей неверный элемент.

Правила, которые схема утверждает о себе

Тип говорит, какой формы значения. Он может сказать и то, какими они должны быть:

type Plan
  price_monthly: Int
  price_yearly:  Int
  max_channels:  Int

  assert price_monthly >= 0, "price must not be negative"
  assert max_channels > 0, "a plan needs at least one channel"
  assert price_yearly <= price_monthly * 12, "yearly must not exceed twelve months"
end

Они выполняются при каждом new Plan, а провал - это E0515 с сообщением, которое написала схема:

[E0515] Error: Plan: yearly must not exceed twelve months

Ради третьего это и стоит иметь: оно связывает два поля, чего не выразит никакое ограничение на отдельное поле. И правило порядка ему не нужно, потому что каждое поле заполнено и проверено по типу до того, как выполнится любой инвариант.

Почему бы просто не написать assert рядом с new

Потому что тогда это не часть типа. Он не переезжает вместе со схемой, так что импортёр получает поля без правил; инструменты его не видят; а следующий, кто создаст экземпляр схемы, просто его не напишет.

Поместите его в схему - и он действует везде, включая манифест автора, который вашего пакета никогда не читал:

import "./lib.aura" as lib

p: new lib.Port
  number: 80 # E0515: Port: ports below 1025 are privileged
end

Что видит инвариант

Свои поля и ничего больше. Ни модуль, где объявлена схема, ни тот, где написан new:

[E0504] Error: use of undefined variable 'limit'
          Help: an invariant sees only this schema's fields (q), so it means the
                same in every manifest that uses the schema; pass anything else
                in as a field

Это сделано намеренно. Правило, которое могло бы читать переменную модуля, значило бы разное в каждом манифесте, импортирующем схему, - а инварианты существуют как раз чтобы решить эту проблему. Заодно из него недостижимо ничего эффектного, так что экземпляр валиден или невалиден одинаково для всех.

assert

Произвольные инварианты — statement assert:

admins = [187650342, 244179081]
assert admins.uniq().len() == admins.len(), "Duplicate admin ids"
assert admins.len() >= 1

Провал — E0530 с вашим сообщением и точной позицией. assert работает и в --dry-run — тестовый прогон обязан находить отказ валидации.

fail в выражениях

Для валидации внутри выражения есть fail:

port: p > 0 && p < 65536 ? p : fail("invalid port #{p}")

Закрытые наборы: enum

Поле String принимает любую строку — опечатка в значении доезжает до прода. enum задаёт закрытый набор допустимых строк:

enum Tier
  "frontend"
  "backend"
end

type Service
  tier: Tier
end

svc: new Service
  tier: "backand" # E0514 + подсказка «did you mean "backend"?»
end

Член enum — обычная строка: в JSON попадёт "backend", никакой обёртки не появится. Это ограничение на значение, а не новый тип данных.

pub enum экспортируется importer’ам (D12). Члены резолвятся там, где объявлена схема, поэтому импортированная схема проверяется против enum своего модуля — даже если у вас в области видимости есть одноимённый.

Статический анализ

aura check (и автоматически перед каждым eval) находит проблемы до запуска:

  • E0504 — использование необъявленной переменной (в том числе внутри #{...});
  • W0501/W0502/W0503 — неиспользуемые переменные / импорты / функции и типы;
  • W0512 — эффектный вызов (env, read_file) в импортированном модуле.

В --strict предупреждения становятся ошибками — режим для CI.