Схемы и валидация
Схемы
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: Int | E0511 | E0512 |
quota: Int? | E0511 | можно |
quota: Int = 100 | будет 100 | E0512 |
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.