Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: ts support standard schema #130

Merged
merged 6 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions languages/dart/dart-client/lib/request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,6 @@ Future<T> parsedArriRequest<T, E extends Exception>(
clientVersion: clientVersion,
);
if (result.statusCode >= 200 && result.statusCode <= 299) {
final b = utf8.decode(result.bodyBytes);
print("BODY: $b");
return parser(utf8.decode(result.bodyBytes));
}
} catch (err) {
Expand Down
8 changes: 4 additions & 4 deletions languages/ts/ts-codegen/src/_index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ const referenceFile = fs.readFileSync(
'utf8',
);
test('Output matches reference file', async () => {
const prettierConfig = JSON.parse(
fs.readFileSync(path.resolve('../../../.prettierrc'), 'utf8'),
);
const result = await createTypescriptClient(appDef, {
clientName: 'ExampleClient',
outputFile: '',
prettierOptions: {
tabWidth: 4,
endOfLine: 'lf',
},
prettierOptions: prettierConfig,
});
expect(normalizeWhitespace(result)).toEqual(
normalizeWhitespace(referenceFile),
Expand Down
2 changes: 1 addition & 1 deletion languages/ts/ts-codegen/src/any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function tsAnyFromSchema(
return `${target} += JSON.stringify(${input})`;
},
toQueryStringTemplate(_input, _target, _key) {
return `console.warn("[WARNING] Cannot serialize any's to query string. Skipping property at ${context.instancePath}.")`;
return `console.warn('[WARNING] Cannot serialize any\\'s to query string. Skipping property at ${context.instancePath}.')`;
},
content: '',
};
Expand Down
2 changes: 1 addition & 1 deletion languages/ts/ts-codegen/src/discriminator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function tsTaggedUnionFromSchema(
return `${target} += $$${prefixedTypeName}.toJsonString(${input});`;
},
toQueryStringTemplate(_: string, __: string, ___: string): string {
return `console.warn("[WARNING] Cannot serialize nested objects to query string. Skipping property at ${context.instancePath}.");`;
return `console.warn('[WARNING] Cannot serialize nested objects to query string. Skipping property at ${context.instancePath}.');`;
},
content: '',
};
Expand Down
2 changes: 1 addition & 1 deletion languages/ts/ts-codegen/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function tsObjectFromSchema(
return `${target} += $$${prefixedTypeName}.toJsonString(${input});`;
},
toQueryStringTemplate(_input, _target) {
return `console.warn("[WARNING] Cannot serialize nested objects to query string. Skipping property at ${context.instancePath}.")`;
return `console.warn('[WARNING] Cannot serialize nested objects to query string. Skipping property at ${context.instancePath}.')`;
},
content: '',
};
Expand Down
2 changes: 1 addition & 1 deletion languages/ts/ts-codegen/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function tsRefFromSchema(
return `${target} += $$${prefixedTypeName}.toJsonString(${input});`;
},
toQueryStringTemplate(_, __, ___) {
return `console.warn("[WARNING] Nested objects cannot be serialized to query string. Ignoring property at ${context.instancePath}.");`;
return `console.warn('[WARNING] Nested objects cannot be serialized to query string. Ignoring property at ${context.instancePath}.');`;
},
content: '',
};
Expand Down
31 changes: 30 additions & 1 deletion languages/ts/ts-schema-typebox-adapter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AObjectSchema,
ASchema,
hideInvalidProperties,
SCHEMA_METADATA,
ValidationError,
type ValueError,
Expand All @@ -22,7 +23,7 @@ export function typeboxAdapter<TInput extends TSchema>(
: ASchema<Static<TInput>> {
const schema = jsonSchemaToJtdSchema(input as unknown as JsonSchemaType);
const compiled = TypeCompiler.Compile<any>(input);
return {
const result = {
...schema,
metadata: {
id: input.$id ?? input.title,
Expand Down Expand Up @@ -57,7 +58,35 @@ export function typeboxAdapter<TInput extends TSchema>(
},
},
},
'~standard': {
version: 1,
vendor: 'arri-typebox',
validate(value: unknown) {
if (compiled.Check(value)) {
return {
value,
};
}
if (typeof value === 'string') {
const parsedVal = JSON.parse(value);
if (compiled.Check(parsedVal)) {
return parsedVal;
}
}
const errors = typeboxErrorsToArriError(compiled.Errors(value));
return {
issues: errors.errors.map((err) => ({
message: err.message ?? 'Unknown error',
path: err.instancePath
.split('/')
.filter((item) => item.length > 0),
})),
};
},
},
} satisfies ASchema<Static<TInput>> as any;
hideInvalidProperties(result);
return result;
}

function typeboxErrorsToArriError(errs: ValueErrorIterator): ValidationError {
Expand Down
14 changes: 10 additions & 4 deletions languages/ts/ts-schema/benchmark/src/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { z } from 'zod';
import { a } from '../../src/_index';

const IntSchema = a.int32();
const IntSchemaValidator = a.compile(IntSchema);
const $$IntSchema = a.compile(IntSchema);
const TypeBoxIntSchema = Type.Integer();
const TypeBoxIntValidator = TypeCompiler.Compile(TypeBoxIntSchema);
const ajv = new Ajv({ strict: false });
Expand All @@ -34,7 +34,13 @@ void benny.suite(
a.validate(IntSchema, intInput);
}),
benny.add('Arri (Compiled)', () => {
IntSchemaValidator.validate(intInput);
$$IntSchema.validate(intInput);
}),
benny.add('Arri (Standard Schema)', () => {
IntSchema['~standard'].validate(intInput);
}),
benny.add('Arri (Compiled + Standard Schema', () => {
$$IntSchema['~standard'].validate(intInput);
}),
benny.add('Ajv - JSON Schema', () => {
ajv.validate(TypeBoxIntSchema, intInput);
Expand Down Expand Up @@ -77,7 +83,7 @@ void benny.suite(
a.parse(IntSchema, intStringInput);
}),
benny.add('Arri (Compiled)', () => {
IntSchemaValidator.parse(intStringInput);
$$IntSchema.parse(intStringInput);
}),
benny.add('Ajv - JTD (Compiled)', () => {
ajvJtdParser(intStringInput);
Expand Down Expand Up @@ -133,7 +139,7 @@ void benny.suite(
a.serialize(IntSchema, intInput);
}),
benny.add('Arri (Compiled)', () => {
IntSchemaValidator.serialize(intInput);
$$IntSchema.serialize(intInput);
}),
benny.add('Ajv - JTD (Compiled)', () => {
ajvJtdSerializer(intInput);
Expand Down
18 changes: 12 additions & 6 deletions languages/ts/ts-schema/benchmark/src/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const ArriUser = a.object({
}),
),
});
const ArriUserValidator = a.compile(ArriUser);
const $$ArriUser = a.compile(ArriUser);
type ArriUser = a.infer<typeof ArriUser>;

const input: ArriUser = {
Expand Down Expand Up @@ -253,7 +253,13 @@ void benny.suite(
a.validate(ArriUser, input);
}),
benny.add('Arri (Compiled)', () => {
ArriUserValidator.validate(input);
$$ArriUser.validate(input);
}),
benny.add('Arri (Standard-Schema)', () => {
ArriUser['~standard'].validate(input);
}),
benny.add('Arri (Compiled + Standard Schema)', () => {
$$ArriUser['~standard'].validate(input);
}),
benny.add('Ajv - JTD', () => {
ajvJtd.validate(ArriUser, input);
Expand Down Expand Up @@ -296,7 +302,7 @@ void benny.suite(
a.parse(ArriUser, inputJson);
}),
benny.add('Arri (Compiled)', () => {
ArriUserValidator.parse(inputJson);
$$ArriUser.parse(inputJson);
}),
benny.add('Ajv - JTD (Compiled)', () => {
AjvJtdUserParser(inputJson);
Expand Down Expand Up @@ -349,11 +355,11 @@ void benny.suite(
a.serialize(ArriUser, input);
}),
benny.add('Arri (Compiled)', () => {
ArriUserValidator.serialize(input);
$$ArriUser.serialize(input);
}),
benny.add('Arri (Compiled) Validate and Serialize', () => {
if (ArriUserValidator.validate(input)) {
ArriUserValidator.serialize(input);
if ($$ArriUser.validate(input)) {
$$ArriUser.serialize(input);
}
}),
benny.add('Ajv - JTD (Compiled)', () => {
Expand Down
3 changes: 2 additions & 1 deletion languages/ts/ts-schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"dependencies": {
"@arrirpc/type-defs": "workspace:*",
"scule": "^1.3.0",
"uncrypto": "^0.1.3"
"uncrypto": "^0.1.3",
"@standard-schema/spec": "1.0.0-rc.0"
},
"devDependencies": {
"ajv": "^8.17.1",
Expand Down
1 change: 1 addition & 0 deletions languages/ts/ts-schema/src/_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './adapter';
export * from './compile';
export * from './lib/_index';
export * from './schemas';
export * from './standardSchema';
export { a };
8 changes: 7 additions & 1 deletion languages/ts/ts-schema/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ValidationError } from './_index';
import { type compile } from './compile';
import { type ASchema, SCHEMA_METADATA, ValidationContext } from './schemas';
import { createStandardSchemaProperty } from './standardSchema';

export type ValidationAdapter = <T>(input: any) => ASchema<T>;

Expand All @@ -11,7 +12,7 @@ export function isAdaptedSchema(input: ASchema) {
export function validatorFromAdaptedSchema(
schema: ASchema,
): ReturnType<typeof compile> {
return {
const result: ReturnType<typeof compile> = {
compiledCode: {
parse: '',
serialize: '',
Expand Down Expand Up @@ -67,5 +68,10 @@ export function validatorFromAdaptedSchema(
};
return schema.metadata[SCHEMA_METADATA].serialize(input, context);
},
'~standard': createStandardSchemaProperty(
schema.metadata[SCHEMA_METADATA].validate,
schema.metadata[SCHEMA_METADATA].parse,
),
};
return result;
}
23 changes: 23 additions & 0 deletions languages/ts/ts-schema/src/compile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StandardSchemaV1 } from '@standard-schema/spec';

import { a } from './_index';

const User = a.object({
id: a.string(),
name: a.string(),
email: a.nullable(a.string()),
createdAt: a.timestamp(),
updatedAt: a.timestamp(),
});
type User = a.infer<typeof User>;
const $$User = a.compile(User);

describe('standard-schema support', () => {
it('properly infers types', async () => {
assertType<StandardSchemaV1<User>>($$User);
const result = await $$User['~standard'].validate('hello world');
if (!result.issues) {
assertType<User>(result.value);
}
});
});
53 changes: 28 additions & 25 deletions languages/ts/ts-schema/src/compile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isSchemaFormEnum, isSchemaFormType } from '@arrirpc/type-defs';
import { StandardSchemaV1 } from '@standard-schema/spec';

import {
type ASchema,
Expand All @@ -24,14 +25,16 @@ import {
uint32Max,
uint32Min,
} from './lib/numberConstants';
import { createStandardSchemaProperty } from './standardSchema';

export {
getSchemaParsingCode,
getSchemaSerializationCode,
getSchemaValidationCode,
};

export interface CompiledValidator<TSchema extends ASchema<any>> {
export interface CompiledValidator<TSchema extends ASchema<any>>
extends StandardSchemaV1<InferType<TSchema>> {
/**
* Determine if a type matches a schema. This is a type guard.
*/
Expand Down Expand Up @@ -67,30 +70,29 @@ export function compile<TSchema extends ASchema<any>>(
const serializer = getCompiledSerializer(schema);

const serializeFn = serializer.fn;
const validate = new Function(
'input',
validateCode,
) as CompiledValidator<TSchema>['validate'];

return {
validate,
parse(input) {
try {
return parseFn(input);
} catch (err) {
const errors = getInputErrors(schema, input);
let errorMessage = err instanceof Error ? err.message : '';
if (errors.length) {
errorMessage =
errors[0]!.message ??
`Parsing error at ${errors[0]!.instancePath}`;
}
throw new ValidationError({
message: errorMessage,
errors,
});
const validate = new Function('input', validateCode) as (
input: unknown,
) => input is InferType<TSchema>;
const parse = (input: unknown): InferType<TSchema> => {
try {
return parseFn(input);
} catch (err) {
const errors = getInputErrors(schema, input);
let errorMessage = err instanceof Error ? err.message : '';
if (errors.length) {
errorMessage =
errors[0]!.message ??
`Parsing error at ${errors[0]!.instancePath}`;
}
},
throw new ValidationError({
message: errorMessage,
errors,
});
}
};
const result: CompiledValidator<TSchema> = {
validate,
parse: parse,
safeParse(input) {
try {
const result = parseFn(input);
Expand All @@ -115,7 +117,6 @@ export function compile<TSchema extends ASchema<any>>(
};
}
},

serialize(input) {
try {
return serializeFn(input);
Expand All @@ -135,7 +136,9 @@ export function compile<TSchema extends ASchema<any>>(
parse: parser.code,
serialize: serializer.code,
},
'~standard': createStandardSchemaProperty(validate, parse),
};
return result;
}

type CompiledParser<TSchema extends ASchema<any>> =
Expand Down
11 changes: 10 additions & 1 deletion languages/ts/ts-schema/src/lib/any.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type Schema } from '@arrirpc/type-defs';
import { StandardSchemaV1 } from '@standard-schema/spec';

import { a } from '../_index';

it('Produces valid JTD Schema', () => {
it('Produces valid ATD Schema', () => {
const Schema = a.any();
expect(JSON.parse(JSON.stringify(Schema))).toStrictEqual({
metadata: {},
Expand All @@ -19,3 +20,11 @@ it('Produces valid JTD Schema', () => {
},
});
});

describe('supports standard-schema', () => {
it('properly infers types', async () => {
assertType<StandardSchemaV1<any>>(a.any());
const result = await a.any()['~standard'].validate('1');
if (!result.issues) assertType<any>(result.value);
});
});
Loading