@zenera/faker 1.1.10 → 1.1.11
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 +7 -5
- package/dist/cache.d.ts +16 -3
- package/dist/cache.js +54 -30
- package/dist/command.js +18 -24
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -60,9 +60,11 @@ for. `GET /users/12324` answering with somebody else's id validates perfectly
|
|
|
60
60
|
and is still wrong.
|
|
61
61
|
|
|
62
62
|
If it fails, the diagnostics go back to the model and it tries again, up to
|
|
63
|
-
`--attempts`. If it passes, the
|
|
63
|
+
`--attempts`. If it passes, the generator is kept in this machine's shared cache
|
|
64
|
+
under `~/.zenera/neo/cache/faker-generator/`, keyed by the operation's shape, and
|
|
64
65
|
every later request is just `podman exec python3 gen.py in.json out.json` — no
|
|
65
|
-
model, no tokens.
|
|
66
|
+
model, no tokens. The store is the machine's, so the same document served from
|
|
67
|
+
another directory costs nothing the second time.
|
|
66
68
|
|
|
67
69
|
Generators run in a container with **no network**, on an image baked once with
|
|
68
70
|
`faker`, `exrex`, `jsonschema` and `python-dateutil`.
|
|
@@ -88,7 +90,7 @@ happen, all in the operation's own names:
|
|
|
88
90
|
- at request time a token identical to the one just sent is **cut** — nulled or
|
|
89
91
|
dropped, whichever the schema allows — and the request line says so. Nothing
|
|
90
92
|
is invented in its place; a generator written before this rule existed is
|
|
91
|
-
still
|
|
93
|
+
still cached, and a cache is not rebuilt because a rule changed.
|
|
92
94
|
|
|
93
95
|
Only paginated operations are affected. Their cache keys changed once, so they
|
|
94
96
|
are written again on first use; everything else keeps the key it had.
|
|
@@ -110,8 +112,8 @@ zen faker cache ls | clear What has been generated, or throw it away.
|
|
|
110
112
|
|
|
111
113
|
Useful options: `--port`, `--host` (reachable only from this machine by
|
|
112
114
|
default), `--model`, `--seed` (same request, same answer), `--rebuild`,
|
|
113
|
-
`--attempts`, `--concurrency`, `--timeout`, `--cache <dir
|
|
114
|
-
`zen help faker` prints the full table.
|
|
115
|
+
`--attempts`, `--concurrency`, `--timeout`, `--cache <dir>` (the container's
|
|
116
|
+
workspace), `--quiet`. `zen help faker` prints the full table.
|
|
115
117
|
|
|
116
118
|
`GET /__faker/routes` lists what is being served; `GET /__faker/health` is a
|
|
117
119
|
health check.
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { Model } from '@zenera/neo';
|
|
2
|
-
import {
|
|
2
|
+
import type { Box } from './box.ts';
|
|
3
3
|
import { BuildFailed } from './generate.ts';
|
|
4
4
|
import type { Operation } from './spec.ts';
|
|
5
5
|
import type { Checks } from './validate.ts';
|
|
6
|
+
export declare const FAKER_KIND = "faker-generator";
|
|
7
|
+
/** What is kept about a generator besides the code, for `zen faker cache ls`. */
|
|
8
|
+
export interface GeneratorMeta {
|
|
9
|
+
operationId?: string;
|
|
10
|
+
method?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
source?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
attempts?: number;
|
|
15
|
+
createdAt?: string;
|
|
16
|
+
}
|
|
6
17
|
export interface Generator {
|
|
7
18
|
key: string;
|
|
8
19
|
source: string;
|
|
9
|
-
/** whether it came
|
|
20
|
+
/** whether it came out of the cache rather than out of a model */
|
|
10
21
|
cached: boolean;
|
|
11
22
|
}
|
|
12
23
|
export interface CacheEvent {
|
|
@@ -26,10 +37,12 @@ export interface CacheOptions {
|
|
|
26
37
|
* on all of them.
|
|
27
38
|
*/
|
|
28
39
|
concurrency?: number;
|
|
29
|
-
/** ignore what is
|
|
40
|
+
/** ignore what is cached and write fresh */
|
|
30
41
|
rebuild?: boolean;
|
|
31
42
|
/** run generators but keep nothing */
|
|
32
43
|
ephemeral?: boolean;
|
|
44
|
+
/** keep them somewhere other than the shared store */
|
|
45
|
+
cacheDir?: string;
|
|
33
46
|
onStart?: (e: CacheEvent) => void;
|
|
34
47
|
onAttempt?: (e: CacheEvent) => void;
|
|
35
48
|
onReady?: (e: CacheEvent & {
|
package/dist/cache.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { writeJson } from '@zenera/cli/lib';
|
|
5
|
-
import { GENERATORS } from "./box.js";
|
|
1
|
+
import { Cache as Store } from '@zenera/cli/lib';
|
|
6
2
|
import { build, BuildFailed } from "./generate.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// The cache
|
|
5
|
+
//
|
|
6
|
+
// Two layers over one identity. `Operation.key` is a function of the
|
|
7
|
+
// operation's shape, so a spec edit produces a new key and the old artefact is
|
|
8
|
+
// simply never asked for again — there is nothing to invalidate, which is the
|
|
9
|
+
// part of a cache that is usually wrong.
|
|
10
|
+
//
|
|
11
|
+
// The generator itself lives in the machine's shared cache rather than beside
|
|
12
|
+
// the container's workspace, so the same spec served from two directories is
|
|
13
|
+
// written once. The box root is scratch: a hit is copied back into it, because
|
|
14
|
+
// the container can only run what is under its mount.
|
|
15
|
+
//
|
|
16
|
+
// The in-flight map is the other half and matters more than it looks: ten
|
|
17
|
+
// requests arriving together for an uncached operation must produce one build,
|
|
18
|
+
// not ten. A failed build is remembered too, for the same reason — an operation
|
|
19
|
+
// the model could not write for should not re-ask on every request.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const FAKER_KIND = 'faker-generator';
|
|
7
22
|
const DEFAULT_CONCURRENCY = 4;
|
|
8
23
|
export class Cache {
|
|
9
24
|
#opts;
|
|
25
|
+
#store;
|
|
10
26
|
#live = new Map();
|
|
11
27
|
#settled = new Set();
|
|
12
28
|
#slots;
|
|
@@ -14,6 +30,7 @@ export class Cache {
|
|
|
14
30
|
#running = 0;
|
|
15
31
|
constructor(opts) {
|
|
16
32
|
this.#opts = opts;
|
|
33
|
+
this.#store = new Store(FAKER_KIND, { dir: opts.cacheDir, mode: 0o600 });
|
|
17
34
|
this.#slots = Math.max(1, opts.concurrency ?? DEFAULT_CONCURRENCY);
|
|
18
35
|
}
|
|
19
36
|
/**
|
|
@@ -51,7 +68,7 @@ export class Cache {
|
|
|
51
68
|
const { box, model, checks, rebuild, ephemeral } = this.#opts;
|
|
52
69
|
// Read before queueing: a cache hit costs nothing and must not wait
|
|
53
70
|
// behind somebody else's model call.
|
|
54
|
-
const source = rebuild ? undefined : await read(box, operation.key);
|
|
71
|
+
const source = rebuild ? undefined : await this.#read(box, operation.key);
|
|
55
72
|
if (source !== undefined) {
|
|
56
73
|
this.#opts.onReady?.({ operation, cached: true, attempts: 0 });
|
|
57
74
|
return { key: operation.key, source, cached: true };
|
|
@@ -67,17 +84,18 @@ export class Cache {
|
|
|
67
84
|
onAttempt: (attempt, diagnostics) => this.#opts.onAttempt?.({ operation, attempt, diagnostics }),
|
|
68
85
|
});
|
|
69
86
|
if (!ephemeral) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
this.#store.put(operation.key, {
|
|
88
|
+
source: built.source,
|
|
89
|
+
meta: {
|
|
90
|
+
operationId: operation.operationId,
|
|
91
|
+
method: operation.method,
|
|
92
|
+
path: operation.path,
|
|
93
|
+
source: operation.source,
|
|
94
|
+
model: model.id,
|
|
95
|
+
attempts: built.attempts,
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
81
99
|
}
|
|
82
100
|
this.#opts.onReady?.({ operation, cached: false, attempts: built.attempts });
|
|
83
101
|
return { key: operation.key, source: built.source, cached: false };
|
|
@@ -91,6 +109,25 @@ export class Cache {
|
|
|
91
109
|
this.#leave();
|
|
92
110
|
}
|
|
93
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* A hit is written into the box before it is returned. The container runs
|
|
114
|
+
* files under its mount and nothing else, and the mount is scratch that any
|
|
115
|
+
* `cache clear` is free to delete.
|
|
116
|
+
*/
|
|
117
|
+
async #read(box, key) {
|
|
118
|
+
const found = this.#store.get(key);
|
|
119
|
+
if (!found?.source?.trim()) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await box.write(key, found.source);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// The mount went away. Writing it again is the model's job.
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return found.source;
|
|
130
|
+
}
|
|
94
131
|
#enter() {
|
|
95
132
|
if (this.#running < this.#slots) {
|
|
96
133
|
this.#running++;
|
|
@@ -108,18 +145,5 @@ export class Cache {
|
|
|
108
145
|
this.#running--;
|
|
109
146
|
}
|
|
110
147
|
}
|
|
111
|
-
async function read(box, key) {
|
|
112
|
-
const path = box.sourceOf(key);
|
|
113
|
-
if (!existsSync(path)) {
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
const source = await readFile(path, 'utf8');
|
|
118
|
-
return source.trim() ? source : undefined;
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
148
|
export { BuildFailed };
|
|
125
149
|
//# sourceMappingURL=cache.js.map
|
package/dist/command.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { bold, cacheItems, clearCache, CliError, cyan, dim, EXIT, green, json, note, ownedContainers, parse, paths, red, removeContainers, table, usageError, write, writeAll, yellow, } from '@zenera/cli/lib';
|
|
1
2
|
import { rmSync } from 'node:fs';
|
|
2
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { join, relative, resolve } from 'node:path';
|
|
4
|
-
import { bold, CliError, cyan, dim, EXIT, green, json, note, ownedContainers, parse, paths, red, removeContainers, table, usageError, write, writeAll, yellow, } from '@zenera/cli/lib';
|
|
5
4
|
import { GENERATORS } from "./box.js";
|
|
5
|
+
import { FAKER_KIND } from "./cache.js";
|
|
6
6
|
import { reason } from "./generate.js";
|
|
7
7
|
import { listen } from "./server.js";
|
|
8
8
|
import { open } from "./setup.js";
|
|
@@ -53,7 +53,7 @@ export const command = {
|
|
|
53
53
|
[' --host <h>', dim('Default 127.0.0.1. Anything else is reachable off-machine.')],
|
|
54
54
|
[' --model <ref>', dim('Which model writes the generators.')],
|
|
55
55
|
[' --image <ref>', dim('Skip the baked image and use this one.')],
|
|
56
|
-
[' --cache <dir>', dim('
|
|
56
|
+
[' --cache <dir>', dim("The container's workspace. Default ~/.zenera/neo/faker.")],
|
|
57
57
|
[' --seed <n>', dim('Answer the same request the same way every time.')],
|
|
58
58
|
[' --attempts <n>', dim('Tries per generator before giving up. Default 3.')],
|
|
59
59
|
[' --concurrency <n>', dim('Generators written at once. Default 4.')],
|
|
@@ -190,7 +190,7 @@ async function cache(args, ctx) {
|
|
|
190
190
|
const root = values.cache ? resolve(ctx.cwd, values.cache) : paths.faker();
|
|
191
191
|
const sub = positionals[0] ?? 'ls';
|
|
192
192
|
if (sub === 'ls') {
|
|
193
|
-
const entries =
|
|
193
|
+
const entries = listGenerators();
|
|
194
194
|
if (ctx.json) {
|
|
195
195
|
json(entries);
|
|
196
196
|
return;
|
|
@@ -211,6 +211,9 @@ async function cache(args, ctx) {
|
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
213
|
if (sub === 'clear') {
|
|
214
|
+
clearCache({ kind: FAKER_KIND });
|
|
215
|
+
// The workspace is scratch — whatever a hit was copied into it is
|
|
216
|
+
// written again from the cache, or by the model.
|
|
214
217
|
rmSync(join(root, GENERATORS), { recursive: true, force: true });
|
|
215
218
|
// The container is named after its configuration, so a stale one would
|
|
216
219
|
// otherwise sit there stopped forever with nothing pointing at it.
|
|
@@ -219,30 +222,21 @@ async function cache(args, ctx) {
|
|
|
219
222
|
if (mine.length > 0) {
|
|
220
223
|
await removeContainers(mine.map((c) => c.name));
|
|
221
224
|
}
|
|
222
|
-
note(`${green('cleared')} ${dim(
|
|
225
|
+
note(`${green('cleared')} ${dim(paths.cache())}`);
|
|
223
226
|
return;
|
|
224
227
|
}
|
|
225
228
|
throw usageError(`unknown cache command "${sub}"`, 'zen faker cache <ls|clear>');
|
|
226
229
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
const meta = JSON.parse(await readFile(join(root, GENERATORS, key, 'meta.json'), 'utf8'));
|
|
239
|
-
out.push({ key, ...meta });
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
out.push({ key });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return out;
|
|
230
|
+
/**
|
|
231
|
+
* The keys are the operation keys, written verbatim — which is why they can be
|
|
232
|
+
* listed at all. Anything the store cannot parse is left out rather than shown
|
|
233
|
+
* as a row with nothing in it.
|
|
234
|
+
*/
|
|
235
|
+
function listGenerators() {
|
|
236
|
+
const { rows } = cacheItems(FAKER_KIND);
|
|
237
|
+
return rows
|
|
238
|
+
.map((row) => ({ key: row.key, ...row.value?.meta }))
|
|
239
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
246
240
|
}
|
|
247
241
|
function summarize(operations) {
|
|
248
242
|
const by = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/faker",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
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.
|
|
49
|
-
"@zenera/neo": "^1.1.
|
|
48
|
+
"@zenera/cli": "^1.1.11",
|
|
49
|
+
"@zenera/neo": "^1.1.11"
|
|
50
50
|
}
|
|
51
51
|
}
|