From 94c4515347e080b97caa98812b961267b64e2cbb Mon Sep 17 00:00:00 2001 From: Vojtech Vitek Date: Tue, 10 Feb 2026 18:14:06 +0100 Subject: [PATCH 1/3] Support the new RIDL "basepath" keyword --- _examples/flutter-go/Makefile | 3 + .../flutter_app/lib/generated/sdk.dart | 941 +++++++++--------- .../flutter-go/go_server/proto/server.gen.go | 328 ++++-- _examples/flutter-go/service.ridl | 1 + client.go.tmpl | 8 +- main.go.tmpl | 2 +- tests/schema/custom.ridl | 1 + tests/test/custom_test.dart | 2 +- 8 files changed, 717 insertions(+), 569 deletions(-) create mode 100644 _examples/flutter-go/Makefile diff --git a/_examples/flutter-go/Makefile b/_examples/flutter-go/Makefile new file mode 100644 index 0000000..d42fdb9 --- /dev/null +++ b/_examples/flutter-go/Makefile @@ -0,0 +1,3 @@ +generate: + webrpc-gen -schema=./service.ridl -target=../../ -client -out=flutter_app/lib/generated/sdk.dart + webrpc-gen -schema=./service.ridl -target=golang -server -out=go_server/proto/server.gen.go diff --git a/_examples/flutter-go/flutter_app/lib/generated/sdk.dart b/_examples/flutter-go/flutter_app/lib/generated/sdk.dart index 285914e..cb3fac6 100644 --- a/_examples/flutter-go/flutter_app/lib/generated/sdk.dart +++ b/_examples/flutter-go/flutter_app/lib/generated/sdk.dart @@ -1,6 +1,6 @@ -// flutter-go v1.0.0 2a329e90418316407f201f4e41ea13510b20bc92 +// flutter-go v1.0.0 287a5e9398d731d587d1910016b4981fe7fe01a4 // -- -// Code generated by webrpc-gen@v0.18.2 with ../../ generator. DO NOT EDIT. +// Code generated by webrpc-gen@v0.32.3 with ../../ generator. DO NOT EDIT. // // webrpc-gen -schema=./service.ridl -target=../../ -client -out=flutter_app/lib/generated/sdk.dart // ignore_for_file: non_constant_identifier_names @@ -16,149 +16,155 @@ const webRPCVersion = "v1"; const webRPCSchemaVersion = "v1.0.0"; /// Schema hash generated from your RIDL schema -const webRPCSchemaHash = "2a329e90418316407f201f4e41ea13510b20bc92"; +const webRPCSchemaHash = "287a5e9398d731d587d1910016b4981fe7fe01a4"; class ExampleServiceImpl implements ExampleService { - ExampleServiceImpl(String hostname, [WebrpcHttpClient? httpClient]) - : _baseUrl = '$hostname/rpc/ExampleService/', - _httpClient = httpClient ?? _MainWebrpcHttpClient(); - - final String _baseUrl; - final WebrpcHttpClient _httpClient; - - @override - Future<({List items})> getItems() async { - final String? body = null; - WebrpcHttpRequest request = WebrpcHttpRequest( - uri: _makeUri('GetItems'), - headers: _makeHeaders(body), - body: body, - ); - final WebrpcHttpResponse response = await _httpClient.post(request); - - await _handleResponse(response); - final Map json = jsonDecode(response.body); - return (items: _getItemsItems(json['items']),); - } - - static List _getItemsItems(dynamic v0) { - final List r0 = []; - for (dynamic v1 in v0) { - final ItemSummary r1 = ItemSummary.fromJson(v1); - r0.add(r1); + ExampleServiceImpl(String hostname, [WebrpcHttpClient? httpClient]) + : _baseUrl = '$hostname/v1/ExampleService/', + _httpClient = httpClient ?? _MainWebrpcHttpClient(); + + final String _baseUrl; + final WebrpcHttpClient _httpClient; + + @override + Future<({List items})> getItems() async { + final String? body = null; + WebrpcHttpRequest request = WebrpcHttpRequest( + uri: _makeUri('GetItems'), + headers: _makeHeaders(), + body: body, + ); + final WebrpcHttpResponse response = await _httpClient.post(request); + + await _handleResponse(response); + final Map json = jsonDecode(response.body); + return ( + items: _getItemsItems(json['items']), + ); + } + + static List _getItemsItems(dynamic v0) { + final List r0 = []; + for (dynamic v1 in v0) { + + final ItemSummary r1 = ItemSummary.fromJson(v1); + r0.add(r1); + } + return r0; } - return r0; - } - - @override - Future<({Item item})> getItem(String itemId) async { - final String body = jsonEncode(toJsonObject({ - 'itemId': itemId, - })); - WebrpcHttpRequest request = WebrpcHttpRequest( - uri: _makeUri('GetItem'), - headers: _makeHeaders(body), - body: body, - ); - final WebrpcHttpResponse response = await _httpClient.post(request); - - await _handleResponse(response); - final Map json = jsonDecode(response.body); - return (item: _getItemItem(json['item']),); - } - - static Item _getItemItem(dynamic v0) { - final Item r0 = Item.fromJson(v0); - return r0; - } - - @override - Future createItem(CreateItemRequest item) async { - final String body = jsonEncode(toJsonObject({ - 'item': item, - })); - WebrpcHttpRequest request = WebrpcHttpRequest( - uri: _makeUri('CreateItem'), - headers: _makeHeaders(body), - body: body, - ); - final WebrpcHttpResponse response = await _httpClient.post(request); - - await _handleResponse(response); - } - - @override - Future putOne(String itemId) async { - final String body = jsonEncode(toJsonObject({ - 'itemId': itemId, - })); - WebrpcHttpRequest request = WebrpcHttpRequest( - uri: _makeUri('PutOne'), - headers: _makeHeaders(body), - body: body, - ); - final WebrpcHttpResponse response = await _httpClient.post(request); - await _handleResponse(response); - } + @override + Future<({Item item})> getItem(String itemId) async { + final String body = jsonEncode(toJsonObject({ + 'itemId': itemId, + })); + WebrpcHttpRequest request = WebrpcHttpRequest( + uri: _makeUri('GetItem'), + headers: _makeHeaders(), + body: body, + ); + final WebrpcHttpResponse response = await _httpClient.post(request); + + await _handleResponse(response); + final Map json = jsonDecode(response.body); + return ( + item: _getItemItem(json['item']), + ); + } + + static Item _getItemItem(dynamic v0) { + final Item r0 = Item.fromJson(v0); + return r0; + } - @override - Future takeOne(String itemId) async { - final String body = jsonEncode(toJsonObject({ - 'itemId': itemId, - })); - WebrpcHttpRequest request = WebrpcHttpRequest( - uri: _makeUri('TakeOne'), - headers: _makeHeaders(body), - body: body, - ); - final WebrpcHttpResponse response = await _httpClient.post(request); + @override + Future createItem(CreateItemRequest item) async { + final String body = jsonEncode(toJsonObject({ + 'item': item, + })); + WebrpcHttpRequest request = WebrpcHttpRequest( + uri: _makeUri('CreateItem'), + headers: _makeHeaders(), + body: body, + ); + final WebrpcHttpResponse response = await _httpClient.post(request); + + await _handleResponse(response); + } - await _handleResponse(response); - } + @override + Future putOne(String itemId) async { + final String body = jsonEncode(toJsonObject({ + 'itemId': itemId, + })); + WebrpcHttpRequest request = WebrpcHttpRequest( + uri: _makeUri('PutOne'), + headers: _makeHeaders(), + body: body, + ); + final WebrpcHttpResponse response = await _httpClient.post(request); + + await _handleResponse(response); + } - @override - Future deleteItem(String itemId) async { - final String body = jsonEncode(toJsonObject({ - 'itemId': itemId, - })); - WebrpcHttpRequest request = WebrpcHttpRequest( - uri: _makeUri('DeleteItem'), - headers: _makeHeaders(body), - body: body, - ); - final WebrpcHttpResponse response = await _httpClient.post(request); + @override + Future takeOne(String itemId) async { + final String body = jsonEncode(toJsonObject({ + 'itemId': itemId, + })); + WebrpcHttpRequest request = WebrpcHttpRequest( + uri: _makeUri('TakeOne'), + headers: _makeHeaders(), + body: body, + ); + final WebrpcHttpResponse response = await _httpClient.post(request); + + await _handleResponse(response); + } - await _handleResponse(response); - } + @override + Future deleteItem(String itemId) async { + final String body = jsonEncode(toJsonObject({ + 'itemId': itemId, + })); + WebrpcHttpRequest request = WebrpcHttpRequest( + uri: _makeUri('DeleteItem'), + headers: _makeHeaders(), + body: body, + ); + final WebrpcHttpResponse response = await _httpClient.post(request); + + await _handleResponse(response); + } - Uri _makeUri(String name) { - return Uri.parse(_baseUrl + name); - } + Uri _makeUri(String name) { + return Uri.parse(_baseUrl + name); + } - static Map _makeHeaders(String? body) { - return { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }; - } + static Map _makeHeaders() { + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + } - Future _handleResponse(WebrpcHttpResponse response) async { - if (response.statusCode >= 400) { - try { - final Map json = jsonDecode(response.body); - final int webrpcErrorCode = json['code']; - if (response.statusCode >= 500) { - throw WebrpcException.fromCode(webrpcErrorCode); - } else { - throw WebrpcError.fromCode(webrpcErrorCode); + Future _handleResponse(WebrpcHttpResponse response) async { + if (response.statusCode >= 400) { + try { + final Map json = jsonDecode(response.body); + final int webrpcErrorCode = json['code']; + if (response.statusCode >= 500) { + throw WebrpcException.fromCode(webrpcErrorCode); + + } else { + throw WebrpcError.fromCode(webrpcErrorCode); + } + } on ArgumentError catch (_) { + // https://github.com/webrpc/webrpc/blob/master/gen/errors.go + throw WebrpcException.fromCode(-5); + } } - } on ArgumentError catch (_) { - // https://github.com/webrpc/webrpc/blob/master/gen/errors.go - throw WebrpcException.fromCode(-5); - } } - } } class _MainWebrpcHttpClient implements WebrpcHttpClient { @@ -196,388 +202,399 @@ class WebrpcHttpResponse { final String body; } + enum ItemTier { - REGULAR, - PREMIUM; - - factory ItemTier.fromJson(dynamic json) { - switch (json) { - case 'REGULAR': - return ItemTier.REGULAR; - case 'PREMIUM': - return ItemTier.PREMIUM; - default: - throw ArgumentError.value(json); + REGULAR, + PREMIUM; + + factory ItemTier.fromJson(dynamic json) { + switch (json) { + case 'REGULAR': + return ItemTier.REGULAR; + case 'PREMIUM': + return ItemTier.PREMIUM; + default: + throw ArgumentError.value(json); + } } - } - String toJson() { - return name; - } + String toJson() { + return name; + } } class Item implements JsonSerializable { - Item( - {required this.id, - required this.name, - required this.tier, - required this.count, - required this.createdAt, - required this.lastUpdate}); - - final String id; - final String name; - final ItemTier tier; - final int count; - final DateTime createdAt; - final DateTime? lastUpdate; - - Item.fromJson(Map json) - : id = _id(json['id']), - name = _name(json['name']), - tier = _tier(json['tier']), - count = _count(json['count']), - createdAt = _createdAt(json['createdAt']), - lastUpdate = _lastUpdate(json['lastUpdate']); - - static String _id(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final String r0 = _cast(v0); - return r0; - } - - static String _name(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final String r0 = _cast(v0); - return r0; - } - - static ItemTier _tier(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final ItemTier r0 = ItemTier.fromJson(v0); - return r0; - } - - static int _count(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final int r0 = _cast(v0); - return r0; - } - - static DateTime _createdAt(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final r0 = _dateTimeFromJson(v0); - return r0; - } - - static DateTime? _lastUpdate(dynamic v0) { - if (v0 == null) return null; - final r0 = _dateTimeFromJsonOptional(v0); - return r0; - } - - @override - Map toJson() { - return { - 'id': toJsonObject(id), - 'name': toJsonObject(name), - 'tier': toJsonObject(tier), - 'count': toJsonObject(count), - 'createdAt': toJsonObject(createdAt), - 'lastUpdate': toJsonObject(lastUpdate), - }; - } + Item({ + required this.id, + required this.name, + required this.tier, + required this.count, + required this.createdAt, + required this.lastUpdate + }); + + final String id; + final String name; + final ItemTier tier; + final int count; + final DateTime createdAt; + final DateTime? lastUpdate; + + Item.fromJson(Map json) + : id = _id(json['id']), + name = _name(json['name']), + tier = _tier(json['tier']), + count = _count(json['count']), + createdAt = _createdAt(json['createdAt']), + lastUpdate = _lastUpdate(json['lastUpdate']); + + static String _id(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final String r0 = _cast(v0); + return r0; + } + + static String _name(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final String r0 = _cast(v0); + return r0; + } + + static ItemTier _tier(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final ItemTier r0 = ItemTier.fromJson(v0); + return r0; + } + + static int _count(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final int r0 = _cast(v0); + return r0; + } + + static DateTime _createdAt(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final r0 = _dateTimeFromJson(v0); + return r0; + } + + static DateTime? _lastUpdate(dynamic v0) { + if (v0 == null) return null; + final r0 = _dateTimeFromJsonOptional(v0); + return r0; + } + + @override + Map toJson() { + return { + 'id': toJsonObject(id), + 'name': toJsonObject(name), + 'tier': toJsonObject(tier), + 'count': toJsonObject(count), + 'createdAt': toJsonObject(createdAt), + 'lastUpdate': toJsonObject(lastUpdate), + }; + } } class CreateItemRequest implements JsonSerializable { - CreateItemRequest({required this.name, required this.tier}); - - final String name; - final ItemTier tier; - - CreateItemRequest.fromJson(Map json) - : name = _name(json['name']), - tier = _tier(json['tier']); - - static String _name(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final String r0 = _cast(v0); - return r0; - } - - static ItemTier _tier(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final ItemTier r0 = ItemTier.fromJson(v0); - return r0; - } - - @override - Map toJson() { - return { - 'name': toJsonObject(name), - 'tier': toJsonObject(tier), - }; - } + CreateItemRequest({ + required this.name, + required this.tier + }); + + final String name; + final ItemTier tier; + + CreateItemRequest.fromJson(Map json) + : name = _name(json['name']), + tier = _tier(json['tier']); + + static String _name(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final String r0 = _cast(v0); + return r0; + } + + static ItemTier _tier(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final ItemTier r0 = ItemTier.fromJson(v0); + return r0; + } + + @override + Map toJson() { + return { + 'name': toJsonObject(name), + 'tier': toJsonObject(tier), + }; + } } class ItemSummary implements JsonSerializable { - ItemSummary({required this.id, required this.name}); - - final String id; - final String name; - - ItemSummary.fromJson(Map json) - : id = _id(json['id']), - name = _name(json['name']); - - static String _id(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final String r0 = _cast(v0); - return r0; - } - - static String _name(dynamic v0) { - if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); - final String r0 = _cast(v0); - return r0; - } - - @override - Map toJson() { - return { - 'id': toJsonObject(id), - 'name': toJsonObject(name), - }; - } + ItemSummary({ + required this.id, + required this.name + }); + + final String id; + final String name; + + ItemSummary.fromJson(Map json) + : id = _id(json['id']), + name = _name(json['name']); + + static String _id(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final String r0 = _cast(v0); + return r0; + } + + static String _name(dynamic v0) { + if (v0 == null) throw WebrpcException.fromCode(ErrorId.webrpcBadResponse.code); + final String r0 = _cast(v0); + return r0; + } + + @override + Map toJson() { + return { + 'id': toJsonObject(id), + 'name': toJsonObject(name), + }; + } } + T _cast(x) { - if ((x == null) && (null is T)) { - return x; - } else if (x is T) { - return x; - } else { - throw ArgumentError.value(x); - } + if ((x == null) && (null is T)) { + return x; + } else if (x is T) { + return x; + } else { + throw ArgumentError.value(x); + } } dynamic toJsonObject(dynamic v) { - if (v == null) return null; - if (v is DateTime) return v.toIso8601String(); - if (v is BigInt) return v.toString(); - // records are impossible to JSON serialize accurately because they do not - // retain runtime info about their structure - // see https://github.com/dart-lang/language/issues/2826 - if (v is Record) return v.toString(); - if (v is List) return v.map(toJsonObject).toList(); - if (v is Map) return v.map((key, value) => MapEntry(key.toString(), toJsonObject(value))); - if (v is JsonSerializable) return v.toJson(); - return v; + if (v == null) return null; + if (v is DateTime) return v.toIso8601String(); + if (v is BigInt) return v.toString(); + // records are impossible to JSON serialize accurately because they do not + // retain runtime info about their structure + // see https://github.com/dart-lang/language/issues/2826 + if (v is Record) return v.toString(); + if (v is List) return v.map(toJsonObject).toList(); + if (v is Map) return v.map((key, value) => MapEntry(key.toString(), toJsonObject(value))); + if (v is JsonSerializable) return v.toJson(); + return v; } DateTime? _dateTimeFromJsonOptional(dynamic v0) { - if (v0 == null) return null; - return _dateTimeFromJson(v0); + if (v0 == null) return null; + return _dateTimeFromJson(v0); } DateTime _dateTimeFromJson(dynamic v0) { - if ((v0 != null) && (v0 is String)) { - return DateTime.parse(v0); - } else { - throw ArgumentError.value(v0, "v0", "Cannot parse to DateTime"); - } + if ((v0 != null) && (v0 is String)) { + return DateTime.parse(v0); + } else { + throw ArgumentError.value(v0, "v0", "Cannot parse to DateTime"); + } } BigInt? _bigIntFromJsonOptional(dynamic v0) { - if (v0 == null) return null; - return _bigIntFromJson(v0); + if (v0 == null) return null; + return _bigIntFromJson(v0); } BigInt _bigIntFromJson(dynamic v0) { - if (v0 is String) { - return BigInt.parse(v0); - } else if (v0 is int) { - return BigInt.from(v0); - } else { - throw ArgumentError.value(v0, "v0", "Required non-null BigInt"); - } + if (v0 is String) { + return BigInt.parse(v0); + } else if (v0 is int) { + return BigInt.from(v0); + } else { + throw ArgumentError.value(v0, "v0", "Required non-null BigInt"); + } } abstract interface class JsonSerializable { - dynamic toJson(); + dynamic toJson(); } + abstract interface class ExampleService { - Future<({List items})> getItems(); - Future<({Item item})> getItem(String itemId); - Future createItem(CreateItemRequest item); - Future putOne(String itemId); - Future takeOne(String itemId); - Future deleteItem(String itemId); + Future<({List items})> getItems(); + Future<({Item item})> getItem(String itemId); + Future createItem(CreateItemRequest item); + Future putOne(String itemId); + Future takeOne(String itemId); + Future deleteItem(String itemId); } /// Unrecoverable errors representing an invalid use of the API, bad schema, or /// failure in the core of Webrpc (i.e. a bug). class WebrpcError extends Error { - WebrpcError._({ - required this.id, - required this.message, - required this.httpStatus, - }); - - factory WebrpcError.fromCode(int code) { - switch (code) { - case 0: - return WebrpcError._( - id: ErrorId.webrpcEndpoint, - message: 'endpoint error', - httpStatus: 400, - ); - - case -1: - return WebrpcError._( - id: ErrorId.webrpcRequestFailed, - message: 'request failed', - httpStatus: 400, - ); - - case -2: - return WebrpcError._( - id: ErrorId.webrpcBadRoute, - message: 'bad route', - httpStatus: 404, - ); - - case -3: - return WebrpcError._( - id: ErrorId.webrpcBadMethod, - message: 'bad method', - httpStatus: 405, - ); - - case -4: - return WebrpcError._( - id: ErrorId.webrpcBadRequest, - message: 'bad request', - httpStatus: 400, - ); - - case -8: - return WebrpcError._( - id: ErrorId.webrpcClientDisconnected, - message: 'client disconnected', - httpStatus: 400, - ); - - case -9: - return WebrpcError._( - id: ErrorId.webrpcStreamLost, - message: 'stream lost', - httpStatus: 400, - ); - - case -10: - return WebrpcError._( - id: ErrorId.webrpcStreamFinished, - message: 'stream finished', - httpStatus: 200, - ); - - case 1: - return WebrpcError._( - id: ErrorId.itemExists, - message: 'item already exists', - httpStatus: 409, - ); - - case 2: - return WebrpcError._( - id: ErrorId.noSuchItem, - message: 'no such item', - httpStatus: 404, - ); - - case 3: - return WebrpcError._( - id: ErrorId.outOfStock, - message: 'item out of stock', - httpStatus: 409, - ); - - default: - throw ArgumentError.value(code, "code", "Unrecognized"); + WebrpcError._({ + required this.id, + required this.message, + required this.httpStatus, + }); + + factory WebrpcError.fromCode(int code) { + switch (code) { + case 0: + return WebrpcError._( + id: ErrorId.webrpcEndpoint, + message: 'endpoint error', + httpStatus: 400, + ); + + case -1: + return WebrpcError._( + id: ErrorId.webrpcRequestFailed, + message: 'request failed', + httpStatus: 400, + ); + + case -2: + return WebrpcError._( + id: ErrorId.webrpcBadRoute, + message: 'bad route', + httpStatus: 404, + ); + + case -3: + return WebrpcError._( + id: ErrorId.webrpcBadMethod, + message: 'bad method', + httpStatus: 405, + ); + + case -4: + return WebrpcError._( + id: ErrorId.webrpcBadRequest, + message: 'bad request', + httpStatus: 400, + ); + + case -8: + return WebrpcError._( + id: ErrorId.webrpcClientAborted, + message: 'request aborted by client', + httpStatus: 400, + ); + + case -9: + return WebrpcError._( + id: ErrorId.webrpcStreamLost, + message: 'stream lost', + httpStatus: 400, + ); + + case -10: + return WebrpcError._( + id: ErrorId.webrpcStreamFinished, + message: 'stream finished', + httpStatus: 200, + ); + + case 1: + return WebrpcError._( + id: ErrorId.itemExists, + message: 'item already exists', + httpStatus: 409, + ); + + case 2: + return WebrpcError._( + id: ErrorId.noSuchItem, + message: 'no such item', + httpStatus: 404, + ); + + case 3: + return WebrpcError._( + id: ErrorId.outOfStock, + message: 'item out of stock', + httpStatus: 409, + ); + + default: + throw ArgumentError.value(code, "code", "Unrecognized"); + } } - } - final ErrorId id; - final String message; - final int httpStatus; + final ErrorId id; + final String message; + final int httpStatus; } /// Recoverable errors that should generally be caught, representing a /// bad state or temporary failure. class WebrpcException implements Exception { - WebrpcException._({ - required this.id, - required this.message, - required this.httpStatus, - }); - - factory WebrpcException.fromCode(int code) { - switch (code) { - case -5: - return WebrpcException._( - id: ErrorId.webrpcBadResponse, - message: 'bad response', - httpStatus: 500, - ); - - case -6: - return WebrpcException._( - id: ErrorId.webrpcServerPanic, - message: 'server panic', - httpStatus: 500, - ); - - case -7: - return WebrpcException._( - id: ErrorId.webrpcInternalError, - message: 'internal error', - httpStatus: 500, - ); - - default: - throw ArgumentError.value(code, "code", "Unrecognized code $code"); + WebrpcException._({ + required this.id, + required this.message, + required this.httpStatus, + }); + + factory WebrpcException.fromCode(int code) { + switch (code) { + case -5: + return WebrpcException._( + id: ErrorId.webrpcBadResponse, + message: 'bad response', + httpStatus: 500, + ); + + case -6: + return WebrpcException._( + id: ErrorId.webrpcServerPanic, + message: 'server panic', + httpStatus: 500, + ); + + case -7: + return WebrpcException._( + id: ErrorId.webrpcInternalError, + message: 'internal error', + httpStatus: 500, + ); + + default: + throw ArgumentError.value(code, "code", "Unrecognized code $code"); + } } - } - final ErrorId id; - final String message; - final int httpStatus; + final ErrorId id; + final String message; + final int httpStatus; } /// Unique ID of a custom schema error or base Webrpc error. enum ErrorId { - webrpcEndpoint(code: 0, name: 'WebrpcEndpoint'), - webrpcRequestFailed(code: -1, name: 'WebrpcRequestFailed'), - webrpcBadRoute(code: -2, name: 'WebrpcBadRoute'), - webrpcBadMethod(code: -3, name: 'WebrpcBadMethod'), - webrpcBadRequest(code: -4, name: 'WebrpcBadRequest'), - webrpcBadResponse(code: -5, name: 'WebrpcBadResponse'), - webrpcServerPanic(code: -6, name: 'WebrpcServerPanic'), - webrpcInternalError(code: -7, name: 'WebrpcInternalError'), - webrpcClientDisconnected(code: -8, name: 'WebrpcClientDisconnected'), - webrpcStreamLost(code: -9, name: 'WebrpcStreamLost'), - webrpcStreamFinished(code: -10, name: 'WebrpcStreamFinished'), - itemExists(code: 1, name: 'ItemExists'), - noSuchItem(code: 2, name: 'NoSuchItem'), - outOfStock(code: 3, name: 'OutOfStock'); - - const ErrorId({ - required this.code, - required this.name, - }); - - final int code; - final String name; + webrpcEndpoint(code: 0, name: 'WebrpcEndpoint'), + webrpcRequestFailed(code: -1, name: 'WebrpcRequestFailed'), + webrpcBadRoute(code: -2, name: 'WebrpcBadRoute'), + webrpcBadMethod(code: -3, name: 'WebrpcBadMethod'), + webrpcBadRequest(code: -4, name: 'WebrpcBadRequest'), + webrpcBadResponse(code: -5, name: 'WebrpcBadResponse'), + webrpcServerPanic(code: -6, name: 'WebrpcServerPanic'), + webrpcInternalError(code: -7, name: 'WebrpcInternalError'), + webrpcClientAborted(code: -8, name: 'WebrpcClientAborted'), + webrpcStreamLost(code: -9, name: 'WebrpcStreamLost'), + webrpcStreamFinished(code: -10, name: 'WebrpcStreamFinished'), + itemExists(code: 1, name: 'ItemExists'), + noSuchItem(code: 2, name: 'NoSuchItem'), + outOfStock(code: 3, name: 'OutOfStock'); + + const ErrorId({ + required this.code, + required this.name, + }); + + final int code; + final String name; } + diff --git a/_examples/flutter-go/go_server/proto/server.gen.go b/_examples/flutter-go/go_server/proto/server.gen.go index 64f4b0a..3943302 100644 --- a/_examples/flutter-go/go_server/proto/server.gen.go +++ b/_examples/flutter-go/go_server/proto/server.gen.go @@ -1,8 +1,8 @@ -// flutter-go v1.0.0 2a329e90418316407f201f4e41ea13510b20bc92 +// flutter-go v1.0.0 287a5e9398d731d587d1910016b4981fe7fe01a4 // -- -// Code generated by webrpc-gen@v0.18.2 with golang generator. DO NOT EDIT. +// Code generated by webrpc-gen@v0.32.3 with ../../../gen-golang generator. DO NOT EDIT. // -// webrpc-gen -schema=./service.ridl -target=golang -server -out=go_server/generated/server.gen.go +// webrpc-gen -schema=./service.ridl -target=../../../gen-golang -server -out=go_server/proto/server.gen.go package proto import ( @@ -28,11 +28,24 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "2a329e90418316407f201f4e41ea13510b20bc92" + return "287a5e9398d731d587d1910016b4981fe7fe01a4" } // -// Common types +// Server interface +// + +type ExampleServiceServer interface { + GetItems(ctx context.Context) ([]*ItemSummary, error) + GetItem(ctx context.Context, itemId string) (*Item, error) + CreateItem(ctx context.Context, item *CreateItemRequest) error + PutOne(ctx context.Context, itemId string) error + TakeOne(ctx context.Context, itemId string) error + DeleteItem(ctx context.Context, itemId string) error +} + +// +// Schema types // type ItemTier uint32 @@ -96,43 +109,6 @@ type ItemSummary struct { Name string `json:"name"` } -var WebRPCServices = map[string][]string{ - "ExampleService": { - "GetItems", - "GetItem", - "CreateItem", - "PutOne", - "TakeOne", - "DeleteItem", - }, -} - -// -// Server types -// - -type ExampleService interface { - GetItems(ctx context.Context) ([]*ItemSummary, error) - GetItem(ctx context.Context, itemId string) (*Item, error) - CreateItem(ctx context.Context, item *CreateItemRequest) error - PutOne(ctx context.Context, itemId string) error - TakeOne(ctx context.Context, itemId string) error - DeleteItem(ctx context.Context, itemId string) error -} - -// -// Client types -// - -type ExampleServiceClient interface { - GetItems(ctx context.Context) ([]*ItemSummary, error) - GetItem(ctx context.Context, itemId string) (*Item, error) - CreateItem(ctx context.Context, item *CreateItemRequest) error - PutOne(ctx context.Context, itemId string) error - TakeOne(ctx context.Context, itemId string) error - DeleteItem(ctx context.Context, itemId string) error -} - // // Server // @@ -141,54 +117,59 @@ type WebRPCServer interface { http.Handler } -type exampleServiceServer struct { - ExampleService - OnError func(r *http.Request, rpcErr *WebRPCError) +type exampleServiceService struct { + ExampleServiceServer + OnError func(r *http.Request, rpcErr *WebRPCError) + OnRequest func(w http.ResponseWriter, r *http.Request) error } -func NewExampleServiceServer(svc ExampleService) *exampleServiceServer { - return &exampleServiceServer{ - ExampleService: svc, +func NewExampleServiceServer(svc ExampleServiceServer) *exampleServiceService { + return &exampleServiceService{ + ExampleServiceServer: svc, } } -func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (s *exampleServiceService) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer func() { // In case of a panic, serve a HTTP 500 error and then panic. if rr := recover(); rr != nil { - s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCause(fmt.Errorf("%v", rr))) + s.sendErrorJSON(w, r, ErrWebrpcServerPanic.WithCausef("%v", rr)) panic(rr) } }() + w.Header().Set(WebrpcHeader, WebrpcHeaderValue) + ctx := r.Context() ctx = context.WithValue(ctx, HTTPResponseWriterCtxKey, w) ctx = context.WithValue(ctx, HTTPRequestCtxKey, r) ctx = context.WithValue(ctx, ServiceNameCtxKey, "ExampleService") + r = r.WithContext(ctx) + var handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) switch r.URL.Path { - case "/rpc/ExampleService/GetItems": + case "/v1/ExampleService/GetItems": handler = s.serveGetItemsJSON - case "/rpc/ExampleService/GetItem": + case "/v1/ExampleService/GetItem": handler = s.serveGetItemJSON - case "/rpc/ExampleService/CreateItem": + case "/v1/ExampleService/CreateItem": handler = s.serveCreateItemJSON - case "/rpc/ExampleService/PutOne": + case "/v1/ExampleService/PutOne": handler = s.servePutOneJSON - case "/rpc/ExampleService/TakeOne": + case "/v1/ExampleService/TakeOne": handler = s.serveTakeOneJSON - case "/rpc/ExampleService/DeleteItem": + case "/v1/ExampleService/DeleteItem": handler = s.serveDeleteItemJSON default: - err := ErrWebrpcBadRoute.WithCause(fmt.Errorf("no handler for path %q", r.URL.Path)) + err := ErrWebrpcBadRoute.WithCausef("no webrpc method defined for path %v", r.URL.Path) s.sendErrorJSON(w, r, err) return } if r.Method != "POST" { w.Header().Add("Allow", "POST") // RFC 9110. - err := ErrWebrpcBadMethod.WithCause(fmt.Errorf("unsupported method %q (only POST is allowed)", r.Method)) + err := ErrWebrpcBadMethod.WithCausef("unsupported HTTP method %v (only POST is allowed)", r.Method) s.sendErrorJSON(w, r, err) return } @@ -201,18 +182,29 @@ func (s *exampleServiceServer) ServeHTTP(w http.ResponseWriter, r *http.Request) switch contentType { case "application/json": + if s.OnRequest != nil { + if err := s.OnRequest(w, r); err != nil { + rpcErr, ok := err.(WebRPCError) + if !ok { + rpcErr = ErrWebrpcEndpoint.WithCause(err) + } + s.sendErrorJSON(w, r, rpcErr) + return + } + } + handler(ctx, w, r) default: - err := ErrWebrpcBadRequest.WithCause(fmt.Errorf("unexpected Content-Type: %q", r.Header.Get("Content-Type"))) + err := ErrWebrpcBadRequest.WithCausef("unsupported Content-Type %q (only application/json is allowed)", r.Header.Get("Content-Type")) s.sendErrorJSON(w, r, err) } } -func (s *exampleServiceServer) serveGetItemsJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { +func (s *exampleServiceService) serveGetItemsJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { ctx = context.WithValue(ctx, MethodNameCtxKey, "GetItems") // Call service method implementation. - ret0, err := s.ExampleService.GetItems(ctx) + ret0, err := s.ExampleServiceServer.GetItems(ctx) if err != nil { rpcErr, ok := err.(WebRPCError) if !ok { @@ -227,7 +219,7 @@ func (s *exampleServiceServer) serveGetItemsJSON(ctx context.Context, w http.Res }{ret0} respBody, err := json.Marshal(respPayload) if err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to marshal json response: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -236,12 +228,12 @@ func (s *exampleServiceServer) serveGetItemsJSON(ctx context.Context, w http.Res w.Write(respBody) } -func (s *exampleServiceServer) serveGetItemJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { +func (s *exampleServiceService) serveGetItemJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { ctx = context.WithValue(ctx, MethodNameCtxKey, "GetItem") reqBody, err := io.ReadAll(r.Body) if err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to read request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() @@ -250,12 +242,12 @@ func (s *exampleServiceServer) serveGetItemJSON(ctx context.Context, w http.Resp Arg0 string `json:"itemId"` }{} if err := json.Unmarshal(reqBody, &reqPayload); err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to unmarshal request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } // Call service method implementation. - ret0, err := s.ExampleService.GetItem(ctx, reqPayload.Arg0) + ret0, err := s.ExampleServiceServer.GetItem(ctx, reqPayload.Arg0) if err != nil { rpcErr, ok := err.(WebRPCError) if !ok { @@ -270,7 +262,7 @@ func (s *exampleServiceServer) serveGetItemJSON(ctx context.Context, w http.Resp }{ret0} respBody, err := json.Marshal(respPayload) if err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCause(fmt.Errorf("failed to marshal json response: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadResponse.WithCausef("failed to marshal json response: %w", err)) return } @@ -279,12 +271,12 @@ func (s *exampleServiceServer) serveGetItemJSON(ctx context.Context, w http.Resp w.Write(respBody) } -func (s *exampleServiceServer) serveCreateItemJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { +func (s *exampleServiceService) serveCreateItemJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { ctx = context.WithValue(ctx, MethodNameCtxKey, "CreateItem") reqBody, err := io.ReadAll(r.Body) if err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to read request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() @@ -293,12 +285,12 @@ func (s *exampleServiceServer) serveCreateItemJSON(ctx context.Context, w http.R Arg0 *CreateItemRequest `json:"item"` }{} if err := json.Unmarshal(reqBody, &reqPayload); err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to unmarshal request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } // Call service method implementation. - err = s.ExampleService.CreateItem(ctx, reqPayload.Arg0) + err = s.ExampleServiceServer.CreateItem(ctx, reqPayload.Arg0) if err != nil { rpcErr, ok := err.(WebRPCError) if !ok { @@ -313,12 +305,12 @@ func (s *exampleServiceServer) serveCreateItemJSON(ctx context.Context, w http.R w.Write([]byte("{}")) } -func (s *exampleServiceServer) servePutOneJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { +func (s *exampleServiceService) servePutOneJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { ctx = context.WithValue(ctx, MethodNameCtxKey, "PutOne") reqBody, err := io.ReadAll(r.Body) if err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to read request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() @@ -327,12 +319,12 @@ func (s *exampleServiceServer) servePutOneJSON(ctx context.Context, w http.Respo Arg0 string `json:"itemId"` }{} if err := json.Unmarshal(reqBody, &reqPayload); err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to unmarshal request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } // Call service method implementation. - err = s.ExampleService.PutOne(ctx, reqPayload.Arg0) + err = s.ExampleServiceServer.PutOne(ctx, reqPayload.Arg0) if err != nil { rpcErr, ok := err.(WebRPCError) if !ok { @@ -347,12 +339,12 @@ func (s *exampleServiceServer) servePutOneJSON(ctx context.Context, w http.Respo w.Write([]byte("{}")) } -func (s *exampleServiceServer) serveTakeOneJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { +func (s *exampleServiceService) serveTakeOneJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { ctx = context.WithValue(ctx, MethodNameCtxKey, "TakeOne") reqBody, err := io.ReadAll(r.Body) if err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to read request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() @@ -361,12 +353,12 @@ func (s *exampleServiceServer) serveTakeOneJSON(ctx context.Context, w http.Resp Arg0 string `json:"itemId"` }{} if err := json.Unmarshal(reqBody, &reqPayload); err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to unmarshal request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } // Call service method implementation. - err = s.ExampleService.TakeOne(ctx, reqPayload.Arg0) + err = s.ExampleServiceServer.TakeOne(ctx, reqPayload.Arg0) if err != nil { rpcErr, ok := err.(WebRPCError) if !ok { @@ -381,12 +373,12 @@ func (s *exampleServiceServer) serveTakeOneJSON(ctx context.Context, w http.Resp w.Write([]byte("{}")) } -func (s *exampleServiceServer) serveDeleteItemJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { +func (s *exampleServiceService) serveDeleteItemJSON(ctx context.Context, w http.ResponseWriter, r *http.Request) { ctx = context.WithValue(ctx, MethodNameCtxKey, "DeleteItem") reqBody, err := io.ReadAll(r.Body) if err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to read request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to read request data: %w", err)) return } defer r.Body.Close() @@ -395,12 +387,12 @@ func (s *exampleServiceServer) serveDeleteItemJSON(ctx context.Context, w http.R Arg0 string `json:"itemId"` }{} if err := json.Unmarshal(reqBody, &reqPayload); err != nil { - s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCause(fmt.Errorf("failed to unmarshal request data: %w", err))) + s.sendErrorJSON(w, r, ErrWebrpcBadRequest.WithCausef("failed to unmarshal request data: %w", err)) return } // Call service method implementation. - err = s.ExampleService.DeleteItem(ctx, reqPayload.Arg0) + err = s.ExampleServiceServer.DeleteItem(ctx, reqPayload.Arg0) if err != nil { rpcErr, ok := err.(WebRPCError) if !ok { @@ -415,7 +407,7 @@ func (s *exampleServiceServer) serveDeleteItemJSON(ctx context.Context, w http.R w.Write([]byte("{}")) } -func (s *exampleServiceServer) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { +func (s *exampleServiceService) sendErrorJSON(w http.ResponseWriter, r *http.Request, rpcErr WebRPCError) { if s.OnError != nil { s.OnError(r, &rpcErr) } @@ -440,8 +432,76 @@ func RespondWithError(w http.ResponseWriter, err error) { w.Write(respBody) } +type method struct { + name string + service string + annotations map[string]string +} + +func (m *method) Name() string { return m.name } +func (m *method) Service() string { return m.service } +func (m *method) Annotations() map[string]string { return m.annotations } + +var methods = map[string]*method{ + "/v1/ExampleService/GetItems": { + name: "GetItems", + service: "ExampleService", + annotations: map[string]string{}, + }, + "/v1/ExampleService/GetItem": { + name: "GetItem", + service: "ExampleService", + annotations: map[string]string{}, + }, + "/v1/ExampleService/CreateItem": { + name: "CreateItem", + service: "ExampleService", + annotations: map[string]string{}, + }, + "/v1/ExampleService/PutOne": { + name: "PutOne", + service: "ExampleService", + annotations: map[string]string{}, + }, + "/v1/ExampleService/TakeOne": { + name: "TakeOne", + service: "ExampleService", + annotations: map[string]string{}, + }, + "/v1/ExampleService/DeleteItem": { + name: "DeleteItem", + service: "ExampleService", + annotations: map[string]string{}, + }, +} + +func MethodCtx(ctx context.Context) (*method, bool) { + req := RequestFromContext(ctx) + if req == nil { + return nil, false + } + + m, ok := methods[req.URL.Path] + return m, ok +} + +func WebrpcMethods() map[string]*method { + return methods +} + +var WebRPCServices = map[string][]string{ + "ExampleService": { + "GetItems", + "GetItem", + "CreateItem", + "PutOne", + "TakeOne", + "DeleteItem", + }, +} + // -// Helpers +// Webrpc helpers // type contextKey struct { @@ -454,12 +514,9 @@ func (k *contextKey) String() string { var ( HTTPResponseWriterCtxKey = &contextKey{"HTTPResponseWriter"} - - HTTPRequestCtxKey = &contextKey{"HTTPRequest"} - - ServiceNameCtxKey = &contextKey{"ServiceName"} - - MethodNameCtxKey = &contextKey{"MethodName"} + MethodNameCtxKey = &contextKey{"MethodName"} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + ServiceNameCtxKey = &contextKey{"ServiceName"} ) func ServiceNameFromContext(ctx context.Context) string { @@ -476,6 +533,10 @@ func RequestFromContext(ctx context.Context) *http.Request { r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) return r } + +// PtrTo is a useful helper when constructing values for optional fields. +func PtrTo[T any](v T) *T { return &v } + func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter { w, _ := ctx.Value(HTTPResponseWriterCtxKey).(http.ResponseWriter) return w @@ -524,6 +585,14 @@ func (e WebRPCError) WithCause(cause error) WebRPCError { return err } +func (e WebRPCError) WithCausef(format string, args ...interface{}) WebRPCError { + cause := fmt.Errorf(format, args...) + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + // Deprecated: Use .WithCause() method on WebRPCError. func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { return rpcErr.WithCause(cause) @@ -531,17 +600,17 @@ func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { // Webrpc errors var ( - ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} - ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} - ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} - ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} - ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} - ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} - ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} - ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} - ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} - ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} - ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientAborted = WebRPCError{Code: -8, Name: "WebrpcClientAborted", Message: "request aborted by client", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} ) // Schema errors @@ -550,3 +619,58 @@ var ( ErrNoSuchItem = WebRPCError{Code: 2, Name: "NoSuchItem", Message: "no such item", HTTPStatus: 404} ErrOutOfStock = WebRPCError{Code: 3, Name: "OutOfStock", Message: "item out of stock", HTTPStatus: 409} ) + +const WebrpcHeader = "Webrpc" + +const WebrpcHeaderValue = "webrpc@v0.32.3;gen-golang@unknown;flutter-go@v1.0.0" + +type WebrpcGenVersions struct { + WebrpcGenVersion string + CodeGenName string + CodeGenVersion string + SchemaName string + SchemaVersion string +} + +func VersionFromHeader(h http.Header) (*WebrpcGenVersions, error) { + if h.Get(WebrpcHeader) == "" { + return nil, fmt.Errorf("header is empty or missing") + } + + versions, err := parseWebrpcGenVersions(h.Get(WebrpcHeader)) + if err != nil { + return nil, fmt.Errorf("webrpc header is invalid: %w", err) + } + + return versions, nil +} + +func parseWebrpcGenVersions(header string) (*WebrpcGenVersions, error) { + versions := strings.Split(header, ";") + if len(versions) < 3 { + return nil, fmt.Errorf("expected at least 3 parts while parsing webrpc header: %v", header) + } + + _, webrpcGenVersion, ok := strings.Cut(versions[0], "@") + if !ok { + return nil, fmt.Errorf("webrpc gen version could not be parsed from: %s", versions[0]) + } + + tmplTarget, tmplVersion, ok := strings.Cut(versions[1], "@") + if !ok { + return nil, fmt.Errorf("tmplTarget and tmplVersion could not be parsed from: %s", versions[1]) + } + + schemaName, schemaVersion, ok := strings.Cut(versions[2], "@") + if !ok { + return nil, fmt.Errorf("schema name and schema version could not be parsed from: %s", versions[2]) + } + + return &WebrpcGenVersions{ + WebrpcGenVersion: webrpcGenVersion, + CodeGenName: tmplTarget, + CodeGenVersion: tmplVersion, + SchemaName: schemaName, + SchemaVersion: schemaVersion, + }, nil +} diff --git a/_examples/flutter-go/service.ridl b/_examples/flutter-go/service.ridl index dc86be0..edc20af 100644 --- a/_examples/flutter-go/service.ridl +++ b/_examples/flutter-go/service.ridl @@ -2,6 +2,7 @@ webrpc = v1 name = flutter-go version = v1.0.0 +basepath = /v1 service ExampleService - GetItems() => (items: []ItemSummary) diff --git a/client.go.tmpl b/client.go.tmpl index 3ef5b10..6aa1b75 100644 --- a/client.go.tmpl +++ b/client.go.tmpl @@ -2,13 +2,15 @@ {{- $typeMap := .TypeMap -}} {{- $opts := .Opts -}} +{{- $basepath := .BasePath -}} +{{- $services := .Services -}} -{{- if .Services}} +{{- if $services }} -{{- range .Services}} +{{- range $services }} class {{.Name}}Impl implements {{.Name}} { {{.Name}}Impl(String hostname, [WebrpcHttpClient? httpClient]) - : _baseUrl = '$hostname/rpc/{{.Name}}/', + : _baseUrl = '$hostname{{$basepath}}{{.Name}}/', _httpClient = httpClient ?? _MainWebrpcHttpClient(); final String _baseUrl; diff --git a/main.go.tmpl b/main.go.tmpl index 450abdf..bab25cd 100644 --- a/main.go.tmpl +++ b/main.go.tmpl @@ -76,7 +76,7 @@ const webRPCSchemaVersion = "{{.SchemaVersion}}"; const webRPCSchemaHash = "{{.SchemaHash}}"; {{- if $opts.client}} -{{template "client" dict "Services" .Services "Opts" $opts "TypeMap" $typeMap}} +{{template "client" dict "BasePath" .BasePath "Services" .Services "Opts" $opts "TypeMap" $typeMap}} {{- end}} {{- if $opts.server}} diff --git a/tests/schema/custom.ridl b/tests/schema/custom.ridl index 602e99f..d25153c 100644 --- a/tests/schema/custom.ridl +++ b/tests/schema/custom.ridl @@ -2,6 +2,7 @@ webrpc = v1 name = flutter-go version = v1.0.0 +basepath = /v1 service CustomService - CoreTypesRequired(v: CoreTypesRequired) => (v: CoreTypesRequired) diff --git a/tests/test/custom_test.dart b/tests/test/custom_test.dart index 46ad324..0dad7d5 100644 --- a/tests/test/custom_test.dart +++ b/tests/test/custom_test.dart @@ -76,7 +76,7 @@ void main() { } Uri uri(String method) { - return Uri.parse("$baseUrl/rpc/CustomService/$method"); + return Uri.parse("$baseUrl/v1/CustomService/$method"); } test('core type: byte', () async { From 75a12974436cc19571442b50c4b3c9bbad2a89b7 Mon Sep 17 00:00:00 2001 From: Vojtech Vitek Date: Tue, 10 Feb 2026 18:42:48 +0100 Subject: [PATCH 2/3] Update webrpc --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90c3341..8d9de43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - webrpc-version: [v0.18.0] + webrpc-version: [v0.32.3] dart-version: [3.1] - + steps: - uses: actions/checkout@v3 @@ -38,7 +38,7 @@ jobs: - uses: dart-lang/setup-dart@v1 with: - sdk: ${{ matrix.dart-version }} + sdk: ${{ matrix.dart-version }} - name: Download webrpc binaries working-directory: ./tests @@ -54,4 +54,4 @@ jobs: - name: Run interoperability tests working-directory: ./tests - run: ./scripts/test.sh \ No newline at end of file + run: ./scripts/test.sh From 670a0da75a2b9c6fe43aa4deff74f0f265004872 Mon Sep 17 00:00:00 2001 From: Vojtech Vitek Date: Thu, 12 Feb 2026 13:13:36 +0100 Subject: [PATCH 3/3] Try to fix CI error --- _examples/Makefile | 2 ++ fromJson.go.tmpl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 _examples/Makefile diff --git a/_examples/Makefile b/_examples/Makefile new file mode 100644 index 0000000..b8ad1d4 --- /dev/null +++ b/_examples/Makefile @@ -0,0 +1,2 @@ +generate: + make -C ./flutter-go generate diff --git a/fromJson.go.tmpl b/fromJson.go.tmpl index 8a4d2ba..2c39cf3 100644 --- a/fromJson.go.tmpl +++ b/fromJson.go.tmpl @@ -15,7 +15,7 @@ {{$ind}}for (MapEntry e{{$depth}} in v{{$depth}}.entries) { {{$ind}} final dynamic v{{add $depth 1}} = e{{$depth}}.value; {{/* JSON requires all object keys to be strings, so we must double decode any non-string map keys */}} - {{- if eq (get $typeMap (mapKeyType $type)) "String"}} + {{- if eq (mapKeyType $type) "string"}} {{$ind}} final String k{{add $depth 1}} = e{{$depth}}.key; {{- else}} {{$ind}} final dynamic k{{add $depth 1}} = jsonDecode(e{{$depth}}.key);