x-openapi-flow 1.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/bin/x-openapi-flow.js +461 -0
- package/examples/non-terminating-api.yaml +70 -0
- package/examples/order-api.yaml +111 -0
- package/examples/payment-api.yaml +114 -0
- package/examples/quality-warning-api.yaml +66 -0
- package/examples/ticket-api.yaml +115 -0
- package/lib/validator.js +712 -0
- package/package.json +49 -0
- package/schema/flow-schema.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 @metiagomarques
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# x-openapi-flow
|
|
2
|
+
|
|
3
|
+
CLI and specification for validating the `x-flow` extension field in OpenAPI documents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install x-openapi-flow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
x-openapi-flow validate openapi.yaml
|
|
15
|
+
x-openapi-flow graph openapi.yaml
|
|
16
|
+
x-openapi-flow doctor
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
|
|
23
|
+
x-openapi-flow init [output-file] [--title "My API"]
|
|
24
|
+
x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
25
|
+
x-openapi-flow doctor [--config path]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Optional Configuration
|
|
29
|
+
|
|
30
|
+
Create `x-openapi-flow.config.json` in your project directory:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"profile": "strict",
|
|
35
|
+
"format": "pretty",
|
|
36
|
+
"strictQuality": false
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## File Compatibility
|
|
41
|
+
|
|
42
|
+
- OpenAPI input in `.yaml`, `.yml`, and `.json`
|
|
43
|
+
- Validation processes OAS content with the `x-flow` extension
|
|
44
|
+
|
|
45
|
+
## Repository and Full Documentation
|
|
46
|
+
|
|
47
|
+
- Repository: https://github.com/tiago-marques/x-openapi-flow
|
|
48
|
+
- Full guide and changelog are available in the root repository.
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const {
|
|
7
|
+
run,
|
|
8
|
+
loadApi,
|
|
9
|
+
extractFlows,
|
|
10
|
+
buildStateGraph,
|
|
11
|
+
} = require("../lib/validator");
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
|
|
14
|
+
|
|
15
|
+
function resolveConfigPath(configPathArg) {
|
|
16
|
+
return configPathArg
|
|
17
|
+
? path.resolve(configPathArg)
|
|
18
|
+
: path.resolve(process.cwd(), DEFAULT_CONFIG_NAME);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadConfig(configPathArg) {
|
|
22
|
+
const configPath = resolveConfigPath(configPathArg);
|
|
23
|
+
if (!fs.existsSync(configPath)) {
|
|
24
|
+
return { path: configPath, exists: false, data: {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
29
|
+
return { path: configPath, exists: true, data: parsed };
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return {
|
|
32
|
+
path: configPath,
|
|
33
|
+
exists: true,
|
|
34
|
+
error: `Could not parse config file: ${err.message}`,
|
|
35
|
+
data: {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printHelp() {
|
|
41
|
+
console.log(`x-openapi-flow CLI
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
|
|
45
|
+
x-openapi-flow init [output-file] [--title "My API"]
|
|
46
|
+
x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
47
|
+
x-openapi-flow doctor [--config path]
|
|
48
|
+
x-openapi-flow --help
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
x-openapi-flow validate examples/order-api.yaml
|
|
52
|
+
x-openapi-flow validate examples/order-api.yaml --profile relaxed
|
|
53
|
+
x-openapi-flow validate examples/order-api.yaml --strict-quality
|
|
54
|
+
x-openapi-flow init my-api.yaml --title "Orders API"
|
|
55
|
+
x-openapi-flow graph examples/order-api.yaml
|
|
56
|
+
x-openapi-flow doctor
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getOptionValue(args, optionName) {
|
|
61
|
+
const index = args.indexOf(optionName);
|
|
62
|
+
if (index === -1) {
|
|
63
|
+
return { found: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const value = args[index + 1];
|
|
67
|
+
if (!value || value.startsWith("--")) {
|
|
68
|
+
return { found: true, error: `Missing value for ${optionName}.` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { found: true, value, index };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findUnknownOptions(args, knownOptionsWithValue, knownFlags) {
|
|
75
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
76
|
+
const token = args[index];
|
|
77
|
+
if (!token.startsWith("--")) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (knownFlags.includes(token)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (knownOptionsWithValue.includes(token)) {
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return token;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseValidateArgs(args) {
|
|
97
|
+
const unknown = findUnknownOptions(
|
|
98
|
+
args,
|
|
99
|
+
["--format", "--profile", "--config"],
|
|
100
|
+
["--strict-quality"]
|
|
101
|
+
);
|
|
102
|
+
if (unknown) {
|
|
103
|
+
return { error: `Unknown option: ${unknown}` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
107
|
+
if (formatOpt.error) {
|
|
108
|
+
return { error: `${formatOpt.error} Use 'pretty' or 'json'.` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const profileOpt = getOptionValue(args, "--profile");
|
|
112
|
+
if (profileOpt.error) {
|
|
113
|
+
return { error: `${profileOpt.error} Use 'core', 'relaxed', or 'strict'.` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const configOpt = getOptionValue(args, "--config");
|
|
117
|
+
if (configOpt.error) {
|
|
118
|
+
return { error: configOpt.error };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const strictQuality = args.includes("--strict-quality");
|
|
122
|
+
const format = formatOpt.found ? formatOpt.value : undefined;
|
|
123
|
+
const profile = profileOpt.found ? profileOpt.value : undefined;
|
|
124
|
+
|
|
125
|
+
if (format && !["pretty", "json"].includes(format)) {
|
|
126
|
+
return { error: `Invalid --format '${format}'. Use 'pretty' or 'json'.` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (profile && !["core", "relaxed", "strict"].includes(profile)) {
|
|
130
|
+
return { error: `Invalid --profile '${profile}'. Use 'core', 'relaxed', or 'strict'.` };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const positional = args.filter((token, index) => {
|
|
134
|
+
if (["--format", "--profile", "--config"].includes(token)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
index > 0 &&
|
|
140
|
+
["--format", "--profile", "--config"].includes(args[index - 1])
|
|
141
|
+
) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return !token.startsWith("--") || token === "-";
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (positional.length === 0) {
|
|
149
|
+
return { error: "Missing OpenAPI file path. Usage: x-openapi-flow validate <openapi-file>" };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (positional.length > 1) {
|
|
153
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
filePath: path.resolve(positional[0]),
|
|
158
|
+
strictQuality,
|
|
159
|
+
format,
|
|
160
|
+
profile,
|
|
161
|
+
configPath: configOpt.found ? configOpt.value : undefined,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseInitArgs(args) {
|
|
166
|
+
const unknown = findUnknownOptions(args, ["--title"], []);
|
|
167
|
+
if (unknown) {
|
|
168
|
+
return { error: `Unknown option: ${unknown}` };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const titleOpt = getOptionValue(args, "--title");
|
|
172
|
+
if (titleOpt.error) {
|
|
173
|
+
return { error: titleOpt.error };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const positional = args.filter((token, index) => {
|
|
177
|
+
if (token === "--title") {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (index > 0 && args[index - 1] === "--title") {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
return !token.startsWith("--");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (positional.length > 1) {
|
|
187
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
outputFile: path.resolve(positional[0] || "x-flow-api.yaml"),
|
|
192
|
+
title: titleOpt.found ? titleOpt.value : "Sample API",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseGraphArgs(args) {
|
|
197
|
+
const unknown = findUnknownOptions(args, ["--format"], []);
|
|
198
|
+
if (unknown) {
|
|
199
|
+
return { error: `Unknown option: ${unknown}` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const formatOpt = getOptionValue(args, "--format");
|
|
203
|
+
if (formatOpt.error) {
|
|
204
|
+
return { error: `${formatOpt.error} Use 'mermaid' or 'json'.` };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const format = formatOpt.found ? formatOpt.value : "mermaid";
|
|
208
|
+
if (!["mermaid", "json"].includes(format)) {
|
|
209
|
+
return { error: `Invalid --format '${format}'. Use 'mermaid' or 'json'.` };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const positional = args.filter((token, index) => {
|
|
213
|
+
if (token === "--format") {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (index > 0 && args[index - 1] === "--format") {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return !token.startsWith("--");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (positional.length === 0) {
|
|
223
|
+
return { error: "Missing OpenAPI file path. Usage: x-openapi-flow graph <openapi-file>" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (positional.length > 1) {
|
|
227
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { filePath: path.resolve(positional[0]), format };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseDoctorArgs(args) {
|
|
234
|
+
const unknown = findUnknownOptions(args, ["--config"], []);
|
|
235
|
+
if (unknown) {
|
|
236
|
+
return { error: `Unknown option: ${unknown}` };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const configOpt = getOptionValue(args, "--config");
|
|
240
|
+
if (configOpt.error) {
|
|
241
|
+
return { error: configOpt.error };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
configPath: configOpt.found ? configOpt.value : undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseArgs(argv) {
|
|
250
|
+
const args = argv.slice(2);
|
|
251
|
+
const command = args[0];
|
|
252
|
+
|
|
253
|
+
if (!command || command === "--help" || command === "-h") {
|
|
254
|
+
return { help: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const commandArgs = args.slice(1);
|
|
258
|
+
if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
|
|
259
|
+
return { help: true, command };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (command === "validate") {
|
|
263
|
+
const parsed = parseValidateArgs(commandArgs);
|
|
264
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (command === "init") {
|
|
268
|
+
const parsed = parseInitArgs(commandArgs);
|
|
269
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (command === "graph") {
|
|
273
|
+
const parsed = parseGraphArgs(commandArgs);
|
|
274
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (command === "doctor") {
|
|
278
|
+
const parsed = parseDoctorArgs(commandArgs);
|
|
279
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { error: `Unknown command: ${command}` };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildTemplate(title) {
|
|
286
|
+
return `openapi: "3.0.3"
|
|
287
|
+
info:
|
|
288
|
+
title: ${title}
|
|
289
|
+
version: "1.0.0"
|
|
290
|
+
paths:
|
|
291
|
+
/resources:
|
|
292
|
+
post:
|
|
293
|
+
summary: Create resource
|
|
294
|
+
operationId: createResource
|
|
295
|
+
x-flow:
|
|
296
|
+
version: "1.0"
|
|
297
|
+
id: create-resource-flow
|
|
298
|
+
current_state: CREATED
|
|
299
|
+
transitions:
|
|
300
|
+
- target_state: COMPLETED
|
|
301
|
+
condition: Resource reaches terminal condition.
|
|
302
|
+
trigger_type: synchronous
|
|
303
|
+
responses:
|
|
304
|
+
"201":
|
|
305
|
+
description: Resource created.
|
|
306
|
+
|
|
307
|
+
/resources/{id}/complete:
|
|
308
|
+
post:
|
|
309
|
+
summary: Complete resource
|
|
310
|
+
operationId: completeResource
|
|
311
|
+
x-flow:
|
|
312
|
+
version: "1.0"
|
|
313
|
+
id: complete-resource-flow
|
|
314
|
+
current_state: COMPLETED
|
|
315
|
+
responses:
|
|
316
|
+
"200":
|
|
317
|
+
description: Resource completed.
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function runInit(parsed) {
|
|
322
|
+
if (fs.existsSync(parsed.outputFile)) {
|
|
323
|
+
console.error(`ERROR: File already exists: ${parsed.outputFile}`);
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fs.writeFileSync(parsed.outputFile, buildTemplate(parsed.title), "utf8");
|
|
328
|
+
console.log(`Template created: ${parsed.outputFile}`);
|
|
329
|
+
console.log(`Next step: x-openapi-flow validate ${parsed.outputFile}`);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function runDoctor(parsed) {
|
|
334
|
+
const config = loadConfig(parsed.configPath);
|
|
335
|
+
let hasErrors = false;
|
|
336
|
+
|
|
337
|
+
console.log("x-openapi-flow doctor");
|
|
338
|
+
console.log(`- Node.js: ${process.version}`);
|
|
339
|
+
|
|
340
|
+
if (config.exists) {
|
|
341
|
+
if (config.error) {
|
|
342
|
+
console.error(`- Config: FAIL (${config.path})`);
|
|
343
|
+
console.error(` ${config.error}`);
|
|
344
|
+
hasErrors = true;
|
|
345
|
+
} else {
|
|
346
|
+
console.log(`- Config: OK (${config.path})`);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
console.log(`- Config: not found (${config.path})`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const defaultApi = path.resolve(process.cwd(), "examples", "payment-api.yaml");
|
|
353
|
+
if (fs.existsSync(defaultApi)) {
|
|
354
|
+
console.log(`- Example API: found (${defaultApi})`);
|
|
355
|
+
} else {
|
|
356
|
+
console.log("- Example API: not found in current directory (this is optional)");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
if (fs.existsSync(defaultApi)) {
|
|
361
|
+
const api = loadApi(defaultApi);
|
|
362
|
+
extractFlows(api);
|
|
363
|
+
console.log("- Validator engine: OK");
|
|
364
|
+
} else {
|
|
365
|
+
console.log("- Validator engine: OK");
|
|
366
|
+
}
|
|
367
|
+
} catch (_err) {
|
|
368
|
+
console.error("- Validator engine: FAIL");
|
|
369
|
+
hasErrors = true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return hasErrors ? 1 : 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildMermaidGraph(filePath) {
|
|
376
|
+
const api = loadApi(filePath);
|
|
377
|
+
const flows = extractFlows(api);
|
|
378
|
+
const graph = buildStateGraph(flows);
|
|
379
|
+
const lines = ["stateDiagram-v2"];
|
|
380
|
+
|
|
381
|
+
for (const state of graph.nodes) {
|
|
382
|
+
lines.push(` state ${state}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const [from, targets] of graph.adjacency.entries()) {
|
|
386
|
+
for (const to of targets) {
|
|
387
|
+
lines.push(` ${from} --> ${to}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
flowCount: flows.length,
|
|
393
|
+
nodes: [...graph.nodes],
|
|
394
|
+
edges: [...graph.adjacency.entries()].flatMap(([from, targets]) =>
|
|
395
|
+
[...targets].map((to) => ({ from, to }))
|
|
396
|
+
),
|
|
397
|
+
mermaid: lines.join("\n"),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function runGraph(parsed) {
|
|
402
|
+
try {
|
|
403
|
+
const graphResult = buildMermaidGraph(parsed.filePath);
|
|
404
|
+
if (parsed.format === "json") {
|
|
405
|
+
console.log(JSON.stringify(graphResult, null, 2));
|
|
406
|
+
} else {
|
|
407
|
+
console.log(graphResult.mermaid);
|
|
408
|
+
}
|
|
409
|
+
return 0;
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.error(`ERROR: Could not build graph — ${err.message}`);
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function main() {
|
|
417
|
+
const parsed = parseArgs(process.argv);
|
|
418
|
+
|
|
419
|
+
if (parsed.help) {
|
|
420
|
+
printHelp();
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (parsed.error) {
|
|
425
|
+
console.error(`ERROR: ${parsed.error}`);
|
|
426
|
+
console.log("");
|
|
427
|
+
printHelp();
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (parsed.command === "init") {
|
|
432
|
+
process.exit(runInit(parsed));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (parsed.command === "doctor") {
|
|
436
|
+
process.exit(runDoctor(parsed));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (parsed.command === "graph") {
|
|
440
|
+
process.exit(runGraph(parsed));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const config = loadConfig(parsed.configPath);
|
|
444
|
+
if (config.error) {
|
|
445
|
+
console.error(`ERROR: ${config.error}`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const options = {
|
|
450
|
+
output: parsed.format || config.data.format || "pretty",
|
|
451
|
+
strictQuality:
|
|
452
|
+
parsed.strictQuality ||
|
|
453
|
+
config.data.strictQuality === true,
|
|
454
|
+
profile: parsed.profile || config.data.profile || "strict",
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const result = run(parsed.filePath, options);
|
|
458
|
+
process.exit(result.ok ? 0 : 1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
main();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
openapi: "3.0.3"
|
|
2
|
+
info:
|
|
3
|
+
title: Non-Terminating States API
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
description: >
|
|
6
|
+
Example API crafted to demonstrate non_terminating_states detection.
|
|
7
|
+
|
|
8
|
+
paths:
|
|
9
|
+
/flows/start:
|
|
10
|
+
post:
|
|
11
|
+
summary: Start flow
|
|
12
|
+
operationId: startFlow
|
|
13
|
+
x-flow:
|
|
14
|
+
version: "1.0"
|
|
15
|
+
id: start-flow
|
|
16
|
+
current_state: START
|
|
17
|
+
transitions:
|
|
18
|
+
- target_state: LOOP_A
|
|
19
|
+
condition: Branch goes to cyclic subflow.
|
|
20
|
+
trigger_type: synchronous
|
|
21
|
+
- target_state: DONE
|
|
22
|
+
condition: Happy path reaches terminal state.
|
|
23
|
+
trigger_type: synchronous
|
|
24
|
+
responses:
|
|
25
|
+
"200":
|
|
26
|
+
description: Flow started.
|
|
27
|
+
|
|
28
|
+
/flows/loop-a:
|
|
29
|
+
post:
|
|
30
|
+
summary: Loop A
|
|
31
|
+
operationId: loopA
|
|
32
|
+
x-flow:
|
|
33
|
+
version: "1.0"
|
|
34
|
+
id: loop-a-flow
|
|
35
|
+
current_state: LOOP_A
|
|
36
|
+
transitions:
|
|
37
|
+
- target_state: LOOP_B
|
|
38
|
+
condition: Internal loop transition.
|
|
39
|
+
trigger_type: webhook
|
|
40
|
+
responses:
|
|
41
|
+
"200":
|
|
42
|
+
description: Loop A handled.
|
|
43
|
+
|
|
44
|
+
/flows/loop-b:
|
|
45
|
+
post:
|
|
46
|
+
summary: Loop B
|
|
47
|
+
operationId: loopB
|
|
48
|
+
x-flow:
|
|
49
|
+
version: "1.0"
|
|
50
|
+
id: loop-b-flow
|
|
51
|
+
current_state: LOOP_B
|
|
52
|
+
transitions:
|
|
53
|
+
- target_state: LOOP_A
|
|
54
|
+
condition: Cycles back to Loop A.
|
|
55
|
+
trigger_type: polling
|
|
56
|
+
responses:
|
|
57
|
+
"200":
|
|
58
|
+
description: Loop B handled.
|
|
59
|
+
|
|
60
|
+
/flows/done:
|
|
61
|
+
post:
|
|
62
|
+
summary: Done
|
|
63
|
+
operationId: doneFlow
|
|
64
|
+
x-flow:
|
|
65
|
+
version: "1.0"
|
|
66
|
+
id: done-flow
|
|
67
|
+
current_state: DONE
|
|
68
|
+
responses:
|
|
69
|
+
"200":
|
|
70
|
+
description: Terminal state.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
openapi: "3.0.3"
|
|
2
|
+
info:
|
|
3
|
+
title: Order API
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
description: >
|
|
6
|
+
Example API demonstrating x-flow for order lifecycle orchestration.
|
|
7
|
+
|
|
8
|
+
paths:
|
|
9
|
+
/orders:
|
|
10
|
+
post:
|
|
11
|
+
summary: Create a new order
|
|
12
|
+
operationId: createOrder
|
|
13
|
+
x-flow:
|
|
14
|
+
version: "1.0"
|
|
15
|
+
id: create-order-flow
|
|
16
|
+
current_state: CREATED
|
|
17
|
+
description: A new order is accepted and created.
|
|
18
|
+
transitions:
|
|
19
|
+
- target_state: CONFIRMED
|
|
20
|
+
condition: Stock and payment checks pass.
|
|
21
|
+
trigger_type: synchronous
|
|
22
|
+
- target_state: CANCELLED
|
|
23
|
+
condition: Validation fails before confirmation.
|
|
24
|
+
trigger_type: synchronous
|
|
25
|
+
responses:
|
|
26
|
+
"201":
|
|
27
|
+
description: Order created successfully.
|
|
28
|
+
|
|
29
|
+
/orders/{id}/confirm:
|
|
30
|
+
post:
|
|
31
|
+
summary: Confirm an order
|
|
32
|
+
operationId: confirmOrder
|
|
33
|
+
x-flow:
|
|
34
|
+
version: "1.0"
|
|
35
|
+
id: confirm-order-flow
|
|
36
|
+
current_state: CONFIRMED
|
|
37
|
+
description: Order was confirmed and handed to fulfillment.
|
|
38
|
+
transitions:
|
|
39
|
+
- target_state: SHIPPED
|
|
40
|
+
condition: Warehouse dispatches package.
|
|
41
|
+
trigger_type: webhook
|
|
42
|
+
parameters:
|
|
43
|
+
- name: id
|
|
44
|
+
in: path
|
|
45
|
+
required: true
|
|
46
|
+
schema:
|
|
47
|
+
type: string
|
|
48
|
+
responses:
|
|
49
|
+
"200":
|
|
50
|
+
description: Order confirmed.
|
|
51
|
+
|
|
52
|
+
/orders/{id}/ship:
|
|
53
|
+
post:
|
|
54
|
+
summary: Mark order as shipped
|
|
55
|
+
operationId: shipOrder
|
|
56
|
+
x-flow:
|
|
57
|
+
version: "1.0"
|
|
58
|
+
id: ship-order-flow
|
|
59
|
+
current_state: SHIPPED
|
|
60
|
+
description: Package leaves warehouse.
|
|
61
|
+
transitions:
|
|
62
|
+
- target_state: DELIVERED
|
|
63
|
+
condition: Carrier confirms delivery.
|
|
64
|
+
trigger_type: webhook
|
|
65
|
+
parameters:
|
|
66
|
+
- name: id
|
|
67
|
+
in: path
|
|
68
|
+
required: true
|
|
69
|
+
schema:
|
|
70
|
+
type: string
|
|
71
|
+
responses:
|
|
72
|
+
"200":
|
|
73
|
+
description: Order shipped.
|
|
74
|
+
|
|
75
|
+
/orders/{id}/deliver:
|
|
76
|
+
post:
|
|
77
|
+
summary: Mark order as delivered
|
|
78
|
+
operationId: deliverOrder
|
|
79
|
+
x-flow:
|
|
80
|
+
version: "1.0"
|
|
81
|
+
id: deliver-order-flow
|
|
82
|
+
current_state: DELIVERED
|
|
83
|
+
description: Final state after customer receives package.
|
|
84
|
+
parameters:
|
|
85
|
+
- name: id
|
|
86
|
+
in: path
|
|
87
|
+
required: true
|
|
88
|
+
schema:
|
|
89
|
+
type: string
|
|
90
|
+
responses:
|
|
91
|
+
"200":
|
|
92
|
+
description: Order delivered.
|
|
93
|
+
|
|
94
|
+
/orders/{id}/cancel:
|
|
95
|
+
post:
|
|
96
|
+
summary: Cancel an order
|
|
97
|
+
operationId: cancelOrder
|
|
98
|
+
x-flow:
|
|
99
|
+
version: "1.0"
|
|
100
|
+
id: cancel-order-flow
|
|
101
|
+
current_state: CANCELLED
|
|
102
|
+
description: Terminal cancellation state.
|
|
103
|
+
parameters:
|
|
104
|
+
- name: id
|
|
105
|
+
in: path
|
|
106
|
+
required: true
|
|
107
|
+
schema:
|
|
108
|
+
type: string
|
|
109
|
+
responses:
|
|
110
|
+
"200":
|
|
111
|
+
description: Order canceled.
|