x-openapi-flow 1.1.1 → 1.1.2

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
@@ -1,6 +1,6 @@
1
1
  # x-openapi-flow
2
2
 
3
- CLI and specification for validating the `x-flow` extension field in OpenAPI documents.
3
+ CLI and specification for validating the `x-openapi-flow` extension field in OpenAPI documents.
4
4
 
5
5
  ## Installation
6
6
 
@@ -20,11 +20,25 @@ x-openapi-flow doctor
20
20
 
21
21
  ```bash
22
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"]
23
+ x-openapi-flow init [openapi-file] [--flows path]
24
+ x-openapi-flow apply [openapi-file] [--flows path] [--out path]
24
25
  x-openapi-flow graph <openapi-file> [--format mermaid|json]
25
26
  x-openapi-flow doctor [--config path]
26
27
  ```
27
28
 
29
+ `init` always works on an existing OpenAPI file in your repository.
30
+ `init` creates/synchronizes `x-openapi-flow.flows.yaml` as a persistent sidecar for your `x-openapi-flow` data.
31
+ Use `apply` to inject sidecar flows back into regenerated OpenAPI files.
32
+ If no OpenAPI/Swagger file exists yet, generate one first with your framework's official OpenAPI/Swagger tooling.
33
+
34
+ ## Recommended Workflow
35
+
36
+ ```bash
37
+ x-openapi-flow init openapi.yaml
38
+ # edit x-openapi-flow.flows.yaml
39
+ x-openapi-flow apply openapi.yaml
40
+ ```
41
+
28
42
  ## Optional Configuration
29
43
 
30
44
  Create `x-openapi-flow.config.json` in your project directory:
@@ -40,7 +54,13 @@ Create `x-openapi-flow.config.json` in your project directory:
40
54
  ## File Compatibility
41
55
 
42
56
  - OpenAPI input in `.yaml`, `.yml`, and `.json`
43
- - Validation processes OAS content with the `x-flow` extension
57
+ - Validation processes OAS content with the `x-openapi-flow` extension
58
+
59
+ ## Swagger UI
60
+
61
+ - There is no Swagger UI-based automated test in this repo today (tests are CLI-only).
62
+ - For UI interpretation of `x-openapi-flow`, use `showExtensions: true` plus the example plugin at `examples/swagger-ui/x-openapi-flow-plugin.js`.
63
+ - A ready HTML example is available at `examples/swagger-ui/index.html`.
44
64
 
45
65
  ## Repository and Full Documentation
46
66
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
+ const yaml = require("js-yaml");
6
7
  const {
7
8
  run,
8
9
  loadApi,
@@ -11,6 +12,7 @@ const {
11
12
  } = require("../lib/validator");
12
13
 
13
14
  const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
15
+ const DEFAULT_FLOWS_FILE = "x-openapi-flow.flows.yaml";
14
16
 
15
17
  function resolveConfigPath(configPathArg) {
16
18
  return configPathArg
@@ -42,7 +44,8 @@ function printHelp() {
42
44
 
43
45
  Usage:
44
46
  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"]
47
+ x-openapi-flow init [openapi-file] [--flows path]
48
+ x-openapi-flow apply [openapi-file] [--flows path] [--out path]
46
49
  x-openapi-flow graph <openapi-file> [--format mermaid|json]
47
50
  x-openapi-flow doctor [--config path]
48
51
  x-openapi-flow --help
@@ -51,7 +54,10 @@ Examples:
51
54
  x-openapi-flow validate examples/order-api.yaml
52
55
  x-openapi-flow validate examples/order-api.yaml --profile relaxed
53
56
  x-openapi-flow validate examples/order-api.yaml --strict-quality
54
- x-openapi-flow init my-api.yaml --title "Orders API"
57
+ x-openapi-flow init openapi.yaml --flows x-openapi-flow.flows.yaml
58
+ x-openapi-flow init
59
+ x-openapi-flow apply openapi.yaml
60
+ x-openapi-flow apply openapi.yaml --out openapi.flow.yaml
55
61
  x-openapi-flow graph examples/order-api.yaml
56
62
  x-openapi-flow doctor
57
63
  `);
@@ -163,21 +169,21 @@ function parseValidateArgs(args) {
163
169
  }
164
170
 
165
171
  function parseInitArgs(args) {
166
- const unknown = findUnknownOptions(args, ["--title"], []);
172
+ const unknown = findUnknownOptions(args, ["--flows"], []);
167
173
  if (unknown) {
168
174
  return { error: `Unknown option: ${unknown}` };
169
175
  }
170
176
 
171
- const titleOpt = getOptionValue(args, "--title");
172
- if (titleOpt.error) {
173
- return { error: titleOpt.error };
177
+ const flowsOpt = getOptionValue(args, "--flows");
178
+ if (flowsOpt.error) {
179
+ return { error: flowsOpt.error };
174
180
  }
175
181
 
176
182
  const positional = args.filter((token, index) => {
177
- if (token === "--title") {
183
+ if (token === "--flows") {
178
184
  return false;
179
185
  }
180
- if (index > 0 && args[index - 1] === "--title") {
186
+ if (index > 0 && args[index - 1] === "--flows") {
181
187
  return false;
182
188
  }
183
189
  return !token.startsWith("--");
@@ -188,8 +194,45 @@ function parseInitArgs(args) {
188
194
  }
189
195
 
190
196
  return {
191
- outputFile: path.resolve(positional[0] || "x-flow-api.yaml"),
192
- title: titleOpt.found ? titleOpt.value : "Sample API",
197
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
198
+ flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
199
+ };
200
+ }
201
+
202
+ function parseApplyArgs(args) {
203
+ const unknown = findUnknownOptions(args, ["--flows", "--out"], []);
204
+ if (unknown) {
205
+ return { error: `Unknown option: ${unknown}` };
206
+ }
207
+
208
+ const flowsOpt = getOptionValue(args, "--flows");
209
+ if (flowsOpt.error) {
210
+ return { error: flowsOpt.error };
211
+ }
212
+
213
+ const outOpt = getOptionValue(args, "--out");
214
+ if (outOpt.error) {
215
+ return { error: outOpt.error };
216
+ }
217
+
218
+ const positional = args.filter((token, index) => {
219
+ if (token === "--flows" || token === "--out") {
220
+ return false;
221
+ }
222
+ if (index > 0 && (args[index - 1] === "--flows" || args[index - 1] === "--out")) {
223
+ return false;
224
+ }
225
+ return !token.startsWith("--");
226
+ });
227
+
228
+ if (positional.length > 1) {
229
+ return { error: `Unexpected argument: ${positional[1]}` };
230
+ }
231
+
232
+ return {
233
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
234
+ flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
235
+ outPath: outOpt.found ? path.resolve(outOpt.value) : undefined,
193
236
  };
194
237
  }
195
238
 
@@ -274,6 +317,11 @@ function parseArgs(argv) {
274
317
  return parsed.error ? parsed : { command, ...parsed };
275
318
  }
276
319
 
320
+ if (command === "apply") {
321
+ const parsed = parseApplyArgs(commandArgs);
322
+ return parsed.error ? parsed : { command, ...parsed };
323
+ }
324
+
277
325
  if (command === "doctor") {
278
326
  const parsed = parseDoctorArgs(commandArgs);
279
327
  return parsed.error ? parsed : { command, ...parsed };
@@ -282,51 +330,330 @@ function parseArgs(argv) {
282
330
  return { error: `Unknown command: ${command}` };
283
331
  }
284
332
 
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
- `;
333
+ function findOpenApiFile(startDirectory) {
334
+ const preferredNames = [
335
+ "openapi.yaml",
336
+ "openapi.yml",
337
+ "openapi.json",
338
+ "swagger.yaml",
339
+ "swagger.yml",
340
+ "swagger.json",
341
+ ];
342
+
343
+ for (const fileName of preferredNames) {
344
+ const candidate = path.join(startDirectory, fileName);
345
+ if (fs.existsSync(candidate)) {
346
+ return candidate;
347
+ }
348
+ }
349
+
350
+ const ignoredDirs = new Set(["node_modules", ".git", "dist", "build"]);
351
+
352
+ function walk(directory) {
353
+ let entries = [];
354
+ try {
355
+ entries = fs.readdirSync(directory, { withFileTypes: true });
356
+ } catch (_err) {
357
+ return null;
358
+ }
359
+
360
+ for (const entry of entries) {
361
+ const fullPath = path.join(directory, entry.name);
362
+
363
+ if (entry.isDirectory()) {
364
+ if (!ignoredDirs.has(entry.name)) {
365
+ const nested = walk(fullPath);
366
+ if (nested) {
367
+ return nested;
368
+ }
369
+ }
370
+ continue;
371
+ }
372
+
373
+ if (preferredNames.includes(entry.name)) {
374
+ return fullPath;
375
+ }
376
+ }
377
+
378
+ return null;
379
+ }
380
+
381
+ return walk(startDirectory);
382
+ }
383
+
384
+ function resolveFlowsPath(openApiFile, customFlowsPath) {
385
+ if (customFlowsPath) {
386
+ return customFlowsPath;
387
+ }
388
+
389
+ if (openApiFile) {
390
+ return path.join(path.dirname(openApiFile), DEFAULT_FLOWS_FILE);
391
+ }
392
+
393
+ return path.resolve(process.cwd(), DEFAULT_FLOWS_FILE);
394
+ }
395
+
396
+ function getOpenApiFormat(filePath) {
397
+ return filePath.endsWith(".json") ? "json" : "yaml";
398
+ }
399
+
400
+ function saveOpenApi(filePath, api) {
401
+ const format = getOpenApiFormat(filePath);
402
+ const content = format === "json"
403
+ ? JSON.stringify(api, null, 2) + "\n"
404
+ : yaml.dump(api, { noRefs: true, lineWidth: -1 });
405
+
406
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
407
+ fs.writeFileSync(filePath, content, "utf8");
408
+ }
409
+
410
+ function extractOperationEntries(api) {
411
+ const entries = [];
412
+ const paths = (api && api.paths) || {};
413
+ const httpMethods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
414
+
415
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
416
+ for (const method of httpMethods) {
417
+ const operation = pathItem[method];
418
+ if (!operation) {
419
+ continue;
420
+ }
421
+
422
+ const operationId = operation.operationId;
423
+ const key = operationId ? `operationId:${operationId}` : `${method.toUpperCase()} ${pathKey}`;
424
+
425
+ entries.push({
426
+ key,
427
+ operationId,
428
+ method,
429
+ path: pathKey,
430
+ });
431
+ }
432
+ }
433
+
434
+ return entries;
435
+ }
436
+
437
+ function readFlowsFile(flowsPath) {
438
+ if (!fs.existsSync(flowsPath)) {
439
+ return {
440
+ version: "1.0",
441
+ operations: [],
442
+ };
443
+ }
444
+
445
+ const content = fs.readFileSync(flowsPath, "utf8");
446
+ const parsed = yaml.load(content);
447
+
448
+ if (!parsed || typeof parsed !== "object") {
449
+ return { version: "1.0", operations: [] };
450
+ }
451
+
452
+ return {
453
+ version: parsed.version || "1.0",
454
+ operations: Array.isArray(parsed.operations) ? parsed.operations : [],
455
+ };
456
+ }
457
+
458
+ function writeFlowsFile(flowsPath, flowsDoc) {
459
+ fs.mkdirSync(path.dirname(flowsPath), { recursive: true });
460
+ const content = yaml.dump(flowsDoc, { noRefs: true, lineWidth: -1 });
461
+ fs.writeFileSync(flowsPath, content, "utf8");
462
+ }
463
+
464
+ function buildOperationLookup(api) {
465
+ const lookupByKey = new Map();
466
+ const lookupByOperationId = new Map();
467
+ const entries = extractOperationEntries(api);
468
+
469
+ for (const entry of entries) {
470
+ lookupByKey.set(entry.key, entry);
471
+ if (entry.operationId) {
472
+ lookupByOperationId.set(entry.operationId, entry);
473
+ }
474
+ }
475
+
476
+ return { entries, lookupByKey, lookupByOperationId };
477
+ }
478
+
479
+ function mergeFlowsWithOpenApi(api, flowsDoc) {
480
+ const { entries, lookupByKey, lookupByOperationId } = buildOperationLookup(api);
481
+
482
+ const existingByKey = new Map();
483
+ for (const entry of flowsDoc.operations) {
484
+ const entryKey = entry.key || (entry.operationId ? `operationId:${entry.operationId}` : null);
485
+ if (entryKey) {
486
+ existingByKey.set(entryKey, entry);
487
+ }
488
+ }
489
+
490
+ const mergedOperations = [];
491
+
492
+ for (const op of entries) {
493
+ const existing = existingByKey.get(op.key);
494
+ if (existing) {
495
+ mergedOperations.push({
496
+ ...existing,
497
+ key: op.key,
498
+ operationId: op.operationId,
499
+ method: op.method,
500
+ path: op.path,
501
+ missing_in_openapi: false,
502
+ });
503
+ } else {
504
+ mergedOperations.push({
505
+ key: op.key,
506
+ operationId: op.operationId,
507
+ method: op.method,
508
+ path: op.path,
509
+ "x-openapi-flow": null,
510
+ missing_in_openapi: false,
511
+ });
512
+ }
513
+ }
514
+
515
+ for (const existing of flowsDoc.operations) {
516
+ const existingKey = existing.key || (existing.operationId ? `operationId:${existing.operationId}` : null);
517
+ const found = existingKey && lookupByKey.has(existingKey);
518
+
519
+ if (!found) {
520
+ mergedOperations.push({
521
+ ...existing,
522
+ missing_in_openapi: true,
523
+ });
524
+ }
525
+ }
526
+
527
+ return {
528
+ version: "1.0",
529
+ operations: mergedOperations,
530
+ };
531
+ }
532
+
533
+ function applyFlowsToOpenApi(api, flowsDoc) {
534
+ const paths = (api && api.paths) || {};
535
+ let appliedCount = 0;
536
+ const { lookupByKey, lookupByOperationId } = buildOperationLookup(api);
537
+
538
+ for (const flowEntry of flowsDoc.operations || []) {
539
+ if (!flowEntry || flowEntry.missing_in_openapi === true) {
540
+ continue;
541
+ }
542
+
543
+ const flowValue = flowEntry["x-openapi-flow"];
544
+ if (!flowValue || typeof flowValue !== "object") {
545
+ continue;
546
+ }
547
+
548
+ let target = null;
549
+ if (flowEntry.operationId && lookupByOperationId.has(flowEntry.operationId)) {
550
+ target = lookupByOperationId.get(flowEntry.operationId);
551
+ } else if (flowEntry.key && lookupByKey.has(flowEntry.key)) {
552
+ target = lookupByKey.get(flowEntry.key);
553
+ }
554
+
555
+ if (!target) {
556
+ continue;
557
+ }
558
+
559
+ const pathItem = paths[target.path];
560
+ if (pathItem && pathItem[target.method]) {
561
+ pathItem[target.method]["x-openapi-flow"] = flowValue;
562
+ appliedCount += 1;
563
+ }
564
+ }
565
+
566
+ return appliedCount;
319
567
  }
320
568
 
321
569
  function runInit(parsed) {
322
- if (fs.existsSync(parsed.outputFile)) {
323
- console.error(`ERROR: File already exists: ${parsed.outputFile}`);
570
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
571
+
572
+ if (!targetOpenApiFile) {
573
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
574
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
575
+ console.error("Create your OpenAPI/Swagger spec first (using your framework's official generator), then run init again.");
576
+ return 1;
577
+ }
578
+
579
+ const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
580
+
581
+ let api;
582
+ try {
583
+ api = loadApi(targetOpenApiFile);
584
+ } catch (err) {
585
+ console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
586
+ return 1;
587
+ }
588
+
589
+ let flowsDoc;
590
+ try {
591
+ flowsDoc = readFlowsFile(flowsPath);
592
+ } catch (err) {
593
+ console.error(`ERROR: Could not parse flows file — ${err.message}`);
594
+ return 1;
595
+ }
596
+
597
+ const mergedFlows = mergeFlowsWithOpenApi(api, flowsDoc);
598
+ writeFlowsFile(flowsPath, mergedFlows);
599
+ const appliedCount = applyFlowsToOpenApi(api, mergedFlows);
600
+ saveOpenApi(targetOpenApiFile, api);
601
+
602
+ const trackedCount = mergedFlows.operations.filter((entry) => !entry.missing_in_openapi).length;
603
+ const orphanCount = mergedFlows.operations.filter((entry) => entry.missing_in_openapi).length;
604
+
605
+ console.log(`Using existing OpenAPI file: ${targetOpenApiFile}`);
606
+ console.log(`Flows sidecar synced: ${flowsPath}`);
607
+ console.log(`Tracked operations: ${trackedCount}`);
608
+ if (orphanCount > 0) {
609
+ console.log(`Orphan flow entries kept in sidecar: ${orphanCount}`);
610
+ }
611
+ console.log(`Applied x-openapi-flow entries to OpenAPI: ${appliedCount}`);
612
+
613
+ console.log(`Validate now: x-openapi-flow validate ${targetOpenApiFile}`);
614
+ return 0;
615
+ }
616
+
617
+ function runApply(parsed) {
618
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
619
+
620
+ if (!targetOpenApiFile) {
621
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
622
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
623
+ return 1;
624
+ }
625
+
626
+ const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
627
+ if (!fs.existsSync(flowsPath)) {
628
+ console.error(`ERROR: Flows sidecar not found: ${flowsPath}`);
629
+ console.error("Run `x-openapi-flow init` first to create and sync the sidecar.");
630
+ return 1;
631
+ }
632
+
633
+ let api;
634
+ let flowsDoc;
635
+ try {
636
+ api = loadApi(targetOpenApiFile);
637
+ } catch (err) {
638
+ console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
324
639
  return 1;
325
640
  }
326
641
 
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}`);
642
+ try {
643
+ flowsDoc = readFlowsFile(flowsPath);
644
+ } catch (err) {
645
+ console.error(`ERROR: Could not parse flows file — ${err.message}`);
646
+ return 1;
647
+ }
648
+
649
+ const appliedCount = applyFlowsToOpenApi(api, flowsDoc);
650
+ const outputPath = parsed.outPath || targetOpenApiFile;
651
+ saveOpenApi(outputPath, api);
652
+
653
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
654
+ console.log(`Flows sidecar: ${flowsPath}`);
655
+ console.log(`Applied x-openapi-flow entries: ${appliedCount}`);
656
+ console.log(`Output written to: ${outputPath}`);
330
657
  return 0;
331
658
  }
332
659
 
@@ -440,6 +767,10 @@ function main() {
440
767
  process.exit(runGraph(parsed));
441
768
  }
442
769
 
770
+ if (parsed.command === "apply") {
771
+ process.exit(runApply(parsed));
772
+ }
773
+
443
774
  const config = loadConfig(parsed.configPath);
444
775
  if (config.error) {
445
776
  console.error(`ERROR: ${config.error}`);
@@ -10,7 +10,7 @@ paths:
10
10
  post:
11
11
  summary: Start flow
12
12
  operationId: startFlow
13
- x-flow:
13
+ x-openapi-flow:
14
14
  version: "1.0"
15
15
  id: start-flow
16
16
  current_state: START
@@ -29,7 +29,7 @@ paths:
29
29
  post:
30
30
  summary: Loop A
31
31
  operationId: loopA
32
- x-flow:
32
+ x-openapi-flow:
33
33
  version: "1.0"
34
34
  id: loop-a-flow
35
35
  current_state: LOOP_A
@@ -45,7 +45,7 @@ paths:
45
45
  post:
46
46
  summary: Loop B
47
47
  operationId: loopB
48
- x-flow:
48
+ x-openapi-flow:
49
49
  version: "1.0"
50
50
  id: loop-b-flow
51
51
  current_state: LOOP_B
@@ -61,7 +61,7 @@ paths:
61
61
  post:
62
62
  summary: Done
63
63
  operationId: doneFlow
64
- x-flow:
64
+ x-openapi-flow:
65
65
  version: "1.0"
66
66
  id: done-flow
67
67
  current_state: DONE
@@ -3,14 +3,14 @@ info:
3
3
  title: Order API
4
4
  version: "1.0.0"
5
5
  description: >
6
- Example API demonstrating x-flow for order lifecycle orchestration.
6
+ Example API demonstrating x-openapi-flow for order lifecycle orchestration.
7
7
 
8
8
  paths:
9
9
  /orders:
10
10
  post:
11
11
  summary: Create a new order
12
12
  operationId: createOrder
13
- x-flow:
13
+ x-openapi-flow:
14
14
  version: "1.0"
15
15
  id: create-order-flow
16
16
  current_state: CREATED
@@ -30,7 +30,7 @@ paths:
30
30
  post:
31
31
  summary: Confirm an order
32
32
  operationId: confirmOrder
33
- x-flow:
33
+ x-openapi-flow:
34
34
  version: "1.0"
35
35
  id: confirm-order-flow
36
36
  current_state: CONFIRMED
@@ -53,7 +53,7 @@ paths:
53
53
  post:
54
54
  summary: Mark order as shipped
55
55
  operationId: shipOrder
56
- x-flow:
56
+ x-openapi-flow:
57
57
  version: "1.0"
58
58
  id: ship-order-flow
59
59
  current_state: SHIPPED
@@ -76,7 +76,7 @@ paths:
76
76
  post:
77
77
  summary: Mark order as delivered
78
78
  operationId: deliverOrder
79
- x-flow:
79
+ x-openapi-flow:
80
80
  version: "1.0"
81
81
  id: deliver-order-flow
82
82
  current_state: DELIVERED
@@ -95,7 +95,7 @@ paths:
95
95
  post:
96
96
  summary: Cancel an order
97
97
  operationId: cancelOrder
98
- x-flow:
98
+ x-openapi-flow:
99
99
  version: "1.0"
100
100
  id: cancel-order-flow
101
101
  current_state: CANCELLED
@@ -3,14 +3,14 @@ info:
3
3
  title: Payment API
4
4
  version: "1.0.0"
5
5
  description: >
6
- Simple payment API demonstrating the x-flow extension for OpenAPI Spec.
6
+ Simple payment API demonstrating the x-openapi-flow extension for OpenAPI Spec.
7
7
 
8
8
  paths:
9
9
  /payments:
10
10
  post:
11
11
  summary: Create and authorize a payment
12
12
  operationId: createPayment
13
- x-flow:
13
+ x-openapi-flow:
14
14
  version: "1.0"
15
15
  id: create-payment-flow
16
16
  current_state: AUTHORIZED
@@ -44,7 +44,7 @@ paths:
44
44
  post:
45
45
  summary: Capture an authorized payment
46
46
  operationId: capturePayment
47
- x-flow:
47
+ x-openapi-flow:
48
48
  version: "1.0"
49
49
  id: capture-payment-flow
50
50
  current_state: CAPTURED
@@ -10,7 +10,7 @@ paths:
10
10
  post:
11
11
  summary: Start flow A
12
12
  operationId: startA
13
- x-flow:
13
+ x-openapi-flow:
14
14
  version: "1.0"
15
15
  id: start-a-flow
16
16
  current_state: A_START
@@ -29,7 +29,7 @@ paths:
29
29
  post:
30
30
  summary: Complete flow A
31
31
  operationId: doneA
32
- x-flow:
32
+ x-openapi-flow:
33
33
  version: "1.0"
34
34
  id: done-a-flow
35
35
  current_state: A_DONE
@@ -41,7 +41,7 @@ paths:
41
41
  post:
42
42
  summary: Start flow B
43
43
  operationId: startB
44
- x-flow:
44
+ x-openapi-flow:
45
45
  version: "1.0"
46
46
  id: start-b-flow
47
47
  current_state: B_START
@@ -57,7 +57,7 @@ paths:
57
57
  post:
58
58
  summary: Complete flow B
59
59
  operationId: doneB
60
- x-flow:
60
+ x-openapi-flow:
61
61
  version: "1.0"
62
62
  id: done-b-flow
63
63
  current_state: B_DONE
@@ -0,0 +1,30 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>x-openapi-flow + Swagger UI</title>
7
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
8
+ <style>
9
+ body { margin: 0; }
10
+ #swagger-ui { max-width: 1200px; margin: 0 auto; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <div id="swagger-ui"></div>
15
+
16
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
17
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
18
+ <script src="./x-openapi-flow-plugin.js"></script>
19
+ <script>
20
+ window.ui = SwaggerUIBundle({
21
+ url: "../payment-api.yaml",
22
+ dom_id: "#swagger-ui",
23
+ deepLinking: true,
24
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
25
+ plugins: [window.XOpenApiFlowPlugin],
26
+ showExtensions: true,
27
+ });
28
+ </script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,46 @@
1
+ window.XOpenApiFlowPlugin = function () {
2
+ return {
3
+ wrapComponents: {
4
+ OperationSummary: (Original, system) => (props) => {
5
+ const operation = props.operation;
6
+ const flow = operation && operation.get && operation.get("x-openapi-flow");
7
+
8
+ if (!flow) {
9
+ return React.createElement(Original, props);
10
+ }
11
+
12
+ const flowObject = flow && flow.toJS ? flow.toJS() : flow;
13
+ const currentState = flowObject.current_state || "-";
14
+ const version = flowObject.version || "-";
15
+
16
+ return React.createElement(
17
+ "div",
18
+ null,
19
+ React.createElement(Original, props),
20
+ React.createElement(
21
+ "div",
22
+ {
23
+ style: {
24
+ marginTop: "8px",
25
+ padding: "8px 10px",
26
+ border: "1px solid #d9d9d9",
27
+ borderRadius: "6px",
28
+ background: "#fafafa",
29
+ fontSize: "12px",
30
+ },
31
+ },
32
+ React.createElement("strong", null, "x-openapi-flow"),
33
+ React.createElement(
34
+ "div",
35
+ { style: { marginTop: "4px" } },
36
+ "version: ",
37
+ version,
38
+ " | current_state: ",
39
+ currentState
40
+ )
41
+ )
42
+ );
43
+ },
44
+ },
45
+ };
46
+ };
@@ -3,14 +3,14 @@ info:
3
3
  title: Support Ticket API
4
4
  version: "1.0.0"
5
5
  description: >
6
- Example API demonstrating x-flow for support ticket lifecycle.
6
+ Example API demonstrating x-openapi-flow for support ticket lifecycle.
7
7
 
8
8
  paths:
9
9
  /tickets:
10
10
  post:
11
11
  summary: Open a support ticket
12
12
  operationId: openTicket
13
- x-flow:
13
+ x-openapi-flow:
14
14
  version: "1.0"
15
15
  id: open-ticket-flow
16
16
  current_state: OPEN
@@ -27,7 +27,7 @@ paths:
27
27
  post:
28
28
  summary: Start ticket handling
29
29
  operationId: startTicket
30
- x-flow:
30
+ x-openapi-flow:
31
31
  version: "1.0"
32
32
  id: start-ticket-flow
33
33
  current_state: IN_PROGRESS
@@ -53,7 +53,7 @@ paths:
53
53
  post:
54
54
  summary: Wait for customer response
55
55
  operationId: waitCustomer
56
- x-flow:
56
+ x-openapi-flow:
57
57
  version: "1.0"
58
58
  id: wait-customer-flow
59
59
  current_state: WAITING_CUSTOMER
@@ -76,7 +76,7 @@ paths:
76
76
  post:
77
77
  summary: Resolve ticket
78
78
  operationId: resolveTicket
79
- x-flow:
79
+ x-openapi-flow:
80
80
  version: "1.0"
81
81
  id: resolve-ticket-flow
82
82
  current_state: RESOLVED
@@ -99,7 +99,7 @@ paths:
99
99
  post:
100
100
  summary: Close ticket
101
101
  operationId: closeTicket
102
- x-flow:
102
+ x-openapi-flow:
103
103
  version: "1.0"
104
104
  id: close-ticket-flow
105
105
  current_state: CLOSED
package/lib/validator.js CHANGED
@@ -38,7 +38,7 @@ function loadApi(filePath) {
38
38
  }
39
39
 
40
40
  /**
41
- * Extract every x-flow object found in the `paths` section of an OAS document.
41
+ * Extract every x-openapi-flow object found in the `paths` section of an OAS document.
42
42
  * @param {object} api - Parsed OAS document.
43
43
  * @returns {{ endpoint: string, flow: object }[]}
44
44
  */
@@ -59,10 +59,10 @@ function extractFlows(api) {
59
59
  ];
60
60
  for (const method of HTTP_METHODS) {
61
61
  const operation = pathItem[method];
62
- if (operation && operation["x-flow"]) {
62
+ if (operation && operation["x-openapi-flow"]) {
63
63
  entries.push({
64
64
  endpoint: `${method.toUpperCase()} ${pathKey}`,
65
- flow: operation["x-flow"],
65
+ flow: operation["x-openapi-flow"],
66
66
  });
67
67
  }
68
68
  }
@@ -76,7 +76,7 @@ function extractFlows(api) {
76
76
  // ---------------------------------------------------------------------------
77
77
 
78
78
  /**
79
- * Validate all x-flow objects against the JSON Schema.
79
+ * Validate all x-openapi-flow objects against the JSON Schema.
80
80
  * @param {{ endpoint: string, flow: object }[]} flows
81
81
  * @returns {{ endpoint: string, errors: object[] }[]} Array of validation failures.
82
82
  */
@@ -105,23 +105,23 @@ function suggestFixes(errors = []) {
105
105
  if (err.keyword === "required" && err.params && err.params.missingProperty) {
106
106
  const missing = err.params.missingProperty;
107
107
  if (missing === "version") {
108
- suggestions.add("Add `version: \"1.0\"` to the x-flow object.");
108
+ suggestions.add("Add `version: \"1.0\"` to the x-openapi-flow object.");
109
109
  } else if (missing === "id") {
110
- suggestions.add("Add a unique `id` to the x-flow object.");
110
+ suggestions.add("Add a unique `id` to the x-openapi-flow object.");
111
111
  } else if (missing === "current_state") {
112
112
  suggestions.add("Add `current_state` to describe the operation state.");
113
113
  } else {
114
- suggestions.add(`Add required property \`${missing}\` in x-flow.`);
114
+ suggestions.add(`Add required property \`${missing}\` in x-openapi-flow.`);
115
115
  }
116
116
  }
117
117
 
118
118
  if (err.keyword === "enum" && err.instancePath.endsWith("/version")) {
119
- suggestions.add("Use supported x-flow version: `\"1.0\"`.");
119
+ suggestions.add("Use supported x-openapi-flow version: `\"1.0\"`.");
120
120
  }
121
121
 
122
122
  if (err.keyword === "additionalProperties" && err.params && err.params.additionalProperty) {
123
123
  suggestions.add(
124
- `Remove unsupported property \`${err.params.additionalProperty}\` from x-flow payload.`
124
+ `Remove unsupported property \`${err.params.additionalProperty}\` from x-openapi-flow payload.`
125
125
  );
126
126
  }
127
127
  }
@@ -474,7 +474,7 @@ function run(apiPath, options = {}) {
474
474
  return result;
475
475
  }
476
476
 
477
- // 2. Extract x-flow objects
477
+ // 2. Extract x-openapi-flow objects
478
478
  const flows = extractFlows(api);
479
479
 
480
480
  if (flows.length === 0) {
@@ -484,14 +484,14 @@ function run(apiPath, options = {}) {
484
484
  if (output === "json") {
485
485
  console.log(JSON.stringify(result, null, 2));
486
486
  } else {
487
- console.warn("WARNING: No x-flow extensions found in the API paths.");
487
+ console.warn("WARNING: No x-openapi-flow extensions found in the API paths.");
488
488
  }
489
489
 
490
490
  return result;
491
491
  }
492
492
 
493
493
  if (output === "pretty") {
494
- console.log(`Found ${flows.length} x-flow definition(s).\n`);
494
+ console.log(`Found ${flows.length} x-openapi-flow definition(s).\n`);
495
495
  }
496
496
 
497
497
  let hasErrors = false;
@@ -504,7 +504,7 @@ function run(apiPath, options = {}) {
504
504
 
505
505
  if (output === "pretty") {
506
506
  if (schemaFailures.length === 0) {
507
- console.log("✔ Schema validation passed for all x-flow definitions.");
507
+ console.log("✔ Schema validation passed for all x-openapi-flow definitions.");
508
508
  } else {
509
509
  console.error("✘ Schema validation FAILED:");
510
510
  for (const { endpoint, errors } of schemaFailures) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-openapi-flow",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "OpenAPI extension for resource workflow and lifecycle management",
5
5
  "main": "lib/validator.js",
6
6
  "repository": {
@@ -39,7 +39,7 @@
39
39
  "openapi-flow",
40
40
  "workflow",
41
41
  "lifecycle",
42
- "x-flow"
42
+ "x-openapi-flow"
43
43
  ],
44
44
  "license": "MIT",
45
45
  "dependencies": {
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://x-flow.dev/schema/flow-schema.json",
4
- "title": "x-flow",
5
- "description": "JSON Schema for the x-flow OpenAPI extension, defining resource lifecycle states and transitions.",
3
+ "$id": "https://x-openapi-flow.dev/schema/flow-schema.json",
4
+ "title": "x-openapi-flow",
5
+ "description": "JSON Schema for the x-openapi-flow OpenAPI extension, defining resource lifecycle states and transitions.",
6
6
  "type": "object",
7
7
  "required": ["version", "id", "current_state"],
8
8
  "properties": {
9
9
  "version": {
10
10
  "type": "string",
11
11
  "enum": ["1.0"],
12
- "description": "Version of the x-flow schema contract used by this definition."
12
+ "description": "Version of the x-openapi-flow schema contract used by this definition."
13
13
  },
14
14
  "id": {
15
15
  "type": "string",