8000 standard schema support (#61438) · angular/angular@fead94d · GitHub
[go: up one dir, main page]

Skip to content

Commit fead94d

Browse files
mmalerbaleonsenftmichael-small
authored
standard schema support (#61438)
* add standard schema support * move node tests under test/node * Apply suggestions from code review Co-authored-by: Leon Senft <leonsenft@users.noreply.github.com> * Update packages/forms/experimental/test/web/standard_schema.spec.ts Co-authored-by: michael-small <33669563+michael-small@users.noreply.github.com> * fix return type docs --------- Co-authored-by: Leon Senft <leonsenft@users.noreply.github.com> Co-authored-by: michael-small <33669563+michael-small@users.noreply.github.com>
1 parent 090f7a3 commit fead94d

24 files changed

+299
-49
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {computed, resource, ɵisPromise} from '@angular/core';
2+
import {validateAsync} from './async';
3+
import {define} from './data';
4+
import {validateTree} from './logic';
5+
import {StandardSchemaV1} from './standard_schema_types';
6+
import {Field, FieldPath, FormTreeError} from './types';
7+
8+
/**
9+
* A validation error produced by running a standard schema validator.
10+
*/
11+
interface StandardSchemaFormTreeError extends FormTreeError {
12+
issue: StandardSchemaV1.Issue;
13+
}
14+
15+
/**
16+
* Validates a field using a `StandardSchemaV1` compatible validator (e.g. a zod validator).
17+
*
18+
* See https://github.com/standard-schema/standard-schema for more about standard schema.
19+
*
20+
* @param path The `FieldPath` to the field to validate.
21+
* @param schema The standard schema compatible validator to use for validation.
22+
* @template T The type of the field being validated.
23+
*/
24+
export function validateStandardSchema<T>(
25+
path: FieldPath<T>,
26+
schema: NoInfer<StandardSchemaV1<T>>,
27+
) {
28+
// We create both a sync and async validator because the standard schema validator can return
29+
// either a sync result or a Promise, and we need to handle both cases. The sync validator
30+
// handles the sync result, and the async validator handles the Promise.
31+
// We memoize the result of the validation function here, so that it is only run once for both
32+
// validators, it can then be passed through both sync & async validation.
33+
const schemaResult = define(path, ({value}) => {
34+
return computed(() => schema['~standard'].validate(value()));
35+
});
36+
37+
validateTree(path, ({state, fieldOf}) => {
38+
// Skip sync validation if the result is a Promise.
39+
const result = state.data(schemaResult)!();
40+
if (ɵisPromise(result)) {
41+
return [];
42+
}
43+
return result.issues?.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue)) ?? [];
44+
});
45+
46+
validateAsync(path, {
47+
params: ({state}) => {
48+
// Skip async validation if the result is *not* a Promise.
49+
const result = state.data(schemaResult)!();
50+
return ɵisPromise(result) ? result : undefined;
51+
},
52+
factory: (params) => {
53+
return resource({
54+
params,
55+
loader: async ({params}) => (await params)?.issues ?? [],
56+
});
57+
},
58+
errors: (issues, {fieldOf}) => {
59+
return issues.map((issue) => standardIssueToFormTreeError(fieldOf(path), issue));
60+
},
61+
});
62+
}
63+
64+
/**
65+
* Converts a `StandardSchemaV1.Issue` to a `FormTreeError`.
66+
*
67+
* @param field The root field to which the issue's path is relative.
68+
* @param issue The `StandardSchemaV1.Issue` to convert.
69+
* @returns A `StandardSchemaFormTreeError` representing the issue.
70+
*/
71+
export function standardIssueToFormTreeError(
72+
field: Field<unknown>,
73+
issue: StandardSchemaV1.Issue,
74+
): StandardSchemaFormTreeError {
75+
let target = field as Field<Record<PropertyKey, unknown>>;
76+
for (const pathPart of issue.path ?? []) {
77+
const pathKey = typeof pathPart === 'object' ? pathPart.key : pathPart;
78+
target = target[pathKey] as Field<Record<PropertyKey, unknown>>;
79+
}
80+
return {
81+
kind: '~standard',
82+
field: target,
83+
issue,
84+
};
85+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// DO NOT EDIT: This file is copy/pasted from https://github.com/standard-schema/standard-schema
2+
// TODO: should we keep this copy/pasted version of depend on it from npm?
3+
4+
/** The Standard Schema interface. */
5+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
6+
/** The Standard Schema properties. */
7+
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
8+
}
9+
10+
export declare namespace StandardSchemaV1 {
11+
/** The Standard Schema properties interface. */
12+
export interface Props<Input = unknown, Output = Input> {
13+
/** The version number of the standard. */
14+
readonly version: 1;
15+
/** The vendor name of the schema library. */
16+
readonly vendor: string;
17+
/** Validates unknown input values. */
18+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
19+
/** Inferred types associated with the schema. */
20+
readonly types?: Types<Input, Output> | undefined;
21+
}
22+
23+
/** The result interface of the validate function. */
24+
export type Result<Output> = SuccessResult<Output> | FailureResult;
25+
26+
/** The result interface if validation succeeds. */
27+
export interface SuccessResult<Output> {
28+
/** The typed output value. */
29+
readonly value: Output;
30+
/** The non-existent issues. */
31+
readonly issues?: undefined;
32+
}
33+
34+
/** The result interface if validation fails. */
35+
export interface FailureResult {
36+
/** The issues of failed validation. */
37+
readonly issues: ReadonlyArray<Issue>;
38+
}
39+
40+
/** The issue interface of the failure output. */
41+
export interface Issue {
42+
/** The error message of the issue. */
43+
readonly message: string;
44+
/** The path of the issue, if any. */
45+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
46+
}
47+
48+
/** The path segment interface of the issue. */
49+
export interface PathSegment {
50+
/** The key representing a path segment. */
51+
readonly key: PropertyKey;
52+
}
53+
54+
/** The Standard Schema types interface. */
55+
export interface Types<Input = unknown, Output = Input> {
56+
/** The input type of the schema. */
57+
readonly input: Input;
58+
/** The output type of the schema. */
59+
readonly output: Output;
60+
}
61+
62+
/** Infers the input type of a Standard Schema. */
63+
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
64+
Schema['~standard']['types']
65+
>['input'];
66+
67+
/** Infers the output type of a Standard Schema. */
68+
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
69+
Schema['~standard']['types']
70+
>['output'];
71+
}

packages/forms/experimental/test/BUILD.bazel renamed to packages/forms/experimental/test/node/BUILD.bazel

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ ts_library(
44
name = "test_lib",
55
testonly = True,
66
srcs = glob(["**/*.ts"]),
7-
# Visible to //:saucelabs_unit_tests_poc target
8-
visibility = ["//:__pkg__"],
97
deps = [
108
"//packages/common/http",
119
"//packages/common/http/testing",
@@ -21,7 +19,7 @@ ts_library(
2119
)
2220

2321
# To run this:
24-
# yarn bazel test //packages/forms/experimental/test:test
22+
# yarn bazel test //packages/forms/experimental/test/node:test
2523

2624
jasmine_node_test(
2725
name = "test",
@@ -31,7 +29,7 @@ jasmine_node_test(
3129
],
3230
)
3331

34-
# yarn bazel test //packages/forms/experimental/test:test_web_chromium
32+
# yarn bazel test //packages/forms/experimental/test/node:test_web_chromium
3533
karma_web_test_suite(
3634
name = "test_web",
3735
tags = ["manual"],

packages/forms/experimental/test/api/validators/email.spec.ts renamed to packages/forms/experimental/test/node/api/validators/email.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Injector, signal} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
11-
import {form, email} from '../../../public_api';
11+
import {email, form} from '../../../../public_api';
1212

1313
describe('email validator', () => {
1414
it('returns requiredTrue error when the value is false', () => {

packages/forms/experimental/test/api/validators/max.spec.ts renamed to packages/forms/experimental/test/node/api/validators/max.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Injector, signal} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
11-
import {MAX, form, max} from '../../../public_api';
11+
import {MAX, form, max} from '../../../../public_api';
1212

1313
describe('max validator', () => {
1414
it('returns max error when the value is larger', () => {

packages/forms/experimental/test/api/validators/max_length.spec.ts renamed to packages/forms/experimental/test/node/api/validators/max_length.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Injector, signal} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
11-
import {MAX_LENGTH, form, maxLength} from '../../../public_api';
11+
import {MAX_LENGTH, form, maxLength} from '../../../../public_api';
1212

1313
describe('maxLength validator', () => {
1414
it('returns maxLength error when the length is larger for strings', () => {

packages/forms/experimental/test/api/validators/min.spec.ts renamed to packages/forms/experimental/test/node/api/validators/min.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injector, signal} from '@angular/core';
10-
import {TestBed} from '@angular/core/testing';
11-
import {MIN, form, min} from '../../../public_api';
9+
import { Injector, signal } from '@angular/core';
10+
import { TestBed } from '@angular/core/testing';
11+
import { MIN, form, min } from '../../../../public_api';
1212

1313
describe('min validator', () => {
1414
it('returns min error when the value is smaller', () => {

packages/forms/experimental/test/api/validators/min_length.spec.ts renamed to packages/forms/experimental/test/node/api/validators/min_length.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Injector, signal} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
11-
import {MIN_LENGTH, form, minLength} from '../../../public_api';
11+
import {MIN_LENGTH, form, minLength} from '../../../../public_api';
1212

1313
describe('minLength validator', () => {
1414
it('returns minLength error when the length is smaller for strings', () => {

packages/forms/experimental/test/api/validators/pattern.spec.ts renamed to packages/forms/experimental/test/node/api/validators/pattern.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Injector, signal} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
11-
import {PATTERN, form, pattern} from '../../../public_api';
11+
import {PATTERN, form, pattern} from '../../../../public_api';
1212

1313
describe('pattern validator', () => {
1414
it('validates whether a value matches the string pattern', () => {

packages/forms/experimental/test/api/validators/required.spec.ts renamed to packages/forms/experimental/test/node/api/validators/required.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injector, signal} from '@angular/core';
10-
import {TestBed} from '@angular/core/testing';
11-
import {form, required} from '../../../public_api';
9+
import { Injector, signal } from '@angular/core';
10+
import { TestBed } from '@angular/core/testing';
11+
import { form, required } from '../../../../public_api';
1212

1313
describe('required validator', () => {
1414
it('returns required Error when the value is not present', () => {

0 commit comments

Comments
 (0)
0