import { TypeAssertion , TypeAssertionMap } from '../../types' ; import * as JsonSchema from '../../types/json-schema-types' ; function addMetaInfo ( a , ty ) { 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 , ty , nestLevel ) { if ( 0 < nestLevel && ty . typeName ) { const ret = { $ref : `#/definitions/${ ty . typeName . replace ( /\./g , '/properties/' ) }` , } ; const r2 = addMetaInfo ( ret , ty ) ; if ( ret !== r2 ) { return generateJsonSchemaInner ( schema , ty , 0 ) ; } else { return ret ; } } switch ( ty . kind ) { case 'symlink' : { const ret = { $ref : `#/definitions/${ ty . symlinkTargetName }` , } ; const r2 = addMetaInfo ( ret , ty ) ; if ( ret !== r2 ) { const t2 = schema . get ( ty . symlinkTargetName ) ?. ty ; if ( t2 ) { return generateJsonSchemaInner ( schema , t2 , 0 ) ; } else { return ret ; } } else { return ret ; } } case 'repeated' : { const ret = { 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 = { 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 = { anyOf : ty . oneOf . map ( x => generateJsonSchemaInner ( schema , x , nestLevel + 1 ) ) , } ; return addMetaInfo ( ret , ty ) ; } case 'optional' : { const ret = { oneOf : [ generateJsonSchemaInner ( schema , ty . optional , nestLevel + 1 ) , { type : 'null' } , ] , } ; return addMetaInfo ( ret , ty ) ; } case 'enum' : { const ret = { type : [ 'string' , 'number' ] , enum : ty . values . map ( x => x [ 1 ] ) , } ; return addMetaInfo ( ret , ty ) ; } case 'object' : { const properties = { } ; const patternProperties = { } ; let patternPropsCount = 0 ; const required = [ ] ; 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 = { 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 = { type : 'null' , } ; return addMetaInfo ( ret , ty ) ; } case 'number' : { const ret = { type : 'number' , } ; return addMetaInfo ( ret , ty ) ; } case 'bigint' : { const ret = { type : [ 'integer' , 'string' ] , } ; return addMetaInfo ( ret , ty ) ; } case 'integer' : { const ret = { type : 'integer' , } ; return addMetaInfo ( ret , ty ) ; } case 'string' : { const ret = { type : 'string' , } ; return addMetaInfo ( ret , ty ) ; } case 'boolean' : { const ret = { type : 'boolean' , } ; return addMetaInfo ( ret , ty ) ; } } } case 'primitive-value' : { switch ( typeof ty . value ) { case 'number' : { const ret = { type : 'number' , enum : [ ty . value ] , } ; return addMetaInfo ( ret , ty ) ; } case 'bigint' : { const ret = { 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 = { type : 'string' , enum : [ ty . value ] , } ; return addMetaInfo ( ret , ty ) ; } case 'boolean' : { const ret = { type : 'boolean' , enum : [ ty . value ] , } ; return addMetaInfo ( ret , ty ) ; } default : throw new Error ( `Unknown primitive-value assertion: ${ typeof ty . value }` ) ; } } case 'never' : { const ret = { type : 'null' , } ; return addMetaInfo ( ret , ty ) ; } case 'any' : case 'unknown' : { const ret = { 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 ) { const ret = { $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 , asTs ? ) { 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 ) ; } }