From 90af9f9bb6726eb8ea2e613d27e003036064cba9 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Thu, 8 Dec 2016 19:24:07 -0500 Subject: [PATCH] Common code :) --- .travis.yml | 1 + README.md | 4 + lib/angel_client.dart | 34 ++++- lib/base_angel_client.dart | 278 +++++++++++++++++++++++++++++++++++++ lib/browser.dart | 234 ++++--------------------------- lib/io.dart | 111 +++++---------- pubspec.yaml | 4 +- test/io_test.dart | 2 +- 8 files changed, 374 insertions(+), 294 deletions(-) create mode 100644 .travis.yml create mode 100644 lib/base_angel_client.dart diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/README.md b/README.md index b439a615..84e85f9e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ # angel_client + +[![pub 1.0.0-dev+16](https://img.shields.io/badge/pub-1.0.0--dev+16-red.svg)](https://pub.dartlang.org/packages/angel_framework) +![build status](https://travis-ci.org/angel-dart/client.svg) + Client library for the Angel framework. # Isomorphic diff --git a/lib/angel_client.dart b/lib/angel_client.dart index 81b10f21..b603c430 100644 --- a/lib/angel_client.dart +++ b/lib/angel_client.dart @@ -2,13 +2,15 @@ library angel_client; import 'dart:async'; -import 'auth_types.dart' as auth_types; +import 'dart:convert'; +export 'package:angel_framework/src/http/angel_http_exception.dart'; /// A function that configures an [Angel] client in some way. typedef Future AngelConfigurer(Angel app); /// Represents an Angel server that we are querying. abstract class Angel { + String get authToken; String basePath; Angel(String this.basePath); @@ -28,11 +30,33 @@ abstract class Angel { } /// Represents the result of authentication with an Angel server. -abstract class AngelAuthResult { - Map get data; - String get token; +class AngelAuthResult { + String _token; + final Map data = {}; + String get token => _token; - Map toJson(); + AngelAuthResult({String token, Map data: const {}}) { + _token = token; + this.data.addAll(data ?? {}); + } + + factory AngelAuthResult.fromMap(Map data) { + final result = new AngelAuthResult(); + + if (data is Map && data.containsKey('token') && data['token'] is String) + result._token = data['token']; + + if (data is Map) result.data.addAll(data['data'] ?? {}); + + return result; + } + + factory AngelAuthResult.fromJson(String json) => + new AngelAuthResult.fromMap(JSON.decode(json)); + + Map toJson() { + return {'token': token, 'data': data}; + } } /// Queries a service on an Angel server, with the same API. diff --git a/lib/base_angel_client.dart b/lib/base_angel_client.dart new file mode 100644 index 00000000..ff6f17e7 --- /dev/null +++ b/lib/base_angel_client.dart @@ -0,0 +1,278 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:angel_framework/src/http/angel_http_exception.dart'; +import 'package:collection/collection.dart'; +import 'package:http/src/base_client.dart' as http; +import 'package:http/src/base_request.dart' as http; +import 'package:http/src/request.dart' as http; +import 'package:http/src/response.dart' as http; +import 'package:http/src/streamed_response.dart' as http; +import 'package:merge_map/merge_map.dart'; +import 'angel_client.dart'; +import 'auth_types.dart' as auth_types; + +const Map _readHeaders = const {'Accept': 'application/json'}; +final Map _writeHeaders = mergeMap([ + _readHeaders, + const {'Content-Type': 'application/json'} +]); + +_buildQuery(Map params) { + if (params == null || params.isEmpty) return ""; + + List query = []; + + params.forEach((k, v) { + query.add('$k=$v'); + }); + + return '?' + query.join('&'); +} + +AngelHttpException failure(http.Response response, {error, StackTrace stack}) { + try { + final json = JSON.decode(response.body); + + if (json is Map && json['isError'] == true) { + return new AngelHttpException.fromMap(json); + } else { + return new AngelHttpException(error, + message: 'Unhandled exception while connecting to Angel backend.', + statusCode: response.statusCode, + stackTrace: stack); + } + } catch (e, st) { + return new AngelHttpException(error ?? e, + message: 'Unhandled exception while connecting to Angel backend.', + statusCode: response.statusCode, + stackTrace: stack ?? st); + } +} + +abstract class BaseAngelClient extends Angel { + @override + String authToken; + + final http.BaseClient client; + + BaseAngelClient(this.client, String basePath) : super(basePath); + + @override + Future authenticate( + {String type: auth_types.LOCAL, + credentials, + String authEndpoint: '/auth', + String reviveEndpoint: '/auth/token'}) async { + if (type == null) { + final url = '$basePath$reviveEndpoint'; + final response = await client.post(url, + headers: mergeMap([ + _writeHeaders, + {'Authorization': 'Bearer ${credentials['token']}'} + ])); + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + final json = JSON.decode(response.body); + + if (json is! Map || + !json.containsKey('data') || + !json.containsKey('token')) { + throw new AngelHttpException.NotAuthenticated( + message: + "Auth endpoint '$url' did not return a proper response."); + } + + return new AngelAuthResult.fromMap(json); + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } else { + final url = '$basePath$authEndpoint/$type'; + http.Response response; + + if (credentials != null) { + response = await client.post(url, + body: JSON.encode(credentials), headers: _writeHeaders); + } else { + response = await client.post(url, headers: _writeHeaders); + } + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + final json = JSON.decode(response.body); + + if (json is! Map || + !json.containsKey('data') || + !json.containsKey('token')) { + throw new AngelHttpException.NotAuthenticated( + message: + "Auth endpoint '$url' did not return a proper response."); + } + + return new AngelAuthResult.fromMap(json); + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } + } + + @override + Service service(String path, {Type type}) { + String uri = path.replaceAll(new RegExp(r"(^/)|(/+$)"), ""); + return new BaseAngelService(client, this, '$basePath/$uri'); + } +} + +class BaseAngelService extends Service { + @override + final Angel app; + final String basePath; + final http.BaseClient client; + + BaseAngelService(this.client, this.app, this.basePath); + + makeBody(x) { + return JSON.encode(x); + } + + /// Sends a non-streaming [Request] and returns a non-streaming [Response]. + Future sendUnstreamed( + String method, url, Map headers, + [body, Encoding encoding]) async { + if (url is String) url = Uri.parse(url); + var request = new http.Request(method, url); + + if (headers != null) request.headers.addAll(headers); + if (encoding != null) request.encoding = encoding; + if (body != null) { + if (body is String) { + request.body = body; + } else if (body is List) { + request.bodyBytes = DelegatingList.typed(body); + } else if (body is Map) { + request.bodyFields = DelegatingMap.typed(body); + } else { + throw new ArgumentError('Invalid request body "$body".'); + } + } + + return http.Response.fromStream(await client.send(request)); + } + + Future send(http.BaseRequest request) { + if (app.authToken != null && app.authToken.isNotEmpty) { + request.headers['Authorization'] = 'Bearer ${app.authToken}'; + } + + return client.send(request); + } + + @override + Future index([Map params]) async { + final response = await sendUnstreamed( + 'GET', '$basePath/${_buildQuery(params)}', _readHeaders); + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + final json = JSON.decode(response.body); + + if (json is! List) { + throw failure(response); + } + + return json; + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } + + @override + Future read(id, [Map params]) async { + final response = await sendUnstreamed( + 'GET', '$basePath/$id${_buildQuery(params)}', _readHeaders); + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + return JSON.decode(response.body); + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } + + @override + Future create(data, [Map params]) async { + final response = await sendUnstreamed( + 'POST', '$basePath/${_buildQuery(params)}', _writeHeaders, makeBody(data)); + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + return JSON.decode(response.body); + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } + + @override + Future modify(id, data, [Map params]) async { + final response = await sendUnstreamed( + 'PATCH', '$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data)); + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + return JSON.decode(response.body); + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } + + @override + Future update(id, data, [Map params]) async { + final response = await sendUnstreamed( + 'POST', '$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data)); + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + return JSON.decode(response.body); + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } + + @override + Future remove(id, [Map params]) async { + final response = await sendUnstreamed( + 'DELETE', '$basePath/$id${_buildQuery(params)}', _readHeaders); + + try { + if (response.statusCode != 200) { + throw failure(response); + } + + return JSON.decode(response.body); + } catch (e, st) { + throw failure(response, error: e, stack: st); + } + } +} diff --git a/lib/browser.dart b/lib/browser.dart index 605ed677..e112f20c 100644 --- a/lib/browser.dart +++ b/lib/browser.dart @@ -1,54 +1,22 @@ /// Browser library for the Angel framework. library angel_client.browser; -import 'dart:async' show Completer, Future; +import 'dart:async' show Future; import 'dart:convert' show JSON; -import 'dart:html' show HttpRequest, window; +import 'dart:html' show window; +import 'package:http/browser_client.dart' as http; import 'angel_client.dart'; import 'auth_types.dart' as auth_types; +import 'base_angel_client.dart'; export 'angel_client.dart'; -_buildQuery(Map params) { - if (params == null || params == {}) return ""; - - String result = ""; - return result; -} - -_send(HttpRequest request, [data]) { - final completer = new Completer(); - - request - ..onLoadEnd.listen((_) { - completer.complete(request.response); - }) - ..onError.listen((_) { - try { - throw new Exception( - 'Request failed with status code ${request.status}.'); - } catch (e, st) { - completer.completeError(e, st); - } - }); - - if (data == null) - request.send(); - else if (data is String) - request.send(data); - else - request.send(JSON.encode(data)); - return completer.future; -} - /// Queries an Angel server via REST. -class Rest extends Angel { - String _authToken; - - Rest(String basePath) : super(basePath); +class Rest extends BaseAngelClient { + Rest(String basePath) : super(new http.BrowserClient(), basePath); @override - Future authenticate( - {String type, + Future authenticate( + {String type: auth_types.LOCAL, credentials, String authEndpoint: '/auth', String reviveEndpoint: '/auth/token'}) async { @@ -58,177 +26,23 @@ class Rest extends Angel { 'Cannot revive token from localStorage - there is none.'); } - final result = new _AngelAuthResultImpl( - token: JSON.decode(window.localStorage['token']), - data: JSON.decode(window.localStorage['user'])); - final completer = new Completer(); - final request = new HttpRequest()..responseType = 'json'; - request.open('POST', '$basePath$reviveEndpoint'); - request.setRequestHeader('Accept', 'application/json'); - request.setRequestHeader('Content-Type', 'application/json'); - request.setRequestHeader('Authorization', 'Bearer ${result.token}'); - - request - ..onLoadEnd.listen((_) { - final result = new _AngelAuthResultImpl.fromMap(request.response); - _authToken = result.token; - window.localStorage['token'] = JSON.encode(result.token); - window.localStorage['user'] = JSON.encode(result.data); - completer.complete(result); - }) - ..onError.listen((_) { - try { - throw new Exception( - 'Request failed with status code ${request.status}.'); - } catch (e, st) { - completer.completeError(e, st); - } - }); - - request.send(JSON.encode(result)); - return completer.future; - } - - final url = '$basePath$authEndpoint/$type'; - - if (type == auth_types.LOCAL) { - final completer = new Completer(); - final request = new HttpRequest(); - request.open('POST', url); - request.responseType = 'json'; - request.setRequestHeader("Accept", "application/json"); - request.setRequestHeader("Content-Type", "application/json"); - - request - ..onLoadEnd.listen((_) { - final result = new _AngelAuthResultImpl.fromMap(request.response); - _authToken = result.token; - window.localStorage['token'] = JSON.encode(result.token); - window.localStorage['user'] = JSON.encode(result.data); - completer.complete(result); - }) - ..onError.listen((_) { - try { - throw new Exception( - 'Request failed with status code ${request.status}.'); - } catch (e, st) { - completer.completeError(e, st); - } - }); - - if (credentials == null) - request.send(); - else - request.send(JSON.encode(credentials)); - - return completer.future; + try { + final result = await super.authenticate( + credentials: {'token': JSON.decode(window.localStorage['token'])}, + reviveEndpoint: reviveEndpoint); + window.localStorage['token'] = JSON.encode(authToken = result.token); + window.localStorage['user'] = JSON.encode(result.data); + return result; + } catch (e, st) { + throw new AngelHttpException(e, + message: 'Failed to revive auth token.', stackTrace: st); + } } else { - throw new Exception('angel_client cannot authenticate as "$type" yet.'); + final result = await super.authenticate( + type: type, credentials: credentials, authEndpoint: authEndpoint); + window.localStorage['token'] = JSON.encode(authToken = result.token); + window.localStorage['user'] = JSON.encode(result.data); + return result; } } - - @override - RestService service(String path, {Type type}) { - String uri = path.replaceAll(new RegExp(r"(^\/)|(\/+$)"), ""); - return new _RestServiceImpl(this, "$basePath/$uri"); - } -} - -abstract class RestService extends Service { - RestService._(String basePath); -} - -class _AngelAuthResultImpl implements AngelAuthResult { - String _token; - final Map data = {}; - String get token => _token; - - _AngelAuthResultImpl({token, Map data: const {}}) { - if (token is String) _token = token; - - this.data.addAll(data ?? {}); - } - - factory _AngelAuthResultImpl.fromMap(Map data) { - final result = new _AngelAuthResultImpl(); - - if (data is Map && data.containsKey('token') && data['token'] is String) - result._token = data['token']; - - if (data is Map) result.data.addAll(data['data'] ?? {}); - - return result; - } - - @override - Map toJson() { - return {'token': token, 'data': data}; - } -} - -/// Queries an Angel service via REST. -class _RestServiceImpl extends RestService { - final Rest app; - String _basePath; - String get basePath => _basePath; - - _RestServiceImpl(this.app, String basePath) : super._(basePath) { - _basePath = basePath; - } - - _makeBody(data) { - return JSON.encode(data); - } - - Future buildRequest(String url, - {String method: "POST", bool write: true}) async { - HttpRequest request = new HttpRequest(); - request.open(method, url); - request.responseType = "json"; - request.setRequestHeader("Accept", "application/json"); - if (write) request.setRequestHeader("Content-Type", "application/json"); - if (app._authToken != null) - request.setRequestHeader("Authorization", "Bearer ${app._authToken}"); - return request; - } - - @override - Future index([Map params]) async { - final request = await buildRequest('$basePath/${_buildQuery(params)}', - method: 'GET', write: false); - return await _send(request); - } - - @override - Future read(id, [Map params]) async { - final request = await buildRequest('$basePath/$id${_buildQuery(params)}', - method: 'GET', write: false); - return await _send(request); - } - - @override - Future create(data, [Map params]) async { - final request = await buildRequest("$basePath/${_buildQuery(params)}"); - return await _send(request, _makeBody(data)); - } - - @override - Future modify(id, data, [Map params]) async { - final request = await buildRequest("$basePath/$id${_buildQuery(params)}", - method: "PATCH"); - return await _send(request, _makeBody(data)); - } - - @override - Future update(id, data, [Map params]) async { - final request = await buildRequest("$basePath/$id${_buildQuery(params)}"); - return await _send(request, _makeBody(data)); - } - - @override - Future remove(id, [Map params]) async { - final request = await buildRequest("$basePath/$id${_buildQuery(params)}", - method: "DELETE"); - return await _send(request); - } } diff --git a/lib/io.dart b/lib/io.dart index cf72778c..0e544e7e 100644 --- a/lib/io.dart +++ b/lib/io.dart @@ -2,109 +2,68 @@ library angel_client.cli; import 'dart:async'; -import 'dart:convert' show JSON; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:json_god/json_god.dart' as god; import 'angel_client.dart'; +import 'base_angel_client.dart'; export 'angel_client.dart'; -_buildQuery(Map params) { - if (params == null || params == {}) - return ""; - - String result = ""; - return result; -} - -const Map _readHeaders = const { - "Accept": "application/json" -}; - -const Map _writeHeaders = const { - "Accept": "application/json", - "Content-Type": "application/json" -}; - /// Queries an Angel server via REST. -class Rest extends Angel { - BaseClient client; - - Rest(String path, BaseClient this.client) :super(path); +class Rest extends BaseAngelClient { + Rest(String path) : super(new http.Client(), path); @override - RestService service(String path, {Type type}) { - String uri = path.replaceAll(new RegExp(r"(^\/)|(\/+$)"), ""); - return new RestService._base("$basePath/$uri", client, type) - ..app = this; + Service service(String path, {Type type}) { + String uri = path.replaceAll(new RegExp(r"(^/)|(/+$)"), ""); + return new RestService(client, this, "$basePath/$uri", type); } } /// Queries an Angel service via REST. -class RestService extends Service { - String basePath; - BaseClient client; - Type outputType; +class RestService extends BaseAngelService { + final Type type; - RestService._base(Pattern path, BaseClient this.client, - Type this.outputType) { - this.basePath = (path is RegExp) ? path.pattern : path; + RestService(http.BaseClient client, Angel app, String url, this.type) + : super(client, app, url); + + deserialize(x) { + if (type != null) { + return god.deserializeDatum(x, outputType: type); + } + + return x; } - _makeBody(data) { - if (outputType == null) - return JSON.encode(data); - else return god.serialize(data); + @override + makeBody(x) { + if (type != null) { + return super.makeBody(god.serializeObject(x)); + } + + return super.makeBody(x); } @override Future index([Map params]) async { - var response = await client.get( - "$basePath/${_buildQuery(params)}", headers: _readHeaders); - - if (outputType == null) - return god.deserialize(response.body); - - else { - return JSON.decode(response.body).map((x) => - god.deserializeDatum(x, outputType: outputType)).toList(); - } + final items = await super.index(params); + return items.map(deserialize).toList(); } @override - Future read(id, [Map params]) async { - var response = await client.get( - "$basePath/$id${_buildQuery(params)}", headers: _readHeaders); - return god.deserialize(response.body, outputType: outputType); - } + Future read(id, [Map params]) => super.read(id, params).then(deserialize); @override - Future create(data, [Map params]) async { - var response = await client.post( - "$basePath/${_buildQuery(params)}", body: _makeBody(data), - headers: _writeHeaders); - return god.deserialize(response.body, outputType: outputType); - } + Future create(data, [Map params]) => + super.create(data, params).then(deserialize); @override - Future modify(id, data, [Map params]) async { - var response = await client.patch( - "$basePath/$id${_buildQuery(params)}", body: _makeBody(data), - headers: _writeHeaders); - return god.deserialize(response.body, outputType: outputType); - } + Future modify(id, data, [Map params]) => + super.modify(id, data, params).then(deserialize); @override - Future update(id, data, [Map params]) async { - var response = await client.patch( - "$basePath/$id${_buildQuery(params)}", body: _makeBody(data), - headers: _writeHeaders); - return god.deserialize(response.body, outputType: outputType); - } + Future update(id, data, [Map params]) => + super.update(id, data, params).then(deserialize); @override - Future remove(id, [Map params]) async { - var response = await client.delete( - "$basePath/$id${_buildQuery(params)}", headers: _readHeaders); - return god.deserialize(response.body, outputType: outputType); - } + Future remove(id, [Map params]) => super.remove(id, params).then(deserialize); } diff --git a/pubspec.yaml b/pubspec.yaml index d9b1ce82..d0e032d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: angel_client -version: 1.0.0-dev+15 +version: 1.0.0-dev+16 description: Client library for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_client dependencies: + angel_framework: ">=1.0.0-dev <2.0.0" http: ">= 0.11.3 < 0.12.0" json_god: ">=2.0.0-beta <3.0.0" merge_map: ">=1.0.0 <2.0.0" dev_dependencies: - angel_framework: ">=1.0.0-dev <2.0.0" test: ">= 0.12.13 < 0.13.0" diff --git a/test/io_test.dart b/test/io_test.dart index 894a3399..d804a485 100644 --- a/test/io_test.dart +++ b/test/io_test.dart @@ -22,7 +22,7 @@ main() { serverApp.use("/postcards", new server.MemoryService()); serverPostcards = serverApp.service("postcards"); - clientApp = new client.Rest(url, new http.Client()); + clientApp = new client.Rest(url); clientPostcards = clientApp.service("postcards"); clientTypedPostcards = clientApp.service("postcards", type: Postcard); });