From 20c3585341e4c03d4535b50a3f3a53e1f690a098 Mon Sep 17 00:00:00 2001 From: BreX900 Date: Tue, 2 Jan 2024 22:36:57 +0100 Subject: [PATCH] fix: Fixes --- example/pubspec.yaml | 24 +- .../example/paypal/.openapiignore | 2 + .../example/paypal/build_paypal_api.dart | 29 +++ .../lib/src/builders/build_api_class.dart | 33 +-- .../lib/src/builders/build_schema_class.dart | 25 +- .../lib/src/code_utils/codecs.dart | 99 ++++---- .../src/code_utils/schema_to_reference.dart | 31 +-- .../dart_collection_codec.dart | 2 +- .../fast_immutable_collection_codec.dart | 5 +- .../lib/src/open_api_generate.dart | 1 + .../lib/src/plugins/open_api_ignore.dart | 54 ++++ .../lib/src/utils/file_utils.dart | 235 +++++++++++++++++- .../lib/src/utils/files_contents.dart | 10 +- open_api_client_generator/pubspec.yaml | 3 +- .../tools/files_contents/api_client.dart | 2 +- .../tools/generate_files_contents.dart | 11 +- open_api_spec/lib/src/read_open_api.dart | 2 +- open_api_spec/lib/src/specs/base_specs.dart | 16 +- open_api_spec/lib/src/specs/base_specs.g.dart | 15 +- open_api_spec/lib/src/specs/schema.dart | 18 +- open_api_spec/lib/src/specs/schema.g.dart | 9 +- .../lib/src/specs/security_open_api.dart | 4 +- .../lib/src/specs/security_open_api.g.dart | 9 +- shelf_open_api/lib/shelf_open_api.dart | 4 +- shelf_open_api_generator/build.yaml | 4 - .../lib/shelf_open_api_generator.dart | 14 +- .../lib/src/handlers/route_handler.dart | 2 +- 27 files changed, 500 insertions(+), 163 deletions(-) create mode 100644 open_api_client_generator/example/paypal/.openapiignore create mode 100644 open_api_client_generator/example/paypal/build_paypal_api.dart create mode 100644 open_api_client_generator/lib/src/plugins/open_api_ignore.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 090966f..6cb5852 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,7 +7,7 @@ environment: scripts: # To generate .g files: - build:runner: dart run build_runner watch --delete-conflicting-outputs --verbose + runner:watch: dart run build_runner watch --delete-conflicting-outputs --verbose dependencies: shelf: ^1.4.0 @@ -15,8 +15,7 @@ dependencies: shelf_static: ^1.1.1 shelf_swagger_ui: ^1.0.0+2 - shelf_open_api: - path: ../shelf_open_api + shelf_open_api: ^1.1.0 mek_data_class: ^1.0.1 json_annotation: ^4.8.0 @@ -28,13 +27,12 @@ dev_dependencies: mek_data_class_generator: ^1.1.1 json_serializable: ^6.6.1 shelf_router_generator: ^1.0.5 - shelf_open_api_generator: - path: ../shelf_open_api_generator - -dependency_overrides: - open_api_specification: - path: ../open_api_spec - shelf_open_api: - path: ../shelf_open_api - shelf_open_api_generator: - path: ../shelf_open_api_generator + shelf_open_api_generator: ^2.0.0 + +#dependency_overrides: +# open_api_specification: +# path: ../open_api_spec +# shelf_open_api: +# path: ../shelf_open_api +# shelf_open_api_generator: +# path: ../shelf_open_api_generator diff --git a/open_api_client_generator/example/paypal/.openapiignore b/open_api_client_generator/example/paypal/.openapiignore new file mode 100644 index 0000000..b1c15bd --- /dev/null +++ b/open_api_client_generator/example/paypal/.openapiignore @@ -0,0 +1,2 @@ +/v2/checkout/orders +/v2/checkout/orders/{id}/capture \ No newline at end of file diff --git a/open_api_client_generator/example/paypal/build_paypal_api.dart b/open_api_client_generator/example/paypal/build_paypal_api.dart new file mode 100644 index 0000000..cae42dd --- /dev/null +++ b/open_api_client_generator/example/paypal/build_paypal_api.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:open_api_client_generator/open_api_client_generator.dart'; +import 'package:open_api_client_generator/src/plugins/open_api_ignore.dart'; + +void main(List args) async { + const root = './example/paypal'; + final codeDir = Directory('$root/api'); + if (!codeDir.existsSync()) codeDir.createSync(); + + final options = Options( + // input: Uri.file('$root/specification/index.yml'), + input: Uri.parse( + 'https://raw.githubusercontent.com/paypal/paypal-rest-api-specifications/main/openapi/checkout_orders_v2.json'), + outputFolder: codeDir.path, + apiClassName: 'PayPalApi', + ); + await generateApi( + options: options, + clientCodec: const DioClientCodec(), + serializationCodec: const JsonSerializableSerializationCodec( + classFieldRename: FieldRename.snake, + ), + plugins: [ + OpenApiIgnore(overrideFilePath: '$root/.openapiignore'), + WriteOpenApiPlugin(options: options) + ], + ); +} diff --git a/open_api_client_generator/lib/src/builders/build_api_class.dart b/open_api_client_generator/lib/src/builders/build_api_class.dart index 61e1252..dd2c97d 100644 --- a/open_api_client_generator/lib/src/builders/build_api_class.dart +++ b/open_api_client_generator/lib/src/builders/build_api_class.dart @@ -33,13 +33,13 @@ class BuildApiClass with ContextMixin { required String name, required String path, }) { - if (id != null) return id; + if (id != null) return codecs.encodeName(id); return '$name${path.replaceAll('/', '_').replaceAll('{', '').replaceAll('}', '').pascalCase}'; } String encodePath(String path) { - return path.replaceAllMapped(RegExp(r'(\{\w*\})'), (match) { - return '\$${codecs.encodeFieldName(match.group(0)!)}'; + return path.replaceAllMapped(RegExp(r'\{(\w*)\}'), (match) { + return '\$${codecs.encodeName(match.group(1)!)}'; }); } @@ -56,7 +56,10 @@ class BuildApiClass with ContextMixin { if (queryParameters.isNotEmpty) { b.write('final _queryParameters = {\n'); b.writeAll(queryParameters.map((e) { - return '${codecs.encodeDartValue(e.name)}: ${e.name.camelCase}${collectionCodec.encodeToCore(schemaToType(e.schema!))},\n'; + final key = codecs.encodeDartValue(e.name); + final varName = codecs.encodeName(e.name); + final varEncoder = collectionCodec.encodeToCore(ref(e.schema!).toNullable(!e.required)); + return '$key: $varName$varEncoder,\n'; })); b.write('};\n'); } @@ -154,7 +157,7 @@ class BuildApiClass with ContextMixin { description: operation.description, params: operation.parameters.expand((param) { return Docs.documentField( - name: codecs.encodeFieldName(param.name), + name: codecs.encodeName(param.name), description: param.description, example: param.example, ); @@ -164,8 +167,8 @@ class BuildApiClass with ContextMixin { ..name = methodName ..requiredParameters.addAll(pathParameters.map((param) { return Parameter((b) => b - ..type = schemaToType(param.schema!) //.toNull(!param.required) - ..name = codecs.encodeFieldName(param.name)); + ..type = ref(param.schema!).toNullable(!param.required) + ..name = codecs.encodeName(param.name)); })) ..requiredParameters.addAll([ if (request != null && requestSchema != null) @@ -173,15 +176,13 @@ class BuildApiClass with ContextMixin { ..type = requestType ..name = '_request') ]) - ..optionalParameters.addAll([ - ...queryParameters.map((e) { - return Parameter((b) => b - ..named = true - ..required = e.required - ..type = schemaToType(e.schema!).toNullable(!e.required) - ..name = e.name.camelCase); - }), - ]) + ..optionalParameters.addAll(queryParameters.map((e) { + return Parameter((b) => b + ..named = true + ..required = e.required + ..type = ref(e.schema!).toNullable(!e.required) + ..name = codecs.encodeName(e.name)); + })) ..modifier = MethodModifier.async ..body = Code(operationCode)); } diff --git a/open_api_client_generator/lib/src/builders/build_schema_class.dart b/open_api_client_generator/lib/src/builders/build_schema_class.dart index 0482dec..8fa6176 100644 --- a/open_api_client_generator/lib/src/builders/build_schema_class.dart +++ b/open_api_client_generator/lib/src/builders/build_schema_class.dart @@ -20,7 +20,7 @@ class BuildSchemaClass with ContextMixin { Reference call(String name, SchemaOpenApi schema) { // ignore: parameter_assignments - name = schema.title ?? name; + name = schema.name ?? name; final cacheEntry = _cache[name]; if (cacheEntry != null) return cacheEntry.type; @@ -35,14 +35,6 @@ class BuildSchemaClass with ContextMixin { return builtSchema.type; } - Iterable _buildImplements(Iterable refs) sync* { - for (final ref in refs) { - final reference = Reference(codecs.encodeType(ref.split('/').last)); - // if (ref.split('/').last == 'Bundle') print('$schemaOrRef'); - yield reference; - } - } - _BuiltSchema _build(String name, SchemaOpenApi schema) { final docs = Docs.format(Docs.documentClass( description: schema.description, @@ -53,7 +45,7 @@ class BuildSchemaClass with ContextMixin { if (items != null) { return _BuiltSchema( // docs: docs.followedBy(built.docs ?? const []), - type: schemaToType(schema), + type: ref(schema), children: {name: items}, ); } @@ -81,7 +73,8 @@ class BuildSchemaClass with ContextMixin { if (schema.isClass) { final properties = schema.allProperties; final allOf = schema.allOf ?? const []; - final implements = allOf.map((e) => e.title).nonNulls; + final implements = allOf.where((e) => e.name != null).toList(); + final implementsNames = implements.map((e) => codecs.encodeType(e.name!)).toList(); final className = codecs.encodeType(name); @@ -91,7 +84,7 @@ class BuildSchemaClass with ContextMixin { schema: schema, docs: docs, name: className, - implements: _buildImplements(implements).map((e) => e.symbol!).toList(), + implements: implementsNames, fields: properties.entries.map((entry) { final MapEntry(key: name, value: prop) = entry; @@ -99,15 +92,15 @@ class BuildSchemaClass with ContextMixin { key: name, docs: const [], // TODO: prop.docs ?? isRequired: schema.isRequired(name), - type: schemaToType(prop).toNullable(schema.canNull(name, properties[name]!)), - name: codecs.encodeFieldName(name), + type: ref(prop).toNullable(schema.canNull(name, properties[name]!)), + name: codecs.encodeName(name), ); }).toList(), ), - children: properties, + children: {...Map.fromEntries(implements.map((e) => MapEntry(e.name!, e))), ...properties}, ); } - final reference = schemaToType(schema); + final reference = ref(schema); if (!reference.isDartCore) { // ignore: avoid_print print('$name $schema'); diff --git a/open_api_client_generator/lib/src/code_utils/codecs.dart b/open_api_client_generator/lib/src/code_utils/codecs.dart index 8afc780..e92b2a7 100644 --- a/open_api_client_generator/lib/src/code_utils/codecs.dart +++ b/open_api_client_generator/lib/src/code_utils/codecs.dart @@ -7,56 +7,31 @@ class Codecs { const Codecs(); @protected - String encodeType(String name) => name; - - static final _keywords = { - 'else', - 'enum', - 'in', - 'assert', - 'super', - 'extends', - 'is', - 'switch', - 'break', - 'this', - 'case', - 'throw', - 'catch', - 'false', - 'new', - 'true', - 'class', - 'final', - 'null', - 'try', - 'const', - 'finally', - 'continue', - 'for', - 'var', - 'void', - 'default', - 'while', - 'rethrow', - 'with', - 'do', - 'if', - 'return', - }; - - @protected - String encodeFieldName(String str) { - str = str.replaceAll('@', ''); - return _keywords.contains(str) ? '$str\$' : str; + String encodeType(String name) { + // Don't start the string with a number + if (name.startsWith(RegExp('[0-9]'))) name = '\$$name'; + return name; } + String encodeName(String str) => _encodeName(str); + String encodeDartValue(Object value) { if (value is String) return "'${value.replaceAll(r'$', r'\$')}'"; return '$value'; } - String encodeEnumValue(Object value) => value is String ? encodeFieldName(value) : 'vl$value'; + String encodeEnumValue(Object value) => value is String ? encodeName(value) : 'vl$value'; + + /// Encode variable name, field name and method name + @protected + String _encodeName(String str) { + if (_keywords.contains(str)) return '$str\$'; + // Remove symbols + str = str.replaceAllMapped(RegExp(r'([^0-9\w])'), (match) => '_'); + // Don't start the string with a number + if (str.startsWith(RegExp('[0-9]'))) str = '\$$str'; + return str; + } } class ApiCodecs extends Codecs { @@ -69,5 +44,41 @@ class ApiCodecs extends Codecs { '${removeDiacritics(super.encodeType(name)).pascalCase}${options.dataClassesPostfix ?? ''}'; @override - String encodeFieldName(String str) => super.encodeFieldName(removeDiacritics(str).camelCase); + String _encodeName(String str) => super._encodeName(removeDiacritics(str).camelCase); } + +final _keywords = { + 'else', + 'enum', + 'in', + 'assert', + 'super', + 'extends', + 'is', + 'switch', + 'break', + 'this', + 'case', + 'throw', + 'catch', + 'false', + 'new', + 'true', + 'class', + 'final', + 'null', + 'try', + 'const', + 'finally', + 'continue', + 'for', + 'var', + 'void', + 'default', + 'while', + 'rethrow', + 'with', + 'do', + 'if', + 'return', +}; diff --git a/open_api_client_generator/lib/src/code_utils/schema_to_reference.dart b/open_api_client_generator/lib/src/code_utils/schema_to_reference.dart index 0b41346..fbc31e5 100644 --- a/open_api_client_generator/lib/src/code_utils/schema_to_reference.dart +++ b/open_api_client_generator/lib/src/code_utils/schema_to_reference.dart @@ -3,21 +3,25 @@ import 'package:open_api_client_generator/src/code_utils/reference_utils.dart'; import 'package:open_api_client_generator/src/options/context.dart'; import 'package:open_api_specification/open_api_spec.dart'; -extension SchemaToType on ContextMixin { - Reference schemaToType(SchemaOpenApi schema, {Reference? target}) { +extension SchemaToRef on ContextMixin { + Reference ref(SchemaOpenApi schema) { + if (schema.isEnum) return Reference(codecs.encodeType(schema.name!)); + switch (schema.format) { case FormatOpenApi.int32: case FormatOpenApi.int64: - return target ?? References.int; + return References.int; case FormatOpenApi.float: case FormatOpenApi.double: - return target ?? References.double; + return References.double; + case FormatOpenApi.string: + return References.string; case FormatOpenApi.date: case FormatOpenApi.dateTime: - return target ?? References.dateTime; + return References.dateTime; case FormatOpenApi.uuid: case FormatOpenApi.email: - return target ?? References.string; + return References.string; case FormatOpenApi.url: case FormatOpenApi.uri: @@ -32,22 +36,21 @@ extension SchemaToType on ContextMixin { switch (schema.type) { case TypeOpenApi.boolean: - return target ?? References.bool; + return References.bool; case TypeOpenApi.integer: - return target ?? References.int; + return References.int; case TypeOpenApi.number: - return target ?? References.num; + return References.num; case TypeOpenApi.string: - return target ?? References.string; + return References.string; case TypeOpenApi.array: - return References.list(schemaToType(schema.items!)); + return References.list(ref(schema.items!)); case TypeOpenApi.object: - if (schema.title != null) return Reference(schema.title!); + if (schema.name != null) return Reference(codecs.encodeType(schema.name!)); - final additionalProperties = schema.additionalProperties; return References.map( key: References.string, - value: additionalProperties != null ? schemaToType(additionalProperties) : null, + value: schema.additionalProperties != null ? ref(schema.additionalProperties!) : null, ); case null: return References.jsonValue; diff --git a/open_api_client_generator/lib/src/collection_codecs/dart_collection_codec.dart b/open_api_client_generator/lib/src/collection_codecs/dart_collection_codec.dart index 2ee8014..7a6ab2a 100644 --- a/open_api_client_generator/lib/src/collection_codecs/dart_collection_codec.dart +++ b/open_api_client_generator/lib/src/collection_codecs/dart_collection_codec.dart @@ -13,7 +13,7 @@ class DartCollectionCodec extends CollectionCodec { @override String encodeToCore(Reference type) { - if (type.isList) return '.toList()'; + // if (type.isList) return '.toList()'; return ''; } } diff --git a/open_api_client_generator/lib/src/collection_codecs/fast_immutable_collection_codec.dart b/open_api_client_generator/lib/src/collection_codecs/fast_immutable_collection_codec.dart index 0ba8ca7..748bdc0 100644 --- a/open_api_client_generator/lib/src/collection_codecs/fast_immutable_collection_codec.dart +++ b/open_api_client_generator/lib/src/collection_codecs/fast_immutable_collection_codec.dart @@ -17,8 +17,9 @@ class FastImmutableCollectionCodec extends CollectionCodecBase { @override String encodeToCore(Reference type) { - if (type.isList) return '.toList()'; - if (type.isMap) return '.unlockView'; + final questionOrEmpty = type.isNullable ? '?' : ''; + if (type.isList) return '$questionOrEmpty.toList()'; + if (type.isMap) return '$questionOrEmpty.unlockView'; return ''; } diff --git a/open_api_client_generator/lib/src/open_api_generate.dart b/open_api_client_generator/lib/src/open_api_generate.dart index 4b87f22..3559f1a 100644 --- a/open_api_client_generator/lib/src/open_api_generate.dart +++ b/open_api_client_generator/lib/src/open_api_generate.dart @@ -99,6 +99,7 @@ Future generateApi({ 'unnecessary_brace_in_string_interps', 'no_leading_underscores_for_local_identifiers', 'always_use_package_imports', + 'cast_nullable_to_non_nullable', ]) ..directives.add(Directive.part('${path_.basenameWithoutExtension(apiFileName)}.g.dart')) ..body.add(apiSpec) diff --git a/open_api_client_generator/lib/src/plugins/open_api_ignore.dart b/open_api_client_generator/lib/src/plugins/open_api_ignore.dart new file mode 100644 index 0000000..79bd2aa --- /dev/null +++ b/open_api_client_generator/lib/src/plugins/open_api_ignore.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:open_api_client_generator/src/options/options.dart'; +import 'package:open_api_client_generator/src/plugins/plugin.dart'; +import 'package:yaml/yaml.dart'; + +class OpenApiIgnore with Plugin { + final String? overrideFilePath; + + OpenApiIgnore({ + this.overrideFilePath, + }); + + @override + Map onSpecifications(Map specifications) { + final overrideFile = File(overrideFilePath ?? '.openapiignore'); + if (overrideFilePath == null && !overrideFile.existsSync()) return specifications; + + final lines = overrideFile.readAsStringSync().split('\n'); + final paths = specifications['paths'] as Map; + return { + ...specifications, + 'paths': Map.fromEntries(lines.map((e) => MapEntry(e, paths[e]!))), + }; + } +} + +class OpenApiOverride with Plugin { + final Options options; + final String overrideFilePath; + + OpenApiOverride({ + required this.options, + required this.overrideFilePath, + }); + + @override + Map onSpecifications(Map specifications) { + final override = loadYaml(File(overrideFilePath).readAsStringSync()); + return _merge(specifications, override)! as Map; + } + + static Object? _merge(Object? original, Object? override) { + if (override is Map) { + if (override.containsKey(r'$ref')) return override; + if (original is Map) { + return Map.fromEntries({...override.keys, ...original.keys}.map((key) { + return MapEntry(key, _merge(original[key], override[key])); + })); + } + } + return override ?? original; + } +} diff --git a/open_api_client_generator/lib/src/utils/file_utils.dart b/open_api_client_generator/lib/src/utils/file_utils.dart index 327cdb9..6b22b4d 100644 --- a/open_api_client_generator/lib/src/utils/file_utils.dart +++ b/open_api_client_generator/lib/src/utils/file_utils.dart @@ -1,8 +1,9 @@ -import 'package:json2yaml/json2yaml.dart'; +import 'dart:convert'; abstract class FileUtils { - static String yamlFrom(Map json) { - return json2yaml(_jsonToYaml(json)); + static String yamlFrom(Object? json) { + return YamlEncoder(toEncodable: (vl) => vl?.toJson()).convert(json); + // return json2yaml(_jsonToYaml(json)); } static final _badKeys = RegExp('[*#]'); @@ -22,3 +23,231 @@ abstract class FileUtils { return node; } } + +/// Why has custom implementation? +/// fhir_yaml: ^0.9.0 package not convert string with "\n" to multiline string +/// yaml_writer: ^1.0.2 package add empty "random" line on yaml generated +class YamlEncoder extends Converter { + final int indent; + final bool shouldMultilineStringInBlock; + final int? maxStringLineWidth; // TODO: Add support to maxStringLineWidth + final Object? Function(dynamic)? toEncodable; + + const YamlEncoder({ + this.indent = 2, + this.shouldMultilineStringInBlock = true, + this.maxStringLineWidth, + this.toEncodable, + }); + + @override + String convert(Object? input) { + final output = StringBuffer(); + _YamlWriter( + sink: output, + shouldMultilineStringInBlock: shouldMultilineStringInBlock, + indent: indent, + toEncodable: toEncodable, + ).write(input); + return output.toString(); + } + + @override + Sink startChunkedConversion(Sink sink) { + return _YamlSink( + sink: sink is StringConversionSink ? sink : StringConversionSink.from(sink), + shouldMultilineStringInBlock: shouldMultilineStringInBlock, + indent: indent, + toEncodable: toEncodable, + ); + } +} + +class _YamlSink implements ChunkedConversionSink { + final StringConversionSink _sink; + final int indent; + final bool shouldMultilineStringInBlock; + final Object? Function(dynamic)? toEncodable; + bool _isDone = false; + + _YamlSink({ + required this.indent, + required this.shouldMultilineStringInBlock, + required this.toEncodable, + required StringConversionSink sink, + }) : _sink = sink; + + @override + void add(Object? object) { + if (_isDone) { + throw StateError('Only one call to add allowed'); + } + _isDone = true; + final stringSink = _sink.asStringSink(); + _YamlWriter( + indent: indent, + shouldMultilineStringInBlock: shouldMultilineStringInBlock, + toEncodable: toEncodable, + sink: stringSink, + ).write(object); + stringSink.close(); + } + + @override + void close() {/* do nothing */} +} + +/// Please dev follow [_JsonStringStringifier] code style +class _YamlWriter { + final StringSink _sink; + + final int indent; + final bool shouldMultilineStringInBlock; + final Object? Function(dynamic)? toEncodable; + + bool _canWriteBlock = false; + int _indentLevel = -1; + + bool get isInitialLine => _indentLevel == -1; + + _YamlWriter({ + required this.indent, + required this.shouldMultilineStringInBlock, + required this.toEncodable, + required StringSink sink, + }) : _sink = sink; + + void write(Object? object) { + if (object == null) { + // Nothing to write + } else if (object is bool) { + writeBoolean(object); + } else if (object is num) { + writeNumber(object); + } else if (object is String) { + writeString(object); + } else if (object is List) { + writeList(object); + } else if (object is Map) { + writeMap(object); + } else if (toEncodable != null) { + write(toEncodable!(object)); + } else { + throw ArgumentError.value(object, null, 'Not support ${object.runtimeType}.'); + } + } + + void writeBoolean(bool boolean) { + if (!isInitialLine) _writeValueIndentation(); + _sink.write(boolean); + } + + void writeNumber(num number) { + if (!isInitialLine) _writeValueIndentation(); + _sink.write(number); + } + + // https://stackoverflow.com/questions/3790454/how-do-i-break-a-string-in-yaml-over-multiple-lines + void writeString(String string) { + if (!isInitialLine) _writeValueIndentation(); + + if (string.contains('\n') && shouldMultilineStringInBlock) { + final hasNewLineEnd = string.endsWith('\n'); + _sink.write(hasNewLineEnd ? '|\n' : '|-\n'); + + _indentLevel += 1; + final lines = string.split('\n'); + for (var i = 0; i < lines.length; i++) { + final isLastLine = (i + 1) == lines.length; + + if (hasNewLineEnd && isLastLine) continue; + + writeIndentation(); + _sink.write(lines[i]); + + if (!isLastLine) _sink.write('\n'); + } + _indentLevel -= 1; + } else { + final hasSpecialCharacters = _specialCharacters.any((e) => string.contains(e)); + final isSpecialKeyword = _spacialKeywords.contains(string) || num.tryParse(string) != null; + final isSpecial = hasSpecialCharacters || isSpecialKeyword; + + if (isSpecial) _sink.write('"'); + _sink.write(_stringWithEscapes(string)); + if (isSpecial) _sink.write('"'); + } + } + + // Using Quotes with YAML Special Characters + final _specialCharacters = r'{}[],&:*#?|-<>=!%@\'.split(''); + final _spacialKeywords = ['null', 'true', 'false']; + + String _stringWithEscapes(String s) => s + .replaceAll(r'\', r'\\') + .replaceAll('\r', r'\r') + .replaceAll('\t', r'\t') + .replaceAll('\n', r'\n') + .replaceAll('"', r'\"') + .replaceAll('™', '\x99') + .replaceAll('', '\x9D'); + + void writeList(List list) { + if (list.isEmpty) { + if (!isInitialLine) _writeValueIndentation(); + _sink.write('[ ]'); + } else { + var isInitialLine = this.isInitialLine; + + _indentLevel += 1; + for (final element in list) { + if (!isInitialLine) _writeBlockIndentation(); + isInitialLine = false; + + _canWriteBlock = true; + _sink.write('-'); + write(element); + } + _indentLevel -= 1; + } + } + + void writeMap(Map map) { + if (map.isEmpty) { + if (!isInitialLine) _writeValueIndentation(); + _sink.write('{ }'); + } else { + var isInitialLine = this.isInitialLine; + + _indentLevel += 1; + map.forEach((key, value) { + if (!isInitialLine) _writeBlockIndentation(); + isInitialLine = false; + + _canWriteBlock = false; + _sink.write(key); + _sink.write(':'); + write(value); + }); + _indentLevel -= 1; + } + } + + void writeIndentation() { + _sink.write((' ' * indent) * _indentLevel); + } + + void _writeValueIndentation() { + _sink.write(' '); + _canWriteBlock = false; + } + + void _writeBlockIndentation() { + if (_canWriteBlock) { + _sink.write(' '); + } else { + _sink.writeln(); + writeIndentation(); + } + } +} diff --git a/open_api_client_generator/lib/src/utils/files_contents.dart b/open_api_client_generator/lib/src/utils/files_contents.dart index da6c927..4da8925 100644 --- a/open_api_client_generator/lib/src/utils/files_contents.dart +++ b/open_api_client_generator/lib/src/utils/files_contents.dart @@ -1,5 +1,7 @@ abstract final class FilesContents { static const String webApiClient = r''' +// ignore_for_file: always_use_package_imports + import 'dart:async'; import 'dart:convert'; import 'dart:html'; @@ -48,6 +50,8 @@ class WebApiClient extends ApiClient implements DartApiClient { '''; static const String ioApiClient = ''' +// ignore_for_file: always_use_package_imports + import 'dart:convert'; import 'dart:io'; @@ -85,6 +89,8 @@ class IoApiClient extends ApiClient implements DartApiClient { '''; static const String httpApiClient = ''' +// ignore_for_file: always_use_package_imports + import 'dart:convert'; import 'package:http/http.dart'; @@ -180,7 +186,7 @@ class ApiClientResponse { }); } -class ApiClientException { +class ApiClientException implements Exception { final ApiClientResponse response; const ApiClientException({ @@ -193,6 +199,8 @@ class ApiClientException { '''; static const String dartApiClient = ''' +// ignore_for_file: always_use_package_imports + import 'api_client.dart'; import 'io_api_client.dart' if (dart.html) 'web_api_client.dart'; diff --git a/open_api_client_generator/pubspec.yaml b/open_api_client_generator/pubspec.yaml index c8f2fa9..c02ebf3 100644 --- a/open_api_client_generator/pubspec.yaml +++ b/open_api_client_generator/pubspec.yaml @@ -18,7 +18,6 @@ dependencies: path: ^1.8.3 http: ^1.1.0 - json2yaml: ^3.0.1 analyzer: ^6.1.0 code_builder: ^4.5.0 @@ -46,6 +45,8 @@ dev_dependencies: built_value: ^8.6.2 built_collection: ^5.1.1 + html: ^0.15.4 + dependency_overrides: open_api_specification: path: ./../open_api_spec diff --git a/open_api_client_generator/tools/files_contents/api_client.dart b/open_api_client_generator/tools/files_contents/api_client.dart index 21fa206..733ee25 100644 --- a/open_api_client_generator/tools/files_contents/api_client.dart +++ b/open_api_client_generator/tools/files_contents/api_client.dart @@ -66,7 +66,7 @@ class ApiClientResponse { }); } -class ApiClientException { +class ApiClientException implements Exception { final ApiClientResponse response; const ApiClientException({ diff --git a/open_api_client_generator/tools/generate_files_contents.dart b/open_api_client_generator/tools/generate_files_contents.dart index f5bfc0d..b134bb6 100644 --- a/open_api_client_generator/tools/generate_files_contents.dart +++ b/open_api_client_generator/tools/generate_files_contents.dart @@ -4,17 +4,18 @@ import 'package:path/path.dart'; import 'package:recase/recase.dart'; void main() { - final root = Directory('./tools/files'); + final root = Directory('./tools/files_contents'); - final variabiles = root.listSync().map((file) { + final variables = root.listSync().map((file) { final content = File(file.path).readAsStringSync(); - return " static const String ${basenameWithoutExtension(file.path).camelCase} = r'''\n" + final rawStringChar = content.contains(r'$') ? 'r' : ''; + return " static const String ${basenameWithoutExtension(file.path).camelCase} = $rawStringChar'''\n" '$content' "\n''';"; }); File('./lib/src/utils/files_contents.dart') .writeAsStringSync('abstract final class FilesContents {\n' - '${variabiles.join('\n')}' - '\n}'); + '${variables.join('\n')}' + '\n}\n'); } diff --git a/open_api_spec/lib/src/read_open_api.dart b/open_api_spec/lib/src/read_open_api.dart index 616d6c1..7ae3361 100644 --- a/open_api_spec/lib/src/read_open_api.dart +++ b/open_api_spec/lib/src/read_open_api.dart @@ -85,7 +85,7 @@ Future> readRef( for (final segment in segments) { data = data[segment] as Map; } - if (!data.containsKey('title')) data = {'title': segments.last, ...data}; + if (!data.containsKey('name')) data = {'name': segments.last, ...data}; } final resolvedData = await _resolveDocumentRefs(uri, document, data, cache: cache); diff --git a/open_api_spec/lib/src/specs/base_specs.dart b/open_api_spec/lib/src/specs/base_specs.dart index c25e6ef..568de37 100644 --- a/open_api_spec/lib/src/specs/base_specs.dart +++ b/open_api_spec/lib/src/specs/base_specs.dart @@ -128,7 +128,7 @@ class OperationOpenApi with PrettyJsonToString { final bool deprecated; @JsonKey(toJson: $nullIfEmpty) - final Map> security; + final List>> security; @JsonKey(toJson: $nullIfEmpty) final List servers; @@ -141,7 +141,7 @@ class OperationOpenApi with PrettyJsonToString { this.requestBody, required this.responses, this.deprecated = false, - this.security = const {}, + this.security = const [], this.servers = const [], }); @@ -168,11 +168,13 @@ class OperationOpenApi with PrettyJsonToString { Map toJson() => _$OperationOpenApiToJson(this); static Map _responsesFromJson(Map json) { - return json.map((key, value) { - return MapEntry( - key is int ? key : int.parse(key as String), - ResponseOpenApi.fromJson(value as Map), - ); + return json.map((code, response) { + if (code is String) { + code = code == 'default' ? 200 : int.parse(code); + } else { + code as int; + } + return MapEntry(code, ResponseOpenApi.fromJson(response as Map)); }); } } diff --git a/open_api_spec/lib/src/specs/base_specs.g.dart b/open_api_spec/lib/src/specs/base_specs.g.dart index 596f7c8..7da4fd3 100644 --- a/open_api_spec/lib/src/specs/base_specs.g.dart +++ b/open_api_spec/lib/src/specs/base_specs.g.dart @@ -136,11 +136,16 @@ OperationOpenApi _$OperationOpenApiFromJson(Map json) => $checkedCreate( security: $checkedConvert( 'security', (v) => - (v as Map?)?.map( - (k, e) => MapEntry(k as String, - (e as List).map((e) => e as String).toList()), - ) ?? - const {}), + (v as List?) + ?.map((e) => (e as Map).map( + (k, e) => MapEntry( + k as String, + (e as List) + .map((e) => e as String) + .toList()), + )) + .toList() ?? + const []), servers: $checkedConvert( 'servers', (v) => diff --git a/open_api_spec/lib/src/specs/schema.dart b/open_api_spec/lib/src/specs/schema.dart index 9416dd7..c83b758 100644 --- a/open_api_spec/lib/src/specs/schema.dart +++ b/open_api_spec/lib/src/specs/schema.dart @@ -69,18 +69,13 @@ enum TypeOpenApi { object, } -extension TypeOpenApiExt on TypeOpenApi { - String toJson() => _$TypeOpenApiEnumMap[this]!; - static TypeOpenApi? maybeFromJson(String? type) => - $enumDecodeNullable(_$TypeOpenApiEnumMap, type); -} - -@JsonEnum() enum FormatOpenApi { int32, int64, double, float, + string, + date, @JsonValue('date-time') dateTime, @@ -93,20 +88,18 @@ enum FormatOpenApi { /// File upload binary, base64; - - String toJson() => _$FormatOpenApiEnumMap[this]!; - static FormatOpenApi? maybeFromJson(String? type) => - $enumDecodeNullable(_$FormatOpenApiEnumMap, type); } @SpecsSerializable() class SchemaOpenApi implements RefOr { + final String? name; final String? title; final String? description; final Object? example; final TypeOpenApi? type; + @JsonKey(unknownEnumValue: JsonKey.nullForUndefinedEnumValue) final FormatOpenApi? format; /// With [TypeOpenApi.integer] | [TypeOpenApi.string] @@ -159,6 +152,7 @@ class SchemaOpenApi implements RefOr { // enum const SchemaOpenApi({ + this.name, this.title, this.description, this.example, @@ -189,6 +183,8 @@ class _SchemaOpenApi extends RefOr with PrettyJsonToString implem SchemaOpenApi? _delegate$; SchemaOpenApi get _delegate => _delegate$ ??= _$SchemaOpenApiFromJson(_json); + @override + String? get name => _delegate.name; @override String? get title => _delegate.title; @override diff --git a/open_api_spec/lib/src/specs/schema.g.dart b/open_api_spec/lib/src/specs/schema.g.dart index 167a31e..68d45ab 100644 --- a/open_api_spec/lib/src/specs/schema.g.dart +++ b/open_api_spec/lib/src/specs/schema.g.dart @@ -93,13 +93,16 @@ SchemaOpenApi _$SchemaOpenApiFromJson(Map json) => $checkedCreate( json, ($checkedConvert) { final val = SchemaOpenApi( + name: $checkedConvert('name', (v) => v as String?), title: $checkedConvert('title', (v) => v as String?), description: $checkedConvert('description', (v) => v as String?), example: $checkedConvert('example', (v) => v), type: $checkedConvert( 'type', (v) => $enumDecodeNullable(_$TypeOpenApiEnumMap, v)), format: $checkedConvert( - 'format', (v) => $enumDecodeNullable(_$FormatOpenApiEnumMap, v)), + 'format', + (v) => $enumDecodeNullable(_$FormatOpenApiEnumMap, v, + unknownValue: JsonKey.nullForUndefinedEnumValue)), enum$: $checkedConvert('enum', (v) => (v as List?)?.map((e) => e as Object).toList()), items: $checkedConvert('items', @@ -138,11 +141,12 @@ Map _$SchemaOpenApiToJson(SchemaOpenApi instance) { } } + writeNotNull('name', instance.name); writeNotNull('title', instance.title); writeNotNull('description', instance.description); writeNotNull('example', instance.example); writeNotNull('type', _$TypeOpenApiEnumMap[instance.type]); - writeNotNull('format', instance.format?.toJson()); + writeNotNull('format', _$FormatOpenApiEnumMap[instance.format]); writeNotNull('enum', instance.enum$); writeNotNull('items', instance.items?.toJson()); writeNotNull('properties', @@ -171,6 +175,7 @@ const _$FormatOpenApiEnumMap = { FormatOpenApi.int64: 'int64', FormatOpenApi.double: 'double', FormatOpenApi.float: 'float', + FormatOpenApi.string: 'string', FormatOpenApi.date: 'date', FormatOpenApi.dateTime: 'date-time', FormatOpenApi.uuid: 'uuid', diff --git a/open_api_spec/lib/src/specs/security_open_api.dart b/open_api_spec/lib/src/specs/security_open_api.dart index d51b66c..4daa1b1 100644 --- a/open_api_spec/lib/src/specs/security_open_api.dart +++ b/open_api_spec/lib/src/specs/security_open_api.dart @@ -93,13 +93,13 @@ class OAuthFlowsOpenApi { /// Version: 3.0.3 @SpecsSerializable() class OAuthFlowOpenApi { - final String authorizationUrl; + final String? authorizationUrl; final String tokenUrl; final String? refreshUrl; final Map scopes; const OAuthFlowOpenApi({ - required this.authorizationUrl, + this.authorizationUrl, required this.tokenUrl, this.refreshUrl, required this.scopes, diff --git a/open_api_spec/lib/src/specs/security_open_api.g.dart b/open_api_spec/lib/src/specs/security_open_api.g.dart index 8bbd219..9ed82df 100644 --- a/open_api_spec/lib/src/specs/security_open_api.g.dart +++ b/open_api_spec/lib/src/specs/security_open_api.g.dart @@ -121,7 +121,7 @@ OAuthFlowOpenApi _$OAuthFlowOpenApiFromJson(Map json) => $checkedCreate( ($checkedConvert) { final val = OAuthFlowOpenApi( authorizationUrl: - $checkedConvert('authorizationUrl', (v) => v as String), + $checkedConvert('authorizationUrl', (v) => v as String?), tokenUrl: $checkedConvert('tokenUrl', (v) => v as String), refreshUrl: $checkedConvert('refreshUrl', (v) => v as String?), scopes: $checkedConvert( @@ -132,10 +132,7 @@ OAuthFlowOpenApi _$OAuthFlowOpenApiFromJson(Map json) => $checkedCreate( ); Map _$OAuthFlowOpenApiToJson(OAuthFlowOpenApi instance) { - final val = { - 'authorizationUrl': instance.authorizationUrl, - 'tokenUrl': instance.tokenUrl, - }; + final val = {}; void writeNotNull(String key, dynamic value) { if (value != null) { @@ -143,6 +140,8 @@ Map _$OAuthFlowOpenApiToJson(OAuthFlowOpenApi instance) { } } + writeNotNull('authorizationUrl', instance.authorizationUrl); + val['tokenUrl'] = instance.tokenUrl; writeNotNull('refreshUrl', instance.refreshUrl); val['scopes'] = instance.scopes; return val; diff --git a/shelf_open_api/lib/shelf_open_api.dart b/shelf_open_api/lib/shelf_open_api.dart index b53b53e..72fd87a 100644 --- a/shelf_open_api/lib/shelf_open_api.dart +++ b/shelf_open_api/lib/shelf_open_api.dart @@ -5,12 +5,12 @@ import 'package:meta/meta_meta.dart'; @TargetKind.method class OpenApiRoute { - final Map> security; + final List>> security; final Type? requestQuery; final Type? requestBody; const OpenApiRoute({ - this.security = const {}, + this.security = const >>[], this.requestQuery, this.requestBody, }); diff --git a/shelf_open_api_generator/build.yaml b/shelf_open_api_generator/build.yaml index 469efa5..1237281 100644 --- a/shelf_open_api_generator/build.yaml +++ b/shelf_open_api_generator/build.yaml @@ -5,10 +5,6 @@ targets: enabled: false shelf_open_api_generator:shelf_routing: enabled: false - shelf_open_api_generator:x: - generate_for: - - lib/x2.dart - - $package$ json_serializable: generate_for: diff --git a/shelf_open_api_generator/lib/shelf_open_api_generator.dart b/shelf_open_api_generator/lib/shelf_open_api_generator.dart index 582964f..a18510b 100644 --- a/shelf_open_api_generator/lib/shelf_open_api_generator.dart +++ b/shelf_open_api_generator/lib/shelf_open_api_generator.dart @@ -85,12 +85,14 @@ class OpenApiBuilder implements Builder { schemasRegistry: schemasRegistry, path: '$routePrefix$route', method: routeAnnotation.read('verb').stringValue, - security: (openApiAnnotation.peek('security')?.mapReader ?? const {}).map((key, value) { - return MapEntry( - key.stringValue, - value.listReader.map((e) => e.stringValue).toList(), - ); - }), + security: (openApiAnnotation.peek('security')?.listReader ?? const []).map((security) { + return security.mapReader.map((securitySchemeKey, permissions) { + return MapEntry( + securitySchemeKey.stringValue, + permissions.listReader.map((e) => e.stringValue).toList(), + ); + }); + }).toList(), requestQuery: openApiAnnotation.peek('requestQuery')?.typeValue, requestBody: openApiAnnotation.peek('requestBody')?.typeValue, ); diff --git a/shelf_open_api_generator/lib/src/handlers/route_handler.dart b/shelf_open_api_generator/lib/src/handlers/route_handler.dart index c6a917a..e5940b4 100644 --- a/shelf_open_api_generator/lib/src/handlers/route_handler.dart +++ b/shelf_open_api_generator/lib/src/handlers/route_handler.dart @@ -15,7 +15,7 @@ class RouteHandler { final SchemasRegistry schemasRegistry; final String path; final String method; - final Map> security; + final List>> security; final DartType? requestQuery; final DartType? requestBody;