@shivaedev/quality 0.0.0 → 0.1.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +127 -1
- package/dist/baseline/compare.d.ts +26 -0
- package/dist/baseline/compare.d.ts.map +1 -0
- package/dist/baseline/compare.js +42 -0
- package/dist/baseline/compare.js.map +1 -0
- package/dist/baseline/format.d.ts +13 -0
- package/dist/baseline/format.d.ts.map +1 -0
- package/dist/baseline/format.js +32 -0
- package/dist/baseline/format.js.map +1 -0
- package/dist/baseline/update.d.ts +24 -0
- package/dist/baseline/update.d.ts.map +1 -0
- package/dist/baseline/update.js +60 -0
- package/dist/baseline/update.js.map +1 -0
- package/dist/cli/args.d.ts +25 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +64 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/baseline.d.ts +5 -0
- package/dist/cli/baseline.d.ts.map +1 -0
- package/dist/cli/baseline.js +37 -0
- package/dist/cli/baseline.js.map +1 -0
- package/dist/cli/lint.d.ts +5 -0
- package/dist/cli/lint.d.ts.map +1 -0
- package/dist/cli/lint.js +20 -0
- package/dist/cli/lint.js.map +1 -0
- package/dist/cli/run-main.d.ts +3 -0
- package/dist/cli/run-main.d.ts.map +1 -0
- package/dist/cli/run-main.js +24 -0
- package/dist/cli/run-main.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +27 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/decode.d.ts +19 -0
- package/dist/config/decode.d.ts.map +1 -0
- package/dist/config/decode.js +25 -0
- package/dist/config/decode.js.map +1 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +47 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/resolve.d.ts +11 -0
- package/dist/config/resolve.d.ts.map +1 -0
- package/dist/config/resolve.js +31 -0
- package/dist/config/resolve.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/decoded.d.ts +13 -0
- package/dist/decoded.d.ts.map +1 -0
- package/dist/decoded.js +11 -0
- package/dist/decoded.js.map +1 -0
- package/dist/engine/evaluate.d.ts +18 -0
- package/dist/engine/evaluate.d.ts.map +1 -0
- package/dist/engine/evaluate.js +20 -0
- package/dist/engine/evaluate.js.map +1 -0
- package/dist/engine/run-rules.d.ts +12 -0
- package/dist/engine/run-rules.d.ts.map +1 -0
- package/dist/engine/run-rules.js +19 -0
- package/dist/engine/run-rules.js.map +1 -0
- package/dist/engine/session.d.ts +19 -0
- package/dist/engine/session.d.ts.map +1 -0
- package/dist/engine/session.js +25 -0
- package/dist/engine/session.js.map +1 -0
- package/dist/engine/violation.d.ts +12 -0
- package/dist/engine/violation.d.ts.map +1 -0
- package/dist/engine/violation.js +23 -0
- package/dist/engine/violation.js.map +1 -0
- package/dist/exceptions/registry.d.ts +24 -0
- package/dist/exceptions/registry.d.ts.map +1 -0
- package/dist/exceptions/registry.js +33 -0
- package/dist/exceptions/registry.js.map +1 -0
- package/dist/failure.d.ts +21 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +12 -0
- package/dist/failure.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory/collect.d.ts +15 -0
- package/dist/inventory/collect.d.ts.map +1 -0
- package/dist/inventory/collect.js +34 -0
- package/dist/inventory/collect.js.map +1 -0
- package/dist/inventory/filesystem.d.ts +16 -0
- package/dist/inventory/filesystem.d.ts.map +1 -0
- package/dist/inventory/filesystem.js +26 -0
- package/dist/inventory/filesystem.js.map +1 -0
- package/dist/inventory/ignore-scope.d.ts +17 -0
- package/dist/inventory/ignore-scope.d.ts.map +1 -0
- package/dist/inventory/ignore-scope.js +26 -0
- package/dist/inventory/ignore-scope.js.map +1 -0
- package/dist/inventory/walk.d.ts +8 -0
- package/dist/inventory/walk.d.ts.map +1 -0
- package/dist/inventory/walk.js +38 -0
- package/dist/inventory/walk.js.map +1 -0
- package/dist/report/plural.d.ts +2 -0
- package/dist/report/plural.d.ts.map +1 -0
- package/dist/report/plural.js +2 -0
- package/dist/report/plural.js.map +1 -0
- package/dist/report/render.d.ts +11 -0
- package/dist/report/render.d.ts.map +1 -0
- package/dist/report/render.js +55 -0
- package/dist/report/render.js.map +1 -0
- package/dist/report/stale.d.ts +8 -0
- package/dist/report/stale.d.ts.map +1 -0
- package/dist/report/stale.js +9 -0
- package/dist/report/stale.js.map +1 -0
- package/dist/rule.d.ts +56 -0
- package/dist/rule.d.ts.map +1 -0
- package/dist/rule.js +23 -0
- package/dist/rule.js.map +1 -0
- package/dist/rules/built-in.d.ts +6 -0
- package/dist/rules/built-in.d.ts.map +1 -0
- package/dist/rules/built-in.js +3 -0
- package/dist/rules/built-in.js.map +1 -0
- package/dist/rules/max-lines.d.ts +6 -0
- package/dist/rules/max-lines.d.ts.map +1 -0
- package/dist/rules/max-lines.js +26 -0
- package/dist/rules/max-lines.js.map +1 -0
- package/dist/standard-schema.d.ts +25 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +3 -0
- package/dist/standard-schema.js.map +1 -0
- package/package.json +53 -4
- package/src/baseline/compare.ts +81 -0
- package/src/baseline/format.ts +53 -0
- package/src/baseline/update.ts +83 -0
- package/src/cli/args.ts +81 -0
- package/src/cli/baseline.ts +52 -0
- package/src/cli/lint.ts +27 -0
- package/src/cli/run-main.ts +29 -0
- package/src/cli.ts +29 -0
- package/src/config/decode.ts +35 -0
- package/src/config/load.ts +58 -0
- package/src/config/resolve.ts +60 -0
- package/src/config.ts +33 -0
- package/src/decoded.ts +15 -0
- package/src/engine/evaluate.ts +39 -0
- package/src/engine/run-rules.ts +40 -0
- package/src/engine/session.ts +40 -0
- package/src/engine/violation.ts +35 -0
- package/src/exceptions/registry.ts +58 -0
- package/src/failure.ts +13 -0
- package/src/index.ts +5 -0
- package/src/inventory/collect.ts +53 -0
- package/src/inventory/filesystem.ts +47 -0
- package/src/inventory/ignore-scope.ts +44 -0
- package/src/inventory/walk.ts +58 -0
- package/src/report/plural.ts +1 -0
- package/src/report/render.ts +85 -0
- package/src/report/stale.ts +30 -0
- package/src/rule.ts +90 -0
- package/src/rules/built-in.ts +3 -0
- package/src/rules/max-lines.ts +31 -0
- package/src/standard-schema.ts +22 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describeEntry, describeTally, type Regression } from "../baseline/compare.ts";
|
|
2
|
+
import { type Outcome, passes } from "../engine/evaluate.ts";
|
|
3
|
+
import { groupBy, keyOf, type Violation } from "../engine/violation.ts";
|
|
4
|
+
import { plural } from "./plural.ts";
|
|
5
|
+
import { staleSections } from "./stale.ts";
|
|
6
|
+
|
|
7
|
+
export type WarningDetail = "summary" | "all";
|
|
8
|
+
|
|
9
|
+
export interface ReportContext {
|
|
10
|
+
readonly descriptions: ReadonlyMap<string, string>;
|
|
11
|
+
readonly registry: string;
|
|
12
|
+
readonly baseline: string;
|
|
13
|
+
readonly checked: number;
|
|
14
|
+
readonly warnings: WarningDetail;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const LISTED_FILES = 5;
|
|
18
|
+
|
|
19
|
+
const locate = (violation: Violation): string => (violation.line === undefined ? violation.file : `${violation.file}:${violation.line}`);
|
|
20
|
+
|
|
21
|
+
const regressionNote = (regression: Regression): string =>
|
|
22
|
+
` ${regression.entry.file} is over its baseline: ${describeTally(regression.tally)} against ${describeEntry(regression.entry)} baselined.`;
|
|
23
|
+
|
|
24
|
+
const heading = (label: string, rule: string, violations: ReadonlyArray<Violation>, context: ReportContext): ReadonlyArray<string> => {
|
|
25
|
+
const description = context.descriptions.get(rule);
|
|
26
|
+
return [`${label} ${rule} (${violations.length})`, ...(description === undefined ? [] : [` ${description}`])];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const detailed = (violations: ReadonlyArray<Violation>, regressions: ReadonlyMap<string, Regression>): ReadonlyArray<string> =>
|
|
30
|
+
[...groupBy(violations, (violation) => violation.file).values()].flatMap((inFile) => {
|
|
31
|
+
const first = inFile[0];
|
|
32
|
+
const regression = first === undefined ? undefined : regressions.get(keyOf(first.rule, first.file));
|
|
33
|
+
return [
|
|
34
|
+
...inFile.map((violation) => ` ${locate(violation)} ${violation.message}`),
|
|
35
|
+
...(regression === undefined ? [] : [regressionNote(regression)]),
|
|
36
|
+
];
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const summarized = (violations: ReadonlyArray<Violation>): ReadonlyArray<string> => {
|
|
40
|
+
const files = [...groupBy(violations, (violation) => violation.file).entries()].sort(([, left], [, right]) => right.length - left.length);
|
|
41
|
+
const listed = files.slice(0, LISTED_FILES).map(([file, inFile]) => `${file} (${inFile.length})`);
|
|
42
|
+
const rest = files.length - listed.length;
|
|
43
|
+
return [` ${listed.join(", ")}${rest > 0 ? `, and ${plural(rest, "more file", "more files")}` : ""}. --warnings all lists each one.`];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const ruleSections = (
|
|
47
|
+
label: string,
|
|
48
|
+
violations: ReadonlyArray<Violation>,
|
|
49
|
+
context: ReportContext,
|
|
50
|
+
body: (inRule: ReadonlyArray<Violation>) => ReadonlyArray<string>,
|
|
51
|
+
): ReadonlyArray<string> =>
|
|
52
|
+
[...groupBy(violations, (violation) => violation.rule).entries()].map(([rule, inRule]) =>
|
|
53
|
+
[...heading(label, rule, inRule, context), ...body(inRule)].join("\n"),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const failures = (outcome: Outcome): string => {
|
|
57
|
+
const stale = outcome.staleBaseline.length + outcome.staleRegistry.length;
|
|
58
|
+
return [
|
|
59
|
+
...(outcome.errors.length > 0 ? [plural(outcome.errors.length, "error")] : []),
|
|
60
|
+
...(stale > 0 ? [plural(stale, "stale entry", "stale entries")] : []),
|
|
61
|
+
].join(" and ");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const summary = (outcome: Outcome, context: ReportContext): string => {
|
|
65
|
+
const verdict = passes(outcome) ? "quality: passed" : `quality: failed with ${failures(outcome)}`;
|
|
66
|
+
const warnings = outcome.warnings.length > 0 ? `; ${plural(outcome.warnings.length, "warning")} from rules still at warn` : "";
|
|
67
|
+
const hidden = [
|
|
68
|
+
...(outcome.baselined > 0 ? [`${outcome.baselined} baselined`] : []),
|
|
69
|
+
...(outcome.registered > 0 ? [`${outcome.registered} registered`] : []),
|
|
70
|
+
];
|
|
71
|
+
const suppressed =
|
|
72
|
+
hidden.length === 0 ? "" : `; ${hidden.join(" and ")} ${outcome.baselined + outcome.registered === 1 ? "violation" : "violations"} not shown`;
|
|
73
|
+
return `${verdict}${warnings}. ${plural(context.checked, "source file")} checked${suppressed}.`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const render = (outcome: Outcome, context: ReportContext): string => {
|
|
77
|
+
const regressions = new Map(outcome.regressions.map((regression) => [keyOf(regression.entry.rule, regression.entry.file), regression]));
|
|
78
|
+
const warningBody = context.warnings === "all" ? (inRule: ReadonlyArray<Violation>) => detailed(inRule, regressions) : summarized;
|
|
79
|
+
return [
|
|
80
|
+
...ruleSections("error", outcome.errors, context, (inRule) => detailed(inRule, regressions)),
|
|
81
|
+
...ruleSections("warn", outcome.warnings, context, warningBody),
|
|
82
|
+
...staleSections(outcome, context),
|
|
83
|
+
summary(outcome, context),
|
|
84
|
+
].join("\n\n");
|
|
85
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StaleBaselineEntry } from "../baseline/compare.ts";
|
|
2
|
+
import type { Outcome } from "../engine/evaluate.ts";
|
|
3
|
+
import type { StaleRegistryEntry } from "../exceptions/registry.ts";
|
|
4
|
+
import { plural } from "./plural.ts";
|
|
5
|
+
|
|
6
|
+
interface StaleFiles {
|
|
7
|
+
readonly registry: string;
|
|
8
|
+
readonly baseline: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const section = (file: string, guidance: string, lines: ReadonlyArray<string>): ReadonlyArray<string> =>
|
|
12
|
+
lines.length === 0 ? [] : [[`error ${file}: ${plural(lines.length, "stale entry", "stale entries")}`, ` ${guidance}`, ...lines].join("\n")];
|
|
13
|
+
|
|
14
|
+
const baselineLine = ({ entry, problem }: StaleBaselineEntry): string => ` ${entry.rule} ${entry.file} ${problem}.`;
|
|
15
|
+
|
|
16
|
+
const registryLine = ({ entry, problem }: StaleRegistryEntry): string =>
|
|
17
|
+
` ${entry.rule} ${entry.file}${entry.subject === undefined ? "" : ` (${entry.subject})`} ${problem}.`;
|
|
18
|
+
|
|
19
|
+
export const staleSections = (outcome: Outcome, files: StaleFiles): ReadonlyArray<string> => [
|
|
20
|
+
...section(
|
|
21
|
+
files.baseline,
|
|
22
|
+
"The baseline only shrinks. Run `quality baseline prune` to drop fixed debt; prune never adds or raises an entry.",
|
|
23
|
+
outcome.staleBaseline.map(baselineLine),
|
|
24
|
+
),
|
|
25
|
+
...section(
|
|
26
|
+
files.registry,
|
|
27
|
+
"Every registered exception must still apply. Remove the entries whose exception is gone.",
|
|
28
|
+
outcome.staleRegistry.map(registryLine),
|
|
29
|
+
),
|
|
30
|
+
];
|
package/src/rule.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describeIssue, type StandardSchemaV1 } from "./standard-schema.ts";
|
|
2
|
+
|
|
3
|
+
export interface Finding {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly line?: number | undefined;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
/** Tells apart exceptions of one rule in one file, such as which Biome rule an override turns off. */
|
|
8
|
+
readonly subject?: string | undefined;
|
|
9
|
+
/** A size where larger is worse, such as a file's line count. A baselined file may not grow past it. */
|
|
10
|
+
readonly measure?: number | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Findings = ReadonlyArray<Finding>;
|
|
14
|
+
|
|
15
|
+
export interface SourceFile {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly text: string;
|
|
18
|
+
readonly lines: ReadonlyArray<string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RuleInputs {
|
|
22
|
+
readonly root: string;
|
|
23
|
+
readonly files: ReadonlyArray<string>;
|
|
24
|
+
readonly sources: ReadonlyArray<SourceFile>;
|
|
25
|
+
readonly readText: (path: string) => Promise<string | undefined>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RuleContext<Options> extends RuleInputs {
|
|
29
|
+
readonly options: Options;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Configured =
|
|
33
|
+
| { readonly _tag: "Invalid"; readonly issues: ReadonlyArray<string> }
|
|
34
|
+
| { readonly _tag: "Ready"; readonly check: (inputs: RuleInputs) => Promise<Findings> };
|
|
35
|
+
|
|
36
|
+
export interface Rule<Id extends string = string, Input = unknown> {
|
|
37
|
+
readonly id: Id;
|
|
38
|
+
readonly description: string;
|
|
39
|
+
// Method syntax keeps rules with different options in one list; configure validates its input at runtime.
|
|
40
|
+
configure(options: Input | undefined): Promise<Configured>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RuleDefinition<Id extends string, Options> {
|
|
44
|
+
readonly id: Id;
|
|
45
|
+
readonly description: string;
|
|
46
|
+
readonly check: (context: RuleContext<Options>) => Findings | PromiseLike<Findings>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface RuleDefinitionWithOptions<Id extends string, Input, Options> extends RuleDefinition<Id, Options> {
|
|
50
|
+
readonly options: StandardSchemaV1<Input, Options>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface RuleDefinitionWithoutOptions<Id extends string> extends RuleDefinition<Id, undefined> {
|
|
54
|
+
readonly options?: undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const noOptions: StandardSchemaV1<undefined, undefined> = {
|
|
58
|
+
"~standard": {
|
|
59
|
+
validate: (value) => (value === undefined ? { value: undefined } : { issues: [{ message: "this rule takes no options" }] }),
|
|
60
|
+
vendor: "@shivaedev/quality",
|
|
61
|
+
version: 1,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const configureWith =
|
|
66
|
+
<Input, Options>(schema: StandardSchemaV1<Input, Options>, check: RuleDefinition<string, Options>["check"], absent: unknown) =>
|
|
67
|
+
async (options: unknown): Promise<Configured> => {
|
|
68
|
+
const result = await schema["~standard"].validate(options ?? absent);
|
|
69
|
+
if (result.issues !== undefined) {
|
|
70
|
+
return { _tag: "Invalid", issues: result.issues.map(describeIssue) };
|
|
71
|
+
}
|
|
72
|
+
const value = result.value;
|
|
73
|
+
return { _tag: "Ready", check: async (inputs) => check({ ...inputs, options: value }) };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Defines a rule. Options are an object validated by a Standard Schema; a rule the config gives no options validates `{}`,
|
|
78
|
+
* so give each option a default or make it optional.
|
|
79
|
+
*/
|
|
80
|
+
export function defineRule<const Id extends string, Input, Options>(definition: RuleDefinitionWithOptions<Id, Input, Options>): Rule<Id, Input>;
|
|
81
|
+
export function defineRule<const Id extends string>(definition: RuleDefinitionWithoutOptions<Id>): Rule<Id, undefined>;
|
|
82
|
+
export function defineRule<const Id extends string, Input, Options>(
|
|
83
|
+
definition: RuleDefinitionWithOptions<Id, Input, Options> | RuleDefinitionWithoutOptions<Id>,
|
|
84
|
+
): Rule<Id, Input> | Rule<Id, undefined> {
|
|
85
|
+
const configure =
|
|
86
|
+
definition.options === undefined
|
|
87
|
+
? configureWith(noOptions, definition.check, undefined)
|
|
88
|
+
: configureWith(definition.options, definition.check, {});
|
|
89
|
+
return { configure, description: definition.description, id: definition.id };
|
|
90
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import ignore from "ignore";
|
|
3
|
+
import { defineRule, type Finding, type SourceFile } from "../rule.ts";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TEST_FILES: ReadonlyArray<string> = ["*.test.*", "*.spec.*", "test/", "tests/", "__tests__/"];
|
|
6
|
+
const DECLARATION = /\.d\.[cm]?ts$/;
|
|
7
|
+
|
|
8
|
+
const Limit = Schema.Int.check(Schema.isGreaterThan(0));
|
|
9
|
+
|
|
10
|
+
const MaxLinesOptions = Schema.Struct({
|
|
11
|
+
source: Limit.pipe(Schema.withDecodingDefaultKey(Effect.succeed(150))),
|
|
12
|
+
test: Limit.pipe(Schema.withDecodingDefaultKey(Effect.succeed(300))),
|
|
13
|
+
testFiles: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_TEST_FILES))),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const oversized = (file: SourceFile, limit: number): ReadonlyArray<Finding> =>
|
|
17
|
+
file.lines.length <= limit
|
|
18
|
+
? []
|
|
19
|
+
: [{ file: file.path, measure: file.lines.length, message: `${file.lines.length} lines exceeds the ${limit}-line limit.` }];
|
|
20
|
+
|
|
21
|
+
export const maxLines = defineRule({
|
|
22
|
+
id: "structure/max-lines",
|
|
23
|
+
description: "Keep each module to one job. Split a long file along its responsibilities; never golf it under the limit.",
|
|
24
|
+
options: Schema.toStandardSchemaV1(MaxLinesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
25
|
+
check: ({ options, sources }) => {
|
|
26
|
+
const tests = ignore().add([...options.testFiles]);
|
|
27
|
+
return sources
|
|
28
|
+
.filter((file) => !DECLARATION.test(file.path))
|
|
29
|
+
.flatMap((file) => oversized(file, tests.ignores(file.path) ? options.test : options.source));
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The Standard Schema V1 contract (standardschema.dev), copied as the specification recommends. Rule options accept any
|
|
2
|
+
// conforming schema, so a consumer's own Effect version, or another schema library, validates them.
|
|
3
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
4
|
+
readonly "~standard": {
|
|
5
|
+
readonly version: 1;
|
|
6
|
+
readonly vendor: string;
|
|
7
|
+
readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
8
|
+
readonly types?: { readonly input: Input; readonly output: Output } | undefined;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type StandardResult<Output> = { readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<StandardIssue> };
|
|
13
|
+
|
|
14
|
+
export interface StandardIssue {
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const segment = (part: PropertyKey | { readonly key: PropertyKey }): string => String(typeof part === "object" ? part.key : part);
|
|
20
|
+
|
|
21
|
+
export const describeIssue = (issue: StandardIssue): string =>
|
|
22
|
+
issue.path === undefined || issue.path.length === 0 ? issue.message : `${issue.path.map(segment).join(".")}: ${issue.message}`;
|