diff --git a/pkgs/_analyzer_macros/lib/query_service.dart b/pkgs/_analyzer_macros/lib/query_service.dart index f7cd94f8..e3199ec4 100644 --- a/pkgs/_analyzer_macros/lib/query_service.dart +++ b/pkgs/_analyzer_macros/lib/query_service.dart @@ -20,7 +20,7 @@ analyzer.LinkedElementFactory get _elementFactory => class AnalyzerQueryService implements QueryService { @override Future handle(QueryRequest request) async { - return QueryResponse(model: _evaluateClassQuery(request.query.target)); + return QueryResponse(model: _evaluateClassQuery(request.query!.target!)); } Model _evaluateClassQuery(QualifiedName target) { diff --git a/pkgs/_cfe_macros/lib/query_service.dart b/pkgs/_cfe_macros/lib/query_service.dart index 9fec81a4..6ef38614 100644 --- a/pkgs/_cfe_macros/lib/query_service.dart +++ b/pkgs/_cfe_macros/lib/query_service.dart @@ -15,7 +15,7 @@ class CfeQueryService implements QueryService { @override Future handle(QueryRequest request) async { return QueryResponse( - model: await _evaluateClassQuery(request.query.target)); + model: await _evaluateClassQuery(request.query!.target!)); } Future _evaluateClassQuery(QualifiedName target) async { diff --git a/pkgs/_macro_client/lib/macro_client.dart b/pkgs/_macro_client/lib/macro_client.dart index 45628800..f08274c1 100644 --- a/pkgs/_macro_client/lib/macro_client.dart +++ b/pkgs/_macro_client/lib/macro_client.dart @@ -30,7 +30,8 @@ class MacroClient { // Tell the host which macros are in this bundle. for (final macro in macros) { _sendRequest(MacroRequest.macroStartedRequest( - MacroStartedRequest(macroDescription: macro.description))); + MacroStartedRequest(macroDescription: macro.description), + id: nextRequestId)); } const Utf8Decoder() @@ -64,7 +65,8 @@ class MacroClient { switch (hostRequest.type) { case HostRequestType.augmentRequest: _sendResponse(Response.augmentResponse( - await macros.single.augment(_host, hostRequest.asAugmentRequest))); + await macros.single.augment(_host, hostRequest.asAugmentRequest), + requestId: hostRequest.id)); default: // Ignore unknown request. // TODO(davidmorgan): make handling of unknown request types a designed @@ -96,7 +98,8 @@ class RemoteMacroHost implements Host { @override Future query(Query query) async { - _client._sendRequest(MacroRequest.queryRequest(QueryRequest(query: query))); + _client._sendRequest(MacroRequest.queryRequest(QueryRequest(query: query), + id: nextRequestId)); // TODO(davidmorgan): this is needed because the constructor doesn't wait // for responses to `MacroStartedRequest`, so we need to discard the // responses. Properly track requests and responses. @@ -105,7 +108,7 @@ class RemoteMacroHost implements Host { if (nextResponse.type == ResponseType.macroStartedResponse) { continue; } - return nextResponse.asQueryResponse.model; + return nextResponse.asQueryResponse.model!; } } diff --git a/pkgs/_macro_client/test/macro_client_test.dart b/pkgs/_macro_client/test/macro_client_test.dart index 05cc1fe3..c3f0645d 100644 --- a/pkgs/_macro_client/test/macro_client_test.dart +++ b/pkgs/_macro_client/test/macro_client_test.dart @@ -45,13 +45,15 @@ void main() { '{"type":"MacroStartedRequest","value":' '{"macroDescription":{"runsInPhases":[2]}}}'); - socket.writeln( - json.encode(HostRequest.augmentRequest(AugmentRequest(phase: 2)))); + var requestId = nextRequestId; + socket.writeln(json.encode( + HostRequest.augmentRequest(AugmentRequest(phase: 2), id: requestId))); final augmentResponse = await responses.next; expect( augmentResponse, '{"type":"AugmentResponse","value":' - '{"augmentations":[{"code":"int get x => 3;"}]}}'); + '{"augmentations":[{"code":"int get x => 3;"}]},' + '"requestId":$requestId}'); }); test('sends query requests to host, sends reponse', () async { @@ -73,23 +75,30 @@ void main() { '{"type":"MacroStartedRequest","value":' '{"macroDescription":{"runsInPhases":[3]}}}'); - socket.writeln(json.encode(HostRequest.augmentRequest(AugmentRequest( - phase: 3, target: QualifiedName('package:foo/foo.dart#Foo'))))); + var requestId = nextRequestId; + socket.writeln(json.encode(HostRequest.augmentRequest( + AugmentRequest( + phase: 3, target: QualifiedName('package:foo/foo.dart#Foo')), + id: nextRequestId))); final queryRequest = await responses.next; expect( queryRequest, '{"type":"QueryRequest","value":' - '{"query":{"target":"package:foo/foo.dart#Foo"}}}', + '{"query":{"target":"package:foo/foo.dart#Foo"}},' + '"id":$requestId}', ); - socket.writeln(json.encode(Response.queryResponse(QueryResponse( - model: Model(uris: {'package:foo/foo.dart': Library()}))))); + socket.writeln(json.encode(Response.queryResponse( + QueryResponse( + model: Model(uris: {'package:foo/foo.dart': Library()})), + requestId: requestId))); final augmentRequest = await responses.next; expect( augmentRequest, '{"type":"AugmentResponse","value":' - '{"augmentations":[{"code":"// {\\"uris\\":{\\"package:foo/foo.dart\\":{}}}"}]}}', + '{"augmentations":[{"code":"// {\\"uris\\":{\\"package:foo/foo.dart\\":{}}}"}]},' + '"requestId":$requestId}', ); }); }); diff --git a/pkgs/_macro_host/lib/macro_host.dart b/pkgs/_macro_host/lib/macro_host.dart index 4ebfe1ea..6c804106 100644 --- a/pkgs/_macro_host/lib/macro_host.dart +++ b/pkgs/_macro_host/lib/macro_host.dart @@ -67,7 +67,7 @@ class MacroHost { // TODO(davidmorgan): this just assumes the macro is running, actually // track macro lifecycle. final response = await macroServer.sendToMacro( - name, HostRequest.augmentRequest(request)); + name, HostRequest.augmentRequest(request, id: nextRequestId)); return response.asAugmentResponse; } } @@ -88,12 +88,15 @@ class _HostService implements HostService { _macroPhases!.complete(request .asMacroStartedRequest.macroDescription.runsInPhases .toSet()); - return Response.macroStartedResponse(MacroStartedResponse()); + return Response.macroStartedResponse(MacroStartedResponse(), + requestId: request.id); case MacroRequestType.queryRequest: return Response.queryResponse( - await queryService.handle(request.asQueryRequest)); + await queryService.handle(request.asQueryRequest), + requestId: request.id); default: - return Response.errorResponse(ErrorResponse(error: 'unsupported')); + return Response.errorResponse(ErrorResponse(error: 'unsupported'), + requestId: request.id); } } } diff --git a/pkgs/_macro_server/test/macro_server_test.dart b/pkgs/_macro_server/test/macro_server_test.dart index a2b5ff40..41a501a2 100644 --- a/pkgs/_macro_server/test/macro_server_test.dart +++ b/pkgs/_macro_server/test/macro_server_test.dart @@ -36,8 +36,10 @@ class TestHostService implements HostService { Future handle(MacroRequest request) async { if (request.type == MacroRequestType.macroStartedRequest) { _macroStartedRequestsController.add(request.asMacroStartedRequest); - return Response.macroStartedResponse(MacroStartedResponse()); + return Response.macroStartedResponse(MacroStartedResponse(), + requestId: request.id); } - return Response.errorResponse(ErrorResponse(error: 'unimplemented')); + return Response.errorResponse(ErrorResponse(error: 'unimplemented'), + requestId: request.id); } } diff --git a/pkgs/dart_model/lib/src/dart_model.g.dart b/pkgs/dart_model/lib/src/dart_model.g.dart index cfd82b12..6e1e7c7c 100644 --- a/pkgs/dart_model/lib/src/dart_model.g.dart +++ b/pkgs/dart_model/lib/src/dart_model.g.dart @@ -1,5 +1,5 @@ // This file is generated. To make changes edit schemas/*.schema.json -// then run from the repo root: dart tool/model_generator/bin/main.dart +// then run from the repo root: dart tool/dart_model_generator/bin/main.dart /// An augmentation to Dart code. TODO(davidmorgan): this is a placeholder. extension type Augmentation.fromJson(Map node) { @@ -165,12 +165,19 @@ enum StaticTypeType { } extension type StaticType.fromJson(Map node) { - static StaticType neverType(NeverType neverType) => - StaticType.fromJson({'type': 'NeverType', 'value': null}); + static StaticType neverType(NeverType neverType) => StaticType.fromJson({ + 'type': 'NeverType', + 'value': neverType, + }); static StaticType nullableType(NullableType nullableType) => - StaticType.fromJson({'type': 'NullableType', 'value': nullableType.node}); - static StaticType voidType(VoidType voidType) => - StaticType.fromJson({'type': 'VoidType', 'value': voidType.string}); + StaticType.fromJson({ + 'type': 'NullableType', + 'value': nullableType, + }); + static StaticType voidType(VoidType voidType) => StaticType.fromJson({ + 'type': 'VoidType', + 'value': voidType, + }); StaticTypeType get type { switch (node['type'] as String) { case 'NeverType': diff --git a/pkgs/macro_service/lib/src/macro_service.dart b/pkgs/macro_service/lib/src/macro_service.dart index 5b6725ab..16b854eb 100644 --- a/pkgs/macro_service/lib/src/macro_service.dart +++ b/pkgs/macro_service/lib/src/macro_service.dart @@ -18,3 +18,20 @@ abstract interface class MacroService { /// Handles [request]. Future handle(HostRequest request); } + +/// Shared implementation of auto incrementing 32 bit IDs. +/// +/// These roll back to 0 once it is greater than 2^32. +/// +/// These are only unique to the process which generates the request, +/// so for instance the host and macro services may generate conflicting ids +/// and that is allowed. +int get nextRequestId { + final next = _nextRequestId++; + if (_nextRequestId > 2 ^ 32) { + _nextRequestId = 0; + } + return next; +} + +int _nextRequestId = 0; diff --git a/pkgs/macro_service/lib/src/macro_service.g.dart b/pkgs/macro_service/lib/src/macro_service.g.dart index 8c3d8371..80cd4678 100644 --- a/pkgs/macro_service/lib/src/macro_service.g.dart +++ b/pkgs/macro_service/lib/src/macro_service.g.dart @@ -1,5 +1,5 @@ // This file is generated. To make changes edit schemas/*.schema.json -// then run from the repo root: dart tool/model_generator/bin/main.dart +// then run from the repo root: dart tool/dart_model_generator/bin/main.dart import 'package:dart_model/dart_model.dart'; @@ -66,9 +66,15 @@ enum HostRequestType { } extension type HostRequest.fromJson(Map node) { - static HostRequest augmentRequest(AugmentRequest augmentRequest) => - HostRequest.fromJson( - {'type': 'AugmentRequest', 'value': augmentRequest.node}); + static HostRequest augmentRequest( + AugmentRequest augmentRequest, { + required int id, + }) => + HostRequest.fromJson({ + 'type': 'AugmentRequest', + 'value': augmentRequest, + 'id': id, + }); HostRequestType get type { switch (node['type'] as String) { case 'AugmentRequest': @@ -84,6 +90,9 @@ extension type HostRequest.fromJson(Map node) { } return AugmentRequest.fromJson(node['value'] as Map); } + + /// The id of this request, must be returned in responses. + int get id => node['id'] as int; } /// Information about a macro that the macro provides to the host. @@ -125,12 +134,23 @@ enum MacroRequestType { extension type MacroRequest.fromJson(Map node) { static MacroRequest macroStartedRequest( - MacroStartedRequest macroStartedRequest) => - MacroRequest.fromJson( - {'type': 'MacroStartedRequest', 'value': macroStartedRequest.node}); - static MacroRequest queryRequest(QueryRequest queryRequest) => - MacroRequest.fromJson( - {'type': 'QueryRequest', 'value': queryRequest.node}); + MacroStartedRequest macroStartedRequest, { + required int id, + }) => + MacroRequest.fromJson({ + 'type': 'MacroStartedRequest', + 'value': macroStartedRequest, + 'id': id, + }); + static MacroRequest queryRequest( + QueryRequest queryRequest, { + required int id, + }) => + MacroRequest.fromJson({ + 'type': 'QueryRequest', + 'value': queryRequest, + 'id': id, + }); MacroRequestType get type { switch (node['type'] as String) { case 'MacroStartedRequest': @@ -155,6 +175,9 @@ extension type MacroRequest.fromJson(Map node) { } return QueryRequest.fromJson(node['value'] as Map); } + + /// The id of this request, must be returned in responses. + int get id => node['id'] as int; } /// Macro's query about the code it should augment. @@ -189,17 +212,42 @@ enum ResponseType { } extension type Response.fromJson(Map node) { - static Response augmentResponse(AugmentResponse augmentResponse) => - Response.fromJson( - {'type': 'AugmentResponse', 'value': augmentResponse.node}); - static Response errorResponse(ErrorResponse errorResponse) => - Response.fromJson({'type': 'ErrorResponse', 'value': errorResponse.node}); + static Response augmentResponse( + AugmentResponse augmentResponse, { + required int requestId, + }) => + Response.fromJson({ + 'type': 'AugmentResponse', + 'value': augmentResponse, + 'requestId': requestId, + }); + static Response errorResponse( + ErrorResponse errorResponse, { + required int requestId, + }) => + Response.fromJson({ + 'type': 'ErrorResponse', + 'value': errorResponse, + 'requestId': requestId, + }); static Response macroStartedResponse( - MacroStartedResponse macroStartedResponse) => - Response.fromJson( - {'type': 'MacroStartedResponse', 'value': macroStartedResponse.node}); - static Response queryResponse(QueryResponse queryResponse) => - Response.fromJson({'type': 'QueryResponse', 'value': queryResponse.node}); + MacroStartedResponse macroStartedResponse, { + required int requestId, + }) => + Response.fromJson({ + 'type': 'MacroStartedResponse', + 'value': macroStartedResponse, + 'requestId': requestId, + }); + static Response queryResponse( + QueryResponse queryResponse, { + required int requestId, + }) => + Response.fromJson({ + 'type': 'QueryResponse', + 'value': queryResponse, + 'requestId': requestId, + }); ResponseType get type { switch (node['type'] as String) { case 'AugmentResponse': @@ -242,4 +290,7 @@ extension type Response.fromJson(Map node) { } return QueryResponse.fromJson(node['value'] as Map); } + + /// The id of the [AugmentRequest] this is responding to. + int get requestId => node['requestId'] as int; } diff --git a/schemas/macro_service.schema.json b/schemas/macro_service.schema.json index a182795a..09d1dc17 100644 --- a/schemas/macro_service.schema.json +++ b/schemas/macro_service.schema.json @@ -62,6 +62,10 @@ "HostRequest": { "$description": "A request sent from host to macro.", "properties": { + "id": { + "description": "The id of this request, must be returned in responses.", + "type": "integer" + }, "type": { "type": "string" }, @@ -72,7 +76,12 @@ } ] } - } + }, + "required": [ + "id", + "type", + "value" + ] }, "MacroDescription": { "type": "object", @@ -103,6 +112,10 @@ "MacroRequest": { "$description": "A request sent from macro to host.", "properties": { + "id": { + "description": "The id of this request, must be returned in responses.", + "type": "integer" + }, "type": { "type": "string" }, @@ -116,7 +129,12 @@ } ] } - } + }, + "required": [ + "id", + "type", + "value" + ] }, "QueryRequest": { "type": "object", @@ -139,6 +157,10 @@ "Response": { "$description": "A response to a [MacroRequest] or [HostRequest].", "properties": { + "requestId": { + "description": "The id of the [AugmentRequest] this is responding to.", + "type": "integer" + }, "type": { "type": "string" }, @@ -158,7 +180,12 @@ } ] } - } + }, + "required": [ + "requestId", + "type", + "value" + ] } } } diff --git a/tool/dart_model_generator/lib/generate_dart_model.dart b/tool/dart_model_generator/lib/generate_dart_model.dart index aa1ab901..7deb9fcb 100644 --- a/tool/dart_model_generator/lib/generate_dart_model.dart +++ b/tool/dart_model_generator/lib/generate_dart_model.dart @@ -26,11 +26,13 @@ void run() { /// Generates and returns code for [schemaJson]. String generate(String schemaJson, - {bool importDartModel = false, String? dartModelJson}) { + {bool importDartModel = false, + bool importMacroService = false, + String? dartModelJson}) { final result = [ '// This file is generated. To make changes edit schemas/*.schema.json', '// then run from the repo root: ' - 'dart tool/model_generator/bin/main.dart', + 'dart tool/dart_model_generator/bin/main.dart', '', if (importDartModel) "import 'package:dart_model/dart_model.dart';", ]; @@ -45,7 +47,7 @@ String generate(String schemaJson, if (_isUnion(value)) { result.add(_generateUnion( key, - value.properties['value']!.oneOf, + value, allDefinitions: allDefinitions, )); } else { @@ -108,22 +110,11 @@ String _generateExtensionType(String name, JsonSchema definition) { } else { result.writeln(' $name({'); for (final property in propertyMetadatas) { - result.writeln(switch (property.type) { - PropertyType.object => - '${property.elementTypeName}? ${property.name},', - PropertyType.bool => 'bool? ${property.name},', - PropertyType.string => 'String? ${property.name},', - PropertyType.integer => 'int? ${property.name},', - PropertyType.list => - 'List<${property.elementTypeName}>? ${property.name},', - PropertyType.map => - 'Map? ${property.name},', - }); + result.writeParameter(property, definition); } result.writeln('}) : this.fromJson({'); for (final property in propertyMetadatas) { - result.writeln('if (${property.name} != null) ' - "'${property.name}': ${property.name},"); + result.writeMapElement(property, definition); } result.writeln('});'); } @@ -135,33 +126,8 @@ String _generateExtensionType(String name, JsonSchema definition) { throw UnsupportedError('Unsupported type: ${definition.type}'); } - // Generate a getter for every field that looks up in the JSON and "creates" - // extension types or casts collections as needed. The getters assume the - // data is present and will throw if it's not. for (final property in propertyMetadatas) { - if (property.description != null) { - result.writeln('/// ${property.description}'); - } - result.writeln(switch (property.type) { - PropertyType.object => - // TODO(davidmorgan): use the extension type constructor instead of - // casting. - '${property.elementTypeName} get ${property.name} => ' - 'node[\'${property.name}\'] ' - 'as ${property.elementTypeName};', - PropertyType.bool => 'bool get ${property.name} => ' - 'node[\'${property.name}\'] as bool;', - PropertyType.string => 'String get ${property.name} => ' - 'node[\'${property.name}\'] as String;', - PropertyType.integer => 'int get ${property.name} => ' - 'node[\'${property.name}\'] as int;', - PropertyType.list => - 'List<${property.elementTypeName}> get ${property.name} => ' - '(node[\'${property.name}\'] as List).cast();', - PropertyType.map => - 'Map get ${property.name} => ' - '(node[\'${property.name}\'] as Map).cast();', - }); + result.writePropertyGetter(property); } result.writeln('}'); return result.toString(); @@ -175,7 +141,7 @@ bool _isUnion(JsonSchema schema) => schema.properties['type']?.schemaMap!['type'] == 'string' && schema.properties['value']?.oneOf != null; -/// Generates a type called [name] that is a union of the specified [oneOf] +/// Generates a type called [name] that is a union of the specified `oneOf` /// types, which must all be `$ref`s to class definitions. /// /// An enum is generated next to it called `${name}Type` with one value for @@ -184,12 +150,14 @@ bool _isUnion(JsonSchema schema) => /// The union type has a `type` getter that returns an instance of this enum, /// and `asFoo` getters that "cast" to each type. /// -/// On the wire the union type is: `{"type": , "value": }` +/// On the wire the union type is: `{"type": , "value": }` and +/// may have additional properties as well. String _generateUnion( String name, - List oneOf, { + JsonSchema definition, { required Map allDefinitions, }) { + final oneOf = definition.properties['value']!.oneOf; final result = StringBuffer(); final unionEntries = oneOf .map((s) => _refName(s.schemaMap![r'$ref'] as String)) @@ -205,15 +173,33 @@ String _generateUnion( ..writeln('bool get isKnown => this != _unknown;') ..writeln('}'); + final extraPropertyMetadatas = [ + for (var MapEntry(:key, :value) in definition.properties.entries) + // These are handled specially for union types. + if (key != 'type' && key != 'value') _readPropertyMetadata(key, value) + ]; + // TODO(davidmorgan): add description. result.writeln('extension type $name.fromJson(Map node) {'); - for (final (type, def) in unionEntries) { + for (final (type, _) in unionEntries) { final lowerType = _firstToLowerCase(type); + result.writeln('static $name $lowerType($type $lowerType'); + if (extraPropertyMetadatas.isNotEmpty) { + result.writeln(', {'); + for (final property in extraPropertyMetadatas) { + result.writeParameter(property, definition); + } + result.write('}'); + } result - ..writeln('static $name $lowerType($type $lowerType) =>') + ..writeln(') =>') ..writeln('$name.fromJson({') ..writeln("'type': '$type',") - ..writeln("'value': ${_dartWrapperToJson(lowerType, def)}});"); + ..writeln("'value': $lowerType,"); + for (final property in extraPropertyMetadatas) { + result.writeMapElement(property, definition); + } + result.writeln('});'); } result @@ -237,6 +223,11 @@ String _generateUnion( "return $type.fromJson(node['value'] as ${_dartJsonType(schema)});") ..writeln('}'); } + + for (final property in extraPropertyMetadatas) { + result.writePropertyGetter(property); + } + result.writeln('}'); return result.toString(); @@ -361,3 +352,65 @@ class LocalRefProvider implements RefProvider { return json.decode(dartModelJson) as Map; }; } + +extension on StringBuffer { + void writeType(PropertyMetadata property, {bool nullable = false}) { + write(switch (property.type) { + PropertyType.object => property.elementTypeName, + PropertyType.bool => 'bool', + PropertyType.string => 'String', + PropertyType.integer => 'int', + PropertyType.list => 'List<${property.elementTypeName}>', + PropertyType.map => 'Map', + }); + if (nullable) write('?'); + } + + /// Writes a map element for [property], assuming there is a variable in + /// scope with the same name (usually, a function parameter). + /// + /// If the property is not required, the element will be omitted if the + /// variable is null. + void writeMapElement(PropertyMetadata property, JsonSchema schema) { + if (!schema.propertyRequired(property.name)) { + write('if (${property.name} != null) '); + } + writeln("'${property.name}': ${property.name},"); + } + + /// Writes a named function parameter for [property]. + /// + /// If the property is required, it will be non-nullable and marked as + /// `required`, otherwise it will be nullable and optional. + void writeParameter(PropertyMetadata property, JsonSchema schema) { + var required = schema.propertyRequired(property.name); + if (required) write('required '); + writeType(property, nullable: !required); + writeln(' ${property.name},'); + } + + /// Writes a getter for [property] that looks up in the JSON and "creates" + /// extension types or casts collections as needed. The getters assume the + /// data is present and will throw if it's not, and return types are always + /// non-nullable. + void writePropertyGetter(PropertyMetadata property) { + if (property.description != null) { + writeln('/// ${property.description}'); + } + writeType(property); + write(' get ${property.name} => '); + switch (property.type) { + case PropertyType.object || + PropertyType.bool || + PropertyType.string || + PropertyType.integer: + write("node['${property.name}'] as "); + writeType(property); + case PropertyType.list: + write("(node['${property.name}'] as List).cast()"); + case PropertyType.map: + write("(node['${property.name}'] as Map).cast()"); + } + writeln(';'); + } +}