paramour 0.5.1 → 0.6.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/dist/codec.d.ts +16 -18
- package/dist/describe.d.ts +14 -3
- package/dist/describe.js +20 -3
- package/dist/errors.d.ts +89 -10
- package/dist/errors.js +79 -13
- package/dist/href.d.ts +38 -41
- package/dist/href.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/internal.d.ts +4 -3
- package/dist/internal.js +4 -3
- package/dist/p.d.ts +11 -10
- package/dist/p.js +45 -37
- package/dist/path.d.ts +26 -26
- package/dist/path.js +78 -42
- package/dist/route.d.ts +73 -73
- package/dist/route.js +30 -30
- package/dist/safe-decode.d.ts +10 -9
- package/dist/safe-decode.js +12 -11
- package/dist/schema.d.ts +4 -4
- package/dist/schema.js +4 -4
- package/dist/search.d.ts +32 -28
- package/dist/search.js +76 -42
- package/dist/standard-schema.d.ts +15 -15
- package/dist/standard-schema.js +15 -15
- package/package.json +1 -1
package/dist/search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { codecShapeLabel } from "./describe.js";
|
|
2
|
+
import { describeType, foreignMessage, ParamourError, ParseError, parseIssueReason, rebrandForeign, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
2
3
|
import { runStandardSchemaSync } from "./schema.js";
|
|
3
4
|
/**
|
|
4
5
|
* Builds the byte-layer query string from decoded pairs. Hand-rolled on
|
|
@@ -19,14 +20,18 @@ export function buildSearchString(pairs) {
|
|
|
19
20
|
* under keys paramour doesn't own can never fail a decode.
|
|
20
21
|
* Throws {@link SearchDecodeError} carrying one issue per failed key.
|
|
21
22
|
*
|
|
22
|
-
* A `RawSearch` config (
|
|
23
|
+
* A `RawSearch` config (SS2) branches to the whole-object schema
|
|
23
24
|
* path instead: every source key reaches the schema (P8 does not apply
|
|
24
25
|
* there — the schema owns stripping or passing through extras).
|
|
26
|
+
*
|
|
27
|
+
* `routePath` anchors a thrown {@link SearchDecodeError} to the owning
|
|
28
|
+
* route's path pattern — route-level surfaces pass `route.path`; standalone
|
|
29
|
+
* callers (nuqs, devtools) omit it and the error stays route-less.
|
|
25
30
|
*/
|
|
26
|
-
export function decodeSearch(config, source) {
|
|
31
|
+
export function decodeSearch(config, source, routePath) {
|
|
27
32
|
requireSearchConfig(config);
|
|
28
33
|
if (isRawSearch(config)) {
|
|
29
|
-
return decodeRawSearch(config, source);
|
|
34
|
+
return decodeRawSearch(config, source, routePath ?? null);
|
|
30
35
|
}
|
|
31
36
|
// The conditional SearchSlot doesn't narrow inside the generic body once
|
|
32
37
|
// the RawSearch branch returns (S stays a generic type parameter); this
|
|
@@ -47,12 +52,21 @@ export function decodeSearch(config, source) {
|
|
|
47
52
|
const sourceValues = readDeclaredValues(searchConfig, source);
|
|
48
53
|
// Absence is presence's job — .catch() only ever recovers parse
|
|
49
54
|
// *failures* (D2), which is why this is shared by both arity branches.
|
|
50
|
-
const recoverParseError = (error, key, codec) => {
|
|
55
|
+
const recoverParseError = (error, key, codec, wire, reason) => {
|
|
51
56
|
if (error instanceof ParseError && codec["~catchValue"] !== undefined) {
|
|
52
57
|
entries.push([key, codec["~catchValue"]()]);
|
|
53
58
|
}
|
|
54
59
|
else if (error instanceof ParseError) {
|
|
55
|
-
issues.push({
|
|
60
|
+
issues.push({
|
|
61
|
+
expected: codecShapeLabel(codec),
|
|
62
|
+
key,
|
|
63
|
+
message: error.message,
|
|
64
|
+
// An explicitly passed reason (the duplicate-scalar rejection) wins;
|
|
65
|
+
// otherwise the ParseError's own selfDescribing flag decides
|
|
66
|
+
// "parse" vs "validate" — structural, never message sniffing.
|
|
67
|
+
reason: reason ?? parseIssueReason(error),
|
|
68
|
+
...(wire === undefined ? {} : { wire }),
|
|
69
|
+
});
|
|
56
70
|
}
|
|
57
71
|
else {
|
|
58
72
|
throw error;
|
|
@@ -64,11 +78,17 @@ export function decodeSearch(config, source) {
|
|
|
64
78
|
// Array codecs consume all values in wire order; absent → [] (P6).
|
|
65
79
|
// Presence modifiers are banned on array codecs, so no absence
|
|
66
80
|
// handling exists here.
|
|
81
|
+
let offending;
|
|
67
82
|
try {
|
|
68
|
-
|
|
83
|
+
const parsed = [];
|
|
84
|
+
for (const raw of values) {
|
|
85
|
+
offending = raw;
|
|
86
|
+
parsed.push(codec["~parseElement"](raw));
|
|
87
|
+
}
|
|
88
|
+
entries.push([key, parsed]);
|
|
69
89
|
}
|
|
70
90
|
catch (error) {
|
|
71
|
-
recoverParseError(error, key, codec);
|
|
91
|
+
recoverParseError(error, key, codec, offending);
|
|
72
92
|
}
|
|
73
93
|
continue;
|
|
74
94
|
}
|
|
@@ -84,7 +104,12 @@ export function decodeSearch(config, source) {
|
|
|
84
104
|
entries.push([key, undefined]);
|
|
85
105
|
break;
|
|
86
106
|
case "required":
|
|
87
|
-
issues.push({
|
|
107
|
+
issues.push({
|
|
108
|
+
expected: codecShapeLabel(codec),
|
|
109
|
+
key,
|
|
110
|
+
message: "required search param is missing",
|
|
111
|
+
reason: "missing",
|
|
112
|
+
});
|
|
88
113
|
break;
|
|
89
114
|
}
|
|
90
115
|
continue;
|
|
@@ -100,18 +125,21 @@ export function decodeSearch(config, source) {
|
|
|
100
125
|
entries.push([key, codec["~parseElement"](first)]);
|
|
101
126
|
}
|
|
102
127
|
catch (error) {
|
|
103
|
-
|
|
128
|
+
// The duplicate-scalar rejection (P5) has no SINGLE offending value —
|
|
129
|
+
// only a genuine one-value parse cites its wire form — and its reason
|
|
130
|
+
// is passed explicitly: duplication is what failed, not the grammar.
|
|
131
|
+
recoverParseError(error, key, codec, values.length > 1 ? undefined : first, values.length > 1 ? "duplicate" : undefined);
|
|
104
132
|
}
|
|
105
133
|
}
|
|
106
134
|
if (issues.length > 0) {
|
|
107
|
-
throw new SearchDecodeError(issues);
|
|
135
|
+
throw new SearchDecodeError(issues, routePath ?? null);
|
|
108
136
|
}
|
|
109
137
|
return Object.fromEntries(entries);
|
|
110
138
|
}
|
|
111
139
|
/**
|
|
112
140
|
* encodeURIComponent throws a raw URIError on lone surrogates; wrap it so
|
|
113
141
|
* the documented "every error is a ParamourError" contract holds (S7).
|
|
114
|
-
* Exported for path.ts (the byte-layer chokepoint is shared with
|
|
142
|
+
* Exported for path.ts (the byte-layer chokepoint is shared with path
|
|
115
143
|
* segment encoding), not from the package barrel.
|
|
116
144
|
*/
|
|
117
145
|
export function encodeComponent(text) {
|
|
@@ -123,7 +151,7 @@ export function encodeComponent(text) {
|
|
|
123
151
|
* Caveat: JS property enumeration puts integer-like keys ("0", "42") first
|
|
124
152
|
* in ascending numeric order regardless of declaration — declaration order
|
|
125
153
|
* is unrecoverable for those, so they sort numerically before all others.
|
|
126
|
-
* Params equal to their `.default()` are elided (
|
|
154
|
+
* Params equal to their `.default()` are elided (D8), compared by
|
|
127
155
|
* serialized wire form against the live default (re-serialized per encode —
|
|
128
156
|
* a build-time snapshot would go stale if a reference-typed default were
|
|
129
157
|
* mutated, silently dropping explicit values that then decode differently).
|
|
@@ -131,9 +159,9 @@ export function encodeComponent(text) {
|
|
|
131
159
|
* time-varying factory would elide an explicit value that later decodes as
|
|
132
160
|
* a different one.
|
|
133
161
|
*
|
|
134
|
-
* A `RawSearch` config (
|
|
135
|
-
*
|
|
136
|
-
*
|
|
162
|
+
* A `RawSearch` config (SS5) branches to a raw pass-through instead: no
|
|
163
|
+
* serializer exists for a whole-object schema, so the caller's record goes
|
|
164
|
+
* straight to the byte layer and the schema never runs on encode.
|
|
137
165
|
*/
|
|
138
166
|
export function encodeSearch(config, input) {
|
|
139
167
|
requireSearchConfig(config);
|
|
@@ -192,7 +220,7 @@ export function encodeSearch(config, input) {
|
|
|
192
220
|
return pairs;
|
|
193
221
|
}
|
|
194
222
|
/**
|
|
195
|
-
* Runtime discriminant for the `search:` slot (
|
|
223
|
+
* Runtime discriminant for the `search:` slot (SS2): probes the
|
|
196
224
|
* `~kind` marker's VALUE, which is unambiguous against a codec map — a map
|
|
197
225
|
* key literally named "~kind" would hold a codec object, never the marker
|
|
198
226
|
* string. Exported from the package barrel so derived surfaces
|
|
@@ -208,10 +236,10 @@ export function isRawSearch(config) {
|
|
|
208
236
|
* the codec's `.catch()` recovery applied (decodeSearch always recovers a
|
|
209
237
|
* caught failure, so a probe through it cannot tell "parsed cleanly" from
|
|
210
238
|
* "failed and was caught"). Exists for reflection-driven tooling — the
|
|
211
|
-
* devtools panel's catch-attribution probe
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
239
|
+
* devtools panel's catch-attribution probe — and any other derived surface
|
|
240
|
+
* that must observe the raw parse outcome. A parse failure throws the
|
|
241
|
+
* codec's own {@link ParseError}; foreign throws from a custom codec
|
|
242
|
+
* propagate unwrapped, matching decodeSearch's taxonomy. For
|
|
215
243
|
* arity-"many" codecs this parses ONE element of the repeated-key array,
|
|
216
244
|
* not the whole array (the same contract as `~parseElement` itself).
|
|
217
245
|
*/
|
|
@@ -219,13 +247,13 @@ export function parseValue(codec, raw) {
|
|
|
219
247
|
return codec["~parseElement"](raw);
|
|
220
248
|
}
|
|
221
249
|
/**
|
|
222
|
-
* The whole-object search escape hatch (
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
250
|
+
* The whole-object search escape hatch (SS1): an explicit, greppable wrapper
|
|
251
|
+
* around a bare Standard Schema so a route's `search:` slot never falls into
|
|
252
|
+
* the degraded raw mode by accident — a bare `search: schema` could be
|
|
253
|
+
* confused for a codec map, but `rawSearch` is a conscious act. Per-key
|
|
254
|
+
* defaults/`.catch()` and round-trip encoding are deliberately unavailable
|
|
255
|
+
* here (SS7); reach for `p.custom` if you need bidirectional per-key
|
|
256
|
+
* transforms instead.
|
|
229
257
|
*/
|
|
230
258
|
export function rawSearch(schema) {
|
|
231
259
|
return { "~kind": "raw-search", "~schema": schema };
|
|
@@ -296,17 +324,17 @@ export function serializeValue(codec, label, value) {
|
|
|
296
324
|
return serialized;
|
|
297
325
|
}
|
|
298
326
|
/**
|
|
299
|
-
* The `RawSearch` decode path (
|
|
327
|
+
* The `RawSearch` decode path (SS3/SS4). The schema receives EVERY
|
|
300
328
|
* source key, normalized to Next's own `searchParams` shape — P8's
|
|
301
329
|
* declared-keys-only stance doesn't apply to a whole-object schema, which
|
|
302
|
-
* owns stripping or passing through extras itself. Sync only
|
|
303
|
-
*
|
|
330
|
+
* owns stripping or passing through extras itself. Sync only — the shared
|
|
331
|
+
* runner throws on an async `validate`. A validator that THROWS
|
|
304
332
|
* (rather than returning issues) is rebranded at this chokepoint — the
|
|
305
|
-
* shared runner deliberately stays throw-preserving
|
|
306
|
-
*
|
|
307
|
-
*
|
|
333
|
+
* shared runner deliberately stays throw-preserving so this call site owns
|
|
334
|
+
* the wrap, mirroring how a foreign throw is branded elsewhere in the
|
|
335
|
+
* package.
|
|
308
336
|
*/
|
|
309
|
-
function decodeRawSearch(config, source) {
|
|
337
|
+
function decodeRawSearch(config, source, routePath) {
|
|
310
338
|
const record = readAllValues(source);
|
|
311
339
|
const result = rebrandForeign(() => runStandardSchemaSync(config["~schema"], record), (error) => new ParamourError(`raw-search schema validation threw: ${foreignMessage(error)}`, { cause: error }));
|
|
312
340
|
if (result.issues) {
|
|
@@ -324,14 +352,20 @@ function decodeRawSearch(config, source) {
|
|
|
324
352
|
// always produces a plain Array and is immune.
|
|
325
353
|
const issues = Array.from(result.issues, (issue) => {
|
|
326
354
|
const key = Array.from(issue.path ?? [], (seg) => String(typeof seg === "object" ? seg.key : seg)).join(".");
|
|
327
|
-
|
|
355
|
+
// "validate": the prose belongs to the user's whole-object schema —
|
|
356
|
+
// same classification as a per-key schema failure.
|
|
357
|
+
return {
|
|
358
|
+
key: key === "" ? "<search>" : key,
|
|
359
|
+
message: issue.message,
|
|
360
|
+
reason: "validate",
|
|
361
|
+
};
|
|
328
362
|
});
|
|
329
|
-
throw new SearchDecodeError(issues);
|
|
363
|
+
throw new SearchDecodeError(issues, routePath);
|
|
330
364
|
}
|
|
331
365
|
return result.value;
|
|
332
366
|
}
|
|
333
367
|
/**
|
|
334
|
-
* The `RawSearch` encode path (
|
|
368
|
+
* The `RawSearch` encode path (SS5): no serializer exists for a
|
|
335
369
|
* whole-object schema, so the caller's already-wire-shaped record is pushed
|
|
336
370
|
* straight to the byte layer — one pair per string value, one repeated pair
|
|
337
371
|
* per array element — and the schema never runs on encode.
|
|
@@ -362,10 +396,10 @@ function encodeRawSearch(input) {
|
|
|
362
396
|
* whole-object schema's `validate`) runs — sibling of
|
|
363
397
|
* {@link readDeclaredValues} that reads all keys instead of declared-only
|
|
364
398
|
* ones (SS3: a whole-object schema has no declared keys of its own).
|
|
365
|
-
* Collapses each key's values by occurrence count
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
399
|
+
* Collapses each key's values by occurrence count: one value → `string`,
|
|
400
|
+
* multiple → `string[]`, uniformly for both `URLSearchParams` and
|
|
401
|
+
* Next-record sources, so the schema author writes one mental model
|
|
402
|
+
* regardless of which source it came from.
|
|
369
403
|
*/
|
|
370
404
|
function readAllValues(source) {
|
|
371
405
|
// The TS contract forbids non-object sources, but plain-JS callers reach
|
|
@@ -2,26 +2,26 @@ import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
|
2
2
|
import type { AnyRoute } from "./route.js";
|
|
3
3
|
import { type SearchOutputOf } from "./search.js";
|
|
4
4
|
/**
|
|
5
|
-
* Standard Schema generate-OUT
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Standard Schema generate-OUT: exports a route's `search:` config as a
|
|
6
|
+
* spec-compliant Standard Schema. The mirror of schema.ts, which runs
|
|
7
|
+
* Standard Schemas coming IN.
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
* The schema {@link standardSearchSchema} returns
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* The schema {@link standardSearchSchema} returns: input is advertised as
|
|
11
|
+
* the wire-shaped record only — `URLSearchParams` is accepted at runtime but
|
|
12
|
+
* kept out of the type, so client-side inference (tRPC) never sees a shape
|
|
13
|
+
* that cannot serialize over JSON. Output is the route's decoded search
|
|
14
|
+
* shape. `types` is carried by this annotation alone; the spec reads it at
|
|
15
|
+
* the type level only, so no runtime key exists.
|
|
16
16
|
*/
|
|
17
17
|
export type StandardSearchSchema<SC> = StandardSchemaV1<Record<string, string | string[] | undefined>, SearchOutputOf<SC>>;
|
|
18
18
|
/**
|
|
19
19
|
* Exports a route's `search:` config as the URL wire contract in Standard
|
|
20
|
-
* Schema form
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* Schema form, for consumers like tRPC inputs or TanStack `validateSearch`.
|
|
21
|
+
* Semantics are byte-identical to `decodeSearch`: defaults apply, `.catch()`
|
|
22
|
+
* recovers parse failures — invalid API input silently coerces to the
|
|
23
|
+
* fallback — unknown keys strip (P8), and duplicate values on a scalar codec
|
|
24
|
+
* reject (P5). No coercion, ever: the schema accepts wire strings (`"42"`),
|
|
25
|
+
* not decoded values (`42`).
|
|
26
26
|
*/
|
|
27
27
|
export declare function standardSearchSchema<R extends AnyRoute>(route: R): StandardSearchSchema<R["~search"]>;
|
package/dist/standard-schema.js
CHANGED
|
@@ -3,16 +3,16 @@ import { safeDecodeSearch } from "./safe-decode.js";
|
|
|
3
3
|
import { isRawSearch, requireSearchConfig, } from "./search.js";
|
|
4
4
|
/**
|
|
5
5
|
* Exports a route's `search:` config as the URL wire contract in Standard
|
|
6
|
-
* Schema form
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Schema form, for consumers like tRPC inputs or TanStack `validateSearch`.
|
|
7
|
+
* Semantics are byte-identical to `decodeSearch`: defaults apply, `.catch()`
|
|
8
|
+
* recovers parse failures — invalid API input silently coerces to the
|
|
9
|
+
* fallback — unknown keys strip (P8), and duplicate values on a scalar codec
|
|
10
|
+
* reject (P5). No coercion, ever: the schema accepts wire strings (`"42"`),
|
|
11
|
+
* not decoded values (`42`).
|
|
12
12
|
*/
|
|
13
13
|
export function standardSearchSchema(route) {
|
|
14
14
|
const config = route["~search"];
|
|
15
|
-
// A missing/malformed config is a programming error and stays loud
|
|
15
|
+
// A missing/malformed config is a programming error and stays loud;
|
|
16
16
|
// checking eagerly fails at construction, not at first validate().
|
|
17
17
|
requireSearchConfig(config);
|
|
18
18
|
// A config's raw/codec-map shape is fixed at construction; hoisted so the
|
|
@@ -31,7 +31,7 @@ export function standardSearchSchema(route) {
|
|
|
31
31
|
return { value: result.data };
|
|
32
32
|
}
|
|
33
33
|
catch (error) {
|
|
34
|
-
//
|
|
34
|
+
// validate() receives genuinely untrusted input, so the
|
|
35
35
|
// source-shape contract the read layer enforces with loud throws
|
|
36
36
|
// softens to issues at this one boundary — SearchSourceError exists
|
|
37
37
|
// as its own branded class exactly so this catch can't swallow
|
|
@@ -39,9 +39,9 @@ export function standardSearchSchema(route) {
|
|
|
39
39
|
if (error instanceof SearchSourceError) {
|
|
40
40
|
return { issues: [toSourceIssue(error)] };
|
|
41
41
|
}
|
|
42
|
-
// Async raw schema
|
|
43
|
-
//
|
|
44
|
-
//
|
|
42
|
+
// Async raw schema, throwing raw validators, and throwing
|
|
43
|
+
// .default()/.catch() factories are true programming errors: they
|
|
44
|
+
// stay loud.
|
|
45
45
|
throw error;
|
|
46
46
|
}
|
|
47
47
|
},
|
|
@@ -51,8 +51,8 @@ export function standardSearchSchema(route) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* Maps a source-shape violation to spec shape
|
|
55
|
-
*
|
|
54
|
+
* Maps a source-shape violation to spec shape: keyed where the read layer
|
|
55
|
+
* could attribute it, root-level (absent path) for a malformed source.
|
|
56
56
|
*/
|
|
57
57
|
function toSourceIssue(error) {
|
|
58
58
|
return error.key === null
|
|
@@ -62,8 +62,8 @@ function toSourceIssue(error) {
|
|
|
62
62
|
/**
|
|
63
63
|
* Maps a decode issue to spec shape. decodeRawSearch collapses a root-level
|
|
64
64
|
* schema issue to the "<search>" sentinel key (SS3/SS4); a Standard Schema
|
|
65
|
-
* expresses "root" as an ABSENT path, so the sentinel un-maps
|
|
66
|
-
*
|
|
65
|
+
* expresses "root" as an ABSENT path, so the sentinel un-maps back to a
|
|
66
|
+
* root-level issue here — for raw configs only, since a codec map's issues
|
|
67
67
|
* are always keyed and a param may literally be named "<search>". (A raw
|
|
68
68
|
* vendor issue whose real path is ["<search>"] still collides with the
|
|
69
69
|
* sentinel — indistinguishable after the flat-key collapse.) Nested
|