@loomcli/validators 0.0.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bounds.d.ts +29 -0
- package/dist/bounds.js +60 -0
- package/dist/codes.d.ts +78 -0
- package/dist/codes.js +134 -0
- package/dist/create.d.ts +22 -0
- package/dist/create.js +114 -0
- package/dist/data.d.ts +8 -0
- package/dist/data.js +25 -0
- package/dist/date.d.ts +4 -0
- package/dist/date.js +32 -0
- package/dist/faults.d.ts +29 -0
- package/dist/faults.js +49 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/integer.d.ts +11 -0
- package/dist/integer.js +31 -0
- package/dist/issue-code.d.ts +24 -0
- package/dist/issue-code.js +183 -0
- package/dist/issues.d.ts +6 -0
- package/dist/issues.js +17 -0
- package/dist/number.d.ts +9 -0
- package/dist/number.js +26 -0
- package/dist/one-of.d.ts +4 -0
- package/dist/one-of.js +79 -0
- package/dist/params.d.ts +54 -0
- package/dist/params.js +68 -0
- package/dist/path.d.ts +13 -0
- package/dist/path.js +64 -0
- package/dist/port.d.ts +4 -0
- package/dist/port.js +18 -0
- package/dist/probe.d.ts +11 -0
- package/dist/probe.js +66 -0
- package/dist/rules.d.ts +32 -0
- package/dist/rules.js +86 -0
- package/dist/text.d.ts +19 -0
- package/dist/text.js +152 -0
- package/dist/uri-grammar.d.ts +9 -0
- package/dist/uri-grammar.js +42 -0
- package/dist/url.d.ts +8 -0
- package/dist/url.js +74 -0
- package/dist/uuid.d.ts +4 -0
- package/dist/uuid.js +13 -0
- package/package.json +24 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Drew Butler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bounds.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@loomcli/core';
|
|
2
|
+
import type { BoundCodes } from './codes.js';
|
|
3
|
+
/** Inclusive bounds on a numeric factory, either or both absent. */
|
|
4
|
+
interface Bounds {
|
|
5
|
+
min: number | undefined;
|
|
6
|
+
max: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** What a numeric factory requires of each bound, stated for its fault messages. */
|
|
9
|
+
interface BoundRule {
|
|
10
|
+
factory: string;
|
|
11
|
+
accepts: (value: number) => boolean;
|
|
12
|
+
requirement: string;
|
|
13
|
+
correction: string;
|
|
14
|
+
}
|
|
15
|
+
/** Reads a numeric factory's `min` and `max`, and faults on a bad bound or `min` above `max`. */
|
|
16
|
+
declare function readBounds(options: unknown, rule: BoundRule): Bounds;
|
|
17
|
+
declare function withinBounds(value: number, { max, min }: Bounds): boolean;
|
|
18
|
+
/** The number a numeric token outputs, with `-0` read as `0`. */
|
|
19
|
+
declare function withoutNegativeZero(value: number): number;
|
|
20
|
+
/** The one issue for a numeric configuration, under the code for the bounds it declares. */
|
|
21
|
+
declare function boundsIssue(codes: BoundCodes, { max, min }: Bounds): StandardSchemaV1.Issue;
|
|
22
|
+
/** The published schema for a numeric type, with each declared bound. */
|
|
23
|
+
declare function boundsSchema(type: 'integer' | 'number', { max, min }: Bounds): {
|
|
24
|
+
type: "integer" | "number";
|
|
25
|
+
minimum?: number;
|
|
26
|
+
maximum?: number;
|
|
27
|
+
};
|
|
28
|
+
export { boundsIssue, boundsSchema, readBounds, withinBounds, withoutNegativeZero };
|
|
29
|
+
export type { Bounds };
|
package/dist/bounds.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fault, readOptions } from './faults.js';
|
|
2
|
+
import { boundsOrder, boundValue } from './rules.js';
|
|
3
|
+
const zero = 0;
|
|
4
|
+
/** One bound a numeric factory declares, checked against its rule; a fault marks the bound. */
|
|
5
|
+
function boundOf(bound, name, value) {
|
|
6
|
+
const { call, rule } = bound;
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
if (typeof value !== 'number' || !rule.accepts(value)) {
|
|
11
|
+
throw fault(boundValue, { ...call, mark: `0.${name}` }, {
|
|
12
|
+
correction: rule.correction,
|
|
13
|
+
sentence: `${rule.factory}() ${name} is not ${rule.requirement}.`,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
/** Reads a numeric factory's `min` and `max`, and faults on a bad bound or `min` above `max`. */
|
|
19
|
+
function readBounds(options, rule) {
|
|
20
|
+
const declared = readOptions(rule.factory, options);
|
|
21
|
+
const call = { arguments: [options], factory: rule.factory };
|
|
22
|
+
const min = boundOf({ call, rule }, 'min', declared.min);
|
|
23
|
+
const max = boundOf({ call, rule }, 'max', declared.max);
|
|
24
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
25
|
+
throw fault(boundsOrder, { ...call, mark: '0.min' }, {
|
|
26
|
+
correction: 'Supply a min at or below max.',
|
|
27
|
+
sentence: `${rule.factory}() min ${String(min)} is above max ${String(max)}.`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return { max, min };
|
|
31
|
+
}
|
|
32
|
+
function withinBounds(value, { max, min }) {
|
|
33
|
+
return (min === undefined || value >= min) && (max === undefined || value <= max);
|
|
34
|
+
}
|
|
35
|
+
/** The number a numeric token outputs, with `-0` read as `0`. */
|
|
36
|
+
function withoutNegativeZero(value) {
|
|
37
|
+
return value === zero ? zero : value;
|
|
38
|
+
}
|
|
39
|
+
/** The one issue for a numeric configuration, under the code for the bounds it declares. */
|
|
40
|
+
function boundsIssue(codes, { max, min }) {
|
|
41
|
+
if (min !== undefined && max !== undefined) {
|
|
42
|
+
return codes.range.issue({ max, min });
|
|
43
|
+
}
|
|
44
|
+
if (min !== undefined) {
|
|
45
|
+
return codes.min.issue({ min });
|
|
46
|
+
}
|
|
47
|
+
if (max !== undefined) {
|
|
48
|
+
return codes.max.issue({ max });
|
|
49
|
+
}
|
|
50
|
+
return codes.unbounded.issue({});
|
|
51
|
+
}
|
|
52
|
+
/** The published schema for a numeric type, with each declared bound. */
|
|
53
|
+
function boundsSchema(type, { max, min }) {
|
|
54
|
+
return {
|
|
55
|
+
type,
|
|
56
|
+
...(min === undefined ? {} : { minimum: min }),
|
|
57
|
+
...(max === undefined ? {} : { maximum: max }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export { boundsIssue, boundsSchema, readBounds, withinBounds, withoutNegativeZero };
|
package/dist/codes.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { IssueCode } from './issue-code.js';
|
|
2
|
+
import type { NoParams } from './params.js';
|
|
3
|
+
import type { PathKind } from './probe.js';
|
|
4
|
+
declare const textNonemptyIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
5
|
+
declare const textMinLengthIssue: IssueCode<{
|
|
6
|
+
min: number;
|
|
7
|
+
}>;
|
|
8
|
+
declare const textMaxLengthIssue: IssueCode<{
|
|
9
|
+
max: number;
|
|
10
|
+
}>;
|
|
11
|
+
declare const textExactLengthIssue: IssueCode<{
|
|
12
|
+
length: number;
|
|
13
|
+
}>;
|
|
14
|
+
declare const textLengthRangeIssue: IssueCode<{
|
|
15
|
+
max: number;
|
|
16
|
+
min: number;
|
|
17
|
+
}>;
|
|
18
|
+
/** The author's own sentence for a `text()` pattern, which is the code's one blank. */
|
|
19
|
+
declare const textPatternIssue: IssueCode<{
|
|
20
|
+
message: string;
|
|
21
|
+
}>;
|
|
22
|
+
declare const integerIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
23
|
+
declare const integerRangeIssue: IssueCode<{
|
|
24
|
+
max: number;
|
|
25
|
+
min: number;
|
|
26
|
+
}>;
|
|
27
|
+
declare const integerMinIssue: IssueCode<{
|
|
28
|
+
min: number;
|
|
29
|
+
}>;
|
|
30
|
+
declare const integerMaxIssue: IssueCode<{
|
|
31
|
+
max: number;
|
|
32
|
+
}>;
|
|
33
|
+
declare const numberIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
34
|
+
declare const numberRangeIssue: IssueCode<{
|
|
35
|
+
max: number;
|
|
36
|
+
min: number;
|
|
37
|
+
}>;
|
|
38
|
+
declare const numberMinIssue: IssueCode<{
|
|
39
|
+
min: number;
|
|
40
|
+
}>;
|
|
41
|
+
declare const numberMaxIssue: IssueCode<{
|
|
42
|
+
max: number;
|
|
43
|
+
}>;
|
|
44
|
+
declare const portIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
45
|
+
declare const oneOfIssue: IssueCode<{
|
|
46
|
+
values: readonly string[];
|
|
47
|
+
}>;
|
|
48
|
+
declare const urlIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
49
|
+
declare const urlSchemeIssue: IssueCode<{
|
|
50
|
+
protocols: readonly string[];
|
|
51
|
+
}>;
|
|
52
|
+
declare const uuidIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
53
|
+
declare const dateIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
54
|
+
declare const pathIssue: IssueCode<Readonly<Record<string, never>>>;
|
|
55
|
+
declare const pathReadableIssue: IssueCode<{
|
|
56
|
+
kind: PathKind;
|
|
57
|
+
}>;
|
|
58
|
+
declare const pathWritableIssue: IssueCode<{
|
|
59
|
+
kind: PathKind;
|
|
60
|
+
}>;
|
|
61
|
+
/** The four codes a numeric factory rejects with, one for each way its bounds are declared. */
|
|
62
|
+
interface BoundCodes {
|
|
63
|
+
unbounded: IssueCode<NoParams>;
|
|
64
|
+
range: IssueCode<{
|
|
65
|
+
max: number;
|
|
66
|
+
min: number;
|
|
67
|
+
}>;
|
|
68
|
+
min: IssueCode<{
|
|
69
|
+
min: number;
|
|
70
|
+
}>;
|
|
71
|
+
max: IssueCode<{
|
|
72
|
+
max: number;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
declare const integerCodes: BoundCodes;
|
|
76
|
+
declare const numberCodes: BoundCodes;
|
|
77
|
+
export { dateIssue, integerCodes, integerIssue, integerMaxIssue, integerMinIssue, integerRangeIssue, numberCodes, numberIssue, numberMaxIssue, numberMinIssue, numberRangeIssue, oneOfIssue, pathIssue, pathReadableIssue, pathWritableIssue, portIssue, textExactLengthIssue, textLengthRangeIssue, textMaxLengthIssue, textMinLengthIssue, textNonemptyIssue, textPatternIssue, urlIssue, urlSchemeIssue, uuidIssue, };
|
|
78
|
+
export type { BoundCodes };
|
package/dist/codes.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { issueCode } from './issue-code.js';
|
|
2
|
+
import { listing } from './issues.js';
|
|
3
|
+
import { isCount, isFiniteNumber, isSafeInteger, kindParams, lengthParams, maxParams, messageParams, minParams, noParams, protocolsParams, rangeParams, valuesParams, } from './params.js';
|
|
4
|
+
/**
|
|
5
|
+
* The catalog's issue codes, one for each sentence a catalog validator prints. Each code's
|
|
6
|
+
* parameters are that sentence's blanks and hold the rule's settings, never the rejected value.
|
|
7
|
+
*/
|
|
8
|
+
const oneCharacter = 1;
|
|
9
|
+
/** A count of characters, reading `character` for a count of 1. */
|
|
10
|
+
function characters(count) {
|
|
11
|
+
return count === oneCharacter ? '1 character' : `${String(count)} characters`;
|
|
12
|
+
}
|
|
13
|
+
/** The catalog's own code for a rule: the package name, `/`, and the rule name. */
|
|
14
|
+
function catalogCode(rule) {
|
|
15
|
+
return `@loomcli/validators/${rule}`;
|
|
16
|
+
}
|
|
17
|
+
const textNonemptyIssue = issueCode(catalogCode('text-nonempty'), {
|
|
18
|
+
message: () => 'Expected a nonempty value.',
|
|
19
|
+
schema: noParams,
|
|
20
|
+
});
|
|
21
|
+
const textMinLengthIssue = issueCode(catalogCode('text-min-length'), {
|
|
22
|
+
message: ({ min }) => `Expected at least ${characters(min)}.`,
|
|
23
|
+
schema: minParams(isCount),
|
|
24
|
+
});
|
|
25
|
+
const textMaxLengthIssue = issueCode(catalogCode('text-max-length'), {
|
|
26
|
+
message: ({ max }) => `Expected at most ${characters(max)}.`,
|
|
27
|
+
schema: maxParams(isCount),
|
|
28
|
+
});
|
|
29
|
+
const textExactLengthIssue = issueCode(catalogCode('text-exact-length'), {
|
|
30
|
+
message: ({ length }) => `Expected exactly ${characters(length)}.`,
|
|
31
|
+
schema: lengthParams,
|
|
32
|
+
});
|
|
33
|
+
const textLengthRangeIssue = issueCode(catalogCode('text-length-range'), {
|
|
34
|
+
message: ({ max, min }) => `Expected from ${String(min)} through ${characters(max)}.`,
|
|
35
|
+
schema: rangeParams(isCount),
|
|
36
|
+
});
|
|
37
|
+
/** The author's own sentence for a `text()` pattern, which is the code's one blank. */
|
|
38
|
+
const textPatternIssue = issueCode(catalogCode('text-pattern'), {
|
|
39
|
+
message: ({ message }) => message,
|
|
40
|
+
schema: messageParams,
|
|
41
|
+
});
|
|
42
|
+
const integerIssue = issueCode(catalogCode('integer'), {
|
|
43
|
+
message: () => 'Expected a whole number.',
|
|
44
|
+
schema: noParams,
|
|
45
|
+
});
|
|
46
|
+
const integerRangeIssue = issueCode(catalogCode('integer-range'), {
|
|
47
|
+
message: ({ max, min }) => `Expected a whole number from ${String(min)} through ${String(max)}.`,
|
|
48
|
+
schema: rangeParams(isSafeInteger),
|
|
49
|
+
});
|
|
50
|
+
const integerMinIssue = issueCode(catalogCode('integer-min'), {
|
|
51
|
+
message: ({ min }) => `Expected a whole number of at least ${String(min)}.`,
|
|
52
|
+
schema: minParams(isSafeInteger),
|
|
53
|
+
});
|
|
54
|
+
const integerMaxIssue = issueCode(catalogCode('integer-max'), {
|
|
55
|
+
message: ({ max }) => `Expected a whole number of at most ${String(max)}.`,
|
|
56
|
+
schema: maxParams(isSafeInteger),
|
|
57
|
+
});
|
|
58
|
+
const numberIssue = issueCode(catalogCode('number'), {
|
|
59
|
+
message: () => 'Expected a number.',
|
|
60
|
+
schema: noParams,
|
|
61
|
+
});
|
|
62
|
+
const numberRangeIssue = issueCode(catalogCode('number-range'), {
|
|
63
|
+
message: ({ max, min }) => `Expected a number from ${String(min)} through ${String(max)}.`,
|
|
64
|
+
schema: rangeParams(isFiniteNumber),
|
|
65
|
+
});
|
|
66
|
+
const numberMinIssue = issueCode(catalogCode('number-min'), {
|
|
67
|
+
message: ({ min }) => `Expected a number of at least ${String(min)}.`,
|
|
68
|
+
schema: minParams(isFiniteNumber),
|
|
69
|
+
});
|
|
70
|
+
const numberMaxIssue = issueCode(catalogCode('number-max'), {
|
|
71
|
+
message: ({ max }) => `Expected a number of at most ${String(max)}.`,
|
|
72
|
+
schema: maxParams(isFiniteNumber),
|
|
73
|
+
});
|
|
74
|
+
const portIssue = issueCode(catalogCode('port'), {
|
|
75
|
+
message: () => 'Expected a port number from 1 through 65535.',
|
|
76
|
+
schema: noParams,
|
|
77
|
+
});
|
|
78
|
+
const oneOfIssue = issueCode(catalogCode('one-of'), {
|
|
79
|
+
message: ({ values }) => `Expected one of: ${values.join(', ')}.`,
|
|
80
|
+
schema: valuesParams,
|
|
81
|
+
});
|
|
82
|
+
const urlIssue = issueCode(catalogCode('url'), {
|
|
83
|
+
message: () => 'Expected an absolute URL, such as https://example.com.',
|
|
84
|
+
schema: noParams,
|
|
85
|
+
});
|
|
86
|
+
const urlSchemeIssue = issueCode(catalogCode('url-scheme'), {
|
|
87
|
+
message: ({ protocols }) => `Expected an absolute URL with the scheme ${listing(protocols)}.`,
|
|
88
|
+
schema: protocolsParams,
|
|
89
|
+
});
|
|
90
|
+
const uuidIssue = issueCode(catalogCode('uuid'), {
|
|
91
|
+
message: () => 'Expected a UUID, such as 123e4567-e89b-12d3-a456-426614174000.',
|
|
92
|
+
schema: noParams,
|
|
93
|
+
});
|
|
94
|
+
const dateIssue = issueCode(catalogCode('date'), {
|
|
95
|
+
message: () => 'Expected a date as YYYY-MM-DD, such as 2026-09-25.',
|
|
96
|
+
schema: noParams,
|
|
97
|
+
});
|
|
98
|
+
const pathIssue = issueCode(catalogCode('path'), {
|
|
99
|
+
message: () => 'Expected a nonempty path with no NUL character.',
|
|
100
|
+
schema: noParams,
|
|
101
|
+
});
|
|
102
|
+
/** How each kind reads in a readable-path sentence. */
|
|
103
|
+
const readableKinds = {
|
|
104
|
+
any: 'file or directory',
|
|
105
|
+
directory: 'directory',
|
|
106
|
+
file: 'file',
|
|
107
|
+
};
|
|
108
|
+
/** How each kind reads in a writable-path sentence. */
|
|
109
|
+
const writableKinds = {
|
|
110
|
+
any: 'path',
|
|
111
|
+
directory: 'directory',
|
|
112
|
+
file: 'file',
|
|
113
|
+
};
|
|
114
|
+
const pathReadableIssue = issueCode(catalogCode('path-readable'), {
|
|
115
|
+
message: ({ kind }) => `Expected a readable ${readableKinds[kind]} that exists.`,
|
|
116
|
+
schema: kindParams,
|
|
117
|
+
});
|
|
118
|
+
const pathWritableIssue = issueCode(catalogCode('path-writable'), {
|
|
119
|
+
message: ({ kind }) => `Expected a writable ${writableKinds[kind]}, or a new ${writableKinds[kind]} in a writable directory.`,
|
|
120
|
+
schema: kindParams,
|
|
121
|
+
});
|
|
122
|
+
const integerCodes = {
|
|
123
|
+
max: integerMaxIssue,
|
|
124
|
+
min: integerMinIssue,
|
|
125
|
+
range: integerRangeIssue,
|
|
126
|
+
unbounded: integerIssue,
|
|
127
|
+
};
|
|
128
|
+
const numberCodes = {
|
|
129
|
+
max: numberMaxIssue,
|
|
130
|
+
min: numberMinIssue,
|
|
131
|
+
range: numberRangeIssue,
|
|
132
|
+
unbounded: numberIssue,
|
|
133
|
+
};
|
|
134
|
+
export { dateIssue, integerCodes, integerIssue, integerMaxIssue, integerMinIssue, integerRangeIssue, numberCodes, numberIssue, numberMaxIssue, numberMinIssue, numberRangeIssue, oneOfIssue, pathIssue, pathReadableIssue, pathWritableIssue, portIssue, textExactLengthIssue, textLengthRangeIssue, textMaxLengthIssue, textMinLengthIssue, textNonemptyIssue, textPatternIssue, urlIssue, urlSchemeIssue, uuidIssue, };
|
package/dist/create.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StandardJSONSchemaV1, StandardSchemaV1, ValidationContext } from '@loomcli/core';
|
|
2
|
+
/** What `parse` returns: `{ value }` for an accepted token or `{ issues }` for a rejected one. */
|
|
3
|
+
type ParseResult<Output> = StandardSchemaV1.Result<Output>;
|
|
4
|
+
/** A validator that publishes its input schema, as every catalog factory returns. */
|
|
5
|
+
type Validator<Output> = StandardSchemaV1<string, Output> & StandardJSONSchemaV1<string, Output>;
|
|
6
|
+
/** The author's definition of one validator. */
|
|
7
|
+
interface ValidatorDefinition<Output> {
|
|
8
|
+
parse: (raw: string, context: ValidationContext) => ParseResult<Output> | Promise<ParseResult<Output>>;
|
|
9
|
+
inputSchema?: Readonly<Record<string, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
/** The vendor every Standard Schema value this package builds reports. */
|
|
12
|
+
declare const vendor = "@loomcli/validators";
|
|
13
|
+
/**
|
|
14
|
+
* Builds a frozen Standard Schema value from the author's parse function.
|
|
15
|
+
* With an input schema it also publishes that schema through Standard JSON Schema.
|
|
16
|
+
*/
|
|
17
|
+
declare function createValidator<Output>(definition: ValidatorDefinition<Output> & {
|
|
18
|
+
inputSchema: Readonly<Record<string, unknown>>;
|
|
19
|
+
}): Validator<Output>;
|
|
20
|
+
declare function createValidator<Output>(definition: ValidatorDefinition<Output>): StandardSchemaV1<string, Output>;
|
|
21
|
+
export { createValidator, vendor };
|
|
22
|
+
export type { ParseResult, Validator, ValidatorDefinition };
|
package/dist/create.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { DeclarationError, validationContext } from '@loomcli/core';
|
|
2
|
+
import { copyRecord, isPlainObject } from './data.js';
|
|
3
|
+
import { fault } from './faults.js';
|
|
4
|
+
import { contextOutsideRun, inputSchema, validatorDefinition } from './rules.js';
|
|
5
|
+
/** The vendor every Standard Schema value this package builds reports. */
|
|
6
|
+
const vendor = '@loomcli/validators';
|
|
7
|
+
const target = 'draft-2020-12';
|
|
8
|
+
const dialect = 'https://json-schema.org/draft/2020-12/schema';
|
|
9
|
+
/** A read of the context outside a run, which no declaration call stands for. */
|
|
10
|
+
function unavailable() {
|
|
11
|
+
throw new DeclarationError(contextOutsideRun, {
|
|
12
|
+
correction: 'Call the validator through an Application run, or leave the context unread.',
|
|
13
|
+
sentence: 'This validator reads the validation context, which only exists during a Loom run.',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The context `parse` receives when core did not make the call.
|
|
18
|
+
* Every field throws at the read, so only a validator that reads the context fails.
|
|
19
|
+
*/
|
|
20
|
+
const outsideRun = Object.freeze({
|
|
21
|
+
get command() {
|
|
22
|
+
return unavailable();
|
|
23
|
+
},
|
|
24
|
+
get host() {
|
|
25
|
+
return unavailable();
|
|
26
|
+
},
|
|
27
|
+
get input() {
|
|
28
|
+
return unavailable();
|
|
29
|
+
},
|
|
30
|
+
get passthrough() {
|
|
31
|
+
return unavailable();
|
|
32
|
+
},
|
|
33
|
+
get phase() {
|
|
34
|
+
return unavailable();
|
|
35
|
+
},
|
|
36
|
+
get supplied() {
|
|
37
|
+
return unavailable();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
/** The converter that publishes a frozen input schema for draft 2020-12 and nothing else. */
|
|
41
|
+
function converter(schema) {
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
input: (options) => {
|
|
44
|
+
if (options.target !== target) {
|
|
45
|
+
throw new Error(`This validator publishes JSON Schema for ${target} only, not ${options.target}.`);
|
|
46
|
+
}
|
|
47
|
+
return { $schema: dialect, ...copyRecord(schema, false) };
|
|
48
|
+
},
|
|
49
|
+
output: () => {
|
|
50
|
+
throw new Error('This validator publishes no output schema.');
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Checks a definition that bypassed the types, and returns its input schema copied and frozen, or
|
|
56
|
+
* undefined when it declares none.
|
|
57
|
+
*/
|
|
58
|
+
function checkDefinition(definition) {
|
|
59
|
+
const at = (mark) => ({ arguments: [definition], factory: 'createValidator', mark });
|
|
60
|
+
if (!isPlainObject(definition)) {
|
|
61
|
+
throw fault(validatorDefinition, at('0'), {
|
|
62
|
+
correction: 'Supply an object with a parse function.',
|
|
63
|
+
sentence: 'createValidator() definition is not a plain object.',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (typeof definition.parse !== 'function') {
|
|
67
|
+
throw fault(validatorDefinition, at('parse' in definition ? '0.parse' : '0'), {
|
|
68
|
+
correction: 'Supply a function that validates one raw string.',
|
|
69
|
+
sentence: 'createValidator() parse is not a function.',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return declaredSchema(definition.inputSchema, at);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The declared input schema, checked, copied, and frozen, or undefined when none is declared. `at`
|
|
76
|
+
* rebuilds the `createValidator()` call, marking the part at fault.
|
|
77
|
+
*/
|
|
78
|
+
function declaredSchema(declared, at) {
|
|
79
|
+
if (declared === undefined) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (!isPlainObject(declared)) {
|
|
83
|
+
throw fault(inputSchema, at('0.inputSchema'), {
|
|
84
|
+
correction: 'Supply the JSON Schema as a plain object.',
|
|
85
|
+
sentence: 'createValidator() inputSchema is not a plain object.',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (Object.hasOwn(declared, '$schema')) {
|
|
89
|
+
throw fault(inputSchema, at('0.inputSchema.$schema'), {
|
|
90
|
+
correction: 'Leave out $schema, which the validator publishes itself.',
|
|
91
|
+
sentence: 'createValidator() inputSchema declares its own $schema.',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return copyRecord(declared, true);
|
|
95
|
+
}
|
|
96
|
+
function createValidator(definition) {
|
|
97
|
+
const schema = checkDefinition(definition);
|
|
98
|
+
const { parse } = definition;
|
|
99
|
+
const props = {
|
|
100
|
+
validate: (value, options) => {
|
|
101
|
+
if (typeof value !== 'string') {
|
|
102
|
+
throw new TypeError(`A validator reads one raw string, and received a value of type ${typeof value}.`);
|
|
103
|
+
}
|
|
104
|
+
return parse(value, validationContext(options) ?? outsideRun);
|
|
105
|
+
},
|
|
106
|
+
vendor,
|
|
107
|
+
version: 1,
|
|
108
|
+
};
|
|
109
|
+
if (schema === undefined) {
|
|
110
|
+
return Object.freeze({ '~standard': Object.freeze(props) });
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze({ '~standard': Object.freeze({ ...props, jsonSchema: converter(schema) }) });
|
|
113
|
+
}
|
|
114
|
+
export { createValidator, vendor };
|
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Whether a value is a plain object: an object literal or one made with a null prototype. */
|
|
2
|
+
declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* A deep copy of plain JSON-like data, so neither the author nor a reader can change the original.
|
|
5
|
+
* Arrays and plain objects are copied, frozen when asked, and every other value is kept as it is.
|
|
6
|
+
*/
|
|
7
|
+
declare function copyRecord(record: Readonly<Record<string, unknown>>, freeze: boolean): Record<string, unknown>;
|
|
8
|
+
export { copyRecord, isPlainObject };
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Whether a value is a plain object: an object literal or one made with a null prototype. */
|
|
2
|
+
function isPlainObject(value) {
|
|
3
|
+
if (typeof value !== 'object' || value === null) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const prototype = Object.getPrototypeOf(value);
|
|
7
|
+
return prototype === Object.prototype || prototype === null;
|
|
8
|
+
}
|
|
9
|
+
function copyValue(value, freeze) {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
const items = value;
|
|
12
|
+
const copy = items.map((item) => copyValue(item, freeze));
|
|
13
|
+
return freeze ? Object.freeze(copy) : copy;
|
|
14
|
+
}
|
|
15
|
+
return isPlainObject(value) ? copyRecord(value, freeze) : value;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A deep copy of plain JSON-like data, so neither the author nor a reader can change the original.
|
|
19
|
+
* Arrays and plain objects are copied, frozen when asked, and every other value is kept as it is.
|
|
20
|
+
*/
|
|
21
|
+
function copyRecord(record, freeze) {
|
|
22
|
+
const copy = Object.fromEntries(Object.entries(record).map(([key, value]) => [key, copyValue(value, freeze)]));
|
|
23
|
+
return freeze ? Object.freeze(copy) : copy;
|
|
24
|
+
}
|
|
25
|
+
export { copyRecord, isPlainObject };
|
package/dist/date.d.ts
ADDED
package/dist/date.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { dateIssue } from './codes.js';
|
|
2
|
+
import { createValidator } from './create.js';
|
|
3
|
+
import { reject } from './issues.js';
|
|
4
|
+
const layout = /^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})$/u;
|
|
5
|
+
/**
|
|
6
|
+
* Five 400-year Gregorian cycles.
|
|
7
|
+
* The calendar repeats every cycle, so a shifted year keeps its leap days, and the shift moves
|
|
8
|
+
* years below 100 out of the range `Date.UTC` maps to the 1900s.
|
|
9
|
+
*/
|
|
10
|
+
const cycleShift = 2000;
|
|
11
|
+
/** The number of the first month, which `Date.UTC` counts from zero. */
|
|
12
|
+
const january = 1;
|
|
13
|
+
/** Whether the parts name a real day of the proleptic Gregorian calendar. */
|
|
14
|
+
function isRealDay(year, month, day) {
|
|
15
|
+
const monthIndex = month - january;
|
|
16
|
+
const probe = new Date(Date.UTC(year + cycleShift, monthIndex, day));
|
|
17
|
+
return probe.getUTCMonth() === monthIndex && probe.getUTCDate() === day;
|
|
18
|
+
}
|
|
19
|
+
/** A calendar date as `YYYY-MM-DD`, kept as the token because a date has no time or zone. */
|
|
20
|
+
function date() {
|
|
21
|
+
const issue = dateIssue.issue({});
|
|
22
|
+
return createValidator({
|
|
23
|
+
inputSchema: { type: 'string', format: 'date' },
|
|
24
|
+
parse: (raw) => {
|
|
25
|
+
const parts = layout.exec(raw)?.groups;
|
|
26
|
+
const real = parts !== undefined &&
|
|
27
|
+
isRealDay(Number(parts.year), Number(parts.month), Number(parts.day));
|
|
28
|
+
return real ? { value: raw } : reject(issue);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export { date };
|
package/dist/faults.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DeclarationError } from '@loomcli/core';
|
|
2
|
+
import type { DiagnosticRule } from '@loomcli/core';
|
|
3
|
+
/** One factory call as a finding rebuilds it: the factory's name and the arguments it received. */
|
|
4
|
+
interface FactoryCall {
|
|
5
|
+
readonly factory: string;
|
|
6
|
+
readonly arguments: readonly unknown[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A factory argument that can never work, under the rule it breaks. The finding rebuilds the call
|
|
10
|
+
* and marks the argument or the key at fault, `mark` being its dotted path among the arguments,
|
|
11
|
+
* with a note when given. The sentence names the factory and the argument and states the problem,
|
|
12
|
+
* and the correction states the fix.
|
|
13
|
+
*/
|
|
14
|
+
declare function fault(rule: DiagnosticRule, at: FactoryCall & {
|
|
15
|
+
readonly mark: string;
|
|
16
|
+
readonly note?: string;
|
|
17
|
+
}, parts: {
|
|
18
|
+
readonly sentence: string;
|
|
19
|
+
readonly correction: string;
|
|
20
|
+
}): DeclarationError;
|
|
21
|
+
/** A declared value as a fault message quotes it: a string in quotes, a primitive as printed. */
|
|
22
|
+
declare function quote(value: unknown): string;
|
|
23
|
+
/**
|
|
24
|
+
* A factory's options read as unknown values, because a JavaScript caller can pass anything.
|
|
25
|
+
* Omitted options read as an empty record.
|
|
26
|
+
*/
|
|
27
|
+
declare function readOptions(factory: string, options: unknown): Readonly<Record<string, unknown>>;
|
|
28
|
+
export { fault, quote, readOptions };
|
|
29
|
+
export type { FactoryCall };
|
package/dist/faults.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DeclarationError } from '@loomcli/core';
|
|
2
|
+
import { isPlainObject } from './data.js';
|
|
3
|
+
import { factoryOptions } from './rules.js';
|
|
4
|
+
/**
|
|
5
|
+
* A factory argument that can never work, under the rule it breaks. The finding rebuilds the call
|
|
6
|
+
* and marks the argument or the key at fault, `mark` being its dotted path among the arguments,
|
|
7
|
+
* with a note when given. The sentence names the factory and the argument and states the problem,
|
|
8
|
+
* and the correction states the fix.
|
|
9
|
+
*/
|
|
10
|
+
function fault(rule, at, parts) {
|
|
11
|
+
const { mark, note } = at;
|
|
12
|
+
const finding = { arguments: at.arguments, call: at.factory, mark };
|
|
13
|
+
return new DeclarationError(rule, {
|
|
14
|
+
correction: parts.correction,
|
|
15
|
+
findings: [note === undefined ? finding : { ...finding, note }],
|
|
16
|
+
sentence: parts.sentence,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** A declared value as a fault message quotes it: a string in quotes, a primitive as printed. */
|
|
20
|
+
function quote(value) {
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'number' ||
|
|
25
|
+
typeof value === 'boolean' ||
|
|
26
|
+
typeof value === 'bigint' ||
|
|
27
|
+
value === null ||
|
|
28
|
+
value === undefined) {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
return `a value of type ${typeof value}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A factory's options read as unknown values, because a JavaScript caller can pass anything.
|
|
35
|
+
* Omitted options read as an empty record.
|
|
36
|
+
*/
|
|
37
|
+
function readOptions(factory, options) {
|
|
38
|
+
if (options === undefined) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
if (!isPlainObject(options)) {
|
|
42
|
+
throw fault(factoryOptions, { arguments: [options], factory, mark: '0' }, {
|
|
43
|
+
correction: 'Supply an object or leave it out.',
|
|
44
|
+
sentence: `${factory}() options is not a plain object.`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
export { fault, quote, readOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { dateIssue, integerIssue, integerMaxIssue, integerMinIssue, integerRangeIssue, numberIssue, numberMaxIssue, numberMinIssue, numberRangeIssue, oneOfIssue, pathIssue, pathReadableIssue, pathWritableIssue, portIssue, textExactLengthIssue, textLengthRangeIssue, textMaxLengthIssue, textMinLengthIssue, textNonemptyIssue, textPatternIssue, urlIssue, urlSchemeIssue, uuidIssue, } from './codes.js';
|
|
2
|
+
export { createValidator } from './create.js';
|
|
3
|
+
export { date } from './date.js';
|
|
4
|
+
export { integer } from './integer.js';
|
|
5
|
+
export { issueCode } from './issue-code.js';
|
|
6
|
+
export { number } from './number.js';
|
|
7
|
+
export { oneOf } from './one-of.js';
|
|
8
|
+
export { path } from './path.js';
|
|
9
|
+
export { port } from './port.js';
|
|
10
|
+
export { text } from './text.js';
|
|
11
|
+
export { url } from './url.js';
|
|
12
|
+
export { uuid } from './uuid.js';
|
|
13
|
+
export type { ParseResult, Validator } from './create.js';
|
|
14
|
+
export type { IssueCode } from './issue-code.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { dateIssue, integerIssue, integerMaxIssue, integerMinIssue, integerRangeIssue, numberIssue, numberMaxIssue, numberMinIssue, numberRangeIssue, oneOfIssue, pathIssue, pathReadableIssue, pathWritableIssue, portIssue, textExactLengthIssue, textLengthRangeIssue, textMaxLengthIssue, textMinLengthIssue, textNonemptyIssue, textPatternIssue, urlIssue, urlSchemeIssue, uuidIssue, } from './codes.js';
|
|
2
|
+
export { createValidator } from './create.js';
|
|
3
|
+
export { date } from './date.js';
|
|
4
|
+
export { integer } from './integer.js';
|
|
5
|
+
export { issueCode } from './issue-code.js';
|
|
6
|
+
export { number } from './number.js';
|
|
7
|
+
export { oneOf } from './one-of.js';
|
|
8
|
+
export { path } from './path.js';
|
|
9
|
+
export { port } from './port.js';
|
|
10
|
+
export { text } from './text.js';
|
|
11
|
+
export { url } from './url.js';
|
|
12
|
+
export { uuid } from './uuid.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Validator } from './create.js';
|
|
2
|
+
interface IntegerOptions {
|
|
3
|
+
min?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
}
|
|
6
|
+
/** The safe integer a token spells in decimal digits, `-0` as `0`, or undefined when none. */
|
|
7
|
+
declare function readInteger(raw: string): number | undefined;
|
|
8
|
+
/** A decimal whole number that is a safe integer within `min` and `max`, both inclusive. */
|
|
9
|
+
declare function integer(options?: IntegerOptions): Validator<number>;
|
|
10
|
+
export { integer, readInteger };
|
|
11
|
+
export type { IntegerOptions };
|