Common code :)

This commit is contained in:
thosakwe 2016-12-08 19:24:07 -05:00
parent c220a48830
commit 90af9f9bb6
8 changed files with 374 additions and 294 deletions

1
.travis.yml Normal file
View file

@ -0,0 +1 @@
language: dart

View file

@ -1,4 +1,8 @@
# angel_client # 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. Client library for the Angel framework.
# Isomorphic # Isomorphic

View file

@ -2,13 +2,15 @@
library angel_client; library angel_client;
import 'dart:async'; 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. /// A function that configures an [Angel] client in some way.
typedef Future AngelConfigurer(Angel app); typedef Future AngelConfigurer(Angel app);
/// Represents an Angel server that we are querying. /// Represents an Angel server that we are querying.
abstract class Angel { abstract class Angel {
String get authToken;
String basePath; String basePath;
Angel(String this.basePath); Angel(String this.basePath);
@ -28,11 +30,33 @@ abstract class Angel {
} }
/// Represents the result of authentication with an Angel server. /// Represents the result of authentication with an Angel server.
abstract class AngelAuthResult { class AngelAuthResult {
Map<String, dynamic> get data; String _token;
String get token; final Map<String, dynamic> data = {};
String get token => _token;
Map<String, dynamic> toJson(); AngelAuthResult({String token, Map<String, dynamic> 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<String, dynamic> toJson() {
return {'token': token, 'data': data};
}
} }
/// Queries a service on an Angel server, with the same API. /// Queries a service on an Angel server, with the same API.

278
lib/base_angel_client.dart Normal file
View file

@ -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<String, String> _readHeaders = const {'Accept': 'application/json'};
final Map<String, String> _writeHeaders = mergeMap([
_readHeaders,
const {'Content-Type': 'application/json'}
]);
_buildQuery(Map params) {
if (params == null || params.isEmpty) return "";
List<String> 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<AngelAuthResult> 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<http.Response> sendUnstreamed(
String method, url, Map<String, String> 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<http.StreamedResponse> send(http.BaseRequest request) {
if (app.authToken != null && app.authToken.isNotEmpty) {
request.headers['Authorization'] = 'Bearer ${app.authToken}';
}
return client.send(request);
}
@override
Future<List> 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);
}
}
}

View file

@ -1,54 +1,22 @@
/// Browser library for the Angel framework. /// Browser library for the Angel framework.
library angel_client.browser; library angel_client.browser;
import 'dart:async' show Completer, Future; import 'dart:async' show Future;
import 'dart:convert' show JSON; 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 'angel_client.dart';
import 'auth_types.dart' as auth_types; import 'auth_types.dart' as auth_types;
import 'base_angel_client.dart';
export '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<HttpRequest>();
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. /// Queries an Angel server via REST.
class Rest extends Angel { class Rest extends BaseAngelClient {
String _authToken; Rest(String basePath) : super(new http.BrowserClient(), basePath);
Rest(String basePath) : super(basePath);
@override @override
Future authenticate( Future<AngelAuthResult> authenticate(
{String type, {String type: auth_types.LOCAL,
credentials, credentials,
String authEndpoint: '/auth', String authEndpoint: '/auth',
String reviveEndpoint: '/auth/token'}) async { String reviveEndpoint: '/auth/token'}) async {
@ -58,177 +26,23 @@ class Rest extends Angel {
'Cannot revive token from localStorage - there is none.'); '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 { try {
throw new Exception( final result = await super.authenticate(
'Request failed with status code ${request.status}.'); credentials: {'token': JSON.decode(window.localStorage['token'])},
} catch (e, st) { reviveEndpoint: reviveEndpoint);
completer.completeError(e, st); window.localStorage['token'] = JSON.encode(authToken = result.token);
}
});
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); window.localStorage['user'] = JSON.encode(result.data);
completer.complete(result); return result;
})
..onError.listen((_) {
try {
throw new Exception(
'Request failed with status code ${request.status}.');
} catch (e, st) { } catch (e, st) {
completer.completeError(e, st); throw new AngelHttpException(e,
message: 'Failed to revive auth token.', stackTrace: st);
} }
});
if (credentials == null)
request.send();
else
request.send(JSON.encode(credentials));
return completer.future;
} else { } 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);
@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<String, dynamic> data = {};
String get token => _token;
_AngelAuthResultImpl({token, Map<String, dynamic> 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; return result;
} }
@override
Map<String, dynamic> 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<HttpRequest> 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<List> 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);
} }
} }

View file

@ -2,109 +2,68 @@
library angel_client.cli; library angel_client.cli;
import 'dart:async'; import 'dart:async';
import 'dart:convert' show JSON; import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'package:json_god/json_god.dart' as god; import 'package:json_god/json_god.dart' as god;
import 'angel_client.dart'; import 'angel_client.dart';
import 'base_angel_client.dart';
export '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. /// Queries an Angel server via REST.
class Rest extends Angel { class Rest extends BaseAngelClient {
BaseClient client; Rest(String path) : super(new http.Client(), path);
Rest(String path, BaseClient this.client) :super(path);
@override @override
RestService service(String path, {Type type}) { Service service(String path, {Type type}) {
String uri = path.replaceAll(new RegExp(r"(^\/)|(\/+$)"), ""); String uri = path.replaceAll(new RegExp(r"(^/)|(/+$)"), "");
return new RestService._base("$basePath/$uri", client, type) return new RestService(client, this, "$basePath/$uri", type);
..app = this;
} }
} }
/// Queries an Angel service via REST. /// Queries an Angel service via REST.
class RestService extends Service { class RestService extends BaseAngelService {
String basePath; final Type type;
BaseClient client;
Type outputType;
RestService._base(Pattern path, BaseClient this.client, RestService(http.BaseClient client, Angel app, String url, this.type)
Type this.outputType) { : super(client, app, url);
this.basePath = (path is RegExp) ? path.pattern : path;
deserialize(x) {
if (type != null) {
return god.deserializeDatum(x, outputType: type);
} }
_makeBody(data) { return x;
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 @override
Future<List> index([Map params]) async { Future<List> index([Map params]) async {
var response = await client.get( final items = await super.index(params);
"$basePath/${_buildQuery(params)}", headers: _readHeaders); return items.map(deserialize).toList();
if (outputType == null)
return god.deserialize(response.body);
else {
return JSON.decode(response.body).map((x) =>
god.deserializeDatum(x, outputType: outputType)).toList();
}
} }
@override @override
Future read(id, [Map params]) async { Future read(id, [Map params]) => super.read(id, params).then(deserialize);
var response = await client.get(
"$basePath/$id${_buildQuery(params)}", headers: _readHeaders);
return god.deserialize(response.body, outputType: outputType);
}
@override @override
Future create(data, [Map params]) async { Future create(data, [Map params]) =>
var response = await client.post( super.create(data, params).then(deserialize);
"$basePath/${_buildQuery(params)}", body: _makeBody(data),
headers: _writeHeaders);
return god.deserialize(response.body, outputType: outputType);
}
@override @override
Future modify(id, data, [Map params]) async { Future modify(id, data, [Map params]) =>
var response = await client.patch( super.modify(id, data, params).then(deserialize);
"$basePath/$id${_buildQuery(params)}", body: _makeBody(data),
headers: _writeHeaders);
return god.deserialize(response.body, outputType: outputType);
}
@override @override
Future update(id, data, [Map params]) async { Future update(id, data, [Map params]) =>
var response = await client.patch( super.update(id, data, params).then(deserialize);
"$basePath/$id${_buildQuery(params)}", body: _makeBody(data),
headers: _writeHeaders);
return god.deserialize(response.body, outputType: outputType);
}
@override @override
Future remove(id, [Map params]) async { Future remove(id, [Map params]) => super.remove(id, params).then(deserialize);
var response = await client.delete(
"$basePath/$id${_buildQuery(params)}", headers: _readHeaders);
return god.deserialize(response.body, outputType: outputType);
}
} }

View file

@ -1,12 +1,12 @@
name: angel_client name: angel_client
version: 1.0.0-dev+15 version: 1.0.0-dev+16
description: Client library for the Angel framework. description: Client library for the Angel framework.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_client homepage: https://github.com/angel-dart/angel_client
dependencies: dependencies:
angel_framework: ">=1.0.0-dev <2.0.0"
http: ">= 0.11.3 < 0.12.0" http: ">= 0.11.3 < 0.12.0"
json_god: ">=2.0.0-beta <3.0.0" json_god: ">=2.0.0-beta <3.0.0"
merge_map: ">=1.0.0 <2.0.0" merge_map: ">=1.0.0 <2.0.0"
dev_dependencies: dev_dependencies:
angel_framework: ">=1.0.0-dev <2.0.0"
test: ">= 0.12.13 < 0.13.0" test: ">= 0.12.13 < 0.13.0"

View file

@ -22,7 +22,7 @@ main() {
serverApp.use("/postcards", new server.MemoryService<Postcard>()); serverApp.use("/postcards", new server.MemoryService<Postcard>());
serverPostcards = serverApp.service("postcards"); serverPostcards = serverApp.service("postcards");
clientApp = new client.Rest(url, new http.Client()); clientApp = new client.Rest(url);
clientPostcards = clientApp.service("postcards"); clientPostcards = clientApp.service("postcards");
clientTypedPostcards = clientApp.service("postcards", type: Postcard); clientTypedPostcards = clientApp.service("postcards", type: Postcard);
}); });