@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/ayepi-mock.md +276 -0
  3. 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 live next to the source in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/mock) and are **not** shipped in the npm tarball.
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.1.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.1.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.1.0"
50
+ "@ayepi/core": "0.2.0"
50
51
  },
51
52
  "keywords": [
52
53
  "ayepi",