paramour 0.1.0 → 0.2.1
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/errors.d.ts +18 -0
- package/dist/errors.js +29 -0
- package/dist/href.d.ts +21 -1
- package/dist/href.js +18 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/path.js +4 -4
- package/dist/route.d.ts +40 -0
- package/dist/route.js +3 -3
- package/dist/search.d.ts +16 -0
- package/dist/search.js +34 -32
- package/dist/standard-schema.d.ts +27 -0
- package/dist/standard-schema.js +77 -0
- package/package.json +5 -1
package/dist/errors.d.ts
CHANGED
|
@@ -31,10 +31,28 @@ export declare class SearchDecodeError extends ParamourError {
|
|
|
31
31
|
constructor(issues: readonly Issue[]);
|
|
32
32
|
static [Symbol.hasInstance](value: unknown): value is SearchDecodeError;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* A search source violated its wire-shape contract (design-08 STD7): a
|
|
36
|
+
* non-object source, or a non-string / non-string[] value under a read key.
|
|
37
|
+
* Thrown by search.ts's source readers; distinct from {@link ParamourError}
|
|
38
|
+
* so the Standard Schema adapter can soften exactly these throws to issues
|
|
39
|
+
* while config-contract violations and rebranded validator throws stay loud.
|
|
40
|
+
*/
|
|
41
|
+
export declare class SearchSourceError extends ParamourError {
|
|
42
|
+
/** The offending source key, or null when the source itself is malformed. */
|
|
43
|
+
readonly key: null | string;
|
|
44
|
+
constructor(message: string, key: null | string);
|
|
45
|
+
static [Symbol.hasInstance](value: unknown): value is SearchSourceError;
|
|
46
|
+
}
|
|
34
47
|
/** A value could not be serialized to the wire (bad type, non-finite, etc.). */
|
|
35
48
|
export declare class SerializeError extends ParamourError {
|
|
36
49
|
static [Symbol.hasInstance](value: unknown): value is SerializeError;
|
|
37
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Renders a value's type for "…, got X" error messages, distinguishing null
|
|
53
|
+
* from typeof's "object". Not exported from the package.
|
|
54
|
+
*/
|
|
55
|
+
export declare function describeType(value: unknown): string;
|
|
38
56
|
/**
|
|
39
57
|
* Best-effort human-readable message for a foreign (non-paramour) throw.
|
|
40
58
|
* Not exported from the package — internal to error branding.
|
package/dist/errors.js
CHANGED
|
@@ -11,6 +11,7 @@ const paramourErrorBrand = Symbol.for("paramour.errors.ParamourError");
|
|
|
11
11
|
const paramsDecodeErrorBrand = Symbol.for("paramour.errors.ParamsDecodeError");
|
|
12
12
|
const parseErrorBrand = Symbol.for("paramour.errors.ParseError");
|
|
13
13
|
const searchDecodeErrorBrand = Symbol.for("paramour.errors.SearchDecodeError");
|
|
14
|
+
const searchSourceErrorBrand = Symbol.for("paramour.errors.SearchSourceError");
|
|
14
15
|
const serializeErrorBrand = Symbol.for("paramour.errors.SerializeError");
|
|
15
16
|
/** Base class for every error paramour throws. */
|
|
16
17
|
export class ParamourError extends Error {
|
|
@@ -68,6 +69,27 @@ export class SearchDecodeError extends ParamourError {
|
|
|
68
69
|
return hasBrand(value, searchDecodeErrorBrand);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A search source violated its wire-shape contract (design-08 STD7): a
|
|
74
|
+
* non-object source, or a non-string / non-string[] value under a read key.
|
|
75
|
+
* Thrown by search.ts's source readers; distinct from {@link ParamourError}
|
|
76
|
+
* so the Standard Schema adapter can soften exactly these throws to issues
|
|
77
|
+
* while config-contract violations and rebranded validator throws stay loud.
|
|
78
|
+
*/
|
|
79
|
+
export class SearchSourceError extends ParamourError {
|
|
80
|
+
static {
|
|
81
|
+
brandPrototype(this, searchSourceErrorBrand);
|
|
82
|
+
}
|
|
83
|
+
/** The offending source key, or null when the source itself is malformed. */
|
|
84
|
+
key;
|
|
85
|
+
constructor(message, key) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.key = key;
|
|
88
|
+
}
|
|
89
|
+
static [Symbol.hasInstance](value) {
|
|
90
|
+
return hasBrand(value, searchSourceErrorBrand);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
71
93
|
/** A value could not be serialized to the wire (bad type, non-finite, etc.). */
|
|
72
94
|
export class SerializeError extends ParamourError {
|
|
73
95
|
static {
|
|
@@ -77,6 +99,13 @@ export class SerializeError extends ParamourError {
|
|
|
77
99
|
return hasBrand(value, serializeErrorBrand);
|
|
78
100
|
}
|
|
79
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Renders a value's type for "…, got X" error messages, distinguishing null
|
|
104
|
+
* from typeof's "object". Not exported from the package.
|
|
105
|
+
*/
|
|
106
|
+
export function describeType(value) {
|
|
107
|
+
return value === null ? "null" : typeof value;
|
|
108
|
+
}
|
|
80
109
|
/**
|
|
81
110
|
* Best-effort human-readable message for a foreign (non-paramour) throw.
|
|
82
111
|
* Not exported from the package — internal to error branding.
|
package/dist/href.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyRoute } from "./route.js";
|
|
1
|
+
import type { AnyRoute, RegisteredStaticRoutePaths } from "./route.js";
|
|
2
2
|
import { type InferParamsInput } from "./path.js";
|
|
3
3
|
import { type SearchInputOf } from "./search.js";
|
|
4
4
|
/**
|
|
@@ -35,6 +35,19 @@ export type HrefArgs<R extends AnyRoute> = Record<never, never> extends InferHre
|
|
|
35
35
|
export type InferHrefInput<R extends AnyRoute> = PartFor<"params", InferParamsInput<R>> & PartFor<"search", SearchInputOf<R["~search"]>> & {
|
|
36
36
|
hash?: string;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* The string form's options (SH4): hash only. `params` is meaningless on a
|
|
40
|
+
* static path, and a query string comes only from a defined route's search
|
|
41
|
+
* codecs — a raw-search escape hatch here would be an untyped side door
|
|
42
|
+
* around library-owned serialization. Both are banned outright (`?: never`,
|
|
43
|
+
* the 2026-07-04 ruling's move) rather than merely omitted, so a non-fresh
|
|
44
|
+
* options object can't smuggle them past excess-property checking.
|
|
45
|
+
*/
|
|
46
|
+
export interface StaticHrefOptions {
|
|
47
|
+
hash?: string;
|
|
48
|
+
params?: never;
|
|
49
|
+
search?: never;
|
|
50
|
+
}
|
|
38
51
|
/**
|
|
39
52
|
* One options property whose presence follows its input type: required iff
|
|
40
53
|
* the input has at least one required key (the design-02 D4
|
|
@@ -54,6 +67,13 @@ type PartFor<Key extends string, Input> = keyof Input extends never ? Partial<Re
|
|
|
54
67
|
* (a missing param codec or `~search` config) are base `ParamourError` — a
|
|
55
68
|
* JS caller omitting a required `search` half falls through to
|
|
56
69
|
* encodeSearch's own required-missing error.
|
|
70
|
+
*
|
|
71
|
+
* The string form (SH1): a registered STATIC path stands in for the route
|
|
72
|
+
* object — same brand, same hash assembly, no route definition needed. The
|
|
73
|
+
* string overload sits first so the route-object overload is last (SH8):
|
|
74
|
+
* TS's "the last overload gave the following error" heuristic keeps
|
|
75
|
+
* route-object misuse diagnostics prominent.
|
|
57
76
|
*/
|
|
77
|
+
export declare function href<P extends RegisteredStaticRoutePaths>(path: P, options?: StaticHrefOptions): Href<P>;
|
|
58
78
|
export declare function href<R extends AnyRoute>(route: R, ...args: HrefArgs<R>): Href<R["path"]>;
|
|
59
79
|
export {};
|
package/dist/href.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ParamourError } from "./errors.js";
|
|
1
2
|
import { buildPath } from "./path.js";
|
|
2
3
|
import { searchToString, } from "./search.js";
|
|
3
4
|
// The conditional HrefArgs tuple is unresolvable inside a generic body, so
|
|
@@ -5,12 +6,27 @@ import { searchToString, } from "./search.js";
|
|
|
5
6
|
// routeData's config cast) instead of per-expression casts: the
|
|
6
7
|
// implementation sees each option half at its loosest honest type.
|
|
7
8
|
export function href(route, options) {
|
|
8
|
-
const path = buildPath(route, options?.params ?? {});
|
|
9
|
-
const query = searchToString(route["~search"], options?.search ?? {});
|
|
10
9
|
// S10: the fragment is appended VERBATIM — no encoding, the caller owns
|
|
11
10
|
// escaping (a value already starting with "#" yields "##…"). The empty
|
|
12
11
|
// string emits no "#".
|
|
13
12
|
const hash = options?.hash;
|
|
14
13
|
const fragment = hash === undefined || hash === "" ? "" : `#${hash}`;
|
|
14
|
+
if (typeof route === "string") {
|
|
15
|
+
// SH6: fail-fast backstop for JS callers and world-A typos of the
|
|
16
|
+
// dynamic-path variety — a bracket means "you need a route object", and
|
|
17
|
+
// query/hash never ride in the path string (query comes only from
|
|
18
|
+
// search codecs, hash only from the option).
|
|
19
|
+
if (!route.startsWith("/") || /[#?[\]]/.test(route)) {
|
|
20
|
+
throw new ParamourError(`href(path) requires a static route path, got ${JSON.stringify(route)}: dynamic segments need a route object, and query/hash never ride in the path string`);
|
|
21
|
+
}
|
|
22
|
+
// SH6: silently dropping a half a JS caller passed would build a wrong
|
|
23
|
+
// link — contract violations stay loud (never the safe-parse error arm).
|
|
24
|
+
if (options?.params !== undefined || options?.search !== undefined) {
|
|
25
|
+
throw new ParamourError(`href(path) takes no params/search — a static path has no params, and a query string needs a route with search codecs`);
|
|
26
|
+
}
|
|
27
|
+
return `${route}${fragment}`;
|
|
28
|
+
}
|
|
29
|
+
const path = buildPath(route, options?.params ?? {});
|
|
30
|
+
const query = searchToString(route["~search"], options?.search ?? {});
|
|
15
31
|
return `${path}${query}${fragment}`;
|
|
16
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { type AnyCodec, type Arity, type Codec, type OutputOf, type ParamCodec, type Presence, type PresenceOf, } from "./codec.js";
|
|
2
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, SerializeError, } from "./errors.js";
|
|
4
|
-
export { href, type Href, type HrefArgs, type InferHrefInput } from "./href.js";
|
|
3
|
+
export { type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
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
|
-
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 Route, type RouteProps, type RouterKind, type SafeResult, type SearchProps, } from "./route.js";
|
|
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
9
|
export { buildSearchString, decodeSearch, encodeSearch, type InferSearchInput, type InferSearchOutput, rawSearch, type RawSearch, type SearchConfig, type SearchOutputOf, type SearchSource, searchToString, } from "./search.js";
|
|
10
|
+
export { standardSearchSchema, type StandardSearchSchema, } from "./standard-schema.js";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export {} from "./codec.js";
|
|
2
2
|
export { describeCodec, describeRoute, } from "./describe.js";
|
|
3
|
-
export { ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SerializeError, } from "./errors.js";
|
|
4
|
-
export { href } from "./href.js";
|
|
3
|
+
export { ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
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
9
|
export { buildSearchString, decodeSearch, encodeSearch, rawSearch, searchToString, } from "./search.js";
|
|
10
|
+
export { standardSearchSchema, } from "./standard-schema.js";
|
package/dist/path.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ParamourError, ParamsDecodeError, ParseError, SerializeError, } from "./errors.js";
|
|
1
|
+
import { describeType, ParamourError, ParamsDecodeError, ParseError, SerializeError, } from "./errors.js";
|
|
2
2
|
import { encodeComponent, readInputValue } 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
|
|
@@ -32,7 +32,7 @@ export function decodeParams(route, source, options) {
|
|
|
32
32
|
// a per-key decode issue.
|
|
33
33
|
const untrusted = source;
|
|
34
34
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
35
|
-
throw new ParamourError(`params source must be an object, got ${untrusted
|
|
35
|
+
throw new ParamourError(`params source must be an object, got ${describeType(untrusted)}`);
|
|
36
36
|
}
|
|
37
37
|
// R5: App-Router surfaces arrive percent-encoded (decode by default); pages
|
|
38
38
|
// surfaces arrive already-decoded and opt out via `{ percentDecode: false }`.
|
|
@@ -182,7 +182,7 @@ export function encodeParams(route, params) {
|
|
|
182
182
|
// here; a null input must fail loud, not read as every-param-absent.
|
|
183
183
|
const untrusted = params;
|
|
184
184
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
185
|
-
throw new SerializeError(`params input must be an object, got ${untrusted
|
|
185
|
+
throw new SerializeError(`params input must be an object, got ${describeType(untrusted)}`);
|
|
186
186
|
}
|
|
187
187
|
const values = untrusted;
|
|
188
188
|
const config = route["~params"];
|
|
@@ -232,7 +232,7 @@ export function encodeStaticParams(route, params) {
|
|
|
232
232
|
// here; a null input must fail loud, not read as every-param-absent.
|
|
233
233
|
const untrusted = params;
|
|
234
234
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
235
|
-
throw new SerializeError(`params input must be an object, got ${untrusted
|
|
235
|
+
throw new SerializeError(`params input must be an object, got ${describeType(untrusted)}`);
|
|
236
236
|
}
|
|
237
237
|
const values = untrusted;
|
|
238
238
|
const config = route["~params"];
|
package/dist/route.d.ts
CHANGED
|
@@ -163,6 +163,27 @@ export type RegisteredAppRoutePaths = ParamourRegister extends {
|
|
|
163
163
|
export type RegisteredPagesRoutePaths = ParamourRegister extends {
|
|
164
164
|
pagesRoutes: infer R extends string;
|
|
165
165
|
} ? R : string;
|
|
166
|
+
/**
|
|
167
|
+
* Static-only subset of {@link RegisteredAppRoutePaths} (SH2): derived by
|
|
168
|
+
* syntactic filter, not emitted — dynamic-ness is a property of the path
|
|
169
|
+
* literal, so the registry format doesn't change. Same world-A `string`
|
|
170
|
+
* fallback as the full union (the filter passes `string` through).
|
|
171
|
+
*/
|
|
172
|
+
export type RegisteredStaticAppRoutePaths = StaticPathsOf<RegisteredAppRoutePaths>;
|
|
173
|
+
/** Pages twin of {@link RegisteredStaticAppRoutePaths} (SH2). */
|
|
174
|
+
export type RegisteredStaticPagesRoutePaths = StaticPathsOf<RegisteredPagesRoutePaths>;
|
|
175
|
+
/**
|
|
176
|
+
* Every registered STATIC path across both routers — the string form of
|
|
177
|
+
* href's path argument (SH1/SH2). Deliberately NOT the union of the two
|
|
178
|
+
* per-router types (SH3): each falls back to `string` when its registry
|
|
179
|
+
* member is absent, and in a single-router project the absent side's
|
|
180
|
+
* `string` would swallow the union and erase verification for the router
|
|
181
|
+
* that HAS routes. The permissive fallback applies only when NEITHER member
|
|
182
|
+
* is present (world A / TR3's empty merge).
|
|
183
|
+
*/
|
|
184
|
+
export type RegisteredStaticRoutePaths = [PresentRegisteredPaths] extends [
|
|
185
|
+
never
|
|
186
|
+
] ? string : StaticPathsOf<PresentRegisteredPaths>;
|
|
166
187
|
/**
|
|
167
188
|
* The router-agnostic core of a defined route (PR3): path, configs, and the
|
|
168
189
|
* define-time token cache. The parse surface is router-specific and lives on
|
|
@@ -234,6 +255,24 @@ export type Segments<S extends string> = S extends `${infer Head}/${infer Rest}`
|
|
|
234
255
|
* `[...slug]` would extract as a single param named `"...slug"`.
|
|
235
256
|
*/
|
|
236
257
|
export type SingleParamNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[[...${string}]]` ? never : S extends `[...${string}]` ? never : S extends `[${infer Name}]` ? NonEmptyName<Name> : never : never;
|
|
258
|
+
/**
|
|
259
|
+
* Union of the registry members that are actually PRESENT — `never` when
|
|
260
|
+
* neither router has generated routes. The input to SH3's combined-union
|
|
261
|
+
* fallback rule; see {@link RegisteredStaticRoutePaths}.
|
|
262
|
+
*/
|
|
263
|
+
type PresentRegisteredPaths = (ParamourRegister extends {
|
|
264
|
+
appRoutes: infer A extends string;
|
|
265
|
+
} ? A : never) | (ParamourRegister extends {
|
|
266
|
+
pagesRoutes: infer P extends string;
|
|
267
|
+
} ? P : never);
|
|
268
|
+
/**
|
|
269
|
+
* Filters a path union to its static members (SH2): any `[` marks a dynamic
|
|
270
|
+
* segment. `string` passes through (it doesn't extend the bracket template),
|
|
271
|
+
* which is exactly what keeps the world-A fallback intact. Note reachability
|
|
272
|
+
* ≠ staticness (SH7): `/docs/[[...slug]]` serves `/docs`, but it carries a
|
|
273
|
+
* codec and decode expectations, so it is excluded here.
|
|
274
|
+
*/
|
|
275
|
+
type StaticPathsOf<P extends string> = P extends `${string}[${string}` ? never : P;
|
|
237
276
|
/**
|
|
238
277
|
* Defines an App Router route: the URL-shaped path literal (RL2) plus its
|
|
239
278
|
* param/search codec configs. Validates the literal eagerly (RL1 —
|
|
@@ -251,3 +290,4 @@ export declare function defineAppRoute<Path extends RegisteredAppRoutePaths & st
|
|
|
251
290
|
* sync context pair (PR10).
|
|
252
291
|
*/
|
|
253
292
|
export declare function definePagesRoute<Path extends RegisteredPagesRoutePaths & string, const PC extends ParamsConfig<Path> = ParamsConfig<Path>, const SC extends Readonly<Partial<Record<PathParamNames<Path>, never>>> & SearchSlot = Record<never, never>>(path: Path, config: RouteConfig<Path, PC, SC>): PagesRoute<Path, PC, SC>;
|
|
293
|
+
export {};
|
package/dist/route.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreignMessage, ParamourError, ParamsDecodeError, SearchDecodeError, } from "./errors.js";
|
|
1
|
+
import { describeType, foreignMessage, ParamourError, ParamsDecodeError, SearchDecodeError, } from "./errors.js";
|
|
2
2
|
import { decodeParams, tokenizePath, } from "./path.js";
|
|
3
3
|
import { decodeSearch, } from "./search.js";
|
|
4
4
|
/**
|
|
@@ -199,12 +199,12 @@ async function safely(run) {
|
|
|
199
199
|
function splitPagesContext(context, paramNames) {
|
|
200
200
|
const untrusted = context;
|
|
201
201
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
202
|
-
throw new ParamourError(`pages context must be an object, got ${untrusted
|
|
202
|
+
throw new ParamourError(`pages context must be an object, got ${describeType(untrusted)}`);
|
|
203
203
|
}
|
|
204
204
|
const { params, query } = untrusted;
|
|
205
205
|
const untrustedQuery = query;
|
|
206
206
|
if (typeof untrustedQuery !== "object" || untrustedQuery === null) {
|
|
207
|
-
throw new ParamourError(`pages context has no query object (got ${untrustedQuery
|
|
207
|
+
throw new ParamourError(`pages context has no query object (got ${describeType(untrustedQuery)}): getStaticProps contexts carry no query string — decode ctx.params with safeDecodeParams instead (PR10)`);
|
|
208
208
|
}
|
|
209
209
|
return [params ?? pickOwn(query, paramNames), omitOwn(query, paramNames)];
|
|
210
210
|
}
|
package/dist/search.d.ts
CHANGED
|
@@ -111,6 +111,13 @@ export declare function encodeComponent(text: string): string;
|
|
|
111
111
|
* record goes straight to the byte layer and the schema never runs on encode.
|
|
112
112
|
*/
|
|
113
113
|
export declare function encodeSearch<S extends SearchSlot>(config: S, input: SearchInputOf<S>): [string, string][];
|
|
114
|
+
/**
|
|
115
|
+
* Runtime discriminant for the `search:` slot (design-04 SS2): the reserved
|
|
116
|
+
* `~kind` marker is unambiguous against a codec map, which never carries a
|
|
117
|
+
* top-level `~`-prefixed key. Module-exported for standard-schema.ts, not
|
|
118
|
+
* barrel-exported.
|
|
119
|
+
*/
|
|
120
|
+
export declare function isRawSearch(config: SearchSlot): config is RawSearch<StandardSchemaV1>;
|
|
114
121
|
/**
|
|
115
122
|
* The whole-object search escape hatch (design-04 SS1, maintainer ruling):
|
|
116
123
|
* an explicit, greppable wrapper around a bare Standard Schema so a route's
|
|
@@ -134,6 +141,15 @@ export declare function rawSearch<S extends StandardSchemaV1>(schema: S): RawSea
|
|
|
134
141
|
* members too.
|
|
135
142
|
*/
|
|
136
143
|
export declare function readInputValue(values: Record<string, unknown>, key: string): unknown;
|
|
144
|
+
/**
|
|
145
|
+
* The TS contract makes a non-object config unrepresentable, but a
|
|
146
|
+
* hand-built route missing `~search` reaches both codecs' entry points via
|
|
147
|
+
* href/parseSearch in plain JS; fail branded — a missing config is a
|
|
148
|
+
* config-contract violation (requireCodec's precedent), never a raw
|
|
149
|
+
* TypeError out of Object.entries/Object.keys. Module-exported for
|
|
150
|
+
* standard-schema.ts, not barrel-exported.
|
|
151
|
+
*/
|
|
152
|
+
export declare function requireSearchConfig(config: SearchSlot): void;
|
|
137
153
|
/** Convenience: encode + build in one step. */
|
|
138
154
|
export declare function searchToString<S extends SearchSlot>(config: S, input: SearchInputOf<S>): string;
|
|
139
155
|
export {};
|
package/dist/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreignMessage, ParamourError, ParseError, rebrandForeign, SearchDecodeError, SerializeError, } from "./errors.js";
|
|
1
|
+
import { describeType, foreignMessage, ParamourError, ParseError, rebrandForeign, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
2
2
|
import { runStandardSchemaSync } from "./schema.js";
|
|
3
3
|
/**
|
|
4
4
|
* Builds the byte-layer query string from decoded pairs. Hand-rolled on
|
|
@@ -147,7 +147,7 @@ export function encodeSearch(config, input) {
|
|
|
147
147
|
// here; a null input must fail loud, not read as every-key-absent.
|
|
148
148
|
const untrusted = input;
|
|
149
149
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
150
|
-
throw new SerializeError(`search input must be an object, got ${untrusted
|
|
150
|
+
throw new SerializeError(`search input must be an object, got ${describeType(untrusted)}`);
|
|
151
151
|
}
|
|
152
152
|
const pairs = [];
|
|
153
153
|
const values = untrusted;
|
|
@@ -187,6 +187,15 @@ export function encodeSearch(config, input) {
|
|
|
187
187
|
}
|
|
188
188
|
return pairs;
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Runtime discriminant for the `search:` slot (design-04 SS2): the reserved
|
|
192
|
+
* `~kind` marker is unambiguous against a codec map, which never carries a
|
|
193
|
+
* top-level `~`-prefixed key. Module-exported for standard-schema.ts, not
|
|
194
|
+
* barrel-exported.
|
|
195
|
+
*/
|
|
196
|
+
export function isRawSearch(config) {
|
|
197
|
+
return "~kind" in config && config["~kind"] === "raw-search";
|
|
198
|
+
}
|
|
190
199
|
/**
|
|
191
200
|
* The whole-object search escape hatch (design-04 SS1, maintainer ruling):
|
|
192
201
|
* an explicit, greppable wrapper around a bare Standard Schema so a route's
|
|
@@ -228,6 +237,20 @@ export function readInputValue(values, key) {
|
|
|
228
237
|
}
|
|
229
238
|
return undefined;
|
|
230
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* The TS contract makes a non-object config unrepresentable, but a
|
|
242
|
+
* hand-built route missing `~search` reaches both codecs' entry points via
|
|
243
|
+
* href/parseSearch in plain JS; fail branded — a missing config is a
|
|
244
|
+
* config-contract violation (requireCodec's precedent), never a raw
|
|
245
|
+
* TypeError out of Object.entries/Object.keys. Module-exported for
|
|
246
|
+
* standard-schema.ts, not barrel-exported.
|
|
247
|
+
*/
|
|
248
|
+
export function requireSearchConfig(config) {
|
|
249
|
+
const untrusted = config;
|
|
250
|
+
if (typeof untrusted !== "object" || untrusted === null) {
|
|
251
|
+
throw new ParamourError(`search config must be an object, got ${describeType(untrusted)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
231
254
|
/** Convenience: encode + build in one step. */
|
|
232
255
|
export function searchToString(config, input) {
|
|
233
256
|
return buildSearchString(encodeSearch(config, input));
|
|
@@ -276,7 +299,7 @@ function decodeRawSearch(config, source) {
|
|
|
276
299
|
function encodeRawSearch(input) {
|
|
277
300
|
const untrusted = input;
|
|
278
301
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
279
|
-
throw new SerializeError(`search input must be an object, got ${untrusted
|
|
302
|
+
throw new SerializeError(`search input must be an object, got ${describeType(untrusted)}`);
|
|
280
303
|
}
|
|
281
304
|
const values = untrusted;
|
|
282
305
|
const pairs = [];
|
|
@@ -294,14 +317,6 @@ function encodeRawSearch(input) {
|
|
|
294
317
|
}
|
|
295
318
|
return pairs;
|
|
296
319
|
}
|
|
297
|
-
/**
|
|
298
|
-
* Runtime discriminant for the `search:` slot (design-04 SS2): the reserved
|
|
299
|
-
* `~kind` marker is unambiguous against a codec map, which never carries a
|
|
300
|
-
* top-level `~`-prefixed key.
|
|
301
|
-
*/
|
|
302
|
-
function isRawSearch(config) {
|
|
303
|
-
return "~kind" in config && config["~kind"] === "raw-search";
|
|
304
|
-
}
|
|
305
320
|
/**
|
|
306
321
|
* Snapshots EVERY source key's wire values, before any user code (the
|
|
307
322
|
* whole-object schema's `validate`) runs — sibling of
|
|
@@ -317,7 +332,7 @@ function readAllValues(source) {
|
|
|
317
332
|
// here; fail branded, not with a raw TypeError out of Object.keys.
|
|
318
333
|
const untrusted = source;
|
|
319
334
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
320
|
-
throw new
|
|
335
|
+
throw new SearchSourceError(`search source must be an object, got ${describeType(untrusted)}`, null);
|
|
321
336
|
}
|
|
322
337
|
const grouped = new Map();
|
|
323
338
|
if (source instanceof URLSearchParams) {
|
|
@@ -326,7 +341,7 @@ function readAllValues(source) {
|
|
|
326
341
|
// Record branch below.
|
|
327
342
|
for (const [key, value] of source) {
|
|
328
343
|
if (typeof value !== "string") {
|
|
329
|
-
throw new
|
|
344
|
+
throw new SearchSourceError(`search source values for "${key}" must be strings, got ${typeof value}`, key);
|
|
330
345
|
}
|
|
331
346
|
const list = grouped.get(key);
|
|
332
347
|
if (list === undefined)
|
|
@@ -363,15 +378,15 @@ function readAllValues(source) {
|
|
|
363
378
|
* user code runs. Declared keys only, on purpose: unknown keys are never
|
|
364
379
|
* validated, so malformed junk under keys paramour doesn't own (qs bracket
|
|
365
380
|
* params, numbers) can't fail a decode (P8). Malformed values under a
|
|
366
|
-
* DECLARED key are a loud {@link
|
|
367
|
-
* its stated contract — never a silent key drop.
|
|
381
|
+
* DECLARED key are a loud {@link SearchSourceError} — the source doesn't
|
|
382
|
+
* match its stated contract — never a silent key drop.
|
|
368
383
|
*/
|
|
369
384
|
function readDeclaredValues(config, source) {
|
|
370
385
|
// The TS contract forbids non-object sources, but plain-JS callers reach
|
|
371
386
|
// here; fail branded, not with a raw TypeError out of Object.hasOwn.
|
|
372
387
|
const untrusted = source;
|
|
373
388
|
if (typeof untrusted !== "object" || untrusted === null) {
|
|
374
|
-
throw new
|
|
389
|
+
throw new SearchSourceError(`search source must be an object, got ${describeType(untrusted)}`, null);
|
|
375
390
|
}
|
|
376
391
|
const values = new Map();
|
|
377
392
|
if (source instanceof URLSearchParams) {
|
|
@@ -383,7 +398,7 @@ function readDeclaredValues(config, source) {
|
|
|
383
398
|
if (!Object.hasOwn(config, key))
|
|
384
399
|
continue;
|
|
385
400
|
if (typeof value !== "string") {
|
|
386
|
-
throw new
|
|
401
|
+
throw new SearchSourceError(`search source values for "${key}" must be strings, got ${typeof value}`, key);
|
|
387
402
|
}
|
|
388
403
|
const list = values.get(key);
|
|
389
404
|
if (list === undefined)
|
|
@@ -417,12 +432,12 @@ function readRecordValues(source, key) {
|
|
|
417
432
|
const copy = [...value];
|
|
418
433
|
for (const element of copy) {
|
|
419
434
|
if (typeof element !== "string") {
|
|
420
|
-
throw new
|
|
435
|
+
throw new SearchSourceError(`search source values for "${key}" must be strings, got ${typeof element}`, key);
|
|
421
436
|
}
|
|
422
437
|
}
|
|
423
438
|
return copy;
|
|
424
439
|
}
|
|
425
|
-
throw new
|
|
440
|
+
throw new SearchSourceError(`search source value for "${key}" must be a string or string[], got ${typeof value}`, key);
|
|
426
441
|
}
|
|
427
442
|
/**
|
|
428
443
|
* Property reads run user getters (the class-instance shape
|
|
@@ -444,19 +459,6 @@ function requireRawSearchString(key, value) {
|
|
|
444
459
|
}
|
|
445
460
|
return value;
|
|
446
461
|
}
|
|
447
|
-
/**
|
|
448
|
-
* The TS contract makes a non-object config unrepresentable, but a
|
|
449
|
-
* hand-built route missing `~search` reaches both codecs' entry points via
|
|
450
|
-
* href/parseSearch in plain JS; fail branded — a missing config is a
|
|
451
|
-
* config-contract violation (requireCodec's precedent), never a raw
|
|
452
|
-
* TypeError out of Object.entries/Object.keys.
|
|
453
|
-
*/
|
|
454
|
-
function requireSearchConfig(config) {
|
|
455
|
-
const untrusted = config;
|
|
456
|
-
if (typeof untrusted !== "object" || untrusted === null) {
|
|
457
|
-
throw new ParamourError(`search config must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
462
|
/**
|
|
461
463
|
* Invokes a codec's serializer and enforces its string contract: a custom
|
|
462
464
|
* codec written in plain JS can return undefined, which would otherwise
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { AnyRoute } from "./route.js";
|
|
3
|
+
import { type SearchOutputOf } from "./search.js";
|
|
4
|
+
/**
|
|
5
|
+
* Standard Schema generate-OUT (design-08): exports a route's `search:`
|
|
6
|
+
* config as a spec-compliant Standard Schema. The mirror of schema.ts, which
|
|
7
|
+
* runs Standard Schemas coming IN.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The schema {@link standardSearchSchema} returns (STD1/STD3): input is
|
|
11
|
+
* advertised as the wire-shaped record only — `URLSearchParams` is accepted
|
|
12
|
+
* at runtime but kept out of the type, so client-side inference (tRPC) never
|
|
13
|
+
* sees a shape that cannot serialize over JSON. Output is the route's decoded
|
|
14
|
+
* search shape. `types` is carried by this annotation alone; the spec reads
|
|
15
|
+
* it at the type level only, so no runtime key exists.
|
|
16
|
+
*/
|
|
17
|
+
export type StandardSearchSchema<SC> = StandardSchemaV1<Record<string, string | string[] | undefined>, SearchOutputOf<SC>>;
|
|
18
|
+
/**
|
|
19
|
+
* Exports a route's `search:` config as the URL wire contract in Standard
|
|
20
|
+
* Schema form (design-08 STD1/STD5), for consumers like tRPC inputs or
|
|
21
|
+
* TanStack `validateSearch`. Semantics are byte-identical to `decodeSearch`
|
|
22
|
+
* (STD6): defaults apply, `.catch()` recovers parse failures — invalid API
|
|
23
|
+
* input silently coerces to the fallback — unknown keys strip (P8), and
|
|
24
|
+
* duplicate values on a scalar codec reject (P5). No coercion, ever (STD2):
|
|
25
|
+
* the schema accepts wire strings (`"42"`), not decoded values (`42`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function standardSearchSchema<R extends AnyRoute>(route: R): StandardSearchSchema<R["~search"]>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SearchSourceError } from "./errors.js";
|
|
2
|
+
import { safeDecodeSearch } from "./safe-decode.js";
|
|
3
|
+
import { isRawSearch, requireSearchConfig, } from "./search.js";
|
|
4
|
+
/**
|
|
5
|
+
* Exports a route's `search:` config as the URL wire contract in Standard
|
|
6
|
+
* Schema form (design-08 STD1/STD5), for consumers like tRPC inputs or
|
|
7
|
+
* TanStack `validateSearch`. Semantics are byte-identical to `decodeSearch`
|
|
8
|
+
* (STD6): defaults apply, `.catch()` recovers parse failures — invalid API
|
|
9
|
+
* input silently coerces to the fallback — unknown keys strip (P8), and
|
|
10
|
+
* duplicate values on a scalar codec reject (P5). No coercion, ever (STD2):
|
|
11
|
+
* the schema accepts wire strings (`"42"`), not decoded values (`42`).
|
|
12
|
+
*/
|
|
13
|
+
export function standardSearchSchema(route) {
|
|
14
|
+
const config = route["~search"];
|
|
15
|
+
// A missing/malformed config is a programming error and stays loud (STD7);
|
|
16
|
+
// checking eagerly fails at construction, not at first validate().
|
|
17
|
+
requireSearchConfig(config);
|
|
18
|
+
// A config's raw/codec-map shape is fixed at construction; hoisted so the
|
|
19
|
+
// sentinel un-mapping below never touches a codec map's keyed issues.
|
|
20
|
+
const raw = isRawSearch(config);
|
|
21
|
+
return {
|
|
22
|
+
"~standard": {
|
|
23
|
+
validate: (value) => {
|
|
24
|
+
try {
|
|
25
|
+
const result = safeDecodeSearch(route, value);
|
|
26
|
+
if (result.status === "error") {
|
|
27
|
+
return {
|
|
28
|
+
issues: result.error.issues.map((issue) => toStandardIssue(issue, raw)),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { value: result.data };
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
// STD7: validate() receives genuinely untrusted input, so the
|
|
35
|
+
// source-shape contract the read layer enforces with loud throws
|
|
36
|
+
// softens to issues at this one boundary — SearchSourceError exists
|
|
37
|
+
// as its own branded class exactly so this catch can't swallow
|
|
38
|
+
// config-contract violations or rebranded validator throws.
|
|
39
|
+
if (error instanceof SearchSourceError) {
|
|
40
|
+
return { issues: [toSourceIssue(error)] };
|
|
41
|
+
}
|
|
42
|
+
// Async raw schema (design-02 D7), throwing raw validators, and
|
|
43
|
+
// throwing .default()/.catch() factories are true programming
|
|
44
|
+
// errors: loud (STD7).
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
vendor: "paramour",
|
|
49
|
+
version: 1,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Maps a source-shape violation to spec shape (STD7): keyed where the read
|
|
55
|
+
* layer could attribute it, root-level (absent path) for a malformed source.
|
|
56
|
+
*/
|
|
57
|
+
function toSourceIssue(error) {
|
|
58
|
+
return error.key === null
|
|
59
|
+
? { message: error.message }
|
|
60
|
+
: { message: error.message, path: [error.key] };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Maps a decode issue to spec shape. decodeRawSearch collapses a root-level
|
|
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 here (STD7
|
|
66
|
+
* "root-level otherwise") — for raw configs only, since a codec map's issues
|
|
67
|
+
* are always keyed and a param may literally be named "<search>". (A raw
|
|
68
|
+
* vendor issue whose real path is ["<search>"] still collides with the
|
|
69
|
+
* sentinel — indistinguishable after the flat-key collapse.) Nested
|
|
70
|
+
* raw-schema paths were already dot-joined into the key and re-emit as ONE
|
|
71
|
+
* segment — keys may contain dots, so splitting back would be unsound.
|
|
72
|
+
*/
|
|
73
|
+
function toStandardIssue(issue, raw) {
|
|
74
|
+
return raw && issue.key === "<search>"
|
|
75
|
+
? { message: issue.message }
|
|
76
|
+
: { message: issue.message, path: [issue.key] };
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paramour",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"dist"
|
|
35
35
|
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
36
40
|
"dependencies": {
|
|
37
41
|
"@standard-schema/spec": "^1.1.0"
|
|
38
42
|
},
|