x-openapi-flow 1.4.4 → 1.5.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 +247 -6
- package/adapters/flow-output-adapters.js +2 -0
- package/adapters/tests/flow-test-adapter.js +254 -0
- package/bin/x-openapi-flow.js +942 -15
- package/lib/error-codes.js +289 -0
- package/lib/openapi-state-machine-adapter.js +151 -0
- package/lib/runtime-guard/core.js +176 -0
- package/lib/runtime-guard/errors.js +85 -0
- package/lib/runtime-guard/express.js +70 -0
- package/lib/runtime-guard/fastify.js +65 -0
- package/lib/runtime-guard/index.js +15 -0
- package/lib/runtime-guard/model.js +85 -0
- package/lib/runtime-guard.js +3 -0
- package/lib/state-machine-engine.js +208 -0
- package/lib/validator.js +378 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<!-- Auto-generated from /README.md via scripts/sync-package-readme.js. Do not edit directly. -->
|
|
2
2
|
|
|
3
|
+
# OpenAPI describes APIs. x-openapi-flow turns them into executable workflows — for developers and AI agents.
|
|
4
|
+
|
|
5
|
+
## Define your API workflows in openapi.x.json and execute them without writing custom clients or orchestration logic.
|
|
6
|
+
|
|
3
7
|

|
|
4
8
|
|
|
5
9
|
[](https://www.npmjs.com/package/x-openapi-flow)
|
|
@@ -11,21 +15,69 @@
|
|
|
11
15
|
[](https://github.com/tiago-marques/x-openapi-flow/issues)
|
|
12
16
|
[](https://github.com/tiago-marques/x-openapi-flow/commits/main)
|
|
13
17
|

|
|
14
|
-
> 🚀
|
|
15
|
-
|
|
16
|
-
# OpenAPI describes APIs. x-openapi-flow describes their workflows — for developers and AI.
|
|
18
|
+
> 🚀 2,100+ downloads in the first 3 weeks!
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## ⚡ Get started in seconds
|
|
21
|
+
> npx x-openapi-flow init
|
|
19
22
|
|
|
23
|
+
### This generates an openapi.x.json file where you can declaratively define how your API should be executed — not just described.
|
|
20
24
|
|
|
21
25
|
> See your API lifecycle come alive from your OpenAPI spec, with one simple command
|
|
22
26
|
|
|
23
27
|
> Validate, document, and generate flow-aware SDKs automatically.
|
|
24
28
|
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
## What is this?
|
|
32
|
+
|
|
33
|
+
### x-openapi-flow extends your OpenAPI specification with a workflow layer.
|
|
34
|
+
|
|
35
|
+
> openapi.json → describes your API
|
|
36
|
+
> openapi.x.json → describes how to use it (flows)
|
|
37
|
+
|
|
38
|
+
### Instead of writing imperative code to orchestrate API calls, you define workflows declaratively and run them anywhere.
|
|
39
|
+
|
|
25
40
|
`x-openapi-flow` adds a **declarative state machine** to your OpenAPI spec.
|
|
26
41
|
|
|
27
42
|
Model resource lifecycles, enforce valid transitions, and generate flow-aware artifacts for documentation, SDKs, and automation.
|
|
28
43
|
|
|
44
|
+
## 🚀 Example
|
|
45
|
+
|
|
46
|
+
> Define stateful workflows and lifecycle transitions directly inside your OpenAPI operations:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"operationId": "createOrder",
|
|
51
|
+
"x-openapi-flow": {
|
|
52
|
+
"id": "create-order",
|
|
53
|
+
"current_state": "created",
|
|
54
|
+
"description": "Creates an order and starts the lifecycle",
|
|
55
|
+
"transitions": [
|
|
56
|
+
{
|
|
57
|
+
"trigger_type": "synchronous",
|
|
58
|
+
"condition": "Payment is confirmed",
|
|
59
|
+
"target_state": "paid",
|
|
60
|
+
"next_operation_id": "payOrder",
|
|
61
|
+
"prerequisite_operation_ids": ["createOrder"],
|
|
62
|
+
"propagated_field_refs": [
|
|
63
|
+
"createOrder:response.201.body.order_id"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This flow defines an order lifecycle directly inside your OpenAPI:
|
|
72
|
+
|
|
73
|
+
* Starts in the `created` state
|
|
74
|
+
* Transitions to `paid` when payment is confirmed
|
|
75
|
+
* Supports both synchronous and polling-based transitions
|
|
76
|
+
* Propagates data between operations automatically
|
|
77
|
+
|
|
78
|
+
Instead of manually orchestrating API calls, the workflow is fully described alongside your API specification.
|
|
79
|
+
|
|
80
|
+
|
|
29
81
|
## Why This Exists
|
|
30
82
|
|
|
31
83
|
Building APIs is cheap. Building **complex, multi-step APIs that teams actually use correctly** is hard.
|
|
@@ -52,7 +104,37 @@ Turn your OpenAPI spec into a single source of truth for API behavior:
|
|
|
52
104
|
- Export [Postman](#postman-demo) and [Insomnia](#insomnia-demo) collections organized by lifecycle
|
|
53
105
|
- Create [AI-ready API contracts](https://github.com/tiago-marques/x-openapi-flow/blob/main/docs/wiki/engineering/AI-Sidecar-Authoring.md) for agentic integrations
|
|
54
106
|
|
|
55
|
-
## Quick Start
|
|
107
|
+
## Quick Start (without OpenAPI file)
|
|
108
|
+
|
|
109
|
+
Fastest way to see value (guided scaffold):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx x-openapi-flow quickstart
|
|
113
|
+
cd x-openapi-flow-quickstart
|
|
114
|
+
npm install
|
|
115
|
+
npm start
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Optional runtime:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx x-openapi-flow quickstart --runtime fastify
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then run:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
curl -s -X POST http://localhost:3110/orders
|
|
128
|
+
curl -i -X POST http://localhost:3110/orders/<id>/ship
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Expected: `409 INVALID_STATE_TRANSITION`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
### If you already have an OpenAPI file, use the sidecar workflow:
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
56
138
|
|
|
57
139
|
Initialize flow support in your project:
|
|
58
140
|
|
|
@@ -76,6 +158,12 @@ This will:
|
|
|
76
158
|
|
|
77
159
|
💡 Tip: run this in CI to enforce API workflow correctness
|
|
78
160
|
|
|
161
|
+
### Less Verbose DSL for Large Flows
|
|
162
|
+
|
|
163
|
+
For larger APIs, you can define flow rules by resource (with shared transitions/defaults) and reduce duplication in sidecar files.
|
|
164
|
+
|
|
165
|
+
See: [Sidecar Contract](https://github.com/tiago-marques/x-openapi-flow/blob/main/docs/wiki/reference/Sidecar-Contract.md)
|
|
166
|
+
|
|
79
167
|
<a id="mermaid-example"></a>
|
|
80
168
|
### Real Lifecycle Example
|
|
81
169
|
|
|
@@ -124,6 +212,147 @@ await payment.capture();
|
|
|
124
212
|
|
|
125
213
|
> This SDK guides developers through valid transition paths, following patterns used by market leaders to ensure safe and intuitive integrations.
|
|
126
214
|
|
|
215
|
+
## Runtime Enforcement (Express + Fastify)
|
|
216
|
+
|
|
217
|
+
CI validation is important, but production safety needs request-time enforcement.
|
|
218
|
+
|
|
219
|
+
`x-openapi-flow` now includes an official runtime guard for Node.js that can block invalid state transitions during request handling.
|
|
220
|
+
|
|
221
|
+
- Works with **Express** and **Fastify**
|
|
222
|
+
- Resolves operations by `operationId` (when available) or by method + route
|
|
223
|
+
- Reads current resource state using your own persistence callback
|
|
224
|
+
- Blocks invalid transitions with explicit `409` error payloads
|
|
225
|
+
|
|
226
|
+
Install and use directly in your API server:
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
const {
|
|
230
|
+
createExpressFlowGuard,
|
|
231
|
+
createFastifyFlowGuard,
|
|
232
|
+
} = require("x-openapi-flow/lib/runtime-guard");
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Express example:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
const express = require("express");
|
|
239
|
+
const { createExpressFlowGuard } = require("x-openapi-flow/lib/runtime-guard");
|
|
240
|
+
const openapi = require("./openapi.flow.json");
|
|
241
|
+
|
|
242
|
+
const app = express();
|
|
243
|
+
|
|
244
|
+
app.use(
|
|
245
|
+
createExpressFlowGuard({
|
|
246
|
+
openapi,
|
|
247
|
+
async getCurrentState({ resourceId }) {
|
|
248
|
+
if (!resourceId) return null;
|
|
249
|
+
return paymentStore.getState(resourceId); // your DB/service lookup
|
|
250
|
+
},
|
|
251
|
+
resolveResourceId: ({ params }) => params.id || null,
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Fastify example:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
const fastify = require("fastify")();
|
|
260
|
+
const { createFastifyFlowGuard } = require("x-openapi-flow/lib/runtime-guard");
|
|
261
|
+
const openapi = require("./openapi.flow.json");
|
|
262
|
+
|
|
263
|
+
fastify.addHook(
|
|
264
|
+
"preHandler",
|
|
265
|
+
createFastifyFlowGuard({
|
|
266
|
+
openapi,
|
|
267
|
+
async getCurrentState({ resourceId }) {
|
|
268
|
+
if (!resourceId) return null;
|
|
269
|
+
return paymentStore.getState(resourceId);
|
|
270
|
+
},
|
|
271
|
+
resolveResourceId: ({ params }) => params.id || null,
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Error payload for blocked transition:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"error": {
|
|
281
|
+
"code": "INVALID_STATE_TRANSITION",
|
|
282
|
+
"message": "Blocked invalid transition for operation 'capturePayment'. Current state 'CREATED' cannot transition to this operation.",
|
|
283
|
+
"operation_id": "capturePayment",
|
|
284
|
+
"current_state": "CREATED",
|
|
285
|
+
"allowed_from_states": ["AUTHORIZED"],
|
|
286
|
+
"resource_id": "pay_123"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
More details: [Runtime Guard](https://github.com/tiago-marques/x-openapi-flow/blob/main/docs/wiki/reference/Runtime-Guard.md)
|
|
292
|
+
|
|
293
|
+
### 5-Minute Demo: Real Runtime Block (E-commerce Orders)
|
|
294
|
+
|
|
295
|
+
Want to see the value immediately? Use the official minimal demo:
|
|
296
|
+
|
|
297
|
+
- [example/runtime-guard/minimal-order/README.md](https://github.com/tiago-marques/x-openapi-flow/blob/main/example/runtime-guard/minimal-order/README.md)
|
|
298
|
+
|
|
299
|
+
Run in under 5 minutes:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
cd example/runtime-guard/minimal-order
|
|
303
|
+
npm install
|
|
304
|
+
npm start
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Create an order, then try to ship before payment (must return `409 INVALID_STATE_TRANSITION`):
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
curl -s -X POST http://localhost:3110/orders
|
|
311
|
+
curl -i -X POST http://localhost:3110/orders/<id>/ship
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
HTTPie equivalent:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
http POST :3110/orders
|
|
318
|
+
http -v POST :3110/orders/<id>/ship
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Programmatic State Machine Engine
|
|
322
|
+
|
|
323
|
+
Use a reusable deterministic engine independently of CLI and OpenAPI parsing:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
const { createStateMachineEngine } = require("x-openapi-flow/lib/state-machine-engine");
|
|
327
|
+
|
|
328
|
+
const engine = createStateMachineEngine({
|
|
329
|
+
transitions: [
|
|
330
|
+
{ from: "CREATED", action: "confirm", to: "CONFIRMED" },
|
|
331
|
+
{ from: "CONFIRMED", action: "ship", to: "SHIPPED" },
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
engine.canTransition("CREATED", "confirm");
|
|
336
|
+
engine.getNextState("CREATED", "confirm");
|
|
337
|
+
engine.validateFlow({ startState: "CREATED", actions: ["confirm", "ship"] });
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
More details: [State Machine Engine](https://github.com/tiago-marques/x-openapi-flow/blob/main/docs/wiki/reference/State-Machine-Engine.md)
|
|
341
|
+
|
|
342
|
+
### OpenAPI to Engine Adapter
|
|
343
|
+
|
|
344
|
+
Convert `x-openapi-flow` metadata to a pure engine definition:
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
const { createStateMachineAdapterModel } = require("x-openapi-flow/lib/openapi-state-machine-adapter");
|
|
348
|
+
const { createStateMachineEngine } = require("x-openapi-flow/lib/state-machine-engine");
|
|
349
|
+
|
|
350
|
+
const model = createStateMachineAdapterModel({ openapiPath: "./openapi.flow.yaml" });
|
|
351
|
+
const engine = createStateMachineEngine(model.definition);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
More details: [OpenAPI State Machine Adapter](https://github.com/tiago-marques/x-openapi-flow/blob/main/docs/wiki/reference/OpenAPI-State-Machine-Adapter.md)
|
|
355
|
+
|
|
127
356
|
## Who Benefits Most
|
|
128
357
|
|
|
129
358
|
x-openapi-flow is ideal for teams and organizations that want **clear, enforceable API workflows**:
|
|
@@ -227,6 +456,8 @@ npx x-openapi-flow version # show version
|
|
|
227
456
|
npx x-openapi-flow doctor [--config path] # check setup and config
|
|
228
457
|
|
|
229
458
|
npx x-openapi-flow completion [bash|zsh] # enable shell autocompletion
|
|
459
|
+
|
|
460
|
+
npx x-openapi-flow quickstart [--dir path] [--runtime express|fastify] [--force] # scaffold runnable onboarding project
|
|
230
461
|
```
|
|
231
462
|
|
|
232
463
|
### Workflow Management
|
|
@@ -239,7 +470,7 @@ npx x-openapi-flow init [--flows path] [--force] [--dry-run]
|
|
|
239
470
|
npx x-openapi-flow apply [openapi-file] [--flows path] [--out path]
|
|
240
471
|
|
|
241
472
|
# validate transitions
|
|
242
|
-
npx x-openapi-flow validate <openapi-file> [--profile core|relaxed|strict] [--strict-quality]
|
|
473
|
+
npx x-openapi-flow validate <openapi-file> [--profile core|relaxed|strict] [--strict-quality] [--semantic]
|
|
243
474
|
```
|
|
244
475
|
|
|
245
476
|
### Visualization & Documentation
|
|
@@ -262,6 +493,16 @@ npx x-openapi-flow export-doc-flows [openapi-file] [--output path] [--format mar
|
|
|
262
493
|
npx x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]
|
|
263
494
|
```
|
|
264
495
|
|
|
496
|
+
### Test Generation
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
# generate executable flow tests (happy path + invalid transitions)
|
|
500
|
+
npx x-openapi-flow generate-flow-tests [openapi-file] [--format jest|vitest|postman] [--output path]
|
|
501
|
+
|
|
502
|
+
# postman/newman-oriented collection with flow scripts
|
|
503
|
+
npx x-openapi-flow generate-flow-tests [openapi-file] --format postman [--output path] [--with-scripts]
|
|
504
|
+
```
|
|
505
|
+
|
|
265
506
|
Full details:
|
|
266
507
|
|
|
267
508
|
- [CLI-Reference.md](https://github.com/tiago-marques/x-openapi-flow/blob/main/docs/wiki/reference/CLI-Reference.md)
|
|
@@ -6,10 +6,12 @@ const { exportDocFlows } = require("./docs/doc-adapter");
|
|
|
6
6
|
const { generatePostmanCollection } = require("./collections/postman-adapter");
|
|
7
7
|
const { generateInsomniaWorkspace } = require("./collections/insomnia-adapter");
|
|
8
8
|
const { generateRedocPackage } = require("./ui/redoc-adapter");
|
|
9
|
+
const { generateFlowTests } = require("./tests/flow-test-adapter");
|
|
9
10
|
|
|
10
11
|
module.exports = {
|
|
11
12
|
exportDocFlows,
|
|
12
13
|
generatePostmanCollection,
|
|
13
14
|
generateInsomniaWorkspace,
|
|
14
15
|
generateRedocPackage,
|
|
16
|
+
generateFlowTests,
|
|
15
17
|
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { loadApi } = require("../../lib/validator");
|
|
6
|
+
const { createStateMachineAdapterModel } = require("../../lib/openapi-state-machine-adapter");
|
|
7
|
+
const { createStateMachineEngine } = require("../../lib/state-machine-engine");
|
|
8
|
+
const { generatePostmanCollection } = require("../collections/postman-adapter");
|
|
9
|
+
|
|
10
|
+
function getFormat(options) {
|
|
11
|
+
const format = String(options.format || "jest").toLowerCase();
|
|
12
|
+
if (!["jest", "vitest", "postman"].includes(format)) {
|
|
13
|
+
throw new Error(`Unsupported test format '${format}'. Use 'jest', 'vitest', or 'postman'.`);
|
|
14
|
+
}
|
|
15
|
+
return format;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getDefaultOutputPath(format) {
|
|
19
|
+
if (format === "postman") {
|
|
20
|
+
return path.resolve(process.cwd(), "x-openapi-flow.flow-tests.postman_collection.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (format === "vitest") {
|
|
24
|
+
return path.resolve(process.cwd(), "x-openapi-flow.flow.generated.vitest.test.js");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return path.resolve(process.cwd(), "x-openapi-flow.flow.generated.jest.test.js");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildHappyPaths(engine) {
|
|
31
|
+
const transitions = engine.getTransitions();
|
|
32
|
+
const outgoing = new Map();
|
|
33
|
+
const indegree = new Map();
|
|
34
|
+
const states = engine.getStates();
|
|
35
|
+
|
|
36
|
+
for (const state of states) {
|
|
37
|
+
outgoing.set(state, []);
|
|
38
|
+
indegree.set(state, 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const transition of transitions) {
|
|
42
|
+
if (!outgoing.has(transition.from)) {
|
|
43
|
+
outgoing.set(transition.from, []);
|
|
44
|
+
}
|
|
45
|
+
outgoing.get(transition.from).push(transition);
|
|
46
|
+
indegree.set(transition.to, (indegree.get(transition.to) || 0) + 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const transitionList of outgoing.values()) {
|
|
50
|
+
transitionList.sort((left, right) => left.action.localeCompare(right.action));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const starts = states.filter((state) => (indegree.get(state) || 0) === 0);
|
|
54
|
+
const roots = starts.length > 0 ? starts : states;
|
|
55
|
+
const maxDepth = Math.max(3, states.length + 2);
|
|
56
|
+
const maxPaths = 50;
|
|
57
|
+
const paths = [];
|
|
58
|
+
|
|
59
|
+
function walk(currentState, actions, visitedKeys, depth) {
|
|
60
|
+
if (paths.length >= maxPaths) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nextTransitions = outgoing.get(currentState) || [];
|
|
65
|
+
if (nextTransitions.length === 0 || depth >= maxDepth) {
|
|
66
|
+
if (actions.length > 0) {
|
|
67
|
+
paths.push({
|
|
68
|
+
startState: actions[0].from,
|
|
69
|
+
actions: actions.map((entry) => entry.action),
|
|
70
|
+
finalState: currentState,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const transition of nextTransitions) {
|
|
77
|
+
const visitKey = `${transition.from}::${transition.action}::${transition.to}`;
|
|
78
|
+
if (visitedKeys.has(visitKey)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const nextVisited = new Set(visitedKeys);
|
|
83
|
+
nextVisited.add(visitKey);
|
|
84
|
+
|
|
85
|
+
walk(
|
|
86
|
+
transition.to,
|
|
87
|
+
actions.concat([{ from: transition.from, action: transition.action }]),
|
|
88
|
+
nextVisited,
|
|
89
|
+
depth + 1
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const root of roots) {
|
|
95
|
+
walk(root, [], new Set(), 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const dedup = new Map();
|
|
99
|
+
for (const entry of paths) {
|
|
100
|
+
const key = `${entry.startState}::${entry.actions.join(",")}`;
|
|
101
|
+
if (!dedup.has(key)) {
|
|
102
|
+
dedup.set(key, entry);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [...dedup.values()];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildInvalidCases(engine) {
|
|
110
|
+
const transitions = engine.getTransitions();
|
|
111
|
+
const states = engine.getStates();
|
|
112
|
+
const knownActions = [...new Set(transitions.map((transition) => transition.action))].sort();
|
|
113
|
+
const cases = [];
|
|
114
|
+
|
|
115
|
+
for (const state of states) {
|
|
116
|
+
const available = engine.getAvailableActions(state);
|
|
117
|
+
const disallowedAction = knownActions.find((action) => !available.includes(action));
|
|
118
|
+
|
|
119
|
+
if (disallowedAction) {
|
|
120
|
+
cases.push({
|
|
121
|
+
state,
|
|
122
|
+
action: disallowedAction,
|
|
123
|
+
availableActions: available,
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
cases.push({
|
|
129
|
+
state,
|
|
130
|
+
action: "__invalid_action__",
|
|
131
|
+
availableActions: available,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return cases;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildJestOrVitestContent({ format, sourcePath, definition, happyPaths, invalidCases }) {
|
|
139
|
+
const frameworkPrelude = format === "vitest"
|
|
140
|
+
? "const { describe, test, expect } = require(\"vitest\");\n"
|
|
141
|
+
: "";
|
|
142
|
+
|
|
143
|
+
const title = format === "vitest" ? "Vitest" : "Jest";
|
|
144
|
+
|
|
145
|
+
return `"use strict";
|
|
146
|
+
|
|
147
|
+
${frameworkPrelude}const { createStateMachineEngine } = require("x-openapi-flow/lib/state-machine-engine");
|
|
148
|
+
|
|
149
|
+
const definition = ${JSON.stringify(definition, null, 2)};
|
|
150
|
+
const happyPaths = ${JSON.stringify(happyPaths, null, 2)};
|
|
151
|
+
const invalidCases = ${JSON.stringify(invalidCases, null, 2)};
|
|
152
|
+
|
|
153
|
+
describe("x-openapi-flow generated tests (${title})", () => {
|
|
154
|
+
const engine = createStateMachineEngine(definition);
|
|
155
|
+
|
|
156
|
+
test("has transitions in definition", () => {
|
|
157
|
+
expect(Array.isArray(definition.transitions)).toBe(true);
|
|
158
|
+
expect(definition.transitions.length).toBeGreaterThan(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("happy paths", () => {
|
|
162
|
+
for (const pathCase of happyPaths) {
|
|
163
|
+
test(
|
|
164
|
+
\`valid flow: \${pathCase.startState} -> \${pathCase.actions.join(" -> ")}\`,
|
|
165
|
+
() => {
|
|
166
|
+
const result = engine.validateFlow({
|
|
167
|
+
startState: pathCase.startState,
|
|
168
|
+
actions: pathCase.actions,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.ok).toBe(true);
|
|
172
|
+
expect(result.finalState).toBe(pathCase.finalState);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("invalid transitions", () => {
|
|
179
|
+
for (const invalidCase of invalidCases) {
|
|
180
|
+
test(
|
|
181
|
+
\`blocks invalid action \${invalidCase.action} from state \${invalidCase.state}\`,
|
|
182
|
+
() => {
|
|
183
|
+
expect(engine.canTransition(invalidCase.state, invalidCase.action)).toBe(false);
|
|
184
|
+
|
|
185
|
+
const result = engine.validateFlow({
|
|
186
|
+
startState: invalidCase.state,
|
|
187
|
+
actions: [invalidCase.action],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(result.ok).toBe(false);
|
|
191
|
+
expect(result.error.code).toBe("INVALID_TRANSITION");
|
|
192
|
+
expect(Array.isArray(result.error.availableActions)).toBe(true);
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Generated from: ${sourcePath}
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function generateFlowTests(options) {
|
|
204
|
+
const format = getFormat(options || {});
|
|
205
|
+
const apiPath = path.resolve(options.apiPath);
|
|
206
|
+
const outputPath = path.resolve(options.outputPath || getDefaultOutputPath(format));
|
|
207
|
+
|
|
208
|
+
if (format === "postman") {
|
|
209
|
+
const postman = generatePostmanCollection({
|
|
210
|
+
apiPath,
|
|
211
|
+
outputPath,
|
|
212
|
+
withScripts: options.withScripts !== false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
format,
|
|
217
|
+
outputPath: postman.outputPath,
|
|
218
|
+
flowCount: postman.flowCount,
|
|
219
|
+
happyPathTests: null,
|
|
220
|
+
invalidCaseTests: null,
|
|
221
|
+
withScripts: postman.withScripts,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const api = loadApi(apiPath);
|
|
226
|
+
const model = createStateMachineAdapterModel({ openapi: api });
|
|
227
|
+
const engine = createStateMachineEngine(model.definition);
|
|
228
|
+
|
|
229
|
+
const happyPaths = buildHappyPaths(engine);
|
|
230
|
+
const invalidCases = buildInvalidCases(engine);
|
|
231
|
+
const content = buildJestOrVitestContent({
|
|
232
|
+
format,
|
|
233
|
+
sourcePath: apiPath,
|
|
234
|
+
definition: model.definition,
|
|
235
|
+
happyPaths,
|
|
236
|
+
invalidCases,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
240
|
+
fs.writeFileSync(outputPath, content, "utf8");
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
format,
|
|
244
|
+
outputPath,
|
|
245
|
+
flowCount: model.definition.transitions.length,
|
|
246
|
+
happyPathTests: happyPaths.length,
|
|
247
|
+
invalidCaseTests: invalidCases.length,
|
|
248
|
+
withScripts: null,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
generateFlowTests,
|
|
254
|
+
};
|