From 65254902c0b203067194f316d3bfb45e425e5cec Mon Sep 17 00:00:00 2001 From: regiostech Date: Fri, 24 Jun 2016 17:06:57 -0400 Subject: [PATCH] Done --- README.md | 49 +++++++++++++++++++++ lib/angel_client.dart | 15 ++++++- lib/rest.dart | 44 ++++++++++++------- pubspec.yaml | 2 +- test/packages | 1 - test/rest.dart | 100 +++++++++++++++++++++++++++++++++++++++++- test/shared.dart | 22 ++++++++-- 7 files changed, 209 insertions(+), 24 deletions(-) delete mode 120000 test/packages diff --git a/README.md b/README.md index 2891d08d..f90b691c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ # angel_client Client library for the Angel framework. + +# Isomorphic +The REST client depends on `http`, because it can run in the browser or on the command-line. +Depending on your environment, you must pass an instance of `BaseClient` to the constructor. + +# Usage +This library provides the same API as an Angel server. + +```dart +import 'package:angel_client/angel_client.dart'; +import 'package:http/browser_client.dart'; + +main() async { + Angel app = new Rest("http://localhost:3000", new BrowserClient()); +} +``` + +You can call `service` to receive an instance of `Service`, which acts as a client to a +service on the server at the given path (say that five times fast!). + +```dart +foo() async { + Service Todos = app.service("todos"); + List todos = await Todos.index(); + + print(todos.length); +} +``` + +The REST client also supports reflection via `json_god`. There is no need to work with Maps; +you can use the same class on the client and the server. + +```dart +class Todo extends Model { + String text; + + Todo({String this.text}); +} + +bar() async { + Service Todos = app.service("todos", type: Todo); + List todos = await Todos.index(); + + print(todos.length); +} +``` + +Just like on the server, services support `index`, `read`, `create`, `modify`, `update` and +`remove`. \ No newline at end of file diff --git a/lib/angel_client.dart b/lib/angel_client.dart index 6a73fde3..ac8a8d3e 100644 --- a/lib/angel_client.dart +++ b/lib/angel_client.dart @@ -4,19 +4,32 @@ library angel_client; import 'dart:async'; import 'dart:convert' show JSON; import 'package:http/http.dart'; +import 'package:json_god/json_god.dart' as god; + part 'rest.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 basePath; Angel(String this.basePath); - Service service(Pattern path); + + /// Applies an [AngelConfigurer] to this instance. + Future configure(AngelConfigurer configurer) async { + await configurer(this); + } + + Service service(Pattern path, {Type type}); } /// Queries a service on an Angel server, with the same API. abstract class Service { + /// The Angel instance powering this service. + Angel app; + /// Retrieves all resources. Future index([Map params]); diff --git a/lib/rest.dart b/lib/rest.dart index e68c3424..072fda44 100644 --- a/lib/rest.dart +++ b/lib/rest.dart @@ -20,12 +20,13 @@ const Map _writeHeaders = const { class Rest extends Angel { BaseClient client; - Rest(String path, BaseClient this.client):super(path); + Rest(String path, BaseClient this.client) :super(path); @override - RestService service(String path) { + RestService service(String path, {Type type}) { String uri = path.replaceAll(new RegExp(r"(^\/)|(\/+$)"), ""); - return new RestService._base("$basePath/$uri", client); + return new RestService._base("$basePath/$uri", client, type) + ..app = this; } } @@ -33,55 +34,68 @@ class Rest extends Angel { class RestService extends Service { String basePath; BaseClient client; + Type outputType; - RestService._base(Pattern path, BaseClient this.client) { + RestService._base(Pattern path, BaseClient this.client, + Type this.outputType) { this.basePath = (path is RegExp) ? path.pattern : path; } + _makeBody(data) { + if (outputType == null) + return JSON.encode(data); + else return god.serialize(data); + } + @override Future index([Map params]) async { var response = await client.get( "$basePath/${_buildQuery(params)}", headers: _readHeaders); - return JSON.decode(response.body); + + if (outputType == null) + return god.deserialize(response.body); + + else { + return JSON.decode(response.body).map((x) => + god.deserializeDatum(x, outputType: outputType)).toList(); + } } @override Future read(id, [Map params]) async { var response = await client.get( "$basePath/$id${_buildQuery(params)}", headers: _readHeaders); - return JSON.decode(response.body); + return god.deserialize(response.body, outputType: outputType); } @override Future create(data, [Map params]) async { var response = await client.post( - "$basePath/${_buildQuery(params)}", body: JSON.encode(data), + "$basePath/${_buildQuery(params)}", body: _makeBody(data), headers: _writeHeaders); - return JSON.decode(response.body); + return god.deserialize(response.body, outputType: outputType); } @override Future modify(id, data, [Map params]) async { var response = await client.patch( - "$basePath/$id${_buildQuery(params)}", body: JSON.encode(data), + "$basePath/$id${_buildQuery(params)}", body: _makeBody(data), headers: _writeHeaders); - return JSON.decode(response.body); + return god.deserialize(response.body, outputType: outputType); } @override Future update(id, data, [Map params]) async { var response = await client.patch( - "$basePath/$id${_buildQuery(params)}", body: JSON.encode(data), + "$basePath/$id${_buildQuery(params)}", body: _makeBody(data), headers: _writeHeaders); - return JSON.decode(response.body); + return god.deserialize(response.body, outputType: outputType); } @override Future remove(id, [Map params]) async { var response = await client.delete( "$basePath/$id${_buildQuery(params)}", headers: _readHeaders); - return JSON.decode(response.body); + return god.deserialize(response.body, outputType: outputType); } - - } diff --git a/pubspec.yaml b/pubspec.yaml index 56c70487..e303f079 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,6 @@ dependencies: 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" + angel_framework: 1.0.0-dev+5 http: ">= 0.11.3 < 0.12.0" test: ">= 0.12.13 < 0.13.0" \ No newline at end of file diff --git a/test/packages b/test/packages deleted file mode 120000 index a16c4050..00000000 --- a/test/packages +++ /dev/null @@ -1 +0,0 @@ -../packages \ No newline at end of file diff --git a/test/rest.dart b/test/rest.dart index 055e65a3..0bcdb6b8 100644 --- a/test/rest.dart +++ b/test/rest.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:angel_client/angel_client.dart' as client; import 'package:angel_framework/angel_framework.dart' as server; import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart' as god; import 'package:test/test.dart'; import 'shared.dart'; @@ -11,16 +12,18 @@ main() { server.HookedService serverPostcards; client.Angel clientApp; client.Service clientPostcards; + client.Service clientTypedPostcards; HttpServer httpServer; setUp(() async { httpServer = - await serverApp.startServer(InternetAddress.LOOPBACK_IP_V4, 3000); + await serverApp.startServer(InternetAddress.LOOPBACK_IP_V4, 3000); serverApp.use("/postcards", new server.MemoryService()); serverPostcards = serverApp.service("postcards"); clientApp = new client.Rest("http://localhost:3000", new http.Client()); clientPostcards = clientApp.service("postcards"); + clientTypedPostcards = clientApp.service("postcards", type: Postcard); }); tearDown(() async { @@ -32,6 +35,99 @@ main() { new Postcard(location: "Niagara Falls", message: "Missing you!")); List indexed = await clientPostcards.index(); print(indexed); + + expect(indexed.length, equals(1)); + expect(indexed[0].keys.length, equals(3)); + expect(indexed[0]['id'], equals(niagaraFalls.id)); + expect(indexed[0]['location'], equals(niagaraFalls.location)); + expect(indexed[0]['message'], equals(niagaraFalls.message)); + + Postcard louvre = await serverPostcards.create(new Postcard( + location: "The Louvre", message: "The Mona Lisa was watching me!")); + print(god.serialize(louvre)); + List typedIndexed = await clientTypedPostcards.index(); + expect(typedIndexed.length, equals(2)); + expect(typedIndexed[1], equals(louvre)); + }); + + test("create/read", () async { + Map opry = {"location": "Grand Ole Opry", "message": "Yeehaw!"}; + var created = await clientPostcards.create(opry); + print(created); + + expect(created['id'] == null, equals(false)); + expect(created["location"], equals(opry["location"])); + expect(created["message"], equals(opry["message"])); + + var read = await clientPostcards.read(created['id']); + print(read); + expect(read['id'], equals(created['id'])); + expect(read['location'], equals(created['location'])); + expect(read['message'], equals(created['message'])); + + Postcard canyon = new Postcard(location: "Grand Canyon", + message: "But did you REALLY experience it???"); + created = await clientTypedPostcards.create(canyon); + print(god.serialize(created)); + + expect(created.location, equals(canyon.location)); + expect(created.message, equals(canyon.message)); + + read = await clientTypedPostcards.read(created.id); + print(god.serialize(read)); + expect(read.id, equals(created.id)); + expect(read.location, equals(created.location)); + expect(read.message, equals(created.message)); + }); + + test("modify/update", () async { + server.MemoryService innerPostcards = serverPostcards.inner; + print(innerPostcards.items); + Postcard mecca = await clientTypedPostcards.create( + new Postcard(location: "Mecca", message: "Pilgrimage")); + print(god.serialize(mecca)); + + // I'm too lazy to write the tests twice, because I know it works + // So I'll modify using the type-based client, and update using the + // map-based one + + print("Postcards on server: " + + god.serialize(await serverPostcards.index())); + print("Postcards on client: " + + god.serialize(await clientPostcards.index())); + + Postcard modified = await clientTypedPostcards.modify( + mecca.id, {"location": "Saudi Arabia"}); + print(god.serialize(modified)); + expect(modified.id, equals(mecca.id)); + expect(modified.location, equals("Saudi Arabia")); + expect(modified.message, equals(mecca.message)); + + Map updated = await clientPostcards.update( + mecca.id, {"location": "Full", "message": "Overwrite"}); + print(updated); + + expect(updated.keys.length, equals(3)); + expect(updated['id'], equals(mecca.id)); + expect(updated['location'], equals("Full")); + expect(updated['message'], equals("Overwrite")); + }); + + test("remove", () async { + Postcard remove1 = await clientTypedPostcards.create( + {"location": "remove", "message": "#1"}); + Postcard remove2 = await clientTypedPostcards.create( + {"location": "remove", "message": "#2"}); + print(god.serialize([remove1, remove2])); + + Map removed1 = await clientPostcards.remove(remove1.id); + expect(removed1.keys.length, equals(3)); + expect(removed1['id'], equals(remove1.id)); + expect(removed1['location'], equals(remove1.location)); + expect(removed1['message'], equals(remove1.message)); + + Postcard removed2 = await clientTypedPostcards.remove(remove2.id); + expect(removed2, equals(remove2)); }); }); -} +} \ No newline at end of file diff --git a/test/shared.dart b/test/shared.dart index 4a8d2052..4b6621a0 100644 --- a/test/shared.dart +++ b/test/shared.dart @@ -1,6 +1,20 @@ -class Postcard { - String location; - String message; +import 'package:angel_framework/angel_framework.dart'; + +class Postcard extends MemoryModel { + int id; + String location; + String message; + + Postcard({String this.location, String this.message}); + + @override + bool operator ==(other) { + if (!(other is Postcard)) + return false; + + return id == other.id && location == other.location && + message == other.message; + } + - Postcard({String this.location, String this.message}); } \ No newline at end of file