diff --git a/src/__demos__/samples.ts b/src/__demos__/samples.ts index 30e9cd3e..609a02e0 100644 --- a/src/__demos__/samples.ts +++ b/src/__demos__/samples.ts @@ -81,7 +81,9 @@ export const types = { 'pubsub.channel.Channel': s.Object( [ s.prop('id', s.str, {title: 'ID of the user'}), - s.prop('payload', s.Ref('pubsub.channel.PayloadType'), {description: 'Yup, the payload.'}), + s.prop('payload', s.Ref('pubsub.channel.PayloadType'), { + description: 'Yup, the payload.', + }), s.prop('meta', s.Object([s.Field('description', s.str)])), ], {description: 'A channel'}, diff --git a/src/codegen/binary/__tests__/testBinaryCodegen.ts b/src/codegen/binary/__tests__/testBinaryCodegen.ts index 72e5df52..a57432f1 100644 --- a/src/codegen/binary/__tests__/testBinaryCodegen.ts +++ b/src/codegen/binary/__tests__/testBinaryCodegen.ts @@ -409,7 +409,9 @@ export const testBinaryCodegen = (transcode: (system: TypeSystem, type: Type, va const t = system.t; system.alias('Obj', t.Object(t.prop('foo', t.str))); const type = t.Ref('Obj'); - expect(transcode(system, type, {foo: 'bar'})).toStrictEqual({foo: 'bar'}); + expect(transcode(system, type, {foo: 'bar'})).toStrictEqual({ + foo: 'bar', + }); }); }); diff --git a/src/codegen/json/__tests__/json.spec.ts b/src/codegen/json/__tests__/json.spec.ts index a60f1929..e2e8ab3b 100644 --- a/src/codegen/json/__tests__/json.spec.ts +++ b/src/codegen/json/__tests__/json.spec.ts @@ -117,7 +117,14 @@ describe('"obj" type', () => { exec(type, {a: 123, b: 'asdf'}); exec(type, {a: 123, b: 'asdf', c: 'qwerty'}); exec(type, {a: 123, d: 4343.3, b: 'asdf', c: 'qwerty', e: 'asdf'}); - exec(type, {a: 123, d: 4343.3, b: 'asdf', c: 'qwerty', e: 'asdf', z: true}); + exec(type, { + a: 123, + d: 4343.3, + b: 'asdf', + c: 'qwerty', + e: 'asdf', + z: true, + }); }); }); diff --git a/src/codegen/validator/ValidatorCodegenContext.ts b/src/codegen/validator/ValidatorCodegenContext.ts index 74c6f2bc..6265357b 100644 --- a/src/codegen/validator/ValidatorCodegenContext.ts +++ b/src/codegen/validator/ValidatorCodegenContext.ts @@ -131,8 +131,14 @@ export class ValidatorCodegenContext { const v = this.linkValidator(validatorName); const rerr = codegen.getRegister(); const rc = codegen.getRegister(); - const err = this.err(ValidationError.VALIDATION, path, {validator: validatorName, refError: rerr}); - const errInCatch = this.err(ValidationError.VALIDATION, path, {validator: validatorName, refError: rc}); + const err = this.err(ValidationError.VALIDATION, path, { + validator: validatorName, + refError: rerr, + }); + const errInCatch = this.err(ValidationError.VALIDATION, path, { + validator: validatorName, + refError: rc, + }); const emitRc = this.options.errors === 'object'; codegen.js(/* js */ `try { var ${rerr} = ${v}(${r}); diff --git a/src/codegen/validator/__tests__/codegen.spec.ts b/src/codegen/validator/__tests__/codegen.spec.ts index dbce0afb..840b8e41 100644 --- a/src/codegen/validator/__tests__/codegen.spec.ts +++ b/src/codegen/validator/__tests__/codegen.spec.ts @@ -77,10 +77,34 @@ test('can have array of unknown elements', () => { exec(type, [1, 2, 3], null); exec(type, [1, 'adsf'], null); exec(type, [1, {}], null); - exec(type, {}, {code: 'ARR', errno: ValidationError.ARR, message: 'Not an array.', path: []}); - exec(type, null, {code: 'ARR', errno: ValidationError.ARR, message: 'Not an array.', path: []}); - exec(type, 123, {code: 'ARR', errno: ValidationError.ARR, message: 'Not an array.', path: []}); - exec(type, 'asdf', {code: 'ARR', errno: ValidationError.ARR, message: 'Not an array.', path: []}); + exec( + type, + {}, + { + code: 'ARR', + errno: ValidationError.ARR, + message: 'Not an array.', + path: [], + }, + ); + exec(type, null, { + code: 'ARR', + errno: ValidationError.ARR, + message: 'Not an array.', + path: [], + }); + exec(type, 123, { + code: 'ARR', + errno: ValidationError.ARR, + message: 'Not an array.', + path: [], + }); + exec(type, 'asdf', { + code: 'ARR', + errno: ValidationError.ARR, + message: 'Not an array.', + path: [], + }); }); test('object can have a field of any type', () => { @@ -90,7 +114,16 @@ test('object can have a field of any type', () => { exec(type, {foo: 123}, null); exec(type, {foo: null}, null); exec(type, {foo: 'asdf'}, null); - exec(type, {}, {code: 'KEY', errno: ValidationError.KEY, message: 'Missing key.', path: ['foo']}); + exec( + type, + {}, + { + code: 'KEY', + errno: ValidationError.KEY, + message: 'Missing key.', + path: ['foo'], + }, + ); }); test('can detect extra properties in object', () => { @@ -102,7 +135,12 @@ test('can detect extra properties in object', () => { exec( type, {foo: 123, bar: 'asdf'}, - {code: 'KEYS', errno: ValidationError.KEYS, message: 'Too many or missing object keys.', path: ['bar']}, + { + code: 'KEYS', + errno: ValidationError.KEYS, + message: 'Too many or missing object keys.', + path: ['bar'], + }, undefined, ); }); @@ -112,9 +150,15 @@ test('can disable extra property check', () => { fields: [s.Field('foo', s.any), s.FieldOpt('zup', s.any)], }); exec(type, {foo: 123}, null, {skipObjectExtraFieldsCheck: true}); - exec(type, {foo: 123, zup: 'asdf'}, null, {skipObjectExtraFieldsCheck: true}); - exec(type, {foo: 123, bar: 'asdf'}, null, {skipObjectExtraFieldsCheck: true}); - exec(type, {foo: 123, zup: '1', bar: 'asdf'}, null, {skipObjectExtraFieldsCheck: true}); + exec(type, {foo: 123, zup: 'asdf'}, null, { + skipObjectExtraFieldsCheck: true, + }); + exec(type, {foo: 123, bar: 'asdf'}, null, { + skipObjectExtraFieldsCheck: true, + }); + exec(type, {foo: 123, zup: '1', bar: 'asdf'}, null, { + skipObjectExtraFieldsCheck: true, + }); }); describe('"str" type', () => { @@ -122,8 +166,18 @@ describe('"str" type', () => { const type = s.str; exec(type, '', null); exec(type, 'asdf', null); - exec(type, 123, {code: 'STR', errno: ValidationError.STR, message: 'Not a string.', path: []}); - exec(type, null, {code: 'STR', errno: ValidationError.STR, message: 'Not a string.', path: []}); + exec(type, 123, { + code: 'STR', + errno: ValidationError.STR, + message: 'Not a string.', + path: [], + }); + exec(type, null, { + code: 'STR', + errno: ValidationError.STR, + message: 'Not a string.', + path: [], + }); }); test('validates "min"', () => { @@ -228,8 +282,18 @@ describe('"bin" type', () => { const type = s.bin; exec(type, b(), null); exec(type, b(1, 2, 3), null); - exec(type, 123, {code: 'BIN', errno: ValidationError.BIN, message: 'Not a binary.', path: []}); - exec(type, null, {code: 'BIN', errno: ValidationError.BIN, message: 'Not a binary.', path: []}); + exec(type, 123, { + code: 'BIN', + errno: ValidationError.BIN, + message: 'Not a binary.', + path: [], + }); + exec(type, null, { + code: 'BIN', + errno: ValidationError.BIN, + message: 'Not a binary.', + path: [], + }); }); test('validates "min"', () => { @@ -335,11 +399,36 @@ describe('"num" type', () => { exec(type, 123, null); exec(type, -123, null); exec(type, 0, null); - exec(type, '123', {code: 'NUM', errno: ValidationError.NUM, message: 'Not a number.', path: []}); - exec(type, '-123', {code: 'NUM', errno: ValidationError.NUM, message: 'Not a number.', path: []}); - exec(type, '0', {code: 'NUM', errno: ValidationError.NUM, message: 'Not a number.', path: []}); - exec(type, '', {code: 'NUM', errno: ValidationError.NUM, message: 'Not a number.', path: []}); - exec(type, null, {code: 'NUM', errno: ValidationError.NUM, message: 'Not a number.', path: []}); + exec(type, '123', { + code: 'NUM', + errno: ValidationError.NUM, + message: 'Not a number.', + path: [], + }); + exec(type, '-123', { + code: 'NUM', + errno: ValidationError.NUM, + message: 'Not a number.', + path: [], + }); + exec(type, '0', { + code: 'NUM', + errno: ValidationError.NUM, + message: 'Not a number.', + path: [], + }); + exec(type, '', { + code: 'NUM', + errno: ValidationError.NUM, + message: 'Not a number.', + path: [], + }); + exec(type, null, { + code: 'NUM', + errno: ValidationError.NUM, + message: 'Not a number.', + path: [], + }); }); test('validates integer type', () => { @@ -347,17 +436,42 @@ describe('"num" type', () => { exec(type, 123, null); exec(type, -123, null); exec(type, 0, null); - exec(type, 123.4, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); - exec(type, -1.1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, 123.4, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); + exec(type, -1.1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); }); test('validates unsigned integer type', () => { const type = s.Number({format: 'u'}); exec(type, 123, null); exec(type, 0, null); - exec(type, -123, {code: 'UINT', errno: ValidationError.UINT, message: 'Not an unsigned integer.', path: []}); - exec(type, 123.4, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); - exec(type, -1.1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, -123, { + code: 'UINT', + errno: ValidationError.UINT, + message: 'Not an unsigned integer.', + path: [], + }); + exec(type, 123.4, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); + exec(type, -1.1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); }); test('validates i8', () => { @@ -368,20 +482,45 @@ describe('"num" type', () => { exec(type, 127, null); exec(type, -127, null); exec(type, -128, null); - exec(type, 128, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); - exec(type, -129, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, 128, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); + exec(type, -129, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); }); test('validates u8', () => { const type = s.Number({format: 'u8'}); exec(type, 123, null); exec(type, 0, null); - exec(type, -12, {code: 'UINT', errno: ValidationError.UINT, message: 'Not an unsigned integer.', path: []}); + exec(type, -12, { + code: 'UINT', + errno: ValidationError.UINT, + message: 'Not an unsigned integer.', + path: [], + }); exec(type, 127, null); exec(type, 222, null); exec(type, 255, null); - exec(type, 256, {code: 'UINT', errno: ValidationError.UINT, message: 'Not an unsigned integer.', path: []}); - exec(type, 333, {code: 'UINT', errno: ValidationError.UINT, message: 'Not an unsigned integer.', path: []}); + exec(type, 256, { + code: 'UINT', + errno: ValidationError.UINT, + message: 'Not an unsigned integer.', + path: [], + }); + exec(type, 333, { + code: 'UINT', + errno: ValidationError.UINT, + message: 'Not an unsigned integer.', + path: [], + }); }); test('validates i16', () => { @@ -395,10 +534,20 @@ describe('"num" type', () => { exec(type, -44, null); exec(type, 0x7fff - 1, null); exec(type, 0x7fff, null); - exec(type, 0x7fff + 1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, 0x7fff + 1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); exec(type, -0x8000 + 1, null); exec(type, -0x8000, null); - exec(type, -0x8000 - 1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, -0x8000 - 1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); }); test('validates u16', () => { @@ -419,7 +568,12 @@ describe('"num" type', () => { path: [], }); exec(type, 0, null); - exec(type, -44, {code: 'UINT', errno: ValidationError.UINT, message: 'Not an unsigned integer.', path: []}); + exec(type, -44, { + code: 'UINT', + errno: ValidationError.UINT, + message: 'Not an unsigned integer.', + path: [], + }); exec(type, 0x7fff - 1, null); exec(type, 0x7fff, null); exec(type, 0xffff - 1, null); @@ -459,10 +613,20 @@ describe('"num" type', () => { exec(type, -44, null); exec(type, 0x7fffffff - 1, null); exec(type, 0x7fffffff, null); - exec(type, 0x7fffffff + 1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, 0x7fffffff + 1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); exec(type, -0x80000000 + 1, null); exec(type, -0x80000000, null); - exec(type, -0x80000000 - 1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, -0x80000000 - 1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); }); test('validates u32', () => { @@ -483,7 +647,12 @@ describe('"num" type', () => { path: [], }); exec(type, 0, null); - exec(type, -44, {code: 'UINT', errno: ValidationError.UINT, message: 'Not an unsigned integer.', path: []}); + exec(type, -44, { + code: 'UINT', + errno: ValidationError.UINT, + message: 'Not an unsigned integer.', + path: [], + }); exec(type, 0x7fff - 1, null); exec(type, 0x7fff, null); exec(type, 0xffff - 1, null); @@ -525,8 +694,18 @@ describe('"num" type', () => { exec(type, -0x3333333333, null); exec(type, -0x333333333333, null); exec(type, 0, null); - exec(type, -44.123, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); - exec(type, 1.1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, -44.123, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); + exec(type, 1.1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); }); test('validates u64', () => { @@ -575,8 +754,18 @@ describe('"num" type', () => { path: [], }); exec(type, 0, null); - exec(type, -44.123, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); - exec(type, 1.1, {code: 'INT', errno: ValidationError.INT, message: 'Not an integer.', path: []}); + exec(type, -44.123, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); + exec(type, 1.1, { + code: 'INT', + errno: ValidationError.INT, + message: 'Not an integer.', + path: [], + }); }); }); @@ -597,7 +786,16 @@ describe('"or" type', () => { }); exec(type, {foo: 123}, null); exec(type, {foo: '123'}, null); - exec(type, {foo: false}, {code: 'OR', errno: ValidationError.OR, message: 'None of types matched.', path: ['foo']}); + exec( + type, + {foo: false}, + { + code: 'OR', + errno: ValidationError.OR, + message: 'None of types matched.', + path: ['foo'], + }, + ); }); test('array can be of multiple types', () => { @@ -624,7 +822,12 @@ describe('"or" type', () => { exec( type, {gg: [1, '3', false]}, - {code: 'OR', errno: ValidationError.OR, message: 'None of types matched.', path: ['gg', 2]}, + { + code: 'OR', + errno: ValidationError.OR, + message: 'None of types matched.', + path: ['gg', 2], + }, ); }); @@ -642,8 +845,18 @@ describe('"or" type', () => { exec(type, 'asdf', null); exec(type, {}, null); exec(type, {foo: 'bar'}, null); - exec(type, [], {code: 'OR', errno: ValidationError.OR, message: 'None of types matched.', path: []}); - exec(type, null, {code: 'OR', errno: ValidationError.OR, message: 'None of types matched.', path: []}); + exec(type, [], { + code: 'OR', + errno: ValidationError.OR, + message: 'None of types matched.', + path: [], + }); + exec(type, null, { + code: 'OR', + errno: ValidationError.OR, + message: 'None of types matched.', + path: [], + }); }); }); @@ -656,7 +869,12 @@ describe('"obj" type', () => { test('"null" is not of type "obj"', () => { const type = s.obj; - exec(type, null, {code: 'OBJ', errno: ValidationError.OBJ, message: 'Not an object.', path: []}); + exec(type, null, { + code: 'OBJ', + errno: ValidationError.OBJ, + message: 'Not an object.', + path: [], + }); }); }); @@ -664,7 +882,12 @@ describe('single root element', () => { test('null', () => { const type = s.nil; exec(type, null, null); - exec(type, '123', {code: 'CONST', errno: ValidationError.CONST, message: 'Invalid constant.', path: []}); + exec(type, '123', { + code: 'CONST', + errno: ValidationError.CONST, + message: 'Invalid constant.', + path: [], + }); }); test('number', () => { @@ -673,7 +896,12 @@ describe('single root element', () => { exec(type, 1.123, null); exec(type, -123, null); exec(type, -5.5, null); - exec(type, '123', {code: 'NUM', errno: ValidationError.NUM, message: 'Not a number.', path: []}); + exec(type, '123', { + code: 'NUM', + errno: ValidationError.NUM, + message: 'Not a number.', + path: [], + }); }); test('const number', () => { @@ -703,7 +931,12 @@ describe('single root element', () => { exec(type, '', null); exec(type, 'a', null); exec(type, 'asdf', null); - exec(type, 123, {code: 'STR', errno: ValidationError.STR, message: 'Not a string.', path: []}); + exec(type, 123, { + code: 'STR', + errno: ValidationError.STR, + message: 'Not a string.', + path: [], + }); }); test('const string', () => { @@ -744,7 +977,12 @@ describe('single root element', () => { const type = s.bool; exec(type, true, null); exec(type, false, null); - exec(type, 123, {code: 'BOOL', errno: ValidationError.BOOL, message: 'Not a boolean.', path: []}); + exec(type, 123, { + code: 'BOOL', + errno: ValidationError.BOOL, + message: 'Not a boolean.', + path: [], + }); }); test('const boolean', () => { diff --git a/src/jtd/__tests__/converter.spec.ts b/src/jtd/__tests__/converter.spec.ts new file mode 100644 index 00000000..48f98c4e --- /dev/null +++ b/src/jtd/__tests__/converter.spec.ts @@ -0,0 +1,60 @@ +import {t} from '../../index'; + +describe('JTD converter', () => { + test('string type', () => { + const stringType = t.str; + const jtdForm = stringType.toJtdForm(); + expect(jtdForm).toEqual({type: 'string'}); + }); + + test('number type with format', () => { + const numberType = t.num.options({format: 'u8'}); + const jtdForm = numberType.toJtdForm(); + expect(jtdForm).toEqual({type: 'uint8'}); + }); + + test('boolean type', () => { + const boolType = t.bool; + const jtdForm = boolType.toJtdForm(); + expect(jtdForm).toEqual({type: 'boolean'}); + }); + + test('const type with string value', () => { + const constType = t.Const('hello'); + const jtdForm = constType.toJtdForm(); + expect(jtdForm).toEqual({type: 'string'}); + }); + + test('const type with number value', () => { + const constType = t.Const(255); + const jtdForm = constType.toJtdForm(); + expect(jtdForm).toEqual({type: 'uint8'}); + }); + + test('any type', () => { + const anyType = t.any; + const jtdForm = anyType.toJtdForm(); + expect(jtdForm).toEqual({nullable: true}); + }); + + test('array type', () => { + const arrayType = t.Array(t.str); + const jtdForm = arrayType.toJtdForm(); + expect(jtdForm).toEqual({ + elements: [{type: 'string'}], + }); + }); + + test('object type', () => { + const objectType = t.Object(t.prop('name', t.str), t.propOpt('age', t.num)); + const jtdForm = objectType.toJtdForm(); + expect(jtdForm).toEqual({ + properties: { + name: {type: 'string'}, + }, + optionalProperties: { + age: {type: 'float64'}, + }, + }); + }); +}); diff --git a/src/jtd/converter.ts b/src/jtd/converter.ts new file mode 100644 index 00000000..01eccaec --- /dev/null +++ b/src/jtd/converter.ts @@ -0,0 +1,128 @@ +import type * as jtd from './types'; +import type * as schema from '../schema'; + +const NUMS_TYPE_MAPPING = new Map([ + ['u8', 'uint8'], + ['u16', 'uint16'], + ['u32', 'uint32'], + ['i8', 'int8'], + ['i16', 'int16'], + ['i32', 'int32'], + ['f32', 'float32'], +]); + +/** + * Main router function that converts any Schema to JTD form. + * Uses a switch statement to route to the appropriate converter logic. + */ +export function toJtdForm(schema: schema.Schema): jtd.JtdForm { + const typeName = schema.kind; + + switch (typeName) { + case 'any': { + const form: jtd.JtdEmptyForm = {nullable: true}; + return form; + } + case 'bool': { + const form: jtd.JtdTypeForm = {type: 'boolean'}; + return form; + } + case 'const': { + const constSchema = schema as schema.ConstSchema; + const value = constSchema.value; + const valueType = typeof value; + switch (valueType) { + case 'boolean': + case 'string': + return {type: valueType}; + case 'number': { + if (value !== Math.round(value)) return {type: 'float64'}; + if (value >= 0) { + if (value <= 255) return {type: 'uint8'}; + if (value <= 65535) return {type: 'uint16'}; + if (value <= 4294967295) return {type: 'uint32'}; + } else { + if (value >= -128) return {type: 'int8'}; + if (value >= -32768) return {type: 'int16'}; + if (value >= -2147483648) return {type: 'int32'}; + } + return {type: 'float64'}; + } + } + const form: jtd.JtdEmptyForm = {nullable: false}; + return form; + } + case 'num': { + const numSchema = schema as schema.NumberSchema; + return { + type: (NUMS_TYPE_MAPPING.get(numSchema.format || '') ?? 'float64') as jtd.JtdType, + }; + } + case 'str': { + return {type: 'string'}; + } + case 'arr': { + const arraySchema = schema as schema.ArraySchema; + return { + elements: [toJtdForm(arraySchema.type)], + }; + } + case 'obj': { + const objSchema = schema as schema.ObjectSchema; + const form: jtd.JtdPropertiesForm = {}; + + if (objSchema.fields && objSchema.fields.length > 0) { + form.properties = {}; + form.optionalProperties = {}; + + for (const field of objSchema.fields) { + const fieldName = field.key; + const fieldType = field.type; + + if (fieldType) { + const fieldJtd = toJtdForm(fieldType); + // Check if field is optional + if (field.optional === true) { + form.optionalProperties[fieldName] = fieldJtd; + } else { + form.properties[fieldName] = fieldJtd; + } + } + } + } + + // Handle additional properties - check the schema for unknownFields + if (objSchema.unknownFields === false) { + form.additionalProperties = false; + } + + return form; + } + case 'tup': { + const tupleSchema = schema as schema.TupleSchema; + return { + elements: tupleSchema.types.map((element: any) => toJtdForm(element)), + }; + } + case 'map': { + const mapSchema = schema as schema.MapSchema; + return { + values: toJtdForm(mapSchema.type), + }; + } + case 'ref': { + const refSchema = schema as schema.RefSchema; + return { + ref: refSchema.ref, + }; + } + // case 'or': + // case 'bin': + // case 'fn': + // case 'fn$': + default: { + const form: jtd.JtdEmptyForm = {nullable: false}; + return form; + } + } +} diff --git a/src/jtd/index.ts b/src/jtd/index.ts new file mode 100644 index 00000000..254c495a --- /dev/null +++ b/src/jtd/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './converter'; diff --git a/src/schema/SchemaBuilder.ts b/src/schema/SchemaBuilder.ts index 3ed28075..eb443192 100644 --- a/src/schema/SchemaBuilder.ts +++ b/src/schema/SchemaBuilder.ts @@ -109,7 +109,11 @@ export class SchemaBuilder { c?: Omit>, 'id' | 'type'>, ): ArraySchema { if (typeof a === 'string') return this.Array(b as T, {id: a, ...(c || {})}); - return {kind: 'arr', ...(b as Omit>, 'id' | 'type'>), type: a as T}; + return { + kind: 'arr', + ...(b as Omit>, 'id' | 'type'>), + type: a as T, + }; } /** @@ -161,7 +165,10 @@ export class SchemaBuilder { ) return {kind: 'obj', ...(first as NoT>)}; if (args.length >= 1 && args[0] instanceof Array) - return this.Object({fields: args[0] as F, ...(args[1] as Optional>)}); + return this.Object({ + fields: args[0] as F, + ...(args[1] as Optional>), + }); return this.Object({fields: args as F}); } diff --git a/src/schema/schema.ts b/src/schema/schema.ts index f206373e..4288e381 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -363,7 +363,9 @@ export type TypeOfValue = T extends BooleanSchema ? (req$: Observable>, ctx?: unknown) => Observable> : never; -export type TypeOfMap> = {[K in keyof M]: TypeOf}; +export type TypeOfMap> = { + [K in keyof M]: TypeOf; +}; type TypeFields = TypeOfFieldMap}>>>; @@ -373,7 +375,9 @@ type ObjectFieldToTuple = F extends ObjectFieldSchema ? [K, type NoEmptyInterface = keyof I extends never ? Record : I; -type OptionalFields = {[K in keyof T]-?: T[K] extends ObjectOptionalFieldSchema ? K : never}[keyof T]; +type OptionalFields = { + [K in keyof T]-?: T[K] extends ObjectOptionalFieldSchema ? K : never; +}[keyof T]; type RequiredFields = Exclude>; diff --git a/src/system/TypeRouter.ts b/src/system/TypeRouter.ts index 4023ef49..a15bdc69 100644 --- a/src/system/TypeRouter.ts +++ b/src/system/TypeRouter.ts @@ -43,7 +43,10 @@ export class TypeRouter { } public extend(routes: (t: this) => NewRoutes): TypeRouter { - const router = new TypeRouter({system: this.system, routes: routes(this)}); + const router = new TypeRouter({ + system: this.system, + routes: routes(this), + }); return this.merge(router); } diff --git a/src/type/__tests__/TypeBuilder.spec.ts b/src/type/__tests__/TypeBuilder.spec.ts index bf538f51..70dbd589 100644 --- a/src/type/__tests__/TypeBuilder.spec.ts +++ b/src/type/__tests__/TypeBuilder.spec.ts @@ -191,7 +191,13 @@ describe('validateSchema()', () => { const type = t.import({ kind: 'obj', description: 'An object', - fields: [{kind: 'field', key: 'id', type: {kind: 'str', ascii: 'bytes'} as any}], + fields: [ + { + kind: 'field', + key: 'id', + type: {kind: 'str', ascii: 'bytes'} as any, + }, + ], }); expect(() => type.validateSchema()).toThrow(new Error('ASCII')); }); @@ -200,7 +206,14 @@ describe('validateSchema()', () => { const type = t.import({ kind: 'obj', description: 'An object', - fields: [{kind: 'field', key: 'id', optional: 123, type: {kind: 'str'}} as any], + fields: [ + { + kind: 'field', + key: 'id', + optional: 123, + type: {kind: 'str'}, + } as any, + ], }); expect(() => type.validateSchema()).toThrow(new Error('OPTIONAL_TYPE')); }); diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index d5f0657f..0d1b2543 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -1,5 +1,4 @@ import type * as schema from '../../schema'; -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; import type {Printable} from 'tree-dump/lib/types'; import { ValidatorCodegenContext, @@ -319,7 +318,8 @@ export abstract class AbstractType implements BaseType< } public toJtdForm(): jtd.JtdForm { - const form: jtd.JtdEmptyForm = {nullable: false}; - return form; + // Use dynamic import to avoid circular dependency + const converter = require('../../jtd/converter'); + return converter.toJtdForm(this.getSchema()); } } diff --git a/src/type/classes/AnyType.ts b/src/type/classes/AnyType.ts index 2cb29608..94927fa1 100644 --- a/src/type/classes/AnyType.ts +++ b/src/type/classes/AnyType.ts @@ -74,9 +74,4 @@ export class AnyType extends AbstractType { public toTypeScriptAst(): ts.TsType { return {node: 'AnyKeyword'}; } - - public toJtdForm(): jtd.JtdEmptyForm { - const form: jtd.JtdEmptyForm = {nullable: true}; - return form; - } } diff --git a/src/type/classes/BooleanType.ts b/src/type/classes/BooleanType.ts index 38ef7221..fa5e794c 100644 --- a/src/type/classes/BooleanType.ts +++ b/src/type/classes/BooleanType.ts @@ -59,9 +59,4 @@ export class BooleanType extends AbstractType { public toJson(value: unknown, system: TypeSystem | undefined = this.system) { return (value ? 'true' : 'false') as json_string; } - - public toJtdForm(): jtd.JtdTypeForm { - const form: jtd.JtdTypeForm = {type: 'boolean'}; - return form; - } } diff --git a/src/type/classes/ConstType.ts b/src/type/classes/ConstType.ts index e37a825c..1c5c64a6 100644 --- a/src/type/classes/ConstType.ts +++ b/src/type/classes/ConstType.ts @@ -82,11 +82,16 @@ export class ConstType extends AbstractType> { return node; } case 'number': { - const node: ts.TsNumericLiteral = {node: 'NumericLiteral', text: value.toString()}; + const node: ts.TsNumericLiteral = { + node: 'NumericLiteral', + text: value.toString(), + }; return node; } case 'boolean': { - const node: ts.TsTrueKeyword | ts.TsFalseKeyword = {node: value ? 'TrueKeyword' : 'FalseKeyword'}; + const node: ts.TsTrueKeyword | ts.TsFalseKeyword = { + node: value ? 'TrueKeyword' : 'FalseKeyword', + }; return node; } case 'object': { @@ -107,28 +112,4 @@ export class ConstType extends AbstractType> { public toString(tab: string = ''): string { return `${super.toString(tab)} → ${JSON.stringify(this.schema.value)}`; } - - public toJtdForm(): jtd.JtdForm { - const value = this.value(); - const type = typeof value; - switch (type) { - case 'boolean': - case 'string': - return {type}; - case 'number': { - if (value !== Math.round(value)) return {type: 'float64'}; - if (value >= 0) { - if (value <= 255) return {type: 'uint8'}; - if (value <= 65535) return {type: 'uint16'}; - if (value <= 4294967295) return {type: 'uint32'}; - } else { - if (value >= -128) return {type: 'int8'}; - if (value >= -32768) return {type: 'int16'}; - if (value >= -2147483648) return {type: 'int32'}; - } - return {type: 'float64'}; - } - } - return super.toJtdForm(); - } } diff --git a/src/type/classes/NumberType.ts b/src/type/classes/NumberType.ts index 75eee3c1..7dfc4d8f 100644 --- a/src/type/classes/NumberType.ts +++ b/src/type/classes/NumberType.ts @@ -121,25 +121,4 @@ export class NumberType extends AbstractType { public toJson(value: unknown, system: TypeSystem | undefined = this.system) { return ('' + value) as json_string; } - - public toJtdForm(): jtd.JtdTypeForm { - switch (this.schema.format) { - case 'u8': - return {type: 'uint8'}; - case 'u16': - return {type: 'uint16'}; - case 'u32': - return {type: 'uint32'}; - case 'i8': - return {type: 'int8'}; - case 'i16': - return {type: 'int16'}; - case 'i32': - return {type: 'int32'}; - case 'f32': - return {type: 'float32'}; - default: - return {type: 'float64'}; - } - } } diff --git a/src/type/classes/RefType.ts b/src/type/classes/RefType.ts index 3831757e..0920e02e 100644 --- a/src/type/classes/RefType.ts +++ b/src/type/classes/RefType.ts @@ -47,7 +47,10 @@ export class RefType extends AbstractType { public toJson(value: unknown, system: TypeSystem | undefined = this.system): json_string { return >(this.schema.noJsonEscape ? '"' + value + '"' : asString(value as string)); } - - public toJtdForm(): jtd.JtdTypeForm { - return {type: 'string'}; - } } diff --git a/src/type/types.ts b/src/type/types.ts index 7e265902..830b55aa 100644 --- a/src/type/types.ts +++ b/src/type/types.ts @@ -24,7 +24,9 @@ export type Type = | classes.FunctionStreamingType; export type SchemaOf = T extends BaseType ? U : never; -export type SchemaOfMap> = {[K in keyof M]: SchemaOf}; +export type SchemaOfMap> = { + [K in keyof M]: SchemaOf; +}; export type SchemaOfObjectFieldType = F extends classes.ObjectOptionalFieldType ? schema.ObjectOptionalFieldSchema> @@ -32,7 +34,9 @@ export type SchemaOfObjectFieldType = F extends classes.ObjectOptionalFieldTy ? schema.ObjectFieldSchema> : never; -export type SchemaOfObjectFields = {[K in keyof F]: SchemaOfObjectFieldType}; +export type SchemaOfObjectFields = { + [K in keyof F]: SchemaOfObjectFieldType; +}; export type TypeMap = {[name: string]: schema.Schema}; diff --git a/src/value/ObjectValue.ts b/src/value/ObjectValue.ts index 8e6b2171..ebc3b1ee 100644 --- a/src/value/ObjectValue.ts +++ b/src/value/ObjectValue.ts @@ -13,7 +13,9 @@ export type UnObjectValue = T extends ObjectValue ? U : never; export type UnObjectFieldTypeVal = T extends classes.ObjectFieldType ? U : never; export type ObjectFieldToTuple = F extends classes.ObjectFieldType ? [K, V] : never; export type ToObject = T extends [string, unknown][] ? {[K in T[number] as K[0]]: K[1]} : never; -export type ObjectValueToTypeMap = ToObject<{[K in keyof F]: ObjectFieldToTuple}>; +export type ObjectValueToTypeMap = ToObject<{ + [K in keyof F]: ObjectFieldToTuple; +}>; export type TuplesToFields = T extends PropDefinition[] ? classes.ObjectFieldType[] : never; // export type MergeObjectsTypes =