@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/dist/probe.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access, stat } from 'node:fs/promises';
|
|
3
|
+
/** The probe failures that are a verdict about the operator's path rather than a broken host. */
|
|
4
|
+
const verdicts = new Set([
|
|
5
|
+
'ENOENT',
|
|
6
|
+
'ENOTDIR',
|
|
7
|
+
'EACCES',
|
|
8
|
+
'EPERM',
|
|
9
|
+
'ELOOP',
|
|
10
|
+
'ENAMETOOLONG',
|
|
11
|
+
'EINVAL',
|
|
12
|
+
]);
|
|
13
|
+
function codeOf(error) {
|
|
14
|
+
return error instanceof Error && 'code' in error && typeof error.code === 'string'
|
|
15
|
+
? error.code
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Runs one probe, reading a verdict failure as `false` and rethrowing any other failure. */
|
|
19
|
+
async function holds(probe) {
|
|
20
|
+
try {
|
|
21
|
+
return await probe();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const code = codeOf(error);
|
|
25
|
+
if (code !== undefined && verdicts.has(code)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isKind(stats, kind) {
|
|
32
|
+
if (kind === 'file') {
|
|
33
|
+
return stats.isFile();
|
|
34
|
+
}
|
|
35
|
+
return kind === 'directory' ? stats.isDirectory() : true;
|
|
36
|
+
}
|
|
37
|
+
/** Whether an entry exists, following symbolic links, is of `kind`, and grants `mode`. */
|
|
38
|
+
async function grants(target, kind, mode) {
|
|
39
|
+
const stats = await stat(target);
|
|
40
|
+
if (!isKind(stats, kind)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
await access(target, mode);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
async function readable(target, kind) {
|
|
47
|
+
return await holds(async () => await grants(target, kind, constants.R_OK));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether the process can write the entry, or create it when no entry exists: then the parent
|
|
51
|
+
* must be an existing directory the process can write. Nothing is created.
|
|
52
|
+
*/
|
|
53
|
+
async function writable(target, kind, parent) {
|
|
54
|
+
return await holds(async () => {
|
|
55
|
+
try {
|
|
56
|
+
return await grants(target, kind, constants.W_OK);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (codeOf(error) !== 'ENOENT') {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
return await grants(parent, 'directory', constants.W_OK);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export { holds, readable, writable };
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DiagnosticRule } from '@loomcli/core';
|
|
2
|
+
/** A factory's options that are not a plain object. */
|
|
3
|
+
declare const factoryOptions: DiagnosticRule;
|
|
4
|
+
/** A `oneOf()` list that is not a list of distinct nonempty strings. */
|
|
5
|
+
declare const oneOfValues: DiagnosticRule;
|
|
6
|
+
/** A bound that is not a number of the kind its validator reads. */
|
|
7
|
+
declare const boundValue: DiagnosticRule;
|
|
8
|
+
/** A lower bound above its upper bound. */
|
|
9
|
+
declare const boundsOrder: DiagnosticRule;
|
|
10
|
+
/** A `url()` protocols list that is not a list of scheme names. */
|
|
11
|
+
declare const urlProtocols: DiagnosticRule;
|
|
12
|
+
/** A `path()` access or kind outside its set, or a kind with no access to narrow. */
|
|
13
|
+
declare const pathCheck: DiagnosticRule;
|
|
14
|
+
/** A `text()` pattern that JSON Schema cannot read as it is. */
|
|
15
|
+
declare const textPattern: DiagnosticRule;
|
|
16
|
+
/** A `text()` pattern without its sentence, or a sentence without a pattern. */
|
|
17
|
+
declare const patternMessage: DiagnosticRule;
|
|
18
|
+
/** An issue code outside the `<package>[/<subpath>...]/<kebab-case-rule>` grammar. */
|
|
19
|
+
declare const issueCodeName: DiagnosticRule;
|
|
20
|
+
/** An `issueCode()` config without a Standard Schema and a message function. */
|
|
21
|
+
declare const issueCodeConfig: DiagnosticRule;
|
|
22
|
+
/** An issue code schema that answers with a promise, throws, or answers with no result. */
|
|
23
|
+
declare const issueCodeSchema: DiagnosticRule;
|
|
24
|
+
/** Parameters an issue code's schema rejects. */
|
|
25
|
+
declare const issueParameters: DiagnosticRule;
|
|
26
|
+
/** A `createValidator()` definition without a parse function. */
|
|
27
|
+
declare const validatorDefinition: DiagnosticRule;
|
|
28
|
+
/** A `createValidator()` input schema that is not a plain object, or declares its own dialect. */
|
|
29
|
+
declare const inputSchema: DiagnosticRule;
|
|
30
|
+
/** A validator that read the validation context outside a Loom run. */
|
|
31
|
+
declare const contextOutsideRun: DiagnosticRule;
|
|
32
|
+
export { boundsOrder, boundValue, contextOutsideRun, factoryOptions, inputSchema, issueCodeConfig, issueCodeName, issueCodeSchema, issueParameters, oneOfValues, pathCheck, patternMessage, textPattern, urlProtocols, validatorDefinition, };
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { diagnosticRule } from '@loomcli/core';
|
|
2
|
+
/*
|
|
3
|
+
* The catalog's rules for the declaration faults its factories raise. Each is declared once here
|
|
4
|
+
* through the public diagnosticRule(), under the package's own name, as any third-party validator
|
|
5
|
+
* package declares its rules, and shared by every site that raises it.
|
|
6
|
+
*/
|
|
7
|
+
/** One rule of this package, named under the package as its issue codes are. */
|
|
8
|
+
function catalogRule(name, definition) {
|
|
9
|
+
return diagnosticRule(`@loomcli/validators/${name}`, definition);
|
|
10
|
+
}
|
|
11
|
+
/** A factory's options that are not a plain object. */
|
|
12
|
+
const factoryOptions = catalogRule('factory-options', {
|
|
13
|
+
explanation: 'A catalog factory reads its settings from one options object, so a value of any other kind leaves it no settings to read.',
|
|
14
|
+
headline: 'Options not an object',
|
|
15
|
+
});
|
|
16
|
+
/** A `oneOf()` list that is not a list of distinct nonempty strings. */
|
|
17
|
+
const oneOfValues = catalogRule('one-of-values', {
|
|
18
|
+
explanation: 'oneOf() accepts a token equal to one of its values, and help, completion, and the published schema offer each value as a choice, so the list holds at least one value, each a distinct nonempty string.',
|
|
19
|
+
headline: 'Invalid oneOf values',
|
|
20
|
+
});
|
|
21
|
+
/** A bound that is not a number of the kind its validator reads. */
|
|
22
|
+
const boundValue = catalogRule('bound-value', {
|
|
23
|
+
explanation: "A bound limits the values or the length a validator accepts, and the published schema carries it, so it is a number of the kind the validator reads: a safe integer for integer(), a finite number for number(), and a non-negative safe integer for text()'s lengths.",
|
|
24
|
+
headline: 'Invalid bound',
|
|
25
|
+
});
|
|
26
|
+
/** A lower bound above its upper bound. */
|
|
27
|
+
const boundsOrder = catalogRule('bounds-order', {
|
|
28
|
+
explanation: 'A validator accepts what lies between its lower and its upper bound, both inclusive, so a lower bound above the upper one would reject every token.',
|
|
29
|
+
headline: 'Bounds out of order',
|
|
30
|
+
});
|
|
31
|
+
/** A `url()` protocols list that is not a list of scheme names. */
|
|
32
|
+
const urlProtocols = catalogRule('url-protocols', {
|
|
33
|
+
explanation: 'url() accepts a URL whose scheme the list names and publishes the list as a pattern, so it holds at least one scheme name: a letter followed by letters, digits, +, -, or ., with no trailing colon.',
|
|
34
|
+
headline: 'Invalid url protocols',
|
|
35
|
+
});
|
|
36
|
+
/** A `path()` access or kind outside its set, or a kind with no access to narrow. */
|
|
37
|
+
const pathCheck = catalogRule('path-check', {
|
|
38
|
+
explanation: 'path() reads the file system only to check an access, read or write, and kind narrows that check to a file, a directory, or any entry, so kind has no meaning without access.',
|
|
39
|
+
headline: 'Invalid path check',
|
|
40
|
+
});
|
|
41
|
+
/** A `text()` pattern that JSON Schema cannot read as it is. */
|
|
42
|
+
const textPattern = catalogRule('invalid-pattern', {
|
|
43
|
+
explanation: 'text() publishes its pattern in the JSON Schema it serves, which reads a pattern under the u flag alone, so the pattern is a regular expression that compiles under u and carries no other flag.',
|
|
44
|
+
headline: 'Invalid pattern',
|
|
45
|
+
});
|
|
46
|
+
/** A `text()` pattern without its sentence, or a sentence without a pattern. */
|
|
47
|
+
const patternMessage = catalogRule('pattern-message', {
|
|
48
|
+
explanation: 'Only the author can say what a pattern accepts, so text() takes a pattern together with one sentence that states it, and prints that sentence when a token does not match.',
|
|
49
|
+
headline: 'Pattern and message apart',
|
|
50
|
+
});
|
|
51
|
+
/** An issue code outside the `<package>[/<subpath>...]/<kebab-case-rule>` grammar. */
|
|
52
|
+
const issueCodeName = catalogRule('issue-code-name', {
|
|
53
|
+
explanation: 'An issue code names the package that declares it and the one sentence it prints, so a view rewords that sentence by its code. It is a package name, any kebab-case subpath segments, and a kebab-case rule name, joined by /.',
|
|
54
|
+
headline: 'Invalid issue code',
|
|
55
|
+
});
|
|
56
|
+
/** An `issueCode()` config without a Standard Schema and a message function. */
|
|
57
|
+
const issueCodeConfig = catalogRule('issue-code-config', {
|
|
58
|
+
explanation: "An issue code checks each issue's parameters with its schema and builds the issue's sentence from them with its message function, so its config holds both.",
|
|
59
|
+
headline: 'Invalid issue code config',
|
|
60
|
+
});
|
|
61
|
+
/** An issue code schema that answers with a promise, throws, or answers with no result. */
|
|
62
|
+
const issueCodeSchema = catalogRule('issue-code-schema', {
|
|
63
|
+
explanation: "issue() and read() check an issue's parameters with the code's schema synchronously, while a validator builds the issue or a view reads it, so the schema returns its value or its issues, and neither throws nor answers with a promise.",
|
|
64
|
+
headline: 'Broken issue code schema',
|
|
65
|
+
});
|
|
66
|
+
/** Parameters an issue code's schema rejects. */
|
|
67
|
+
const issueParameters = catalogRule('issue-parameters', {
|
|
68
|
+
explanation: "An issue carries parameters its code's schema accepts, so every view that reads them through read() gets the shape the code declares.",
|
|
69
|
+
headline: 'Issue parameters rejected',
|
|
70
|
+
});
|
|
71
|
+
/** A `createValidator()` definition without a parse function. */
|
|
72
|
+
const validatorDefinition = catalogRule('validator-definition', {
|
|
73
|
+
explanation: 'createValidator() builds a Standard Schema value around one parse function that validates a raw string, so its definition is an object that holds that function.',
|
|
74
|
+
headline: 'Invalid validator definition',
|
|
75
|
+
});
|
|
76
|
+
/** A `createValidator()` input schema that is not a plain object, or declares its own dialect. */
|
|
77
|
+
const inputSchema = catalogRule('input-schema', {
|
|
78
|
+
explanation: 'createValidator() publishes its input schema as draft 2020-12 JSON Schema and adds the $schema dialect itself, so inputSchema is a plain object with no $schema of its own.',
|
|
79
|
+
headline: 'Invalid input schema',
|
|
80
|
+
});
|
|
81
|
+
/** A validator that read the validation context outside a Loom run. */
|
|
82
|
+
const contextOutsideRun = catalogRule('context-outside-run', {
|
|
83
|
+
explanation: 'Core attaches the validation context to each call it makes to a validator. A call from anywhere else, such as a unit test, carries none, so a validator that reads it runs inside an Application run.',
|
|
84
|
+
headline: 'Context outside a run',
|
|
85
|
+
});
|
|
86
|
+
export { boundsOrder, boundValue, contextOutsideRun, factoryOptions, inputSchema, issueCodeConfig, issueCodeName, issueCodeSchema, issueParameters, oneOfValues, pathCheck, patternMessage, textPattern, urlProtocols, validatorDefinition, };
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Validator } from './create.js';
|
|
2
|
+
/**
|
|
3
|
+
* The bounds on a token's length, and a pattern with the author's sentence for what it accepts.
|
|
4
|
+
* Only the author can say what a pattern accepts, so `message` comes with `pattern` or not at all.
|
|
5
|
+
*/
|
|
6
|
+
type TextOptions = {
|
|
7
|
+
minLength?: number;
|
|
8
|
+
maxLength?: number;
|
|
9
|
+
} & ({
|
|
10
|
+
pattern?: undefined;
|
|
11
|
+
message?: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
pattern: RegExp;
|
|
14
|
+
message: string;
|
|
15
|
+
});
|
|
16
|
+
/** A string whose code-point length is within bounds and which the pattern matches, if any. */
|
|
17
|
+
declare function text(options?: TextOptions): Validator<string>;
|
|
18
|
+
export { text };
|
|
19
|
+
export type { TextOptions };
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { textExactLengthIssue, textLengthRangeIssue, textMaxLengthIssue, textMinLengthIssue, textNonemptyIssue, textPatternIssue, } from './codes.js';
|
|
2
|
+
import { createValidator } from './create.js';
|
|
3
|
+
import { fault, readOptions } from './faults.js';
|
|
4
|
+
import { reject } from './issues.js';
|
|
5
|
+
import { boundsOrder, boundValue, patternMessage, textPattern } from './rules.js';
|
|
6
|
+
const noLength = 0;
|
|
7
|
+
const oneCharacter = 1;
|
|
8
|
+
const defaultMinLength = 1;
|
|
9
|
+
/** The `text()` call a fault marks one key of. */
|
|
10
|
+
function textAt(call, key) {
|
|
11
|
+
return { ...call, mark: `0.${key}` };
|
|
12
|
+
}
|
|
13
|
+
function lengthOf(call, name, value) {
|
|
14
|
+
if (value === undefined) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < noLength) {
|
|
18
|
+
throw fault(boundValue, textAt(call, name), {
|
|
19
|
+
correction: 'Supply a whole number of 0 or more.',
|
|
20
|
+
sentence: `text() ${name} is not a non-negative safe integer.`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
/** Faults on a pattern flag other than `u`, which JSON Schema has no way to state. */
|
|
26
|
+
function checkFlags(call, pattern) {
|
|
27
|
+
const others = pattern.flags.replace('u', '');
|
|
28
|
+
if (others !== '') {
|
|
29
|
+
const noun = others.length === oneCharacter ? 'flag' : 'flags';
|
|
30
|
+
throw fault(textPattern, textAt(call, 'pattern'), {
|
|
31
|
+
correction: 'Supply a pattern with no flag other than u.',
|
|
32
|
+
sentence: `text() pattern carries the ${noun} ${others}.`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The author's pattern recompiled under the `u` flag, as JSON Schema reads a `pattern`. */
|
|
37
|
+
function patternOf(call, value) {
|
|
38
|
+
if (value === undefined) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!(value instanceof RegExp)) {
|
|
42
|
+
throw fault(textPattern, textAt(call, 'pattern'), {
|
|
43
|
+
correction: 'Supply a regular expression literal.',
|
|
44
|
+
sentence: 'text() pattern is not a RegExp.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
checkFlags(call, value);
|
|
48
|
+
try {
|
|
49
|
+
return new RegExp(value.source, 'u');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw fault(textPattern, textAt(call, 'pattern'), {
|
|
53
|
+
correction: 'Supply a pattern that is valid with the u flag.',
|
|
54
|
+
sentence: 'text() pattern does not compile under the u flag.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function messageOf(call, value) {
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value !== 'string' || value === '') {
|
|
63
|
+
throw fault(patternMessage, textAt(call, 'message'), {
|
|
64
|
+
correction: 'Supply one sentence that states the expectation.',
|
|
65
|
+
sentence: 'text() message is not a nonempty string.',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
/** The pattern rule, or undefined without a pattern; a pattern and a message each need the other. */
|
|
71
|
+
function patternRule(call, pattern, message) {
|
|
72
|
+
if (pattern === undefined) {
|
|
73
|
+
if (message !== undefined) {
|
|
74
|
+
throw fault(patternMessage, textAt(call, 'message'), {
|
|
75
|
+
correction: 'Supply a pattern or leave out message.',
|
|
76
|
+
sentence: 'text() message has no pattern to describe.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (message === undefined) {
|
|
82
|
+
throw fault(patternMessage, textAt(call, 'pattern'), {
|
|
83
|
+
correction: 'Supply a message that states what the pattern accepts.',
|
|
84
|
+
sentence: 'text() pattern has no message to describe it.',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return { issue: textPatternIssue.issue({ message }), pattern };
|
|
88
|
+
}
|
|
89
|
+
/** The issue for bounds with a `maxLength`, where `min` is `minLength` after its default. */
|
|
90
|
+
function boundedIssue(min, max) {
|
|
91
|
+
if (min === max) {
|
|
92
|
+
return textExactLengthIssue.issue({ length: min });
|
|
93
|
+
}
|
|
94
|
+
if (min === noLength) {
|
|
95
|
+
return textMaxLengthIssue.issue({ max });
|
|
96
|
+
}
|
|
97
|
+
return textLengthRangeIssue.issue({ max, min });
|
|
98
|
+
}
|
|
99
|
+
/** The length rule for the effective bounds, or undefined when every length passes. */
|
|
100
|
+
function lengthRule(min, max) {
|
|
101
|
+
if (max !== undefined) {
|
|
102
|
+
return { fits: (count) => count >= min && count <= max, issue: boundedIssue(min, max) };
|
|
103
|
+
}
|
|
104
|
+
if (min === noLength) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const issue = min === oneCharacter ? textNonemptyIssue.issue({}) : textMinLengthIssue.issue({ min });
|
|
108
|
+
return { fits: (count) => count >= min, issue };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The token's length in Unicode code points, as JSON Schema counts it.
|
|
112
|
+
* Under the `u` flag a dot matches one code point, a lone surrogate included, so replacing each
|
|
113
|
+
* match with one code unit leaves a string as long as the count.
|
|
114
|
+
*/
|
|
115
|
+
function codePoints(raw) {
|
|
116
|
+
return raw.replaceAll(/./gsu, '.').length;
|
|
117
|
+
}
|
|
118
|
+
/** A string whose code-point length is within bounds and which the pattern matches, if any. */
|
|
119
|
+
function text(options) {
|
|
120
|
+
const declared = readOptions('text', options);
|
|
121
|
+
const call = { arguments: [options], factory: 'text' };
|
|
122
|
+
const minLength = lengthOf(call, 'minLength', declared.minLength) ?? defaultMinLength;
|
|
123
|
+
const maxLength = lengthOf(call, 'maxLength', declared.maxLength);
|
|
124
|
+
if (maxLength !== undefined && minLength > maxLength) {
|
|
125
|
+
// A minLength left to its default has no key to mark, so the maxLength below it is marked.
|
|
126
|
+
const key = declared.minLength === undefined ? 'maxLength' : 'minLength';
|
|
127
|
+
throw fault(boundsOrder, textAt(call, key), {
|
|
128
|
+
correction: 'Supply a minLength at or below maxLength.',
|
|
129
|
+
sentence: `text() minLength ${minLength} is above maxLength ${maxLength}.`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const matching = patternRule(call, patternOf(call, declared.pattern), messageOf(call, declared.message));
|
|
133
|
+
const length = lengthRule(minLength, maxLength);
|
|
134
|
+
return createValidator({
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
minLength,
|
|
138
|
+
...(maxLength === undefined ? {} : { maxLength }),
|
|
139
|
+
...(matching === undefined ? {} : { pattern: matching.pattern.source }),
|
|
140
|
+
},
|
|
141
|
+
parse: (raw) => {
|
|
142
|
+
if (length !== undefined && !length.fits(codePoints(raw))) {
|
|
143
|
+
return reject(length.issue);
|
|
144
|
+
}
|
|
145
|
+
if (matching !== undefined && !matching.pattern.test(raw)) {
|
|
146
|
+
return reject(matching.issue);
|
|
147
|
+
}
|
|
148
|
+
return { value: raw };
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export { text };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RFC 3986 `URI` production, built from the RFC's own rule names:
|
|
3
|
+
* `scheme ":" hier-part [ "?" query ] [ "#" fragment ]`.
|
|
4
|
+
* Letters are spelled in both cases, so the expression needs no `i` flag.
|
|
5
|
+
*/
|
|
6
|
+
declare const uriPattern: RegExp;
|
|
7
|
+
/** The RFC 3986 `scheme` production on its own, for checking a declared protocol. */
|
|
8
|
+
declare const schemePattern: RegExp;
|
|
9
|
+
export { schemePattern, uriPattern };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RFC 3986 `URI` production, built from the RFC's own rule names:
|
|
3
|
+
* `scheme ":" hier-part [ "?" query ] [ "#" fragment ]`.
|
|
4
|
+
* Letters are spelled in both cases, so the expression needs no `i` flag.
|
|
5
|
+
*/
|
|
6
|
+
const hex = '[0-9A-Fa-f]';
|
|
7
|
+
const pctEncoded = `%${hex}{2}`;
|
|
8
|
+
const unreserved = String.raw `A-Za-z0-9\-._~`;
|
|
9
|
+
const subDelims = "!$&'()*+,;=";
|
|
10
|
+
const pchar = `(?:[${unreserved}${subDelims}:@]|${pctEncoded})`;
|
|
11
|
+
const scheme = String.raw `[A-Za-z][A-Za-z0-9+\-.]*`;
|
|
12
|
+
const userinfo = `(?:[${unreserved}${subDelims}:]|${pctEncoded})*`;
|
|
13
|
+
const decOctet = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])';
|
|
14
|
+
const ipv4 = String.raw `${decOctet}(?:\.${decOctet}){3}`;
|
|
15
|
+
const h16 = `${hex}{1,4}`;
|
|
16
|
+
const ls32 = `(?:${h16}:${h16}|${ipv4})`;
|
|
17
|
+
const ipv6 = `(?:${[
|
|
18
|
+
`(?:${h16}:){6}${ls32}`,
|
|
19
|
+
`::(?:${h16}:){5}${ls32}`,
|
|
20
|
+
`(?:${h16})?::(?:${h16}:){4}${ls32}`,
|
|
21
|
+
`(?:(?:${h16}:){0,1}${h16})?::(?:${h16}:){3}${ls32}`,
|
|
22
|
+
`(?:(?:${h16}:){0,2}${h16})?::(?:${h16}:){2}${ls32}`,
|
|
23
|
+
`(?:(?:${h16}:){0,3}${h16})?::${h16}:${ls32}`,
|
|
24
|
+
`(?:(?:${h16}:){0,4}${h16})?::${ls32}`,
|
|
25
|
+
`(?:(?:${h16}:){0,5}${h16})?::${h16}`,
|
|
26
|
+
`(?:(?:${h16}:){0,6}${h16})?::`,
|
|
27
|
+
].join('|')})`;
|
|
28
|
+
const ipvFuture = String.raw `[vV]${hex}+\.[${unreserved}${subDelims}:]+`;
|
|
29
|
+
const ipLiteral = String.raw `\[(?:${ipv6}|${ipvFuture})\]`;
|
|
30
|
+
const regName = `(?:[${unreserved}${subDelims}]|${pctEncoded})*`;
|
|
31
|
+
const host = `(?:${ipLiteral}|${ipv4}|${regName})`;
|
|
32
|
+
const authority = `(?:${userinfo}@)?${host}(?::[0-9]*)?`;
|
|
33
|
+
const pathAbempty = `(?:/${pchar}*)*`;
|
|
34
|
+
const pathAbsolute = `/(?:${pchar}+(?:/${pchar}*)*)?`;
|
|
35
|
+
const pathRootless = `${pchar}+(?:/${pchar}*)*`;
|
|
36
|
+
// RFC 3986 also allows an empty path, which the published `uri` format refuses, so soundness drops it.
|
|
37
|
+
const hierPart = `(?://${authority}${pathAbempty}|${pathAbsolute}|${pathRootless})`;
|
|
38
|
+
const queryOrFragment = `(?:${pchar}|[/?])*`;
|
|
39
|
+
const uriPattern = new RegExp(String.raw `^${scheme}:${hierPart}(?:\?${queryOrFragment})?(?:#${queryOrFragment})?$`, 'u');
|
|
40
|
+
/** The RFC 3986 `scheme` production on its own, for checking a declared protocol. */
|
|
41
|
+
const schemePattern = new RegExp(`^${scheme}$`, 'u');
|
|
42
|
+
export { schemePattern, uriPattern };
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Validator } from './create.js';
|
|
2
|
+
interface UrlOptions {
|
|
3
|
+
protocols?: readonly [string, ...string[]];
|
|
4
|
+
}
|
|
5
|
+
/** An absolute RFC 3986 URI that the WHATWG parser also reads, optionally with a listed scheme. */
|
|
6
|
+
declare function url(options?: UrlOptions): Validator<URL>;
|
|
7
|
+
export { url };
|
|
8
|
+
export type { UrlOptions };
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { urlIssue, urlSchemeIssue } from './codes.js';
|
|
2
|
+
import { createValidator } from './create.js';
|
|
3
|
+
import { fault, quote, readOptions } from './faults.js';
|
|
4
|
+
import { reject } from './issues.js';
|
|
5
|
+
import { urlProtocols } from './rules.js';
|
|
6
|
+
import { schemePattern, uriPattern } from './uri-grammar.js';
|
|
7
|
+
const empty = 0;
|
|
8
|
+
/** The `protocols` a `url()` call declares, checked, and each fault marks it or one entry. */
|
|
9
|
+
function protocolsOf(options, value) {
|
|
10
|
+
if (value === undefined) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const at = (mark) => ({ arguments: [options], factory: 'url', mark });
|
|
14
|
+
if (!Array.isArray(value)) {
|
|
15
|
+
throw fault(urlProtocols, at('0.protocols'), {
|
|
16
|
+
correction: 'Supply an array of scheme names.',
|
|
17
|
+
sentence: 'url() protocols is not an array.',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const items = value;
|
|
21
|
+
if (items.length === empty) {
|
|
22
|
+
throw fault(urlProtocols, at('0.protocols'), {
|
|
23
|
+
correction: 'List at least one scheme.',
|
|
24
|
+
sentence: 'url() protocols is empty.',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// `Array.from` reads a hole as `undefined`, so a sparse list faults instead of skipping it.
|
|
28
|
+
return Array.from(items, (item, index) => {
|
|
29
|
+
if (typeof item !== 'string' || !schemePattern.test(item)) {
|
|
30
|
+
throw fault(urlProtocols, at(`0.protocols.${String(index)}`), {
|
|
31
|
+
correction: 'List a letter followed by letters, digits, +, -, or ., with no trailing colon.',
|
|
32
|
+
sentence: `url() protocols lists ${quote(item)}, which is not a scheme name.`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return item;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One scheme as a case-free pattern: each letter as a two-case class, `+` and `.` escaped.
|
|
40
|
+
* A hyphen stays bare, because the `u` flag JSON Schema patterns use refuses `\-` outside a class.
|
|
41
|
+
*/
|
|
42
|
+
function spellScheme(scheme) {
|
|
43
|
+
return scheme
|
|
44
|
+
.replaceAll(/[+.]/gu, String.raw `\$&`)
|
|
45
|
+
.replaceAll(/[A-Za-z]/gu, (letter) => `[${letter.toLowerCase()}${letter.toUpperCase()}]`);
|
|
46
|
+
}
|
|
47
|
+
/** The pattern that publishes a scheme list, anchored at the start and ending at the colon. */
|
|
48
|
+
function schemesPattern(protocols) {
|
|
49
|
+
return `^(?:${protocols.map((protocol) => spellScheme(protocol)).join('|')}):`;
|
|
50
|
+
}
|
|
51
|
+
/** An absolute RFC 3986 URI that the WHATWG parser also reads, optionally with a listed scheme. */
|
|
52
|
+
function url(options) {
|
|
53
|
+
const protocols = protocolsOf(options, readOptions('url', options).protocols);
|
|
54
|
+
// The WHATWG parser lowercases the scheme and ends `protocol` with a colon.
|
|
55
|
+
const listed = new Set(protocols?.map((protocol) => `${protocol.toLowerCase()}:`));
|
|
56
|
+
const issue = protocols === undefined ? urlIssue.issue({}) : urlSchemeIssue.issue({ protocols });
|
|
57
|
+
return createValidator({
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
format: 'uri',
|
|
61
|
+
...(protocols === undefined ? {} : { pattern: schemesPattern(protocols) }),
|
|
62
|
+
},
|
|
63
|
+
parse: (raw) => {
|
|
64
|
+
if (!uriPattern.test(raw) || !URL.canParse(raw)) {
|
|
65
|
+
return reject(issue);
|
|
66
|
+
}
|
|
67
|
+
const parsed = new URL(raw);
|
|
68
|
+
return protocols === undefined || listed.has(parsed.protocol)
|
|
69
|
+
? { value: parsed }
|
|
70
|
+
: reject(issue);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export { url };
|
package/dist/uuid.d.ts
ADDED
package/dist/uuid.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { uuidIssue } from './codes.js';
|
|
2
|
+
import { createValidator } from './create.js';
|
|
3
|
+
import { reject } from './issues.js';
|
|
4
|
+
const grouped = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/u;
|
|
5
|
+
/** A UUID of any version in any letter case, read as lowercase so two spellings compare equal. */
|
|
6
|
+
function uuid() {
|
|
7
|
+
const issue = uuidIssue.issue({});
|
|
8
|
+
return createValidator({
|
|
9
|
+
inputSchema: { type: 'string', format: 'uuid' },
|
|
10
|
+
parse: (raw) => grouped.test(raw) ? { value: raw.toLowerCase() } : reject(issue),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export { uuid };
|
package/package.json
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomcli/validators",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "The Loom CLI validator catalog. Ships Standard Schema values for common command-line input shapes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/dbtlr/loomcli.git"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"ajv": "^8.20.0",
|
|
23
|
+
"ajv-formats": "^3.0.1"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@loomcli/core": "0.6.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"bun": ">=1.4.0",
|
|
30
|
+
"node": ">=22.23.2"
|
|
9
31
|
}
|
|
10
32
|
}
|