// Copyright (c) 2019 Shellyl_N and Authors // license: ISC // https://github.com/shellyln import { TypeAssertion, ObjectAssertion, SerializedSchemaInfo, TypeAssertionSetValue, TypeAssertionMap } from './types'; import { escapeString } from './lib/escape'; import { resolveSchema } from './lib/resolver'; export const TynderSchemaVersion = 'tynder/1.0'; function hasMetaInfo(ty: TypeAssertion) { let hasInfo = false; if (ty.messages) { hasInfo = true; } if (ty.message) { hasInfo = true; } if (ty.messageId) { hasInfo = true; } switch (ty.kind) { case 'repeated': if (typeof ty.min === 'number') { hasInfo = true; } if (typeof ty.max === 'number') { hasInfo = true; } break; case 'primitive': if (typeof ty.minValue === 'number') { hasInfo = true; } if (typeof ty.maxValue === 'number') { hasInfo = true; } if (typeof ty.greaterThanValue === 'number') { hasInfo = true; } if (typeof ty.lessThanValue === 'number') { hasInfo = true; } if (typeof ty.minLength === 'number') { hasInfo = true; } if (typeof ty.maxLength === 'number') { hasInfo = true; } if (ty.pattern) { hasInfo = true; } break; } return hasInfo; } function serializeInner(ty: TypeAssertion, nestLevel: number): TypeAssertion { if (0 < nestLevel && ty.typeName && !hasMetaInfo(ty)) { switch (ty.kind) { case 'optional': // nothing to do. break; default: return ({ ...{ kind: 'symlink', symlinkTargetName: ty.typeName as string, // NOTE: type inference failed if the switch statement is exists. typeName: ty.typeName, }, ...(ty.name ? {name: ty.name} : {}), ...(ty.docComment ? {docComment: ty.docComment} : {}), }); } } const ret: TypeAssertion = {...ty}; switch (ret.kind) { case 'never': case 'any': case 'unknown': case 'symlink': case 'operator': break; case 'primitive-value': if (typeof ret.value === 'bigint') { ret.value = String(ret.value); ret.primitiveName = 'bigint'; } break; case 'primitive': if (ret.pattern) { ret.pattern = `/${ret.pattern.source}/${ret.pattern.flags}` as any; } break; case 'repeated': ret.repeated = serializeInner(ret.repeated, nestLevel + 1); break; case 'spread': ret.spread = serializeInner(ret.spread, nestLevel + 1); break; case 'sequence': ret.sequence = ret.sequence.map(x => serializeInner(x, nestLevel + 1)); break; case 'one-of': ret.oneOf = ret.oneOf.map(x => serializeInner(x, nestLevel + 1)); break; case 'optional': ret.optional = serializeInner(ret.optional, nestLevel + 1); break; case 'enum': ret.values = ret.values.slice().map(x => x[2] === null || x[2] === void 0 ? x.slice(0, 2) : x) as any; break; case 'object': ret.members = ret.members .map(x => [x[0], serializeInner(x[1], nestLevel + 1), ...x.slice(2)]) as any; if (ret.additionalProps) { ret.additionalProps = ret.additionalProps .map(x => [x[0].map( p => typeof p === 'string' ? p : `/${p.source}/${p.flags}`), serializeInner(x[1], nestLevel + 1), ...x.slice(2)]) as any; } if (ret.baseTypes) { // NOTE: convert 'baseTypes' to 'symlink'. ret.baseTypes = ret.baseTypes.map(x => serializeInner(x, nestLevel + 1)) as ObjectAssertion[]; } break; default: throw new Error(`Unknown type assertion: ${(ret as any).kind}`); } return ret; } export function serializeToObject(schema: TypeAssertionMap): SerializedSchemaInfo { const ret: SerializedSchemaInfo = { version: TynderSchemaVersion, ns: {}, }; const current = {}; for (const ty of schema.entries()) { current[ty[0]] = serializeInner(ty[1].ty, 0); } ret.ns['.'] = current; return ret; } export function serialize(schema: TypeAssertionMap, asTs?: boolean): string { const ret = serializeToObject(schema); if (asTs) { return ( `\n// tslint:disable: object-literal-key-quotes\n` + `const schema = ${JSON.stringify(ret, null, 2)};\nexport default schema;\n\n` + `export const enum Schema {\n${Object.keys(ret.ns['.']).filter(x => { return (! (/^[0-9]/.test(x) || /[\u0000-\u001f\u007f]/.test(x) || /\s/.test(x) || /[@#$%^&+-=:;.,?!'"`/|{}()<>[\]\*\\]/.test(x)) ); }).map(x => ` ${x} = '${x}',\n`).join('')}` + `}\n// tslint:enable: object-literal-key-quotes\n` ); } else { return JSON.stringify(ret, null, 2); } } function deserializeRegExp(pat: string, errMsg: string) { const m = (/^\/(.*)\/([gimsuy]*)$/s).exec(pat); if (m) { return new RegExp(m[1], m[2]); } else { throw new Error(errMsg); } } function deserializeInner(ty: TypeAssertion) { const ret: TypeAssertion = {...ty}; switch (ret.kind) { case 'never': case 'any': case 'unknown': case 'enum': case 'symlink': case 'operator': // NOTE: 'symlink' and 'operator' will resolved by calling 'resolveSymbols()' in 'deserialize()'. break; case 'primitive-value': if (ret.primitiveName === 'bigint') { delete ret.primitiveName; ret.value = BigInt(ret.value); } break; case 'primitive': if (ret.pattern) { ret.pattern = deserializeRegExp( ret.pattern as any, `Unknown pattern match assertion: ${ret.pattern as any}`); } break; case 'repeated': ret.repeated = deserializeInner(ret.repeated); break; case 'spread': ret.spread = deserializeInner(ret.spread); break; case 'sequence': ret.sequence = ret.sequence.map(x => deserializeInner(x)); break; case 'one-of': ret.oneOf = ret.oneOf.map(x => deserializeInner(x)); break; case 'optional': ret.optional = deserializeInner(ret.optional); break; case 'object': ret.members = ret.members .map(x => [x[0], deserializeInner(x[1]), ...x.slice(2)]) as any; if (ret.additionalProps) { ret.additionalProps = ret.additionalProps .map(x => [x[0].map( p => String(p).startsWith('/') ? deserializeRegExp(p as any, `Unknown additional props: ${p}`) : p), deserializeInner(x[1]), ...x.slice(2)]) as any; } // NOTE: keep 'baseTypes' as 'symlink'. break; default: throw new Error(`Unknown type assertion: ${(ret as any).kind}`); } return ret; } export function deserializeFromObject(obj: any) { if (obj.version !== TynderSchemaVersion) { throw new Error(`Unknown schema version: ${obj.version}`); } const schema: TypeAssertionMap = new Map(); const current = obj.ns['.']; for (const k in current) { if (! Object.prototype.hasOwnProperty.call(current, k)) { continue; } schema.set(k, { ty: deserializeInner(current[k]), exported: false, isDeclare: false, resolved: false, }); } return resolveSchema(schema, {isDeserialization: true}); } export function deserialize(text: string) { const parsed = JSON.parse(text); return deserializeFromObject(parsed); }