@acid-cat/catalog 0.9.0 → 0.10.1

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/README.md CHANGED
@@ -9,6 +9,8 @@ pnpm add -D @acid-cat/catalog
9
9
  ```
10
10
 
11
11
  ```ts
12
+ import { writeFile } from "node:fs/promises";
13
+
12
14
  import { Catalog } from "@acid-cat/catalog";
13
15
 
14
16
  const catalog = new Catalog({ title: "Neighbourhood lending library" });
@@ -20,10 +22,17 @@ catalog.capability("library.loan.return", {
20
22
  input: { type: "object", required: ["loan"], properties: { loan: { type: "string" } } },
21
23
  failures: { NotFound: notFound },
22
24
  });
25
+ const book = catalog.type("Book", { type: "object", properties: { isbn: { type: "string" } } });
26
+ catalog.event("library.book.returned", {
27
+ description: "A member returned a book. The occurrence gives the book.",
28
+ audience: ["desk"],
29
+ input: { type: "object", properties: {} },
30
+ payload: book,
31
+ });
23
32
 
24
33
  await writeFile("acidcat.catalog.json", catalog.render());
25
34
  ```
26
35
 
27
- `validate()` lists the rules a catalog breaks, and `render()` refuses to render one that breaks any. The full example is [`examples/library.ts`](examples/library.ts), a lending library.
36
+ `event()` declares an [event](../../../spec/events.md): clients subscribe to it, and get its payload in each occurrence. A payload is an object, or a reference to a type that is an object. `validate()` lists the rules a catalog breaks, and `render()` refuses to render one that breaks any. The full example is [`examples/library.ts`](examples/library.ts), a lending library.
28
37
 
29
38
  The tests compare the output of the library with files in this directory: the lending-library catalog [`examples/library.acidcat.json`](examples/library.acidcat.json), and a catalog of values that languages write differently, [`test/fixtures/edge.acidcat.json`](test/fixtures/edge.acidcat.json). A test also compares [`examples/aircon.ts`](examples/aircon.ts) with the air-conditioner catalog of the repository. Each committed catalog file is in [canonical form](../../../spec/catalog.md#canonical-form), and the root `moon run check` checks this. Thus all catalog libraries agree on the canonical form, and they do not share a test file. Test with `moon run catalog-typescript:test`.
@@ -4,7 +4,5 @@
4
4
  */
5
5
  /** A catalog in canonical form, as text. */
6
6
  export declare function canonicalText(catalog: unknown): string;
7
- /** Put a catalog's members in canonical order. */
8
- export declare function canonicalCatalog(catalog: unknown): unknown;
9
7
  /** Order strings by Unicode code point, as Python, Go and Rust compare them. */
10
8
  export declare function byCodePoint(left: string, right: string): number;
package/dist/canonical.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/canonical.ts
1
+ // src/canonical.ts
2
2
  const isObject = (value) => value !== null && typeof value === "object";
3
3
  const CATALOG_ORDER = [
4
4
  "acidcat",
@@ -6,7 +6,8 @@ const CATALOG_ORDER = [
6
6
  "description",
7
7
  "targets",
8
8
  "$defs",
9
- "capabilities"
9
+ "capabilities",
10
+ "events"
10
11
  ];
11
12
  const TARGET_ORDER = [
12
13
  "bindings",
@@ -22,30 +23,40 @@ const CAPABILITY_ORDER = [
22
23
  "readOnly",
23
24
  "state"
24
25
  ];
26
+ const EVENT_ORDER = [
27
+ "description",
28
+ "audience",
29
+ "input",
30
+ "payload"
31
+ ];
25
32
  /** A catalog in canonical form, as text. */
26
33
  function canonicalText(catalog) {
27
34
  return `${write(canonicalCatalog(catalog), "")}\n`;
28
35
  }
29
- /** Put a catalog's members in canonical order. */
36
+ /**
37
+ * Put a catalog's members in canonical order. Each object becomes a `Map`, not a plain object, because a plain object
38
+ * puts integer-like keys such as "9" and "10" first, in numeric order, whatever order they were added in.
39
+ */
30
40
  function canonicalCatalog(catalog) {
31
41
  return ordered(catalog, CATALOG_ORDER, (key, value) => {
32
42
  if (key === "targets") return byKey(value, (target) => ordered(target, TARGET_ORDER, sortedMember));
33
43
  if (key === "$defs") return byKey(value, sorted);
34
44
  if (key === "capabilities") return byKey(value, (capability) => ordered(capability, CAPABILITY_ORDER, (member, inner) => member === "failures" ? byKey(inner, sorted) : sortedMember(member, inner)));
45
+ if (key === "events") return byKey(value, (event) => ordered(event, EVENT_ORDER, sortedMember));
35
46
  return sortedMember(key, value);
36
47
  });
37
48
  }
38
49
  /** The known members in their order, then every other member sorted by key. A value that is not an object is sorted. */
39
50
  function ordered(value, order, each) {
40
- if (!isObject(value)) return sorted(value);
41
- const known = order.filter((key) => key in value);
51
+ if (!isObject(value) || Array.isArray(value)) return sorted(value);
52
+ const known = order.filter((key) => Object.hasOwn(value, key));
42
53
  const others = Object.keys(value).filter((key) => !order.includes(key)).sort(byCodePoint);
43
- return Object.fromEntries([...known, ...others].map((key) => [key, each(key, value[key])]));
54
+ return new Map([...known, ...others].map((key) => [key, each(key, value[key])]));
44
55
  }
45
56
  /** A map keyed by name or id: its keys sorted, each value shaped by `each`. A value that is not an object is sorted. */
46
57
  function byKey(value, each) {
47
- if (!isObject(value)) return sorted(value);
48
- return Object.fromEntries(Object.keys(value).sort(byCodePoint).map((key) => [key, each(value[key])]));
58
+ if (!isObject(value) || Array.isArray(value)) return sorted(value);
59
+ return new Map(Object.keys(value).sort(byCodePoint).map((key) => [key, each(value[key])]));
49
60
  }
50
61
  function sortedMember(_key, value) {
51
62
  return sorted(value);
@@ -66,21 +77,21 @@ function byCodePoint(left, right) {
66
77
  }
67
78
  return a.length - b.length;
68
79
  }
69
- /** Write a JSON value with two-space indentation, one member or item per line. */
80
+ /** Write a JSON value with two-space indentation, one member or item per line. A `Map` keeps its order. */
70
81
  function write(value, indent) {
71
82
  if (Array.isArray(value)) {
72
83
  if (value.length === 0) return "[]";
73
84
  const inner = `${indent} `;
74
85
  return `[\n${value.map((item) => inner + write(item, inner)).join(",\n")}\n${indent}]`;
75
86
  }
76
- if (isObject(value)) {
77
- const keys = Object.keys(value);
78
- if (keys.length === 0) return "{}";
87
+ if (value instanceof Map) {
88
+ const entries = [...value];
89
+ if (entries.length === 0) return "{}";
79
90
  const inner = `${indent} `;
80
- return `{\n${keys.map((key) => `${inner}${JSON.stringify(key)}: ${write(value[key], inner)}`).join(",\n")}\n${indent}}`;
91
+ return `{\n${entries.map(([key, member]) => `${inner}${JSON.stringify(key)}: ${write(member, inner)}`).join(",\n")}\n${indent}}`;
81
92
  }
93
+ if (isObject(value)) return write(sorted(value), indent);
82
94
  if (typeof value === "number" && !Number.isFinite(value)) throw new TypeError("A catalog holds only finite numbers");
83
95
  return JSON.stringify(value);
84
96
  }
85
- //#endregion
86
- export { byCodePoint, canonicalCatalog, canonicalText };
97
+ export { byCodePoint, canonicalText };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,16 @@ export interface CapabilityOptions {
32
32
  /** The schema of a value that clients can observe. */
33
33
  readonly state?: Schema;
34
34
  }
35
+ export interface EventOptions {
36
+ /** What happens, and when the provider sends an occurrence, written for people and agents. */
37
+ readonly description: string;
38
+ /** The targets the event is offered to. Empty means no target. */
39
+ readonly audience?: readonly string[];
40
+ /** The input schema of a subscription, for example a filter. It must describe an object. Absent means an empty object. */
41
+ readonly input?: Schema;
42
+ /** The schema of the data of each occurrence. It must describe an object. Absent means an empty object. */
43
+ readonly payload?: Schema;
44
+ }
35
45
  export declare class Catalog {
36
46
  #private;
37
47
  constructor(options?: {
@@ -44,6 +54,8 @@ export declare class Catalog {
44
54
  type(name: string, schema: Schema): Schema;
45
55
  /** Declare a capability. */
46
56
  capability(id: string, options: CapabilityOptions): this;
57
+ /** Declare an event (spec/events.md). */
58
+ event(id: string, options: EventOptions): this;
47
59
  /** The catalog rules this catalog breaks. Empty when it conforms. */
48
60
  validate(): string[];
49
61
  /** The catalog as a JSON value. Throws when the catalog breaks a rule. */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { canonicalText } from "./canonical.js";
2
- //#region src/index.ts
2
+ // src/index.ts
3
3
  /**
4
4
  * Write an AcidCat catalog in TypeScript, and render it to the JSON document the toolchain checks.
5
5
  *
@@ -16,6 +16,7 @@ var Catalog = class {
16
16
  #targets = /* @__PURE__ */ new Map();
17
17
  #types = /* @__PURE__ */ new Map();
18
18
  #capabilities = /* @__PURE__ */ new Map();
19
+ #events = /* @__PURE__ */ new Map();
19
20
  constructor(options = {}) {
20
21
  this.#title = options.title;
21
22
  this.#description = options.description;
@@ -38,6 +39,12 @@ var Catalog = class {
38
39
  this.#capabilities.set(id, options);
39
40
  return this;
40
41
  }
42
+ /** Declare an event (spec/events.md). */
43
+ event(id, options) {
44
+ if (this.#events.has(id)) throw new Error(`The event ${id} is declared twice`);
45
+ this.#events.set(id, options);
46
+ return this;
47
+ }
41
48
  /** The catalog rules this catalog breaks. Empty when it conforms. */
42
49
  validate() {
43
50
  const problems = [];
@@ -52,12 +59,7 @@ var Catalog = class {
52
59
  problems.push(...this.#referenceProblems(schema).map((problem) => `the type ${name} ${problem}`));
53
60
  }
54
61
  for (const [id, capability] of this.#capabilities) {
55
- if (id.length > 128 || !ID.test(id)) problems.push(`the id ${id} is malformed`);
56
- if (capability.description.trim() === "") problems.push(`${id} has no description`);
57
- const audience = capability.audience ?? [];
58
- if (new Set(audience).size !== audience.length) problems.push(`${id} repeats a target in its audience`);
59
- for (const target of audience) if (!this.#targets.has(target)) problems.push(`${id} names the undeclared target ${target}`);
60
- if (capability.input !== void 0 && this.#resolve(capability.input).type !== "object") problems.push(`${id} input does not describe an object`);
62
+ problems.push(...this.#entryProblems(id, capability));
61
63
  const schemas = [
62
64
  capability.input,
63
65
  capability.output,
@@ -66,6 +68,23 @@ var Catalog = class {
66
68
  ];
67
69
  for (const schema of schemas) if (schema !== void 0) problems.push(...this.#referenceProblems(schema).map((problem) => `${id} ${problem}`));
68
70
  }
71
+ for (const [id, event] of this.#events) {
72
+ if (this.#capabilities.has(id)) problems.push(`${id} is both a capability and an event`);
73
+ problems.push(...this.#entryProblems(id, event));
74
+ if (event.payload !== void 0 && this.#resolve(event.payload).type !== "object") problems.push(`${id} payload does not describe an object`);
75
+ for (const schema of [event.input, event.payload]) if (schema !== void 0) problems.push(...this.#referenceProblems(schema).map((problem) => `${id} ${problem}`));
76
+ }
77
+ return problems;
78
+ }
79
+ /** The problems of the id, the description, the audience and the input of a capability or an event. */
80
+ #entryProblems(id, entry) {
81
+ const problems = [];
82
+ if (id.length > 128 || !ID.test(id)) problems.push(`the id ${id} is malformed`);
83
+ if (entry.description.trim() === "") problems.push(`${id} has no description`);
84
+ const audience = entry.audience ?? [];
85
+ if (new Set(audience).size !== audience.length) problems.push(`${id} repeats a target in its audience`);
86
+ for (const target of audience) if (!this.#targets.has(target)) problems.push(`${id} names the undeclared target ${target}`);
87
+ if (entry.input !== void 0 && this.#resolve(entry.input).type !== "object") problems.push(`${id} input does not describe an object`);
69
88
  return problems;
70
89
  }
71
90
  /** The catalog as a JSON value. Throws when the catalog breaks a rule. */
@@ -90,6 +109,12 @@ var Catalog = class {
90
109
  failures: Object.keys(capability.failures ?? {}).length === 0 ? void 0 : capability.failures,
91
110
  readOnly: capability.readOnly,
92
111
  state: capability.state
112
+ })])),
113
+ events: this.#events.size === 0 ? void 0 : Object.fromEntries([...this.#events].map(([id, event]) => [id, compact({
114
+ description: event.description,
115
+ audience: event.audience ?? [],
116
+ input: event.input,
117
+ payload: event.payload
93
118
  })]))
94
119
  });
95
120
  }
@@ -104,7 +129,7 @@ var Catalog = class {
104
129
  #referenceProblems(schema) {
105
130
  const problems = [];
106
131
  visit(schema, (node) => {
107
- if ("$defs" in node) problems.push("carries its own $defs; declare shared types with type()");
132
+ if (Object.hasOwn(node, "$defs")) problems.push("carries its own $defs; declare shared types with type()");
108
133
  if (node.$ref === void 0) return;
109
134
  const name = typeof node.$ref === "string" ? REFERENCE.exec(node.$ref)?.[1] : void 0;
110
135
  if (name === void 0 || !this.#types.has(name)) problems.push(`references ${typeof node.$ref === "string" ? node.$ref : JSON.stringify(node.$ref)}, which is not a type`);
@@ -124,5 +149,4 @@ function visit(value, onObject) {
124
149
  Object.values(value).forEach((item) => visit(item, onObject));
125
150
  }
126
151
  }
127
- //#endregion
128
152
  export { Catalog, canonicalText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acid-cat/catalog",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Write an AcidCat catalog in TypeScript and render it in canonical form.",
5
5
  "keywords": [
6
6
  "acidcat",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "26.6.5",
34
+ "@types/node": "26.6.6",
35
35
  "typescript": "7.0.2"
36
36
  }
37
37
  }