@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.
- package/dist/openapi/spec.d.ts +4 -2
- package/dist/openapi/spec.js +22 -7
- package/package.json +4 -3
- package/src/openapi/spec.test.ts +59 -3
- package/src/openapi/spec.ts +22 -10
package/dist/openapi/spec.d.ts
CHANGED
|
@@ -79,8 +79,10 @@ export interface OperationInfo {
|
|
|
79
79
|
eject: boolean;
|
|
80
80
|
summary?: string;
|
|
81
81
|
}
|
|
82
|
-
/** Parse an OpenAPI document from text.
|
|
83
|
-
*
|
|
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;
|
package/dist/openapi/spec.js
CHANGED
|
@@ -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
|
|
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.
|
|
37
|
-
*
|
|
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 (
|
|
44
|
-
|
|
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.
|
|
4
|
-
"description": "Urban: build and run code-first apps on Nano
|
|
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",
|
package/src/openapi/spec.test.ts
CHANGED
|
@@ -51,9 +51,65 @@ const doc: OpenApiDoc = {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
test("parseSpec throws a clear error on non-
|
|
55
|
-
|
|
56
|
-
assert.throws(() => parseSpec("42"), /must be
|
|
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", () => {
|
package/src/openapi/spec.ts
CHANGED
|
@@ -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
|
|
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.
|
|
120
|
-
*
|
|
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 (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|