Skip to content

Commit

Permalink
feat: add "onError" hook to generated clients (#102)
Browse files Browse the repository at this point in the history
* add general onError hook to ts client

* add onError hook to dart client

* fix issue with ts onError hook
add test to ensure onError hook fires in generated ts clients

* improve dart result types

* add onError hooks to kotlin client

* add error hooks to kotlin sse implementation

* add error hooks to swift client

* test onError hooks (swift and ts)

* add integration tests for onError hook (dart and kotlin)

* document error hooks
  • Loading branch information
joshmossas authored Nov 29, 2024
1 parent a98b25f commit e8b01bf
Show file tree
Hide file tree
Showing 32 changed files with 936 additions and 426 deletions.
101 changes: 77 additions & 24 deletions languages/dart/dart-client/lib/request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,26 +126,36 @@ Future<T> parsedArriRequest<T, E extends Exception>(
HttpMethod method = HttpMethod.post,
Map<String, dynamic>? params,
FutureOr<Map<String, String>> Function()? headers,
Function(Object)? onError,
String? clientVersion,
required T Function(String) parser,
}) async {
final result = await arriRequest(
url,
httpClient: httpClient,
method: method,
params: params,
headers: headers,
clientVersion: clientVersion,
);
if (result.statusCode >= 200 && result.statusCode <= 299) {
return parser(utf8.decode(result.bodyBytes));
final http.Response result;

try {
result = await arriRequest(
url,
httpClient: httpClient,
method: method,
params: params,
headers: headers,
clientVersion: clientVersion,
);
if (result.statusCode >= 200 && result.statusCode <= 299) {
return parser(utf8.decode(result.bodyBytes));
}
} catch (err) {
onError?.call(err);
rethrow;
}
throw ArriError.fromResponse(result);
final err = ArriError.fromResponse(result);
onError?.call(err);
throw err;
}

/// Perform a raw HTTP request to an Arri RPC server. This function does not thrown an error. Instead it returns a request result
/// in which both value and the error can be null.
Future<ArriRequestResult<T>> parsedArriRequestSafe<T>(
Future<ArriResult<T>> parsedArriRequestSafe<T>(
String url, {
http.Client? httpClient,
HttpMethod httpMethod = HttpMethod.get,
Expand All @@ -164,22 +174,65 @@ Future<ArriRequestResult<T>> parsedArriRequestSafe<T>(
method: httpMethod,
httpClient: httpClient,
);
return ArriRequestResult(value: result);
return ArriResultOk(result);
} catch (err) {
return ArriRequestResult(error: err is ArriError ? err : null);
return ArriResultErr(
err is ArriError
? err
: ArriError(
code: 0,
message: err.toString(),
data: err,
),
);
}
}

/// Container for holding a request result or a request error
class ArriRequestResult<T> {
final T? value;
final ArriError? error;
const ArriRequestResult({this.value, this.error});
/// Container for holding a request data or a request error
sealed class ArriResult<T> {
bool get isOk;
bool get isErr;
T? get unwrap;
T unwrapOr(T fallback);
ArriError? get unwrapErr;
}

/// Abstract endpoint to use as a base for generated client route enums
abstract class ArriEndpoint {
final String path;
final HttpMethod method;
const ArriEndpoint({required this.path, required this.method});
class ArriResultOk<T> implements ArriResult<T> {
final T _data;
const ArriResultOk(this._data);

@override
bool get isOk => true;

@override
bool get isErr => false;

@override
T get unwrap => _data;

@override
T unwrapOr(T fallback) => _data;

@override
ArriError? get unwrapErr => null;
}

class ArriResultErr<T> implements ArriResult<T> {
final ArriError _err;
const ArriResultErr(this._err);

@override
bool get isErr => true;

@override
bool get isOk => false;

@override
T? get unwrap => null;

@override
ArriError get unwrapErr => _err;

@override
T unwrapOr(T fallback) => fallback;
}
1 change: 1 addition & 0 deletions languages/dart/dart-client/lib/ws.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Future<ArriWebsocketController<TServerMessage, TClientMessage>>
FutureOr<Map<String, String>> Function()? headers,
required TServerMessage Function(String msg) parser,
required String Function(TClientMessage msg) serializer,
Function(Object)? onError,
String? clientVersion,
}) async {
var finalUrl =
Expand Down
4 changes: 1 addition & 3 deletions languages/dart/dart-client/test/arri_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ main() {
test("invalid url", () async {
final response =
await parsedArriRequestSafe(nonExistentUrl, parser: (data) {});
if (response.error != null) {
expect(response.error!.code, equals(500));
}
expect(response.unwrapErr?.code, equals(0));
});

test('auto retry sse', () async {
Expand Down
30 changes: 25 additions & 5 deletions languages/dart/dart-codegen-reference/lib/reference_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ class ExampleClient {
final http.Client? _httpClient;
final String _baseUrl;
final String _clientVersion = "20";
late final FutureOr<Map<String, String>> Function()? _headers;
final FutureOr<Map<String, String>> Function()? _headers;
final Function(Object)? _onError;
ExampleClient({
http.Client? httpClient,
required String baseUrl,
FutureOr<Map<String, String>> Function()? headers,
Function(Object)? onError,
}) : _httpClient = httpClient,
_baseUrl = baseUrl,
_headers = headers;
_headers = headers,
_onError = onError;

Future<NestedObject> sendObject(NestedObject params) async {
return parsedArriRequest(
Expand All @@ -27,28 +30,33 @@ class ExampleClient {
clientVersion: _clientVersion,
params: params.toJson(),
parser: (body) => NestedObject.fromJsonString(body),
onError: _onError,
);
}

ExampleClientBooksService get books => ExampleClientBooksService(
baseUrl: _baseUrl,
headers: _headers,
httpClient: _httpClient,
onError: _onError,
);
}

class ExampleClientBooksService {
final http.Client? _httpClient;
final String _baseUrl;
final String _clientVersion = "20";
late final FutureOr<Map<String, String>> Function()? _headers;
final FutureOr<Map<String, String>> Function()? _headers;
final Function(Object)? _onError;
ExampleClientBooksService({
http.Client? httpClient,
required String baseUrl,
FutureOr<Map<String, String>> Function()? headers,
Function(Object)? onError,
}) : _httpClient = httpClient,
_baseUrl = baseUrl,
_headers = headers;
_headers = headers,
_onError = onError;

/// Get a book
Future<Book> getBook(BookParams params) async {
Expand All @@ -60,6 +68,7 @@ class ExampleClientBooksService {
clientVersion: _clientVersion,
params: params.toJson(),
parser: (body) => Book.fromJsonString(body),
onError: _onError,
);
}

Expand All @@ -74,6 +83,7 @@ class ExampleClientBooksService {
clientVersion: _clientVersion,
params: params.toJson(),
parser: (body) => Book.fromJsonString(body),
onError: _onError,
);
}

Expand Down Expand Up @@ -103,7 +113,16 @@ class ExampleClientBooksService {
onMessage: onMessage,
onOpen: onOpen,
onClose: onClose,
onError: onError,
onError: onError != null && _onError != null
? (err, es) {
_onError?.call(onError);
return onError(err, es);
}
: onError != null
? onError
: _onError != null
? (err, _) => _onError?.call(err)
: null,
);
}

Expand All @@ -114,6 +133,7 @@ class ExampleClientBooksService {
clientVersion: _clientVersion,
parser: (msg) => Book.fromJsonString(msg),
serializer: (msg) => msg.toJsonString(),
onError: _onError,
);
}
}
Expand Down
29 changes: 19 additions & 10 deletions languages/dart/dart-codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,28 +69,37 @@ final service = MyClientUsersService(
);
```

#### Client / Service Options

| name | Type | description |
| ---------- | ------------------------------------------- | ------------------------------------------------------------- |
| httpClient | `http.Client` | Use this to pass in a custom `http.Client` instance |
| baseUrl | `String` | The base url for the backend server |
| headers | `FutureOr<Map<String, String>> Function()?` | A function that returns a Map of headers |
| onError | `Function(Object)?` | A hook that fires whenever any error is thrown by the client. |

### Using Arri Models

All generated models will be immutable. They will have access to the following features:

**Methods**:

- `Map<String, dynamic> toJson()`
- `String toJsonString()`
- `String toUrlQueryParams()`
- `copyWith()`
- `Map<String, dynamic> toJson()`
- `String toJsonString()`
- `String toUrlQueryParams()`
- `copyWith()`

**Factory Methods**:

- `empty()`
- `fromJson(Map<String, dynamic> input)`
- `fromJsonString(String input)`
- `empty()`
- `fromJson(Map<String, dynamic> input)`
- `fromJsonString(String input)`

**Overrides**:

- `==` operator (allows for deep equality checking)
- `hashMap` (allows for deep equality checking)
- `toString` (will print out all properties and values instead of `Instance of X`)
- `==` operator (allows for deep equality checking)
- `hashMap` (allows for deep equality checking)
- `toString` (will print out all properties and values instead of `Instance of X`)

This library was generated with [Nx](https://nx.dev).

Expand Down
8 changes: 6 additions & 2 deletions languages/dart/dart-codegen/src/_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,17 @@ class ${clientName} {
final http.Client? _httpClient;
final String _baseUrl;
final String _clientVersion = "${context.clientVersion ?? ""}";
late final FutureOr<Map<String, String>> Function()? _headers;
final FutureOr<Map<String, String>> Function()? _headers;
final Function(Object)? _onError;
${clientName}({
http.Client? httpClient,
required String baseUrl,
FutureOr<Map<String, String>> Function()? headers,
Function(Object)? onError,
}) : _httpClient = httpClient,
_baseUrl = baseUrl,
_headers = headers;
_headers = headers,
_onError = onError;
${rpcParts.join("\n\n")}
Expand All @@ -184,6 +187,7 @@ ${subServices
baseUrl: _baseUrl,
headers: _headers,
httpClient: _httpClient,
onError: _onError,
);`,
)
.join("\n\n")}
Expand Down
21 changes: 18 additions & 3 deletions languages/dart/dart-codegen/src/procedures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,16 @@ export function dartHttpRpcFromSchema(
onMessage: onMessage,
onOpen: onOpen,
onClose: onClose,
onError: onError,
onError: onError != null && _onError != null
? (err, es) {
_onError?.call(onError);
return onError(err, es);
}
: onError != null
? onError
: _onError != null
? (err, _) => _onError?.call(err)
: null,
);
}`;
}
Expand All @@ -87,6 +96,7 @@ export function dartHttpRpcFromSchema(
clientVersion: _clientVersion,
${paramsType ? "params: params.toJson()," : ""}
parser: (body) ${schema.response ? `=> ${responseType}.fromJsonString(body)` : "{}"},
onError: _onError,
);
}`;
}
Expand Down Expand Up @@ -120,6 +130,7 @@ export function dartWsRpcFromSchema(
clientVersion: _clientVersion,
parser: (msg) ${responseType ? `=> ${responseType}.fromJsonString(msg)` : "{}"},
serializer: (msg) ${paramsType ? "=> msg.toJsonString()" : '=> ""'},
onError: _onError,
);
}`;
}
Expand Down Expand Up @@ -180,14 +191,17 @@ export function dartServiceFromSchema(
final http.Client? _httpClient;
final String _baseUrl;
final String _clientVersion = "${context.clientVersion}";
late final FutureOr<Map<String, String>> Function()? _headers;
final FutureOr<Map<String, String>> Function()? _headers;
final Function(Object)? _onError;
${serviceName}({
http.Client? httpClient,
required String baseUrl,
FutureOr<Map<String, String>> Function()? headers,
Function(Object)? onError,
}) : _httpClient = httpClient,
_baseUrl = baseUrl,
_headers = headers;
_headers = headers,
_onError = onError;
${rpcParts.join("\n\n")}
Expand All @@ -197,6 +211,7 @@ export function dartServiceFromSchema(
baseUrl: _baseUrl,
headers: _headers,
httpClient: _httpClient,
onError: _onError,
);`,
)
.join("\n\n")}
Expand Down
Loading

0 comments on commit e8b01bf

Please sign in to comment.