99 */
1010export function generateTypeScript ( schema ) {
1111 /** @type {[string, string][] } */
12- let imports = [ ]
12+ const imports = [ ]
1313
1414 let typesrc = ''
1515 for ( const [ typeName , typeDefn ] of Object . entries ( schema . types ) ) {
16+ if ( Object . keys ( typeDefn ) . length !== 1 ) {
17+ throw new Error ( 'Unexpected type definition: ' + JSON . stringify ( typeDefn ) )
18+ }
19+ const typeKind = Object . keys ( typeDefn ) [ 0 ]
1620 if ( 'struct' in typeDefn ) {
1721 typesrc += `export type ${ typeName } = {\n`
1822
23+ /** @type {string[] } */
24+ const fieldValidators = [ ]
25+ let requiredFieldCount = 0
1926 for ( let [ fieldName , fieldDefn ] of Object . entries ( typeDefn . struct . fields ) ) {
27+ if ( ! fieldDefn . optional && ! fieldDefn . optional ) {
28+ requiredFieldCount ++
29+ }
2030 /** @type { { [k in string]: string }[] } */
2131 let annotations = [ ]
2232 if ( typeof typeDefn . struct . annotations === 'object' && typeof typeDefn . struct . annotations . type === 'object' ) {
@@ -60,17 +70,97 @@ export function generateTypeScript (schema) {
6070 }
6171 fieldType = fixTypeScriptType ( imports , fieldType , slice )
6272 fieldName = fixTypeScriptName ( annotations , fieldName )
63- typesrc += ` ${ fieldName } : ${ fieldType } ${ linecomment } \n`
73+ typesrc += ` ${ fieldName } ${ fieldDefn . optional ? '?' : '' } : ${ fieldType } ${ linecomment } \n`
74+ const inCheck = ! fieldDefn . optional && ! fieldDefn . optional ? `'${ fieldName } ' in value &&` : `!('${ fieldName } ' in value) ||`
75+ if ( fieldType . endsWith ( '[]' ) ) {
76+ let elementType = getTypeScriptType ( [ ] , fieldType . slice ( 0 , - 2 ) )
77+ elementType = fixTypeScriptType ( imports , elementType , false )
78+ fieldValidators . push ( ` (${ inCheck } (${ fieldDefn . nullable ? `value.${ fieldName } === null || ` : '' } (Array.isArray(value.${ fieldName } ) && value.${ fieldName } .every(${ elementType } .is${ elementType } ))))` )
79+ } else {
80+ fieldValidators . push ( ` (${ inCheck } (${ fieldDefn . nullable ? `value.${ fieldName } === null || ` : '' } (${ fieldType } .is${ fieldType } (value.${ fieldName } ))))` )
81+ }
6482 }
6583
6684 typesrc += '}\n\n'
85+
86+ const kind = fixTypeScriptType ( imports , '@ipld/schema/schema-schema.js#KindMap' , false )
87+ typesrc += `export namespace ${ typeName } {\n`
88+ typesrc += ` export function is${ typeName } (value: any): value is ${ typeName } {\n`
89+ typesrc += ` if (!${ kind } .is${ kind } (value)) {\n`
90+ typesrc += ' return false\n'
91+ typesrc += ' }\n'
92+ typesrc += ' const keyCount = Object.keys(value).length\n'
93+ typesrc += ' return '
94+ if ( requiredFieldCount === Object . keys ( typeDefn . struct . fields ) . length ) {
95+ typesrc += `keyCount === ${ requiredFieldCount } &&\n`
96+ } else {
97+ // TODO: this isn't really a complete check, we probably should check for extra fields
98+ typesrc += `keyCount >= ${ requiredFieldCount } && keyCount <= ${ Object . keys ( typeDefn . struct . fields ) . length } &&\n`
99+ }
100+ typesrc += fieldValidators . join ( ' &&\n' )
101+ typesrc += '\n }\n'
102+ typesrc += '}\n\n'
103+ } else if ( 'list' in typeDefn ) {
104+ if ( typeof typeDefn . list . valueType !== 'string' ) {
105+ throw new Error ( 'Unhandled list value type: ' + JSON . stringify ( typeDefn ) )
106+ }
107+ let valueType = getTypeScriptType ( [ ] , typeDefn . list . valueType )
108+ valueType = fixTypeScriptType ( imports , valueType , false )
109+ typesrc += `export type ${ typeName } = ${ valueType } []\n\n`
110+ typesrc += `export namespace ${ typeName } {\n`
111+ typesrc += ` export function is${ typeName } (value: any): value is ${ typeName } {\n`
112+ typesrc += ` return Array.isArray(value) && value.every(${ valueType } .is${ valueType } )\n`
113+ typesrc += ' }\n'
114+ typesrc += '}\n\n'
115+ } else if ( 'copy' in typeDefn ) {
116+ const { fromType } = typeDefn . copy
117+ typesrc += `export type ${ typeName } = ${ fromType } \n\n`
118+ typesrc += `export namespace ${ typeName } {\n`
119+ typesrc += ` export function is${ typeName } (value: any): value is ${ typeName } {\n`
120+ typesrc += ` return ${ fromType } .is${ fromType } (value)\n`
121+ typesrc += ' }\n'
122+ typesrc += '}\n\n'
123+ } else if ( [ 'bool' , 'string' , 'bytes' , 'int' , 'float' , 'link' , 'null' ] . includes ( typeKind ) ) {
124+ const kind = fixTypeScriptType ( imports , `@ipld/schema/schema-schema.js#Kind${ typeKind . charAt ( 0 ) . toUpperCase ( ) } ${ typeKind . slice ( 1 ) } ` , false )
125+ typesrc += `export type ${ typeName } = ${ kind } \n\n`
126+ typesrc += `export namespace ${ typeName } {\n`
127+ typesrc += ` export function is${ typeName } (value: any): value is ${ typeName } {\n`
128+ typesrc += ` return ${ kind } .is${ kind } (value)\n`
129+ typesrc += ' }\n'
130+ typesrc += '}\n\n'
131+ } else if ( 'union' in typeDefn ) {
132+ if ( ! ( 'kinded' in typeDefn . union . representation ) ) {
133+ throw new Error ( 'Unhandled union representation: ' + Object . keys ( typeDefn . union . representation ) [ 0 ] )
134+ }
135+ if ( typeDefn . union . members . some ( ( member ) => typeof member !== 'string' ) ) {
136+ throw new Error ( 'Unhandled union member type(s): ' + JSON . stringify ( typeDefn . union . members ) )
137+ }
138+ const kinds = typeDefn . union . members . map ( ( member ) => {
139+ return fixTypeScriptType ( imports , getTypeScriptType ( [ ] , String ( member ) ) , false )
140+ } )
141+ typesrc += `export type ${ typeName } = ${ kinds . join ( ' | ' ) } \n\n`
142+ typesrc += `export namespace ${ typeName } {\n`
143+ typesrc += ` export function is${ typeName } (value: any): value is ${ typeName } {\n`
144+ typesrc += ` return ${ kinds . map ( ( kind ) => `${ kind } .is${ kind } (value)` ) . join ( ' || ' ) } \n`
145+ typesrc += ' }\n'
146+ typesrc += '}\n\n'
147+ } else {
148+ throw new Error ( 'Unimplemented type kind: ' + typeKind )
67149 }
68150 }
69151
70152 let ts = ''
71- imports = fixTypeScriptImports ( imports )
72- for ( const imp of imports ) {
73- ts += `import { ${ imp [ 1 ] } } from '${ imp [ 0 ] } '\n`
153+ const fixedImports = fixTypeScriptImports ( imports )
154+ for ( const imp of fixedImports ) {
155+ if ( imp [ 1 ] . length === 1 ) {
156+ ts += `import { ${ imp [ 1 ] } } from '${ imp [ 0 ] } '\n`
157+ } else {
158+ ts += 'import {\n'
159+ for ( const imported of imp [ 1 ] ) {
160+ ts += ` ${ imported } ,\n`
161+ }
162+ ts += `} from '${ imp [ 0 ] } '\n`
163+ }
74164 }
75165 if ( imports . length > 0 ) {
76166 ts += '\n'
@@ -93,8 +183,7 @@ function fixTypeScriptName (annotations, fieldName) {
93183 }
94184 }
95185 }
96- // snakeCase with lower-case first letter
97- return fieldName . charAt ( 0 ) . toLowerCase ( ) + fieldName . slice ( 1 )
186+ return fieldName
98187}
99188
100189/**
@@ -112,26 +201,21 @@ function getTypeScriptType (annotations, ipldType) {
112201 }
113202 }
114203 switch ( ipldType ) {
115- case 'Int' :
116- return 'number'
117- case 'Float' :
118- return 'number'
119204 case 'Bool' :
120- return 'boolean'
121205 case 'String' :
122- return 'string'
123206 case 'Bytes' :
124- return 'Uint8Array'
125- case 'Link ' :
126- return 'multiformats/cid#CID'
207+ case 'Int' :
208+ case 'Float ' :
209+ case 'Null' :
127210 case 'Map' :
128- return 'object'
129211 case 'List' :
130- return 'any[]'
212+ case 'Link' :
131213 case 'Union' :
132- return 'any' // TODO:
214+ case 'Struct' :
215+ case 'Enum' :
216+ return `@ipld/schema/schema-schema.js#Kind${ ipldType } `
133217 case 'Any' :
134- return 'any' // TODO:
218+ return 'any' // TODO: something here?
135219 }
136220
137221 return ipldType
@@ -160,9 +244,25 @@ function fixTypeScriptType (imports, tstype, slice) {
160244
161245/**
162246 * @param {[string, string][] } imports
163- * @returns {[string, string][] }
247+ * @returns {[string, string[] ][] }
164248 */
165249function fixTypeScriptImports ( imports ) {
166- // TODO: implement for user imports
167- return imports
250+ /** @type {Record<string, string[]> } */
251+ const groupedImports = { }
252+ for ( const [ source , imported ] of imports ) {
253+ if ( ! groupedImports [ source ] ) {
254+ groupedImports [ source ] = [ ]
255+ }
256+ if ( ! groupedImports [ source ] . includes ( imported ) ) {
257+ groupedImports [ source ] . push ( imported )
258+ }
259+ }
260+
261+ /** @type {[string, string[]][] } */
262+ const result = [ ]
263+ for ( const source in groupedImports ) {
264+ result . push ( [ source , groupedImports [ source ] . sort ( ) ] )
265+ }
266+
267+ return result
168268}
0 commit comments