@@ -72,9 +72,17 @@ const toCamelCase = (value, upper = false) => {
7272 . filter ( Boolean )
7373 . map ( ( token ) => token . toLowerCase ( ) ) ;
7474 if ( tokens . length === 0 ) return value ;
75- const [ first , ...rest ] = tokens ;
76- const firstToken = upper ? first . charAt ( 0 ) . toUpperCase ( ) + first . slice ( 1 ) : first ;
77- return [ firstToken , ...rest . map ( ( token ) => token . charAt ( 0 ) . toUpperCase ( ) + token . slice ( 1 ) ) ] . join ( '' ) ;
75+ const normalized = tokens . map ( ( token ) => ( token === 'ios' ? 'IOS' : token ) ) ;
76+ const [ first , ...rest ] = normalized ;
77+ const formatFirst = ( ) => {
78+ if ( first === 'IOS' ) {
79+ return upper ? 'IOS' : 'ios' ;
80+ }
81+ return upper ? first . charAt ( 0 ) . toUpperCase ( ) + first . slice ( 1 ) : first ;
82+ } ;
83+ const firstToken = formatFirst ( ) ;
84+ const restTokens = rest . map ( ( token ) => ( token === 'IOS' ? 'IOS' : token . charAt ( 0 ) . toUpperCase ( ) + token . slice ( 1 ) ) ) ;
85+ return [ firstToken , ...restTokens ] . join ( '' ) ;
7886} ;
7987
8088const toPascalCase = ( value ) => toCamelCase ( value , true ) ;
@@ -93,20 +101,6 @@ const scalarMap = new Map([
93101 [ 'Float' , 'double' ] ,
94102] ) ;
95103
96- const getDartType = ( graphqlType ) => {
97- if ( graphqlType instanceof GraphQLNonNull ) {
98- const inner = getDartType ( graphqlType . ofType ) ;
99- return { type : inner . type , nullable : false } ;
100- }
101- if ( graphqlType instanceof GraphQLList ) {
102- const inner = getDartType ( graphqlType . ofType ) ;
103- const element = inner . type + ( inner . nullable ? '?' : '' ) ;
104- return { type : `List<${ element } >` , nullable : true } ;
105- }
106- const mapped = scalarMap . get ( graphqlType . name ) ?? graphqlType . name ;
107- return { type : mapped , nullable : true } ;
108- } ;
109-
110104const addDocComment = ( lines , description , indent = '' ) => {
111105 if ( ! description ) return ;
112106 for ( const docLine of description . split ( / \r ? \n / ) ) {
@@ -154,11 +148,128 @@ for (const name of typeNames) {
154148 objects . push ( type ) ;
155149 continue ;
156150 }
157- if ( isInputObjectType ( type ) ) {
151+ if ( isInputObjectType ( type ) ) {
158152 inputs . push ( type ) ;
159153 }
160154}
161155
156+ const enumNames = new Set ( enums . map ( ( value ) => value . name ) ) ;
157+ const interfaceNames = new Set ( interfaces . map ( ( value ) => value . name ) ) ;
158+ const objectNames = new Set ( objects . map ( ( value ) => value . name ) ) ;
159+ const inputNames = new Set ( inputs . map ( ( value ) => value . name ) ) ;
160+ const unionNames = new Set ( unions . map ( ( value ) => value . name ) ) ;
161+
162+ const getTypeMetadata = ( graphqlType ) => {
163+ if ( graphqlType instanceof GraphQLNonNull ) {
164+ const inner = getTypeMetadata ( graphqlType . ofType ) ;
165+ return { ...inner , nullable : false } ;
166+ }
167+ if ( graphqlType instanceof GraphQLList ) {
168+ const inner = getTypeMetadata ( graphqlType . ofType ) ;
169+ const innerType = inner . dartType + ( inner . nullable ? '?' : '' ) ;
170+ return {
171+ kind : 'list' ,
172+ nullable : true ,
173+ elementType : inner ,
174+ dartType : `List<${ innerType } >` ,
175+ } ;
176+ }
177+ const typeName = graphqlType . name ;
178+ let kind = 'object' ;
179+ if ( scalarMap . has ( typeName ) ) {
180+ kind = 'scalar' ;
181+ } else if ( enumNames . has ( typeName ) ) {
182+ kind = 'enum' ;
183+ } else if ( interfaceNames . has ( typeName ) ) {
184+ kind = 'interface' ;
185+ } else if ( inputNames . has ( typeName ) ) {
186+ kind = 'input' ;
187+ } else if ( unionNames . has ( typeName ) ) {
188+ kind = 'union' ;
189+ } else if ( objectNames . has ( typeName ) ) {
190+ kind = 'object' ;
191+ }
192+ const dartType = scalarMap . get ( typeName ) ?? typeName ;
193+ return {
194+ kind,
195+ name : typeName ,
196+ nullable : true ,
197+ dartType,
198+ } ;
199+ } ;
200+
201+ const getDartType = ( graphqlType ) => {
202+ const metadata = getTypeMetadata ( graphqlType ) ;
203+ return { type : metadata . dartType , nullable : metadata . nullable , metadata } ;
204+ } ;
205+
206+ const buildFromJsonExpression = ( metadata , sourceExpression ) => {
207+ if ( metadata . kind === 'list' ) {
208+ const listCast = `(${ sourceExpression } as List<dynamic>${ metadata . nullable ? '?' : '' } )` ;
209+ const elementExpression = buildFromJsonExpression ( metadata . elementType , 'e' ) ;
210+ const mapCall = ( target ) => `${ target } .map((e) => ${ elementExpression } ).toList()` ;
211+ if ( metadata . nullable ) {
212+ return `${ listCast } == null ? null : ${ mapCall ( `${ listCast } !` ) } ` ;
213+ }
214+ return mapCall ( listCast ) ;
215+ }
216+ if ( metadata . kind === 'scalar' ) {
217+ switch ( metadata . name ) {
218+ case 'Float' :
219+ return metadata . nullable
220+ ? `(${ sourceExpression } as num?)?.toDouble()`
221+ : `(${ sourceExpression } as num).toDouble()` ;
222+ case 'Int' :
223+ return metadata . nullable
224+ ? `${ sourceExpression } as int?`
225+ : `${ sourceExpression } as int` ;
226+ case 'Boolean' :
227+ return metadata . nullable
228+ ? `${ sourceExpression } as bool?`
229+ : `${ sourceExpression } as bool` ;
230+ case 'ID' :
231+ case 'String' :
232+ return metadata . nullable
233+ ? `${ sourceExpression } as String?`
234+ : `${ sourceExpression } as String` ;
235+ default :
236+ return metadata . nullable ? `${ sourceExpression } ` : `${ sourceExpression } ` ;
237+ }
238+ }
239+ if ( metadata . kind === 'enum' ) {
240+ return metadata . nullable
241+ ? `${ sourceExpression } != null ? ${ metadata . name } .fromJson(${ sourceExpression } as String) : null`
242+ : `${ metadata . name } .fromJson(${ sourceExpression } as String)` ;
243+ }
244+ if ( [ 'object' , 'input' , 'interface' , 'union' ] . includes ( metadata . kind ) ) {
245+ return metadata . nullable
246+ ? `${ sourceExpression } != null ? ${ metadata . dartType } .fromJson(${ sourceExpression } as Map<String, dynamic>) : null`
247+ : `${ metadata . dartType } .fromJson(${ sourceExpression } as Map<String, dynamic>)` ;
248+ }
249+ return metadata . nullable ? `${ sourceExpression } ` : `${ sourceExpression } ` ;
250+ } ;
251+
252+ const buildToJsonExpression = ( metadata , accessorExpression ) => {
253+ if ( metadata . kind === 'list' ) {
254+ const inner = buildToJsonExpression ( metadata . elementType , 'e' ) ;
255+ if ( metadata . nullable ) {
256+ return `${ accessorExpression } == null ? null : ${ accessorExpression } !.map((e) => ${ inner } ).toList()` ;
257+ }
258+ return `${ accessorExpression } .map((e) => ${ inner } ).toList()` ;
259+ }
260+ if ( metadata . kind === 'enum' ) {
261+ return metadata . nullable
262+ ? `${ accessorExpression } ?.toJson()`
263+ : `${ accessorExpression } .toJson()` ;
264+ }
265+ if ( [ 'object' , 'input' , 'interface' , 'union' ] . includes ( metadata . kind ) ) {
266+ return metadata . nullable
267+ ? `${ accessorExpression } ?.toJson()`
268+ : `${ accessorExpression } .toJson()` ;
269+ }
270+ return accessorExpression ;
271+ } ;
272+
162273const lines = [ ] ;
163274lines . push (
164275 '// ============================================================================' ,
@@ -178,12 +289,37 @@ const printEnum = (enumType) => {
178289 const values = enumType . getValues ( ) ;
179290 values . forEach ( ( value , index ) => {
180291 addDocComment ( lines , value . description , ' ' ) ;
181- const name = escapeDartName ( toCamelCase ( value . name ) ) ;
292+ const name = escapeDartName ( toPascalCase ( value . name ) ) ;
182293 const rawValue = toConstantCase ( value . name ) ;
183294 const suffix = index === values . length - 1 ? ';' : ',' ;
184295 lines . push ( ` ${ name } ('${ rawValue } ')${ suffix } ` ) ;
185296 } ) ;
186- lines . push ( '' , ` const ${ enumType . name } (this.value);` , ' final String value;' , '}' , '' ) ;
297+ lines . push (
298+ '' ,
299+ ` const ${ enumType . name } (this.value);` ,
300+ ' final String value;' ,
301+ '' ,
302+ ` factory ${ enumType . name } .fromJson(String value) {` ,
303+ ' switch (value) {'
304+ ) ;
305+ values . forEach ( ( value ) => {
306+ const name = escapeDartName ( toPascalCase ( value . name ) ) ;
307+ const rawValue = toConstantCase ( value . name ) ;
308+ const schemaValue = value . name ;
309+ lines . push ( ` case '${ rawValue } ':` , ` return ${ enumType . name } .${ name } ;` ) ;
310+ if ( schemaValue !== rawValue ) {
311+ lines . push ( ` case '${ schemaValue } ':` , ` return ${ enumType . name } .${ name } ;` ) ;
312+ }
313+ } ) ;
314+ lines . push (
315+ ' }' ,
316+ ` throw ArgumentError('Unknown ${ enumType . name } value: $value');` ,
317+ ' }' ,
318+ '' ,
319+ ' String toJson() => value;' ,
320+ '}' ,
321+ ''
322+ ) ;
187323} ;
188324
189325const printInterface = ( interfaceType ) => {
@@ -203,30 +339,53 @@ const printInterface = (interfaceType) => {
203339const printObject = ( objectType ) => {
204340 addDocComment ( lines , objectType . description ) ;
205341 const interfacesForObject = objectType . getInterfaces ( ) . map ( ( iface ) => iface . name ) ;
206- const unionInterfaces = unionMembership . has ( objectType . name )
342+ const unionsForObject = unionMembership . has ( objectType . name )
207343 ? Array . from ( unionMembership . get ( objectType . name ) ) . sort ( )
208344 : [ ] ;
209- const implementsList = [ ...interfacesForObject , ...unionInterfaces ] ;
210- const implementsClause = implementsList . length ? ` implements ${ implementsList . join ( ', ' ) } ` : '' ;
211- lines . push ( `class ${ objectType . name } ${ implementsClause } {` ) ;
345+ const baseUnion = unionsForObject . shift ( ) ?? null ;
346+ const extendsClause = baseUnion ? ` extends ${ baseUnion } ` : '' ;
347+ const implementsTargets = [ ...interfacesForObject , ...unionsForObject ] ;
348+ const implementsClause = implementsTargets . length ? ` implements ${ implementsTargets . join ( ', ' ) } ` : '' ;
349+ lines . push ( `class ${ objectType . name } ${ extendsClause } ${ implementsClause } {` ) ;
212350 lines . push ( ` const ${ objectType . name } ({` ) ;
213351 const fields = Object . values ( objectType . getFields ( ) ) . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
214- fields . forEach ( ( field , index ) => {
215- addDocComment ( lines , field . description , ' ' ) ;
216- const { type, nullable } = getDartType ( field . type ) ;
217- const fieldType = `${ type } ${ nullable ? '?' : '' } ` ;
352+ const fieldInfos = fields . map ( ( field ) => {
353+ const { type, nullable, metadata } = getDartType ( field . type ) ;
218354 const fieldName = escapeDartName ( field . name ) ;
355+ return { field, fieldName, type, nullable, metadata } ;
356+ } ) ;
357+ fieldInfos . forEach ( ( { field, nullable, fieldName } ) => {
358+ addDocComment ( lines , field . description , ' ' ) ;
219359 const line = ` ${ nullable ? '' : 'required ' } this.${ fieldName } ,` ;
220360 lines . push ( line ) ;
221361 } ) ;
222362 lines . push ( ' });' , '' ) ;
223- fields . forEach ( ( field ) => {
363+ fieldInfos . forEach ( ( { field, type , nullable , fieldName } ) => {
224364 addDocComment ( lines , field . description , ' ' ) ;
225- const { type, nullable } = getDartType ( field . type ) ;
226365 const fieldType = `${ type } ${ nullable ? '?' : '' } ` ;
227- const fieldName = escapeDartName ( field . name ) ;
228366 lines . push ( ` final ${ fieldType } ${ fieldName } ;` ) ;
229367 } ) ;
368+ lines . push ( '' ) ;
369+ lines . push ( ` factory ${ objectType . name } .fromJson(Map<String, dynamic> json) {` ) ;
370+ lines . push ( ` return ${ objectType . name } (` ) ;
371+ fieldInfos . forEach ( ( { field, fieldName, metadata } ) => {
372+ const jsonExpression = buildFromJsonExpression ( metadata , `json['${ field . name } ']` ) ;
373+ lines . push ( ` ${ fieldName } : ${ jsonExpression } ,` ) ;
374+ } ) ;
375+ lines . push ( ' );' ) ;
376+ lines . push ( ' }' , '' ) ;
377+ if ( baseUnion ) {
378+ lines . push ( ' @override' ) ;
379+ }
380+ lines . push ( ' Map<String, dynamic> toJson() {' ) ;
381+ lines . push ( ' return {' ) ;
382+ lines . push ( ` '__typename': '${ objectType . name } ',` ) ;
383+ fieldInfos . forEach ( ( { field, fieldName, metadata } ) => {
384+ const toJsonExpression = buildToJsonExpression ( metadata , fieldName ) ;
385+ lines . push ( ` '${ field . name } ': ${ toJsonExpression } ,` ) ;
386+ } ) ;
387+ lines . push ( ' };' ) ;
388+ lines . push ( ' }' ) ;
230389 lines . push ( '}' , '' ) ;
231390} ;
232391
@@ -235,29 +394,58 @@ const printInput = (inputType) => {
235394 lines . push ( `class ${ inputType . name } {` ) ;
236395 lines . push ( ` const ${ inputType . name } ({` ) ;
237396 const fields = Object . values ( inputType . getFields ( ) ) . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
238- fields . forEach ( ( field ) => {
239- addDocComment ( lines , field . description , ' ' ) ;
240- const { type, nullable } = getDartType ( field . type ) ;
241- const fieldType = `${ type } ${ nullable ? '?' : '' } ` ;
397+ const fieldInfos = fields . map ( ( field ) => {
398+ const { type, nullable, metadata } = getDartType ( field . type ) ;
242399 const fieldName = escapeDartName ( field . name ) ;
400+ return { field, fieldName, type, nullable, metadata } ;
401+ } ) ;
402+ fieldInfos . forEach ( ( { field, nullable, fieldName } ) => {
403+ addDocComment ( lines , field . description , ' ' ) ;
243404 const line = ` ${ nullable ? '' : 'required ' } this.${ fieldName } ,` ;
244405 lines . push ( line ) ;
245406 } ) ;
246407 lines . push ( ' });' , '' ) ;
247- fields . forEach ( ( field ) => {
408+ fieldInfos . forEach ( ( { field, type , nullable , fieldName } ) => {
248409 addDocComment ( lines , field . description , ' ' ) ;
249- const { type, nullable } = getDartType ( field . type ) ;
250410 const fieldType = `${ type } ${ nullable ? '?' : '' } ` ;
251- const fieldName = escapeDartName ( field . name ) ;
252411 lines . push ( ` final ${ fieldType } ${ fieldName } ;` ) ;
253412 } ) ;
413+ lines . push ( '' ) ;
414+ lines . push ( ` factory ${ inputType . name } .fromJson(Map<String, dynamic> json) {` ) ;
415+ lines . push ( ` return ${ inputType . name } (` ) ;
416+ fieldInfos . forEach ( ( { field, fieldName, metadata } ) => {
417+ const jsonExpression = buildFromJsonExpression ( metadata , `json['${ field . name } ']` ) ;
418+ lines . push ( ` ${ fieldName } : ${ jsonExpression } ,` ) ;
419+ } ) ;
420+ lines . push ( ' );' ) ;
421+ lines . push ( ' }' , '' ) ;
422+ lines . push ( ' Map<String, dynamic> toJson() {' ) ;
423+ lines . push ( ' return {' ) ;
424+ fieldInfos . forEach ( ( { field, fieldName, metadata } ) => {
425+ const toJsonExpression = buildToJsonExpression ( metadata , fieldName ) ;
426+ lines . push ( ` '${ field . name } ': ${ toJsonExpression } ,` ) ;
427+ } ) ;
428+ lines . push ( ' };' ) ;
429+ lines . push ( ' }' ) ;
254430 lines . push ( '}' , '' ) ;
255431} ;
256432
257433const printUnion = ( unionType ) => {
258434 addDocComment ( lines , unionType . description ) ;
259- lines . push ( `abstract class ${ unionType . name } {}` ) ;
260- lines . push ( '' ) ;
435+ const members = unionType . getTypes ( ) . map ( ( member ) => member . name ) . sort ( ) ;
436+ lines . push ( `sealed class ${ unionType . name } {` ) ;
437+ lines . push ( ` const ${ unionType . name } ();` , '' ) ;
438+ lines . push ( ` factory ${ unionType . name } .fromJson(Map<String, dynamic> json) {` ) ;
439+ lines . push ( ` final typeName = json['__typename'] as String?;` ) ;
440+ lines . push ( ' switch (typeName) {' ) ;
441+ members . forEach ( ( member ) => {
442+ lines . push ( ` case '${ member } ':` , ` return ${ member } .fromJson(json);` ) ;
443+ } ) ;
444+ lines . push ( ' }' ) ;
445+ lines . push ( ` throw ArgumentError('Unknown __typename for ${ unionType . name } : $typeName');` ) ;
446+ lines . push ( ' }' , '' ) ;
447+ lines . push ( ' Map<String, dynamic> toJson();' ) ;
448+ lines . push ( '}' , '' ) ;
261449} ;
262450
263451const printOperationInterface = ( operationType ) => {
0 commit comments