From 1774306b06ad18e7bb3f9c12b25799da65f118af Mon Sep 17 00:00:00 2001 From: Fabian Stamm Date: Thu, 21 Jul 2022 18:33:23 +0200 Subject: [PATCH] Add service client support --- examples/Dart/Test.dart | 42 +++++++-- examples/Typescript/server.ts | 62 +++++++++++++ examples/Typescript/test.ts | 2 +- src/targets/dart.ts | 140 +++++++++++++++++++++-------- templates/Dart/base.dart | 52 +++++++++++ templates/Dart/service_client.dart | 86 ++++++++++++++++++ 6 files changed, 338 insertions(+), 46 deletions(-) create mode 100644 examples/Typescript/server.ts create mode 100644 templates/Dart/base.dart create mode 100644 templates/Dart/service_client.dart diff --git a/examples/Dart/Test.dart b/examples/Dart/Test.dart index df12525..daee3ee 100644 --- a/examples/Dart/Test.dart +++ b/examples/Dart/Test.dart @@ -1,12 +1,42 @@ +import 'dart:async'; +import 'dart:io'; + import "./out/lib/example.dart"; import "dart:convert"; -int main() { - var t = TestAtom(val_boolean: false, val_number: 1, val_string: "hi"); - print(jsonEncode(t)); +void main() async { + var str = StreamController>(); - var t2 = TestEnum.VAL2; - print(jsonEncode(t2)); + var provider = ServiceProvider(str.stream); + var sock = await Socket.connect("127.0.0.1", 8859); - return 0; + utf8.decoder.bind(sock).transform(new LineSplitter()).listen((line) { + str.add(jsonDecode(line)); + }); + + provider.output.stream.listen((event) { + sock.writeln(jsonEncode(event)); + }); + + var s = new TestServiceClient(provider); + + var r = await s.AddValuesMultipleParams(10, 15); + print(r); + + var r2 = + await s.AddValuesSingleParam(AddValueRequest(value1: 10, value2: 15)); + print(r2?.value); + + var catched = false; + await s.ThrowingError().catchError((err) { + catched = true; + print("Expected error was catched: " + err.toString()); + }); + + if (!catched) { + throw Error(); + } + + await sock.close(); + // exit(0); } diff --git a/examples/Typescript/server.ts b/examples/Typescript/server.ts new file mode 100644 index 0000000..0f22576 --- /dev/null +++ b/examples/Typescript/server.ts @@ -0,0 +1,62 @@ + +import * as net from "net"; +import { Server, AddValueRequest, AddValueResponse } from "./out"; +import * as readline from 'node:readline'; + + +const server = new Server.ServiceProvider(); + +class TestService extends Server.TestService { + async AddValuesSingleParam( + request: AddValueRequest, + ctx: undefined + ): Promise { + return { + value: request.value1 + request!.value2, + }; + } + async AddValuesMultipleParams( + value1: number, + value2: number, + ctx: undefined + ): Promise { + return value1 + value2; + } + + async ReturningVoid(param1) { + console.log("Calling Returning Void"); + } + + OnEvent(param1: string, ctx: undefined): void { + console.log("Received notification", param1); + } + + async FunctionWithArrayAsParamAndReturn(vals1, vals2) { + return [...vals1, ...vals2]; + } + + async ThrowingError(ctx: undefined): Promise { + throw new Error("Remote error!"); + } +} + +server.addService(new TestService()); + +net.createServer((socket) => { + socket.on("error", console.error); + + console.log("Connection from:", socket.remoteAddress); + const sess = server.getSession(msg => { + const s = JSON.stringify(msg); + console.log("Sending:", s); + socket.write(s + "\n"); + }); + const rl = readline.createInterface(socket); + rl.on("line", line => { + console.log("Receiving:", line); + sess.onMessage(JSON.parse(line)); + }) + rl.on("error", console.error); +}).listen(8859).on("listening", () => { + console.log("Is listening on :8859"); +}).on("error", console.error) \ No newline at end of file diff --git a/examples/Typescript/test.ts b/examples/Typescript/test.ts index 5529293..05a0e1a 100644 --- a/examples/Typescript/test.ts +++ b/examples/Typescript/test.ts @@ -22,7 +22,7 @@ class TestService extends Server.TestService { ctx: undefined ): Promise { return { - value: request.value1 + request.value2, + value: request.value1 + request!.value2, }; } async AddValuesMultipleParams( diff --git a/src/targets/dart.ts b/src/targets/dart.ts index 3ad2c0b..609b361 100644 --- a/src/targets/dart.ts +++ b/src/targets/dart.ts @@ -10,7 +10,7 @@ const conversion = { float: "double", string: "String", void: "void", - bytes: "Uint8List", //TODO: Check this + // bytes: "Uint8List", //TODO: Check this }; function toDartType(type: string): string { @@ -35,11 +35,20 @@ export class DartTarget extends CompileTarget<{ dart_library_name: string }> { } generateImports(a: lineAppender, def: TypeDefinition | ServiceDefinition) { + a(0, `import "./base.dart";`) def.depends.forEach((dep) => { a(0, this.getImport(dep)); }); } + getTypeParse(type: string, value: string) { + if (conversion[type]) { + return `${toDartType(type)}_fromJson(${value})`; + } else { + return `${toDartType(type)}.fromJson(${value})`; + } + } + generateType(definition: TypeDefinition): void { const { a, getResult } = LineAppender(); @@ -53,8 +62,7 @@ export class DartTarget extends CompileTarget<{ dart_library_name: string }> { } else if (field.map) { a( 1, - `Map<${toDartType(field.map)},${toDartType(field.type)}>? ${ - field.name + `Map<${toDartType(field.map)},${toDartType(field.type)}>? ${field.name };` ); } else { @@ -74,18 +82,10 @@ export class DartTarget extends CompileTarget<{ dart_library_name: string }> { for (const field of definition.fields) { a(2, `if(json.containsKey("${field.name}")) {`); - const parseField = (value: string) => { - if (conversion[field.type]) { - return value; - } else { - return `${field.type}.fromJson(${value})`; - } - }; - if (field.array) { a(3, `this.${field.name} = [];`); a(3, `(json["${field.name}"] as List).forEach((e) => {`); - a(4, `this.${field.name}!.add(${parseField("e")})`); + a(4, `this.${field.name}!.add(${this.getTypeParse(field.type, "e")})`); a(3, `});`); } else if (field.map) { a(3, `this.${field.name} = {};`); @@ -95,12 +95,12 @@ export class DartTarget extends CompileTarget<{ dart_library_name: string }> { field.map )},dynamic>).forEach((key, value) => {` ); - a(4, `this.${field.name}![key] = ${parseField("value")}`); + a(4, `this.${field.name}![key] = ${this.getTypeParse(field.type, "value")}`); a(3, `});`); } else { a( 3, - `this.${field.name} = ${parseField(`json["${field.name}"]`)};` + `this.${field.name} = ${this.getTypeParse(field.type, `json["${field.name}"]`)};` ); } a(2, `} else {`); @@ -178,40 +178,102 @@ export class DartTarget extends CompileTarget<{ dart_library_name: string }> { this.writeFile(`lib/src/${definition.name}.dart`, getResult()); } - generateService(definition: ServiceDefinition): void { + generateServiceClient(definition: ServiceDefinition): void { + const { a, getResult } = LineAppender(); + + this.generateImports(a, definition); + a(0, `import "./service_client.dart";`); + a(0, ``); + a(0, `class ${definition.name}Client extends Service {`); + a(0, ``); + + a(1, `${definition.name}Client(ServiceProvider provider):super(provider, "${definition.name}");`); + + a(0, ``); + + for (const func of definition.functions) { + const args = func.inputs.map(inp => + (inp.array ? `List<${toDartType(inp.type)}>` : toDartType(inp.type)) + " " + inp.name + ).join(", "); + + const asParams = func.inputs.map(e => e.name).join(", "); + + if (!func.return) { + a(1, `void ${func.name}(${args}) {`) + a(2, `provider.sendNotification("${definition.name}.${func.name}", [${asParams}]);`); + a(1, `}`); + } else { + const baseReturnType = func.return.type != "void" ? (toDartType(func.return.type) + "?") : toDartType(func.return.type); + const returnType = func.return.array ? `List<${baseReturnType}>` : baseReturnType; + + a(1, `Future<${returnType}> ${func.name}(${args}) async {`); + a(2, `var res = await this.provider.sendRequest("${definition.name}.${func.name}", [${asParams}]);`); + if (func.return.type !== "void") { + if(func.return.array) { + a(2, `return res.map((entry) =>${this.getTypeParse(func.return.type, "entry")}).toList();`); + } else { + a(2, `return ${this.getTypeParse(func.return.type, "res")};`); + } + } + a(1, `}`); + } + a(0, ``); + } + a(0, `}`); + a(0, ``); + + this.writeFile(`lib/src/${definition.name}Client.dart`, getResult()); + } + + generateServiceServer(definition: ServiceDefinition): void { console.log( chalk.yellow("[DART] WARNING:"), - "DART support for services is not yet there. Service generation is skipped!" + "DART support for services is not yet there. Service generation is currently limited to clients!" ); } + generateService(definition: ServiceDefinition): void { + this.generateServiceClient(definition); + this.writeFile("lib/src/service_client.dart", this.getTemplate("Dart/service_client.dart")) + } + finalize(steps: Step[]): void { - const { a, getResult } = LineAppender(); + const { a, getResult } = LineAppender(); - a(0, `library ${this.options.dart_library_name};`) - a(0, ``); + a(0, `library ${this.options.dart_library_name};`) + a(0, ``); - steps.forEach(([type, def]) => { - switch (type) { - case "type": - a(0, `export 'src/${def.name}.dart';`); - break; - case "enum": - a(0, `export 'src/${def.name}.dart';`); - break; - default: - console.warn( - chalk.yellow("[DART] WARNING:"), - "unimplemented step found:", - type - ); - // case "service": - } - }); + let hasService = false; - this.writeFile(`lib/${this.options.dart_library_name}.dart`, getResult()); - this.writeFile(`pubspec.yaml`, this.getTemplate("Dart/pubspec.yaml").replace("__NAME__", this.options.dart_library_name)); - } + steps.forEach(([type, def]) => { + switch (type) { + case "type": + a(0, `export 'src/${def.name}.dart';`); + break; + case "enum": + a(0, `export 'src/${def.name}.dart';`); + break; + case "service": + a(0, `export 'src/${def.name}Client.dart';`); + hasService = true; + break; + default: + console.warn( + chalk.yellow("[DART] WARNING:"), + "unimplemented step found:", + type + ); + } + }); + + if (hasService) { + a(0, `export 'src/service_client.dart';`) + } + + this.writeFile(`lib/${this.options.dart_library_name}.dart`, getResult()); + this.writeFile(`pubspec.yaml`, this.getTemplate("Dart/pubspec.yaml").replace("__NAME__", this.options.dart_library_name)); + this.writeFile(`lib/src/base.dart`, this.getTemplate("Dart/base.dart")); + } } diff --git a/templates/Dart/base.dart b/templates/Dart/base.dart new file mode 100644 index 0000000..1321f27 --- /dev/null +++ b/templates/Dart/base.dart @@ -0,0 +1,52 @@ +class JRPCError extends Error { + String message; + JRPCError(this.message); + + @override + String toString() => message; +} + +int? int_fromJson(dynamic val) { + if (val == null) { + return null; + } else if (val is int) { + return val; + } else if (val is double) { + return val.toInt(); + } else { + throw JRPCError("Not a number!"); + } +} + +double? double_fromJson(dynamic val) { + if (val == null) { + return null; + } else if (val is int) { + return val.toDouble(); + } else if (val is double) { + return val; + } else { + throw JRPCError("Not a number!"); + } +} + +bool? bool_fromJson(dynamic val) { + if (val == null) { + return null; + } else if (val is bool) { + return val; + } else { + throw JRPCError("Not a bool!"); + } +} + +String? String_fromJson(dynamic val) { + if (val == null) { + return null; + } else if (val is String) { + return val; + } else { + return val.toString(); // TODO: Either this or error + // throw JRPCError("Not a string!"); + } +} diff --git a/templates/Dart/service_client.dart b/templates/Dart/service_client.dart new file mode 100644 index 0000000..658ef92 --- /dev/null +++ b/templates/Dart/service_client.dart @@ -0,0 +1,86 @@ +import "dart:async"; +import 'dart:math'; + +import "./base.dart"; + +abstract class Service { + String name; + ServiceProvider provider; + + Service(this.provider, this.name) { + provider._services[name] = this; + } +} + +class ServiceProvider { + final Map _services = {}; + final Map> _requests = {}; + + StreamController> output = StreamController(); + late StreamSubscription s; + + ServiceProvider(Stream> input) { + s = input.listen(onMessage); + } + + void onMessage(Map msg) { + // print("Working on message"); + if (msg.containsKey("method")) { + if (msg.containsKey("id")) { + print("Message is request"); + // Request, not supported! + return; + } else { + // print("Message is notification"); + // Notification + // TODO: Implement + } + } else { + // print("Message is response"); + // Response + var req = _requests[msg["id"]]; + + if (req == null) { + // print("Could not find related request. Ignoring"); + return; // Irrelevant response + } + if (msg.containsKey("error")) { + //TODO: + req.completeError(JRPCError(msg["error"]["message"])); + } else { + req.complete(msg["result"]); + } + } + } + + Future sendRequest(String method, dynamic params) { + var id = nanoid(10); + var req = {"jsonrpc": "2.0", "id": id, "method": method, "params": params}; + + var completer = Completer(); + + output.add(req); + _requests[id] = completer; + + return completer.future; + } + + void sendNotification(String method, dynamic params) { + var req = {"jsonrpc": "2.0", "method": method, "params": params}; + output.add(req); + } +} + +// Copied from: https://github.com/pd4d10/nanoid-dart (MIT License) +final _random = Random.secure(); +const urlAlphabet = + 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'; + +String nanoid([int size = 21]) { + final len = urlAlphabet.length; + String id = ''; + while (0 < size--) { + id += urlAlphabet[_random.nextInt(len)]; + } + return id; +}