1070 auto const*
const object = std::get_if<Json::object_t>(&json.data);
1071 if (
object ==
nullptr) {
1072 refuse(
"",
"a document is a JSON object");
1076 doc.
vocab = text(*
object,
"vocab",
"",
true);
1078 refuse(
"/vocab",
"the vocabulary `" + doc.
vocab +
"` is not supported; this client reads ui/1");
1080 doc.
id = text(*
object,
"id",
"",
true);
1081 if (doc.
id.empty()) {
1082 refuse(
"/id",
"a document has a non-empty id");
1084 doc.
title = optionalExpr(*
object,
"title",
"");
1085 doc.
identity = optionalExpr(*
object,
"identity",
"");
1086 doc.
dirty = optionalExpr(*
object,
"dirty",
"");
1087 doc.
subtitle = optionalExpr(*
object,
"subtitle",
"");
1088 doc.
status = topParts(*
object,
"status",
"headBadge");
1089 doc.
actions = topParts(*
object,
"actions",
"actionButton");
1090 if (doc.
id ==
"app") {
1092 for (std::string_view
const key : {
"subtitle",
"status",
"actions"}) {
1093 if (member(*
object, key) !=
nullptr) {
1095 "`" + std::string{key} +
"` is a screen's; the app document has none");
1100 if (
Json const*
const params = member(*
object,
"params")) {
1101 for (
auto const& [name, type] : objectOf(*params,
"/params",
"`params` maps names to types")) {
1103 checkName(name, path);
1104 doc.
params.emplace_back(name, typeOf(type, path));
1107 doc.
scope = scope(*
object,
"");
1108 for (std::string_view
const key : {
"onMount",
"onUnmount",
"onClose",
"onSave",
"onBackendChange"}) {
1109 if (
Json const*
const commands = member(*
object, key)) {
1113 if (
Json const*
const keys = member(*
object,
"keys")) {
1114 doc.
keys = keyBindings(*keys,
"/keys");
1116 if (
Json const*
const prefetch = member(*
object,
"prefetch")) {
1117 doc.
prefetch = nameList(*prefetch,
"/prefetch");
1119 if (
Json const*
const components = member(*
object,
"components")) {
1120 for (
auto const& [name, entry] : objectOf(*components,
"/components",
"`components` is an object")) {
1124 if (
Json const*
const points = member(*
object,
"customize")) {
1125 for (
auto const& [name, entry] : objectOf(*points,
"/customize",
"`customize` is an object")) {
1129 if (
Json const*
const view = member(*
object,
"view")) {
1130 doc.
view = node(*view,
"/view");
1131 }
else if (doc.
id !=
"app") {
1132 refuse(
"",
"a screen has a `view`");
1138 [[nodiscard]]
static Json const* member(Json::object_t
const&
object, std::string_view key) {
1139 auto const*
const found =
object.find(key);
1140 return found ==
object.end() ? nullptr : &found->second;
1143 [[nodiscard]]
static Json::object_t
const& objectOf(
Json const& json, std::string
const& path,
char const* what) {
1144 auto const*
const object = std::get_if<Json::object_t>(&json.data);
1145 if (
object ==
nullptr) {
1151 static void checkName(std::string
const& name, std::string
const& path) {
1152 if (!isName(name)) {
1153 refuse(path,
"`" + name +
"` is not a name: names match [A-Za-z_][A-Za-z0-9_]*");
1157 [[nodiscard]]
static std::string text(Json::object_t
const&
object, std::string_view key, std::string
const& path,
1159 Json
const*
const value = member(
object, key);
1161 if (value ==
nullptr) {
1163 refuse(path,
"`" + std::string{key} +
"` is required");
1167 auto const*
const string = std::get_if<std::string>(&value->data);
1168 if (
string ==
nullptr) {
1169 refuse(keyPath,
"`" + std::string{key} +
"` is a string");
1174 [[nodiscard]]
static std::string choiceOf(Json::object_t
const&
object, std::string_view key,
1175 std::string
const& path, std::span<std::string_view const> choices) {
1176 std::string value = text(
object, key, path,
false);
1177 if (!value.empty() && !
listed(choices, value)) {
1178 refuse(
pointerAppend(path, key),
"`" + value +
"` is not one of the values of `" + std::string{key} +
"`");
1183 [[nodiscard]]
static std::string showErrors(Json::object_t
const&
object, std::string
const& path) {
1184 static constexpr std::array<std::string_view, 3> kPolicies = {
"touched",
"submit",
"always"};
1185 return choiceOf(
object,
"showErrors", path, kPolicies);
1188 [[nodiscard]]
static std::int64_t integer(Json::object_t
const&
object, std::string_view key,
1189 std::string
const& path) {
1190 Json
const*
const value = member(
object, key);
1191 if (value ==
nullptr) {
1194 auto const number = detail::jsonInt(*value);
1195 if (!number || *number < 0) {
1201 [[nodiscard]]
static bool flag(Json::object_t
const&
object, std::string_view key, std::string
const& path) {
1202 Json
const*
const value = member(
object, key);
1203 if (value ==
nullptr) {
1206 bool const*
const set = std::get_if<bool>(&value->data);
1207 if (set ==
nullptr) {
1213 [[nodiscard]]
static Type typeOf(Json
const& json, std::string
const& path) {
1214 auto const*
const spelled = std::get_if<std::string>(&json.data);
1215 if (spelled ==
nullptr) {
1216 refuse(path,
"a type is written as a string, such as `int?`");
1220 refuse(path, type.error());
1225 [[nodiscard]]
static std::vector<std::string> nameList(Json
const& json, std::string
const& path) {
1226 auto const*
const array = std::get_if<Json::array_t>(&json.data);
1227 if (array ==
nullptr) {
1228 refuse(path,
"a list of names is an array of strings");
1230 std::vector<std::string>
names;
1231 for (std::size_t index = 0; index < array->size(); ++index) {
1232 auto const*
const name = std::get_if<std::string>(&array->at(index).data);
1233 if (name ==
nullptr || name->empty()) {
1236 names.push_back(*name);
1241 [[nodiscard]]
static ExprPtr expr(Json
const& json, std::string
const& path) {
1244 throw DocumentRefused{std::move(parsed.error())};
1246 return std::move(*parsed);
1249 [[nodiscard]]
static ExprPtr optionalExpr(Json::object_t
const&
object, std::string_view key,
1250 std::string
const& path) {
1251 Json
const*
const value = member(
object, key);
1252 return value ==
nullptr ? nullptr : expr(*value,
pointerAppend(path, key));
1257 [[nodiscard]] std::vector<DocNodePtr> topParts(Json::object_t
const&
object, std::string_view key,
1258 std::string_view kind) {
1259 std::vector<DocNodePtr> parts;
1260 Json
const*
const value = member(
object, key);
1261 if (value ==
nullptr) {
1265 auto const*
const array = std::get_if<Json::array_t>(&value->data);
1266 if (array ==
nullptr) {
1267 refuse(path,
"`" + std::string{key} +
"` is an array");
1269 for (std::size_t index = 0; index < array->size(); ++index) {
1270 parts.push_back(part(array->at(index), kind,
pointerAppend(path, index)));
1276 [[nodiscard]] ScopeDecl scope(Json::object_t
const&
object, std::string
const& path) {
1278 if (Json
const*
const models = member(
object,
"models")) {
1280 for (
auto const& [name, entry] : objectOf(*models, where,
"`models` maps aliases to models")) {
1281 decl.models.push_back(model(name, entry,
pointerAppend(where, name)));
1284 if (Json
const*
const state = member(
object,
"state")) {
1286 for (
auto const& [name, entry] : objectOf(*state, where,
"`state` maps names to declarations")) {
1288 checkName(name, entryPath);
1289 auto const& fields = objectOf(entry, entryPath, R
"(a state declaration is {"init": e, "type": T})");
1291 StateDecl stateDecl{.name = name, .path = entryPath, .type = Type::any(), .init = nullptr};
1292 if (Json
const*
const type = member(fields,
"type")) {
1293 stateDecl.type = typeOf(*type,
pointerAppend(entryPath,
"type"));
1295 stateDecl.init = optionalExpr(fields,
"init", entryPath);
1296 decl.state.push_back(std::move(stateDecl));
1299 if (Json
const*
const lets = member(
object,
"let")) {
1301 for (
auto const& [name, entry] : objectOf(*lets, where,
"`let` maps names to expressions")) {
1303 checkName(name, entryPath);
1304 decl.lets.push_back(LetDecl{.name = name, .path = entryPath, .value = expr(entry, entryPath)});
1307 if (Json
const*
const queries = member(
object,
"queries")) {
1309 for (
auto const& [name, entry] : objectOf(*queries, where,
"`queries` maps names to queries")) {
1310 decl.queries.push_back(query(name, entry,
pointerAppend(where, name)));
1313 if (Json
const*
const mutations = member(
object,
"mutations")) {
1315 for (
auto const& [name, entry] : objectOf(*mutations, where,
"`mutations` maps names to mutations")) {
1316 decl.mutations.push_back(mutation(name, entry,
pointerAppend(where, name)));
1319 if (Json
const*
const watches = member(
object,
"watch")) {
1321 auto const*
const array = std::get_if<Json::array_t>(&watches->data);
1322 if (array ==
nullptr) {
1323 refuse(where,
"`watch` is an array");
1325 for (std::size_t index = 0; index < array->size(); ++index) {
1326 decl.watches.push_back(watch(array->at(index),
pointerAppend(where, index)));
1329 if (Json
const*
const checks = member(
object,
"checks")) {
1331 auto const*
const array = std::get_if<Json::array_t>(&checks->data);
1332 if (array ==
nullptr) {
1333 refuse(where,
"`checks` is an array");
1335 for (std::size_t index = 0; index < array->size(); ++index) {
1336 decl.checks.push_back(check(array->at(index),
pointerAppend(where, index)));
1342 [[nodiscard]]
static ModelDecl model(std::string
const& name, Json
const& json, std::string
const& path) {
1343 checkName(name, path);
1344 auto const& fields = objectOf(json, path, R
"(a model alias is {"type": ..., "instance": ...})");
1346 ModelDecl decl{.name = name,
1348 .type = text(fields, "type", path,
true),
1351 .borrowedFrom = {}};
1352 Json
const*
const instance = member(fields,
"instance");
1353 if (instance ==
nullptr) {
1357 auto const*
const word = std::get_if<std::string>(&instance->data);
1358 if (word !=
nullptr && (*word ==
"private" || *word ==
"shared")) {
1359 decl.instance = *word ==
"private" ? InstanceMode::Private : InstanceMode::Shared;
1362 if (
auto const*
const object = std::get_if<Json::object_t>(&instance->data)) {
1363 if (Json
const*
const from = member(*
object,
"from")) {
1364 auto const*
const alias = std::get_if<std::string>(&from->data);
1365 if (alias ==
nullptr || !
isName(*alias) || object->size() != 1) {
1366 refuse(where, R
"(a borrowed instance is {"from": alias})");
1368 decl.instance = InstanceMode::Borrowed;
1369 decl.borrowedFrom = *alias;
1373 decl.instance = InstanceMode::Keyed;
1374 decl.key = expr(*instance, where);
1382 static void refuseRefreshKey(Json::object_t
const& fields, std::string
const& path, std::string_view key) {
1383 if (member(fields, key) !=
nullptr) {
1385 std::format(
"a document does not declare `{}`: an action's effects say what it changes, and every "
1386 "query holding a changed record refreshes; a refresh no action declares is a `refetch` "
1392 [[nodiscard]]
static QueryDecl query(std::string
const& name, Json
const& json, std::string
const& path) {
1393 checkName(name, path);
1394 auto const& fields = objectOf(json, path,
"a query is an object");
1399 decl.model = text(fields,
"model", path,
true);
1400 std::string
const action = text(fields,
"action", path,
false);
1401 std::string
const evaluate = text(fields,
"evaluate", path,
false);
1402 if (action.empty() ==
evaluate.empty()) {
1403 refuse(path,
"a query names exactly one of `action` and `evaluate`");
1405 decl.action = action.empty() ?
evaluate : action;
1407 decl.body = optionalExpr(fields,
"body", path);
1408 decl.when = optionalExpr(fields,
"when", path);
1409 decl.debounce = integer(fields,
"debounce", path);
1410 decl.refreshEvery = integer(fields,
"refreshEvery", path);
1411 decl.staleTime = integer(fields,
"staleTime", path);
1412 refuseRefreshKey(fields, path,
"refreshOn");
1420 [[nodiscard]]
static OptimisticDecl optimistic(Json
const& json, std::string
const& path) {
1421 static constexpr char const* shape = R
"(an optimistic write is {"query": q, "at"?: m, "key": e, "with": e})";
1422 auto const& fields = objectOf(json, path, shape);
1424 Json
const*
const key = member(fields,
"key");
1425 Json
const*
const with = member(fields,
"with");
1426 if (key ==
nullptr || with ==
nullptr) {
1429 std::string listMember = text(fields,
"at", path,
false);
1430 if (member(fields,
"at") !=
nullptr && listMember.empty()) {
1431 refuse(
pointerAppend(path,
"at"),
"`at` names the member of the query's record that holds the list");
1433 return OptimisticDecl{.query = text(fields,
"query", path,
true),
1434 .at = std::move(listMember),
1439 [[nodiscard]] MutationDecl mutation(std::string
const& name, Json
const& json, std::string
const& path) {
1440 checkName(name, path);
1441 auto const& fields = objectOf(json, path,
"a mutation is an object");
1446 decl.model = text(fields,
"model", path,
true);
1447 decl.action = text(fields,
"action", path,
true);
1448 decl.body = optionalExpr(fields,
"body", path);
1449 static constexpr std::array<std::string_view, 4> kModes = {
"exclusive",
"serial",
"latest",
"parallel"};
1450 std::string
const mode = choiceOf(fields,
"concurrency", path, kModes);
1451 if (mode ==
"serial") {
1452 decl.concurrency = Concurrency::Serial;
1453 }
else if (mode ==
"latest") {
1454 decl.concurrency = Concurrency::Latest;
1455 }
else if (mode ==
"parallel") {
1456 decl.concurrency = Concurrency::Parallel;
1458 decl.serialKey = optionalExpr(fields,
"serialKey", path);
1459 if (decl.serialKey && decl.concurrency != Concurrency::Serial) {
1460 refuse(
pointerAppend(path,
"serialKey"),
"`serialKey` applies to `serial` concurrency only");
1462 refuseRefreshKey(fields, path,
"invalidates");
1463 refuseRefreshKey(fields, path,
"patch");
1464 if (Json
const*
const value = member(fields,
"optimistic")) {
1465 decl.optimistic = optimistic(*value,
pointerAppend(path,
"optimistic"));
1467 if (Json
const*
const commands = member(fields,
"onSuccess")) {
1468 decl.onSuccess = commandList(*commands,
pointerAppend(path,
"onSuccess"));
1470 if (Json
const*
const commands = member(fields,
"onError")) {
1471 decl.onError = commandList(*commands,
pointerAppend(path,
"onError"));
1473 static constexpr std::array<std::string_view, 1> kOffline = {
"queue"};
1474 decl.offlineQueue = !choiceOf(fields,
"offline", path, kOffline).empty();
1478 [[nodiscard]] WatchDecl watch(Json
const& json, std::string
const& path) {
1479 auto const& fields = objectOf(json, path, R
"(a watch is {"on": e, "do": [...]})");
1481 Json const*
const watched = member(fields,
"on");
1482 Json
const*
const body = member(fields,
"do");
1483 if (watched ==
nullptr || body ==
nullptr) {
1484 refuse(path, R
"(a watch is {"on": e, "do": [...]})");
1486 static constexpr std::array<std::string_view, 2> kSources = {
"any",
"user"};
1490 .source = choiceOf(fields,
"source", path, kSources) ==
"user" ? WatchSource::User : WatchSource::Any,
1494 [[nodiscard]]
static CheckDecl check(Json
const& json, std::string
const& path) {
1495 auto const& fields = objectOf(json, path, R
"(a check is {"id": ..., "assert": e, ...})");
1497 Json const*
const assertion = member(fields,
"assert");
1498 if (assertion ==
nullptr) {
1499 refuse(path,
"a check has an `assert`");
1502 decl.id = text(fields,
"id", path,
false);
1504 decl.assertion = expr(*assertion,
pointerAppend(path,
"assert"));
1505 decl.message = optionalExpr(fields,
"message", path);
1506 if (Json
const*
const targets = member(fields,
"targets")) {
1507 decl.targets = nameList(*targets,
pointerAppend(path,
"targets"));
1509 decl.blocking = flag(fields,
"blocking", path);
1513 struct CommandSpec {
1514 std::string_view key;
1516 std::array<std::string_view, 6> companions;
1520 static constexpr std::array<CommandSpec, 23> kCommands = {{
1521 {
"set", CommandKind::Set, {
"value"}},
1522 {
"reset", CommandKind::Reset, {}},
1523 {
"run", CommandKind::Run, {
"body",
"as",
"each"}},
1524 {
"refetch", CommandKind::Refetch, {}},
1525 {
"open", CommandKind::Open, {}},
1526 {
"close", CommandKind::Close, {}},
1527 {
"navigate", CommandKind::Navigate, {
"params",
"as",
"onResult",
"onCancel"}},
1528 {
"return", CommandKind::Return, {}},
1529 {
"focus", CommandKind::Focus, {}},
1530 {
"host", CommandKind::Host, {
"args"}},
1531 {
"switchBackend", CommandKind::SwitchBackend, {}},
1532 {
"resetPrefs", CommandKind::ResetPrefs, {}},
1533 {
"setLocale", CommandKind::SetLocale, {}},
1534 {
"notify", CommandKind::Notify, {
"role"}},
1535 {
"seq", CommandKind::Seq, {}},
1536 {
"all", CommandKind::All, {}},
1537 {
"if", CommandKind::If, {}},
1538 {
"cancelClose", CommandKind::CancelClose, {}},
1539 {
"retry", CommandKind::Retry, {}},
1540 {
"signIn", CommandKind::SignIn, {}},
1541 {
"signOut", CommandKind::SignOut, {}},
1542 {
"upload", CommandKind::Upload, {
"to",
"as",
"id",
"into",
"nameAs",
"typeAs"}},
1543 {
"download", CommandKind::Download, {
"save",
"name",
"to",
"as",
"from",
"type"}},
1547 [[nodiscard]]
Commands commandList(Json
const& json, std::string
const& path) {
1549 if (
auto const*
const array = std::get_if<Json::array_t>(&json.data)) {
1550 for (std::size_t index = 0; index < array->size(); ++index) {
1551 commands.push_back(command(array->at(index),
pointerAppend(path, index)));
1555 commands.push_back(command(json, path));
1559 [[nodiscard]]
static std::string nameOf(Json
const& json, std::string
const& path,
char const* what,
1560 bool dotted =
false) {
1561 auto const*
const name = std::get_if<std::string>(&json.data);
1562 bool valid = name !=
nullptr && !name->empty();
1563 if (valid && dotted) {
1565 valid = segments.has_value() &&
isName(segments->front());
1575 [[nodiscard]]
static std::string nonEmpty(Json
const& json, std::string
const& path,
char const* what) {
1576 auto const*
const name = std::get_if<std::string>(&json.data);
1577 if (name ==
nullptr || name->empty()) {
1583 static void requireTrue(Json
const& json, std::string
const& path, std::string_view key) {
1584 bool const*
const value = std::get_if<bool>(&json.data);
1585 if (value ==
nullptr || !*value) {
1586 refuse(path,
"`" + std::string{key} +
"` is written `true`");
1591 [[nodiscard]] Command command(Json
const& json, std::string
const& path) {
1592 auto const& fields = objectOf(json, path, R
"(a command is an object such as {"set": ..., "value": ...})");
1593 CommandSpec const* chosen =
nullptr;
1594 for (
auto const& [key, value] : fields) {
1595 static_cast<void>(value);
1596 for (CommandSpec
const& spec : kCommands) {
1597 if (spec.key == key) {
1598 if (chosen !=
nullptr) {
1600 "a command has one kind; found `" + std::string{chosen->key} +
"` and `" + key +
"`");
1606 if (chosen ==
nullptr) {
1607 refuse(path,
"not a command of ui/1");
1611 cmd.kind = chosen->kind;
1613 Json
const& primary = *member(fields, chosen->key);
1615 switch (chosen->kind) {
1616 case CommandKind::Set: {
1617 cmd.target = nameOf(primary, where,
"`set` names state or an element as `name.path`",
true);
1618 Json
const*
const value = member(fields,
"value");
1619 if (value ==
nullptr) {
1620 refuse(path,
"`set` needs a `value`");
1625 case CommandKind::Run:
1626 cmd.target = nameOf(primary, where,
"`run` names a mutation");
1627 cmd.value = optionalExpr(fields,
"body", path);
1628 cmd.extra = optionalExpr(fields,
"each", path);
1629 if (Json
const*
const binding = member(fields,
"as")) {
1630 cmd.binding = nameOf(*binding,
pointerAppend(path,
"as"),
"`as` is a name");
1633 case CommandKind::Reset:
1634 case CommandKind::Refetch:
1635 case CommandKind::Open:
1636 case CommandKind::Focus:
1637 cmd.target = nameOf(primary, where,
"the command names a declaration or an element");
1639 case CommandKind::Close:
1640 cmd.target = nameOf(primary, where,
"`close` names a dialog, or `self`");
1642 case CommandKind::Navigate:
1643 navigate(cmd, fields, primary, path);
1645 case CommandKind::Return:
1646 cmd.value = expr(primary, where);
1648 case CommandKind::Host:
1649 cmd.target = nonEmpty(primary, where,
"`host` names a registered command");
1650 cmd.value = optionalExpr(fields,
"args", path);
1652 case CommandKind::SwitchBackend:
1653 case CommandKind::SetLocale:
1654 cmd.target = nonEmpty(primary, where,
"the command takes a non-empty string");
1656 case CommandKind::ResetPrefs:
1657 cmd.target = nameOf(primary, where,
"`resetPrefs` names a customization point, or `all`");
1659 case CommandKind::Notify: {
1660 cmd.value = expr(primary, where);
1661 static constexpr std::array<std::string_view, 4> kRoles = {
"info",
"success",
"warning",
"error"};
1662 cmd.option = choiceOf(fields,
"role", path, kRoles);
1665 case CommandKind::Seq:
1666 case CommandKind::All: {
1667 if (!std::holds_alternative<Json::array_t>(primary.data)) {
1668 refuse(where,
"`" + std::string{chosen->key} +
"` takes an array of commands");
1670 cmd.commands = commandList(primary, where);
1673 case CommandKind::If: {
1674 auto const*
const array = std::get_if<Json::array_t>(&primary.data);
1675 if (array ==
nullptr || array->size() < 2 || array->size() > 3) {
1676 refuse(where,
"`if` takes [condition, [then...], [else...]]");
1678 cmd.value = expr(array->at(0),
pointerAppend(where, std::size_t{0}));
1679 cmd.commands = commandList(array->at(1),
pointerAppend(where, 1));
1680 if (array->size() == 3) {
1681 cmd.alternative = commandList(array->at(2),
pointerAppend(where, 2));
1685 case CommandKind::CancelClose:
1686 case CommandKind::Retry:
1687 case CommandKind::SignOut:
1688 requireTrue(primary, where, chosen->key);
1690 case CommandKind::SignIn: {
1691 auto const*
const object = std::get_if<Json::object_t>(&primary.data);
1692 Json
const*
const from =
object ==
nullptr ? nullptr : member(*
object,
"from");
1693 if (from ==
nullptr) {
1694 refuse(where, R
"(`signIn` is {"from": mutation})");
1696 cmd.target = nameOf(*from, pointerAppend(where, "from"),
"`signIn` names its mutation");
1699 case CommandKind::Upload: {
1700 cmd.value = expr(primary, where);
1701 Json
const*
const destination = member(fields,
"to");
1702 if (destination ==
nullptr) {
1703 refuse(path,
"`upload` names its mutation with `to`");
1705 cmd.target = nameOf(*destination,
pointerAppend(path,
"to"),
"`to` names a mutation");
1706 if (Json
const*
const binding = member(fields,
"as")) {
1707 cmd.binding = nameOf(*binding,
pointerAppend(path,
"as"),
"`as` is a name");
1709 if (Json
const*
const transfer = member(fields,
"id")) {
1710 cmd.transfer = nameOf(*transfer,
pointerAppend(path,
"id"),
"`id` is a name");
1712 static constexpr std::array<std::string_view, 2> kInto = {
"ref",
"bytes"};
1713 cmd.bytes = choiceOf(fields,
"into", path, kInto) ==
"bytes";
1714 for (
auto const& [key, slot] : {std::pair{
"nameAs", &cmd.nameAs}, std::pair{
"typeAs", &cmd.typeAs}}) {
1715 if (Json
const*
const name = member(fields, key)) {
1718 "`" + std::string{key} +
1719 R
"(` names a body member for inline bytes; give `"into": "bytes"`)");
1726 case CommandKind::Download:
1728 download(cmd, fields, primary, path);
1734 void navigate(Command& cmd, Json::object_t
const& fields, Json
const& primary, std::string
const& path) {
1736 if (
auto const*
const route = std::get_if<Json::object_t>(&primary.data)) {
1737 Json
const*
const value = member(*route,
"route");
1738 if (value ==
nullptr || route->size() != 1) {
1739 refuse(where, R
"(`navigate` names a screen, or is {"route": e})");
1743 cmd.target = nonEmpty(primary, where, R
"(`navigate` names a screen, or is {"route": e})");
1745 cmd.value = optionalExpr(fields, "params", path);
1746 static constexpr std::array<std::string_view, 4> kModes = {
"tab",
"dialog",
"drawer",
"window"};
1747 cmd.option = choiceOf(fields,
"as", path, kModes);
1748 if (Json
const*
const result = member(fields,
"onResult")) {
1749 cmd.commands = commandList(*result,
pointerAppend(path,
"onResult"));
1751 if (Json
const*
const cancel = member(fields,
"onCancel")) {
1752 cmd.alternative = commandList(*cancel,
pointerAppend(path,
"onCancel"));
1756 static void download(Command& cmd, Json::object_t
const& fields, Json
const& primary, std::string
const& path) {
1758 static constexpr std::array<std::string_view, 2> kFrom = {
"ref",
"bytes"};
1759 cmd.bytes = choiceOf(fields,
"from", path, kFrom) ==
"bytes";
1762 if (member(fields,
"name") ==
nullptr) {
1763 refuse(path,
"a download from bytes names its file with `name`");
1765 cmd.extra = optionalExpr(fields,
"name", path);
1766 cmd.mediaType = optionalExpr(fields,
"type", path);
1767 }
else if (member(fields,
"type") !=
nullptr) {
1768 refuse(
pointerAppend(path,
"type"), R
"(`type` is the media type of inline bytes; give `"from": "bytes"`)");
1770 std::string const save = text(fields,
"save", path,
false);
1771 std::string
const destination = text(fields,
"to", path,
false);
1772 if (save ==
"dialog" && destination.empty()) {
1773 cmd.option =
"dialog";
1774 cmd.extra = optionalExpr(fields,
"name", path);
1777 if (destination ==
"preview" && save.empty()) {
1778 Json
const*
const binding = member(fields,
"as");
1779 if (binding ==
nullptr) {
1780 refuse(path,
"a download to preview binds a state with `as`");
1782 cmd.option =
"preview";
1783 cmd.target = nameOf(*binding,
pointerAppend(path,
"as"),
"`as` names a state");
1786 refuse(path, R
"(`download` takes `"save": "dialog"` or `"to": "preview"`)");
1789 [[nodiscard]] std::map<std::string, Commands, std::less<>> keyBindings(Json const& json, std::string
const& path) {
1790 std::map<std::string, Commands, std::less<>> keys;
1791 for (
auto const& [chord, commands] : objectOf(json, path,
"`keys` maps chords to commands")) {
1794 keys.emplace(chord, commandList(commands, where));
1800 [[nodiscard]] Component component(std::string
const& name, Json
const& json, std::string
const& path) {
1801 checkName(name, path);
1802 auto const& fields = objectOf(json, path,
"a component is an object");
1807 decl.qml = text(fields,
"qml", path,
false);
1808 decl.host = text(fields,
"host", path,
false);
1809 if (decl.qml.empty() == decl.host.empty()) {
1810 refuse(path,
"a component names exactly one of `qml` and `host`");
1812 if (!decl.qml.empty()) {
1813 bool hash = decl.qml.size() == 71 && decl.qml.starts_with(
"sha256:");
1814 for (
char const chr : std::string_view{decl.qml}.substr(std::min<std::size_t>(7, decl.qml.size()))) {
1815 hash = hash && ((chr >=
'0' && chr <=
'9') || (chr >=
'a' && chr <=
'f'));
1821 Json
const*
const version = member(fields,
"version");
1822 auto const number = version ==
nullptr ? std::nullopt : detail::jsonInt(*version);
1824 refuse(path,
"a host component has an integer `version`");
1826 decl.version = *number;
1828 static constexpr std::array<std::string_view, 6> kPropTypes = {
"string",
"bool",
"int",
1829 "exact",
"number",
"json"};
1830 if (Json
const*
const props = member(fields,
"props")) {
1832 for (
auto const& [prop, type] : objectOf(*props, where,
"`props` maps names to prop types")) {
1833 auto const*
const spelled = std::get_if<std::string>(&type.data);
1834 if (spelled ==
nullptr || !
listed(kPropTypes, *spelled)) {
1835 refuse(
pointerAppend(where, prop),
"a prop type is string, bool, int, exact, number or json");
1837 decl.props.emplace(prop, *spelled);
1840 if (Json
const*
const events = member(fields,
"events")) {
1842 for (
auto const& [event, type] : objectOf(*events, where,
"`events` maps names to payload types")) {
1843 decl.events.emplace(event, typeOf(type,
pointerAppend(where, event)));
1846 Json
const*
const fallback = member(fields,
"fallback");
1847 if (fallback ==
nullptr) {
1848 refuse(path,
"a component names a `fallback` of built-in nodes");
1850 decl.fallback = node(*fallback,
pointerAppend(path,
"fallback"));
1854 [[nodiscard]]
static CustomizePoint point(std::string
const& name, Json
const& json, std::string
const& path,
1856 checkName(name, path);
1857 auto const& fields = objectOf(json, path,
"a customization point is an object");
1859 static constexpr std::array<std::string_view, 7> kPointKinds = {
"columns",
"choice",
"value",
"layout",
1860 "saved",
"pins",
"arrange"};
1861 CustomizePoint decl;
1864 decl.kind = text(fields,
"kind", path,
true);
1865 if (!
listed(kPointKinds, decl.kind)) {
1866 refuse(
pointerAppend(path,
"kind"),
"`" + decl.kind +
"` is not a kind of customization point");
1868 if (decl.kind ==
"pins" && !appDocument) {
1872 if (decl.kind ==
"arrange" && appDocument) {
1873 refuse(
pointerAppend(path,
"kind"),
"an `arrange` point belongs to the screen whose container it orders");
1875 decl.of = text(fields,
"of", path, decl.kind ==
"columns" || decl.kind ==
"arrange");
1876 Json
const*
const fallback = member(fields,
"default");
1877 if (decl.kind ==
"choice") {
1878 Json
const*
const options = member(fields,
"options");
1879 if (options ==
nullptr) {
1880 refuse(path,
"a `choice` point lists its `options`");
1882 decl.options = nameList(*options,
pointerAppend(path,
"options"));
1883 if (fallback !=
nullptr) {
1884 auto const*
const chosen = std::get_if<std::string>(&fallback->data);
1885 if (chosen ==
nullptr || std::ranges::find(decl.options, *chosen) == decl.options.end()) {
1886 refuse(
pointerAppend(path,
"default"),
"the default of a `choice` is one of its options");
1888 decl.defaultValue = *chosen;
1890 decl.type =
Type::of(TypeKind::String);
1891 }
else if (decl.kind ==
"value") {
1892 Json
const*
const type = member(fields,
"type");
1893 if (type ==
nullptr) {
1894 refuse(path,
"a `value` point declares its `type`");
1897 if (fallback !=
nullptr) {
1901 "the default does not have the point's type: " + value.error());
1903 decl.defaultValue = std::move(*value);
1909 [[nodiscard]]
DocNodePtr node(Json
const& json, std::string
const& path) {
1910 auto const*
const object = std::get_if<Json::object_t>(&json.data);
1911 if (
object ==
nullptr) {
1912 refuse(path,
"a node is an object with a `kind`");
1914 std::string
const kind = text(*
object,
"kind", path,
true);
1915 KindSpec
const*
const spec =
findKind(kind,
false);
1916 if (spec ==
nullptr) {
1919 auto made = std::make_shared<DocNode>();
1923 fill(*made, *
object, *spec, path);
1927 void fill(DocNode& made, Json::object_t
const&
object, KindSpec
const& spec, std::string
const& path) {
1928 bool const isPart = (spec.flags &
kPart) != 0;
1929 auto apply = [&](KeySpec
const& key) {
1930 Json
const*
const value = member(
object, key.key);
1931 if (value ==
nullptr) {
1932 if ((key.flags & kRequired) != 0) {
1933 refuse(path,
"`" + std::string{key.key} +
"` is required on `" + std::string{spec.kind} +
"`");
1940 for (KeySpec
const& key : kCommonKeys) {
1943 if ((spec.flags & kField) != 0) {
1944 for (KeySpec
const& key : kFieldKeys) {
1949 for (KeySpec
const& key : spec.keys) {
1952 if ((spec.flags & kScope) != 0) {
1953 if (Json
const*
const declarations = member(
object,
"scope")) {
1955 auto const& fields = objectOf(*declarations, where,
"`scope` holds a scope's declarations");
1957 made.scope = scope(fields, where);
1959 made.scope = ScopeDecl{};
1962 if (made.kind ==
"fields") {
1963 for (
auto const& [field, overlay] :
object) {
1964 made.parts[
"fields"].push_back(fieldOverlay(field, overlay,
pointerAppend(path, field)));
1971 [[nodiscard]]
static DocNodePtr fieldOverlay(std::string
const& field, Json
const& overlay,
1972 std::string
const& where) {
1973 auto const& settings = objectOf(overlay, where,
"a field overlay is an object");
1974 auto part = std::make_shared<DocNode>();
1975 part->kind =
"field";
1978 for (
auto const& [key, value] : settings) {
1980 if (key ==
"component") {
1982 auto const*
const name = std::get_if<std::string>(&value.data);
1983 if (name ==
nullptr || !
isName(*name)) {
1984 refuse(keyPath,
"`component` is a name");
1986 part->names.emplace(key, *name);
1987 }
else if (key ==
"props") {
1988 auto record = std::make_shared<DocNode>();
1989 record->kind =
"props";
1990 record->path = keyPath;
1991 for (
auto const& [prop, propValue] : objectOf(value, keyPath,
"`props` is a record of expressions")) {
1992 record->props.emplace(prop, expr(propValue,
pointerAppend(keyPath, prop)));
1994 part->parts[
"props"].push_back(std::move(record));
1996 part->props.emplace(key, expr(value, keyPath));
2002 [[nodiscard]]
DocNodePtr part(Json
const& json, std::string_view kind, std::string
const& path) {
2003 KindSpec
const*
const spec =
findKind(kind,
true);
2004 auto const&
object = objectOf(json, path,
"a part of a node is an object");
2006 auto made = std::make_shared<DocNode>();
2007 made->kind = std::string{kind};
2009 fill(*made,
object, *spec, path);
2014 void entry(DocNode& made, KeySpec
const& key, Json
const& value, std::string
const& path) {
2015 std::string
const name{key.key};
2016 switch (key.shape) {
2018 case Shape::Condition:
2019 made.props.emplace(name, expr(value, path));
2021 case Shape::Commands:
2022 made.events.emplace(name, commandList(value, path));
2024 case Shape::Nodes: {
2025 std::vector<DocNodePtr> nodes;
2026 if (
auto const*
const array = std::get_if<Json::array_t>(&value.data)) {
2027 for (std::size_t index = 0; index < array->size(); ++index) {
2028 nodes.push_back(node(array->at(index),
pointerAppend(path, index)));
2031 nodes.push_back(node(value, path));
2033 made.children.emplace(name, std::move(nodes));
2038 auto const*
const string = std::get_if<std::string>(&value.data);
2039 if (
string ==
nullptr || (key.shape == Shape::Name && !
isName(*
string))) {
2040 refuse(path,
"`" + name +
"` is " + (key.shape == Shape::Name ?
"a name" :
"a string"));
2045 made.names.emplace(name, *
string);
2050 made.parts[name].push_back(part(value, key.part, path));
2053 case Shape::PartsOrExpr: {
2054 auto const*
const array = std::get_if<Json::array_t>(&value.data);
2055 if (array ==
nullptr && key.shape == Shape::PartsOrExpr) {
2056 made.props.emplace(name, expr(value, path));
2059 if (array ==
nullptr) {
2060 refuse(path,
"`" + name +
"` is an array");
2062 auto&
list = made.parts[name];
2063 for (std::size_t index = 0; index < array->size(); ++index) {
2068 case Shape::ExprRecord: {
2069 if (!key.part.empty()) {
2070 made.parts[name].push_back(part(value, key.part, path));
2073 auto record = std::make_shared<DocNode>();
2074 record->kind = name;
2075 record->path = path;
2076 for (
auto const& [field, fieldValue] : objectOf(value, path,
"a record of expressions")) {
2077 record->props.emplace(field, expr(fieldValue,
pointerAppend(path, field)));
2079 made.parts[name].push_back(std::move(record));
2082 case Shape::CommandRecord:
2083 for (
auto const& [event, commands] : objectOf(value, path,
"a record of command lists")) {
2084 made.events.emplace(std::format(
"{}.{}", name, event),
2089 auto const*
const flag = std::get_if<bool>(&value.data);
2090 if (flag ==
nullptr) {
2091 refuse(path,
"`" + name +
"` is a literal `true` or `false`");
2093 made.flags.emplace(name, *flag);
2098 made.keys = keyBindings(value, path);
2258 ScopeInfo& screen = newScope(
nullptr,
false);
2259 screen.screen =
true;
2260 for (
auto const& [name, type] : _doc->params) {
2265 add(screen, name, std::move(entry));
2267 declare(_doc->scope, screen);
2268 for (
auto const& [key, commands] : _doc->lifecycle) {
2269 static_cast<void>(key);
2270 collectTransfers(commands, screen);
2272 for (
auto const& [chord, commands] : _doc->keys) {
2273 static_cast<void>(chord);
2274 collectTransfers(commands, screen);
2277 collect(*_doc->view, screen);
2280 collectPart(*part, screen, screen);
2282 std::vector<ScopeInfo*> componentScopes;
2283 for (
auto const& [name, component] : _doc->components) {
2284 static_cast<void>(name);
2285 ScopeInfo& fallbackScope = newScope(
nullptr,
false);
2286 collect(*component.fallback, fallbackScope);
2287 componentScopes.push_back(&fallbackScope);
2291 .scope = &screen, .commands =
false, .boundaryError =
false, .onClose =
false, .bindings = {}};
2292 checkScope(_doc->scope, root);
2294 checkExpr(*_doc->title, root);
2296 if (_doc->identity) {
2297 checkExpr(*_doc->identity, root);
2300 checkCondition(*_doc->dirty, root,
"`dirty`");
2302 if (_doc->subtitle) {
2303 checkExpr(*_doc->subtitle, root);
2306 checkElement(*part, root,
true);
2308 checkActionButtons(_doc->actions,
"/actions");
2309 for (
auto const& [key, commands] : _doc->lifecycle) {
2311 context.
onClose = key ==
"onClose";
2312 checkCommands(commands, context);
2314 for (
auto const& [chord, commands] : _doc->keys) {
2315 static_cast<void>(chord);
2316 checkCommands(commands, root);
2320 checkNode(*_doc->view, root);
2322 std::size_t index = 0;
2323 for (
auto const& [name, component] : _doc->components) {
2324 static_cast<void>(name);
2325 CheckContext context{.scope = componentScopes.at(index++),
2327 .boundaryError =
false,
2331 checkNode(*component.fallback, context);
2333 checkCustomizePoints();
2340 [[nodiscard]] std::vector<LoadWarning>
takeWarnings() noexcept {
return std::move(_warnings); }
2345 return std::move(_dateValueKinds);
2353 for (
ScopeInfo* current = scope; current !=
nullptr; current = current->parent) {
2354 auto const found = current->names.find(name);
2355 if (found != current->names.end()) {
2356 return &found->second;
2370 if (entry.
visit == 1) {
2372 auto const start = std::ranges::find(_deriving, name);
2373 for (
auto step = start; step != _deriving.end(); ++step) {
2374 cycle += *step +
" → ";
2376 std::string
const graph =
2378 return std::unexpected(graph + cycle + std::string{name});
2381 _deriving.emplace_back(name);
2383 .scope = entry.
scope, .commands =
false, .boundaryError =
false, .onClose =
false, .bindings = {}};
2385 _deriving.pop_back();
2397 ScopeInfo& made = _scopes.emplace_back();
2403 static void add(ScopeInfo& scope, std::string
const& name, NameEntry entry) {
2404 static constexpr std::array<std::string_view, 6> kReserved = {
"event",
"app",
"boundary",
2405 "props",
"result",
"body"};
2406 if (listed(kReserved, name)) {
2407 refuse(entry.path,
"`" + name +
"` is reserved and cannot be declared");
2409 auto const found = scope.
names.find(name);
2410 if (found != scope.names.end()) {
2411 refuse(entry.path,
"`" + name +
"` is declared twice in this scope (first at " + found->second.path +
")");
2413 entry.scope = &scope;
2414 scope.names.emplace(name, std::move(entry));
2418 void checkCatalog(ModelDecl
const& model) {
2419 if (!_options->catalog.model) {
2422 auto const info = _options->catalog.model(model.type);
2424 refuse(
pointerAppend(model.path,
"type"),
"the model type `" + model.type +
"` is not in the catalog");
2426 if (model.instance == InstanceMode::Shared && !info->stateless) {
2428 "the model type `" + model.type +
2429 "` keeps state, so `shared` names no instance: it needs a key, or `private`");
2431 if (model.instance == InstanceMode::Keyed && !info->keyed) {
2433 "the model type `" + model.type +
"` is not keyed, so its instance is not an expression");
2437 void declare(ScopeDecl
const& decl, ScopeInfo& scope) {
2438 for (ModelDecl
const& model : decl.models) {
2439 if (scope.row && model.instance == InstanceMode::Default) {
2440 refuse(model.path,
"a model alias in a row scope names its `instance`");
2442 if (!scope.screen && model.instance == InstanceMode::Borrowed) {
2443 refuse(model.path,
"only a screen's own aliases borrow the opener's instance");
2445 checkCatalog(model);
2447 entry.category = NameCategory::Model;
2448 entry.path = model.path;
2449 entry.model = &model;
2450 add(scope, model.name, std::move(entry));
2452 for (StateDecl
const& state : decl.state) {
2454 entry.category = NameCategory::State;
2455 entry.path = state.path;
2456 entry.type = state.type;
2457 entry.derivedFrom = state.type.isAny() ? state.init :
nullptr;
2458 add(scope, state.name, std::move(entry));
2460 for (LetDecl
const& let : decl.lets) {
2462 entry.category = NameCategory::Let;
2463 entry.path = let.path;
2464 entry.derivedFrom = let.value;
2465 add(scope, let.name, std::move(entry));
2467 for (QueryDecl
const& query : decl.queries) {
2469 entry.category = NameCategory::Query;
2470 entry.path = query.path;
2471 entry.query = &query;
2472 add(scope, query.name, std::move(entry));
2473 _queries.emplace(query.name, &query);
2474 _queryScopes.emplace_back(&query, &scope);
2476 for (MutationDecl
const& mutation : decl.mutations) {
2478 entry.category = NameCategory::Mutation;
2479 entry.path = mutation.path;
2480 entry.mutation = &mutation;
2481 add(scope, mutation.name, std::move(entry));
2482 _mutations.emplace_back(&mutation, &scope);
2483 collectTransfers(mutation.onSuccess, scope);
2484 collectTransfers(mutation.onError, scope);
2486 for (WatchDecl
const& watch : decl.watches) {
2487 _watches.emplace_back(&watch, &scope);
2488 collectTransfers(watch.body, scope);
2492 void collectTransfers(Commands
const& commands, ScopeInfo& scope) {
2493 for (Command
const& cmd : commands) {
2494 if (cmd.kind == CommandKind::Upload && !cmd.transfer.empty()) {
2496 entry.category = NameCategory::Transfer;
2498 add(scope, cmd.transfer, std::move(entry));
2500 collectTransfers(cmd.commands, scope);
2501 collectTransfers(cmd.alternative, scope);
2505 void collect(DocNode
const& node, ScopeInfo& scope) { collectIn(node, scope); }
2507 void collectIn(DocNode
const& node, ScopeInfo& scope) {
2508 KindSpec
const*
const spec =
findKind(node.kind,
false);
2509 if (!node.id.empty()) {
2511 entry.category = NameCategory::Element;
2513 entry.element = &node;
2514 add(scope, node.id, std::move(entry));
2515 _elements.emplace(node.id, &node);
2517 ScopeInfo* inner = &scope;
2518 if (spec !=
nullptr && (spec->flags & kScope) != 0) {
2519 inner = &newScope(&scope, (spec->flags & kRowScope) != 0);
2520 _scopeOf.emplace(&node, inner);
2522 declare(*node.scope, *inner);
2524 _innerOf.emplace(&node, inner);
2525 for (
auto const& [key, commands] : node.events) {
2526 collectTransfers(commands, isInner(spec, key) ? *inner : scope);
2528 for (
auto const& [chord, commands] : node.keys) {
2529 static_cast<void>(chord);
2530 collectTransfers(commands, scope);
2532 for (
auto const& [key, nodes] : node.children) {
2533 for (DocNodePtr
const& child : nodes) {
2534 collectIn(*child, isInner(spec, key) ? *inner : scope);
2537 for (
auto const& [key, parts] : node.parts) {
2538 for (DocNodePtr
const& child : parts) {
2539 collectPart(*child, isInner(spec, key) ? *inner : scope, *inner);
2544 void collectPart(DocNode
const& part, ScopeInfo& outer, ScopeInfo& ownerInner) {
2545 KindSpec
const*
const spec =
findKind(part.kind,
true);
2546 ScopeInfo* inner = &ownerInner;
2547 if (spec !=
nullptr && (spec->flags & kScope) != 0) {
2548 inner = &newScope(&outer,
false);
2549 _scopeOf.emplace(&part, inner);
2551 declare(*part.scope, *inner);
2553 _innerOf.emplace(&part, inner);
2554 for (
auto const& [key, commands] : part.events) {
2555 collectTransfers(commands, outer);
2557 for (
auto const& [key, nodes] : part.children) {
2558 for (DocNodePtr
const& child : nodes) {
2559 collectIn(*child, isInner(spec, key) ? *inner : outer);
2562 for (
auto const& [key, parts] : part.parts) {
2563 static_cast<void>(key);
2564 for (DocNodePtr
const& child : parts) {
2565 collectPart(*child, outer, ownerInner);
2570 [[nodiscard]]
static bool isInner(KindSpec
const* spec, std::string_view key) {
2571 if (spec ==
nullptr) {
2574 auto const found = std::ranges::find(spec->keys, key, &
KeySpec::key);
2575 return found != spec->keys.end() && (found->flags &
kInner) != 0;
2580 [[nodiscard]]
static std::uint8_t keyFlags(KindSpec
const& spec, std::string_view key) {
2581 return std::ranges::find(spec.keys, key, &
KeySpec::key)->flags;
2584 [[nodiscard]]
static bool isCondition(KindSpec
const* spec, std::string_view key) {
2585 auto shapeOf = [key](std::span<KeySpec const> keys) {
2586 auto const found = std::ranges::find(keys, key, &
KeySpec::key);
2587 return found == keys.end() ? Shape::Expr : found->shape;
2589 return (spec !=
nullptr && shapeOf(spec->keys) == Shape::Condition) ||
2590 shapeOf(kCommonKeys) == Shape::Condition || shapeOf(kFieldKeys) == Shape::Condition;
2593 Type checkExpr(Expr
const& expr, CheckContext
const& context) {
2594 ScopeResolver
const resolver{*
this, context};
2597 throw DocumentRefused{std::move(checked.error())};
2599 return std::move(*checked);
2602 void checkCondition(Expr
const& expr, CheckContext
const& context, std::string_view what) {
2603 Type
const type = checkExpr(expr, context);
2605 refuse(expr.path, std::string{what} +
" is " + type.name() +
"; expected bool");
2610 void checkDateValue(DocNode
const& node, Expr
const& expr, Type
const& type) {
2611 if (type.kind == TypeKind::String || type.kind == TypeKind::Timestamp) {
2612 _dateValueKinds.insert_or_assign(&node, type.kind);
2613 }
else if (type.kind != TypeKind::Any && type.kind != TypeKind::Key) {
2615 "`dateTimeInput` shows a timestamp or an ISO-8601 date or date-time string; `value` is " +
2620 [[nodiscard]]
static NameEntry& require(CheckContext
const& context, std::string
const& name,
2621 std::string
const& path, std::initializer_list<NameCategory> categories,
2622 std::string_view what) {
2623 NameEntry*
const entry =
lookup(context.scope, name);
2624 if (entry ==
nullptr || std::ranges::find(categories, entry->category) == categories.end()) {
2625 refuse(path,
"`" + name +
"` is not " + std::string{what} +
" of this scope");
2631 std::optional<ActionInfo> checkModelAction(CheckContext
const& context, std::string
const& alias,
2632 std::string
const& action, std::string
const& path) {
2634 NameEntry
const& model = require(context, alias, modelPath, {NameCategory::Model},
"a model alias");
2635 if (!_options->catalog.action) {
2636 return std::nullopt;
2638 auto const info = _options->catalog.action(action);
2640 refuse(path,
"the action `" + action +
"` is not in the catalog");
2642 if (info->modelType != model.model->type) {
2643 refuse(path,
"the action `" + action +
"` belongs to `" + info->modelType +
"`, not `" +
2644 model.model->type +
"`");
2649 void checkScope(ScopeDecl
const& decl, CheckContext
const& context) {
2650 for (ModelDecl
const& model : decl.models) {
2652 checkExpr(*model.key, context);
2655 for (StateDecl
const& state : decl.state) {
2659 Type
const type = checkExpr(*state.init, context);
2660 bool const literalFits = state.init->kind != ExprKind::Literal || state.type.admits(state.init->literal);
2661 if (!literalFits || !
compatible(state.type, type)) {
2663 "the init of `" + state.name +
"` does not have its type " + state.type.name());
2666 auto const derived =
derivedType(*
lookup(context.scope, state.name), state.name);
2667 static_cast<void>(derived);
2669 for (LetDecl
const& let : decl.lets) {
2671 static_cast<void>(type);
2673 for (QueryDecl
const& query : decl.queries) {
2674 checkQuery(query, context);
2676 for (MutationDecl
const& mutation : decl.mutations) {
2677 checkMutation(mutation, context);
2679 for (WatchDecl
const& watch : decl.watches) {
2680 checkExpr(*watch.on, context);
2681 checkCommands(watch.body, context);
2683 for (CheckDecl
const& check : decl.checks) {
2684 checkCondition(*check.assertion, context,
"the `assert` of a check");
2685 if (check.message) {
2686 checkExpr(*check.message, context);
2688 for (std::size_t index = 0; index < check.targets.size(); ++index) {
2689 static_cast<void>(require(context, check.targets.at(index),
2691 {NameCategory::Element},
"an element"));
2696 void checkQuery(QueryDecl
const& query, CheckContext
const& context) {
2697 auto const action = checkModelAction(context, query.model, query.action, query.path);
2699 if (action && action->effects && !query.evaluate) {
2700 refuse(query.path, std::format(
"a query reads, so its action declares no effects; `{}` declares {}, "
2701 "so it runs as a mutation",
2702 query.action, describeEffects(*action->effects)));
2705 checkExpr(*query.body, context);
2708 checkCondition(*query.when, context,
"`when`");
2712 void checkMutation(MutationDecl
const& mutation, CheckContext
const& context) {
2713 auto const action = checkModelAction(context, mutation.model, mutation.action, mutation.path);
2714 if (mutation.body) {
2715 checkExpr(*mutation.body, context);
2717 if (mutation.serialKey) {
2718 CheckContext keyContext = context;
2719 keyContext.bindings.emplace_back(
"body",
Type::any());
2720 checkExpr(*mutation.serialKey, keyContext);
2722 if (mutation.optimistic) {
2723 OptimisticDecl
const& decl = *mutation.optimistic;
2724 std::string
const path =
pointerAppend(mutation.path,
"optimistic");
2725 auto const target = _queries.find(decl.query);
2726 if (target == _queries.end()) {
2727 refuse(
pointerAppend(path,
"query"),
"`" + decl.query +
"` is not a query of this document");
2729 CheckContext bodyContext = context;
2730 bodyContext.bindings.emplace_back(
"body",
Type::any());
2731 checkExpr(*decl.key, bodyContext);
2732 checkExpr(*decl.with, bodyContext);
2733 checkPatchAt(decl.query, decl.at, *target->second, path);
2734 checkOptimistic(decl, *target->second);
2736 if (mutation.offlineQueue && action) {
2737 if (action->credential || !action->secretFields.empty()) {
2739 "a mutation that sends a credential or returns a secret is never queued offline");
2742 checkCommands(mutation.onSuccess, context);
2743 checkCommands(mutation.onError, context);
2749 void checkPatchAt(std::string
const& queryName, std::string
const& listMember, QueryDecl
const& query,
2750 std::string
const& path) {
2751 if (!_options->catalog.action) {
2754 auto const info = _options->catalog.action(query.action);
2755 if (!info || !info->listFields) {
2758 if (listMember.empty()) {
2759 refuse(path, std::format(
"`{}` answers a record, so a patch of `{}` names the member holding its list "
2761 query.action, queryName));
2763 if (std::ranges::find(*info->listFields, listMember) == info->listFields->end()) {
2765 std::format(
"the result of `{}` has no list member `{}`", query.action, listMember));
2769 void checkOptimistic(OptimisticDecl
const& decl, QueryDecl
const& query) {
2770 if (!_options->catalog.action || decl.with->kind != ExprKind::Object) {
2773 auto const info = _options->catalog.action(query.action);
2777 for (
auto const& [field, value] : decl.with->fields) {
2778 if (std::ranges::find(info->calculatedFields, field) != info->calculatedFields.end()) {
2780 std::format(
"`{}` is calculated by the server and is never written optimistically", field));
2785 void checkPrefetch() {
2786 for (std::size_t index = 0; index < _doc->prefetch.size(); ++index) {
2787 std::string
const& name = _doc->prefetch.at(index);
2789 auto const found = std::ranges::find(_doc->scope.queries, name, &
QueryDecl::name);
2790 if (found == _doc->scope.queries.end()) {
2791 refuse(path,
"`" + name +
"` is not a query of the screen");
2793 if (found->body && !readsOnlyParams(*found->body)) {
2794 refuse(path,
"the body of `" + name +
"` reads more than the screen's params, so it cannot prefetch");
2799 [[nodiscard]]
bool readsOnlyParams(Expr
const& expr)
const {
2800 if (expr.kind == ExprKind::Ref) {
2801 return std::ranges::find(_doc->params, expr.name, &std::pair<std::string, Type>::first) !=
2804 if (expr.kind != ExprKind::Literal && expr.kind != ExprKind::Call && expr.kind != ExprKind::If &&
2805 expr.kind != ExprKind::Object && expr.kind != ExprKind::List) {
2809 for (ExprPtr
const& arg : expr.args) {
2810 only = only && readsOnlyParams(*arg);
2812 for (
auto const& [name, field] : expr.fields) {
2813 static_cast<void>(name);
2814 only = only && readsOnlyParams(*field);
2819 void checkCustomizePoints() {
2820 std::set<std::string, std::less<>> arranged;
2821 for (
auto const& [name, point] : _doc->customize) {
2822 static_cast<void>(name);
2823 if (point.kind ==
"columns" && _doc->view) {
2824 auto const found = _elements.find(point.of);
2825 if (found == _elements.end() || found->second->kind !=
"table") {
2827 "a `columns` point names a table; `" + point.of +
"` is not one");
2830 if (point.kind ==
"arrange") {
2831 checkArranged(point, arranged);
2838 void checkArranged(CustomizePoint
const& point, std::set<std::string, std::less<>>& arranged) {
2839 auto const found = _elements.find(point.of);
2840 if (found == _elements.end() || (found->second->kind !=
"column" && found->second->kind !=
"row") ||
2841 found->second->nodes(
"children").empty()) {
2843 "an `arrange` point names a column or row of this screen that has children; `" + point.of +
2846 for (DocNodePtr
const& child : found->second->nodes(
"children")) {
2847 if (child->id.empty()) {
2849 "an `arrange` point orders its container's children by id; a child of `" + point.of +
2854 std::string
const asList =
2855 "an arranged `" + found->second->kind +
"` is drawn as a list, which takes no `align` or `justify`";
2856 for (std::string_view
const key : {
"align",
"justify"}) {
2857 if (!found->second->name(key).empty()) {
2861 if (!arranged.insert(point.of).second) {
2862 refuse(
pointerAppend(point.path,
"of"),
"`" + point.of +
"` is arranged by another point");
2866 [[nodiscard]] CheckContext innerContext(DocNode
const& node, CheckContext
const& context)
const {
2867 CheckContext inner = context;
2868 inner.scope = _innerOf.at(&node);
2872 void checkNode(DocNode
const& node, CheckContext
const& context) {
2873 if (!node.name(
"alignSelf").empty()) {
2876 checkElement(node, context,
false);
2880 static constexpr std::string_view kAlignSelfPlaces =
"`alignSelf` places a node in its row, column or grid";
2884 static void checkPlacements(DocNode
const& parent) {
2886 parent.kind ==
"row" || parent.kind ==
"column" || parent.kind ==
"grid" || parent.kind ==
"forEach";
2887 for (
auto const& [key, nodes] : parent.children) {
2888 for (DocNodePtr
const& child : nodes) {
2889 if (!child->name(
"alignSelf").empty() && (!places || key !=
"children")) {
2896 void checkElement(DocNode
const& node, CheckContext
const& outer,
bool part) {
2897 KindSpec
const*
const spec =
findKind(node.kind, part);
2898 CheckContext
const inner = innerContext(node, outer);
2899 if (
auto const found = _scopeOf.find(&node); found != _scopeOf.end()) {
2901 checkScope(*node.scope, inner);
2903 checkProps(node, spec, inner, outer);
2904 for (
auto const& [key, commands] : node.events) {
2905 if (node.kind ==
"custom") {
2906 checkCustomEvent(node, key.substr(3), node.path);
2908 checkCommands(commands, isInner(spec, key) ? inner : outer);
2910 for (
auto const& [chord, commands] : node.keys) {
2911 static_cast<void>(chord);
2912 checkCommands(commands, outer);
2914 for (
auto const& [key, nodes] : node.children) {
2915 CheckContext context = isInner(spec, key) ? inner : outer;
2916 context.boundaryError = (keyFlags(*spec, key) &
kBoundaryError) != 0 || context.boundaryError;
2917 for (DocNodePtr
const& child : nodes) {
2918 checkElement(*child, context,
false);
2921 for (
auto const& [key, parts] : node.parts) {
2922 CheckContext
const& context = isInner(spec, key) ? inner : outer;
2923 for (DocNodePtr
const& child : parts) {
2924 checkElement(*child, context,
true);
2927 checkPlacements(node);
2930 checkLayoutKeys(node);
2931 checkKindRules(node, outer);
2933 checkActionButtons(node.partList(
"actions"),
pointerAppend(node.path,
"actions"));
2934 checkFieldComponents(node);
2938 void checkProps(DocNode
const& node, KindSpec
const* spec, CheckContext
const& inner, CheckContext
const& outer) {
2939 for (
auto const& [key, expr] : node.props) {
2940 CheckContext
const& context = isInner(spec, key) ? inner : outer;
2941 if (isCondition(spec, key)) {
2942 checkCondition(*expr, context,
"`" + key +
"`");
2945 Type
const type = checkExpr(*expr, context);
2946 if (node.kind ==
"dateTimeInput" && key ==
"value") {
2947 checkDateValue(node, *expr, type);
2952 void checkCustomEvent(DocNode
const& node, std::string
const& event, std::string
const& path) {
2953 auto const found = _doc->components.find(node.name(
"name"));
2954 if (found != _doc->components.end() && !found->second.events.contains(event)) {
2956 "the component `" + found->second.name +
"` declares no event `" + event +
"`");
2961 static void checkFlags(DocNode
const& node) {
2962 if (node.flag(
"showLabel") && node.prop(
"label") ==
nullptr) {
2963 refuse(
pointerAppend(node.path,
"showLabel"),
"`showLabel` shows the `label`, and the node gives none");
2967 void checkKindRules(DocNode
const& node, CheckContext
const& context) {
2968 if (node.kind ==
"custom") {
2969 checkCustomProps(node);
2970 }
else if (node.kind ==
"customize" && !_doc->customize.contains(node.name(
"point"))) {
2972 "`" + std::string{node.name(
"point")} +
"` is not a customization point of this document");
2974 checkKindValues(node);
2975 std::string_view
const model = node.name(
"model");
2976 if (!model.empty()) {
2977 if (node.name(
"action").empty()) {
2978 refuse(node.path,
"a `" + node.kind +
"` with a `model` names its `action`");
2980 checkModelAction(context, std::string{model}, std::string{node.name(
"action")}, node.path);
2981 }
else if (node.kind ==
"table" && !node.prop(
"rows")) {
2982 refuse(node.path,
"a table takes `rows`, or a `model` and `action`");
2987 void checkCustomProps(DocNode
const& node)
const {
2988 auto const found = _doc->components.find(node.name(
"name"));
2989 if (found == _doc->components.end()) {
2991 "`" + std::string{node.name(
"name")} +
"` is not a component of this document");
2993 for (DocNodePtr
const& props : node.partList(
"props")) {
2994 for (
auto const& [prop, value] : props->props) {
2995 if (!found->second.props.contains(prop)) {
2996 refuse(value->path,
"the component `" + found->first +
"` declares no prop `" + prop +
"`");
3003 static void checkKindValues(DocNode
const& node) {
3004 if (node.kind ==
"button") {
3005 static constexpr std::array<std::string_view, 4> kEmphasis = {
"normal",
"primary",
"danger",
"ghost"};
3006 checkChoice(node,
"emphasis", kEmphasis);
3007 static constexpr std::array<std::string_view, 2> kStyles = {
"push",
"item"};
3008 checkChoice(node,
"style", kStyles);
3009 if (node.prop(
"selected") !=
nullptr && node.name(
"style") !=
"item") {
3011 "`selected` marks a list item; give the button `style: item`");
3013 }
else if (node.kind ==
"grid" && !node.name(
"justify").empty()) {
3015 "a grid's columns share its width; there is no spare length to justify");
3016 }
else if (node.kind ==
"textInput") {
3017 static constexpr std::array<std::string_view, 3> kCommit = {
"change",
"blur",
"enter"};
3018 checkChoice(node,
"commitOn", kCommit);
3019 }
else if (node.kind ==
"select") {
3020 static constexpr std::array<std::string_view, 2> kStale = {
"clear",
"keep"};
3021 checkChoice(node,
"onStale", kStale);
3022 }
else if (node.kind ==
"dialog" || node.kind ==
"drawer" || node.kind ==
"form") {
3023 static constexpr std::array<std::string_view, 3> kPolicies = {
"touched",
"submit",
"always"};
3024 checkChoice(node,
"showErrors", kPolicies);
3026 static constexpr std::array<std::string_view, 2> kSubmitModes = {
"explicit",
"auto"};
3027 checkChoice(node,
"submit", kSubmitModes);
3028 }
else if (node.kind ==
"text") {
3029 checkTextRole(node);
3031 if (node.kind ==
"form" || node.kind ==
"collection" || node.kind ==
"wizard") {
3032 checkFormColumns(node);
3033 static constexpr std::array<std::string_view, 2> kFieldLayouts = {
"stacked",
"inline"};
3034 checkChoice(node,
"fieldLayout", kFieldLayouts);
3036 static constexpr std::array<std::string_view, 4> kAlign = {
"start",
"center",
"end",
"stretch"};
3037 static constexpr std::array<std::string_view, 4> kJustify = {
"start",
"center",
"end",
"between"};
3038 checkChoice(node,
"align", kAlign);
3039 checkChoice(node,
"justify", kJustify);
3040 checkChoice(node,
"alignSelf", kAlign);
3044 void checkFieldComponents(DocNode
const& form)
const {
3045 if (
form.kind !=
"form") {
3048 for (DocNodePtr
const& record :
form.partList(
"fields")) {
3049 for (DocNodePtr
const& field : record->partList(
"fields")) {
3050 if (!field->name(
"component").empty()) {
3051 checkFieldComponent(*field);
3059 void checkFieldComponent(DocNode
const& field)
const {
3060 std::string_view
const name = field.name(
"component");
3061 std::string
const where =
pointerAppend(field.path,
"component");
3062 auto const found = _doc->components.find(name);
3063 if (found == _doc->components.end()) {
3064 refuse(where,
"`" + std::string{name} +
"` is not a component of this document");
3066 Component
const& component = found->second;
3067 if (!component.props.contains(
"value") || !component.events.contains(
"change")) {
3068 refuse(where,
"the component `" + component.name +
3069 "` draws a form field, so it declares a prop `value` and an event `change`");
3071 if (field.prop(
"widget") !=
nullptr) {
3072 refuse(where,
"`component` and `widget` both choose the field's control; give one");
3074 for (DocNodePtr
const& props : field.partList(
"props")) {
3075 for (
auto const& [prop, value] : props->props) {
3076 if (!component.props.contains(prop)) {
3077 refuse(value->path,
"the component `" + component.name +
"` declares no prop `" + prop +
"`");
3084 [[nodiscard]] std::vector<DocNodePtr> headParts()
const {
3085 std::vector<DocNodePtr> parts = _doc->status;
3086 parts.insert(parts.end(), _doc->actions.begin(), _doc->actions.end());
3091 static void checkActionButtons(std::span<DocNodePtr const> buttons, std::string
const& path) {
3092 static constexpr std::array<std::string_view, 4> kEmphasis = {
"normal",
"primary",
"danger",
"ghost"};
3093 std::set<std::string_view> ids;
3094 for (DocNodePtr
const& button : buttons) {
3095 if (!ids.insert(
button->
id).second) {
3097 "the action id `" +
button->
id +
"` is used twice in " + path +
"; each action has its own");
3099 checkChoice(*button,
"emphasis", kEmphasis);
3105 static std::optional<std::int64_t> sizingDimension(Value
const& value, std::string
const& path) {
3106 static constexpr std::string_view kForms =
3107 "`sizing` is `\"content\"`, `{\"fixed\": n}`, `{\"stretch\": w}`, or a record of `width` and `height` "
3109 if (std::string
const*
const text = value.asString(); text !=
nullptr && *text ==
"content") {
3110 return std::nullopt;
3112 ValueRecord const*
const record = value.asRecord();
3113 if (record !=
nullptr && record->size() == 1) {
3114 auto const& [
form, amount] = *record->begin();
3115 std::int64_t
const*
const units = amount.asInt();
3116 if (form ==
"fixed" && units !=
nullptr && *units >= 0) {
3119 if (form ==
"stretch" && units !=
nullptr && *units >= 1) {
3120 return std::nullopt;
3123 refuse(path, std::string{kForms});
3129 static void checkLayoutKeys(DocNode
const& node) { checkSizing(node, literalBounds(node)); }
3133 static std::array<std::optional<std::int64_t>, 4> literalBounds(DocNode
const& node) {
3134 static constexpr std::array<std::string_view, 4> kBounds = {
"minWidth",
"maxWidth",
"minHeight",
"maxHeight"};
3135 std::array<std::optional<std::int64_t>, 4> bounds{};
3136 for (std::size_t index = 0; index < kBounds.size(); ++index) {
3137 std::string_view
const key = kBounds.at(index);
3138 ExprPtr const bound = node.prop(key);
3139 if (bound ==
nullptr) {
3142 if (node.kind ==
"dialog" || node.kind ==
"drawer") {
3143 refuse(bound->path,
"an overlay is sized by the frontend, not by its container");
3145 if (node.kind ==
"custom") {
3147 "a custom node carries no common properties, so it takes no `" + std::string{key} +
"`");
3149 std::int64_t
const*
const units =
3150 bound->kind == ExprKind::Literal ? bound->literal.asInt() :
static_cast<std::int64_t const*
>(
nullptr);
3151 if (units ==
nullptr || *units < 0) {
3152 refuse(bound->path,
"`" + std::string{key} +
"` is a literal integer of 0 or more, in backend units");
3154 bounds.at(index) = *units;
3156 for (std::size_t axis = 0; axis < 2; ++axis) {
3157 auto const& minimum = bounds.at(2 * axis);
3158 auto const& maximum = bounds.at((2 * axis) + 1);
3159 if (minimum && maximum && *minimum > *maximum) {
3161 "`" + std::string{kBounds.at(2 * axis)} +
"` is more than `" +
3162 std::string{kBounds.at((2 * axis) + 1)} +
"`");
3172 static bool isStretch(Value
const& value) {
3173 ValueRecord const*
const record = value.asRecord();
3174 return record !=
nullptr && record->size() == 1 && record->begin()->first ==
"stretch";
3180 static void checkOverlaySizing(DocNode
const& node, ExprPtr
const& sizing) {
3181 if (node.kind !=
"dialog" && node.kind !=
"drawer") {
3184 ValueRecord const*
const record = sizing->literal.asRecord();
3185 bool const stretches =
3186 isStretch(sizing->literal) ||
3187 (record !=
nullptr && (record->contains(
"width") || record->contains(
"height")) &&
3188 std::ranges::any_of(*record, [](
auto const& entry) { return isStretch(entry.second); }));
3191 "an overlay is sized by the frontend, not by its container; give it `{\"fixed\": n}` instead "
3192 "of `{\"stretch\": w}`");
3197 static void checkSizing(DocNode
const& node, std::array<std::optional<std::int64_t>, 4>
const& bounds) {
3198 ExprPtr const sizing = node.prop(
"sizing");
3199 if (sizing ==
nullptr || sizing->kind != ExprKind::Literal) {
3202 checkOverlaySizing(node, sizing);
3203 std::optional<std::int64_t> width;
3204 std::optional<std::int64_t> height;
3205 ValueRecord const*
const record = sizing->literal.asRecord();
3206 if (record !=
nullptr && (record->contains(
"width") || record->contains(
"height"))) {
3207 for (
auto const& [dimension, value] : *record) {
3208 if (dimension ==
"width") {
3209 width = sizingDimension(value, sizing->path);
3210 }
else if (dimension ==
"height") {
3211 height = sizingDimension(value, sizing->path);
3213 refuse(sizing->path,
"a `sizing` record has `width` and `height`; `" + dimension +
"` is neither");
3217 width = sizingDimension(sizing->literal, sizing->path);
3219 for (
auto const& [fixed, axis] : {std::pair{width, std::size_t{0}}, std::pair{height, std::size_t{1}}}) {
3220 auto const& minimum = bounds.at(2 * axis);
3221 auto const& maximum = bounds.at((2 * axis) + 1);
3222 if (fixed && (*fixed < minimum.value_or(*fixed) || *fixed > maximum.value_or(*fixed))) {
3223 refuse(sizing->path, std::string{
"a fixed size has no room to bound: the "} +
3224 (axis == 0 ?
"width" :
"height") +
" is fixed outside its bounds");
3231 static void checkTextRole(DocNode
const& node) {
3232 ExprPtr const role = node.prop(
"role");
3233 if (role ==
nullptr || role->kind != ExprKind::Literal) {
3236 static constexpr std::array<std::string_view, 7> kRoles = {
"normal",
"muted",
"heading",
"title",
3237 "section",
"error",
"success"};
3238 std::string
const*
const name = role->literal.asString();
3239 if (name ==
nullptr) {
3242 if (!
listed(kRoles, *name)) {
3243 refuse(
pointerAppend(node.path,
"role"),
"`" + *name +
"` is not one of the values of `role`");
3248 static void checkFormColumns(DocNode
const& node) {
3249 ExprPtr const columns = node.prop(
"columns");
3250 if (columns ==
nullptr) {
3253 std::int64_t
const*
const count =
3254 columns->kind == ExprKind::Literal ? columns->literal.asInt() :
static_cast<std::int64_t const*
>(
nullptr);
3255 if (count ==
nullptr) {
3256 refuse(columns->path,
"`columns` is a literal integer");
3258 if (*count < 1 || *count > 12) {
3259 refuse(columns->path,
"`columns` is 1 to 12; a form lays its fields in at least one column");
3263 static void checkChoice(DocNode
const& node, std::string_view key, std::span<std::string_view const> choices) {
3264 std::string_view
const value = node.name(key);
3265 if (!value.empty() && !
listed(choices, value)) {
3267 "`" + std::string{value} +
"` is not one of the values of `" + std::string{key} +
"`");
3271 void checkCommands(Commands
const& commands, CheckContext
const& context,
bool sequence =
false) {
3272 CheckContext current = context;
3273 current.commands =
true;
3274 for (Command
const& cmd : commands) {
3275 checkCommand(cmd, current);
3276 if (sequence && cmd.kind == CommandKind::Run && !cmd.binding.empty() && !cmd.extra) {
3277 current.bindings.emplace_back(cmd.binding,
Type::any());
3283 void checkCommand(Command
const& cmd, CheckContext
const& context) {
3284 std::string
const where = cmd.path;
3286 case CommandKind::Set:
3287 checkSet(cmd, context);
3289 case CommandKind::Reset: {
3290 NameEntry
const& entry = require(context, cmd.target, where,
3291 {NameCategory::State, NameCategory::Element},
"state or a dialog");
3292 if (entry.category == NameCategory::Element && entry.element->kind !=
"dialog" &&
3293 entry.element->kind !=
"drawer") {
3294 refuse(where,
"`reset` restores state, or the state of a dialog; `" + cmd.target +
"` is a " +
3295 entry.element->kind);
3299 case CommandKind::Run: {
3300 static_cast<void>(require(context, cmd.target, where, {NameCategory::Mutation},
"a mutation"));
3301 CheckContext body = context;
3303 checkExpr(*cmd.extra, context);
3304 if (!cmd.binding.empty()) {
3305 body.bindings.emplace_back(cmd.binding,
Type::any());
3309 checkExpr(*cmd.value, body);
3313 case CommandKind::Refetch:
3314 static_cast<void>(require(context, cmd.target, where, {NameCategory::Query},
"a query"));
3316 case CommandKind::Open:
3317 case CommandKind::Close:
3318 if (cmd.kind == CommandKind::Close && cmd.target ==
"self") {
3321 checkDialog(cmd, context);
3323 case CommandKind::Focus:
3324 static_cast<void>(require(context, cmd.target, where, {NameCategory::Element},
"an element"));
3326 case CommandKind::ResetPrefs:
3327 if (cmd.target !=
"all" && !_doc->customize.contains(cmd.target)) {
3328 refuse(where,
"`" + cmd.target +
"` is not a customization point of this document");
3331 case CommandKind::If:
3332 checkCondition(*cmd.value, context,
"the condition of `if`");
3333 checkCommands(cmd.commands, context);
3334 checkCommands(cmd.alternative, context);
3336 case CommandKind::Seq:
3337 checkCommands(cmd.commands, context,
true);
3339 case CommandKind::All:
3340 case CommandKind::Navigate:
3342 checkExpr(*cmd.value, context);
3345 checkExpr(*cmd.extra, context);
3347 checkCommands(cmd.commands, context);
3348 checkCommands(cmd.alternative, context);
3350 case CommandKind::CancelClose:
3351 if (!context.onClose) {
3352 refuse(where,
"`cancelClose` is valid only in `onClose`");
3355 case CommandKind::Retry:
3356 if (!context.boundaryError) {
3357 refuse(where,
"`retry` is valid only inside a boundary's `error`");
3360 case CommandKind::SignIn:
3361 static_cast<void>(require(context, cmd.target, where, {NameCategory::Mutation},
"a mutation"));
3363 case CommandKind::Upload:
3364 static_cast<void>(require(context, cmd.target, where, {NameCategory::Mutation},
"a mutation"));
3365 checkExpr(*cmd.value, context);
3367 case CommandKind::Download:
3368 checkExpr(*cmd.value, context);
3370 checkExpr(*cmd.extra, context);
3372 if (cmd.mediaType) {
3373 checkExpr(*cmd.mediaType, context);
3375 if (cmd.option ==
"preview") {
3376 static_cast<void>(require(context, cmd.target, where, {NameCategory::State},
"state"));
3379 case CommandKind::Return:
3380 case CommandKind::Host:
3381 case CommandKind::Notify:
3382 case CommandKind::SwitchBackend:
3383 case CommandKind::SetLocale:
3384 case CommandKind::SignOut:
3387 checkExpr(*cmd.value, context);
3393 static void checkDialog(Command
const& cmd, CheckContext
const& context) {
3394 NameEntry
const& entry = require(context, cmd.target, cmd.path, {NameCategory::Element},
"a dialog");
3395 if (entry.element->kind !=
"dialog" && entry.element->kind !=
"drawer") {
3396 refuse(cmd.path,
"`" + cmd.target +
"` is a " + entry.element->kind +
", not a dialog or drawer");
3400 void checkSet(Command
const& cmd, CheckContext
const& context) {
3401 auto const segments =
splitPath(cmd.target);
3402 std::string
const& head = segments->front();
3403 Type
const value = checkExpr(*cmd.value, context);
3404 if (head ==
"app") {
3408 require(context, head, cmd.path, {NameCategory::State, NameCategory::Element},
"state or an input");
3409 if (entry.category == NameCategory::Element) {
3410 KindSpec
const*
const spec =
findKind(entry.element->kind,
false);
3411 if ((spec->flags & kWritable) == 0) {
3412 refuse(cmd.path,
"`set` writes state or an input's draft; `" + head +
"` is a " + entry.element->kind);
3417 if (type && segments->size() == 1 && !
compatible(*type, value)) {
3418 refuse(cmd.value->path,
"`" + head +
"` is " + type->name() +
"; the value is " + value.name());
3423 void reads(Expr
const& expr, ScopeInfo* scope, std::set<NameEntry const*>& out, std::vector<std::string>& locals,
3424 std::set<NameEntry const*>& seenLets) {
3425 if (expr.kind == ExprKind::Ref && std::ranges::find(locals, expr.name) == locals.end()) {
3426 NameEntry
const*
const entry =
lookup(scope, expr.name);
3427 if (entry !=
nullptr &&
3428 (entry->category == NameCategory::State || entry->category == NameCategory::Element)) {
3430 }
else if (entry !=
nullptr && entry->category == NameCategory::Let && seenLets.insert(entry).second) {
3431 std::vector<std::string> inner;
3432 reads(*entry->derivedFrom, entry->scope, out, inner, seenLets);
3435 bool const binds = expr.kind == ExprKind::Map || expr.kind == ExprKind::Filter;
3436 for (std::size_t index = 0; index < expr.args.size(); ++index) {
3437 if (binds && index == 1) {
3438 locals.push_back(expr.name);
3440 reads(*expr.args.at(index), scope, out, locals, seenLets);
3441 if (binds && index == 1) {
3445 for (
auto const& [name, field] : expr.fields) {
3446 static_cast<void>(name);
3447 reads(*field, scope, out, locals, seenLets);
3452 static void writes(Commands
const& commands, ScopeInfo* scope, std::set<NameEntry const*>& out) {
3453 for (Command
const& cmd : commands) {
3454 if (cmd.kind == CommandKind::Set || cmd.kind == CommandKind::Reset) {
3455 auto const segments =
splitPath(cmd.target);
3456 NameEntry
const*
const entry =
lookup(scope, segments->front());
3457 if (entry !=
nullptr) {
3461 writes(cmd.commands, scope, out);
3462 writes(cmd.alternative, scope, out);
3466 void checkWatchGraph() {
3467 std::size_t
const count = _watches.size();
3468 std::vector<std::set<NameEntry const*>> observed(count);
3469 std::vector<std::set<NameEntry const*>> written(count);
3470 for (std::size_t index = 0; index <
count; ++index) {
3471 auto const& [watch, scope] = _watches.at(index);
3472 std::vector<std::string> locals;
3473 std::set<NameEntry const*> seenLets;
3474 reads(*watch->on, scope, observed.at(index), locals, seenLets);
3475 writes(watch->body, scope, written.at(index));
3478 std::vector<int> state(count, 0);
3479 std::vector<std::size_t> trail;
3480 std::function<void(std::size_t)> visit = [&](std::size_t from) {
3482 trail.push_back(from);
3483 for (std::size_t to = 0; to <
count; ++to) {
3485 for (NameEntry
const* entry : written.at(from)) {
3486 edge = edge || observed.at(to).contains(entry);
3491 if (state.at(to) == 1) {
3493 auto const start = std::ranges::find(trail, to);
3494 for (
auto step = start; step != trail.end(); ++step) {
3495 cycle += _watches.at(*step).first->path +
" → ";
3497 refuse(_watches.at(to).first->path,
3498 "the watch graph has a cycle: " + cycle + _watches.at(to).first->path);
3500 if (state.at(to) == 0) {
3507 for (std::size_t index = 0; index <
count; ++index) {
3508 if (state.at(index) == 0) {
3519 [[nodiscard]]
static std::string describeEffects(std::vector<ActionEffect>
const& effects) {
3521 for (ActionEffect
const& effect : effects) {
3522 out += (out.empty() ?
"`" :
", `") + effect.kind + (effect.record ?
" " + *effect.record :
"") +
"`";
3528 [[nodiscard]]
static bool changesData(ActionInfo
const& info) {
3529 return info.effects &&
3530 std::ranges::any_of(*info.effects, [](ActionEffect
const& effect) { return effect.kind !=
"none"; });
3533 void warn(LoadWarningKind kind, std::string path, std::string text) {
3534 _warnings.push_back(LoadWarning{.kind = kind, .path = std::move(path), .text = std::move(text)});
3537 void checkEffects() {
3538 if (!_options->catalog.action) {
3541 for (
auto const& entry : _mutations) {
3542 checkMutationEffects(*entry.first);
3544 for (
auto const& [watch, scope] : _watches) {
3545 checkWatchFeedback(*watch, scope);
3549 void checkMutationEffects(MutationDecl
const& mutation) {
3550 auto const action = _options->catalog.action(mutation.action);
3554 if (!action->effects && !refetches(mutation.onSuccess)) {
3555 warn(LoadWarningKind::Uncovered, mutation.path,
3556 std::format(
"nothing refreshes after `{}` succeeds: `{}` declares no effects, and no `refetch` "
3557 "follows it; declare what it changes, or `effects::none` when it changes nothing",
3558 mutation.name, mutation.action));
3563 [[nodiscard]]
static bool refetches(Commands
const& commands) {
3564 return std::ranges::any_of(commands, [](Command
const& cmd) {
3565 return cmd.kind == CommandKind::Refetch || refetches(cmd.commands) || refetches(cmd.alternative);
3571 void checkWatchFeedback(WatchDecl
const& watch, ScopeInfo* scope) {
3572 if (watch.source == WatchSource::User) {
3575 std::vector<std::string> locals;
3576 std::set<NameEntry const*> seenLets;
3577 auto const query = readQuery(*watch.on, scope, locals, seenLets);
3581 if (
auto const mutation = runsWriter(watch.body, scope)) {
3582 warn(LoadWarningKind::WatchFeedback, watch.path,
3583 std::format(
"the watch reads the query `{}` and runs `{}`, which changes data: another client's "
3584 "change refetches the query and runs it again; give the watch \"source\": \"user\"",
3585 *query, *mutation));
3590 [[nodiscard]]
static std::optional<std::string> readQuery(Expr
const& expr, ScopeInfo* scope,
3591 std::vector<std::string>& locals,
3592 std::set<NameEntry const*>& seenLets) {
3593 if (expr.kind == ExprKind::Query) {
3596 if (expr.kind == ExprKind::Ref && std::ranges::find(locals, expr.name) == locals.end()) {
3597 NameEntry
const*
const entry =
lookup(scope, expr.name);
3598 if (entry !=
nullptr && entry->category == NameCategory::Query) {
3601 if (entry !=
nullptr && entry->category == NameCategory::Let && seenLets.insert(entry).second) {
3602 std::vector<std::string> inner;
3603 if (
auto found = readQuery(*entry->derivedFrom, entry->scope, inner, seenLets)) {
3608 bool const binds = expr.kind == ExprKind::Map || expr.kind == ExprKind::Filter;
3609 for (std::size_t index = 0; index < expr.args.size(); ++index) {
3610 if (binds && index == 1) {
3611 locals.push_back(expr.name);
3613 auto found = readQuery(*expr.args.at(index), scope, locals, seenLets);
3614 if (binds && index == 1) {
3621 for (
auto const& [name, field] : expr.fields) {
3622 static_cast<void>(name);
3623 if (
auto found = readQuery(*field, scope, locals, seenLets)) {
3627 return std::nullopt;
3631 [[nodiscard]] std::optional<std::string> runsWriter(Commands
const& commands, ScopeInfo* scope)
const {
3632 for (Command
const& cmd : commands) {
3634 cmd.kind == CommandKind::Run || cmd.kind == CommandKind::Upload || cmd.kind == CommandKind::SignIn;
3635 NameEntry
const*
const entry = runs ?
lookup(scope, cmd.target) : nullptr;
3636 if (entry !=
nullptr && entry->mutation !=
nullptr) {
3637 auto const action = _options->catalog.action(entry->mutation->action);
3638 if (action && changesData(*action)) {
3642 if (
auto found = runsWriter(cmd.commands, scope)) {
3645 if (
auto found = runsWriter(cmd.alternative, scope)) {
3649 return std::nullopt;
3652 Document
const* _doc;
3653 LoadOptions
const* _options;
3654 std::deque<ScopeInfo> _scopes;
3655 std::map<DocNode const*, ScopeInfo*> _scopeOf;
3656 std::map<DocNode const*, ScopeInfo*> _innerOf;
3657 std::map<std::string, QueryDecl const*, std::less<>> _queries;
3658 std::map<std::string, DocNode const*, std::less<>> _elements;
3659 std::vector<std::pair<WatchDecl const*, ScopeInfo*>> _watches;
3660 std::vector<std::pair<MutationDecl const*, ScopeInfo*>> _mutations;
3661 std::vector<std::pair<QueryDecl const*, ScopeInfo*>> _queryScopes;
3662 std::vector<LoadWarning> _warnings;
3663 std::map<DocNode const*, TypeKind> _dateValueKinds;
3664 std::vector<std::string> _deriving;