This commit is contained in:
regiostech 2016-06-24 17:06:57 -04:00
parent d588411d67
commit 65254902c0
7 changed files with 209 additions and 24 deletions

View file

@ -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<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`.

View file

@ -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<List> index([Map params]);

View file

@ -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<List> 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);
}
}

View file

@ -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"

View file

@ -1 +0,0 @@
../packages

View file

@ -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<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 {
@ -32,6 +35,99 @@ main() {
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));
});
});
}
}

View file

@ -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});
}