paramour 0.3.0 → 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 +2 -2
- package/dist/describe.d.ts +1 -1
- package/dist/describe.js +19 -7
- package/dist/p.d.ts +19 -2
- package/dist/p.js +111 -38
- package/package.json +1 -1
package/dist/codec.d.ts
CHANGED
|
@@ -66,8 +66,8 @@ export interface Codec<Out, P extends Presence = "required", C extends boolean =
|
|
|
66
66
|
/** Stored as a thunk regardless of the form passed to `.default()`. */
|
|
67
67
|
readonly "~defaultValue": (() => Out) | undefined;
|
|
68
68
|
/**
|
|
69
|
-
* Element codec of a composite list codec (
|
|
70
|
-
* per-segment scalar; undefined for every non-composite kind (CV6).
|
|
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
71
|
*/
|
|
72
72
|
readonly "~element": AnyCodec | undefined;
|
|
73
73
|
/** Members of a `p.enum` codec; undefined for every other kind. */
|
package/dist/describe.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface CodecDescription {
|
|
|
24
24
|
readonly defaultValue?: CodecDefaultDescription;
|
|
25
25
|
/**
|
|
26
26
|
* Nested description of a composite list codec's element scalar (CV6;
|
|
27
|
-
*
|
|
27
|
+
* `p.csv` and `p.array`).
|
|
28
28
|
*/
|
|
29
29
|
readonly element?: CodecDescription;
|
|
30
30
|
readonly enumMembers?: readonly string[];
|
package/dist/describe.js
CHANGED
|
@@ -10,8 +10,9 @@ export function describeCodec(codec) {
|
|
|
10
10
|
arity: codec["~arity"],
|
|
11
11
|
caught: codec["~caught"],
|
|
12
12
|
...(defaultValue === undefined ? {} : { defaultValue }),
|
|
13
|
-
// Recursion terminates:
|
|
14
|
-
//
|
|
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>>.
|
|
15
16
|
...(element === undefined ? {} : { element: describeCodec(element) }),
|
|
16
17
|
...(enumMembers === undefined ? {} : { enumMembers }),
|
|
17
18
|
kind: codec["~kind"],
|
|
@@ -63,11 +64,22 @@ export function formatCodecDescription(description, style) {
|
|
|
63
64
|
const kindLabel = (part) => part.enumMembers === undefined
|
|
64
65
|
? part.kind
|
|
65
66
|
: `enum(${part.enumMembers.join(memberSeparator)})`;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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);
|
|
71
83
|
if (style === "compact") {
|
|
72
84
|
if (description.presence === "optional")
|
|
73
85
|
label += "?";
|
package/dist/p.d.ts
CHANGED
|
@@ -5,10 +5,17 @@ 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>;
|
|
9
16
|
/**
|
|
10
17
|
* A comma-separated scalar list in ONE wire value (design-11 CV1): arity
|
|
11
|
-
* "single", so the full modifier set applies — unlike `p.
|
|
18
|
+
* "single", so the full modifier set applies — unlike `p.array`'s
|
|
12
19
|
* repeated-key format (CV7: both are first-class; csv is the one-key
|
|
13
20
|
* packing). Elements are strings unless an element codec is given.
|
|
14
21
|
*/
|
|
@@ -20,11 +27,21 @@ export declare const p: {
|
|
|
20
27
|
serialize: (value: Out) => string;
|
|
21
28
|
}): Codec<Out>;
|
|
22
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>>;
|
|
23
41
|
integer<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
|
|
24
42
|
isoDate(): Codec<Date>;
|
|
25
43
|
json<S extends StandardSchemaV1>(schema: S): Codec<StandardSchemaV1.InferOutput<S>>;
|
|
26
44
|
number<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
|
|
27
45
|
string<S extends StandardSchemaV1<string, string>>(schema?: S): Codec<S extends undefined ? string : StandardSchemaV1.InferOutput<S>>;
|
|
28
|
-
stringArray(): Codec<string[], "required", false, "many">;
|
|
29
46
|
timestamp(): Codec<Date>;
|
|
30
47
|
};
|
package/dist/p.js
CHANGED
|
@@ -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.
|
|
@@ -87,16 +121,43 @@ function stringifyJson(value) {
|
|
|
87
121
|
return rebrandForeign(() => JSON.stringify(value), (error) => new SerializeError("Value is not JSON-serializable", { cause: error }));
|
|
88
122
|
}
|
|
89
123
|
/**
|
|
90
|
-
* Shared element for no-arg `p.csv()` — codecs are immutable, so
|
|
91
|
-
* schemaless string codec serves every list. Lazily built: `p` does not
|
|
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
|
|
92
126
|
* exist yet while the module initializes.
|
|
93
127
|
*/
|
|
94
|
-
let
|
|
128
|
+
let defaultListElement;
|
|
95
129
|
/**
|
|
96
130
|
* The `p.*` wire-codec builders (DESIGN §5 layer 1, design-02 D9).
|
|
97
131
|
* Each codec defines how one value crosses the URL boundary, both directions.
|
|
98
132
|
*/
|
|
99
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
|
+
},
|
|
100
161
|
boolean() {
|
|
101
162
|
return createCodec({
|
|
102
163
|
kind: "boolean",
|
|
@@ -117,28 +178,26 @@ export const p = {
|
|
|
117
178
|
},
|
|
118
179
|
/**
|
|
119
180
|
* A comma-separated scalar list in ONE wire value (design-11 CV1): arity
|
|
120
|
-
* "single", so the full modifier set applies — unlike `p.
|
|
181
|
+
* "single", so the full modifier set applies — unlike `p.array`'s
|
|
121
182
|
* repeated-key format (CV7: both are first-class; csv is the one-key
|
|
122
183
|
* packing). Elements are strings unless an element codec is given.
|
|
123
184
|
*/
|
|
124
185
|
csv(element) {
|
|
125
186
|
// CV2: presence, catch, and arity-many inners are excluded by the
|
|
126
|
-
// parameter type (the D3 philosophy);
|
|
127
|
-
// type-state for JS consumers (the RL1 ethos)
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
134
198
|
if (inner["~element"] !== undefined) {
|
|
135
199
|
throw new ParamourError("p.csv() elements cannot themselves be csv lists");
|
|
136
200
|
}
|
|
137
|
-
if (inner["~arity"] === "many" ||
|
|
138
|
-
inner["~caught"] ||
|
|
139
|
-
inner["~presence"] !== "required") {
|
|
140
|
-
throw new ParamourError("p.csv() elements cannot carry modifiers (.optional()/.default()/.catch()) or be array codecs");
|
|
141
|
-
}
|
|
142
201
|
const parseInner = inner["~parseElement"];
|
|
143
202
|
const serializeInner = inner["~serializeElement"];
|
|
144
203
|
return createCodec({
|
|
@@ -226,21 +285,48 @@ export const p = {
|
|
|
226
285
|
},
|
|
227
286
|
});
|
|
228
287
|
},
|
|
229
|
-
|
|
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) {
|
|
230
299
|
return createCodec({
|
|
231
|
-
kind: "
|
|
300
|
+
kind: "index",
|
|
232
301
|
parseElement: (raw) => {
|
|
233
|
-
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;
|
|
234
307
|
return schema ? refine(schema, value) : value;
|
|
235
308
|
},
|
|
236
309
|
serializeElement: (value) => {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
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`);
|
|
241
313
|
}
|
|
242
|
-
|
|
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);
|
|
319
|
+
},
|
|
320
|
+
});
|
|
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;
|
|
243
328
|
},
|
|
329
|
+
serializeElement: (value) => String(serializeIntegerElement(schema, value)),
|
|
244
330
|
});
|
|
245
331
|
},
|
|
246
332
|
isoDate() {
|
|
@@ -311,19 +397,6 @@ export const p = {
|
|
|
311
397
|
},
|
|
312
398
|
});
|
|
313
399
|
},
|
|
314
|
-
stringArray() {
|
|
315
|
-
return createCodec({
|
|
316
|
-
arity: "many",
|
|
317
|
-
kind: "string",
|
|
318
|
-
parseElement: (raw) => raw,
|
|
319
|
-
serializeElement: (value) => {
|
|
320
|
-
if (typeof value !== "string") {
|
|
321
|
-
throw new SerializeError("Expected an array of strings");
|
|
322
|
-
}
|
|
323
|
-
return value;
|
|
324
|
-
},
|
|
325
|
-
});
|
|
326
|
-
},
|
|
327
400
|
timestamp() {
|
|
328
401
|
return createCodec({
|
|
329
402
|
kind: "timestamp",
|