@zenera/faker 1.1.20 → 1.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/spec.js CHANGED
@@ -3,6 +3,11 @@ import { CliError, EXIT } from '@zenera/cli/lib';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { pagingOf } from "./paging.js";
5
5
  import { normalize } from "./schema.js";
6
+ /**
7
+ * Version of generator prompt rules and environment contracts.
8
+ * Bumping this invalidates cached Python generators and triggers regeneration.
9
+ */
10
+ export const GENERATOR_RULES_VERSION = 2;
6
11
  // ---------------------------------------------------------------------------
7
12
  // Documents, flattened
8
13
  //
@@ -57,13 +62,14 @@ export async function loadSpec(file) {
57
62
  if (!isObject(op)) {
58
63
  continue;
59
64
  }
60
- const path = join(prefix, template);
61
- const merged = override(shared, params(op.parameters, dialect));
65
+ const { path, fixed } = splitQuery(join(prefix, template));
66
+ const merged = override(override(shared, params(op.parameters, dialect)), pinned(fixed));
62
67
  out.push(build({
63
68
  source: file,
64
69
  dialect,
65
70
  method,
66
71
  path,
72
+ fixed,
67
73
  op,
68
74
  params: merged,
69
75
  body: requestBody(op, dialect),
@@ -88,11 +94,42 @@ function join(prefix, template) {
88
94
  const base = prefix.replace(/\/+$/, '');
89
95
  return `${base}${template.startsWith('/') ? '' : '/'}${template}` || '/';
90
96
  }
97
+ /** The path as it has to be called — what a person needs to see. */
98
+ export const called = (op) => op.fixed ? `${op.path}?${new URLSearchParams(op.fixed).toString()}` : op.path;
99
+ /**
100
+ * A path template has no query string, but VMware, Oracle and others write one
101
+ * anyway — `/x/{id}?action=retry` — because they need two `post`s under one
102
+ * path and the object model gives them one. Taken verbatim the key matches
103
+ * nothing, since a request path never contains a `?`. So it is read for what
104
+ * it means: the path is the part before the `?`, and each pair after it is a
105
+ * query value this operation insists on.
106
+ */
107
+ function splitQuery(template) {
108
+ const cut = template.indexOf('?');
109
+ if (cut < 0) {
110
+ return { path: template };
111
+ }
112
+ const fixed = {};
113
+ for (const [name, value] of new URLSearchParams(template.slice(cut + 1))) {
114
+ fixed[name] = value;
115
+ }
116
+ const path = template.slice(0, cut) || '/';
117
+ return Object.keys(fixed).length > 0 ? { path, fixed } : { path };
118
+ }
119
+ /** A one-value enum, so the check, the page and the generator all see the rule. */
120
+ function pinned(fixed) {
121
+ return Object.entries(fixed ?? {}).map(([name, value]) => ({
122
+ name,
123
+ in: 'query',
124
+ required: true,
125
+ schema: { type: 'string', enum: [value] },
126
+ }));
127
+ }
91
128
  function build(b) {
92
129
  const success = successOf(b.op, b.dialect);
93
130
  const operationId = typeof b.op.operationId === 'string' && b.op.operationId
94
131
  ? b.op.operationId
95
- : synthesizeId(b.method, b.path);
132
+ : synthesizeId(b.method, b.path, b.fixed);
96
133
  const operation = {
97
134
  key: '',
98
135
  source: b.source,
@@ -102,9 +139,10 @@ function build(b) {
102
139
  summary: typeof b.op.summary === 'string' ? b.op.summary : undefined,
103
140
  description: typeof b.op.description === 'string' ? b.op.description : undefined,
104
141
  params: b.params,
142
+ fixed: b.fixed,
105
143
  requestBody: b.body,
106
144
  success,
107
- paging: pagingOf(b.params, success.schema),
145
+ paging: pagingOf(b.params, success.schema, b.body?.schema),
108
146
  };
109
147
  return { ...operation, key: keyOf(operation) };
110
148
  }
@@ -117,6 +155,7 @@ function build(b) {
117
155
  */
118
156
  function keyOf(op) {
119
157
  const shape = {
158
+ version: GENERATOR_RULES_VERSION,
120
159
  method: op.method,
121
160
  path: op.path,
122
161
  operationId: op.operationId,
@@ -146,9 +185,8 @@ function canonical(value) {
146
185
  }
147
186
  return value;
148
187
  }
149
- function synthesizeId(method, path) {
150
- const words = path
151
- .split('/')
188
+ function synthesizeId(method, path, fixed) {
189
+ const words = [...path.split('/'), ...Object.values(fixed ?? {})]
152
190
  .filter(Boolean)
153
191
  .map((s) => s.replace(/[{}]/g, '').replace(/[^A-Za-z0-9]+/g, '_'));
154
192
  return [method, ...words].join('_');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/faker",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
4
4
  "description": "Mock HTTP server for swagger/OpenAPI documents, with response bodies generated by a model.",
5
5
  "keywords": [
6
6
  "openapi",
@@ -45,7 +45,7 @@
45
45
  "@apidevtools/swagger-parser": "^12.0.0",
46
46
  "ajv": "^8.17.1",
47
47
  "ajv-formats": "^3.0.1",
48
- "@zenera/cli": "^1.1.20",
49
- "@zenera/neo": "^1.1.20"
48
+ "@zenera/cli": "^1.1.22",
49
+ "@zenera/neo": "^1.1.22"
50
50
  }
51
51
  }