// Copyright (c) 2019 Shellyl_N and Authors // license: ISC // https://github.com/shellyln import { TypeAssertion, TypeAssertionMap } from '../../types'; import * as JsonSchema from '../../types/json-schema-types'; function addMetaInfo(a: JsonSchema.JsonSchemaAssertion, ty: TypeAssertion) { const a2 = {...a}; let changed = false; if (ty.docComment) { a2.description = ty.docComment; changed = true; } switch (ty.kind) { case 'repeated': if (typeof ty.min === 'number') { (a2 as JsonSchema.JsonSchemaArrayAssertion).minItems = ty.min; changed = true; } if (typeof ty.max === 'number') { (a2 as JsonSchema.JsonSchemaArrayAssertion).maxItems = ty.max; changed = true; } break; case 'primitive': if (typeof ty.minValue === 'number') { (a2 as JsonSchema.JsonSchemaNumberAssertion).minimum = ty.minValue; changed = true; } if (typeof ty.maxValue === 'number') { (a2 as JsonSchema.JsonSchemaNumberAssertion).maximum = ty.maxValue; changed = true; } if (typeof ty.greaterThanValue === 'number') { (a2 as JsonSchema.JsonSchemaNumberAssertion).exclusiveMinimum = ty.greaterThanValue; changed = true; } if (typeof ty.lessThanValue === 'number') { (a2 as JsonSchema.JsonSchemaNumberAssertion).exclusiveMaximum = ty.lessThanValue; changed = true; } if (typeof ty.minLength === 'number') { (a2 as JsonSchema.JsonSchemaStringAssertion).minLength = ty.minLength; changed = true; } if (typeof ty.maxLength === 'number') { (a2 as JsonSchema.JsonSchemaStringAssertion).maxLength = ty.maxLength; changed = true; } if (ty.pattern) { (a2 as JsonSchema.JsonSchemaStringAssertion).pattern = ty.pattern.source; changed = true; } break; } return (changed ? a2 : a); } function generateJsonSchemaInner(schema: TypeAssertionMap, ty: TypeAssertion, nestLevel: number): JsonSchema.JsonSchemaAssertion { if (0 < nestLevel && ty.typeName) { const ret: JsonSchema.JsonSchemaRefAssertion = { $ref: `#/definitions/${ty.typeName.replace(/\./g, '/properties/')}`, }; const r2 = addMetaInfo(ret, ty); if (ret !== r2) { // NOTE: `$ref` cannot have value constraints. return generateJsonSchemaInner(schema, ty, 0); } else { return ret; } } switch (ty.kind) { case 'symlink': { const ret: JsonSchema.JsonSchemaRefAssertion = { $ref: `#/definitions/${ty.symlinkTargetName}`, }; const r2 = addMetaInfo(ret, ty); if (ret !== r2) { // NOTE: `$ref` cannot have value constraints. const t2 = schema.get(ty.symlinkTargetName)?.ty; if (t2) { return generateJsonSchemaInner(schema, t2, 0); } else { // Drop constraints. return ret; } } else { return ret; } } case 'repeated': { const ret: JsonSchema.JsonSchemaArrayAssertion = { type: 'array', items: generateJsonSchemaInner(schema, ty.repeated, nestLevel + 1), }; if (typeof ty.min === 'number') { ret.minItems = ty.min; } if (typeof ty.max === 'number') { ret.maxItems = ty.max; } return addMetaInfo(ret, ty); } case 'sequence': { const ret: JsonSchema.JsonSchemaArrayAssertion = { type: 'array', items: { anyOf: ty.sequence.map(x => generateJsonSchemaInner(schema, x, nestLevel + 1)) }, }; return addMetaInfo(ret, ty); } case 'spread': { return generateJsonSchemaInner(schema, ty.spread, nestLevel + 1); } case 'one-of': { const ret: JsonSchema.JsonSchemaAnyOfAssertion = { anyOf: ty.oneOf.map(x => generateJsonSchemaInner(schema, x, nestLevel + 1)), }; return addMetaInfo(ret, ty); } case 'optional': { const ret: JsonSchema.JsonSchemaOneOfAssertion = { oneOf: [ generateJsonSchemaInner(schema, ty.optional, nestLevel + 1), {type: 'null'}, ], }; return addMetaInfo(ret, ty); } case 'enum': { const ret: JsonSchema.JsonSchemaTsEnumAssertion = { type: ['string', 'number'], enum: ty.values.map(x => x[1]), }; return addMetaInfo(ret, ty); } case 'object': { const properties: JsonSchema.JsonSchemaObjectPropertyAssertion = {}; const patternProperties: JsonSchema.JsonSchemaObjectPropertyAssertion = {}; let patternPropsCount = 0; const required: string[] = []; for (const m of ty.members) { const z = generateJsonSchemaInner(schema, m[1].kind === 'optional' ? m[1].optional : m[1], nestLevel + 1); if (m[3]) { z.description = m[3]; } else { delete z.description; } properties[m[0]] = z; if (m[1].kind !== 'optional') { required.push(m[0]); } } for (const m of ty.additionalProps || []) { const z = generateJsonSchemaInner(schema, m[1], nestLevel + 1); if (m[3]) { z.description = m[3]; } else { delete z.description; } for (const k of m[0]) { patternPropsCount++; switch (k) { case 'number': patternProperties['^[0-9]+$'] = z; break; case 'string': patternProperties['^.*$'] = z; break; default: patternProperties[k.source] = z; break; } } } const ret: JsonSchema.JsonSchemaObjectAssertion = { type: 'object', properties, ...(0 < patternPropsCount ? {patternProperties} : {}), ...(0 < required.length ? {required} : {}), additionalProperties: false, }; return addMetaInfo(ret, ty); } case 'primitive': { switch (ty.primitiveName) { case 'null': case 'undefined': { const ret: JsonSchema.JsonSchemaNullAssertion = { type: 'null', }; return addMetaInfo(ret, ty); } case 'number': { const ret: JsonSchema.JsonSchemaNumberAssertion = { type: 'number', }; return addMetaInfo(ret, ty); } case 'bigint': { const ret: JsonSchema.JsonSchemaBigIntAssertion = { type: ['integer', 'string'], }; return addMetaInfo(ret, ty); } case 'integer': { const ret: JsonSchema.JsonSchemaNumberAssertion = { type: 'integer', }; return addMetaInfo(ret, ty); } case 'string': { const ret: JsonSchema.JsonSchemaStringAssertion = { type: 'string', }; return addMetaInfo(ret, ty); } case 'boolean': { const ret: JsonSchema.JsonSchemaBooleanAssertion = { type: 'boolean', }; return addMetaInfo(ret, ty); } } // TODO: Function, DateStr, DateTimeStr } case 'primitive-value': { switch (typeof ty.value) { case 'number': { const ret: JsonSchema.JsonSchemaNumberValueAssertion = { type: 'number', enum: [ty.value], }; return addMetaInfo(ret, ty); } case 'bigint': { const ret: JsonSchema.JsonSchemaBigIntNumberValueAssertion = { type: ['integer', 'string'], enum: [ty.value.toString()], }; if (BigInt(Number.MIN_SAFE_INTEGER) <= ty.value && ty.value <= BigInt(Number.MAX_SAFE_INTEGER)) { ret.enum.push(Number(ty.value)); } return addMetaInfo(ret, ty); } case 'string': { const ret: JsonSchema.JsonSchemaStringValueAssertion = { type: 'string', enum: [ty.value], }; return addMetaInfo(ret, ty); } case 'boolean': { const ret: JsonSchema.JsonSchemaBooleanValueAssertion = { type: 'boolean', enum: [ty.value], }; return addMetaInfo(ret, ty); } default: throw new Error(`Unknown primitive-value assertion: ${typeof ty.value}`); } } case 'never': { const ret: JsonSchema.JsonSchemaNullAssertion = { type: 'null', }; return addMetaInfo(ret, ty); } case 'any': case 'unknown': { const ret: JsonSchema.JsonSchemaAnyAssertion = { type: ['null', 'integer', 'number', 'string', 'boolean', 'array', 'object'], }; return addMetaInfo(ret, ty); } case 'operator': throw new Error(`Unexpected type assertion: ${(ty as any).kind}`); default: throw new Error(`Unknown type assertion: ${(ty as any).kind}`); } } export function generateJsonSchemaObject(schema: TypeAssertionMap) { const ret: JsonSchema.JsonSchemaRootAssertion = { $schema: 'http://json-schema.org/draft-06/schema#', definitions: {}, }; for (const ty of schema.entries()) { if (ty[1].ty.kind === 'never' && ty[1].ty.passThruCodeBlock) { continue; } (ret.definitions as object)[ty[0]] = generateJsonSchemaInner(schema, ty[1].ty, 0); } return ret; } export function generateJsonSchema(schema: TypeAssertionMap, asTs?: boolean): string { const ret = generateJsonSchemaObject(schema); if (asTs) { return ( `\n// tslint:disable: object-literal-key-quotes\n` + `const schema = ${JSON.stringify(ret, null, 2)};\nexport default schema;` + `\n// tslint:enable: object-literal-key-quotes\n` ); } else { return JSON.stringify(ret, null, 2); } }