diff --git a/pkgs/hook/pubspec.yaml b/pkgs/hook/pubspec.yaml index f24cbbd230..b79c808235 100644 --- a/pkgs/hook/pubspec.yaml +++ b/pkgs/hook/pubspec.yaml @@ -10,5 +10,7 @@ environment: dev_dependencies: dart_flutter_team_lints: ^2.1.1 json_schema: ^5.2.0 + json_syntax_generator: + path: ../json_syntax_generator/ path: ^1.9.1 test: ^1.25.15 diff --git a/pkgs/hook/test/schema/helpers.dart b/pkgs/hook/test/schema/helpers.dart index 3e4bbe850e..31c5771251 100644 --- a/pkgs/hook/test/schema/helpers.dart +++ b/pkgs/hook/test/schema/helpers.dart @@ -96,6 +96,16 @@ AllSchemas loadSchemas(List directories) { ); allSchemas[entry.key] = schema; } + + final allSchemasInverted = allSchemas.map( + (key, value) => MapEntry(value, key), + ); + if (allSchemas.length != allSchemasInverted.length) { + throw StateError( + 'Some schemas are not unique, try adding a unique title field.', + ); + } + return allSchemas; } diff --git a/pkgs/hook/tool/generate_schemas.dart b/pkgs/hook/tool/generate_schemas.dart index e860d8e949..90a7ba43aa 100644 --- a/pkgs/hook/tool/generate_schemas.dart +++ b/pkgs/hook/tool/generate_schemas.dart @@ -6,7 +6,6 @@ import 'dart:convert'; import 'dart:io'; import '../test/schema/helpers.dart'; -import 'generate_syntax.dart'; import 'normalize.dart'; void main() { @@ -184,3 +183,10 @@ void generateEntryPoints() { String definitionName(Hook hook, InputOrOutput inputOrOutput) => '${ucFirst(hook.name)}${ucFirst(inputOrOutput.name)}'; + +String ucFirst(String str) { + if (str.isEmpty) { + return ''; + } + return str[0].toUpperCase() + str.substring(1); +} diff --git a/pkgs/hook/tool/generate_syntax.dart b/pkgs/hook/tool/generate_syntax.dart index da0f51a42b..3be51e452b 100644 --- a/pkgs/hook/tool/generate_syntax.dart +++ b/pkgs/hook/tool/generate_syntax.dart @@ -6,7 +6,7 @@ import 'dart:io'; -import 'package:json_schema/json_schema.dart'; +import 'package:json_syntax_generator/json_syntax_generator.dart'; import '../test/schema/helpers.dart'; @@ -17,1139 +17,63 @@ final rootSchemas = loadSchemas([ packageUri.resolve('../code_assets/doc/schema/'), packageUri.resolve('../data_assets/doc/schema/'), ]); -final rootSchemas2 = rootSchemas.map((key, value) => MapEntry(value, key)); - -/// These classes are constructed peacemeal instead of in one go. -/// -/// Generate public setters. -const _publicSetters = { - 'BuildOutput', - 'Config', - 'HookInput', - 'HookOutput', - 'LinkOutput', -}; void main() { - if (rootSchemas.length != rootSchemas2.length) { - throw StateError( - 'Some schemas are not unique, try adding a unique title field.', - ); - } - for (final packageName in generateFor) { const schemaName = 'shared'; final schemaUri = packageUri.resolve( '../$packageName/doc/schema/$schemaName/shared_definitions.schema.json', ); + final schema = rootSchemas[schemaUri]!; + final analyzedSchema = + SchemaAnalyzer( + schema, + capitalizationOverrides: { + 'ios': 'iOS', + 'Ios': 'IOS', + 'macos': 'macOS', + 'Macos': 'MacOS', + }, + publicSetters: [ + 'BuildOutput', + 'Config', + 'HookInput', + 'HookOutput', + 'LinkOutput', + ], + classSorting: + packageName == 'code_assets' + ? [ + 'AndroidCodeConfig', + 'Architecture', + 'Asset', + 'NativeCodeAsset', + 'CCompilerConfig', + 'Windows', + 'DeveloperCommandPrompt', + 'CodeConfig', + 'Config', + 'IOSCodeConfig', + 'LinkMode', + 'DynamicLoadingBundleLinkMode', + 'DynamicLoadingExecutableLinkMode', + 'DynamicLoadingProcessLinkMode', + 'DynamicLoadingSystemLinkMode', + 'StaticLinkMode', + 'LinkModePreference', + 'MacOSCodeConfig', + 'OS', + ] + : null, + ).analyze(); + final output = SyntaxGenerator(analyzedSchema).generate(); final outputUri = packageUri.resolve( '../native_assets_cli/lib/src/$packageName/syntax.g.dart', ); - generate( - JsonSchemas(rootSchemas[schemaUri]!), - schemaName, - packageName, - outputUri, - ); + File.fromUri(outputUri).writeAsStringSync(output); + Process.runSync(Platform.executable, ['format', outputUri.toFilePath()]); + print('Generated $outputUri'); } } Uri packageUri = findPackageRoot('hook'); - -const remapPropertyKeys = { - // TODO: Fix the casing in the schema. - // 'assetsForLinking': 'assets_for_linking_old', -}; - -String remapPropertyKey(String name) { - return remapPropertyKeys[name] ?? name; -} - -void generate( - JsonSchemas schemas, - String schemaName, - String packageName, - Uri outputUri, -) { - final classes = []; - for (final definitionKey in schemas.definitionKeys) { - final definitionSchemas = schemas.getDefinition(definitionKey); - if (definitionSchemas.generateOpenEnum) { - classes.add(generateEnumClass(definitionSchemas, name: definitionKey)); - } else if (definitionSchemas.generateClass) { - classes.add(generateClass(definitionSchemas, name: definitionKey)); - } - } - - final output2 = ''' -// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -// This file is generated, do not edit. - -import '../utils/json.dart'; - -${classes.join('\n\n')} -'''; - final file = File.fromUri(outputUri); - file.createSync(recursive: true); - file.writeAsStringSync(output2); - Process.runSync(Platform.executable, ['format', outputUri.toFilePath()]); - print('Generated $outputUri'); -} - -/// Generates the a class that can serialize/deserialize from a JSON object. -/// -/// The sub classes are identified by a `type` property. -List generateSubClasses(JsonSchemas schemas) { - final classes = []; - final typeName = schemas.className; - final propertyKey = schemas.propertyKeys.single; - final typeProperty = schemas.property(propertyKey); - final anyOf = typeProperty.anyOfs.single; - final subTypes = anyOf.map((e) => e.constValue).whereType().toList(); - final ifThenSchemas = schemas.ifThenSchemas; - for (final subType in subTypes) { - final subTypeNameShort = ucFirst(snakeToCamelCase(subType)); - final subTypeName = '$subTypeNameShort$typeName'; - JsonSchemas? subtypeSchema; - for (final (ifSchema, thenSchema) in ifThenSchemas) { - if (ifSchema.property(propertyKey).constValue == subType) { - subtypeSchema = thenSchema; - break; - } - } - if (subtypeSchema != null) { - classes.add( - generateClass( - subtypeSchema, - name: subTypeName, - superclass: typeName, - identifyingSubtype: subType, - ), - ); - } else { - classes.add(''' -class $subTypeName extends $typeName { - $subTypeName.fromJson(super.json) : super.fromJson(); - - $subTypeName() : super( - type: '$subType' - ); -} -'''); - } - classes.add(''' -extension ${subTypeName}Extension on $typeName { - bool get is$subTypeName => type == '$subType'; - - $subTypeName get as$subTypeName => $subTypeName.fromJson(json); -} -'''); - } - - return classes; -} - -/// Generates the a class that can serialize/deserialize from a JSON object. -/// -/// May have a super class, but it not distinguishable by a `type` property. -/// For that case see [generateSubClasses]. -String generateClass( - JsonSchemas schemas, { - String? name, - String? superclass, - String? identifyingSubtype, -}) { - var typeName = schemas.className; - typeName ??= ucFirst(snakeToCamelCase(name!)); - final accessors = []; - final constructorParams = []; - final setupParams = []; - final constructorSuperParams = []; - final constructorSetterCalls = []; - final classes = []; - final superClassName = schemas.superClassName; - superclass ??= superClassName; - final superSchemas = schemas.superClassSchemas; - final propertyKeys = schemas.propertyKeys; - final settersPrivate = !_publicSetters.contains(typeName); - for (final propertyKey in propertyKeys) { - if (propertyKey == r'$schema') continue; - final propertySchemas = schemas.property(propertyKey); - final required = schemas.propertyRequired(propertyKey); - final allowEnum = !schemas.generateSubClasses; - final parentPropertySchemas = superSchemas?.property(propertyKey); - final dartType = generateDartType( - propertyKey, - propertySchemas, - required, - allowEnum: allowEnum, - ); - final fieldName = snakeToCamelCase(remapPropertyKey(propertyKey)); - - if (parentPropertySchemas == null) { - accessors.add( - generateGetterAndSetter( - propertyKey, - propertySchemas, - required, - allowEnum: allowEnum, - setterPrivate: settersPrivate, - ), - ); - constructorParams.add( - '${required ? 'required' : ''} $dartType $fieldName', - ); - setupParams.add('${required ? 'required' : ''} $dartType $fieldName'); - if (settersPrivate) { - constructorSetterCalls.add('_$fieldName = $fieldName;'); - } else { - constructorSetterCalls.add('this.$fieldName = $fieldName;'); - } - } else { - if (parentPropertySchemas.className != propertySchemas.className || - propertySchemas.type != parentPropertySchemas.type) { - final override = - parentPropertySchemas?.type != null || - propertySchemas.className != null; - accessors.add( - generateGetterAndSetter( - propertyKey, - propertySchemas, - required, - override: override, - setterPrivate: settersPrivate, - ), - ); - if (override) { - constructorParams.add( - '${required ? 'required' : ''} super.$fieldName', - ); - } else { - constructorParams.add( - '${required ? 'required' : ''} $dartType $fieldName', - ); - setupParams.add('${required ? 'required' : ''} $dartType $fieldName'); - if (settersPrivate) { - constructorSetterCalls.add('_$fieldName = $fieldName;'); - } else { - constructorSetterCalls.add('this.$fieldName = $fieldName;'); - } - } - } else { - constructorParams.add('${required ? 'required' : ''} super.$fieldName'); - } - } - if (propertySchemas.generateClass && propertySchemas.refs.isEmpty) { - classes.add(generateClass(propertySchemas, name: propertyKey)); - } - } - - if (schemas.generateSubClasses) { - classes.addAll(generateSubClasses(schemas)); - } - - if (identifyingSubtype != null) { - constructorSuperParams.add("type: '$identifyingSubtype'"); - } - - if (constructorSetterCalls.isNotEmpty) { - constructorSetterCalls.add('json.sortOnKey();'); - } - - String wrapBracesIfNotEmpty(String input) => - input.isEmpty ? input : '{$input}'; - - String wrapInBracesOrSemicolon(String input) => - input.isEmpty ? ';' : '{ $input }'; - - if (superclass != null) { - return ''' -class $typeName extends $superclass { - $typeName.fromJson(super.json) : super.fromJson(); - - $typeName({ - ${constructorParams.join(',')} - }) : super( - ${constructorSuperParams.join(',')} - ) ${wrapInBracesOrSemicolon(constructorSetterCalls.join('\n'))} - - /// Setup all fields for [$typeName] that are not in - /// [$superclass]. - void setup ( - ${wrapBracesIfNotEmpty(setupParams.join(','))} - ) { - ${constructorSetterCalls.join('\n')} - } - - ${accessors.join('\n\n')} - - @override - String toString() => '$typeName(\$json)'; -} - -${classes.join('\n\n')} -'''; - } - - return ''' -class $typeName { - final Map json; - - $typeName.fromJson(this.json); - - $typeName({ - ${constructorParams.join(',')} - }) : json = {} { - ${constructorSetterCalls.join('\n')} - } - - ${accessors.join('\n\n')} - - @override - String toString() => '$typeName(\$json)'; -} - -${classes.join('\n\n')} -'''; -} - -/// Generates an open enum class. -/// -/// The 'name' or 'type' is an open enum. Static consts are generated for all -/// known values. Parsing an unknown value doesn't fail. -String generateEnumClass(JsonSchemas schemas, {String? name}) { - if (schemas.type != SchemaType.string) { - throw UnimplementedError(schemas.type.toString()); - } - var typeName = schemas.className; - typeName ??= ucFirst(snakeToCamelCase(name!)); - - final anyOf = schemas.anyOfs.single; - final values = - anyOf.map((e) => e.constValue).whereType().toList()..sort(); - - final staticFinals = []; - - for (final value in values) { - final valueName = snakeToCamelCase(value); - staticFinals.add(''' -static const $valueName = $typeName._('$value'); -'''); - } - - return ''' - class $typeName { - final String name; - - const $typeName._(this.name); - - ${staticFinals.join('\n\n')} - - static const List<$typeName> values = [ - ${values.map(snakeToCamelCase).join(',')} - ]; - - static final Map _byName = { - for (final value in values) value.name: value, - }; - - $typeName.unknown(this.name) : assert(!_byName.keys.contains(name)); - - factory $typeName.fromJson(String name) { - final knownValue = _byName[name]; - if(knownValue != null) { - return knownValue; - } - return $typeName.unknown(name); - } - - bool get isKnown => _byName[name] != null; - - @override - String toString() => name; - } - '''; -} - -/// Generates the Dart type representing the type for a property. -/// -/// Used for generating field types, getters, and setters. -String generateDartType( - String propertyKey, - JsonSchemas schemas, - bool required, { - bool allowEnum = true, -}) { - final type = schemas.type; - final String dartTypeNonNullable; - switch (type) { - case SchemaType.boolean: - dartTypeNonNullable = 'bool'; - case SchemaType.integer: - dartTypeNonNullable = 'int'; - case SchemaType.string: - if (schemas.generateUri) { - dartTypeNonNullable = 'Uri'; - } else if (schemas.generateEnum && allowEnum) { - dartTypeNonNullable = schemas.className!; - } else { - dartTypeNonNullable = 'String'; - } - case SchemaType.object: - final additionalPropertiesSchema = schemas.additionalPropertiesSchemas; - if (schemas.generateMapOf) { - final additionalPropertiesType = additionalPropertiesSchema.type; - switch (additionalPropertiesType) { - case SchemaType.array: - final items = additionalPropertiesSchema.items; - final itemType = items.type; - switch (itemType) { - case SchemaType.object: - final typeName = items.className!; - dartTypeNonNullable = 'Map>'; - default: - throw UnimplementedError(itemType.toString()); - } - case SchemaType.object: - final additionalPropertiesBool = - additionalPropertiesSchema.additionalPropertiesBool; - if (additionalPropertiesBool != true) { - throw UnimplementedError( - 'Expected an object with arbitrary properties.', - ); - } - dartTypeNonNullable = 'Map>'; - case null: - if (schemas.additionalPropertiesBool != true) { - throw UnimplementedError( - 'Expected an object with arbitrary properties.', - ); - } - dartTypeNonNullable = 'Map'; - default: - throw UnimplementedError(additionalPropertiesType.toString()); - } - } else { - var typeName = schemas.className; - typeName ??= ucFirst(snakeToCamelCase(propertyKey)); - dartTypeNonNullable = typeName; - } - case SchemaType.array: - final items = schemas.items; - final itemType = items.type; - switch (itemType) { - case SchemaType.string: - if (items.patterns.isNotEmpty) { - dartTypeNonNullable = 'List'; - } else { - dartTypeNonNullable = 'List'; - } - case SchemaType.object: - final typeName = items.className!; - dartTypeNonNullable = 'List<$typeName>'; - default: - throw UnimplementedError(itemType.toString()); - } - - default: - throw UnimplementedError(type.toString()); - } - if (required) { - return dartTypeNonNullable; - } - return '$dartTypeNonNullable?'; -} - -/// Generate getter and setter pairs for a property. -String generateGetterAndSetter( - String propertyKey, - JsonSchemas schemas, - bool required, { - bool override = false, - bool allowEnum = true, - bool setterPrivate = true, -}) { - var result = StringBuffer(); - final type = schemas.type; - final fieldName = snakeToCamelCase(remapPropertyKey(propertyKey)); - final setterName = setterPrivate ? '_$fieldName' : fieldName; - final sortOnKey = setterPrivate ? '' : 'json.sortOnKey();'; - if (override) { - result += '@override\n'; - } - switch (type) { - case SchemaType.string: - if (schemas.generateUri) { - if (required) { - result += ''' -Uri get $fieldName => json.path('$propertyKey'); - -set $setterName(Uri value){ - json['$propertyKey'] = value.toFilePath(); - $sortOnKey -} -'''; - } else { - result += ''' -Uri? get $fieldName => json.optionalPath('$propertyKey'); - -set $setterName(Uri? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value.toFilePath(); - } - $sortOnKey -} -'''; - } - } else if (schemas.generateEnum && allowEnum) { - var typeName = schemas.className; - typeName ??= ucFirst(snakeToCamelCase(propertyKey)); - if (required) { - result += ''' -$typeName get $fieldName => $typeName.fromJson(json.string('$propertyKey')); - -set $setterName($typeName value) { - json['$propertyKey'] = value.name; - $sortOnKey -} -'''; - } else { - result += ''' -$typeName? get $fieldName { - final string = json.optionalString('$propertyKey'); - if(string == null) return null; - return $typeName.fromJson(string); -} - -set $setterName($typeName? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value.name; - } - $sortOnKey -} -'''; - } - } else { - if (required) { - result += ''' -String get $fieldName => json.string('$propertyKey'); - -set $setterName(String value) { - json['$propertyKey'] = value; - $sortOnKey -} -'''; - } else { - result += ''' -String? get $fieldName => json.optionalString('$propertyKey'); - -set $setterName(String? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value; - } - $sortOnKey -} -'''; - } - } - case SchemaType.object: - final additionalPropertiesSchema = schemas.additionalPropertiesSchemas; - if (schemas.generateMapOf) { - final additionalPropertiesType = additionalPropertiesSchema.type; - switch (additionalPropertiesType) { - case SchemaType.array: - final items = additionalPropertiesSchema.items; - final itemType = items.type; - switch (itemType) { - case SchemaType.object: - if (required) { - throw UnimplementedError( - 'Only implemented for nullable property.', - ); - } - final typeName = items.className!; - result += ''' -Map>? get $fieldName { - final map_ = json.optionalMap('$propertyKey'); - if(map_ == null){ - return null; - } - return { - for (final MapEntry(:key, :value) in map_.entries) - key : [ - for (final item in value as List) - $typeName.fromJson(item as Map) - ], - }; -} - -set $setterName(Map>? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = { - for (final MapEntry(:key, :value) in value.entries) - key : [ - for (final item in value) - item.json, - ], - }; - } - $sortOnKey -} -'''; - default: - throw UnimplementedError(itemType.toString()); - } - case SchemaType.object: - final additionalPropertiesBool = - additionalPropertiesSchema.additionalPropertiesBool; - if (additionalPropertiesBool != true) { - throw UnimplementedError( - 'Expected an object with arbitrary properties.', - ); - } - result += ''' -Map>? get $fieldName => - json.optionalMap>('$propertyKey'); - -set $setterName(Map>? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value; - } - $sortOnKey -} -'''; - case null: - if (schemas.additionalPropertiesBool != true) { - throw UnimplementedError( - 'Expected an object with arbitrary properties.', - ); - } - result += ''' -Map? get $fieldName => json.optionalMap('$propertyKey'); - -set $setterName(Map? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value; - } - $sortOnKey -} -'''; - default: - throw UnimplementedError(additionalPropertiesType.toString()); - } - } else { - var typeName = schemas.className; - typeName ??= ucFirst(snakeToCamelCase(propertyKey)); - if (required) { - result += ''' -$typeName get $fieldName => $typeName.fromJson( json.map\$('$propertyKey') ); -'''; - if (!override) { - result += ''' -set $setterName($typeName value) => json['$propertyKey'] = value.json; -'''; - } - } else { - result += ''' -$typeName? get $fieldName { - final map_ = json.optionalMap('$propertyKey'); - if(map_ == null){ - return null; - } - return $typeName.fromJson(map_); -} - -set $setterName($typeName? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value.json; - } - $sortOnKey -} -'''; - } - } - case SchemaType.boolean: - if (required) { - result += ''' -bool get $fieldName => json.get('$propertyKey'); - -set $setterName(bool value) { - json['$propertyKey'] = value; - $sortOnKey -} -'''; - } else { - result += ''' -bool? get $fieldName => json.getOptional('$propertyKey'); - -set $setterName(bool? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value; - } - $sortOnKey -}; -'''; - } - case SchemaType.integer: - if (required) { - result += ''' -int get $fieldName => json.get('$propertyKey'); - -set $setterName(int value) => json['$propertyKey'] = value; -'''; - } else { - result += ''' -int? get $fieldName => json.getOptional('$propertyKey'); - -set $setterName(int? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value; - } - $sortOnKey -} -'''; - } - case SchemaType.array: - final items = schemas.items; - final itemType = items.type; - switch (itemType) { - case SchemaType.string: - if (items.patterns.isNotEmpty) { - if (required) { - result += ''' -List get $fieldName => json.pathList('$propertyKey'); - -set $setterName(List value) => json['$propertyKey'] = value.toJson(); -'''; - } else { - result += ''' -List? get $fieldName => json.optionalPathList('$propertyKey'); - -set $setterName(List? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value.toJson(); - } - $sortOnKey -} -'''; - } - } else { - if (required) { - result += ''' -List get $fieldName => json.stringList('$propertyKey'); - -set $setterName(List value) { - json['$propertyKey'] = value; - $sortOnKey -} -'''; - } else { - result += ''' -List? get $fieldName => json.optionalStringList('$propertyKey'); - -set $setterName(List? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = value; - } - $sortOnKey -} -'''; - } - } - case SchemaType.object: - final typeName = items.className!; - if (required) { - throw UnimplementedError('Expected an optional property.'); - } else { - result += ''' -List<$typeName>? get $fieldName { - final list_ = json.optionalList('$propertyKey')?.cast>(); - if(list_ == null){ - return null; - } - final result = <$typeName>[]; - for(final item in list_){ - result.add($typeName.fromJson(item)); - } - return result; -} - -set $setterName(List<$typeName>? value) { - if (value == null) { - json.remove('$propertyKey'); - } - else { - json['$propertyKey'] = [ - for (final item in value) - item.json - ]; - } - $sortOnKey -} -'''; - } - default: - throw UnimplementedError(itemType.toString()); - } - - default: - throw UnimplementedError(type.toString()); - } - return result.toString(); -} - -String snakeToCamelCase(String string) { - if (string.isEmpty) { - return ''; - } - - final parts = string.replaceAll('-', '_').split('_'); - - const capitalizationOverrides = { - 'ios': 'iOS', - 'Ios': 'IOS', - 'macos': 'macOS', - 'Macos': 'MacOS', - }; - - String remapCapitalization(String input) => - capitalizationOverrides[input] ?? input; - - var result = StringBuffer(); - result += remapCapitalization(parts[0]); - - for (var i = 1; i < parts.length; i++) { - if (parts[i].isNotEmpty) { - result += remapCapitalization( - parts[i][0].toUpperCase() + parts[i].substring(1), - ); - } - } - - return result.toString(); -} - -String ucFirst(String str) { - if (str.isEmpty) { - return ''; - } - return str[0].toUpperCase() + str.substring(1); -} - -/// A view on [JsonSchema]s that extend/override each other. -extension type JsonSchemas._(List _schemas) { - factory JsonSchemas(JsonSchema schema) => JsonSchemas._([schema])._flatten(); - - JsonSchemas _flatten() { - final flattened = []; - final queue = [..._schemas]; - while (queue.isNotEmpty) { - final item = queue.first; - queue.removeAt(0); - if (flattened.contains(item)) { - continue; - } - flattened.add(item); - queue.addAll(item.allOf); - final ref = item.ref; - if (ref != null) { - queue.add(item.resolvePath(ref)); - } - } - final result = JsonSchemas._(flattened); - return result; - } - - List get propertyKeys => - {for (final schema in _schemas) ...schema.properties.keys}.toList() - ..sort(); - - JsonSchemas property(String key) { - final propertySchemas = []; - for (final schema in _schemas) { - final propertySchema = schema.properties[key]; - if (propertySchema != null) { - propertySchemas.add(propertySchema); - } - } - return JsonSchemas._(propertySchemas)._flatten(); - } - - bool propertyRequired(String? property) => - _schemas.any((e) => e.propertyRequired(property)); - - SchemaType? get type { - final types = {}; - for (final schema in _schemas) { - final schemaTypes = schema.typeList; - if (schemaTypes != null) { - for (final schemaType in schemaTypes) { - if (schemaType != null) types.add(schemaType); - } - } - } - if (types.length > 1) { - throw StateError('Multiple types found'); - } - return types.singleOrNull; - } - - List get patterns { - final patterns = {}; - for (final schema in _schemas) { - final pattern = schema.pattern; - if (pattern != null) { - patterns.add(pattern); - } - } - return patterns.toList(); - } - - JsonSchemas get items { - final items = []; - for (final schema in _schemas) { - final item = schema.items; - if (item != null) { - items.add(item); - } - } - return JsonSchemas._(items)._flatten(); - } - - List get paths { - final paths = {}; - for (final schema in _schemas) { - final path = schema.path; - if (path != null) paths.add(path); - } - return paths.toList(); - } - - List get definitionKeys => - {for (final schema in _schemas) ...schema.definitions.keys}.toList() - ..sort(); - - JsonSchemas getDefinition(String key) { - final definitionSchemas = []; - for (final schema in _schemas) { - final propertySchema = schema.definitions[key]; - if (propertySchema != null) { - definitionSchemas.add(propertySchema); - } - } - return JsonSchemas._(definitionSchemas)._flatten(); - } - - JsonSchemas get additionalPropertiesSchemas { - final schemas = []; - for (final schema in _schemas) { - final additionalPropertiesSchema = schema.additionalPropertiesSchema; - if (additionalPropertiesSchema != null) { - schemas.add(additionalPropertiesSchema); - } - } - return JsonSchemas._(schemas)._flatten(); - } - - bool? get additionalPropertiesBool { - final result = []; - for (final schema in _schemas) { - final additionalPropertiesBool = schema.additionalPropertiesBool; - if (additionalPropertiesBool != null) { - result.add(additionalPropertiesBool); - } - } - if (result.length > 1) { - throw StateError('Both yes and no for additional properties.'); - } - return result.singleOrNull; - } - - bool get isNotEmpty => _schemas.isNotEmpty; - - List> get anyOfs { - final result = >[]; - for (final schema in _schemas) { - final anyOf = schema.anyOf; - final tempResult = []; - for (final option in anyOf) { - tempResult.add(JsonSchemas(option)._flatten()); - } - if (tempResult.isNotEmpty) { - result.add(tempResult); - } - } - return result; - } - - Object? get constValue { - final result = []; - for (final schema in _schemas) { - final item = schema.constValue; - if (item != null) { - result.add(item as Object); - } - } - if (result.length > 1) { - throw UnimplementedError('Conflicting const values.'); - } - return result.singleOrNull; - } - - List get refs { - final result = []; - for (final schema in _schemas) { - final ref = schema.ref; - if (ref != null) { - result.add(ref); - } - } - return result; - } - - List<(JsonSchemas, JsonSchemas)> get ifThenSchemas { - final result = <(JsonSchemas, JsonSchemas)>[]; - for (final schema in _schemas) { - final ifSchema = schema.ifSchema; - final thenSchema = schema.thenSchema; - if (ifSchema != null && thenSchema != null) { - result.add(( - JsonSchemas(ifSchema)._flatten(), - JsonSchemas(thenSchema)._flatten(), - )); - } - } - return result; - } -} - -extension CodeGenDecisions on JsonSchemas { - /// Either [generateClosedEnum] or [generateOpenEnum]. - bool get generateEnum => type == SchemaType.string && anyOfs.isNotEmpty; - - /// A class with opaque members. - bool get generateClosedEnum => - generateEnum && !anyOfs.single.any((e) => e.type != null); - - /// A class with opaque members and an `unknown` option. - bool get generateOpenEnum => - generateEnum && anyOfs.single.any((e) => e.type != null); - - /// Generate getters/setters as `Map. - bool get generateMapOf => - type == SchemaType.object && - (additionalPropertiesSchemas.isNotEmpty || - additionalPropertiesBool == true); - - bool get generateSubClasses => - type == SchemaType.object && - propertyKeys.length == 1 && - property(propertyKeys.single).anyOfs.isNotEmpty; - - bool get generateClass => type == SchemaType.object && !generateMapOf; - - /// Generate getters/setters as `Uri`. - bool get generateUri => type == SchemaType.string && patterns.isNotEmpty; - - static String? _pathToClassName(String path) { - if (path.contains('#/definitions/')) { - final splits = path.split('/'); - final indexOf = splits.indexOf('definitions'); - final nameParts = splits.skip(indexOf + 1).where((e) => e.isNotEmpty); - if (nameParts.length == 1 && nameParts.single.startsWithUpperCase()) { - return nameParts.single; - } - } - return null; - } - - /// This is all the inferred class names from definitions in order of - /// traversal. - List get classNames { - final result = []; - for (final path in paths) { - final className = _pathToClassName(path); - if (className != null) { - if (!result.contains(className)) { - result.add(className); - } - } - } - return result; - } - - String? get className => classNames.firstOrNull; - - String? get superClassName { - final names = classNames; - if (names.length == 2) { - return names[1]; - } - if (names.length > 2) { - throw UnimplementedError('Deeper inheritance not implemented.'); - } - return null; - } - - JsonSchemas? get superClassSchemas { - final parentClassName = superClassName; - if (parentClassName == null) { - return null; - } - for (final schema in _schemas) { - final path = schema.path; - if (path == null) continue; - final className = _pathToClassName(path); - if (className != parentClassName) continue; - return JsonSchemas(schema)._flatten(); - } - throw StateError('No super class schema found for $parentClassName.'); - } -} - -extension on String { - bool startsWithUpperCase() { - final codeUnit = codeUnitAt(0); - return codeUnit >= 'A'.codeUnitAt(0) && codeUnit <= 'Z'.codeUnitAt(0); - } -} - -extension on StringBuffer { - StringBuffer operator +(String value) => this..write(value); -} diff --git a/pkgs/json_syntax_generator/README.md b/pkgs/json_syntax_generator/README.md new file mode 100644 index 0000000000..c75e7f37d7 --- /dev/null +++ b/pkgs/json_syntax_generator/README.md @@ -0,0 +1,39 @@ +# JSON Syntax Generator + +`package:json_syntax_generator` provides a powerful and flexible way to generate +Dart code from JSON schemas. It simplifies the process of working with JSON data +by automatically creating Dart classes that represent the structure of your JSON +data, including support for complex schema features. + +## Features + +This package is designed to handle a wide range of JSON schema features, including: + +* **Naming Conventions:** Converts snake-cased keys from JSON schemas to + camel-cased Dart names, following Effective Dart guidelines. +* **Optional and Required Fields:** Generates Dart constructors and getters + with correct nullability based on the `required` property in the JSON + schema. +* **Subclassing:** Automatically recognizes and generates Dart subclasses from + JSON schemas that use `allOf` to reference other definitions. +* **Tagged Unions:** Handles tagged unions defined using `if`, `properties`, + `type`, `const`, and `then` in the schema, generating appropriate Dart class + hierarchies. +* **Open Enums:** Interprets JSON schemas with `anyOf` containing `const` + values and `type: string` as open enums, generating Dart classes with + `static const` members for known values and support for unknown values. + +## How It Works + +`package:json_syntax_generator` operates as a pipeline with two steps: + +1. **Schema Analysis:** The `SchemaAnalyzer` class analyzes a JSON schema and + extracts relevant information. It makes decisions about how code should be + generated and encodes them in a `SchemaInfo` object. This includes + determining class names, property types, inheritance relationships, and + other schema features. +2. **Code Generation:** The `SyntaxGenerator` class takes the `SchemaInfo` + object and generates the corresponding Dart code. This code includes class + definitions, constructors, getters, and setters, all tailored to the + specific structure and requirements of the JSON schema. + diff --git a/pkgs/json_syntax_generator/analysis_options.yaml b/pkgs/json_syntax_generator/analysis_options.yaml new file mode 100644 index 0000000000..349b9d631b --- /dev/null +++ b/pkgs/json_syntax_generator/analysis_options.yaml @@ -0,0 +1,17 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + errors: + todo: ignore + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - dangling_library_doc_comments + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_in_for_each + - prefer_final_locals diff --git a/pkgs/json_syntax_generator/lib/json_syntax_generator.dart b/pkgs/json_syntax_generator/lib/json_syntax_generator.dart new file mode 100644 index 0000000000..12d667a078 --- /dev/null +++ b/pkgs/json_syntax_generator/lib/json_syntax_generator.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// `package:json_syntax_generator` provides a powerful and flexible way to +/// generate Dart code from JSON schemas. It simplifies the process of +/// working with JSON data by automatically creating Dart classes that +/// represent the structure of your JSON data, including support for complex +/// schema features. +library; + +export 'src/generator/syntax_generator.dart' show SyntaxGenerator; +export 'src/model/class_info.dart'; +export 'src/model/dart_type.dart'; +export 'src/model/property_info.dart'; +export 'src/model/schema_info.dart'; +export 'src/parser/schema_analyzer.dart' show SchemaAnalyzer; diff --git a/pkgs/json_syntax_generator/lib/src/generator/syntax_generator.dart b/pkgs/json_syntax_generator/lib/src/generator/syntax_generator.dart new file mode 100644 index 0000000000..1ee2e774cd --- /dev/null +++ b/pkgs/json_syntax_generator/lib/src/generator/syntax_generator.dart @@ -0,0 +1,694 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../model/class_info.dart'; +import '../model/dart_type.dart'; +import '../model/property_info.dart'; +import '../model/schema_info.dart'; + +/// Generates Dart code from a [SchemaInfo]. +/// +/// This is a simple code generator, all code generation decisions are already +/// made in the [schemaInfo] and its properties. +/// +/// It supports the following features: +/// * Optional and required fields. Constructors and getters are generated with +/// the right nullability. +/// * Subclassing and tagged unions. A subtype hierarchy is generated and the +/// constructors call the parent constructor. For tagged unions, the field +/// identifying the subtype does not have to be provided in the constructor. +/// For normal subtyping all parent fields have to be supplied. +/// * Open enums. These are generated with `static const`s for the known values. +class SyntaxGenerator { + final SchemaInfo schemaInfo; + + SyntaxGenerator(this.schemaInfo); + + String generate() { + final buffer = StringBuffer(); + + buffer.writeln(''' +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// This file is generated, do not edit. + +import '../utils/json.dart'; +'''); + + for (final classInfo in schemaInfo.classes) { + buffer.writeln(_generateClass(classInfo)); + } + + return buffer.toString(); + } + + String _generateClass(ClassInfo classInfo) { + switch (classInfo) { + case NormalClassInfo(): + return _generateNormalClass(classInfo); + case EnumClassInfo(): + return _generateEnumClass(classInfo); + } + } + + String _generateNormalClass(NormalClassInfo classInfo) { + final buffer = StringBuffer(); + final className = classInfo.name; + final superclass = classInfo.superclass; + final superclassName = superclass?.name; + final properties = classInfo.properties; + final identifyingSubtype = classInfo.taggedUnionKey; + + final constructorParams = []; + final setupParams = []; + final constructorSetterCalls = []; + final accessors = []; + final superParams = []; + + final propertyNames = + { + for (final property in properties) property.name, + if (superclass != null) + for (final property in superclass.properties) property.name, + }.toList() + ..sort(); + for (final propertyName in propertyNames) { + final superClassProperty = superclass?.getProperty(propertyName); + final thisClassProperty = classInfo.getProperty(propertyName); + final property = superClassProperty ?? thisClassProperty!; + if (superClassProperty != null) { + if (identifyingSubtype == null) { + constructorParams.add( + '${property.isRequired ? 'required ' : ''}super.${property.name}', + ); + } else { + superParams.add("type: '$identifyingSubtype'"); + } + } else { + final dartType = property.type; + constructorParams.add( + '${property.isRequired ? 'required ' : ''}$dartType ${property.name}', + ); + setupParams.add( + '${property.isRequired ? 'required' : ''} $dartType ${property.name}', + ); + if (property.setterPrivate) { + constructorSetterCalls.add('_${property.name} = ${property.name};'); + } else { + constructorSetterCalls.add( + 'this.${property.name} = ${property.name};', + ); + } + } + if (thisClassProperty != null) { + accessors.add(_generateGetterAndSetter(thisClassProperty)); + } + } + + if (constructorSetterCalls.isNotEmpty) { + constructorSetterCalls.add('json.sortOnKey();'); + } + + if (superclass != null) { + buffer.writeln(''' +class $className extends $superclassName { + $className.fromJson(super.json) : super.fromJson(); + + $className(${_wrapBracesIfNotEmpty(constructorParams.join(', '))}) + : super(${superParams.join(',')}) + ${_wrapInBracesOrSemicolon(constructorSetterCalls.join('\n '))} +'''); + if (setupParams.isNotEmpty) { + buffer.writeln(''' + /// Setup all fields for [$className] that are not in + /// [$superclassName]. + void setup ( + ${_wrapBracesIfNotEmpty(setupParams.join(','))} + ) { + ${constructorSetterCalls.join('\n')} + } +'''); + } + + buffer.writeln(''' + ${accessors.join('\n')} + + @override + String toString() => '$className(\$json)'; +} +'''); + } else { + buffer.writeln(''' +class $className { + final Map json; + + $className.fromJson(this.json); + + $className(${_wrapBracesIfNotEmpty(constructorParams.join(', '))}) : json = {} { + ${constructorSetterCalls.join('\n ')} + } + + ${accessors.join('\n')} + + @override + String toString() => '$className(\$json)'; +} +'''); + } + + if (identifyingSubtype != null) { + buffer.writeln(''' +extension ${className}Extension on $superclassName { + bool get is$className => type == '$identifyingSubtype'; + + $className get as$className => $className.fromJson(json); +} +'''); + } + + return buffer.toString(); + } + + String _generateEnumClass(EnumClassInfo classInfo) { + final buffer = StringBuffer(); + final className = classInfo.name; + final enumValues = classInfo.enumValues; + + final staticFinals = []; + for (final value in enumValues) { + final valueName = value.name; + final jsonValue = value.jsonValue; + staticFinals.add( + 'static const $valueName = $className._(\'$jsonValue\');', + ); + } + + buffer.writeln(''' +class $className { + final String name; + + const $className._(this.name); + + ${staticFinals.join('\n\n')} + + static const List<$className> values = [ + ${enumValues.map((e) => e.name).join(',')} + ]; + + static final Map _byName = { + for (final value in values) value.name: value, + }; + + $className.unknown(this.name) : assert(!_byName.keys.contains(name)); + + factory $className.fromJson(String name) { + final knownValue = _byName[name]; + if(knownValue != null) { + return knownValue; + } + return $className.unknown(name); + } + + bool get isKnown => _byName[name] != null; + + @override + String toString() => name; +} +'''); + return buffer.toString(); + } + + String _generateGetterAndSetter(PropertyInfo property) { + final buffer = StringBuffer(); + final dartType = property.type; + final fieldName = property.name; + final jsonKey = property.jsonKey; + final setterName = property.setterPrivate ? '_$fieldName' : fieldName; + final sortOnKey = property.setterPrivate ? '' : 'json.sortOnKey();'; + + if (property.isOverride) { + buffer.writeln('@override'); + } + + switch (dartType) { + case ClassDartType(): + _generateClassTypeGetterSetter( + buffer, + property, + dartType, + jsonKey, + setterName, + sortOnKey, + ); + case SimpleDartType(): + _generateSimpleTypeGetterSetter( + buffer, + property, + dartType, + jsonKey, + setterName, + sortOnKey, + ); + case MapDartType(): + _generateMapTypeGetterSetter( + buffer, + property, + dartType, + jsonKey, + setterName, + sortOnKey, + ); + case ListDartType(): + _generateListTypeGetterSetter( + buffer, + property, + dartType, + jsonKey, + setterName, + sortOnKey, + ); + case UriDartType(): + _generateUriTypeGetterSetter( + buffer, + property, + dartType, + jsonKey, + setterName, + sortOnKey, + ); + } + + return buffer.toString(); + } + + void _generateClassTypeGetterSetter( + StringBuffer buffer, + PropertyInfo property, + ClassDartType dartType, + String jsonKey, + String setterName, + String sortOnKey, + ) { + final classInfo = dartType.classInfo; + final classType = classInfo.name; + final fieldName = property.name; + final required = property.isRequired; + + switch (classInfo) { + case EnumClassInfo(): + if (required) { + buffer.writeln(''' +$dartType get $fieldName => $classType.fromJson( json.string('$jsonKey') ); +'''); + if (!property.isOverride) { + buffer.writeln(''' +set $setterName($dartType value) { + json['$jsonKey'] = value.name; +} +'''); + } + } else { + buffer.writeln(''' +$dartType get $fieldName { + final string = json.optionalString('$jsonKey'); + if(string == null) return null; + return $classType.fromJson(string); +} +'''); + if (!property.isOverride) { + buffer.writeln(''' +set $setterName($dartType value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value.name; + } + $sortOnKey +} +'''); + } + } + case NormalClassInfo(): + if (required) { + buffer.writeln(''' +$dartType get $fieldName => $classType.fromJson( json.map\$('$jsonKey') ); +'''); + if (!property.isOverride) { + buffer.writeln(''' +set $setterName($dartType value) => json['$jsonKey'] = value.json; +'''); + } + } else { + buffer.writeln(''' +$dartType get $fieldName { + final map_ = json.optionalMap('$jsonKey'); + if(map_ == null){ + return null; + } + return $classType.fromJson(map_); +} +'''); + if (!property.isOverride) { + buffer.writeln(''' +set $setterName($dartType value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value.json; + } + $sortOnKey +} +'''); + } + } + } + } + + void _generateSimpleTypeGetterSetter( + StringBuffer buffer, + PropertyInfo property, + SimpleDartType dartType, + String jsonKey, + String setterName, + String sortOnKey, + ) { + final fieldName = property.name; + final required = property.isRequired; + + switch (dartType.typeName) { + case 'String': + if (required) { + buffer.writeln(''' +String get $fieldName => json.string('$jsonKey'); + +set $setterName(String value) { + json['$jsonKey'] = value; + $sortOnKey +} +'''); + } else { + buffer.writeln(''' +String? get $fieldName => json.optionalString('$jsonKey'); + +set $setterName(String? value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value; + } + $sortOnKey +} +'''); + } + case 'int': + if (required) { + buffer.writeln(''' +int get $fieldName => json.get('$jsonKey'); + +set $setterName(int value) => json['$jsonKey'] = value; +'''); + } else { + buffer.writeln(''' +int? get $fieldName => json.getOptional('$jsonKey'); + +set $setterName(int? value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value; + } + $sortOnKey +} +'''); + } + case 'bool': + if (required) { + buffer.writeln(''' +bool get $fieldName => json.get('$jsonKey'); + +set $setterName(bool value) { + json['$jsonKey'] = value; + $sortOnKey +} +'''); + } else { + buffer.writeln(''' +bool? get $fieldName => json.getOptional('$jsonKey'); + +set $setterName(bool? value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value; + } + $sortOnKey +} +'''); + } + default: + throw UnimplementedError( + 'Unsupported SimpleDartType: ${dartType.typeName}', + ); + } + } + + void _generateMapTypeGetterSetter( + StringBuffer buffer, + PropertyInfo property, + MapDartType dartType, + String jsonKey, + String setterName, + String sortOnKey, + ) { + final fieldName = property.name; + final valueType = dartType.valueType; + + switch (valueType) { + case MapDartType(): + buffer.writeln(''' +Map>? get $fieldName => + json.optionalMap>('$jsonKey'); + +set $setterName(Map>? value) { + if (value == null) { + json.remove('$jsonKey'); + } else { + json['$jsonKey'] = value; + } + $sortOnKey +} +'''); + case ListDartType(): + final itemType = valueType.itemType; + final typeName = itemType.toString(); + buffer.writeln(''' +Map>? get $fieldName { + final map_ = json.optionalMap('$jsonKey'); + if(map_ == null){ + return null; + } + return { + for (final MapEntry(:key, :value) in map_.entries) + key : [ + for (final item in value as List) + $typeName.fromJson(item as Map) + ], + }; +} + +set $setterName(Map>? value) { + if (value == null) { + json.remove('$jsonKey'); + } else { + json['$jsonKey'] = { + for (final MapEntry(:key, :value) in value.entries) + key : [ + for (final item in value) + item.json, + ], + }; + } + $sortOnKey +} +'''); + case SimpleDartType(): + switch (valueType.typeName) { + case 'Object': + if (valueType.isNullable) { + buffer.writeln(''' +Map? get $fieldName => json.optionalMap('$jsonKey'); + +set $setterName(Map? value) { + if (value == null) { + json.remove('$jsonKey'); + } else { + json['$jsonKey'] = value; + } + $sortOnKey +} +'''); + } else { + throw UnimplementedError(valueType.toString()); + } + default: + throw UnimplementedError(valueType.toString()); + } + default: + throw UnimplementedError(valueType.toString()); + } + } + + void _generateListTypeGetterSetter( + StringBuffer buffer, + PropertyInfo property, + ListDartType dartType, + String jsonKey, + String setterName, + String sortOnKey, + ) { + final fieldName = property.name; + final itemType = dartType.itemType; + final typeName = itemType.toString(); + final required = property.isRequired; + + switch (itemType) { + case ClassDartType(): + if (required) { + throw UnimplementedError('Expected an optional property.'); + } + buffer.writeln(''' +List<$typeName>? get $fieldName { + final list_ = json.optionalList('$jsonKey')?.cast>(); + if(list_ == null){ + return null; + } + final result = <$typeName>[]; + for(final item in list_){ + result.add($typeName.fromJson(item)); + } + return result; +} + +set $setterName(List<$typeName>? value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = [ + for (final item in value) + item.json + ]; + } + $sortOnKey +} +'''); + case SimpleDartType(): + switch (itemType.typeName) { + case 'String': + if (required) { + buffer.writeln(''' +List get $fieldName => json.stringList('$jsonKey'); + +set $setterName(List value) { + json['$jsonKey'] = value; + $sortOnKey +} +'''); + } else { + buffer.writeln(''' +List? get $fieldName => json.optionalStringList('$jsonKey'); + +set $setterName(List? value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value; + } + $sortOnKey +} +'''); + } + default: + throw UnimplementedError(itemType.toString()); + } + case UriDartType(): + if (required) { + buffer.writeln(''' +List get $fieldName => json.pathList('$jsonKey'); + +set $setterName(List value) { + json['$jsonKey'] = value; + $sortOnKey +} +'''); + } else { + buffer.writeln(''' +List? get $fieldName => json.optionalPathList('$jsonKey'); + +set $setterName(List? value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value.toJson(); + } + $sortOnKey +} +'''); + } + default: + throw UnimplementedError(itemType.toString()); + } + } + + void _generateUriTypeGetterSetter( + StringBuffer buffer, + PropertyInfo property, + UriDartType dartType, + String jsonKey, + String setterName, + String sortOnKey, + ) { + final fieldName = property.name; + final required = property.isRequired; + if (required) { + buffer.writeln(''' +Uri get $fieldName => json.path('$jsonKey'); + +set $setterName(Uri value){ + json['$jsonKey'] = value.toFilePath(); + $sortOnKey +} +'''); + } else { + buffer.writeln(''' +Uri? get $fieldName => json.optionalPath('$jsonKey'); + +set $setterName(Uri? value) { + if (value == null) { + json.remove('$jsonKey'); + } + else { + json['$jsonKey'] = value.toFilePath(); + } + $sortOnKey +} +'''); + } + } +} + +String _wrapBracesIfNotEmpty(String input) => + input.isEmpty ? input : '{$input}'; + +String _wrapInBracesOrSemicolon(String input) => + input.isEmpty ? ';' : '{ $input }'; diff --git a/pkgs/json_syntax_generator/lib/src/model/class_info.dart b/pkgs/json_syntax_generator/lib/src/model/class_info.dart new file mode 100644 index 0000000000..e4574fbf39 --- /dev/null +++ b/pkgs/json_syntax_generator/lib/src/model/class_info.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'property_info.dart'; +import 'utils.dart'; + +sealed class ClassInfo { + /// The Dart class name. + final String name; + + /// Reference to the super class. + final ClassInfo? superclass; + + /// References to all subclasses. + /// + /// Constructed lazily on instantiating sub classes. + final List subclasses = []; + + /// The properties of this class. + /// + /// Includes properties that override properties in a super class. Does not + /// include properties not overridden in the super class. + final List properties; + + ClassInfo({required this.name, this.superclass, required this.properties}) { + superclass?.subclasses.add(this); + } + + PropertyInfo? getProperty(String name) => + properties.where((e) => e.name == name).firstOrNull; +} + +class NormalClassInfo extends ClassInfo { + final String? taggedUnionKey; + + NormalClassInfo({ + required super.name, + super.superclass, + required super.properties, + required this.taggedUnionKey, + }) : super(); + + @override + String toString() { + final propertiesString = properties + .map((p) => indentLines(p.toString(), level: 2)) + .join(',\n'); + return ''' +$runtimeType( + name: $name, + superclassName: ${superclass?.name}, + subclassNames: [ ${subclasses.map((e) => e.name).join(', ')} ] + properties: [ +$propertiesString + ], + taggedUnionKey: $taggedUnionKey +)'''; + } +} + +class EnumClassInfo extends ClassInfo { + final List enumValues; + final bool isOpen; + + EnumClassInfo({ + required super.name, + required this.enumValues, + required this.isOpen, + }) : super(properties: const []); + + @override + String toString() { + final enumValuesString = enumValues + .map((p) => indentLines(p.toString(), level: 2)) + .join(',\n'); + return ''' +$runtimeType( + name: $name, + enumValues: [ +$enumValuesString + ], + isOpen: $isOpen +)'''; + } +} + +class EnumValue { + final String jsonValue; + final String name; + + EnumValue({required this.jsonValue, required this.name}); + + @override + String toString() => ''' +$runtimeType( + name: $name, + jsonValue: $jsonValue +)'''; +} diff --git a/pkgs/json_syntax_generator/lib/src/model/dart_type.dart b/pkgs/json_syntax_generator/lib/src/model/dart_type.dart new file mode 100644 index 0000000000..17b2fd430a --- /dev/null +++ b/pkgs/json_syntax_generator/lib/src/model/dart_type.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'class_info.dart'; + +sealed class DartType { + final bool isNullable; + + const DartType({required this.isNullable}); + + @override + String toString() { + final typeString = toNonNullableString(); + return isNullable ? '$typeString?' : typeString; + } + + String toNonNullableString(); +} + +/// A simple Dart type. +/// +/// This is 'bool', 'int', 'String', or 'Object'. +class SimpleDartType extends DartType { + /// This is 'bool', 'int', 'String', or 'Object'. + final String typeName; + + const SimpleDartType({required this.typeName, required super.isNullable}); + + @override + String toNonNullableString() => typeName; +} + +class ClassDartType extends DartType { + final ClassInfo classInfo; + + const ClassDartType({required this.classInfo, required super.isNullable}); + + @override + String toNonNullableString() => classInfo.name; +} + +class ListDartType extends DartType { + final DartType itemType; + + ListDartType({required this.itemType, required super.isNullable}); + + @override + String toNonNullableString() => 'List<$itemType>'; +} + +class MapDartType extends DartType { + final DartType keyType; + final DartType valueType; + + MapDartType({ + this.keyType = const SimpleDartType(typeName: 'String', isNullable: false), + required this.valueType, + required super.isNullable, + }); + + @override + String toNonNullableString() => 'Map<$keyType, $valueType>'; +} + +class UriDartType extends DartType { + UriDartType({required super.isNullable}); + + @override + String toNonNullableString() => 'Uri'; +} diff --git a/pkgs/json_syntax_generator/lib/src/model/property_info.dart b/pkgs/json_syntax_generator/lib/src/model/property_info.dart new file mode 100644 index 0000000000..ac13cc174d --- /dev/null +++ b/pkgs/json_syntax_generator/lib/src/model/property_info.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../parser/schema_analyzer.dart'; +import 'dart_type.dart'; + +class PropertyInfo { + /// The Dart getter and setter name. + final String name; + + /// The key in the json object for this property. + final String jsonKey; + + /// The Dart type for this property. + final DartType type; + + /// Whether this property overrides a super property. + /// + /// Overrides must have a more specific type. This means no setter for this + /// property will be generated on subclasses. A more specific getter, and a + /// more specific constructor param are generated. + final bool isOverride; + + /// Whether the setter is private. + /// + /// By default, setters are hidden. Constructors are visible and constructors + /// have required parameters for all required fields. This force the generate + /// syntax API user to provide all required fields. + /// + /// Some use cases require public setters as the object is constructed peace + /// meal. See [SchemaAnalyzer.publicSetters]. + final bool setterPrivate; + + bool get isRequired => !type.isNullable; + + PropertyInfo({ + required this.name, + required this.jsonKey, + required this.type, + this.isOverride = false, + this.setterPrivate = true, + }); + + @override + String toString() => ''' +PropertyInfo( + name: $name, + jsonKey: $jsonKey, + type: $type, + isOverride: $isOverride, + setterPrivate: $setterPrivate, + isRequired: $isRequired +)'''; +} diff --git a/pkgs/json_syntax_generator/lib/src/model/schema_info.dart b/pkgs/json_syntax_generator/lib/src/model/schema_info.dart new file mode 100644 index 0000000000..583ed7acbc --- /dev/null +++ b/pkgs/json_syntax_generator/lib/src/model/schema_info.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'class_info.dart'; +import 'utils.dart'; + +/// Information extracted from a JSON schema which can be used to generate Dart +/// code. +class SchemaInfo { + final List classes; + + SchemaInfo({required this.classes}); + + @override + String toString() { + final classesString = classes + .map((c) => indentLines(c.toString(), level: 2)) + .join(',\n'); + return ''' +SchemaInfo( + classes: [ +$classesString + ] +)'''; + } +} diff --git a/pkgs/json_syntax_generator/lib/src/model/utils.dart b/pkgs/json_syntax_generator/lib/src/model/utils.dart new file mode 100644 index 0000000000..eb0b05a9af --- /dev/null +++ b/pkgs/json_syntax_generator/lib/src/model/utils.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +String indentLines(String text, {int level = 1, int spacesPerLevel = 2}) { + final indent = ' ' * (level * spacesPerLevel); + return text.split('\n').map((line) => '$indent$line').join('\n'); +} diff --git a/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart b/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart new file mode 100644 index 0000000000..4f68ebf6fa --- /dev/null +++ b/pkgs/json_syntax_generator/lib/src/parser/schema_analyzer.dart @@ -0,0 +1,659 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:json_schema/json_schema.dart'; + +import '../model/class_info.dart'; +import '../model/dart_type.dart'; +import '../model/property_info.dart'; +import '../model/schema_info.dart'; + +/// Analyzes a JSON schema and extracts information to a [SchemaInfo]. +/// +/// This analyzer makes decisions on how code should be generated and encodes +/// them in the [SchemaInfo]. This enables the code generator to be plain and +/// simple. +/// +/// It supports the following features: +/// * Subclassing. JSON definitions refering with an `allOf` to another JSON +/// definition with a different name are interpreted as a subclass relation. +/// * Open enums. JSON definitions with an `anyOf` of `const`s and `type` +/// `string` are interpreted as an open enum. +/// * Tagged unions. JSON definitions with an `if`-`properties`-`type`-`const` +/// and `then`-`properties` are interpreted as tagged unions. +/// +/// Code geneneration decisions made in this class: +/// * Naming according to "effective Dart". This class expects a JSON schema +/// with snake-cased keys and produces [SchemaInfo] with camel-cased Dart +/// names and types. +/// * Renaming with [capitalizationOverrides]. +/// * Whether setters are public or not with [publicSetters]. +/// * Output sorting alphabetically or with [classSorting]. +class SchemaAnalyzer { + final JsonSchema schema; + + /// Overriding configuration for capitalization of camel cased strings. + final Map capitalizationOverrides; + + /// Optional custom ordering for the output classes. + /// + /// If `null`, then classes are sorted alphabetically. + final List? classSorting; + + /// Generate public setters for these class names. + final List publicSetters; + + SchemaAnalyzer( + this.schema, { + this.capitalizationOverrides = const {}, + this.classSorting, + this.publicSetters = const [], + }); + + /// Accumulator for all classes during the analysis. + /// + /// Because classes can have properties typed by other classes or subclass + /// other classes, these classes are looked up by name during the analysis. + /// The analysis ensures to add the classes in order: properties before main + /// class and super classes before sub classes. + final _classes = {}; + + SchemaInfo analyze() { + _classes.clear(); + final schemas = JsonSchemas(schema); + for (final definitionKey in schemas.definitionKeys) { + final definitionSchemas = schemas.getDefinition(definitionKey); + if (definitionSchemas.generateOpenEnum) { + _analyzeEnumClass(definitionSchemas, name: definitionKey); + } else if (definitionSchemas.generateClass) { + _analyzeClass(definitionSchemas, name: definitionKey); + } + } + if (classSorting != null) { + _classes.sortOnKey(keysSorted: classSorting); + } else { + _classes.sortOnKey(); + } + return SchemaInfo(classes: _classes.values.toList()); + } + + void _analyzeEnumClass(JsonSchemas schemas, {String? name}) { + if (schemas.type != SchemaType.string) { + throw UnimplementedError(schemas.type.toString()); + } + var typeName = schemas.className; + typeName ??= _ucFirst(_snakeToCamelCase(name!)); + if (_classes[typeName] != null) return; // Already analyzed. + + final anyOf = schemas.anyOfs.single; + final values = + anyOf.map((e) => e.constValue).whereType().toList()..sort(); + _classes[typeName] = EnumClassInfo( + name: typeName, + enumValues: [ + for (final value in values) + EnumValue(name: _snakeToCamelCase(value), jsonValue: value), + ], + isOpen: schemas.generateOpenEnum, + ); + } + + void _analyzeClass( + JsonSchemas schemas, { + String? name, + ClassInfo? superclass, + String? identifyingSubtype, + }) { + var typeName = schemas.className; + if (_classes[typeName] != null) return; // Already analyzed. + + typeName ??= _ucFirst(_snakeToCamelCase(name!)); + final properties = []; + + if (superclass == null) { + final superClassSchemas = schemas.superClassSchemas; + if (superClassSchemas != null) { + _analyzeClass(superClassSchemas); + } + final superClassName = schemas.superClassName; + if (superClassName != null) { + superclass = _classes[superClassName]!; + } + } + final superSchemas = schemas.superClassSchemas; + final propertyKeys = schemas.propertyKeys; + final settersPrivate = !publicSetters.contains(typeName); + for (final propertyKey in propertyKeys) { + if (propertyKey == r'$schema') continue; + final propertySchemas = schemas.property(propertyKey); + final required = schemas.propertyRequired(propertyKey); + final allowEnum = !schemas.generateSubClasses; + final parentPropertySchemas = superSchemas?.property(propertyKey); + final dartType = _analyzeDartType( + propertyKey, + propertySchemas, + required, + allowEnum: allowEnum, + ); + final fieldName = _snakeToCamelCase(propertyKey); + final isOverride = + parentPropertySchemas != null && + (parentPropertySchemas?.type != null || + propertySchemas.className != null); + if (parentPropertySchemas == null || + parentPropertySchemas.className != propertySchemas.className || + propertySchemas.type != parentPropertySchemas.type) { + properties.add( + PropertyInfo( + name: fieldName, + jsonKey: propertyKey, + type: dartType, + isOverride: isOverride, + setterPrivate: settersPrivate, + ), + ); + } + } + + final classInfo = NormalClassInfo( + name: typeName, + superclass: superclass, + properties: properties, + taggedUnionKey: identifyingSubtype, + ); + _classes[typeName] = classInfo; + if (schemas.generateSubClasses) { + _analyzeSubClasses(schemas, name: name, superclass: classInfo); + return; + } + } + + void _analyzeSubClasses( + JsonSchemas schemas, { + String? name, + ClassInfo? superclass, + }) { + final typeName = schemas.className; + + final propertyKey = schemas.propertyKeys.single; + final typeProperty = schemas.property(propertyKey); + final anyOf = typeProperty.anyOfs.single; + final subTypes = + anyOf.map((e) => e.constValue).whereType().toList(); + for (final subType in subTypes) { + final ifThenSchemas = schemas.ifThenSchemas; + final subTypeNameShort = _ucFirst(_snakeToCamelCase(subType)); + final subTypeName = '$subTypeNameShort$typeName'; + JsonSchemas? subtypeSchema; + for (final (ifSchema, thenSchema) in ifThenSchemas) { + if (ifSchema.property(propertyKey).constValue == subType) { + subtypeSchema = thenSchema; + break; + } + } + if (subtypeSchema != null) { + _analyzeClass( + subtypeSchema, + name: subTypeName, + superclass: superclass, + identifyingSubtype: subType, + ); + } else { + // This is a tagged union without any defined properties. + _classes[subTypeName] = NormalClassInfo( + name: subTypeName, + superclass: superclass, + properties: [], + taggedUnionKey: subType, + ); + } + } + } + + DartType _analyzeDartType( + String propertyKey, + JsonSchemas schemas, + bool required, { + bool allowEnum = true, + }) { + final type = schemas.type; + final DartType dartType; + switch (type) { + case SchemaType.boolean: + dartType = SimpleDartType(typeName: 'bool', isNullable: !required); + case SchemaType.integer: + dartType = SimpleDartType(typeName: 'int', isNullable: !required); + case SchemaType.string: + if (schemas.generateUri) { + dartType = UriDartType(isNullable: !required); + } else if (schemas.generateEnum && allowEnum) { + _analyzeEnumClass(schemas); + final classInfo = _classes[schemas.className]!; + dartType = ClassDartType(classInfo: classInfo, isNullable: !required); + } else { + dartType = SimpleDartType(typeName: 'String', isNullable: !required); + } + case SchemaType.object: + final additionalPropertiesSchema = schemas.additionalPropertiesSchemas; + if (schemas.generateMapOf) { + final additionalPropertiesType = additionalPropertiesSchema.type; + switch (additionalPropertiesType) { + case SchemaType.array: + final items = additionalPropertiesSchema.items; + final itemType = items.type; + switch (itemType) { + case SchemaType.object: + _analyzeClass(items); + final itemClass = _classes[items.className]!; + dartType = MapDartType( + valueType: ListDartType( + itemType: ClassDartType( + classInfo: itemClass, + isNullable: false, + ), + isNullable: false, + ), + isNullable: !required, + ); + default: + throw UnimplementedError(itemType.toString()); + } + case SchemaType.object: + final additionalPropertiesBool = + additionalPropertiesSchema.additionalPropertiesBool; + if (additionalPropertiesBool != true) { + throw UnimplementedError( + 'Expected an object with arbitrary properties.', + ); + } + dartType = MapDartType( + valueType: MapDartType( + valueType: const SimpleDartType( + typeName: 'Object', + isNullable: true, + ), + isNullable: false, + ), + isNullable: !required, + ); + case null: + if (schemas.additionalPropertiesBool != true) { + throw UnimplementedError( + 'Expected an object with arbitrary properties.', + ); + } + dartType = MapDartType( + valueType: const SimpleDartType( + typeName: 'Object', + isNullable: true, + ), + isNullable: !required, + ); + default: + throw UnimplementedError(additionalPropertiesType.toString()); + } + } else { + var typeName = schemas.className; + typeName ??= _ucFirst(_snakeToCamelCase(propertyKey)); + _analyzeClass(schemas, name: typeName); + final classInfo = _classes[typeName]!; + dartType = ClassDartType(classInfo: classInfo, isNullable: !required); + } + case SchemaType.array: + final items = schemas.items; + final itemType = items.type; + switch (itemType) { + case SchemaType.string: + if (items.patterns.isNotEmpty) { + dartType = ListDartType( + itemType: UriDartType(isNullable: false), + isNullable: !required, + ); + } else { + dartType = ListDartType( + itemType: const SimpleDartType( + typeName: 'String', + isNullable: false, + ), + isNullable: !required, + ); + } + case SchemaType.object: + final typeName = items.className!; + _analyzeClass(items); + final classInfo = _classes[typeName]!; + dartType = ListDartType( + itemType: ClassDartType(classInfo: classInfo, isNullable: false), + isNullable: !required, + ); + default: + throw UnimplementedError(itemType.toString()); + } + + default: + throw UnimplementedError(type.toString()); + } + return dartType; + } + + String _snakeToCamelCase(String string) { + if (string.isEmpty) { + return ''; + } + + final parts = string.replaceAll('-', '_').split('_'); + + String remapCapitalization(String input) => + capitalizationOverrides[input] ?? input; + + var result = StringBuffer(); + result += remapCapitalization(parts[0]); + + for (var i = 1; i < parts.length; i++) { + if (parts[i].isNotEmpty) { + result += remapCapitalization( + parts[i][0].toUpperCase() + parts[i].substring(1), + ); + } + } + + return result.toString(); + } +} + +String _ucFirst(String str) { + if (str.isEmpty) { + return ''; + } + return str[0].toUpperCase() + str.substring(1); +} + +/// A view on [JsonSchema]s that extend/override each other. +extension type JsonSchemas._(List _schemas) { + factory JsonSchemas(JsonSchema schema) => JsonSchemas._([schema])._flatten(); + + JsonSchemas _flatten() { + final flattened = []; + final queue = [..._schemas]; + while (queue.isNotEmpty) { + final item = queue.first; + queue.removeAt(0); + if (flattened.contains(item)) { + continue; + } + flattened.add(item); + queue.addAll(item.allOf); + final ref = item.ref; + if (ref != null) { + queue.add(item.resolvePath(ref)); + } + } + final result = JsonSchemas._(flattened); + return result; + } + + List get propertyKeys => + {for (final schema in _schemas) ...schema.properties.keys}.toList() + ..sort(); + + JsonSchemas property(String key) { + final propertySchemas = []; + for (final schema in _schemas) { + final propertySchema = schema.properties[key]; + if (propertySchema != null) { + propertySchemas.add(propertySchema); + } + } + return JsonSchemas._(propertySchemas)._flatten(); + } + + bool propertyRequired(String? property) => + _schemas.any((e) => e.propertyRequired(property)); + + SchemaType? get type { + final types = {}; + for (final schema in _schemas) { + final schemaTypes = schema.typeList; + if (schemaTypes != null) { + for (final schemaType in schemaTypes) { + if (schemaType != null) types.add(schemaType); + } + } + } + if (types.length > 1) { + throw StateError('Multiple types found'); + } + return types.singleOrNull; + } + + List get patterns { + final patterns = {}; + for (final schema in _schemas) { + final pattern = schema.pattern; + if (pattern != null) { + patterns.add(pattern); + } + } + return patterns.toList(); + } + + JsonSchemas get items { + final items = []; + for (final schema in _schemas) { + final item = schema.items; + if (item != null) { + items.add(item); + } + } + return JsonSchemas._(items)._flatten(); + } + + List get paths { + final paths = {}; + for (final schema in _schemas) { + final path = schema.path; + if (path != null) paths.add(path); + } + return paths.toList(); + } + + List get definitionKeys => + {for (final schema in _schemas) ...schema.definitions.keys}.toList() + ..sort(); + + JsonSchemas getDefinition(String key) { + final definitionSchemas = []; + for (final schema in _schemas) { + final propertySchema = schema.definitions[key]; + if (propertySchema != null) { + definitionSchemas.add(propertySchema); + } + } + return JsonSchemas._(definitionSchemas)._flatten(); + } + + JsonSchemas get additionalPropertiesSchemas { + final schemas = []; + for (final schema in _schemas) { + final additionalPropertiesSchema = schema.additionalPropertiesSchema; + if (additionalPropertiesSchema != null) { + schemas.add(additionalPropertiesSchema); + } + } + return JsonSchemas._(schemas)._flatten(); + } + + bool? get additionalPropertiesBool { + final result = []; + for (final schema in _schemas) { + final additionalPropertiesBool = schema.additionalPropertiesBool; + if (additionalPropertiesBool != null) { + result.add(additionalPropertiesBool); + } + } + if (result.length > 1) { + throw StateError('Both yes and no for additional properties.'); + } + return result.singleOrNull; + } + + bool get isNotEmpty => _schemas.isNotEmpty; + + List> get anyOfs { + final result = >[]; + for (final schema in _schemas) { + final anyOf = schema.anyOf; + final tempResult = []; + for (final option in anyOf) { + tempResult.add(JsonSchemas(option)._flatten()); + } + if (tempResult.isNotEmpty) { + result.add(tempResult); + } + } + return result; + } + + Object? get constValue { + final result = []; + for (final schema in _schemas) { + final item = schema.constValue; + if (item != null) { + result.add(item as Object); + } + } + if (result.length > 1) { + throw UnimplementedError('Conflicting const values.'); + } + return result.singleOrNull; + } + + List get refs { + final result = []; + for (final schema in _schemas) { + final ref = schema.ref; + if (ref != null) { + result.add(ref); + } + } + return result; + } + + List<(JsonSchemas, JsonSchemas)> get ifThenSchemas { + final result = <(JsonSchemas, JsonSchemas)>[]; + for (final schema in _schemas) { + final ifSchema = schema.ifSchema; + final thenSchema = schema.thenSchema; + if (ifSchema != null && thenSchema != null) { + result.add(( + JsonSchemas(ifSchema)._flatten(), + JsonSchemas(thenSchema)._flatten(), + )); + } + } + return result; + } +} + +extension on JsonSchemas { + bool get generateEnum => type == SchemaType.string && anyOfs.isNotEmpty; + + /// A class with opaque members and an `unknown` option. + bool get generateOpenEnum => + generateEnum && anyOfs.single.any((e) => e.type != null); + + /// Generate getters/setters as `Map. + bool get generateMapOf => + type == SchemaType.object && + (additionalPropertiesSchemas.isNotEmpty || + additionalPropertiesBool == true); + + bool get generateSubClasses => + type == SchemaType.object && + propertyKeys.length == 1 && + property(propertyKeys.single).anyOfs.isNotEmpty; + + bool get generateClass => type == SchemaType.object && !generateMapOf; + + /// Generate getters/setters as `Uri`. + bool get generateUri => type == SchemaType.string && patterns.isNotEmpty; + + static String? _pathToClassName(String path) { + if (path.contains('#/definitions/')) { + final splits = path.split('/'); + final indexOf = splits.indexOf('definitions'); + final nameParts = splits.skip(indexOf + 1).where((e) => e.isNotEmpty); + if (nameParts.length == 1 && nameParts.single.startsWithUpperCase()) { + return nameParts.single; + } + } + return null; + } + + /// This is all the inferred class names from definitions in order of + /// traversal. + List get classNames { + final result = []; + for (final path in paths) { + final className = _pathToClassName(path); + if (className != null) { + if (!result.contains(className)) { + result.add(className); + } + } + } + return result; + } + + String? get className => classNames.firstOrNull; + + String? get superClassName { + final names = classNames; + if (names.length == 2) { + return names[1]; + } + if (names.length > 2) { + throw UnimplementedError('Deeper inheritance not implemented.'); + } + return null; + } + + JsonSchemas? get superClassSchemas { + final parentClassName = superClassName; + if (parentClassName == null) { + return null; + } + for (final schema in _schemas) { + final path = schema.path; + if (path == null) continue; + final className = _pathToClassName(path); + if (className != parentClassName) continue; + return JsonSchemas(schema)._flatten(); + } + throw StateError('No super class schema found for $parentClassName.'); + } +} + +extension on String { + bool startsWithUpperCase() { + final codeUnit = codeUnitAt(0); + return codeUnit >= 'A'.codeUnitAt(0) && codeUnit <= 'Z'.codeUnitAt(0); + } +} + +extension on StringBuffer { + StringBuffer operator +(String value) => this..write(value); +} + +extension, V extends Object?> on Map { + void sortOnKey({List? keysSorted}) { + final result = {}; + keysSorted ??= keys.toList()..sort(); + for (final key in keysSorted) { + result[key] = this[key] as V; + } + clear(); + addAll(result); + } +} diff --git a/pkgs/json_syntax_generator/pubspec.yaml b/pkgs/json_syntax_generator/pubspec.yaml new file mode 100644 index 0000000000..2019e2f08b --- /dev/null +++ b/pkgs/json_syntax_generator/pubspec.yaml @@ -0,0 +1,21 @@ +name: json_syntax_generator +version: 0.1.0-wip +repository: https://github.com/dart-lang/native/tree/main/pkgs/json_syntax_generator +description: | + `package:json_syntax_generator` provides a powerful and flexible way to + generate Dart code from JSON schemas. It simplifies the process of working + with JSON data by automatically creating Dart classes that represent the + structure of your JSON data, including support for complex schema features. + +publish_to: none + +environment: + sdk: '>=3.7.0 <4.0.0' + +dependencies: + json_schema: ^5.2.0 + +dev_dependencies: + dart_flutter_team_lints: ^2.1.1 + path: ^1.9.1 + test: ^1.25.15 diff --git a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart index a44d5d54ef..e515eac434 100644 --- a/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart +++ b/pkgs/native_assets_cli/lib/src/code_assets/syntax.g.dart @@ -166,6 +166,7 @@ class NativeCodeAsset extends Asset { } LinkMode get linkMode => LinkMode.fromJson(json.map$('link_mode')); + set _linkMode(LinkMode value) => json['link_mode'] = value.json; OS get os => OS.fromJson(json.string('os')); @@ -525,6 +526,9 @@ class DynamicLoadingBundleLinkMode extends LinkMode { DynamicLoadingBundleLinkMode.fromJson(super.json) : super.fromJson(); DynamicLoadingBundleLinkMode() : super(type: 'dynamic_loading_bundle'); + + @override + String toString() => 'DynamicLoadingBundleLinkMode($json)'; } extension DynamicLoadingBundleLinkModeExtension on LinkMode { @@ -539,6 +543,9 @@ class DynamicLoadingExecutableLinkMode extends LinkMode { DynamicLoadingExecutableLinkMode() : super(type: 'dynamic_loading_executable'); + + @override + String toString() => 'DynamicLoadingExecutableLinkMode($json)'; } extension DynamicLoadingExecutableLinkModeExtension on LinkMode { @@ -553,6 +560,9 @@ class DynamicLoadingProcessLinkMode extends LinkMode { DynamicLoadingProcessLinkMode.fromJson(super.json) : super.fromJson(); DynamicLoadingProcessLinkMode() : super(type: 'dynamic_loading_process'); + + @override + String toString() => 'DynamicLoadingProcessLinkMode($json)'; } extension DynamicLoadingProcessLinkModeExtension on LinkMode { @@ -599,6 +609,9 @@ class StaticLinkMode extends LinkMode { StaticLinkMode.fromJson(super.json) : super.fromJson(); StaticLinkMode() : super(type: 'static'); + + @override + String toString() => 'StaticLinkMode($json)'; } extension StaticLinkModeExtension on LinkMode { diff --git a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart index eb0300a1b9..5bd62885da 100644 --- a/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart +++ b/pkgs/native_assets_cli/lib/src/hook/syntax.g.dart @@ -208,6 +208,7 @@ class HookInput { } Config get config => Config.fromJson(json.map$('config')); + set config(Config value) => json['config'] = value.json; Uri get outDir => json.path('out_dir'); @@ -399,10 +400,6 @@ class LinkOutput extends HookOutput { required super.version, }) : super(); - /// Setup all fields for [LinkOutput] that are not in - /// [HookOutput]. - void setup() {} - @override String toString() => 'LinkOutput($json)'; }