Skip to content

Instantly share code, notes, and snippets.

@vsavkin
Created June 22, 2014 20:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vsavkin/e80d68d8bfc0b2074c37 to your computer and use it in GitHub Desktop.
Save vsavkin/e80d68d8bfc0b2074c37 to your computer and use it in GitHub Desktop.
AngularDart REST Client Spike
library rest_api_spec;
import 'package:guinness/guinness.dart';
import 'package:unittest/unittest.dart' hide expect;
import 'dart:async';
import 'dart:mirrors';
import 'dart:convert' as cvt;
import 'package:angular/angular.dart';
import 'package:angular/mock/module.dart';
/**
* -------------------------------------
* -------------Library-----------------
* -------------------------------------
*/
class Resource {
final Object type, id;
final Map content;
Resource(this.type, this.id, this.content);
}
Resource res(type, id, [content]) => new Resource(type, id, content);
class ResourceStore {
Http http;
Resource parent;
Map config = {};
ResourceStore(this.http);
ResourceStore scope(parent) => new ResourceStore(http)..config = config..parent = parent;
Future<Resource> one(resourceType, resourceId) {
final url = "${_route(resourceType)}/$resourceId";
final parse = (resp) => _parse(resourceType, resp.data);
return http.get(url).then(parse);
}
Future<List<Resource>> list(resourceType) {
final url = _route(resourceType);
final parseOne = (d) => _parse(resourceType, d);
final parse = (resp) => resp.data.map(parseOne).toList();
return http.get(url).then(parse);
}
Future save(Resource resource) {
final content = new cvt.JsonEncoder().convert(resource.content);
final url = "${_route(resource.type)}/${resource.id}";
return http.put(url, content);
}
_parse(resourceType, data) => res(resourceType, data["id"], data);
_route(resourceType) {
if (parent != null) {
return "/${_resourceRoute(parent.type)}/${parent.id}/${_resourceRoute(resourceType)}";
} else {
return "/${_resourceRoute(resourceType)}";
}
}
_resourceRoute(resourceType) =>
config[resourceType] == null ? resourceType : config[resourceType]['route'];
}
class ObjectStore {
ResourceStore resourceStore;
ObjectStore(this.resourceStore);
ObjectStore scope(obj) =>
new ObjectStore(resourceStore.scope(_wrapInResource(obj)));
Future one(type, id) =>
resourceStore.one(type, id).then(_deserialize);
Future<List> list(type) =>
resourceStore.list(type).then((list) => list.map(_deserialize).toList());
Future save(object) =>
resourceStore.save(_wrapInResource(object));
_deserialize(resource) {
final c = resourceStore.config[resource.type];
return c["deserialize"](resource);
}
_wrapInResource(object) {
final c = resourceStore.config[object.runtimeType];
return c["serialize"](object);
}
}
class SimpleObjectStore {
ResourceStore resourceStore;
SimpleObjectStore(this.resourceStore);
SimpleObjectStore scope(type, id) =>
new SimpleObjectStore(resourceStore.scope(res(type, id)));
Future one(type, id) =>
resourceStore.one(type, id).then(_deserialize);
Future<List> list(type) =>
resourceStore.list(type).then((list) => list.map(_deserialize).toList());
Future save(type, id, content) =>
resourceStore.save(res(type, id, content));
_deserialize(resource) => resource.content;
}
class MirrorBasedSerializer {
List<String> attrs;
MirrorBasedSerializer(this.attrs);
Resource call(obj) {
final m = reflect(obj);
final id = m.getField(#id).reflectee;
final content = attrs.fold({}, (res, String attr) {
res[attr] = m.getField(new Symbol(attr)).reflectee;
return res;
});
return res(obj.runtimeType, id, content);
}
}
class MirrorBasedDeserializer {
List<String> attrs;
MirrorBasedDeserializer(this.attrs);
Object call(Resource r) {
final m = reflectClass(r.type).newInstance(const Symbol(''), []);
r.content.forEach((name, value) {
m.setField(new Symbol(name), value);
});
return m.reflectee;
}
}
/**
* -------------------------------------
* -------------Example-----------------
* -------------------------------------
*/
class Post {
int id;
String title;
}
class Comment {
int id;
String text;
}
Post deserializePost(Resource r) => new Post()
..id = r.id
..title = r.content["title"];
Resource serializePost(Post post) =>
res(Post, post.id, {"id" : post.id, "title" : post.title});
Comment deserializeComment(Resource r) => new Comment()
..id = r.id
..text = r.content["text"];
Resource serializeComment(Comment comment) =>
res(Comment, comment.id, {"id" : comment.id, "text" : comment.text});
/**
* -------------------------------------
* --------------Tests------------------
* -------------------------------------
*/
main() {
beforeEach(setUpInjector);
afterEach(tearDownInjector);
beforeEach(module((Module m) => m..bind(ResourceStore)..bind(ObjectStore)..bind(SimpleObjectStore)));
describe("ObjectStore", () {
beforeEach(inject((ResourceStore store) {
store.config = {
Post : {
"route": 'posts',
"deserialize" : deserializePost,
"serialize" : serializePost
},
Comment : {
"route": "comments",
"deserialize" : deserializeComment,
"serialize" : serializeComment
}
};
}));
it("returns a post", inject((MockHttpBackend hb, ObjectStore store) {
hb.when("/posts/123").respond('{"id": 123, "title" : "SampleTitle"}');
waitForHttp(store.one(Post, 123), (Post post) {
expect(post.title).toEqual("SampleTitle");
});
}));
it("returns many posts", inject((MockHttpBackend hb, ObjectStore store) {
hb.when("/posts").respond('[{"id": 123, "title" : "SampleTitle"}]');
waitForHttp(store.list(Post), (List<Post> posts) {
expect(posts.length).toEqual(1);
expect(posts[0].title).toEqual("SampleTitle");
});
}));
it("returns a comment", inject((MockHttpBackend hb, ObjectStore store) {
final post = new Post()..id = 123;
hb.when("/posts/123/comments/456").respond('{"id": 123, "text" : "SampleComment"}');
waitForHttp(store.scope(post).one(Comment, 456), (Comment comment) {
expect(comment.text).toEqual("SampleComment");
});
}));
it("saves a post", inject((MockHttpBackend hb, ObjectStore store) {
hb.expectPUT("/posts/123", '{"id":123,"title":"New"}').respond(['OK']);
final post = new Post()..id = 123..title = "New";
waitForHttp(store.save(post));
}));
it("saves a comment", inject((MockHttpBackend hb, ObjectStore store) {
hb.expectPUT("/posts/123/comments/456", '{"id":456,"text":"New"}').respond(['OK']);
final post = new Post()..id = 123;
final comment = new Comment()..id = 456..text = "New";
waitForHttp(store.scope(post).save(comment));
}));
});
describe("SimpleObjectStore", () {
it("returns a comment", inject((MockHttpBackend hb, SimpleObjectStore store) {
hb.when("/posts/123/comments/456").respond('{"id":123, "text" : "SampleComment"}');
waitForHttp(store.scope('posts', 123).one("coment", 456), (Map comment) {
expect(comment["text"]).toEqual("SampleComment");
});
}));
it("saves a comment", inject((MockHttpBackend hb, SimpleObjectStore store) {
hb.expectPUT("/posts/123/comments/456", '{"id":456,"text":"New"}').respond(['OK']);
final comment = {"id" : 456, "text": 'New'};
waitForHttp(store.scope('posts', 123).save("comments", 456, comment));
}));
});
describe("ObjectStore extras", () {
beforeEach(inject((ResourceStore store) {
store.config = {
Post : {
"route": 'posts',
"deserialize" : new MirrorBasedDeserializer(["id", "title"]),
"serialize" : new MirrorBasedSerializer(["id", "title"])
}
};
}));
it("returns a post", inject((MockHttpBackend hb, ObjectStore store) {
hb.when("/posts/123").respond('{"id": 123, "title" : "SampleTitle"}');
waitForHttp(store.one(Post, 123), (Post post) {
expect(post.title).toEqual("SampleTitle");
});
}));
it("saves a post", inject((MockHttpBackend hb, ObjectStore store) {
hb.expectPUT("/posts/123", '{"id":123,"title":"New"}').respond(['OK']);
final post = new Post()..id = 123..title = "New";
waitForHttp(store.save(post));
}));
});
describe("ResourceStore", () {
beforeEach(inject((ResourceStore store) {
store.config = {
"posts" : {"route": 'posts'},
"comments": {"route": "comments"}
};
}));
it("returns a post", inject((MockHttpBackend hb, ResourceStore store) {
hb.when("/posts/123").respond('{"id": 123, "title" : "SampleTitle"}');
waitForHttp(store.one("posts", 123), (Resource post) {
expect(post.id).toEqual(123);
expect(post.content["title"]).toEqual("SampleTitle");
});
}));
it("returns many posts", inject((MockHttpBackend hb, ResourceStore store) {
hb.when("/posts").respond('[{"id": 123, "title" : "SampleTitle"}]');
waitForHttp(store.list("posts"), (List<Resource> posts) {
expect(posts.length).toEqual(1);
expect(posts[0].content["title"]).toEqual("SampleTitle");
});
}));
it("returns a comment", inject((MockHttpBackend hb, ResourceStore store) {
hb.when("/posts/123/comments/456").respond('{"id": 456, "text" : "SampleComment"}');
waitForHttp(store.scope(res("posts", 123)).one("comments", 456), (Resource comment) {
expect(comment.id).toEqual(456);
expect(comment.content["text"]).toEqual("SampleComment");
});
}));
it("saves a post", inject((MockHttpBackend hb, ResourceStore store) {
hb.expectPUT("/posts/123", '{"id":123,"title":"New"}').respond(['OK']);
waitForHttp(store.save(res("posts", 123, {"id": 123, "title": "New"})));
}));
it("saves a comment", inject((MockHttpBackend hb, ResourceStore store) {
hb.expectPUT("/posts/123/comments/456", '{"id":456,"text":"New"}').respond(['OK']);
final post = res("posts", 123);
final comment = res("comments", 456, {"id": 456, "text" : "New"});
waitForHttp(store.scope(post).save(comment));
}));
it("deafults route to resource name", inject((MockHttpBackend hb, ResourceStore store) {
hb.when("/some/123").respond('{"id": 123, "field" : "value"}');
waitForHttp(store.one("some", 123), (Resource post) {
expect(post.id).toEqual(123);
expect(post.content["field"]).toEqual("value");
});
}));
});
guinness.initSpecs();
}
waitForHttp(future, [callback]) =>
scheduleMicrotask(() {
callback = callback != null ? callback : (_){};
inject((MockHttpBackend http) => http.flush());
future.then(callback);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment