Done
This commit is contained in:
parent
d588411d67
commit
65254902c0
7 changed files with 209 additions and 24 deletions
49
README.md
49
README.md
|
@ -1,2 +1,51 @@
|
||||||
# angel_client
|
# angel_client
|
||||||
Client library for the Angel framework.
|
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<Map> 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<Todo> todos = await Todos.index();
|
||||||
|
|
||||||
|
print(todos.length);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Just like on the server, services support `index`, `read`, `create`, `modify`, `update` and
|
||||||
|
`remove`.
|
|
@ -4,19 +4,32 @@ library angel_client;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert' show JSON;
|
import 'dart:convert' show JSON;
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:json_god/json_god.dart' as god;
|
||||||
|
|
||||||
part 'rest.dart';
|
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.
|
/// Represents an Angel server that we are querying.
|
||||||
abstract class Angel {
|
abstract class Angel {
|
||||||
String basePath;
|
String basePath;
|
||||||
|
|
||||||
Angel(String this.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.
|
/// Queries a service on an Angel server, with the same API.
|
||||||
abstract class Service {
|
abstract class Service {
|
||||||
|
/// The Angel instance powering this service.
|
||||||
|
Angel app;
|
||||||
|
|
||||||
/// Retrieves all resources.
|
/// Retrieves all resources.
|
||||||
Future<List> index([Map params]);
|
Future<List> index([Map params]);
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,13 @@ const Map _writeHeaders = const {
|
||||||
class Rest extends Angel {
|
class Rest extends Angel {
|
||||||
BaseClient client;
|
BaseClient client;
|
||||||
|
|
||||||
Rest(String path, BaseClient this.client):super(path);
|
Rest(String path, BaseClient this.client) :super(path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RestService service(String path) {
|
RestService 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);
|
return new RestService._base("$basePath/$uri", client, type)
|
||||||
|
..app = this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,55 +34,68 @@ class Rest extends Angel {
|
||||||
class RestService extends Service {
|
class RestService extends Service {
|
||||||
String basePath;
|
String basePath;
|
||||||
BaseClient client;
|
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;
|
this.basePath = (path is RegExp) ? path.pattern : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_makeBody(data) {
|
||||||
|
if (outputType == null)
|
||||||
|
return JSON.encode(data);
|
||||||
|
else return god.serialize(data);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List> index([Map params]) async {
|
Future<List> index([Map params]) async {
|
||||||
var response = await client.get(
|
var response = await client.get(
|
||||||
"$basePath/${_buildQuery(params)}", headers: _readHeaders);
|
"$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
|
@override
|
||||||
Future read(id, [Map params]) async {
|
Future read(id, [Map params]) async {
|
||||||
var response = await client.get(
|
var response = await client.get(
|
||||||
"$basePath/$id${_buildQuery(params)}", headers: _readHeaders);
|
"$basePath/$id${_buildQuery(params)}", headers: _readHeaders);
|
||||||
return JSON.decode(response.body);
|
return god.deserialize(response.body, outputType: outputType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future create(data, [Map params]) async {
|
Future create(data, [Map params]) async {
|
||||||
var response = await client.post(
|
var response = await client.post(
|
||||||
"$basePath/${_buildQuery(params)}", body: JSON.encode(data),
|
"$basePath/${_buildQuery(params)}", body: _makeBody(data),
|
||||||
headers: _writeHeaders);
|
headers: _writeHeaders);
|
||||||
return JSON.decode(response.body);
|
return god.deserialize(response.body, outputType: outputType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future modify(id, data, [Map params]) async {
|
Future modify(id, data, [Map params]) async {
|
||||||
var response = await client.patch(
|
var response = await client.patch(
|
||||||
"$basePath/$id${_buildQuery(params)}", body: JSON.encode(data),
|
"$basePath/$id${_buildQuery(params)}", body: _makeBody(data),
|
||||||
headers: _writeHeaders);
|
headers: _writeHeaders);
|
||||||
return JSON.decode(response.body);
|
return god.deserialize(response.body, outputType: outputType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future update(id, data, [Map params]) async {
|
Future update(id, data, [Map params]) async {
|
||||||
var response = await client.patch(
|
var response = await client.patch(
|
||||||
"$basePath/$id${_buildQuery(params)}", body: JSON.encode(data),
|
"$basePath/$id${_buildQuery(params)}", body: _makeBody(data),
|
||||||
headers: _writeHeaders);
|
headers: _writeHeaders);
|
||||||
return JSON.decode(response.body);
|
return god.deserialize(response.body, outputType: outputType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future remove(id, [Map params]) async {
|
Future remove(id, [Map params]) async {
|
||||||
var response = await client.delete(
|
var response = await client.delete(
|
||||||
"$basePath/$id${_buildQuery(params)}", headers: _readHeaders);
|
"$basePath/$id${_buildQuery(params)}", headers: _readHeaders);
|
||||||
return JSON.decode(response.body);
|
return god.deserialize(response.body, outputType: outputType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,6 @@ dependencies:
|
||||||
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"
|
angel_framework: 1.0.0-dev+5
|
||||||
http: ">= 0.11.3 < 0.12.0"
|
http: ">= 0.11.3 < 0.12.0"
|
||||||
test: ">= 0.12.13 < 0.13.0"
|
test: ">= 0.12.13 < 0.13.0"
|
|
@ -1 +0,0 @@
|
||||||
../packages
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||||
import 'package:angel_client/angel_client.dart' as client;
|
import 'package:angel_client/angel_client.dart' as client;
|
||||||
import 'package:angel_framework/angel_framework.dart' as server;
|
import 'package:angel_framework/angel_framework.dart' as server;
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:json_god/json_god.dart' as god;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'shared.dart';
|
import 'shared.dart';
|
||||||
|
|
||||||
|
@ -11,16 +12,18 @@ main() {
|
||||||
server.HookedService serverPostcards;
|
server.HookedService serverPostcards;
|
||||||
client.Angel clientApp;
|
client.Angel clientApp;
|
||||||
client.Service clientPostcards;
|
client.Service clientPostcards;
|
||||||
|
client.Service clientTypedPostcards;
|
||||||
HttpServer httpServer;
|
HttpServer httpServer;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
httpServer =
|
httpServer =
|
||||||
await serverApp.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
|
await serverApp.startServer(InternetAddress.LOOPBACK_IP_V4, 3000);
|
||||||
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("http://localhost:3000", new http.Client());
|
clientApp = new client.Rest("http://localhost:3000", new http.Client());
|
||||||
clientPostcards = clientApp.service("postcards");
|
clientPostcards = clientApp.service("postcards");
|
||||||
|
clientTypedPostcards = clientApp.service("postcards", type: Postcard);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
@ -32,6 +35,99 @@ main() {
|
||||||
new Postcard(location: "Niagara Falls", message: "Missing you!"));
|
new Postcard(location: "Niagara Falls", message: "Missing you!"));
|
||||||
List<Map> indexed = await clientPostcards.index();
|
List<Map> indexed = await clientPostcards.index();
|
||||||
print(indexed);
|
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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -1,6 +1,20 @@
|
||||||
class Postcard {
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
String location;
|
|
||||||
String message;
|
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});
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue