@nanobpm/urban 0.33.0 → 0.34.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.
@@ -79,8 +79,10 @@ export interface OperationInfo {
79
79
  eject: boolean;
80
80
  summary?: string;
81
81
  }
82
- /** Parse an OpenAPI document from text. JSON only in this slice (ADR 0058 open question: YAML).
83
- * Throws with a clear message on malformed input. */
82
+ /** Parse an OpenAPI document from text. Accepts both YAML (the sanctioned authoring format for
83
+ * the HTTP surface — ADR 0059) and JSON (still accepted, e.g. a generated interchange artifact).
84
+ * JSON is a strict subset of YAML 1.2, so JSON is tried first for a precise error + fast path,
85
+ * then the text is parsed as YAML. Throws with a clear message on malformed input. */
84
86
  export declare function parseSpec(text: string): OpenApiDoc;
85
87
  /** The basename of a local `#/components/schemas/Name` ref, else undefined. */
86
88
  export declare function refName(ref: string): string | undefined;
@@ -2,12 +2,15 @@
2
2
  // deriver (`toolkit/derivers/api.ts`, authoring-time type + wrapper emission) and the runtime
3
3
  // (`runtime/core/modules/api.ts`, request validation + routing) build on this one module, so the
4
4
  // derived types and the runtime routes/validation always agree. No IO, no `node:*`, no `Deno` —
5
- // it operates on an already-parsed document object.
5
+ // it operates on document text/objects. Its one external dependency is `yaml` (a browser-safe,
6
+ // pure-JS parser with a dedicated browser build) so `parseSpec` accepts YAML — the sanctioned
7
+ // authoring format for the HTTP surface (ADR 0059) — as well as JSON.
6
8
  //
7
9
  // Scope: the ADR 0058 "supported profile" — JSON bodies, path/query parameters, a JSON
8
10
  // requestBody, response codes, and a JSON-Schema subset validator (type/required/enum/numeric &
9
11
  // string bounds/pattern/array & object shape/nullable/$ref). Exotic OpenAPI (callbacks, links,
10
12
  // XML, discriminated oneOf composition) is intentionally out of scope for this slice.
13
+ import { parse as parseYaml } from "yaml";
11
14
  /**
12
15
  * Whether a schema describes an object shape. OpenAPI/JSON-Schema let the `type`
13
16
  * keyword be omitted when `properties`/`required`/`additionalProperties` already imply
@@ -33,18 +36,30 @@ function isRecord(v) {
33
36
  function isSchema(v) {
34
37
  return isRecord(v);
35
38
  }
36
- /** Parse an OpenAPI document from text. JSON only in this slice (ADR 0058 open question: YAML).
37
- * Throws with a clear message on malformed input. */
39
+ /** Parse an OpenAPI document from text. Accepts both YAML (the sanctioned authoring format for
40
+ * the HTTP surface — ADR 0059) and JSON (still accepted, e.g. a generated interchange artifact).
41
+ * JSON is a strict subset of YAML 1.2, so JSON is tried first for a precise error + fast path,
42
+ * then the text is parsed as YAML. Throws with a clear message on malformed input. */
38
43
  export function parseSpec(text) {
39
44
  let doc;
40
45
  try {
46
+ // Fast path + precise diagnostics for JSON (and the generated `openapi.json` interchange file).
41
47
  doc = JSON.parse(text);
42
48
  }
43
- catch (e) {
44
- throw new Error(`OpenAPI spec is not valid JSON (YAML is not yet supported — ADR 0058): ${e instanceof Error ? e.message : String(e)}`);
49
+ catch (jsonError) {
50
+ // Not JSON — parse as YAML (which also subsumes JSON, so authored `.yaml`/`.yml` specs load).
51
+ try {
52
+ doc = parseYaml(text);
53
+ }
54
+ catch (yamlError) {
55
+ const jsonMessage = jsonError instanceof Error ? jsonError.message : String(jsonError);
56
+ const yamlMessage = yamlError instanceof Error ? yamlError.message : String(yamlError);
57
+ throw new Error(`OpenAPI spec is not valid YAML or JSON: JSON parse error: ${jsonMessage}; YAML parse error: ${yamlMessage}`);
58
+ }
59
+ }
60
+ if (!isRecord(doc)) {
61
+ throw new Error("OpenAPI spec must be an object (a mapping at the document root)");
45
62
  }
46
- if (!isRecord(doc))
47
- throw new Error("OpenAPI spec must be a JSON object");
48
63
  return doc;
49
64
  }
50
65
  /** The basename of a local `#/components/schemas/Name` ref, else undefined. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.33.0",
4
- "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
3
+ "version": "0.34.0",
4
+ "description": "Urban: build and run code-first apps on Nano \u2014 runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "keywords": [
@@ -66,7 +66,8 @@
66
66
  "@nanobpm/nano-sdk": "^1.2.3",
67
67
  "@nanobpm/workflow": "^0.8.0",
68
68
  "bpmn-auto-layout": "^2.0.0-alpha.2",
69
- "create-urban-app": "^0.9.0"
69
+ "create-urban-app": "^0.9.0",
70
+ "yaml": "^2.9.0"
70
71
  },
71
72
  "devDependencies": {
72
73
  "@types/node": "^22.20.1",
@@ -51,9 +51,65 @@ const doc: OpenApiDoc = {
51
51
  },
52
52
  };
53
53
 
54
- test("parseSpec throws a clear error on non-JSON and non-object", () => {
55
- assert.throws(() => parseSpec("not json"), /not valid JSON/);
56
- assert.throws(() => parseSpec("42"), /must be a JSON object/);
54
+ test("parseSpec throws a clear error on non-object and unparseable input", () => {
55
+ // A bare scalar parses (as JSON or YAML) but is not a document object.
56
+ assert.throws(() => parseSpec("42"), /must be an object/);
57
+ // Unbalanced/garbage that is neither valid JSON nor valid YAML.
58
+ assert.throws(() => parseSpec("{ : ]["), /not valid YAML or JSON/);
59
+ });
60
+
61
+ test("parseSpec includes both JSON and YAML diagnostics when neither parser accepts the input", () => {
62
+ assert.throws(
63
+ () => parseSpec("{ : ]["),
64
+ /JSON parse error:.*YAML parse error:/,
65
+ );
66
+ });
67
+
68
+ test("parseSpec parses a YAML OpenAPI document (ADR 0059 authoring format)", () => {
69
+ const yaml = [
70
+ "openapi: 3.0.0",
71
+ "info:",
72
+ " title: Demo",
73
+ " version: 1.0.0",
74
+ "paths:",
75
+ " /invoices:",
76
+ " post:",
77
+ " operationId: createInvoice",
78
+ " requestBody:",
79
+ " required: true",
80
+ " content:",
81
+ " application/json:",
82
+ " schema:",
83
+ " $ref: '#/components/schemas/Invoice'",
84
+ " responses:",
85
+ " '201':",
86
+ " description: created",
87
+ "components:",
88
+ " schemas:",
89
+ " Invoice:",
90
+ " type: object",
91
+ " required: [id, amount]",
92
+ " properties:",
93
+ " id: { type: string }",
94
+ " amount: { type: integer, minimum: 1 }",
95
+ "",
96
+ ].join("\n");
97
+ const parsed = parseSpec(yaml);
98
+ assert.equal(parsed.openapi, "3.0.0");
99
+ const ops = collectOperations(parsed);
100
+ assert.deepEqual(
101
+ ops.map((o) => o.operationId),
102
+ ["createInvoice"],
103
+ );
104
+ // The $ref resolves against the YAML-authored components, same as a JSON spec.
105
+ const resolved = resolveSchema(parsed, { $ref: "#/components/schemas/Invoice" });
106
+ assert.equal(resolved?.type, "object");
107
+ assert.deepEqual(resolved?.required, ["id", "amount"]);
108
+ });
109
+
110
+ test("parseSpec still parses JSON (accepted interchange format)", () => {
111
+ const parsed = parseSpec(JSON.stringify(doc));
112
+ assert.equal(parsed.openapi, "3.0.0");
57
113
  });
58
114
 
59
115
  test("collectOperations returns id-bearing ops in stable (path, method) order", () => {
@@ -2,13 +2,17 @@
2
2
  // deriver (`toolkit/derivers/api.ts`, authoring-time type + wrapper emission) and the runtime
3
3
  // (`runtime/core/modules/api.ts`, request validation + routing) build on this one module, so the
4
4
  // derived types and the runtime routes/validation always agree. No IO, no `node:*`, no `Deno` —
5
- // it operates on an already-parsed document object.
5
+ // it operates on document text/objects. Its one external dependency is `yaml` (a browser-safe,
6
+ // pure-JS parser with a dedicated browser build) so `parseSpec` accepts YAML — the sanctioned
7
+ // authoring format for the HTTP surface (ADR 0059) — as well as JSON.
6
8
  //
7
9
  // Scope: the ADR 0058 "supported profile" — JSON bodies, path/query parameters, a JSON
8
10
  // requestBody, response codes, and a JSON-Schema subset validator (type/required/enum/numeric &
9
11
  // string bounds/pattern/array & object shape/nullable/$ref). Exotic OpenAPI (callbacks, links,
10
12
  // XML, discriminated oneOf composition) is intentionally out of scope for this slice.
11
13
 
14
+ import { parse as parseYaml } from "yaml";
15
+
12
16
  /** A JSON Schema (the OpenAPI subset we read). Kept structural and permissive — unknown keywords
13
17
  * are ignored by the validator and fall back to `unknown` in the type emitter. */
14
18
  export interface OpenApiSchema {
@@ -116,20 +120,28 @@ function isSchema(v: unknown): v is OpenApiSchema {
116
120
  return isRecord(v);
117
121
  }
118
122
 
119
- /** Parse an OpenAPI document from text. JSON only in this slice (ADR 0058 open question: YAML).
120
- * Throws with a clear message on malformed input. */
123
+ /** Parse an OpenAPI document from text. Accepts both YAML (the sanctioned authoring format for
124
+ * the HTTP surface — ADR 0059) and JSON (still accepted, e.g. a generated interchange artifact).
125
+ * JSON is a strict subset of YAML 1.2, so JSON is tried first for a precise error + fast path,
126
+ * then the text is parsed as YAML. Throws with a clear message on malformed input. */
121
127
  export function parseSpec(text: string): OpenApiDoc {
122
128
  let doc: unknown;
123
129
  try {
130
+ // Fast path + precise diagnostics for JSON (and the generated `openapi.json` interchange file).
124
131
  doc = JSON.parse(text);
125
- } catch (e) {
126
- throw new Error(
127
- `OpenAPI spec is not valid JSON (YAML is not yet supported — ADR 0058): ${
128
- e instanceof Error ? e.message : String(e)
129
- }`,
130
- );
132
+ } catch (jsonError) {
133
+ // Not JSON — parse as YAML (which also subsumes JSON, so authored `.yaml`/`.yml` specs load).
134
+ try {
135
+ doc = parseYaml(text);
136
+ } catch (yamlError) {
137
+ const jsonMessage = jsonError instanceof Error ? jsonError.message : String(jsonError);
138
+ const yamlMessage = yamlError instanceof Error ? yamlError.message : String(yamlError);
139
+ throw new Error(`OpenAPI spec is not valid YAML or JSON: JSON parse error: ${jsonMessage}; YAML parse error: ${yamlMessage}`);
140
+ }
141
+ }
142
+ if (!isRecord(doc)) {
143
+ throw new Error("OpenAPI spec must be an object (a mapping at the document root)");
131
144
  }
132
- if (!isRecord(doc)) throw new Error("OpenAPI spec must be a JSON object");
133
145
  return doc;
134
146
  }
135
147