@mark1russell7/coeval 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ladder.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { type TypeRef, typeName } from "./doc.ts";
2
+ import { type Kind, type World, lookupKind } from "./world.ts";
3
+
4
+ export type { Kind, Namespace, PartsSpec, Side, World, WorldEntry } from "./world.ts";
5
+
6
+ /** The rungs of the ladder, in the order that a question visits them. */
7
+ export const RUNGS = ["own", "kind", "default"] as const;
8
+
9
+ /** One rung of the ladder. */
10
+ export type Rung = (typeof RUNGS)[number];
11
+
12
+ /** The place where the ladder found the type. */
13
+ export type KindSource = "document" | "container";
14
+
15
+ /**
16
+ * The outcome of one rung.
17
+ *
18
+ * - `answers`: the rung gives an evaluator.
19
+ * - `no-entry`: the rung has nothing for this verb, thus the question goes to the next rung.
20
+ * - `skipped`: the rung does not apply, because no type is known.
21
+ * - `unknown-kind`: the type is not a kind of the world. The question stops.
22
+ */
23
+ export type Outcome = "answers" | "no-entry" | "skipped" | "unknown-kind";
24
+
25
+ /** The record of one rung that a question visited. */
26
+ export interface Step {
27
+ readonly rung: Rung;
28
+ readonly outcome: Outcome;
29
+ /** The evaluator, when the outcome is `answers`. */
30
+ readonly evaluator?: string;
31
+ /** The text of the type, at the kind rung. */
32
+ readonly kind?: string;
33
+ /** The place of the type, at the kind rung. */
34
+ readonly source?: KindSource;
35
+ }
36
+
37
+ /** The answer to a question. It keeps the record of each rung that the question visited. */
38
+ export interface Answer {
39
+ readonly verb: string;
40
+ /** The evaluator that answers. A question that stops with an error has no evaluator. */
41
+ readonly evaluator?: string;
42
+ /** The rung that answered. */
43
+ readonly rung?: Rung;
44
+ /** The reason when no rung answers. */
45
+ readonly error?: "unknown-kind" | "no-answer";
46
+ readonly steps: readonly Step[];
47
+ }
48
+
49
+ /**
50
+ * The header that the ladder reads: the type and the own entries. An envelope is a header. A bare value has an
51
+ * empty header, and its position can declare its type.
52
+ */
53
+ export interface Header {
54
+ readonly type?: TypeRef;
55
+ readonly own?: Readonly<Record<string, string>>;
56
+ }
57
+
58
+ /** The options of a question. */
59
+ export interface AskOptions {
60
+ /** The type that the position of a bare value declares. The type of an envelope wins. */
61
+ readonly declaredType?: string;
62
+ }
63
+
64
+ function entryOf(verbs: Readonly<Record<string, string>> | undefined, verb: string): string | undefined {
65
+ return verbs !== undefined && Object.hasOwn(verbs, verb) ? verbs[verb] : undefined;
66
+ }
67
+
68
+ /**
69
+ * This function finds the kind that a type names, and follows each alias. It gives `undefined` for an unknown
70
+ * type.
71
+ *
72
+ * @param world - The world.
73
+ * @param type - The reference to the type.
74
+ * @returns The kind, or `undefined`.
75
+ */
76
+ export function kindOf(world: World, type: TypeRef): Kind | undefined {
77
+ const found = lookupKind(world, type);
78
+ return found.ok ? found.kind : undefined;
79
+ }
80
+
81
+ /**
82
+ * This function finds a kind by a text type. It is the same as `kindOf` for a one-key reference.
83
+ *
84
+ * @param world - The world.
85
+ * @param name - The text of the type.
86
+ * @returns The kind, or `undefined`.
87
+ */
88
+ export function kindNamed(world: World, name: TypeRef): Kind | undefined {
89
+ return kindOf(world, name);
90
+ }
91
+
92
+ /**
93
+ * This function asks a document a question, and gives the evaluator that answers it.
94
+ *
95
+ * The question visits the rungs in order: the own entry of the envelope, then its kind, then the default of the
96
+ * world. The type comes from the envelope, or else from the type that the position declares. The type is a
97
+ * reference into the world. The ladder does not guess a type from the shape of a value.
98
+ *
99
+ * @param world - The kinds and the defaults.
100
+ * @param doc - The header of the document: an envelope, or `{}` for a bare value.
101
+ * @param verb - The verb of the question, for example `eval` or `view`.
102
+ * @param options - The type that the position declares.
103
+ * @returns The answer, with the record of each rung that the question visited.
104
+ */
105
+ export function ask(world: World, doc: Header, verb: string, options: AskOptions = {}): Answer {
106
+ const steps: Step[] = [];
107
+
108
+ const own = entryOf(doc.own, verb);
109
+ if (own !== undefined) {
110
+ steps.push({ rung: "own", outcome: "answers", evaluator: own });
111
+ return { verb, evaluator: own, rung: "own", steps };
112
+ }
113
+ steps.push({ rung: "own", outcome: "no-entry" });
114
+
115
+ const type: TypeRef | undefined = doc.type ?? options.declaredType;
116
+ if (type === undefined) {
117
+ steps.push({ rung: "kind", outcome: "skipped" });
118
+ } else {
119
+ const source: KindSource = doc.type === undefined ? "container" : "document";
120
+ const name = typeName(type);
121
+ const kind = kindOf(world, type);
122
+ if (kind === undefined) {
123
+ steps.push({ rung: "kind", outcome: "unknown-kind", kind: name, source });
124
+ return { verb, error: "unknown-kind", steps };
125
+ }
126
+ const entry = entryOf(kind.verbs, verb);
127
+ if (entry !== undefined) {
128
+ steps.push({ rung: "kind", outcome: "answers", evaluator: entry, kind: name, source });
129
+ return { verb, evaluator: entry, rung: "kind", steps };
130
+ }
131
+ steps.push({ rung: "kind", outcome: "no-entry", kind: name, source });
132
+ }
133
+
134
+ const fallback = entryOf(world.defaults, verb);
135
+ if (fallback !== undefined) {
136
+ steps.push({ rung: "default", outcome: "answers", evaluator: fallback });
137
+ return { verb, evaluator: fallback, rung: "default", steps };
138
+ }
139
+ steps.push({ rung: "default", outcome: "no-entry" });
140
+ return { verb, error: "no-answer", steps };
141
+ }
package/src/order.ts ADDED
@@ -0,0 +1,111 @@
1
+ /** One step of a sequence: its name, the names of the earlier steps that it reads, and if it has an effect. */
2
+ export interface StepSpec {
3
+ readonly id: string;
4
+ readonly reads: readonly string[];
5
+ readonly effect: boolean;
6
+ }
7
+
8
+ /** An edge from a step that must come first to a step that must come after it. */
9
+ export type Edge = readonly [before: string, after: string];
10
+
11
+ /** A reference that a sequence cannot resolve. */
12
+ export interface OrderProblem {
13
+ readonly step: string;
14
+ readonly reads: string;
15
+ /** `unknown-name`: no step has this name. `read-before-defined`: the step with this name comes later. */
16
+ readonly problem: "unknown-name" | "read-before-defined";
17
+ }
18
+
19
+ /**
20
+ * This function finds the references that a sequence cannot resolve. A step can read only the steps before it.
21
+ *
22
+ * @param steps - The steps, in the written order.
23
+ * @returns One problem for each bad reference, in the written order.
24
+ */
25
+ export function checkSteps(steps: readonly StepSpec[]): readonly OrderProblem[] {
26
+ const problems: OrderProblem[] = [];
27
+ steps.forEach((step, index) => {
28
+ for (const name of step.reads) {
29
+ const at = steps.findIndex((other) => other.id === name);
30
+ if (at === -1) problems.push({ step: step.id, reads: name, problem: "unknown-name" });
31
+ else if (at >= index) problems.push({ step: step.id, reads: name, problem: "read-before-defined" });
32
+ }
33
+ });
34
+ return problems;
35
+ }
36
+
37
+ /**
38
+ * This function gives the data edges: a step comes after each earlier step that it reads.
39
+ *
40
+ * @param steps - The steps, in the written order.
41
+ * @returns The edges. A bad reference gives no edge.
42
+ */
43
+ export function dataEdges(steps: readonly StepSpec[]): readonly Edge[] {
44
+ const edges: Edge[] = [];
45
+ steps.forEach((step, index) => {
46
+ for (const name of step.reads) {
47
+ const at = steps.findIndex((other) => other.id === name);
48
+ if (at !== -1 && at < index) edges.push([name, step.id]);
49
+ }
50
+ });
51
+ return edges;
52
+ }
53
+
54
+ /**
55
+ * This function gives the effect edges: each step with an effect comes after the previous step with an effect.
56
+ * Effects keep their written order. Steps without an effect get no effect edge.
57
+ *
58
+ * @param steps - The steps, in the written order.
59
+ * @returns The edges, one for each pair of consecutive effect steps.
60
+ */
61
+ export function effectEdges(steps: readonly StepSpec[]): readonly Edge[] {
62
+ const edges: Edge[] = [];
63
+ let previous: string | undefined;
64
+ for (const step of steps) {
65
+ if (!step.effect) continue;
66
+ if (previous !== undefined) edges.push([previous, step.id]);
67
+ previous = step.id;
68
+ }
69
+ return edges;
70
+ }
71
+
72
+ /**
73
+ * This function tells if an order of the steps gives the same meaning as the written order.
74
+ * The order must contain each step one time, and it must obey the data edges and the effect edges.
75
+ *
76
+ * @param steps - The steps, in the written order.
77
+ * @param order - The names of the steps in the order to examine.
78
+ * @returns `true` when the order is permitted.
79
+ */
80
+ export function isAllowedOrder(steps: readonly StepSpec[], order: readonly string[]): boolean {
81
+ if (order.length !== steps.length) return false;
82
+ if (new Set(order).size !== order.length) return false;
83
+ if (!steps.every((step) => order.includes(step.id))) return false;
84
+ const position = new Map(order.map((id, index) => [id, index] as const));
85
+ return [...dataEdges(steps), ...effectEdges(steps)].every(
86
+ ([before, after]) => (position.get(before) ?? 0) < (position.get(after) ?? 0),
87
+ );
88
+ }
89
+
90
+ /**
91
+ * This function puts the steps in layers. Each step goes one layer after the last step that it must follow.
92
+ * The steps of one layer do not depend on each other, thus they can run at the same time.
93
+ *
94
+ * @param steps - The steps, in the written order.
95
+ * @returns The layers, first to last. Each layer keeps the written order.
96
+ */
97
+ export function layers(steps: readonly StepSpec[]): readonly (readonly string[])[] {
98
+ const edges = [...dataEdges(steps), ...effectEdges(steps)];
99
+ const depth = new Map<string, number>();
100
+ for (const step of steps) {
101
+ const before = edges.filter(([, after]) => after === step.id).map(([first]) => depth.get(first) ?? 0);
102
+ depth.set(step.id, before.length === 0 ? 0 : Math.max(...before) + 1);
103
+ }
104
+ const result: string[][] = [];
105
+ for (const step of steps) {
106
+ const at = depth.get(step.id) ?? 0;
107
+ while (result.length <= at) result.push([]);
108
+ result[at]?.push(step.id);
109
+ }
110
+ return result;
111
+ }
package/src/parts.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { type Json, type Key, isEnvelope, isRecord } from "./doc.ts";
2
+ import type { Shape } from "./world.ts";
3
+
4
+ /** One part of a value: its place in the value, the JSON of the part, and the type that its position declares. */
5
+ export interface PartSlot {
6
+ /** The keys from the value of the document to the part. */
7
+ readonly path: readonly Key[];
8
+ /** The path as one text, with a dot between the keys, for example `bind.x`. A part of a list has its index. */
9
+ readonly key: string;
10
+ readonly item: Json;
11
+ readonly of?: string;
12
+ }
13
+
14
+ type Template =
15
+ | { readonly t: "data"; readonly value: unknown }
16
+ | { readonly t: "slot"; readonly index: number }
17
+ | { readonly t: "list"; readonly items: readonly Template[] }
18
+ | { readonly t: "record"; readonly fields: readonly (readonly [string, Template])[] };
19
+
20
+ /** The value of a document, split into its parts, by the shape that its kind declares. */
21
+ export type Split =
22
+ | {
23
+ readonly ok: true;
24
+ /** The parts, in the order of the value. */
25
+ readonly parts: readonly PartSlot[];
26
+ /** The value without the parts: a field that holds a part is not in it, and a list of parts gives `null`. */
27
+ readonly data: Json;
28
+ /**
29
+ * This function gives the value with a handle in the place of each part.
30
+ *
31
+ * @param handles - One handle for each part, in the order of `parts`.
32
+ * @returns The value with the handles.
33
+ */
34
+ fill<P>(handles: readonly P[]): unknown;
35
+ }
36
+ | { readonly ok: false; readonly reason: string; readonly at: readonly Key[] };
37
+
38
+ class Bad {
39
+ constructor(
40
+ readonly reason: string,
41
+ readonly at: readonly Key[],
42
+ ) {}
43
+ }
44
+
45
+ function setOwn(target: Record<string, unknown>, key: string, value: unknown): void {
46
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
47
+ }
48
+
49
+ function hasSlots(template: Template): boolean {
50
+ return template.t !== "data";
51
+ }
52
+
53
+ /**
54
+ * This function splits a value into its parts, by the shape that its kind declares. The evaluator, the view and
55
+ * the static kit use the same split, thus each one sees the same parts.
56
+ *
57
+ * @param shape - The shape of the value. The default is data.
58
+ * @param value - The value of the document.
59
+ * @returns The parts and a way to fill their places, or the reason why the value does not have the shape.
60
+ */
61
+ export function splitParts(shape: Shape | undefined, value: Json): Split {
62
+ const parts: PartSlot[] = [];
63
+ const slot = (item: Json, path: readonly Key[], of: string | undefined): Template => {
64
+ parts.push({ path, key: path.join("."), item, ...(of === undefined ? {} : { of }) });
65
+ return { t: "slot", index: parts.length - 1 };
66
+ };
67
+
68
+ const deep = (item: Json, path: readonly Key[], of: string | undefined): Template => {
69
+ if (isEnvelope(item)) return slot(item, path, of);
70
+ if (Array.isArray(item)) {
71
+ const items = (item as readonly Json[]).map((child, index) => deep(child, [...path, index], of));
72
+ return items.some(hasSlots) ? { t: "list", items } : { t: "data", value: item };
73
+ }
74
+ if (isRecord(item)) {
75
+ const fields = Object.keys(item).map((key) => [key, deep(item[key] as Json, [...path, key], of)] as const);
76
+ return fields.some(([, child]) => hasSlots(child)) ? { t: "record", fields } : { t: "data", value: item };
77
+ }
78
+ return { t: "data", value: item };
79
+ };
80
+
81
+ const walk = (spec: Shape | undefined, item: Json, path: readonly Key[]): Template => {
82
+ if (spec === undefined || spec.shape === "none") return { t: "data", value: item };
83
+ if (spec.shape === "term") return slot(item, path, spec.of);
84
+ if (spec.shape === "deep") return deep(item, path, spec.of);
85
+ if (spec.shape === "list") {
86
+ if (!Array.isArray(item)) throw new Bad("The value must be a list.", path);
87
+ const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
88
+ return { t: "list", items: (item as readonly Json[]).map((child, index) => walk(each, child, [...path, index])) };
89
+ }
90
+ if (!isRecord(item)) throw new Bad("The value must be a record.", path);
91
+ if (spec.shape === "map") {
92
+ const each: Shape = spec.item ?? { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) };
93
+ return { t: "record", fields: Object.keys(item).map((key) => [key, walk(each, item[key] as Json, [...path, key])] as const) };
94
+ }
95
+ for (const name of spec.required ?? []) {
96
+ if (!Object.hasOwn(item, name)) throw new Bad(`The field ${name} is missing.`, path);
97
+ }
98
+ const named: Readonly<Record<string, Shape>> = Array.isArray(spec.fields)
99
+ ? Object.fromEntries((spec.fields as readonly string[]).map((name) => [name, { shape: "term", ...(spec.of === undefined ? {} : { of: spec.of }) } as Shape]))
100
+ : (spec.fields as Readonly<Record<string, Shape>>);
101
+ const fields: (readonly [string, Template])[] = [];
102
+ for (const key of Object.keys(item)) {
103
+ const fieldShape = Object.hasOwn(named, key) ? named[key] : spec.rest;
104
+ fields.push([key, walk(fieldShape, item[key] as Json, [...path, key])]);
105
+ }
106
+ return { t: "record", fields };
107
+ };
108
+
109
+ let template: Template;
110
+ try {
111
+ template = walk(shape, value, []);
112
+ } catch (error) {
113
+ if (error instanceof Bad) return { ok: false, reason: error.reason, at: error.at };
114
+ throw error;
115
+ }
116
+
117
+ const strip = (node: Template): Json | undefined => {
118
+ if (node.t === "data") return node.value as Json;
119
+ if (node.t === "slot") return undefined;
120
+ if (node.t === "list") return node.items.every((child) => child.t === "slot") ? null : (node.items.map((child) => strip(child) ?? null) as Json);
121
+ const out: Record<string, Json> = {};
122
+ for (const [key, child] of node.fields) {
123
+ const kept = strip(child);
124
+ if (kept !== undefined) setOwn(out, key, kept);
125
+ }
126
+ return out;
127
+ };
128
+
129
+ const fillWith = <P>(node: Template, handles: readonly P[]): unknown => {
130
+ if (node.t === "data") return node.value;
131
+ if (node.t === "slot") return handles[node.index];
132
+ if (node.t === "list") return node.items.map((child) => fillWith(child, handles));
133
+ const out: Record<string, unknown> = {};
134
+ for (const [key, child] of node.fields) setOwn(out, key, fillWith(child, handles));
135
+ return out;
136
+ };
137
+
138
+ return { ok: true, parts, data: strip(template) ?? null, fill: (handles) => fillWith(template, handles) };
139
+ }
140
+
141
+ /**
142
+ * This function tells if the path of a part matches a pattern of a scope rule. The key `"*"` of a pattern matches
143
+ * each key.
144
+ *
145
+ * @param path - The path of the part.
146
+ * @param pattern - The pattern.
147
+ * @returns `true` when the path matches.
148
+ */
149
+ export function pathMatches(path: readonly Key[], pattern: readonly Key[]): boolean {
150
+ if (path.length !== pattern.length) return false;
151
+ for (let index = 0; index < path.length; index += 1) {
152
+ const want = pattern[index];
153
+ if (want !== "*" && String(want) !== String(path[index])) return false;
154
+ }
155
+ return true;
156
+ }