@ayepi/mock 0.1.0 → 0.2.0
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 +1 -1
- package/ayepi-mock.md +276 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -45,5 +45,5 @@ This package ships dense, machine-oriented reference docs written for **AI codin
|
|
|
45
45
|
|
|
46
46
|
- [`ayepi-mock.md`](./ayepi-mock.md)
|
|
47
47
|
|
|
48
|
-
They
|
|
48
|
+
They ship with this package and also live in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/mock).
|
|
49
49
|
|
package/ayepi-mock.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ayepi-mock.md — reference for `@ayepi/mock`, written for coding agents.
|
|
3
|
+
|
|
4
|
+
Copy this file into any project that depends on `@ayepi/mock` (e.g. into your repo's
|
|
5
|
+
`docs/` or `.claude/` directory) and reference it from your agents and slash commands.
|
|
6
|
+
It documents the public API, the patterns the package expects, and how it works under the
|
|
7
|
+
hood, with copy-pasteable examples. Keep it in sync with the installed package version.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
# `@ayepi/mock`
|
|
11
|
+
|
|
12
|
+
Generate **schema-valid fake data** from an ayepi spec. Point it at a `spec(...)` and
|
|
13
|
+
get a real [`Server`](./ayepi-core.md) whose every endpoint returns generated data that
|
|
14
|
+
parses cleanly against its own response schema — so you can develop a frontend, demo a
|
|
15
|
+
flow, or seed tests before the backend exists.
|
|
16
|
+
|
|
17
|
+
Two things make it useful beyond a one-off faker:
|
|
18
|
+
|
|
19
|
+
- **Deterministic by default.** The PRNG is seeded from `seed + endpoint + JSON(request)`,
|
|
20
|
+
so identical inputs always produce identical output. A `GET /users/u1` returns the same
|
|
21
|
+
user every time; pagination is naturally stable across calls.
|
|
22
|
+
- **Generic over zod.** It walks any zod schema (objects, arrays, unions, enums, formats,
|
|
23
|
+
wrappers, …) and fills every leaf with a valid value. No per-endpoint fixtures.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm add -D @ayepi/mock # peers: @ayepi/core, zod
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { mockServer } from '@ayepi/mock'
|
|
31
|
+
import { api } from './spec' // your spec(...)
|
|
32
|
+
|
|
33
|
+
const app = mockServer(api, { seed: 1, arraySize: 5 })
|
|
34
|
+
const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
35
|
+
const body = await res.json() // schema-valid, deterministic, 10 items
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Public API
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { generate, mockServer, mockHandlers } from '@ayepi/mock'
|
|
44
|
+
import type { MockOptions, MockOverrides, GenContext, Override } from '@ayepi/mock'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `generate(schema, opts?, ctx?) => unknown`
|
|
48
|
+
|
|
49
|
+
One-off fake value from **any** zod schema. The returned value parses against `schema`.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { z } from 'zod'
|
|
53
|
+
|
|
54
|
+
const user = generate(z.object({ id: z.uuid(), name: z.string(), age: z.int().min(18) }))
|
|
55
|
+
// → { id: '…uuid…', name: 'qkzl…', age: 42 }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `opts: MockOptions` — seeding, sizing, overrides, clock (see below).
|
|
59
|
+
- `ctx: Partial<GenContext>` — pin parts of the generation context directly:
|
|
60
|
+
- `request` — value mixed into the seed (different requests ⇒ different output).
|
|
61
|
+
- `query` — drives array sizing (pagination). e.g. `{ query: { limit: 5 } }`.
|
|
62
|
+
- `path` — starting path for override lookup (rarely needed).
|
|
63
|
+
- `rng` — supply your own `() => number` to fully control randomness (tests).
|
|
64
|
+
|
|
65
|
+
### `mockHandlers(spec, opts?) => Record<string, Handler>`
|
|
66
|
+
|
|
67
|
+
The handler bag for `server(spec, [implement(spec).handlers(bag)])`. One generated handler
|
|
68
|
+
per endpoint. Use this when you want to mix mock handlers with real ones, or add
|
|
69
|
+
middleware/options yourself.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { server, implement } from '@ayepi/core'
|
|
73
|
+
const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `mockServer(spec, opts?) => Server`
|
|
77
|
+
|
|
78
|
+
The convenience path: builds the handler bag and wires it into
|
|
79
|
+
`server(spec, [implement(spec).handlers(bag)])`, returning a real ayepi `Server`. Routing, **input validation**, **output validation**, and
|
|
80
|
+
the OpenAPI/AsyncAPI docs surface all behave exactly as a real server's — the only
|
|
81
|
+
difference is the bodies are generated.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const app = mockServer(api, { seed: 1 })
|
|
85
|
+
app.fetch(req) // Promise<Response>
|
|
86
|
+
app.openapi() // generated OpenAPI 3.1 doc
|
|
87
|
+
app.manifest() // zod-free runtime manifest
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Because the generated value is `.parse()`d by core on the way out, **a generation bug
|
|
91
|
+
surfaces as a 500**, not as silently-invalid data. If you see one, the schema has a shape
|
|
92
|
+
the generator doesn't satisfy — see [Gotchas](#gotchas).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Options — `MockOptions`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
interface MockOptions {
|
|
100
|
+
seed?: number | string // base seed (default '0')
|
|
101
|
+
deterministic?: boolean // default true → pure; false → Math.random
|
|
102
|
+
arraySize?: number // default count when no size hint (default 3)
|
|
103
|
+
limitKeys?: readonly string[] // query keys that size arrays (default ['limit','pageSize','count'])
|
|
104
|
+
overrides?: {
|
|
105
|
+
fields?: Record<string, (g: GenContext) => unknown> // by property name or dotted path
|
|
106
|
+
formats?: Record<string, (g: GenContext) => unknown> // by zod string format
|
|
107
|
+
}
|
|
108
|
+
now?: () => number // clock for date/datetime generation (default Date.now)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Seeding & determinism
|
|
113
|
+
|
|
114
|
+
With `deterministic: true` (the default), the PRNG is a pure function of
|
|
115
|
+
`seed + endpointName + JSON(requestData)`:
|
|
116
|
+
|
|
117
|
+
- **Same** seed + endpoint + request ⇒ byte-for-byte identical output.
|
|
118
|
+
- **Different** seed ⇒ different output (change `seed` to reshuffle every value).
|
|
119
|
+
- **Different** request (path/query/body) ⇒ different output for that call.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const a = generate(schema, { seed: 7 }, { request: { id: 'u1' } })
|
|
123
|
+
const b = generate(schema, { seed: 7 }, { request: { id: 'u1' } })
|
|
124
|
+
// a deep-equals b
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Set `deterministic: false` to use `Math.random` (non-reproducible). The seed is ignored.
|
|
128
|
+
|
|
129
|
+
### Pagination — array sizing
|
|
130
|
+
|
|
131
|
+
An array's element count is resolved in this order:
|
|
132
|
+
|
|
133
|
+
1. **Field override size** — if a `fields` override for that property returns an array,
|
|
134
|
+
that array (and its length) is used verbatim.
|
|
135
|
+
2. **`limit` query value** — the first key in `limitKeys` present in the request query with
|
|
136
|
+
a non-negative numeric value. So `?limit=10` ⇒ 10 elements.
|
|
137
|
+
3. **Default** — `arraySize` (default `3`).
|
|
138
|
+
|
|
139
|
+
A schema's own `.min()` / `.max()` length bounds clamp the result, so a `z.array(...).max(2)`
|
|
140
|
+
never exceeds 2 even with `?limit=10`. Typed `streamOut` item streams are sized by the
|
|
141
|
+
**same** rule, so a paged list endpoint and its streaming twin agree.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
mockServer(api).fetch(new Request('http://x/listUsers?limit=5', { method: 'POST' }))
|
|
145
|
+
// → body arrays have length 5; stable across repeated calls (deterministic seeding)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Overrides — pin specific values
|
|
149
|
+
|
|
150
|
+
`fields` is keyed by **property name** (leaf) or **dotted path**; `formats` by zod string
|
|
151
|
+
format. Precedence: **field (path) → field (name) → format → default generation**.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const app = mockServer(api, {
|
|
155
|
+
overrides: {
|
|
156
|
+
fields: {
|
|
157
|
+
email: (g) => `user-${g.path}@acme.test`, // every `email` property
|
|
158
|
+
'user.id': () => 'fixed-id', // only the nested user.id
|
|
159
|
+
},
|
|
160
|
+
formats: {
|
|
161
|
+
uuid: () => '00000000-0000-4000-8000-000000000000', // every z.uuid()
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Each callback receives a `GenContext`:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
interface GenContext {
|
|
171
|
+
path: string // e.g. 'user.address.city' or 'items.0'
|
|
172
|
+
rng: () => number // the seeded PRNG (floats in [0,1))
|
|
173
|
+
request: unknown // request data that seeded this generation
|
|
174
|
+
query: Record<string, unknown> // request query (drives pagination)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Return whatever you like — it is **not** re-validated against the schema, so an override is
|
|
179
|
+
also the escape hatch for shapes the generator can't satisfy (regex strings, branded types).
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Supported zod types
|
|
184
|
+
|
|
185
|
+
The generator switches on zod v4's internal type discriminator (the same introspection
|
|
186
|
+
core uses to build JSON Schema), so it tracks the installed zod version.
|
|
187
|
+
|
|
188
|
+
| zod | generated |
|
|
189
|
+
| ------------------------------------- | ---------------------------------------------------------------- |
|
|
190
|
+
| `z.string()` | random word, honoring `.min()`/`.max()`/`.length()` |
|
|
191
|
+
| string **formats** (see below) | a value matching the format |
|
|
192
|
+
| `z.number()` / `z.int()` / `.min/.max`| number honoring int-ness and bounds (`gt/lt` handled) |
|
|
193
|
+
| `z.bigint()` (+ bounds) | bigint within bounds |
|
|
194
|
+
| `z.boolean()` | `true`/`false` |
|
|
195
|
+
| `z.date()` | `new Date(now())` |
|
|
196
|
+
| `z.literal(v)` | exactly `v` |
|
|
197
|
+
| `z.enum([...])` | a random member |
|
|
198
|
+
| `z.object({...})` | each property generated under its path |
|
|
199
|
+
| `z.array(T)` | N elements (pagination rule), clamped to length bounds |
|
|
200
|
+
| `z.tuple([...], rest?)` | one value per item, plus one rest element if present |
|
|
201
|
+
| `z.record(K, V)` | a few `word → V` entries |
|
|
202
|
+
| `z.union([...])` | a random member generated |
|
|
203
|
+
| `z.optional(T)` / `z.nullable(T)` | the inner value most of the time, else `undefined` / `null` |
|
|
204
|
+
| `z.default(T, d)` / `prefault` | the inner value, or the default |
|
|
205
|
+
| `z.catch` / `z.readonly` / `nonoptional` | unwrapped to the inner type |
|
|
206
|
+
| `z.null()` / `z.undefined()` / `z.void()` | `null` / `undefined` / `undefined` |
|
|
207
|
+
| `z.any()` / `z.unknown()` / other | a plain word (fallback) |
|
|
208
|
+
|
|
209
|
+
**String formats** detected and generated: `email`, `url`, `uuid`/`guid`, `datetime`,
|
|
210
|
+
`date`, `time`, `duration`, `ipv4`, `ipv6`, `cuid`/`cuid2`, `ulid`, `nanoid`, `emoji`,
|
|
211
|
+
`e164`, `base64`/`base64url`. **Integer formats**: `safeint` (`z.int()`), `int32`, `uint32`.
|
|
212
|
+
A format the generator doesn't special-case (e.g. `jwt`, `.regex(...)`) falls back to a plain
|
|
213
|
+
word — pin those with a `fields`/`formats` override.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Recipes
|
|
218
|
+
|
|
219
|
+
**Mix mock and real handlers** — implement the endpoints you have, mock the rest.
|
|
220
|
+
`mockHandlers` returns a plain handler bag, so merge it with your real handlers (real keys
|
|
221
|
+
win) and wrap the merged bag in a single `implement(api).handlers(...)` builder:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { implement, server } from '@ayepi/core'
|
|
225
|
+
import { mockHandlers } from '@ayepi/mock'
|
|
226
|
+
|
|
227
|
+
const real = { getUser: ({ data }) => loadUser(data.id) }
|
|
228
|
+
const bag = { ...mockHandlers(api), ...real } // mocks fill the gaps; real overrides
|
|
229
|
+
const app = server(api, [implement(api).handlers(bag)])
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Stable demo data** — fix the seed and clock so screenshots never drift:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const app = mockServer(api, { seed: 'demo', now: () => Date.parse('2024-01-01') })
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Frontend dev server** — serve the mock with any adapter
|
|
239
|
+
([`@ayepi/node`](./ayepi-node.md), `@ayepi/bun`, `@ayepi/deno`):
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { serve } from '@ayepi/node'
|
|
243
|
+
serve(mockServer(api, { seed: 1 }), { port: 3000 })
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Gotchas
|
|
249
|
+
|
|
250
|
+
- **Generated values are validated by core (`mockServer`).** A failing generation shows up
|
|
251
|
+
as a `500`, not bad data. The usual cause is a schema the generator can't satisfy — fix
|
|
252
|
+
it with an override:
|
|
253
|
+
- `.regex(...)` / `.includes(...)` string refinements → no format detected → a plain
|
|
254
|
+
word that may not match. Override the field.
|
|
255
|
+
- `.refine(...)` custom predicates, branded types, cross-field invariants → the generator
|
|
256
|
+
doesn't know the predicate. Override the field/object.
|
|
257
|
+
- **`generate()` does not re-validate overrides.** A `fields`/`formats` callback can return
|
|
258
|
+
anything; with `generate()` (no server) nothing checks it. Under `mockServer`, core's
|
|
259
|
+
output validation will reject a bad override value.
|
|
260
|
+
- **Raw byte `streamOut` / `download`** (a `string` content-type, not a schema) can't be
|
|
261
|
+
meaningfully synthesized; those endpoints return an empty body. Typed (schema) item
|
|
262
|
+
streams are fully generated.
|
|
263
|
+
- **No-response endpoints** (no `response`/`responses`/`streamOut`) return `204`.
|
|
264
|
+
- **Multi-status endpoints** (`responses`) deterministically pick the **smallest** declared
|
|
265
|
+
status code and generate against that schema.
|
|
266
|
+
- **Determinism depends on stable input.** Since the seed includes `JSON(requestData)`, a
|
|
267
|
+
request body with non-deterministic key order or volatile fields (timestamps) will shift
|
|
268
|
+
the output. Normalize inputs if you need cross-client reproducibility.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## See also
|
|
273
|
+
|
|
274
|
+
- [`ayepi-core.md`](./ayepi-core.md) — `spec`, `server`, the `Server` surface, the client.
|
|
275
|
+
- [`ayepi-core-endpoints.md`](./ayepi-core-endpoints.md) — `EndpointConfig`: `response`,
|
|
276
|
+
`responses`, `streamOut`, `query`/`params` — the schemas this package generates against.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ayepi/mock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Mock server for @ayepi/core — generate schema-valid fake data from a spec, with deterministic seeding and automatic pagination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"type": "module",
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"ayepi-*.md"
|
|
22
23
|
],
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"zod": "^4",
|
|
41
|
-
"@ayepi/core": "^0.
|
|
42
|
+
"@ayepi/core": "^0.2.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@vitest/coverage-v8": "^2.1.8",
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
"tsdown": "^0.12.0",
|
|
47
48
|
"vitest": "^2.1.8",
|
|
48
49
|
"zod": "^4.4.3",
|
|
49
|
-
"@ayepi/core": "0.
|
|
50
|
+
"@ayepi/core": "0.2.0"
|
|
50
51
|
},
|
|
51
52
|
"keywords": [
|
|
52
53
|
"ayepi",
|