From 6f7321a4af8d33f3547a349263cf6cd1526c9331 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 25 Jun 2016 14:37:49 -0400 Subject: [PATCH] Browser --- README.md | 10 +-- lib/browser.dart | 92 +++++++++++++++++++++++++ lib/cli.dart | 110 +++++++++++++++++++++++++++++ lib/shared.dart | 44 ++++++++++++ test/browser.dart | 10 +++ test/cli.dart | 134 ++++++++++++++++++++++++++++++++++++ test/for_browser_tests.dart | 20 ++++++ test/index.html | 10 +++ test/packages | 1 + 9 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 lib/browser.dart create mode 100644 lib/cli.dart create mode 100644 lib/shared.dart create mode 100644 test/browser.dart create mode 100644 test/cli.dart create mode 100644 test/for_browser_tests.dart create mode 100644 test/index.html create mode 120000 test/packages diff --git a/README.md b/README.md index f90b691c..b96c82f1 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,17 @@ 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. +The REST client can run in the browser or on the command-line. # 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'; +// Import this file to import symbols "Angel" and "Service" +import 'package:angel_cli/shared.dart'; +// Choose one or the other, depending on platform +import 'package:angel_client/cli.dart'; +import 'package:angel_client/browser.dart'; main() async { Angel app = new Rest("http://localhost:3000", new BrowserClient()); diff --git a/lib/browser.dart b/lib/browser.dart new file mode 100644 index 00000000..86ffa0e7 --- /dev/null +++ b/lib/browser.dart @@ -0,0 +1,92 @@ +/// Browser library for the Angel framework. +library angel_client.browser; + +import 'dart:async'; +import 'dart:convert' show JSON; +import 'dart:html'; +import 'shared.dart'; + +_buildQuery(Map params) { + if (params == null || params == {}) + return ""; + + String result = ""; + return result; +} + +/// Queries an Angel server via REST. +class Rest extends Angel { + Rest(String basePath) :super(basePath); + + @override + RestService service(String path, {Type type}) { + String uri = path.replaceAll(new RegExp(r"(^\/)|(\/+$)"), ""); + return new RestService._base("$basePath/$uri") + ..app = this; + } +} + +/// Queries an Angel service via REST. +class RestService extends Service { + String basePath; + + RestService._base(Pattern path) { + this.basePath = (path is RegExp) ? path.pattern : path; + } + + _makeBody(data) { + return JSON.encode(data); + } + + HttpRequest _buildRequest(String url, + {String method: "POST", bool write: true}) { + HttpRequest request = new HttpRequest(); + request.open(method, url, async: false); + request.responseType = "json"; + request.setRequestHeader("Accept", "application/json"); + if (write) + request.setRequestHeader("Content-Type", "application/json"); + return request; + } + + @override + Future index([Map params]) async { + return JSON.decode( + await HttpRequest.getString("$basePath/${_buildQuery(params)}")); + } + + @override + Future read(id, [Map params]) async { + return JSON.decode( + await HttpRequest.getString("$basePath/$id${_buildQuery(params)}")); + } + + @override + Future create(data, [Map params]) async { + var request = _buildRequest("$basePath/${_buildQuery(params)}"); + request.send(_makeBody(data)); + return request.response; + } + + @override + Future modify(id, data, [Map params]) async { + var request = _buildRequest("$basePath/$id${_buildQuery(params)}", method: "PATCH"); + request.send(_makeBody(data)); + return request.response; + } + + @override + Future update(id, data, [Map params]) async { + var request = _buildRequest("$basePath/$id${_buildQuery(params)}"); + request.send(_makeBody(data)); + return request.response; + } + + @override + Future remove(id, [Map params]) async { + var request = _buildRequest("$basePath/$id${_buildQuery(params)}", method: "DELETE"); + request.send(); + return request.response; + } +} + diff --git a/lib/cli.dart b/lib/cli.dart new file mode 100644 index 00000000..837c70ac --- /dev/null +++ b/lib/cli.dart @@ -0,0 +1,110 @@ +/// Command-line client library for the Angel framework. +library angel_client.cli; + +import 'dart:async'; +import 'dart:convert' show JSON; +import 'package:http/http.dart'; +import 'package:json_god/json_god.dart' as god; +import 'shared.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); + + @override + RestService service(String path, {Type type}) { + String uri = path.replaceAll(new RegExp(r"(^\/)|(\/+$)"), ""); + return new RestService._base("$basePath/$uri", client, type) + ..app = this; + } +} + +/// Queries an Angel service via REST. +class RestService extends Service { + String basePath; + BaseClient client; + Type outputType; + + 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); + + 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 god.deserialize(response.body, outputType: outputType); + } + + @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); + } + + @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); + } + + @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); + } + + @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); + } +} + diff --git a/lib/shared.dart b/lib/shared.dart new file mode 100644 index 00000000..654b3167 --- /dev/null +++ b/lib/shared.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +/// 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 { + /// The URL of the server. + String basePath; + + Angel(String this.basePath); + + /// Applies an [AngelConfigurer] to this instance. + Future configure(AngelConfigurer configurer) async { + await configurer(this); + } + + /// Returns a representation of a service on the server. + 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]); + + /// Retrieves the desired resource. + Future read(id, [Map params]); + + /// Creates a resource. + Future create(data, [Map params]); + + /// Modifies a resource. + Future modify(id, data, [Map params]); + + /// Overwrites a resource. + Future update(id, data, [Map params]); + + /// Removes the given resource. + Future remove(id, [Map params]); +} \ No newline at end of file diff --git a/test/browser.dart b/test/browser.dart new file mode 100644 index 00000000..a5b5295f --- /dev/null +++ b/test/browser.dart @@ -0,0 +1,10 @@ +import 'package:angel_client/shared.dart'; +import 'package:angel_client/browser.dart'; +import 'package:test/test.dart'; + +main() async { + Angel app = new Rest("http://localhost:3000"); + Service Todos = app.service("todos"); + + print(await Todos.index()); +} \ No newline at end of file diff --git a/test/cli.dart b/test/cli.dart new file mode 100644 index 00000000..f10192d7 --- /dev/null +++ b/test/cli.dart @@ -0,0 +1,134 @@ +import 'dart:io'; +import 'package:angel_client/shared.dart' as clientLib; +import 'package:angel_client/cli.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'; + +main() { + group("rest", () { + server.Angel serverApp = new server.Angel(); + server.HookedService serverPostcards; + clientLib.Angel clientApp; + clientLib.Service clientPostcards; + clientLib.Service clientTypedPostcards; + HttpServer httpServer; + + setUp(() async { + httpServer = + 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 { + await httpServer.close(force: true); + }); + + test("index", () async { + Postcard niagaraFalls = await serverPostcards.create( + 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/for_browser_tests.dart b/test/for_browser_tests.dart new file mode 100644 index 00000000..ea5253cc --- /dev/null +++ b/test/for_browser_tests.dart @@ -0,0 +1,20 @@ +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; + +main() async { + Angel app = new Angel(); + app.before.add((req, ResponseContext res) { + res.header("Access-Control-Allow-Origin", "*"); + }); + + app.use("/todos", new MemoryService()); + + await app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000); + print("Server up on localhost:3000"); +} + +class Todo extends MemoryModel { + String hello; + + Todo({String this.hello}); +} \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 00000000..6d164dc3 --- /dev/null +++ b/test/index.html @@ -0,0 +1,10 @@ + + + + + Browser + + + + + \ No newline at end of file diff --git a/test/packages b/test/packages new file mode 120000 index 00000000..a16c4050 --- /dev/null +++ b/test/packages @@ -0,0 +1 @@ +../packages \ No newline at end of file