x-openapi-flow 1.2.0 → 1.2.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 +28 -12
- package/bin/x-openapi-flow.js +110 -52
- package/examples/swagger-ui/x-openapi-flow-plugin.js +165 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# x-openapi-flow
|
|
2
2
|
|
|
3
|
-
CLI and
|
|
3
|
+
CLI and extension contract for documenting and validating resource lifecycle workflows in OpenAPI using `x-openapi-flow`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`x-openapi-flow` validates:
|
|
8
|
+
|
|
9
|
+
- Extension schema correctness
|
|
10
|
+
- Lifecycle graph consistency
|
|
11
|
+
- Optional quality checks for transitions and references
|
|
12
|
+
|
|
13
|
+
It also supports a sidecar workflow (`init` + `apply`) to preserve lifecycle metadata when OpenAPI files are regenerated.
|
|
4
14
|
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
@@ -23,7 +33,7 @@ If authentication is required, include this in your `.npmrc`:
|
|
|
23
33
|
|
|
24
34
|
Use a GitHub PAT with `read:packages` (install) and `write:packages` (publish).
|
|
25
35
|
|
|
26
|
-
## Quick
|
|
36
|
+
## Quick Start
|
|
27
37
|
|
|
28
38
|
```bash
|
|
29
39
|
x-openapi-flow validate openapi.yaml
|
|
@@ -31,7 +41,7 @@ x-openapi-flow graph openapi.yaml
|
|
|
31
41
|
x-openapi-flow doctor
|
|
32
42
|
```
|
|
33
43
|
|
|
34
|
-
## Commands
|
|
44
|
+
## CLI Commands
|
|
35
45
|
|
|
36
46
|
```bash
|
|
37
47
|
x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
|
|
@@ -41,20 +51,22 @@ x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
|
41
51
|
x-openapi-flow doctor [--config path]
|
|
42
52
|
```
|
|
43
53
|
|
|
54
|
+
## Sidecar Workflow
|
|
55
|
+
|
|
44
56
|
`init` always works on an existing OpenAPI file in your repository.
|
|
45
|
-
`init` creates/synchronizes `
|
|
57
|
+
`init` creates/synchronizes `{context}-openapi-flow.(json|yaml)` as a persistent sidecar for your `x-openapi-flow` data.
|
|
46
58
|
Use `apply` to inject sidecar flows back into regenerated OpenAPI files.
|
|
47
59
|
If no OpenAPI/Swagger file exists yet, generate one first with your framework's official OpenAPI/Swagger tooling.
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
### Recommended Sequence
|
|
50
62
|
|
|
51
63
|
```bash
|
|
52
64
|
x-openapi-flow init openapi.yaml
|
|
53
|
-
# edit
|
|
65
|
+
# edit {context}-openapi-flow.(json|yaml)
|
|
54
66
|
x-openapi-flow apply openapi.yaml
|
|
55
67
|
```
|
|
56
68
|
|
|
57
|
-
##
|
|
69
|
+
## Configuration
|
|
58
70
|
|
|
59
71
|
Create `x-openapi-flow.config.json` in your project directory:
|
|
60
72
|
|
|
@@ -66,12 +78,12 @@ Create `x-openapi-flow.config.json` in your project directory:
|
|
|
66
78
|
}
|
|
67
79
|
```
|
|
68
80
|
|
|
69
|
-
##
|
|
81
|
+
## Compatibility
|
|
70
82
|
|
|
71
83
|
- OpenAPI input in `.yaml`, `.yml`, and `.json`
|
|
72
84
|
- Validation processes OAS content with the `x-openapi-flow` extension
|
|
73
85
|
|
|
74
|
-
|
|
86
|
+
## Transition Guidance Fields
|
|
75
87
|
|
|
76
88
|
- `next_operation_id`: operationId usually called for the next state transition
|
|
77
89
|
- `prerequisite_operation_ids`: operationIds expected before a transition
|
|
@@ -83,21 +95,25 @@ Field reference format:
|
|
|
83
95
|
- `operationId:request.body.field`
|
|
84
96
|
- `operationId:response.<status>.body.field`
|
|
85
97
|
|
|
86
|
-
##
|
|
98
|
+
## Visualization
|
|
99
|
+
|
|
100
|
+
### Swagger UI
|
|
87
101
|
|
|
88
102
|
- There is no Swagger UI-based automated test in this repo today (tests are CLI-only).
|
|
89
103
|
- For UI interpretation of `x-openapi-flow`, use `showExtensions: true` plus the example plugin at `examples/swagger-ui/x-openapi-flow-plugin.js`.
|
|
90
104
|
- A ready HTML example is available at `examples/swagger-ui/index.html`.
|
|
105
|
+
- The plugin renders a global **Flow Overview** (Mermaid image) near the top of the docs, plus operation-level flow cards.
|
|
91
106
|
|
|
92
107
|

|
|
93
108
|
|
|
94
|
-
|
|
109
|
+
### Graph Output Example
|
|
95
110
|
|
|
96
111
|
`x-openapi-flow graph` includes transition guidance labels in Mermaid output when present (`next_operation_id`, `prerequisite_operation_ids`).
|
|
112
|
+
The `graph` command accepts both full OpenAPI files and sidecar files (`{context}-openapi-flow.(json|yaml)`).
|
|
97
113
|
|
|
98
114
|

|
|
99
115
|
|
|
100
|
-
## Repository and
|
|
116
|
+
## Repository and Documentation
|
|
101
117
|
|
|
102
118
|
- Repository: https://github.com/tiago-marques/x-openapi-flow
|
|
103
119
|
- Full guide and changelog are available in the root repository.
|
package/bin/x-openapi-flow.js
CHANGED
|
@@ -53,7 +53,7 @@ Examples:
|
|
|
53
53
|
x-openapi-flow validate examples/order-api.yaml
|
|
54
54
|
x-openapi-flow validate examples/order-api.yaml --profile relaxed
|
|
55
55
|
x-openapi-flow validate examples/order-api.yaml --strict-quality
|
|
56
|
-
x-openapi-flow init openapi.yaml --flows
|
|
56
|
+
x-openapi-flow init openapi.yaml --flows openapi-openapi-flow.yaml
|
|
57
57
|
x-openapi-flow init
|
|
58
58
|
x-openapi-flow apply openapi.yaml
|
|
59
59
|
x-openapi-flow apply openapi.yaml --out openapi.flow.yaml
|
|
@@ -386,7 +386,10 @@ function resolveFlowsPath(openApiFile, customFlowsPath) {
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
if (openApiFile) {
|
|
389
|
-
|
|
389
|
+
const parsed = path.parse(openApiFile);
|
|
390
|
+
const extension = parsed.ext.toLowerCase() === ".json" ? ".json" : ".yaml";
|
|
391
|
+
const fileName = `${parsed.name}-openapi-flow${extension}`;
|
|
392
|
+
return path.join(path.dirname(openApiFile), fileName);
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
return path.resolve(process.cwd(), DEFAULT_FLOWS_FILE);
|
|
@@ -406,6 +409,15 @@ function saveOpenApi(filePath, api) {
|
|
|
406
409
|
fs.writeFileSync(filePath, content, "utf8");
|
|
407
410
|
}
|
|
408
411
|
|
|
412
|
+
function buildFallbackOperationId(method, pathKey) {
|
|
413
|
+
const raw = `${method}_${pathKey}`.toLowerCase();
|
|
414
|
+
const sanitized = raw
|
|
415
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
416
|
+
.replace(/^_+|_+$/g, "");
|
|
417
|
+
|
|
418
|
+
return sanitized || "operation";
|
|
419
|
+
}
|
|
420
|
+
|
|
409
421
|
function extractOperationEntries(api) {
|
|
410
422
|
const entries = [];
|
|
411
423
|
const paths = (api && api.paths) || {};
|
|
@@ -419,13 +431,16 @@ function extractOperationEntries(api) {
|
|
|
419
431
|
}
|
|
420
432
|
|
|
421
433
|
const operationId = operation.operationId;
|
|
434
|
+
const resolvedOperationId = operationId || buildFallbackOperationId(method, pathKey);
|
|
422
435
|
const key = operationId ? `operationId:${operationId}` : `${method.toUpperCase()} ${pathKey}`;
|
|
423
436
|
|
|
424
437
|
entries.push({
|
|
425
438
|
key,
|
|
426
439
|
operationId,
|
|
440
|
+
resolvedOperationId,
|
|
427
441
|
method,
|
|
428
442
|
path: pathKey,
|
|
443
|
+
operation,
|
|
429
444
|
});
|
|
430
445
|
}
|
|
431
446
|
}
|
|
@@ -456,10 +471,22 @@ function readFlowsFile(flowsPath) {
|
|
|
456
471
|
|
|
457
472
|
function writeFlowsFile(flowsPath, flowsDoc) {
|
|
458
473
|
fs.mkdirSync(path.dirname(flowsPath), { recursive: true });
|
|
459
|
-
const content =
|
|
474
|
+
const content = flowsPath.endsWith(".json")
|
|
475
|
+
? `${JSON.stringify(flowsDoc, null, 2)}\n`
|
|
476
|
+
: yaml.dump(flowsDoc, { noRefs: true, lineWidth: -1 });
|
|
460
477
|
fs.writeFileSync(flowsPath, content, "utf8");
|
|
461
478
|
}
|
|
462
479
|
|
|
480
|
+
function buildFlowTemplate(operationId) {
|
|
481
|
+
const safeOperationId = operationId || "operation";
|
|
482
|
+
return {
|
|
483
|
+
version: "1.0",
|
|
484
|
+
id: `${safeOperationId}_FLOW_ID`,
|
|
485
|
+
current_state: `${safeOperationId}_STATE`,
|
|
486
|
+
transitions: [],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
463
490
|
function buildOperationLookup(api) {
|
|
464
491
|
const lookupByKey = new Map();
|
|
465
492
|
const lookupByOperationId = new Map();
|
|
@@ -467,8 +494,8 @@ function buildOperationLookup(api) {
|
|
|
467
494
|
|
|
468
495
|
for (const entry of entries) {
|
|
469
496
|
lookupByKey.set(entry.key, entry);
|
|
470
|
-
if (entry.
|
|
471
|
-
lookupByOperationId.set(entry.
|
|
497
|
+
if (entry.resolvedOperationId) {
|
|
498
|
+
lookupByOperationId.set(entry.resolvedOperationId, entry);
|
|
472
499
|
}
|
|
473
500
|
}
|
|
474
501
|
|
|
@@ -476,51 +503,41 @@ function buildOperationLookup(api) {
|
|
|
476
503
|
}
|
|
477
504
|
|
|
478
505
|
function mergeFlowsWithOpenApi(api, flowsDoc) {
|
|
479
|
-
const { entries, lookupByKey
|
|
506
|
+
const { entries, lookupByKey } = buildOperationLookup(api);
|
|
480
507
|
|
|
508
|
+
const existingByOperationId = new Map();
|
|
481
509
|
const existingByKey = new Map();
|
|
482
510
|
for (const entry of flowsDoc.operations) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
511
|
+
if (entry && entry.operationId) {
|
|
512
|
+
existingByOperationId.set(entry.operationId, entry);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const legacyKey = entry && entry.key ? entry.key : null;
|
|
516
|
+
if (legacyKey) {
|
|
517
|
+
existingByKey.set(legacyKey, entry);
|
|
486
518
|
}
|
|
487
519
|
}
|
|
488
520
|
|
|
489
521
|
const mergedOperations = [];
|
|
490
522
|
|
|
491
523
|
for (const op of entries) {
|
|
492
|
-
const existing =
|
|
493
|
-
|
|
494
|
-
mergedOperations.push({
|
|
495
|
-
...existing,
|
|
496
|
-
key: op.key,
|
|
497
|
-
operationId: op.operationId,
|
|
498
|
-
method: op.method,
|
|
499
|
-
path: op.path,
|
|
500
|
-
missing_in_openapi: false,
|
|
501
|
-
});
|
|
502
|
-
} else {
|
|
503
|
-
mergedOperations.push({
|
|
504
|
-
key: op.key,
|
|
505
|
-
operationId: op.operationId,
|
|
506
|
-
method: op.method,
|
|
507
|
-
path: op.path,
|
|
508
|
-
"x-openapi-flow": null,
|
|
509
|
-
missing_in_openapi: false,
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
}
|
|
524
|
+
const existing = (op.resolvedOperationId && existingByOperationId.get(op.resolvedOperationId))
|
|
525
|
+
|| existingByKey.get(op.key);
|
|
513
526
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
527
|
+
const openApiFlow =
|
|
528
|
+
op.operation && typeof op.operation["x-openapi-flow"] === "object"
|
|
529
|
+
? op.operation["x-openapi-flow"]
|
|
530
|
+
: null;
|
|
517
531
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
532
|
+
const sidecarFlow =
|
|
533
|
+
existing && typeof existing["x-openapi-flow"] === "object"
|
|
534
|
+
? existing["x-openapi-flow"]
|
|
535
|
+
: null;
|
|
536
|
+
|
|
537
|
+
mergedOperations.push({
|
|
538
|
+
operationId: op.resolvedOperationId,
|
|
539
|
+
"x-openapi-flow": sidecarFlow || openApiFlow || buildFlowTemplate(op.resolvedOperationId),
|
|
540
|
+
});
|
|
524
541
|
}
|
|
525
542
|
|
|
526
543
|
return {
|
|
@@ -535,12 +552,12 @@ function applyFlowsToOpenApi(api, flowsDoc) {
|
|
|
535
552
|
const { lookupByKey, lookupByOperationId } = buildOperationLookup(api);
|
|
536
553
|
|
|
537
554
|
for (const flowEntry of flowsDoc.operations || []) {
|
|
538
|
-
if (!flowEntry
|
|
555
|
+
if (!flowEntry) {
|
|
539
556
|
continue;
|
|
540
557
|
}
|
|
541
558
|
|
|
542
559
|
const flowValue = flowEntry["x-openapi-flow"];
|
|
543
|
-
if (!flowValue || typeof flowValue !== "object") {
|
|
560
|
+
if (!flowValue || typeof flowValue !== "object" || Object.keys(flowValue).length === 0) {
|
|
544
561
|
continue;
|
|
545
562
|
}
|
|
546
563
|
|
|
@@ -595,19 +612,12 @@ function runInit(parsed) {
|
|
|
595
612
|
|
|
596
613
|
const mergedFlows = mergeFlowsWithOpenApi(api, flowsDoc);
|
|
597
614
|
writeFlowsFile(flowsPath, mergedFlows);
|
|
598
|
-
const
|
|
599
|
-
saveOpenApi(targetOpenApiFile, api);
|
|
600
|
-
|
|
601
|
-
const trackedCount = mergedFlows.operations.filter((entry) => !entry.missing_in_openapi).length;
|
|
602
|
-
const orphanCount = mergedFlows.operations.filter((entry) => entry.missing_in_openapi).length;
|
|
615
|
+
const trackedCount = mergedFlows.operations.length;
|
|
603
616
|
|
|
604
617
|
console.log(`Using existing OpenAPI file: ${targetOpenApiFile}`);
|
|
605
618
|
console.log(`Flows sidecar synced: ${flowsPath}`);
|
|
606
619
|
console.log(`Tracked operations: ${trackedCount}`);
|
|
607
|
-
|
|
608
|
-
console.log(`Orphan flow entries kept in sidecar: ${orphanCount}`);
|
|
609
|
-
}
|
|
610
|
-
console.log(`Applied x-openapi-flow entries to OpenAPI: ${appliedCount}`);
|
|
620
|
+
console.log("OpenAPI source unchanged. Edit the sidecar and run apply to generate the full spec.");
|
|
611
621
|
|
|
612
622
|
console.log(`Validate now: x-openapi-flow validate ${targetOpenApiFile}`);
|
|
613
623
|
return 0;
|
|
@@ -699,8 +709,11 @@ function runDoctor(parsed) {
|
|
|
699
709
|
}
|
|
700
710
|
|
|
701
711
|
function buildMermaidGraph(filePath) {
|
|
702
|
-
const
|
|
703
|
-
|
|
712
|
+
const flows = extractFlowsForGraph(filePath);
|
|
713
|
+
if (flows.length === 0) {
|
|
714
|
+
throw new Error("No x-openapi-flow definitions found in OpenAPI or sidecar file");
|
|
715
|
+
}
|
|
716
|
+
|
|
704
717
|
const lines = ["stateDiagram-v2"];
|
|
705
718
|
const nodes = new Set();
|
|
706
719
|
const edges = [];
|
|
@@ -760,6 +773,51 @@ function buildMermaidGraph(filePath) {
|
|
|
760
773
|
};
|
|
761
774
|
}
|
|
762
775
|
|
|
776
|
+
function extractFlowsForGraph(filePath) {
|
|
777
|
+
let flows = [];
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const api = loadApi(filePath);
|
|
781
|
+
flows = extractFlows(api);
|
|
782
|
+
} catch (_err) {
|
|
783
|
+
flows = [];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (flows.length > 0) {
|
|
787
|
+
return flows;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
791
|
+
const parsed = yaml.load(content);
|
|
792
|
+
|
|
793
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.operations)) {
|
|
794
|
+
return [];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const sidecarFlows = [];
|
|
798
|
+
for (const operationEntry of parsed.operations) {
|
|
799
|
+
if (!operationEntry || typeof operationEntry !== "object") {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const flow = operationEntry["x-openapi-flow"];
|
|
804
|
+
if (!flow || typeof flow !== "object") {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (!flow.current_state) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
sidecarFlows.push({
|
|
813
|
+
endpoint: operationEntry.operationId || operationEntry.key || "sidecar-operation",
|
|
814
|
+
flow,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return sidecarFlows;
|
|
819
|
+
}
|
|
820
|
+
|
|
763
821
|
function runGraph(parsed) {
|
|
764
822
|
try {
|
|
765
823
|
const graphResult = buildMermaidGraph(parsed.filePath);
|
|
@@ -160,6 +160,9 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
160
160
|
.xof-graph-title { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
|
|
161
161
|
.xof-edge { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; }
|
|
162
162
|
.xof-empty { opacity: 0.85; font-style: italic; }
|
|
163
|
+
.xof-overview { margin: 10px 0 16px; }
|
|
164
|
+
.xof-overview img { width: 100%; max-width: 760px; border: 1px solid rgba(255,255,255,0.3); border-radius: 6px; background: #fff; }
|
|
165
|
+
.xof-overview-code { margin-top: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 11px; opacity: 0.9; white-space: pre-wrap; }
|
|
163
166
|
`;
|
|
164
167
|
|
|
165
168
|
document.head.appendChild(style);
|
|
@@ -215,6 +218,167 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
215
218
|
`;
|
|
216
219
|
}
|
|
217
220
|
|
|
221
|
+
function getSpecFromUi() {
|
|
222
|
+
try {
|
|
223
|
+
if (!window.ui || !window.ui.specSelectors || !window.ui.specSelectors.specJson) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const spec = window.ui.specSelectors.specJson();
|
|
228
|
+
return spec && spec.toJS ? spec.toJS() : spec;
|
|
229
|
+
} catch (_error) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function extractFlowsFromSpec(spec) {
|
|
235
|
+
const result = [];
|
|
236
|
+
const paths = (spec && spec.paths) || {};
|
|
237
|
+
const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
238
|
+
|
|
239
|
+
Object.entries(paths).forEach(([pathKey, pathItem]) => {
|
|
240
|
+
if (!pathItem || typeof pathItem !== 'object') return;
|
|
241
|
+
|
|
242
|
+
methods.forEach((method) => {
|
|
243
|
+
const operation = pathItem[method];
|
|
244
|
+
if (!operation || typeof operation !== 'object') return;
|
|
245
|
+
|
|
246
|
+
const flow = operation['x-openapi-flow'];
|
|
247
|
+
if (!flow || typeof flow !== 'object' || !flow.current_state) return;
|
|
248
|
+
|
|
249
|
+
result.push({
|
|
250
|
+
operationId: operation.operationId || `${method}_${pathKey}`,
|
|
251
|
+
flow,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildOverviewMermaid(flows) {
|
|
260
|
+
const lines = ['stateDiagram-v2'];
|
|
261
|
+
const states = new Set();
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
|
|
264
|
+
flows.forEach(({ flow }) => {
|
|
265
|
+
const current = flow.current_state;
|
|
266
|
+
if (!current) return;
|
|
267
|
+
|
|
268
|
+
states.add(current);
|
|
269
|
+
const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
|
|
270
|
+
transitions.forEach((transition) => {
|
|
271
|
+
const target = transition.target_state;
|
|
272
|
+
if (!target) return;
|
|
273
|
+
states.add(target);
|
|
274
|
+
|
|
275
|
+
const labelParts = [];
|
|
276
|
+
if (transition.next_operation_id) {
|
|
277
|
+
labelParts.push(`next:${text(transition.next_operation_id)}`);
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(transition.prerequisite_operation_ids) && transition.prerequisite_operation_ids.length) {
|
|
280
|
+
labelParts.push(`requires:${transition.prerequisite_operation_ids.join(',')}`);
|
|
281
|
+
}
|
|
282
|
+
const label = labelParts.join(' | ');
|
|
283
|
+
const key = `${current}::${target}::${label}`;
|
|
284
|
+
if (seen.has(key)) return;
|
|
285
|
+
seen.add(key);
|
|
286
|
+
lines.push(` ${current} --> ${target}${label ? `: ${label}` : ''}`);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
Array.from(states)
|
|
291
|
+
.sort()
|
|
292
|
+
.forEach((state) => {
|
|
293
|
+
lines.splice(1, 0, ` state ${state}`);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return lines.join('\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let mermaidLoaderPromise = null;
|
|
300
|
+
function ensureMermaid() {
|
|
301
|
+
if (window.mermaid) {
|
|
302
|
+
return Promise.resolve(window.mermaid);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (mermaidLoaderPromise) {
|
|
306
|
+
return mermaidLoaderPromise;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
mermaidLoaderPromise = new Promise((resolve, reject) => {
|
|
310
|
+
const script = document.createElement('script');
|
|
311
|
+
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
|
|
312
|
+
script.async = true;
|
|
313
|
+
script.onload = () => {
|
|
314
|
+
if (window.mermaid) {
|
|
315
|
+
window.mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
|
|
316
|
+
resolve(window.mermaid);
|
|
317
|
+
} else {
|
|
318
|
+
reject(new Error('Mermaid library not available after load'));
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
script.onerror = () => reject(new Error('Could not load Mermaid library'));
|
|
322
|
+
document.head.appendChild(script);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return mermaidLoaderPromise;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function svgToDataUri(svg) {
|
|
329
|
+
const encoded = window.btoa(unescape(encodeURIComponent(svg)));
|
|
330
|
+
return `data:image/svg+xml;base64,${encoded}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let overviewRenderedHash = null;
|
|
334
|
+
async function renderOverview() {
|
|
335
|
+
const spec = getSpecFromUi();
|
|
336
|
+
const flows = extractFlowsFromSpec(spec);
|
|
337
|
+
if (!flows.length) return;
|
|
338
|
+
|
|
339
|
+
const mermaid = buildOverviewMermaid(flows);
|
|
340
|
+
const currentHash = `${flows.length}:${mermaid}`;
|
|
341
|
+
if (overviewRenderedHash === currentHash) return;
|
|
342
|
+
|
|
343
|
+
const infoContainer = document.querySelector('.swagger-ui .information-container');
|
|
344
|
+
if (!infoContainer) return;
|
|
345
|
+
|
|
346
|
+
let holder = document.getElementById('xof-overview-holder');
|
|
347
|
+
if (!holder) {
|
|
348
|
+
holder = document.createElement('div');
|
|
349
|
+
holder.id = 'xof-overview-holder';
|
|
350
|
+
holder.className = 'xof-overview xof-card';
|
|
351
|
+
infoContainer.parentNode.insertBefore(holder, infoContainer.nextSibling);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
holder.innerHTML = '<div class="xof-title">x-openapi-flow — Flow Overview</div><div class="xof-empty">Rendering Mermaid graph...</div>';
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const mermaidLib = await ensureMermaid();
|
|
358
|
+
const renderId = `xof-overview-${Date.now()}`;
|
|
359
|
+
const renderResult = await mermaidLib.render(renderId, mermaid);
|
|
360
|
+
const svg = renderResult && renderResult.svg ? renderResult.svg : renderResult;
|
|
361
|
+
const dataUri = svgToDataUri(svg);
|
|
362
|
+
|
|
363
|
+
holder.innerHTML = `
|
|
364
|
+
<div class="xof-title">x-openapi-flow — Flow Overview</div>
|
|
365
|
+
<img src="${dataUri}" alt="x-openapi-flow overview graph" />
|
|
366
|
+
<details style="margin-top:8px;">
|
|
367
|
+
<summary style="cursor:pointer;">Mermaid source</summary>
|
|
368
|
+
<div class="xof-overview-code">${mermaid.replace(/</g, '<').replace(/>/g, '>')}</div>
|
|
369
|
+
</details>
|
|
370
|
+
`;
|
|
371
|
+
} catch (_error) {
|
|
372
|
+
holder.innerHTML = `
|
|
373
|
+
<div class="xof-title">x-openapi-flow — Flow Overview</div>
|
|
374
|
+
<div class="xof-empty">Could not render Mermaid image in this environment.</div>
|
|
375
|
+
<div class="xof-overview-code">${mermaid.replace(/</g, '<').replace(/>/g, '>')}</div>
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
overviewRenderedHash = currentHash;
|
|
380
|
+
}
|
|
381
|
+
|
|
218
382
|
function findXOpenApiFlowValueCell(opblock) {
|
|
219
383
|
const rows = opblock.querySelectorAll('tr');
|
|
220
384
|
for (const row of rows) {
|
|
@@ -249,6 +413,7 @@ window.XOpenApiFlowPlugin = function () {
|
|
|
249
413
|
injectStyles();
|
|
250
414
|
const opblocks = document.querySelectorAll('.opblock');
|
|
251
415
|
opblocks.forEach((opblock) => enhanceOperation(opblock));
|
|
416
|
+
renderOverview();
|
|
252
417
|
}
|
|
253
418
|
|
|
254
419
|
const observer = new MutationObserver(() => {
|