Skip to content

Commit 3e1a043

Browse files
feat: implement Standard Schema (#1500)
* feat: add standard schema as a dependency * feat: implement standard schema * feat: add StandardSchema to validateEquals * fix: fix typing * test: add tests for standard schema * fix: fix factories didn't have standard schemas * fix: fix test * fix: fix test failing * fix: fix parser * fix: fix parser again * chore: remove $input from path * test: add tests via generator * fix: fix test compile failing * fix: fix path parser * fix: fix test of standard validator * fix: fix test failing --------- Co-authored-by: Jeongho Nam <samchon.github@gmail.com>
1 parent c6ede16 commit 3e1a043

File tree

178 files changed

+1932
-13
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

178 files changed

+1932
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"homepage": "https://typia.io",
4444
"dependencies": {
4545
"@samchon/openapi": "^4.2.0",
46+
"@standard-schema/spec": "^1.0.0",
4647
"commander": "^10.0.0",
4748
"comment-json": "^4.2.3",
4849
"inquirer": "^8.2.5",

pnpm-lock.yaml

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { StandardSchemaV1 } from "@standard-schema/spec";
2+
3+
import { IValidation } from "../IValidation";
4+
5+
export const _createStandardSchema = <T>(
6+
fn: (input: unknown) => IValidation<T>,
7+
) =>
8+
Object.assign(fn, {
9+
"~standard": {
10+
version: 1,
11+
vendor: "typia",
12+
validate: (input: unknown): StandardSchemaV1.Result<T> => {
13+
const result = fn(input);
14+
if (result.success) {
15+
return {
16+
value: result.data,
17+
} satisfies StandardSchemaV1.SuccessResult<T>;
18+
} else {
19+
return {
20+
issues: result.errors.map((error) => ({
21+
message: `expected ${error.expected}, got ${error.value}`,
22+
path: typiaPathToStandardSchemaPath(error.path),
23+
})),
24+
} satisfies StandardSchemaV1.FailureResult;
25+
}
26+
},
27+
},
28+
} satisfies StandardSchemaV1<unknown, T>);
29+
30+
enum PathParserState {
31+
// Start of a new segment
32+
// When the parser is in this state,
33+
// the pointer must point `.` or `[` or equal to length of the path
34+
Start,
35+
// Parsing a property key (`.hoge`)
36+
Property,
37+
// Parsing a string key (`["fuga"]`)
38+
StringKey,
39+
// Parsing a number key (`[42]`)
40+
NumberKey,
41+
}
42+
43+
const typiaPathToStandardSchemaPath = (
44+
path: string,
45+
): ReadonlyArray<StandardSchemaV1.PathSegment> => {
46+
if (!path.startsWith("$input")) {
47+
throw new Error(`Invalid path: ${JSON.stringify(path)}`);
48+
}
49+
50+
const segments: StandardSchemaV1.PathSegment[] = [];
51+
let currentSegment = "";
52+
let state: PathParserState = PathParserState.Start;
53+
let index = "$input".length - 1;
54+
while (index < path.length - 1) {
55+
index++;
56+
const char = path[index];
57+
58+
if (state === PathParserState.Property) {
59+
if (char === "." || char === "[") {
60+
// End of property
61+
segments.push({
62+
key: currentSegment,
63+
});
64+
state = PathParserState.Start;
65+
} else if (index === path.length - 1) {
66+
// End of path
67+
currentSegment += char;
68+
segments.push({
69+
key: currentSegment,
70+
});
71+
index++;
72+
state = PathParserState.Start;
73+
} else {
74+
currentSegment += char;
75+
}
76+
} else if (state === PathParserState.StringKey) {
77+
if (char === '"') {
78+
// End of string key
79+
segments.push({
80+
key: JSON.parse(currentSegment + char),
81+
});
82+
// Skip `"` and `]`
83+
index += 2;
84+
state = PathParserState.Start;
85+
} else if (char === "\\") {
86+
// Skip the next character from parsing
87+
currentSegment += path[index];
88+
index++;
89+
currentSegment += path[index];
90+
} else {
91+
currentSegment += char;
92+
}
93+
} else if (state === PathParserState.NumberKey) {
94+
if (char === "]") {
95+
// End of number key
96+
segments.push({
97+
key: Number.parseInt(currentSegment),
98+
});
99+
index++;
100+
state = PathParserState.Start;
101+
} else {
102+
currentSegment += char;
103+
}
104+
}
105+
106+
if (state === PathParserState.Start && index < path.length - 1) {
107+
const newChar = path[index];
108+
currentSegment = "";
109+
if (newChar === "[") {
110+
if (path[index + 1] === '"') {
111+
// Start of string key
112+
// NOTE: Typia uses JSON.stringify for this kind of keys, so `'` will not used as a string delimiter
113+
state = PathParserState.StringKey;
114+
index++;
115+
currentSegment = '"';
116+
} else {
117+
// Start of number key
118+
state = PathParserState.NumberKey;
119+
}
120+
} else if (newChar === ".") {
121+
// Start of property
122+
state = PathParserState.Property;
123+
} else {
124+
throw new Error("Unreachable: pointer points invalid character");
125+
}
126+
}
127+
}
128+
129+
if (state !== PathParserState.Start) {
130+
throw new Error(`Failed to parse path: ${JSON.stringify(path)}`);
131+
}
132+
133+
return segments;
134+
};

src/module.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { StandardSchemaV1 } from "@standard-schema/spec";
12
import { NoTransformConfigurationError } from "./transformers/NoTransformConfigurationError";
23

34
import { AssertionGuard } from "./AssertionGuard";
@@ -731,12 +732,14 @@ export function createValidate(): never;
731732
*
732733
* @author Jeongho Nam - https://github.com/samchon
733734
*/
734-
export function createValidate<T>(): (input: unknown) => IValidation<T>;
735+
export function createValidate<T>(): ((input: unknown) => IValidation<T>) &
736+
StandardSchemaV1<unknown, T>;
735737

736738
/**
737739
* @internal
738740
*/
739-
export function createValidate(): (input: unknown) => IValidation {
741+
export function createValidate(): ((input: unknown) => IValidation) &
742+
StandardSchemaV1<unknown, unknown> {
740743
NoTransformConfigurationError("createValidate");
741744
}
742745

@@ -887,12 +890,16 @@ export function createValidateEquals(): never;
887890
*
888891
* @author Jeongho Nam - https://github.com/samchon
889892
*/
890-
export function createValidateEquals<T>(): (input: unknown) => IValidation<T>;
893+
export function createValidateEquals<T>(): ((
894+
input: unknown,
895+
) => IValidation<T>) &
896+
StandardSchemaV1<unknown, T>;
891897

892898
/**
893899
* @internal
894900
*/
895-
export function createValidateEquals(): (input: unknown) => IValidation {
901+
export function createValidateEquals(): ((input: unknown) => IValidation) &
902+
StandardSchemaV1<unknown, unknown> {
896903
NoTransformConfigurationError("createValidateEquals");
897904
}
898905

src/programmers/FeatureProgrammer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ export namespace FeatureProgrammer {
297297
modulo: ts.LeftHandSideExpression;
298298
functor: FunctionProgrammer;
299299
result: IDecomposed;
300+
returnWrapper?: (arrow: ts.ArrowFunction) => ts.Expression;
300301
}): ts.CallExpression =>
301302
ts.factory.createCallExpression(
302303
ts.factory.createArrowFunction(
@@ -311,7 +312,11 @@ export namespace FeatureProgrammer {
311312
.filter(([k]) => props.functor.hasLocal(k))
312313
.map(([_k, v]) => v),
313314
...props.result.statements,
314-
ts.factory.createReturnStatement(props.result.arrow),
315+
ts.factory.createReturnStatement(
316+
props.returnWrapper
317+
? props.returnWrapper(props.result.arrow)
318+
: props.result.arrow,
319+
),
315320
]),
316321
),
317322
undefined,

src/programmers/ValidateProgrammer.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { check_object } from "./internal/check_object";
2020
export namespace ValidateProgrammer {
2121
export interface IConfig {
2222
equals: boolean;
23+
standardSchema?: boolean;
2324
}
2425
export interface IProps extends IProgrammerProps {
2526
config: IConfig;
@@ -241,6 +242,14 @@ export namespace ValidateProgrammer {
241242
modulo: props.modulo,
242243
functor,
243244
result,
245+
returnWrapper: props.config.standardSchema
246+
? (arrow) =>
247+
ts.factory.createCallExpression(
248+
props.context.importer.internal("createStandardSchema"),
249+
undefined,
250+
[arrow],
251+
)
252+
: undefined,
244253
});
245254
};
246255
}

src/transformers/CallExpressionTransformer.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,20 @@ const FUNCTORS: Record<string, Record<string, () => Task>> = {
195195
CreateAssertTransformer.transform({ equals: false, guard: false }),
196196
createIs: () => CreateIsTransformer.transform({ equals: false }),
197197
createValidate: () =>
198-
CreateValidateTransformer.transform({ equals: false }),
198+
CreateValidateTransformer.transform({
199+
equals: false,
200+
standardSchema: true,
201+
}),
199202
createAssertEquals: () =>
200203
CreateAssertTransformer.transform({ equals: true, guard: false }),
201204
createAssertGuardEquals: () =>
202205
CreateAssertTransformer.transform({ equals: true, guard: true }),
203206
createEquals: () => CreateIsTransformer.transform({ equals: true }),
204207
createValidateEquals: () =>
205-
CreateValidateTransformer.transform({ equals: true }),
208+
CreateValidateTransformer.transform({
209+
equals: true,
210+
standardSchema: true,
211+
}),
206212
createRandom: () => CreateRandomTransformer.transform,
207213
},
208214
functional: {

test/build/internal/TestFeature.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import { write_random } from "../writers/write_random";
77

88
export interface TestFeature {
99
module: string | null;
10+
prefix?: string;
1011
method: string;
1112
creatable: boolean;
13+
createOnly?: boolean;
1214
spoilable: boolean;
1315
formData?: boolean;
1416
custom?: true;
@@ -53,6 +55,14 @@ export namespace TestFeature {
5355
creatable: true,
5456
spoilable: true,
5557
},
58+
{
59+
module: null,
60+
prefix: "standardSchema",
61+
method: "validate",
62+
creatable: false,
63+
createOnly: true,
64+
spoilable: true,
65+
},
5666

5767
// STRICT VALIDATORS
5868
{

test/build/template.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ async function generate(
4848
"src",
4949
"features",
5050
[
51-
feat.module ? `${feat.module}.${method}` : method,
52-
...(feat.custom === true ? ["Custom"] : ""),
51+
feat.prefix ? `${feat.prefix}.` : "",
52+
feat.module ? `${feat.module}.` : "",
53+
method,
54+
feat.custom === true ? "Custom" : "",
5355
].join(""),
5456
].join("/");
5557

@@ -72,6 +74,8 @@ async function generate(
7274
else if (feat.dynamic === false && s.name.startsWith("Dynamic")) continue;
7375

7476
const location: string = `${path}/test_${
77+
feat.prefix ? `${feat.prefix}_` : ""
78+
}${
7579
feat.module ? `${feat.module}_` : ""
7680
}${method}${feat.custom === true ? "Custom" : ""}_${s.name}.ts`;
7781
await fs.promises.writeFile(
@@ -92,6 +96,7 @@ function script(
9296
? feat.programmer(create)(struct.name)
9397
: write_common({
9498
module: feat.module,
99+
prefix: feat.prefix,
95100
method,
96101
})(create)(struct.name);
97102
if (false === method.toLowerCase().includes("assert")) return content;
@@ -154,8 +159,12 @@ async function main(): Promise<void> {
154159
})),
155160
];
156161
for (const feature of featureList) {
157-
await generate(feature, structures, false);
158-
if (feature.creatable) await generate(feature, structures, true);
162+
if (feature.createOnly) {
163+
await generate(feature, structures, true);
164+
} else {
165+
await generate(feature, structures, false);
166+
if (feature.creatable) await generate(feature, structures, true);
167+
}
159168
}
160169

161170
// SCHEMAS

test/build/writers/write_common.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export const ${file(p)}_${structure} = _${file({
2929
)(${functor(p)(create)(structure)});
3030
`;
3131

32-
const file = (p: IProps) => "test_" + method(p).replace(".", "_");
32+
const file = (p: IProps) =>
33+
"test_" + (p.prefix ? `${p.prefix}_` : "") + method(p).replace(".", "_");
3334
const method = (p: IProps) =>
3435
[p.module, p.method].filter((str) => !!str).join(".");
3536
const functor = (p: IProps) => (create: boolean) => (structure: string) =>
@@ -39,5 +40,6 @@ const functor = (p: IProps) => (create: boolean) => (structure: string) =>
3940

4041
interface IProps {
4142
module: string | null;
43+
prefix?: string;
4244
method: string;
4345
}

0 commit comments

Comments
 (0)