Browser
This commit is contained in:
parent
2a1f0ec736
commit
6f7321a4af
9 changed files with 427 additions and 4 deletions
10
README.md
10
README.md
|
@ -2,15 +2,17 @@
|
||||||
Client library for the Angel framework.
|
Client library for the Angel framework.
|
||||||
|
|
||||||
# Isomorphic
|
# Isomorphic
|
||||||
The REST client depends on `http`, because it can run in the browser or on the command-line.
|
The REST client 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
|
# Usage
|
||||||
This library provides the same API as an Angel server.
|
This library provides the same API as an Angel server.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
import 'package:angel_client/angel_client.dart';
|
// Import this file to import symbols "Angel" and "Service"
|
||||||
import 'package:http/browser_client.dart';
|
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 {
|
main() async {
|
||||||
Angel app = new Rest("http://localhost:3000", new BrowserClient());
|
Angel app = new Rest("http://localhost:3000", new BrowserClient());
|
||||||
|
|
92
lib/browser.dart
Normal file
92
lib/browser.dart
Normal file
|
@ -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<List> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
110
lib/cli.dart
Normal file
110
lib/cli.dart
Normal file
|
@ -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<List> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
lib/shared.dart
Normal file
44
lib/shared.dart
Normal file
|
@ -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<List> 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]);
|
||||||
|
}
|
10
test/browser.dart
Normal file
10
test/browser.dart
Normal file
|
@ -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());
|
||||||
|
}
|
134
test/cli.dart
Normal file
134
test/cli.dart
Normal file
|
@ -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<Postcard>());
|
||||||
|
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<Map> 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<Postcard> 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<Postcard> 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
20
test/for_browser_tests.dart
Normal file
20
test/for_browser_tests.dart
Normal file
|
@ -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<Todo>());
|
||||||
|
|
||||||
|
await app.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
|
||||||
|
print("Server up on localhost:3000");
|
||||||
|
}
|
||||||
|
|
||||||
|
class Todo extends MemoryModel {
|
||||||
|
String hello;
|
||||||
|
|
||||||
|
Todo({String this.hello});
|
||||||
|
}
|
10
test/index.html
Normal file
10
test/index.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Browser</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="application/dart" src="browser.dart"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
test/packages
Symbolic link
1
test/packages
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../packages
|
Loading…
Reference in a new issue