paramour 0.2.1 → 0.4.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 +48 -7
- package/dist/codec.js +10 -1
- package/dist/describe.d.ts +20 -0
- package/dist/describe.js +63 -0
- package/dist/errors.d.ts +6 -2
- package/dist/errors.js +6 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/p.d.ts +25 -1
- package/dist/p.js +180 -22
- package/dist/path.js +7 -11
- package/dist/search.d.ts +32 -4
- package/dist/search.js +47 -20
- package/package.json +1 -1
package/dist/codec.d.ts
CHANGED
|
@@ -9,25 +9,41 @@ export type Arity = "many" | "single";
|
|
|
9
9
|
/**
|
|
10
10
|
* A bidirectional wire codec.
|
|
11
11
|
*
|
|
12
|
-
* `Out` is the decoded in-memory type. `P`, `C`, and `
|
|
12
|
+
* `Out` is the decoded in-memory type. `P`, `C`, `A`, and `E` are type-state:
|
|
13
13
|
* modifier methods are conditionally `never`, so illegal chains
|
|
14
14
|
* (`.optional().default()`, double `.catch()`) fail to compile (design-02 D3).
|
|
15
|
+
* `E` carries `~defaultElides` as a literal after `.default()` (NQ6a).
|
|
15
16
|
* Presence modifiers are also `never` for arity-"many" codecs: absent and `[]`
|
|
16
17
|
* are the same wire state (S6/P6), so `.default()`/`.optional()` could never
|
|
17
18
|
* round-trip there.
|
|
18
19
|
*
|
|
19
20
|
* `.default()` and `.catch()` accept either a value or a zero-arg factory;
|
|
20
21
|
* factories are invoked per decode/encode, so reference-typed defaults can be
|
|
21
|
-
* isolated per call
|
|
22
|
+
* isolated per call. Array values are shallow-copied per call for the same
|
|
23
|
+
* isolation; other plain object values are returned by reference.
|
|
22
24
|
*
|
|
23
25
|
* Properties prefixed `~` are internal machinery, not public API. For
|
|
24
26
|
* arity-"many" codecs the element functions operate on single elements of
|
|
25
27
|
* `Out` (which is an array type).
|
|
26
28
|
*/
|
|
27
|
-
export interface Codec<Out, P extends Presence = "required", C extends boolean = false, A extends Arity = "single"> {
|
|
28
|
-
readonly catch: C extends false ? (fallback: (() => Out) | Out) => Codec<Out, P, true, A> : never;
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
export interface Codec<Out, P extends Presence = "required", C extends boolean = false, A extends Arity = "single", E extends boolean = boolean> {
|
|
30
|
+
readonly catch: C extends false ? (fallback: (() => Out) | Out) => Codec<Out, P, true, A, E> : never;
|
|
31
|
+
/**
|
|
32
|
+
* Overloaded so the value/factory split is visible in type-state (NQ6a):
|
|
33
|
+
* the factory overload comes FIRST and must stay first. The runtime
|
|
34
|
+
* {@link isFactory} check treats ANY function as a factory, so a function
|
|
35
|
+
* argument must either match the factory overload or fail to compile
|
|
36
|
+
* ({@link NonFactoryValue}) — E=true is only ever inferred for arguments
|
|
37
|
+
* the runtime will also treat as values. The one statically-invisible
|
|
38
|
+
* residue: an argument whose static type is not a function (e.g.
|
|
39
|
+
* `unknown`) but holds one at runtime lands on the runtime's factory
|
|
40
|
+
* branch anyway; no type-level check can see that.
|
|
41
|
+
*/
|
|
42
|
+
readonly default: A extends "single" ? P extends "required" ? {
|
|
43
|
+
(value: () => Out): Codec<Out, "defaulted", C, A, false>;
|
|
44
|
+
<V extends Out>(value: NonFactoryValue<V> & V): Codec<Out, "defaulted", C, A, true>;
|
|
45
|
+
} : never : never;
|
|
46
|
+
readonly optional: A extends "single" ? P extends "required" ? () => Codec<Out, "optional", C, A, E> : never : never;
|
|
31
47
|
readonly "~arity": A;
|
|
32
48
|
/** Stored as a thunk regardless of the form passed to `.catch()`. */
|
|
33
49
|
readonly "~catchValue": (() => Out) | undefined;
|
|
@@ -38,10 +54,22 @@ export interface Codec<Out, P extends Presence = "required", C extends boolean =
|
|
|
38
54
|
* re-serialized per encode. Factory defaults never elide: a time-varying
|
|
39
55
|
* factory would elide an explicitly-passed value that later decodes as a
|
|
40
56
|
* different one.
|
|
57
|
+
*
|
|
58
|
+
* Literal-typed via `E` (NQ6a) so derived surfaces (`@paramour-js/nuqs`)
|
|
59
|
+
* can give value-defaulted keys non-nullable reads while keeping
|
|
60
|
+
* factory-defaulted keys honestly nullable. A hand-written
|
|
61
|
+
* `Codec<…, "defaulted">` leaves `E` at its `boolean` default, which
|
|
62
|
+
* consumers must treat as the factory (nullable) branch — the safe
|
|
63
|
+
* reading.
|
|
41
64
|
*/
|
|
42
|
-
readonly "~defaultElides":
|
|
65
|
+
readonly "~defaultElides": E;
|
|
43
66
|
/** Stored as a thunk regardless of the form passed to `.default()`. */
|
|
44
67
|
readonly "~defaultValue": (() => Out) | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Element codec of a composite list codec (`p.csv`, `p.array`) — the
|
|
70
|
+
* per-segment/per-key scalar; undefined for every non-composite kind (CV6).
|
|
71
|
+
*/
|
|
72
|
+
readonly "~element": AnyCodec | undefined;
|
|
45
73
|
/** Members of a `p.enum` codec; undefined for every other kind. */
|
|
46
74
|
readonly "~enumMembers": readonly string[] | undefined;
|
|
47
75
|
/**
|
|
@@ -66,11 +94,24 @@ export type ParamCodec = Codec<any, "required", boolean>;
|
|
|
66
94
|
*/
|
|
67
95
|
export type Presence = "defaulted" | "optional" | "required";
|
|
68
96
|
export type PresenceOf<C extends AnyCodec> = C["~presence"];
|
|
97
|
+
/**
|
|
98
|
+
* Rejects value-form `.default()` arguments whose static type includes any
|
|
99
|
+
* function member: runtime {@link isFactory} would treat them as factories,
|
|
100
|
+
* so letting them infer the value branch would let type-state assert an
|
|
101
|
+
* elision (`E = true`) the runtime never performs (NQ6a). Non-distributive
|
|
102
|
+
* on purpose — a union with a function member is rejected whole, since its
|
|
103
|
+
* runtime branch is unknowable at compile time.
|
|
104
|
+
*/
|
|
105
|
+
type NonFactoryValue<V> = [Extract<V, (...args: never[]) => unknown>] extends [
|
|
106
|
+
never
|
|
107
|
+
] ? unknown : never;
|
|
69
108
|
/** Internal factory used by the `p.*` builders. */
|
|
70
109
|
export declare function createCodec<Out, A extends Arity = "single">(impl: {
|
|
71
110
|
arity?: A;
|
|
111
|
+
element?: AnyCodec;
|
|
72
112
|
enumMembers?: readonly string[];
|
|
73
113
|
kind?: string;
|
|
74
114
|
parseElement: (raw: string) => unknown;
|
|
75
115
|
serializeElement: (value: unknown) => string;
|
|
76
116
|
}): Codec<Out, "required", false, A>;
|
|
117
|
+
export {};
|
package/dist/codec.js
CHANGED
|
@@ -6,6 +6,7 @@ export function createCodec(impl) {
|
|
|
6
6
|
catchValue: undefined,
|
|
7
7
|
defaultElides: false,
|
|
8
8
|
defaultValue: undefined,
|
|
9
|
+
element: impl.element,
|
|
9
10
|
enumMembers: impl.enumMembers,
|
|
10
11
|
kind: impl.kind ?? "custom",
|
|
11
12
|
parseElement: impl.parseElement,
|
|
@@ -60,6 +61,7 @@ function build(state) {
|
|
|
60
61
|
"~caught": state.catchValue !== undefined,
|
|
61
62
|
"~defaultElides": state.defaultElides,
|
|
62
63
|
"~defaultValue": state.defaultValue,
|
|
64
|
+
"~element": state.element,
|
|
63
65
|
"~enumMembers": state.enumMembers,
|
|
64
66
|
"~kind": state.kind,
|
|
65
67
|
"~parseElement": state.parseElement,
|
|
@@ -90,8 +92,15 @@ function serializeDefault(serializeElement, value) {
|
|
|
90
92
|
* the one chokepoint where a throwing user factory is branded ParamourError.
|
|
91
93
|
*/
|
|
92
94
|
function toThunk(stored, what) {
|
|
93
|
-
if (!isFactory(stored))
|
|
95
|
+
if (!isFactory(stored)) {
|
|
96
|
+
// Array values are handed out as fresh shallow copies: a consumer
|
|
97
|
+
// mutating a decoded fallback must not pollute later decodes or shift
|
|
98
|
+
// D8 elision (p.csv makes array defaults idiomatic — CV5). Non-array
|
|
99
|
+
// reference values stay by-reference; use a factory to isolate those.
|
|
100
|
+
if (Array.isArray(stored))
|
|
101
|
+
return () => stored.slice();
|
|
94
102
|
return () => stored;
|
|
103
|
+
}
|
|
95
104
|
return () => {
|
|
96
105
|
try {
|
|
97
106
|
return stored();
|
package/dist/describe.d.ts
CHANGED
|
@@ -22,10 +22,17 @@ export interface CodecDescription {
|
|
|
22
22
|
readonly arity: Arity;
|
|
23
23
|
readonly caught: boolean;
|
|
24
24
|
readonly defaultValue?: CodecDefaultDescription;
|
|
25
|
+
/**
|
|
26
|
+
* Nested description of a composite list codec's element scalar (CV6;
|
|
27
|
+
* `p.csv` and `p.array`).
|
|
28
|
+
*/
|
|
29
|
+
readonly element?: CodecDescription;
|
|
25
30
|
readonly enumMembers?: readonly string[];
|
|
26
31
|
readonly kind: string;
|
|
27
32
|
readonly presence: Presence;
|
|
28
33
|
}
|
|
34
|
+
/** Rendering styles accepted by {@link formatCodecDescription}. */
|
|
35
|
+
export type CodecFormatStyle = "compact" | "verbose";
|
|
29
36
|
/** A param codec plus the dynamic-segment kind that hosts it. */
|
|
30
37
|
export interface ParamDescription extends CodecDescription {
|
|
31
38
|
readonly segmentKind: "catchall" | "optional-catchall" | "single";
|
|
@@ -61,3 +68,16 @@ export declare function describeCodec(codec: AnyCodec): CodecDescription;
|
|
|
61
68
|
* both router brands — reflection only needs the data core.
|
|
62
69
|
*/
|
|
63
70
|
export declare function describeRoute(route: AnyRoute): RouteDescription;
|
|
71
|
+
/**
|
|
72
|
+
* One-line label for a {@link CodecDescription} — THE shared walk over the
|
|
73
|
+
* description's fields, so every consumer (the devtools panel's shape
|
|
74
|
+
* column, `paramour list`'s annotations) renders the same structure and a
|
|
75
|
+
* future field lands everywhere at once. Two skins over one walk:
|
|
76
|
+
*
|
|
77
|
+
* - `"compact"`: `enum(asc|desc)? =asc catch`, `csv<enum(a|b)>`, `string[]`
|
|
78
|
+
* — `?` for optional presence, the default's wire form (`=3`) or `=ƒ()`
|
|
79
|
+
* for a factory default, bare `catch`.
|
|
80
|
+
* - `"verbose"`: `enum(asc, desc) (optional) (default: asc) (catch)` —
|
|
81
|
+
* parenthesized annotations in fixed order: presence, default, catch.
|
|
82
|
+
*/
|
|
83
|
+
export declare function formatCodecDescription(description: CodecDescription, style: CodecFormatStyle): string;
|
package/dist/describe.js
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export function describeCodec(codec) {
|
|
6
6
|
const defaultValue = describeDefault(codec);
|
|
7
|
+
const element = codec["~element"];
|
|
7
8
|
const enumMembers = codec["~enumMembers"];
|
|
8
9
|
return {
|
|
9
10
|
arity: codec["~arity"],
|
|
10
11
|
caught: codec["~caught"],
|
|
11
12
|
...(defaultValue === undefined ? {} : { defaultValue }),
|
|
13
|
+
// Recursion terminates: composite nesting is bounded at construction —
|
|
14
|
+
// csv rejects nested csv (CV2) and array rejects arity-many inners
|
|
15
|
+
// (PP1), so the deepest legal chain is array<csv<scalar>>.
|
|
16
|
+
...(element === undefined ? {} : { element: describeCodec(element) }),
|
|
12
17
|
...(enumMembers === undefined ? {} : { enumMembers }),
|
|
13
18
|
kind: codec["~kind"],
|
|
14
19
|
presence: codec["~presence"],
|
|
@@ -42,6 +47,64 @@ export function describeRoute(route) {
|
|
|
42
47
|
search: describeSearch(route["~search"]),
|
|
43
48
|
};
|
|
44
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* One-line label for a {@link CodecDescription} — THE shared walk over the
|
|
52
|
+
* description's fields, so every consumer (the devtools panel's shape
|
|
53
|
+
* column, `paramour list`'s annotations) renders the same structure and a
|
|
54
|
+
* future field lands everywhere at once. Two skins over one walk:
|
|
55
|
+
*
|
|
56
|
+
* - `"compact"`: `enum(asc|desc)? =asc catch`, `csv<enum(a|b)>`, `string[]`
|
|
57
|
+
* — `?` for optional presence, the default's wire form (`=3`) or `=ƒ()`
|
|
58
|
+
* for a factory default, bare `catch`.
|
|
59
|
+
* - `"verbose"`: `enum(asc, desc) (optional) (default: asc) (catch)` —
|
|
60
|
+
* parenthesized annotations in fixed order: presence, default, catch.
|
|
61
|
+
*/
|
|
62
|
+
export function formatCodecDescription(description, style) {
|
|
63
|
+
const memberSeparator = style === "compact" ? "|" : ", ";
|
|
64
|
+
const kindLabel = (part) => part.enumMembers === undefined
|
|
65
|
+
? part.kind
|
|
66
|
+
: `enum(${part.enumMembers.join(memberSeparator)})`;
|
|
67
|
+
// Composite labels: a one-key list wraps its element (`csv<integer>`); a
|
|
68
|
+
// repeated-key list IS its element, pluralized (`integer[]`) — the
|
|
69
|
+
// "array" kind never appears in a label, the `[]` carries it. The elision
|
|
70
|
+
// keys on the kind, NOT arity: consumers force arity "many" onto
|
|
71
|
+
// non-array descriptions (render.ts's catch-all params), where a csv
|
|
72
|
+
// wrapper must survive as `csv<integer>[]`. Recursive so `array<csv<E>>`
|
|
73
|
+
// renders `csv<E>[]`.
|
|
74
|
+
const shapeLabel = (part) => {
|
|
75
|
+
const base = part.element === undefined
|
|
76
|
+
? kindLabel(part)
|
|
77
|
+
: part.kind === "array"
|
|
78
|
+
? shapeLabel(part.element)
|
|
79
|
+
: `${part.kind}<${shapeLabel(part.element)}>`;
|
|
80
|
+
return part.arity === "many" ? `${base}[]` : base;
|
|
81
|
+
};
|
|
82
|
+
let label = shapeLabel(description);
|
|
83
|
+
if (style === "compact") {
|
|
84
|
+
if (description.presence === "optional")
|
|
85
|
+
label += "?";
|
|
86
|
+
if (description.defaultValue !== undefined) {
|
|
87
|
+
label +=
|
|
88
|
+
description.defaultValue.kind === "value"
|
|
89
|
+
? ` =${description.defaultValue.wire}`
|
|
90
|
+
: " =ƒ()";
|
|
91
|
+
}
|
|
92
|
+
if (description.caught)
|
|
93
|
+
label += " catch";
|
|
94
|
+
return label;
|
|
95
|
+
}
|
|
96
|
+
const notes = [];
|
|
97
|
+
if (description.presence === "optional")
|
|
98
|
+
notes.push("(optional)");
|
|
99
|
+
if (description.defaultValue !== undefined) {
|
|
100
|
+
notes.push(description.defaultValue.kind === "value"
|
|
101
|
+
? `(default: ${description.defaultValue.wire})`
|
|
102
|
+
: "(default: factory)");
|
|
103
|
+
}
|
|
104
|
+
if (description.caught)
|
|
105
|
+
notes.push("(catch)");
|
|
106
|
+
return [label, ...notes].join(" ");
|
|
107
|
+
}
|
|
45
108
|
/**
|
|
46
109
|
* Value-form defaults re-serialize the live value (the D8 ethos — never a
|
|
47
110
|
* stale snapshot); a throwing serialize here means the default was mutated
|
package/dist/errors.d.ts
CHANGED
|
@@ -54,8 +54,12 @@ export declare class SerializeError extends ParamourError {
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function describeType(value: unknown): string;
|
|
56
56
|
/**
|
|
57
|
-
* Best-effort human-readable message for a foreign (non-paramour) throw
|
|
58
|
-
*
|
|
57
|
+
* Best-effort human-readable message for a foreign (non-paramour) throw:
|
|
58
|
+
* an `Error`'s message, else a {@link showValue}-hardened `String()` — safe
|
|
59
|
+
* even for values whose primitive conversion itself throws (null-prototype
|
|
60
|
+
* objects, `Symbol.toPrimitive` throwers). Public via the barrel so derived
|
|
61
|
+
* tooling that catches user-code throws (the devtools panel's edit preview)
|
|
62
|
+
* shares the hardening instead of re-implementing it minus the guard.
|
|
59
63
|
*/
|
|
60
64
|
export declare function foreignMessage(error: unknown): string;
|
|
61
65
|
/**
|
package/dist/errors.js
CHANGED
|
@@ -107,8 +107,12 @@ export function describeType(value) {
|
|
|
107
107
|
return value === null ? "null" : typeof value;
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
-
* Best-effort human-readable message for a foreign (non-paramour) throw
|
|
111
|
-
*
|
|
110
|
+
* Best-effort human-readable message for a foreign (non-paramour) throw:
|
|
111
|
+
* an `Error`'s message, else a {@link showValue}-hardened `String()` — safe
|
|
112
|
+
* even for values whose primitive conversion itself throws (null-prototype
|
|
113
|
+
* objects, `Symbol.toPrimitive` throwers). Public via the barrel so derived
|
|
114
|
+
* tooling that catches user-code throws (the devtools panel's edit preview)
|
|
115
|
+
* shares the hardening instead of re-implementing it minus the guard.
|
|
112
116
|
*/
|
|
113
117
|
export function foreignMessage(error) {
|
|
114
118
|
return error instanceof Error ? error.message : showValue(error);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { type AnyCodec, type Arity, type Codec, type OutputOf, type ParamCodec, type Presence, type PresenceOf, } from "./codec.js";
|
|
2
|
-
export { type CodecDefaultDescription, type CodecDescription, describeCodec, describeRoute, type ParamDescription, type RouteDescription, type SearchDescription, } from "./describe.js";
|
|
3
|
-
export { type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
2
|
+
export { type CodecDefaultDescription, type CodecDescription, type CodecFormatStyle, describeCodec, describeRoute, formatCodecDescription, type ParamDescription, type RouteDescription, type SearchDescription, } from "./describe.js";
|
|
3
|
+
export { foreignMessage, type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
4
4
|
export { href, type Href, type HrefArgs, type InferHrefInput, type StaticHrefOptions, } from "./href.js";
|
|
5
5
|
export { p } from "./p.js";
|
|
6
6
|
export { buildPath, decodeParams, type DecodeParamsOptions, encodeParams, encodeStaticParams, type InferStaticParams, type ParamsSource, } from "./path.js";
|
|
7
7
|
export { type AnyAppRoute, type AnyPagesRoute, type AnyRoute, type AppRoute, defineAppRoute, definePagesRoute, type InferRouteParams, type PagesContext, type PagesRoute, type ParamourRegister, type ParamsConfig, type ParamsProps, type RegisteredAppRoutePaths, type RegisteredPagesRoutePaths, type RegisteredStaticAppRoutePaths, type RegisteredStaticPagesRoutePaths, type RegisteredStaticRoutePaths, type Route, type RouteProps, type RouterKind, type SafeResult, type SearchProps, } from "./route.js";
|
|
8
8
|
export { safeDecodeParams, safeDecodeSearch } from "./safe-decode.js";
|
|
9
|
-
export { buildSearchString, decodeSearch, encodeSearch, type InferSearchInput, type InferSearchOutput, rawSearch, type RawSearch, type SearchConfig, type SearchOutputOf, type SearchSource, searchToString, } from "./search.js";
|
|
9
|
+
export { buildSearchString, decodeSearch, encodeSearch, type InferSearchInput, type InferSearchOutput, isRawSearch, parseValue, rawSearch, type RawSearch, type SearchConfig, type SearchOutputOf, type SearchSource, searchToString, serializeValue, } from "./search.js";
|
|
10
10
|
export { standardSearchSchema, type StandardSearchSchema, } from "./standard-schema.js";
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export {} from "./codec.js";
|
|
2
|
-
export { describeCodec, describeRoute, } from "./describe.js";
|
|
3
|
-
export { ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
2
|
+
export { describeCodec, describeRoute, formatCodecDescription, } from "./describe.js";
|
|
3
|
+
export { foreignMessage, ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
4
4
|
export { href, } from "./href.js";
|
|
5
5
|
export { p } from "./p.js";
|
|
6
6
|
export { buildPath, decodeParams, encodeParams, encodeStaticParams, } from "./path.js";
|
|
7
7
|
export { defineAppRoute, definePagesRoute, } from "./route.js";
|
|
8
8
|
export { safeDecodeParams, safeDecodeSearch } from "./safe-decode.js";
|
|
9
|
-
export { buildSearchString, decodeSearch, encodeSearch, rawSearch, searchToString, } from "./search.js";
|
|
9
|
+
export { buildSearchString, decodeSearch, encodeSearch, isRawSearch, parseValue, rawSearch, searchToString, serializeValue, } from "./search.js";
|
|
10
10
|
export { standardSearchSchema, } from "./standard-schema.js";
|
package/dist/p.d.ts
CHANGED
|
@@ -5,7 +5,21 @@ import { type Codec } from "./codec.js";
|
|
|
5
5
|
* Each codec defines how one value crosses the URL boundary, both directions.
|
|
6
6
|
*/
|
|
7
7
|
export declare const p: {
|
|
8
|
+
/**
|
|
9
|
+
* A repeated-key list (`?tags=a&tags=b`, design-13 PP1): arity "many", so
|
|
10
|
+
* presence modifiers are unavailable — absent and `[]` are the same wire
|
|
11
|
+
* state (S6/P6) — unlike `p.csv`'s one-key packing (CV7). Elements are
|
|
12
|
+
* strings unless an element codec is given.
|
|
13
|
+
*/
|
|
14
|
+
array<E = string>(element?: Codec<E>): Codec<E[], "required", false, "many">;
|
|
8
15
|
boolean(): Codec<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* A comma-separated scalar list in ONE wire value (design-11 CV1): arity
|
|
18
|
+
* "single", so the full modifier set applies — unlike `p.array`'s
|
|
19
|
+
* repeated-key format (CV7: both are first-class; csv is the one-key
|
|
20
|
+
* packing). Elements are strings unless an element codec is given.
|
|
21
|
+
*/
|
|
22
|
+
csv<E = string>(element?: Codec<E>): Codec<E[]>;
|
|
9
23
|
custom<Out>(codec: {
|
|
10
24
|
/** Reflection name shown by describeCodec/`paramour list` (default "custom"). */
|
|
11
25
|
label?: string;
|
|
@@ -13,11 +27,21 @@ export declare const p: {
|
|
|
13
27
|
serialize: (value: Out) => string;
|
|
14
28
|
}): Codec<Out>;
|
|
15
29
|
enum<const M extends readonly [string, ...string[]]>(members: M): Codec<M[number]>;
|
|
30
|
+
/**
|
|
31
|
+
* A 1-based-on-wire / 0-based-in-memory integer for pagination-style
|
|
32
|
+
* params (`?page=1` ↔ index 0, design-13 PP5). Deliberately stricter than
|
|
33
|
+
* nuqs's `parseAsIndex`, whose below-floor behavior is unspecified: wire
|
|
34
|
+
* values below 1 are a ParseError (recoverable via `.catch()`, like any
|
|
35
|
+
* other malformed input), and a negative in-memory index — which cannot
|
|
36
|
+
* round-trip through the 1-based wire floor — is a SerializeError at
|
|
37
|
+
* link-build time (the RL1 ethos). The optional schema validates the
|
|
38
|
+
* in-memory (0-based) value on both sides.
|
|
39
|
+
*/
|
|
40
|
+
index<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
|
|
16
41
|
integer<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
|
|
17
42
|
isoDate(): Codec<Date>;
|
|
18
43
|
json<S extends StandardSchemaV1>(schema: S): Codec<StandardSchemaV1.InferOutput<S>>;
|
|
19
44
|
number<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
|
|
20
45
|
string<S extends StandardSchemaV1<string, string>>(schema?: S): Codec<S extends undefined ? string : StandardSchemaV1.InferOutput<S>>;
|
|
21
|
-
stringArray(): Codec<string[], "required", false, "many">;
|
|
22
46
|
timestamp(): Codec<Date>;
|
|
23
47
|
};
|
package/dist/p.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCodec } from "./codec.js";
|
|
2
|
-
import { foreignMessage, ParseError, rebrandForeign, SerializeError, showValue, } from "./errors.js";
|
|
2
|
+
import { foreignMessage, ParamourError, ParseError, rebrandForeign, SerializeError, showValue, } from "./errors.js";
|
|
3
3
|
import { runStandardSchemaSync } from "./schema.js";
|
|
4
4
|
// Wire grammars per wire-format spec §4. `Number()` alone is too loose
|
|
5
5
|
// (accepts hex, trims whitespace), hence explicit anchored patterns.
|
|
@@ -73,12 +73,46 @@ function refineForSerialize(schema, value) {
|
|
|
73
73
|
}
|
|
74
74
|
return result.value;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Shared element admission for `p.array`/`p.csv` (PP1/CV2): resolves the
|
|
78
|
+
* no-arg default and mirrors the type-state exclusions — presence, catch,
|
|
79
|
+
* and arity-many inners — for JS consumers (the RL1 ethos). Only the
|
|
80
|
+
* element's parse/serialize functions are captured by the list builders, so
|
|
81
|
+
* an accepted modifier would be silently dropped, not applied; one guard
|
|
82
|
+
* keeps that runtime mirror of the type-state in a single place.
|
|
83
|
+
*/
|
|
84
|
+
function resolveListElement(element, builder) {
|
|
85
|
+
const inner = element ?? (defaultListElement ??= p.string());
|
|
86
|
+
if (inner["~arity"] === "many" ||
|
|
87
|
+
inner["~caught"] ||
|
|
88
|
+
inner["~presence"] !== "required") {
|
|
89
|
+
throw new ParamourError(`${builder}() elements cannot carry modifiers (.optional()/.default()/.catch()) or be array codecs`);
|
|
90
|
+
}
|
|
91
|
+
return inner;
|
|
92
|
+
}
|
|
76
93
|
function serializeFiniteNumber(value) {
|
|
77
94
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
78
95
|
throw new SerializeError(`Expected a finite number, got ${showValue(value)}`);
|
|
79
96
|
}
|
|
80
97
|
return String(value);
|
|
81
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Serialize-side twin of {@link parseIntegerElement}, shared by `p.integer`
|
|
101
|
+
* and `p.index`: schema refinement plus the finite and safe-integer guards.
|
|
102
|
+
* Returns the refined NUMBER — `p.integer` stringifies it as-is, `p.index`
|
|
103
|
+
* shifts it into the 1-based wire form first — so the happy path pays no
|
|
104
|
+
* throwaway stringification.
|
|
105
|
+
*/
|
|
106
|
+
function serializeIntegerElement(schema, value) {
|
|
107
|
+
const refined = schema ? refineForSerialize(schema, value) : value;
|
|
108
|
+
if (typeof refined !== "number" || !Number.isFinite(refined)) {
|
|
109
|
+
throw new SerializeError(`Expected a finite number, got ${showValue(refined)}`);
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isSafeInteger(refined)) {
|
|
112
|
+
throw new SerializeError(`${String(refined)} is not a safe integer`);
|
|
113
|
+
}
|
|
114
|
+
return refined;
|
|
115
|
+
}
|
|
82
116
|
/**
|
|
83
117
|
* JSON.stringify throws raw TypeErrors (circular refs, BigInt) and lets
|
|
84
118
|
* toJSON() exceptions escape; wrap them so the ParamourError contract holds.
|
|
@@ -86,11 +120,44 @@ function serializeFiniteNumber(value) {
|
|
|
86
120
|
function stringifyJson(value) {
|
|
87
121
|
return rebrandForeign(() => JSON.stringify(value), (error) => new SerializeError("Value is not JSON-serializable", { cause: error }));
|
|
88
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Shared element for no-arg `p.csv()`/`p.array()` — codecs are immutable, so
|
|
125
|
+
* one schemaless string codec serves every list. Lazily built: `p` does not
|
|
126
|
+
* exist yet while the module initializes.
|
|
127
|
+
*/
|
|
128
|
+
let defaultListElement;
|
|
89
129
|
/**
|
|
90
130
|
* The `p.*` wire-codec builders (DESIGN §5 layer 1, design-02 D9).
|
|
91
131
|
* Each codec defines how one value crosses the URL boundary, both directions.
|
|
92
132
|
*/
|
|
93
133
|
export const p = {
|
|
134
|
+
/**
|
|
135
|
+
* A repeated-key list (`?tags=a&tags=b`, design-13 PP1): arity "many", so
|
|
136
|
+
* presence modifiers are unavailable — absent and `[]` are the same wire
|
|
137
|
+
* state (S6/P6) — unlike `p.csv`'s one-key packing (CV7). Elements are
|
|
138
|
+
* strings unless an element codec is given.
|
|
139
|
+
*/
|
|
140
|
+
array(element) {
|
|
141
|
+
// PP1: same element-by-composition shape as p.csv (CV2) — presence,
|
|
142
|
+
// catch, and arity-many inners are excluded by the parameter type;
|
|
143
|
+
// resolveListElement mirrors that type-state for JS consumers. Unlike
|
|
144
|
+
// csv there is no nested-composite special case: a csv element is a
|
|
145
|
+
// legal whole-value scalar per repeated key (?m=a,b&m=c,d), and
|
|
146
|
+
// repeated-key values have no separator for element serializations to
|
|
147
|
+
// collide with, so no CV4 twin is needed either.
|
|
148
|
+
const inner = resolveListElement(element, "p.array");
|
|
149
|
+
// The element functions already have the arity-"many" per-element
|
|
150
|
+
// contract (one wire value per array item), so they pass through as-is;
|
|
151
|
+
// the string-return contract on custom serializers is enforced at the
|
|
152
|
+
// search.ts/path.ts call sites, same as using the element directly.
|
|
153
|
+
return createCodec({
|
|
154
|
+
arity: "many",
|
|
155
|
+
element: inner,
|
|
156
|
+
kind: "array",
|
|
157
|
+
parseElement: inner["~parseElement"],
|
|
158
|
+
serializeElement: inner["~serializeElement"],
|
|
159
|
+
});
|
|
160
|
+
},
|
|
94
161
|
boolean() {
|
|
95
162
|
return createCodec({
|
|
96
163
|
kind: "boolean",
|
|
@@ -109,6 +176,83 @@ export const p = {
|
|
|
109
176
|
},
|
|
110
177
|
});
|
|
111
178
|
},
|
|
179
|
+
/**
|
|
180
|
+
* A comma-separated scalar list in ONE wire value (design-11 CV1): arity
|
|
181
|
+
* "single", so the full modifier set applies — unlike `p.array`'s
|
|
182
|
+
* repeated-key format (CV7: both are first-class; csv is the one-key
|
|
183
|
+
* packing). Elements are strings unless an element codec is given.
|
|
184
|
+
*/
|
|
185
|
+
csv(element) {
|
|
186
|
+
// CV2: presence, catch, and arity-many inners are excluded by the
|
|
187
|
+
// parameter type (the D3 philosophy); resolveListElement mirrors that
|
|
188
|
+
// type-state for JS consumers (the RL1 ethos). Nesting is detected
|
|
189
|
+
// structurally via ~element (never via ~kind, which is reflection-only
|
|
190
|
+
// and free-form for p.custom labels). Comma-emitting p.custom inners
|
|
191
|
+
// are undetectable here and are caught by the CV4 serialize guard
|
|
192
|
+
// instead.
|
|
193
|
+
const inner = resolveListElement(element, "p.csv");
|
|
194
|
+
// The shared guard ran first: p.array also carries ~element (PP1), and
|
|
195
|
+
// the arity guard owns the "array codecs" wording — the ~element guard
|
|
196
|
+
// here is then specifically the nested-csv (arity-"single" composite)
|
|
197
|
+
// case.
|
|
198
|
+
if (inner["~element"] !== undefined) {
|
|
199
|
+
throw new ParamourError("p.csv() elements cannot themselves be csv lists");
|
|
200
|
+
}
|
|
201
|
+
const parseInner = inner["~parseElement"];
|
|
202
|
+
const serializeInner = inner["~serializeElement"];
|
|
203
|
+
return createCodec({
|
|
204
|
+
element: inner,
|
|
205
|
+
kind: "csv",
|
|
206
|
+
parseElement: (raw) => {
|
|
207
|
+
// CV3: the empty wire string is [] — checked before split, because
|
|
208
|
+
// "".split(",") is [""], which the strict grammar would reject.
|
|
209
|
+
if (raw === "")
|
|
210
|
+
return [];
|
|
211
|
+
const result = [];
|
|
212
|
+
for (const segment of raw.split(",")) {
|
|
213
|
+
// CV3: strict grammar — "a,,b", trailing "a,", and a lone "," are
|
|
214
|
+
// ParseErrors, recoverable via the LIST's .catch() (D2).
|
|
215
|
+
if (segment === "") {
|
|
216
|
+
throw new ParseError(`"${raw}" contains an empty list element`);
|
|
217
|
+
}
|
|
218
|
+
// CV3: the first element failure aborts the list parse — the
|
|
219
|
+
// element codec's own ParseError propagates unwrapped.
|
|
220
|
+
result.push(parseInner(segment));
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
},
|
|
224
|
+
serializeElement: (value) => {
|
|
225
|
+
if (!Array.isArray(value)) {
|
|
226
|
+
throw new SerializeError(`Expected an array, got ${showValue(value)}`);
|
|
227
|
+
}
|
|
228
|
+
const parts = [];
|
|
229
|
+
for (const item of value) {
|
|
230
|
+
const serialized = serializeInner(item);
|
|
231
|
+
// A plain-JS custom element serializer can return a non-string;
|
|
232
|
+
// enforce the string contract here — as search.ts/path.ts do at
|
|
233
|
+
// their ~serializeElement call sites — so the CV4 guards below
|
|
234
|
+
// cannot throw raw TypeErrors.
|
|
235
|
+
if (typeof serialized !== "string") {
|
|
236
|
+
throw new SerializeError(`List element serializer must return a string, got ${typeof serialized}`);
|
|
237
|
+
}
|
|
238
|
+
// CV4: an empty segment on re-parse; [""] is deliberately
|
|
239
|
+
// unrepresentable — the empty wire string already means [].
|
|
240
|
+
if (serialized === "") {
|
|
241
|
+
throw new SerializeError(`List element ${showValue(item)} serializes to the empty string`);
|
|
242
|
+
}
|
|
243
|
+
// CV4: would mis-split on re-parse.
|
|
244
|
+
if (serialized.includes(",")) {
|
|
245
|
+
throw new SerializeError(`List element serialization "${serialized}" contains a comma`);
|
|
246
|
+
}
|
|
247
|
+
parts.push(serialized);
|
|
248
|
+
}
|
|
249
|
+
// [] joins to "" (CV5) — which is also why .default([])'s
|
|
250
|
+
// define-time pre-serialization succeeds and D8 elision compares
|
|
251
|
+
// [] against the empty wire form.
|
|
252
|
+
return parts.join(",");
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
},
|
|
112
256
|
custom(codec) {
|
|
113
257
|
// Paramour's own errors are never downgraded: ANY ParamourError thrown
|
|
114
258
|
// by user parse/serialize code — config-level failures (async schema,
|
|
@@ -141,23 +285,50 @@ export const p = {
|
|
|
141
285
|
},
|
|
142
286
|
});
|
|
143
287
|
},
|
|
144
|
-
|
|
288
|
+
/**
|
|
289
|
+
* A 1-based-on-wire / 0-based-in-memory integer for pagination-style
|
|
290
|
+
* params (`?page=1` ↔ index 0, design-13 PP5). Deliberately stricter than
|
|
291
|
+
* nuqs's `parseAsIndex`, whose below-floor behavior is unspecified: wire
|
|
292
|
+
* values below 1 are a ParseError (recoverable via `.catch()`, like any
|
|
293
|
+
* other malformed input), and a negative in-memory index — which cannot
|
|
294
|
+
* round-trip through the 1-based wire floor — is a SerializeError at
|
|
295
|
+
* link-build time (the RL1 ethos). The optional schema validates the
|
|
296
|
+
* in-memory (0-based) value on both sides.
|
|
297
|
+
*/
|
|
298
|
+
index(schema) {
|
|
145
299
|
return createCodec({
|
|
146
|
-
kind: "
|
|
300
|
+
kind: "index",
|
|
147
301
|
parseElement: (raw) => {
|
|
148
|
-
const
|
|
302
|
+
const wire = parseIntegerElement(raw);
|
|
303
|
+
if (wire < 1) {
|
|
304
|
+
throw new ParseError(`"${raw}" is below the 1-based wire floor of 1`);
|
|
305
|
+
}
|
|
306
|
+
const value = wire - 1;
|
|
149
307
|
return schema ? refine(schema, value) : value;
|
|
150
308
|
},
|
|
151
309
|
serializeElement: (value) => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
throw new SerializeError(`${serialized} is not a safe integer`);
|
|
310
|
+
const index = serializeIntegerElement(schema, value);
|
|
311
|
+
if (index < 0) {
|
|
312
|
+
throw new SerializeError(`${String(index)} is negative and cannot round-trip through the 1-based wire form`);
|
|
156
313
|
}
|
|
157
|
-
|
|
314
|
+
const wire = index + 1;
|
|
315
|
+
if (!Number.isSafeInteger(wire)) {
|
|
316
|
+
throw new SerializeError(`${String(index)} is outside the 1-based wire form's safe integer range`);
|
|
317
|
+
}
|
|
318
|
+
return String(wire);
|
|
158
319
|
},
|
|
159
320
|
});
|
|
160
321
|
},
|
|
322
|
+
integer(schema) {
|
|
323
|
+
return createCodec({
|
|
324
|
+
kind: "integer",
|
|
325
|
+
parseElement: (raw) => {
|
|
326
|
+
const value = parseIntegerElement(raw);
|
|
327
|
+
return schema ? refine(schema, value) : value;
|
|
328
|
+
},
|
|
329
|
+
serializeElement: (value) => String(serializeIntegerElement(schema, value)),
|
|
330
|
+
});
|
|
331
|
+
},
|
|
161
332
|
isoDate() {
|
|
162
333
|
return createCodec({
|
|
163
334
|
kind: "isoDate",
|
|
@@ -226,19 +397,6 @@ export const p = {
|
|
|
226
397
|
},
|
|
227
398
|
});
|
|
228
399
|
},
|
|
229
|
-
stringArray() {
|
|
230
|
-
return createCodec({
|
|
231
|
-
arity: "many",
|
|
232
|
-
kind: "string",
|
|
233
|
-
parseElement: (raw) => raw,
|
|
234
|
-
serializeElement: (value) => {
|
|
235
|
-
if (typeof value !== "string") {
|
|
236
|
-
throw new SerializeError("Expected an array of strings");
|
|
237
|
-
}
|
|
238
|
-
return value;
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
},
|
|
242
400
|
timestamp() {
|
|
243
401
|
return createCodec({
|
|
244
402
|
kind: "timestamp",
|
package/dist/path.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describeType, ParamourError, ParamsDecodeError, ParseError, SerializeError, } from "./errors.js";
|
|
2
|
-
import { encodeComponent, readInputValue } from "./search.js";
|
|
2
|
+
import { encodeComponent, readInputValue, serializeValue } from "./search.js";
|
|
3
3
|
// Anchored per the wire-format spec's regex ethos; name charset excludes
|
|
4
4
|
// brackets so nesting can't smuggle through. Match order mirrors the type
|
|
5
5
|
// grammar: `[[...` before `[...` before `[`.
|
|
@@ -394,18 +394,14 @@ function serializeDynamicSegment(codec, segment, value) {
|
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
/**
|
|
397
|
-
* Serializes one segment value into its wire-string form.
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
* static surfaces must skip it.
|
|
397
|
+
* Serializes one segment value into its wire-string form. The string
|
|
398
|
+
* contract is search.ts's shared {@link serializeValue}; the path surface
|
|
399
|
+
* layers R4 on top (a segment additionally can't be empty).
|
|
400
|
+
* Percent-encoding is deliberately NOT applied here: that is encodeParams'
|
|
401
|
+
* byte-layer step (S7), and the static surfaces must skip it.
|
|
403
402
|
*/
|
|
404
403
|
function serializeSegmentValue(codec, name, value) {
|
|
405
|
-
const serialized = codec
|
|
406
|
-
if (typeof serialized !== "string") {
|
|
407
|
-
throw new SerializeError(`serializer for route param "${name}" must return a string, got ${typeof serialized}`);
|
|
408
|
-
}
|
|
404
|
+
const serialized = serializeValue(codec, `route param "${name}"`, value);
|
|
409
405
|
if (serialized === "") {
|
|
410
406
|
// R4: "" would produce "//" or a vanishing segment — same rationale as R3.
|
|
411
407
|
throw new SerializeError(`route param "${name}" serialized to an empty string, which cannot form a path segment`);
|
package/dist/search.d.ts
CHANGED
|
@@ -112,12 +112,28 @@ export declare function encodeComponent(text: string): string;
|
|
|
112
112
|
*/
|
|
113
113
|
export declare function encodeSearch<S extends SearchSlot>(config: S, input: SearchInputOf<S>): [string, string][];
|
|
114
114
|
/**
|
|
115
|
-
* Runtime discriminant for the `search:` slot (design-04 SS2): the
|
|
116
|
-
* `~kind` marker is unambiguous against a codec map
|
|
117
|
-
*
|
|
118
|
-
* barrel
|
|
115
|
+
* Runtime discriminant for the `search:` slot (design-04 SS2): probes the
|
|
116
|
+
* `~kind` marker's VALUE, which is unambiguous against a codec map — a map
|
|
117
|
+
* key literally named "~kind" would hold a codec object, never the marker
|
|
118
|
+
* string. Exported from the package barrel so derived surfaces
|
|
119
|
+
* (`@paramour-js/nuqs`) share the discriminant instead of duplicating the
|
|
120
|
+
* literal.
|
|
119
121
|
*/
|
|
120
122
|
export declare function isRawSearch(config: SearchSlot): config is RawSearch<StandardSchemaV1>;
|
|
123
|
+
/**
|
|
124
|
+
* Invokes a codec's element parser on one wire string — the parse twin of
|
|
125
|
+
* {@link serializeValue}, and the ONE sanctioned way to run a parse WITHOUT
|
|
126
|
+
* the codec's `.catch()` recovery applied (decodeSearch always recovers a
|
|
127
|
+
* caught failure, so a probe through it cannot tell "parsed cleanly" from
|
|
128
|
+
* "failed and was caught"). Exists for reflection-driven tooling — the
|
|
129
|
+
* devtools panel's catch-attribution probe (design-12 DT7) — and any other
|
|
130
|
+
* derived surface that must observe the raw parse outcome. A parse failure
|
|
131
|
+
* throws the codec's own {@link ParseError}; foreign throws from a custom
|
|
132
|
+
* codec propagate unwrapped, matching decodeSearch's taxonomy. For
|
|
133
|
+
* arity-"many" codecs this parses ONE element of the repeated-key array,
|
|
134
|
+
* not the whole array (the same contract as `~parseElement` itself).
|
|
135
|
+
*/
|
|
136
|
+
export declare function parseValue(codec: AnyCodec, raw: string): unknown;
|
|
121
137
|
/**
|
|
122
138
|
* The whole-object search escape hatch (design-04 SS1, maintainer ruling):
|
|
123
139
|
* an explicit, greppable wrapper around a bare Standard Schema so a route's
|
|
@@ -152,4 +168,16 @@ export declare function readInputValue(values: Record<string, unknown>, key: str
|
|
|
152
168
|
export declare function requireSearchConfig(config: SearchSlot): void;
|
|
153
169
|
/** Convenience: encode + build in one step. */
|
|
154
170
|
export declare function searchToString<S extends SearchSlot>(config: S, input: SearchInputOf<S>): string;
|
|
171
|
+
/**
|
|
172
|
+
* Invokes a codec's serializer and enforces its string contract: a custom
|
|
173
|
+
* codec written in plain JS can return undefined, which would otherwise
|
|
174
|
+
* reach the byte layer as the literal text "undefined" — or, worse, match
|
|
175
|
+
* an absent default and silently drop the param. `label` names the value's
|
|
176
|
+
* site in the error (`search param "q"`, `route param "id"`, …). Exported
|
|
177
|
+
* from the package barrel as the ONE implementation of this contract:
|
|
178
|
+
* path.ts segments and derived surfaces (`@paramour-js/nuqs` eq/
|
|
179
|
+
* clearOnDefault) share it so their judgment stays identical to D8
|
|
180
|
+
* elision's by construction, not by parallel copies.
|
|
181
|
+
*/
|
|
182
|
+
export declare function serializeValue(codec: AnyCodec, label: string, value: unknown): string;
|
|
155
183
|
export {};
|
package/dist/search.js
CHANGED
|
@@ -164,7 +164,10 @@ export function encodeSearch(config, input) {
|
|
|
164
164
|
}
|
|
165
165
|
// Array codecs cannot carry defaults, so no elision applies.
|
|
166
166
|
for (const element of value) {
|
|
167
|
-
pairs.push([
|
|
167
|
+
pairs.push([
|
|
168
|
+
key,
|
|
169
|
+
serializeValue(codec, `search param "${key}"`, element),
|
|
170
|
+
]);
|
|
168
171
|
}
|
|
169
172
|
continue;
|
|
170
173
|
}
|
|
@@ -174,13 +177,14 @@ export function encodeSearch(config, input) {
|
|
|
174
177
|
}
|
|
175
178
|
continue; // absent optional/defaulted param → key omitted (S3)
|
|
176
179
|
}
|
|
177
|
-
const serialized = serializeValue(codec, key
|
|
180
|
+
const serialized = serializeValue(codec, `search param "${key}"`, value);
|
|
178
181
|
// D8 elision, gated on an elidable default existing — an ungated
|
|
179
182
|
// comparison would let a (contract-violating) serialize that returns
|
|
180
183
|
// undefined match undefined and silently drop the param.
|
|
181
184
|
if (codec["~defaultElides"] &&
|
|
182
185
|
codec["~defaultValue"] !== undefined &&
|
|
183
|
-
serialized ===
|
|
186
|
+
serialized ===
|
|
187
|
+
serializeValue(codec, `search param "${key}"`, codec["~defaultValue"]())) {
|
|
184
188
|
continue;
|
|
185
189
|
}
|
|
186
190
|
pairs.push([key, serialized]);
|
|
@@ -188,14 +192,32 @@ export function encodeSearch(config, input) {
|
|
|
188
192
|
return pairs;
|
|
189
193
|
}
|
|
190
194
|
/**
|
|
191
|
-
* Runtime discriminant for the `search:` slot (design-04 SS2): the
|
|
192
|
-
* `~kind` marker is unambiguous against a codec map
|
|
193
|
-
*
|
|
194
|
-
* barrel
|
|
195
|
+
* Runtime discriminant for the `search:` slot (design-04 SS2): probes the
|
|
196
|
+
* `~kind` marker's VALUE, which is unambiguous against a codec map — a map
|
|
197
|
+
* key literally named "~kind" would hold a codec object, never the marker
|
|
198
|
+
* string. Exported from the package barrel so derived surfaces
|
|
199
|
+
* (`@paramour-js/nuqs`) share the discriminant instead of duplicating the
|
|
200
|
+
* literal.
|
|
195
201
|
*/
|
|
196
202
|
export function isRawSearch(config) {
|
|
197
203
|
return "~kind" in config && config["~kind"] === "raw-search";
|
|
198
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Invokes a codec's element parser on one wire string — the parse twin of
|
|
207
|
+
* {@link serializeValue}, and the ONE sanctioned way to run a parse WITHOUT
|
|
208
|
+
* the codec's `.catch()` recovery applied (decodeSearch always recovers a
|
|
209
|
+
* caught failure, so a probe through it cannot tell "parsed cleanly" from
|
|
210
|
+
* "failed and was caught"). Exists for reflection-driven tooling — the
|
|
211
|
+
* devtools panel's catch-attribution probe (design-12 DT7) — and any other
|
|
212
|
+
* derived surface that must observe the raw parse outcome. A parse failure
|
|
213
|
+
* throws the codec's own {@link ParseError}; foreign throws from a custom
|
|
214
|
+
* codec propagate unwrapped, matching decodeSearch's taxonomy. For
|
|
215
|
+
* arity-"many" codecs this parses ONE element of the repeated-key array,
|
|
216
|
+
* not the whole array (the same contract as `~parseElement` itself).
|
|
217
|
+
*/
|
|
218
|
+
export function parseValue(codec, raw) {
|
|
219
|
+
return codec["~parseElement"](raw);
|
|
220
|
+
}
|
|
199
221
|
/**
|
|
200
222
|
* The whole-object search escape hatch (design-04 SS1, maintainer ruling):
|
|
201
223
|
* an explicit, greppable wrapper around a bare Standard Schema so a route's
|
|
@@ -255,6 +277,24 @@ export function requireSearchConfig(config) {
|
|
|
255
277
|
export function searchToString(config, input) {
|
|
256
278
|
return buildSearchString(encodeSearch(config, input));
|
|
257
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Invokes a codec's serializer and enforces its string contract: a custom
|
|
282
|
+
* codec written in plain JS can return undefined, which would otherwise
|
|
283
|
+
* reach the byte layer as the literal text "undefined" — or, worse, match
|
|
284
|
+
* an absent default and silently drop the param. `label` names the value's
|
|
285
|
+
* site in the error (`search param "q"`, `route param "id"`, …). Exported
|
|
286
|
+
* from the package barrel as the ONE implementation of this contract:
|
|
287
|
+
* path.ts segments and derived surfaces (`@paramour-js/nuqs` eq/
|
|
288
|
+
* clearOnDefault) share it so their judgment stays identical to D8
|
|
289
|
+
* elision's by construction, not by parallel copies.
|
|
290
|
+
*/
|
|
291
|
+
export function serializeValue(codec, label, value) {
|
|
292
|
+
const serialized = codec["~serializeElement"](value);
|
|
293
|
+
if (typeof serialized !== "string") {
|
|
294
|
+
throw new SerializeError(`serializer for ${label} must return a string, got ${typeof serialized}`);
|
|
295
|
+
}
|
|
296
|
+
return serialized;
|
|
297
|
+
}
|
|
258
298
|
/**
|
|
259
299
|
* The `RawSearch` decode path (design-04 SS3/SS4). The schema receives EVERY
|
|
260
300
|
* source key, normalized to Next's own `searchParams` shape — P8's
|
|
@@ -459,16 +499,3 @@ function requireRawSearchString(key, value) {
|
|
|
459
499
|
}
|
|
460
500
|
return value;
|
|
461
501
|
}
|
|
462
|
-
/**
|
|
463
|
-
* Invokes a codec's serializer and enforces its string contract: a custom
|
|
464
|
-
* codec written in plain JS can return undefined, which would otherwise
|
|
465
|
-
* reach the byte layer as the literal text "undefined" — or, worse, match
|
|
466
|
-
* an absent default and silently drop the param.
|
|
467
|
-
*/
|
|
468
|
-
function serializeValue(codec, key, value) {
|
|
469
|
-
const serialized = codec["~serializeElement"](value);
|
|
470
|
-
if (typeof serialized !== "string") {
|
|
471
|
-
throw new SerializeError(`serializer for search param "${key}" must return a string, got ${typeof serialized}`);
|
|
472
|
-
}
|
|
473
|
-
return serialized;
|
|
474
|
-
}
|