From 682e7d2a0a9a6331de9f6a069f02c0f8d7144128 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sat, 16 Mar 2019 20:33:13 -0700 Subject: [PATCH 1/9] WIP --- example/cars_server/controller.dart | 8 ++- lib/src/client/client.dart | 12 ++--- lib/src/document/document.dart | 44 +++++++++++------ lib/src/document/resource_json.dart | 4 +- lib/src/server/request.dart | 8 +-- lib/src/server/server.dart | 29 +++++------ test/functional/fetch_test.dart | 15 ++++++ test/unit/document_test.dart | 16 +++++- test/unit/example.json | 76 +++++++++++++++++++++++++++++ 9 files changed, 166 insertions(+), 46 deletions(-) create mode 100644 test/unit/example.json diff --git a/example/cars_server/controller.dart b/example/cars_server/controller.dart index f1c0ba1..196819e 100644 --- a/example/cars_server/controller.dart +++ b/example/cars_server/controller.dart @@ -58,7 +58,13 @@ class CarsController implements JsonApiController { if (res == null) { return r.notFound([JsonApiError(detail: 'Resource not found')]); } - return r.resource(res); + final fetchById = (Identifier _) => dao[_.type].fetchByIdAsResource(_.id); + + final children = res.toOne.values + .map(fetchById) + .followedBy(res.toMany.values.expand((_) => _.map(fetchById))); + + return r.resource(res, included: children); } @override diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index ed80123..b6a0c67 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -25,13 +25,13 @@ class JsonApiClient { /// Use [headers] to pass extra HTTP headers. Future> fetchCollection(Uri uri, {Map headers}) => - _get(ResourceCollectionData.parseDocument, uri, headers); + _get(ResourceCollectionData.parse, uri, headers); /// Fetches a single resource /// Use [headers] to pass extra HTTP headers. Future> fetchResource(Uri uri, {Map headers}) => - _get(ResourceData.parseDocument, uri, headers); + _get(ResourceData.parse, uri, headers); /// Fetches a to-one relationship /// Use [headers] to pass extra HTTP headers. @@ -57,7 +57,7 @@ class JsonApiClient { /// https://jsonapi.org/format/#crud-creating Future> createResource(Uri uri, Resource resource, {Map headers}) => - _post(ResourceData.parseDocument, uri, + _post(ResourceData.parse, uri, ResourceData(ResourceJson.fromResource(resource)), headers); /// Deletes the resource. @@ -71,7 +71,7 @@ class JsonApiClient { /// https://jsonapi.org/format/#crud-updating Future> updateResource(Uri uri, Resource resource, {Map headers}) => - _patch(ResourceData.parseDocument, uri, + _patch(ResourceData.parse, uri, ResourceData(ResourceJson.fromResource(resource)), headers); /// Updates a to-one relationship via PATCH request @@ -136,7 +136,7 @@ class JsonApiClient { _call( parse, (_) => _.post(uri, - body: json.encode(Document.data(data)), + body: json.encode(Document(data)), headers: {} ..addAll(headers ?? {}) ..addAll({ @@ -160,7 +160,7 @@ class JsonApiClient { _call( parse, (_) => _.patch(uri, - body: json.encode(Document.data(data)), + body: json.encode(Document(data)), headers: {} ..addAll(headers ?? {}) ..addAll({ diff --git a/lib/src/document/document.dart b/lib/src/document/document.dart index 34f50ad..64ba72d 100644 --- a/lib/src/document/document.dart +++ b/lib/src/document/document.dart @@ -1,33 +1,36 @@ import 'package:json_api/src/document/error.dart'; import 'package:json_api/src/document/primary_data.dart'; import 'package:json_api/src/document/resource_json.dart'; -import 'package:json_api/src/nullable.dart'; class Document { /// The Primary Data final Data data; /// For Compound Documents this member contains the included resources - final included = []; + final List included; final List errors; final Map meta; - Document.data(Data data, {Map meta}) - : this._(data: data, meta: nullable((_) => Map.from(_))(meta)); + Document(this.data, + {Map meta, Iterable included}) + : this.errors = null, + this.included = + (included == null || included.isEmpty ? null : List.from(included)), + this.meta = (meta == null || meta.isEmpty ? null : Map.from(meta)); Document.error(Iterable errors, {Map meta}) - : this._( - errors: List.from(errors), - meta: nullable((_) => Map.from(_))(meta)); + : this.data = null, + this.included = null, + this.errors = List.from(errors), + this.meta = (meta == null || meta.isEmpty ? null : Map.from(meta)); - Document.empty(Map meta) : this._(meta: Map.from(meta)); - - Document._({this.data, this.errors, this.meta}) { - if (data == null && errors == null && meta.isEmpty) { - throw ArgumentError( - 'The `meta` member may not be empty for meta-only documents'); - } + Document.empty(Map meta) + : this.data = null, + this.errors = null, + this.included = null, + this.meta = (meta == null || meta.isEmpty ? null : Map.from(meta)) { + ArgumentError.checkNotNull(meta, 'meta'); } static Document parse( @@ -41,8 +44,14 @@ class Document { meta: json['meta']); } } else if (json.containsKey('data')) { - final data = parsePrimaryData(json); - return Document.data(data, meta: json['meta']); + final included = json['included']; + final resources = []; + if (included is List) { + resources.addAll(included.map(ResourceJson.parse)); + } + return Document(parsePrimaryData(json), + meta: json['meta'], + included: resources.isNotEmpty ? resources : null); } else { return Document.empty(json['meta']); } @@ -54,6 +63,9 @@ class Document { Map json = {}; if (data != null) { json = data.toJson(); + if (included != null && included.isNotEmpty) { + json['included'] = included; + } } else if (errors != null) { json = {'errors': errors}; } diff --git a/lib/src/document/resource_json.dart b/lib/src/document/resource_json.dart index 5c64e96..618d915 100644 --- a/lib/src/document/resource_json.dart +++ b/lib/src/document/resource_json.dart @@ -104,7 +104,7 @@ class ResourceData extends PrimaryData { ResourceData(this.resourceObject, {Link self}) : super(self: self); /// Parse the document - static ResourceData parseDocument(Object json) { + static ResourceData parse(Object json) { if (json is Map) { final links = Link.parseLinks(json['links']); final data = ResourceJson.parse(json['data']); @@ -136,7 +136,7 @@ class ResourceCollectionData extends PrimaryData { } /// Parse the document - static ResourceCollectionData parseDocument(Object json) { + static ResourceCollectionData parse(Object json) { if (json is Map) { final links = Link.parseLinks(json['links']); final data = json['data']; diff --git a/lib/src/server/request.dart b/lib/src/server/request.dart index 176a0f8..09e408c 100644 --- a/lib/src/server/request.dart +++ b/lib/src/server/request.dart @@ -110,8 +110,8 @@ class FetchResource extends JsonApiRequest { Future call(JsonApiController controller) => controller.fetchResource(this); - Future resource(Resource resource) => - _server.resource(_response, route, resource); + Future resource(Resource resource, {Iterable included}) => + _server.resource(_response, route, resource, included: included); } class DeleteResource extends JsonApiRequest { @@ -132,7 +132,7 @@ class CreateResource extends JsonApiRequest { CreateResource(HttpRequest request, this.route) : super(request); Future resource() async { - return ResourceData.parseDocument(await _body).resourceObject.toResource(); + return ResourceData.parse(await _body).resourceObject.toResource(); } Future call(JsonApiController controller) => controller.createResource(this); @@ -152,7 +152,7 @@ class UpdateResource extends JsonApiRequest { UpdateResource(HttpRequest request, this.route) : super(request); Future resource() async { - return ResourceData.parseDocument(await _body).resourceObject.toResource(); + return ResourceData.parse(await _body).resourceObject.toResource(); } Future call(JsonApiController controller) => controller.updateResource(this); diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index 0d93b67..45f3a04 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -32,7 +32,7 @@ class JsonApiServer { Iterable resource, {Page page}) => write(response, 200, - document: Document.data( + document: Document( ResourceCollectionData(resource.map(ResourceJson.fromResource), self: Link(route.self(url, parameters: route.parameters)), pagination: page == null @@ -47,38 +47,36 @@ class JsonApiServer { Future relatedCollection(HttpResponse response, RelatedRoute route, Iterable collection) => write(response, 200, - document: Document.data(ResourceCollectionData( + document: Document(ResourceCollectionData( collection.map(ResourceJson.fromResource), self: Link(route.self(url))))); Future relatedResource( HttpResponse response, RelatedRoute route, Resource resource) => write(response, 200, - document: Document.data(ResourceData( - ResourceJson.fromResource(resource), + document: Document(ResourceData(ResourceJson.fromResource(resource), self: Link(route.self(url))))); - Future resource( - HttpResponse response, ResourceRoute route, Resource resource) => + Future resource(HttpResponse response, ResourceRoute route, Resource resource, + {Iterable included}) => write(response, 200, - document: Document.data(ResourceData( - ResourceJson.fromResource(resource), - self: Link(route.self(url))))); + document: Document( + ResourceData(ResourceJson.fromResource(resource), + self: Link(route.self(url))), + included: included?.map(ResourceJson.fromResource))); Future toMany(HttpResponse response, RelationshipRoute route, Iterable collection) => write(response, 200, - document: Document.data(ToMany( + document: Document(ToMany( collection.map(IdentifierJson.fromIdentifier), self: Link(route.self(url)), related: Link(route.related(url))))); Future toOne(HttpResponse response, RelationshipRoute route, Identifier id) => write(response, 200, - document: Document.data(ToOne( - nullable(IdentifierJson.fromIdentifier)(id), - self: Link(route.self(url)), - related: Link(route.related(url))))); + document: Document(ToOne(nullable(IdentifierJson.fromIdentifier)(id), + self: Link(route.self(url)), related: Link(route.related(url))))); Future meta(HttpResponse response, ResourceRoute route, Map meta) => @@ -87,8 +85,7 @@ class JsonApiServer { Future created( HttpResponse response, CollectionRoute route, Resource resource) => write(response, 201, - document: - Document.data(ResourceData(ResourceJson.fromResource(resource))), + document: Document(ResourceData(ResourceJson.fromResource(resource))), headers: { 'Location': url.resource(resource.type, resource.id).toString() }); diff --git a/test/functional/fetch_test.dart b/test/functional/fetch_test.dart index d102f87..4d1092e 100644 --- a/test/functional/fetch_test.dart +++ b/test/functional/fetch_test.dart @@ -81,6 +81,21 @@ void main() async { expect(r.data.self.uri, uri); }); + test('single resource compound document', () async { + final uri = Url.resource('companies', '1'); + final r = await client.fetchResource(uri); + expect(r.status, 200); + expect(r.isSuccessful, true); + expect(r.data.toResource().attributes['name'], 'Tesla'); + expect(r.data.self.uri, uri); + expect(r.document.included.length, 5); + expect(r.document.included.first.type, 'cities'); + expect(r.document.included.first.attributes['name'], 'Palo Alto'); + expect(r.document.included.last.type, 'models'); + expect(r.document.included.last.attributes['name'], 'Model 3'); + + }); + test('404 on type', () async { final r = await client.fetchResource(Url.resource('unicorns', '1')); expect(r.status, 404); diff --git a/test/unit/document_test.dart b/test/unit/document_test.dart index dfe6ca6..180535a 100644 --- a/test/unit/document_test.dart +++ b/test/unit/document_test.dart @@ -1,3 +1,7 @@ +import 'dart:convert'; +import 'dart:io'; + +@TestOn('vm') import 'package:json_api/document.dart'; import 'package:json_matcher/json_matcher.dart'; import 'package:test/test.dart'; @@ -6,7 +10,7 @@ void main() { group('Document', () { group('JSON Conversion', () { test('Can convert a single resource', () { - final doc = Document.data(ResourceData(ResourceJson('foo', 'bar'))); + final doc = Document(ResourceData(ResourceJson('foo', 'bar'))); expect( doc, @@ -15,5 +19,15 @@ void main() { })); }); }); + + group('Standard compliance', () { + test('Can parse the example document', () { + final jsonString = + new File('test/unit/example.json').readAsStringSync(); + final jsonObject = json.decode(jsonString); + final doc = Document.parse(jsonObject, ResourceCollectionData.parse); + expect(doc, encodesToJson(jsonObject)); + }); + }); }); } diff --git a/test/unit/example.json b/test/unit/example.json new file mode 100644 index 0000000..85ace8f --- /dev/null +++ b/test/unit/example.json @@ -0,0 +1,76 @@ +{ + "links": { + "self": "http://example.com/articles", + "next": "http://example.com/articles?page=2", + "last": "http://example.com/articles?page=10" + }, + "data": [{ + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON:API paints my bikeshed!" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + }, + "comments": { + "links": { + "self": "http://example.com/articles/1/relationships/comments", + "related": "http://example.com/articles/1/comments" + }, + "data": [ + { "type": "comments", "id": "5" }, + { "type": "comments", "id": "12" } + ] + } + }, + "links": { + "self": "http://example.com/articles/1" + } + }], + "included": [{ + "type": "people", + "id": "9", + "attributes": { + "firstName": "Dan", + "lastName": "Gebhardt", + "twitter": "dgeb" + }, + "links": { + "self": "http://example.com/people/9" + } + }, { + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "2" } + } + }, + "links": { + "self": "http://example.com/comments/5" + } + }, { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "9" } + } + }, + "links": { + "self": "http://example.com/comments/12" + } + }] +} \ No newline at end of file From 5f512a381b6d80d07c7bca68562549d5b5078e98 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sat, 16 Mar 2019 20:50:59 -0700 Subject: [PATCH 2/9] wip --- CHANGELOG.md | 6 ++++++ README.md | 2 +- lib/src/document/relationship.dart | 6 +++--- lib/src/document/resource_json.dart | 31 +++++++++++++++++++---------- lib/src/server/request.dart | 4 ++-- test/browser_compat_test.dart | 2 +- test/functional/create_test.dart | 2 +- test/functional/fetch_test.dart | 12 +++++------ test/unit/document_test.dart | 21 ++++++++++++------- 9 files changed, 54 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2624d..00cdedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Some BC-breaking changes in the Document + +### Added +- Compound documents support in Client (Server-side support is still very limited) + ## [0.3.0] - 2019-03-16 ### Changed - Huge BC-breaking refactoring in the Document model which propagated everywhere diff --git a/README.md b/README.md index 60f3a69..0cfefa7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The features here are roughly ordered by priority. Feel free to open an issue if - [x] Updating resource's attributes - [x] Updating resource's relationships - [x] Updating relationships -- [ ] Compound documents +- [x] Compound documents - [ ] Related collection pagination - [ ] Asynchronous processing - [ ] Optional check for `Content-Type` header in incoming responses diff --git a/lib/src/document/relationship.dart b/lib/src/document/relationship.dart index 96a306c..836d9cd 100644 --- a/lib/src/document/relationship.dart +++ b/lib/src/document/relationship.dart @@ -45,9 +45,9 @@ class Relationship extends PrimaryData { throw 'Can not parse Relationship map from $json'; } - Map toLinks() => - related == null ? super.toLinks() : super.toLinks() - ..['related'] = related; + Map toLinks() => related == null + ? super.toLinks() + : (super.toLinks()..['related'] = related); /// Top-level JSON object Map toJson() { diff --git a/lib/src/document/resource_json.dart b/lib/src/document/resource_json.dart index 618d915..ce603e9 100644 --- a/lib/src/document/resource_json.dart +++ b/lib/src/document/resource_json.dart @@ -18,12 +18,14 @@ import 'package:json_api/src/nullable.dart'; class ResourceJson { final String type; final String id; + final Link self; final attributes = {}; final relationships = {}; ResourceJson(this.type, this.id, {Map attributes, - Map relationships}) { + Map relationships, + this.self}) { this.attributes.addAll(attributes ?? {}); this.relationships.addAll(relationships ?? {}); } @@ -34,17 +36,19 @@ class ResourceJson { if (json is Map) { final relationships = json['relationships']; final attributes = json['attributes']; + final links = Link.parseLinks(json['links']); if (mapOrNull(relationships) && mapOrNull(attributes)) { return ResourceJson(json['type'], json['id'], attributes: attributes, - relationships: Relationship.parseRelationships(relationships)); + relationships: Relationship.parseRelationships(relationships), + self: links['self']); } } throw 'Can not parse ResourceObject from $json'; } - static ResourceJson fromResource(Resource resource) { + static ResourceJson fromResource(Resource resource, {Link self}) { final relationships = {} ..addAll(resource.toOne.map((k, v) => MapEntry(k, ToOne(nullable(IdentifierJson.fromIdentifier)(v))))) @@ -52,7 +56,9 @@ class ResourceJson { (k, v) => MapEntry(k, ToMany(v.map(IdentifierJson.fromIdentifier))))); return ResourceJson(resource.type, resource.id, - attributes: resource.attributes, relationships: relationships); + attributes: resource.attributes, + relationships: relationships, + self: self); } /// Returns the JSON object to be used in the `data` or `included` members @@ -65,6 +71,9 @@ class ResourceJson { if (relationships.isNotEmpty) { json['relationships'] = relationships; } + if (self != null) { + json['links'] = {'self': self}; + } return json; } @@ -99,9 +108,9 @@ class ResourceJson { /// Represents a single resource or a single related resource of a to-one relationship\\\\\\\\ class ResourceData extends PrimaryData { - final ResourceJson resourceObject; + final ResourceJson resourceJson; - ResourceData(this.resourceObject, {Link self}) : super(self: self); + ResourceData(this.resourceJson, {Link self}) : super(self: self); /// Parse the document static ResourceData parse(Object json) { @@ -115,24 +124,24 @@ class ResourceData extends PrimaryData { @override Map toJson() { - final json = {'data': resourceObject}; + final json = {'data': resourceJson}; final links = toLinks(); if (links.isNotEmpty) json['links'] = links; return json; } - Resource toResource() => resourceObject.toResource(); + Resource toResource() => resourceJson.toResource(); } /// Represents a resource collection or a collection of related resources of a to-many relationship class ResourceCollectionData extends PrimaryData { - final resourceObjects = []; + final collection = []; final Pagination pagination; ResourceCollectionData(Iterable collection, {Link self, this.pagination = const Pagination.empty()}) : super(self: self) { - this.resourceObjects.addAll(collection); + this.collection.addAll(collection); } /// Parse the document @@ -150,7 +159,7 @@ class ResourceCollectionData extends PrimaryData { @override Map toJson() { - final json = {'data': resourceObjects}; + final json = {'data': collection}; final links = toLinks()..addAll(pagination.toLinks()); if (links.isNotEmpty) json['links'] = links; return json; diff --git a/lib/src/server/request.dart b/lib/src/server/request.dart index 09e408c..e4ae388 100644 --- a/lib/src/server/request.dart +++ b/lib/src/server/request.dart @@ -132,7 +132,7 @@ class CreateResource extends JsonApiRequest { CreateResource(HttpRequest request, this.route) : super(request); Future resource() async { - return ResourceData.parse(await _body).resourceObject.toResource(); + return ResourceData.parse(await _body).resourceJson.toResource(); } Future call(JsonApiController controller) => controller.createResource(this); @@ -152,7 +152,7 @@ class UpdateResource extends JsonApiRequest { UpdateResource(HttpRequest request, this.route) : super(request); Future resource() async { - return ResourceData.parse(await _body).resourceObject.toResource(); + return ResourceData.parse(await _body).resourceJson.toResource(); } Future call(JsonApiController controller) => controller.updateResource(this); diff --git a/test/browser_compat_test.dart b/test/browser_compat_test.dart index a24afd7..bee47d2 100644 --- a/test/browser_compat_test.dart +++ b/test/browser_compat_test.dart @@ -13,6 +13,6 @@ void main() async { .fetchCollection(Uri.parse('http://localhost:$port/companies')); expect(r.status, 200); expect(r.isSuccessful, true); - expect(r.data.resourceObjects.first.attributes['name'], 'Tesla'); + expect(r.data.collection.first.attributes['name'], 'Tesla'); }); } diff --git a/test/functional/create_test.dart b/test/functional/create_test.dart index a90e3b8..56f27c3 100644 --- a/test/functional/create_test.dart +++ b/test/functional/create_test.dart @@ -42,7 +42,7 @@ void main() async { // Make sure the resource is available final r1 = await client .fetchResource(Url.resource('models', r0.data.toResource().id)); - expect(r1.data.resourceObject.attributes['name'], 'Model Y'); + expect(r1.data.resourceJson.attributes['name'], 'Model Y'); }); /// If a POST request did include a Client-Generated ID and the requested diff --git a/test/functional/fetch_test.dart b/test/functional/fetch_test.dart index 4d1092e..1846149 100644 --- a/test/functional/fetch_test.dart +++ b/test/functional/fetch_test.dart @@ -22,7 +22,7 @@ void main() async { final r = await client.fetchCollection(uri); expect(r.status, 200); expect(r.isSuccessful, true); - expect(r.data.resourceObjects.first.attributes['name'], 'Tesla'); + expect(r.data.collection.first.attributes['name'], 'Tesla'); expect(r.data.self.uri, uri); }); @@ -34,23 +34,23 @@ void main() async { final r1 = await client.fetchCollection(somePage.pagination.next.uri); final secondPage = r1.data; - expect(secondPage.resourceObjects.first.attributes['name'], 'BMW'); + expect(secondPage.collection.first.attributes['name'], 'BMW'); expect(secondPage.self.uri, somePage.pagination.next.uri); final r2 = await client.fetchCollection(secondPage.pagination.last.uri); final lastPage = r2.data; - expect(lastPage.resourceObjects.first.attributes['name'], 'Toyota'); + expect(lastPage.collection.first.attributes['name'], 'Toyota'); expect(lastPage.self.uri, secondPage.pagination.last.uri); final r3 = await client.fetchCollection(lastPage.pagination.prev.uri); final secondToLastPage = r3.data; - expect(secondToLastPage.resourceObjects.first.attributes['name'], 'Audi'); + expect(secondToLastPage.collection.first.attributes['name'], 'Audi'); expect(secondToLastPage.self.uri, lastPage.pagination.prev.uri); final r4 = await client.fetchCollection(secondToLastPage.pagination.first.uri); final firstPage = r4.data; - expect(firstPage.resourceObjects.first.attributes['name'], 'Tesla'); + expect(firstPage.collection.first.attributes['name'], 'Tesla'); expect(firstPage.self.uri, secondToLastPage.pagination.first.uri); }); @@ -59,7 +59,7 @@ void main() async { final r = await client.fetchCollection(uri); expect(r.status, 200); expect(r.isSuccessful, true); - expect(r.data.resourceObjects.first.attributes['name'], 'Roadster'); + expect(r.data.collection.first.attributes['name'], 'Roadster'); expect(r.data.self.uri, uri); }); diff --git a/test/unit/document_test.dart b/test/unit/document_test.dart index 180535a..dfe3a26 100644 --- a/test/unit/document_test.dart +++ b/test/unit/document_test.dart @@ -21,13 +21,20 @@ void main() { }); group('Standard compliance', () { - test('Can parse the example document', () { - final jsonString = - new File('test/unit/example.json').readAsStringSync(); - final jsonObject = json.decode(jsonString); - final doc = Document.parse(jsonObject, ResourceCollectionData.parse); - expect(doc, encodesToJson(jsonObject)); - }); + try { + test('Can parse the example document', () { + // This is a slightly modified example from the JSON:API site + // See: https://jsonapi.org/ + final jsonString = + new File('test/unit/example.json').readAsStringSync(); + final jsonObject = json.decode(jsonString); + final doc = Document.parse(jsonObject, ResourceCollectionData.parse); + + expect(doc, encodesToJson(jsonObject)); + }); + } catch (e, s) { + print(s); + } }); }); } From 13cace31cd03d6211cf4a6502cae22730f6b3d37 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sat, 16 Mar 2019 22:25:21 -0700 Subject: [PATCH 3/9] WIP --- example/cars_server.dart | 2 +- example/cars_server/controller.dart | 100 +++++++-------- lib/server.dart | 1 - lib/src/server/controller.dart | 122 +++++++++++++++++-- lib/src/server/request.dart | 170 -------------------------- lib/src/server/requests.dart | 174 +++++++++++++++++++++++++++ lib/src/server/route.dart | 25 +++- lib/src/server/routing.dart | 59 +-------- lib/src/server/server.dart | 2 +- lib/src/server/standard_routing.dart | 58 +++++++++ lib/src/server/uri_builder.dart | 8 +- 11 files changed, 429 insertions(+), 292 deletions(-) delete mode 100644 lib/src/server/request.dart create mode 100644 lib/src/server/requests.dart create mode 100644 lib/src/server/standard_routing.dart diff --git a/example/cars_server.dart b/example/cars_server.dart index 4842ddb..3eb4d6c 100644 --- a/example/cars_server.dart +++ b/example/cars_server.dart @@ -49,7 +49,7 @@ Future createServer(InternetAddress addr, int port) async { final controller = CarsController( {'companies': companies, 'cities': cities, 'models': models}); - final routing = StandardRouting(Uri.parse('http://localhost:$port')); + final routing = Routing(Uri.parse('http://localhost:$port')); final server = JsonApiServer(routing); diff --git a/example/cars_server/controller.dart b/example/cars_server/controller.dart index 196819e..6d48cb6 100644 --- a/example/cars_server/controller.dart +++ b/example/cars_server/controller.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:json_api/document.dart'; -import 'package:json_api/server.dart'; +import 'package:json_api/src/server/controller.dart'; +import 'package:json_api/src/server/numbered_page.dart'; import 'package:uuid/uuid.dart'; import 'dao.dart'; @@ -12,13 +13,13 @@ class CarsController implements JsonApiController { CarsController(this.dao); @override - Future fetchCollection(FetchCollection r) async { + Future fetchCollection(FetchCollectionRequest r) async { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final page = NumberedPage.fromQueryParameters(r.queryParameters, + final page = NumberedPage.fromQueryParameters(r.route.parameters, total: dao[r.route.type].length); - return r.collection( + return r.sendCollection( dao[r.route.type] .fetchCollection(offset: page.offset) .map(dao[r.route.type].toResource), @@ -26,37 +27,37 @@ class CarsController implements JsonApiController { } @override - Future fetchRelated(FetchRelated r) { + Future fetchRelated(FetchRelatedRequest r) { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } final res = dao[r.route.type].fetchByIdAsResource(r.route.id); if (res == null) { - return r.notFound([JsonApiError(detail: 'Resource not found')]); + return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } if (res.toOne.containsKey(r.route.relationship)) { final id = res.toOne[r.route.relationship]; final resource = dao[id.type].fetchByIdAsResource(id.id); - return r.resource(resource); + return r.sendResource(resource); } if (res.toMany.containsKey(r.route.relationship)) { final resources = res.toMany[r.route.relationship] .map((id) => dao[id.type].fetchByIdAsResource(id.id)); - return r.collection(resources); + return r.sendCollection(resources); } - return r.notFound([JsonApiError(detail: 'Relationship not found')]); + return r.errorNotFound([JsonApiError(detail: 'Relationship not found')]); } @override - Future fetchResource(FetchResource r) { + Future fetchResource(FetchResourceRequest r) { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } final res = dao[r.route.type].fetchByIdAsResource(r.route.id); if (res == null) { - return r.notFound([JsonApiError(detail: 'Resource not found')]); + return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } final fetchById = (Identifier _) => dao[_.type].fetchByIdAsResource(_.id); @@ -64,63 +65,64 @@ class CarsController implements JsonApiController { .map(fetchById) .followedBy(res.toMany.values.expand((_) => _.map(fetchById))); - return r.resource(res, included: children); + return r.sendResource(res, included: children); } @override - Future fetchRelationship(FetchRelationship r) { + Future fetchRelationship(FetchRelationshipRequest r) { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } final res = dao[r.route.type].fetchByIdAsResource(r.route.id); if (res == null) { - return r.notFound([JsonApiError(detail: 'Resource not found')]); + return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } if (res.toOne.containsKey(r.route.relationship)) { final id = res.toOne[r.route.relationship]; - return r.toOne(id); + return r.sendToOne(id); } if (res.toMany.containsKey(r.route.relationship)) { final ids = res.toMany[r.route.relationship]; - return r.toMany(ids); + return r.sendToMany(ids); } - return r.notFound([JsonApiError(detail: 'Relationship not found')]); + return r.errorNotFound([JsonApiError(detail: 'Relationship not found')]); } @override - Future deleteResource(DeleteResource r) { + Future deleteResource(DeleteResourceRequest r) { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } final res = dao[r.route.type].fetchByIdAsResource(r.route.id); if (res == null) { - return r.notFound([JsonApiError(detail: 'Resource not found')]); + return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } final dependenciesCount = dao[r.route.type].deleteById(r.route.id); if (dependenciesCount == 0) { - return r.noContent(); + return r.sendNoContent(); } - return r.meta({'dependenciesCount': dependenciesCount}); + return r.sendMeta({'dependenciesCount': dependenciesCount}); } - Future createResource(CreateResource r) async { + Future createResource(CreateResourceRequest r) async { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final resource = await r.resource(); + final resource = await r.getResource(); if (r.route.type != resource.type) { - return r.conflict([JsonApiError(detail: 'Incompatible type')]); + return r.errorConflict([JsonApiError(detail: 'Incompatible type')]); } if (resource.hasId) { if (dao[r.route.type].fetchById(resource.id) != null) { - return r.conflict([JsonApiError(detail: 'Resource already exists')]); + return r + .errorConflict([JsonApiError(detail: 'Resource already exists')]); } final created = dao[r.route.type].create(resource); dao[r.route.type].insert(created); - return r.noContent(); + return r.sendNoContent(); } final created = dao[r.route.type].create(Resource( @@ -129,53 +131,53 @@ class CarsController implements JsonApiController { toMany: resource.toMany, toOne: resource.toOne)); dao[r.route.type].insert(created); - return r.created(dao[r.route.type].toResource(created)); + return r.sendCreated(dao[r.route.type].toResource(created)); } @override - Future updateResource(UpdateResource r) async { + Future updateResource(UpdateResourceRequest r) async { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final resource = await r.resource(); + final resource = await r.getResource(); if (r.route.type != resource.type) { - return r.conflict([JsonApiError(detail: 'Incompatible type')]); + return r.errorConflict([JsonApiError(detail: 'Incompatible type')]); } if (dao[r.route.type].fetchById(r.route.id) == null) { - return r.notFound([JsonApiError(detail: 'Resource not found')]); + return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } final updated = dao[r.route.type].update(r.route.id, resource); if (updated == null) { - return r.noContent(); + return r.sendNoContent(); } - return r.updated(updated); + return r.sendUpdated(updated); } @override - Future replaceRelationship(ReplaceRelationship r) async { + Future replaceRelationship(ReplaceRelationshipRequest r) async { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final rel = await r.relationshipData(); + final rel = await r.getRelationship(); if (rel is ToOne) { dao[r.route.type] .replaceToOne(r.route.id, r.route.relationship, rel.toIdentifier()); - return r.noContent(); + return r.sendNoContent(); } if (rel is ToMany) { dao[r.route.type] .replaceToMany(r.route.id, r.route.relationship, rel.identifiers); - return r.noContent(); + return r.sendNoContent(); } } @override - Future addToRelationship(AddToRelationship r) async { + Future addToRelationship(AddToRelationshipRequest r) async { if (!dao.containsKey(r.route.type)) { - return r.notFound([JsonApiError(detail: 'Unknown resource type')]); + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } final result = dao[r.route.type] - .addToMany(r.route.id, r.route.relationship, await r.identifiers()); - return r.toMany(result); + .addToMany(r.route.id, r.route.relationship, await r.getIdentifiers()); + return r.sendToMany(result); } } diff --git a/lib/server.dart b/lib/server.dart index 84eb691..1927d9b 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -1,7 +1,6 @@ export 'package:json_api/src/server/controller.dart'; export 'package:json_api/src/server/numbered_page.dart'; export 'package:json_api/src/server/page.dart'; -export 'package:json_api/src/server/request.dart'; export 'package:json_api/src/server/route.dart'; export 'package:json_api/src/server/route_resolver.dart'; export 'package:json_api/src/server/routing.dart'; diff --git a/lib/src/server/controller.dart b/lib/src/server/controller.dart index 2e00e35..df699cf 100644 --- a/lib/src/server/controller.dart +++ b/lib/src/server/controller.dart @@ -1,23 +1,125 @@ import 'dart:async'; -import 'package:json_api/src/server/request.dart'; +import 'package:json_api/document.dart'; +import 'package:json_api/src/server/page.dart'; +import 'package:json_api/src/server/route.dart'; abstract class JsonApiController { - Future fetchCollection(FetchCollection request); + Future fetchCollection(FetchCollectionRequest rq); - Future fetchRelated(FetchRelated request); + Future fetchRelated(FetchRelatedRequest rq); - Future fetchResource(FetchResource request); + Future fetchResource(FetchResourceRequest rq); - Future fetchRelationship(FetchRelationship request); + Future fetchRelationship(FetchRelationshipRequest rq); - Future deleteResource(DeleteResource request); + Future deleteResource(DeleteResourceRequest rq); - Future createResource(CreateResource request); + Future createResource(CreateResourceRequest rq); - Future updateResource(UpdateResource request); + Future updateResource(UpdateResourceRequest rq); - Future replaceRelationship(ReplaceRelationship request); + Future replaceRelationship(ReplaceRelationshipRequest rq); - Future addToRelationship(AddToRelationship request); + Future addToRelationship(AddToRelationshipRequest rq); +} + +abstract class FetchCollectionRequest { + CollectionRoute get route; + + Future sendCollection(Iterable resources, {Page page}); + + Future errorNotFound(Iterable errors); +} + +abstract class FetchRelatedRequest { + RelatedRoute get route; + + Future sendCollection(Iterable collection); + + Future sendResource(Resource resource); + + Future errorNotFound(Iterable errors); +} + +abstract class FetchRelationshipRequest { + RelationshipRoute get route; + + Future sendToMany(Iterable collection); + + Future sendToOne(Identifier id); + + Future errorNotFound(Iterable errors); +} + +abstract class ReplaceRelationshipRequest { + RelationshipRoute get route; + + Future getRelationship(); + + Future sendNoContent(); + + Future sendToMany(Iterable collection); + + Future sendToOne(Identifier id); + + Future errorNotFound(Iterable errors); +} + +abstract class AddToRelationshipRequest { + RelationshipRoute get route; + + Future> getIdentifiers(); + + Future sendToMany(Iterable collection); + + Future errorNotFound(Iterable errors); +} + +abstract class FetchResourceRequest { + ResourceRoute get route; + + Future sendResource(Resource resource, {Iterable included}); + + Future errorNotFound(Iterable errors); +} + +abstract class DeleteResourceRequest { + ResourceRoute get route; + + Future sendNoContent(); + + Future sendMeta(Map meta); + + Future errorNotFound(Iterable errors); +} + +abstract class CreateResourceRequest { + CollectionRoute get route; + + Future getResource(); + + Future sendCreated(Resource resource); + + Future sendNoContent(); + + Future errorConflict(Iterable errors); + + Future errorNotFound(Iterable errors); +} + +abstract class UpdateResourceRequest { + ResourceRoute get route; + + Future getResource(); + + Future sendUpdated(Resource resource); + + Future sendNoContent(); + + Future errorConflict(Iterable errors); + + Future errorForbidden(Iterable errors); + + Future errorNotFound(Iterable errors); } diff --git a/lib/src/server/request.dart b/lib/src/server/request.dart deleted file mode 100644 index e4ae388..0000000 --- a/lib/src/server/request.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:json_api/document.dart'; -import 'package:json_api/src/server/controller.dart'; -import 'package:json_api/src/server/page.dart'; -import 'package:json_api/src/server/route.dart'; -import 'package:json_api/src/server/server.dart'; - -abstract class JsonApiRequest { - final HttpRequest _request; - JsonApiServer _server; - - JsonApiRequest(this._request); - - Map get queryParameters => - _request.requestedUri.queryParameters; - - HttpResponse get _response => _request.response; - - Future get _body async => - json.decode(await _request.transform(utf8.decoder).join()); - - Future call(JsonApiController controller); - - Future notFound([List errors = const []]) => - _server.error(_response, 404, errors); - - bind(JsonApiServer server) => _server = server; -} - -class FetchCollection extends JsonApiRequest { - final CollectionRoute route; - - FetchCollection(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.fetchCollection(this); - - Future collection(Iterable resources, {Page page}) => - _server.collection(_response, route, resources, page: page); -} - -class FetchRelated extends JsonApiRequest { - final RelatedRoute route; - - FetchRelated(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.fetchRelated(this); - - Future collection(Iterable collection) => - _server.relatedCollection(_response, route, collection); - - Future resource(Resource resource) => - _server.relatedResource(_response, route, resource); -} - -class FetchRelationship extends JsonApiRequest { - final RelationshipRoute route; - - FetchRelationship(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => - controller.fetchRelationship(this); - - Future toMany(Iterable collection) => - _server.toMany(_response, route, collection); - - Future toOne(Identifier id) => _server.toOne(_response, route, id); -} - -class ReplaceRelationship extends JsonApiRequest { - final RelationshipRoute route; - - ReplaceRelationship(HttpRequest request, this.route) : super(request); - - Future relationshipData() async => - Relationship.parse(await _body); - - Future call(JsonApiController controller) => - controller.replaceRelationship(this); - - Future noContent() => _server.write(_response, 204); - - Future toMany(Iterable collection) => - _server.toMany(_response, route, collection); - - Future toOne(Identifier id) => _server.toOne(_response, route, id); -} - -class AddToRelationship extends JsonApiRequest { - final RelationshipRoute route; - - AddToRelationship(HttpRequest request, this.route) : super(request); - - Future> identifiers() async => - ToMany.parse(await _body).identifiers; - - Future call(JsonApiController controller) => - controller.addToRelationship(this); - - Future toMany(Iterable collection) => - _server.toMany(_response, route, collection); -} - -class FetchResource extends JsonApiRequest { - final ResourceRoute route; - - FetchResource(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.fetchResource(this); - - Future resource(Resource resource, {Iterable included}) => - _server.resource(_response, route, resource, included: included); -} - -class DeleteResource extends JsonApiRequest { - final ResourceRoute route; - - DeleteResource(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.deleteResource(this); - - Future noContent() => _server.write(_response, 204); - - Future meta(Map meta) => _server.meta(_response, route, meta); -} - -class CreateResource extends JsonApiRequest { - final CollectionRoute route; - - CreateResource(HttpRequest request, this.route) : super(request); - - Future resource() async { - return ResourceData.parse(await _body).resourceJson.toResource(); - } - - Future call(JsonApiController controller) => controller.createResource(this); - - Future created(Resource resource) => - _server.created(_response, route, resource); - - Future conflict(List errors) => - _server.error(_response, 409, errors); - - Future noContent() => _server.write(_response, 204); -} - -class UpdateResource extends JsonApiRequest { - final ResourceRoute route; - - UpdateResource(HttpRequest request, this.route) : super(request); - - Future resource() async { - return ResourceData.parse(await _body).resourceJson.toResource(); - } - - Future call(JsonApiController controller) => controller.updateResource(this); - - Future updated(Resource resource) => - _server.resource(_response, route, resource); - - Future conflict(List errors) => - _server.error(_response, 409, errors); - - Future forbidden(List errors) => - _server.error(_response, 403, errors); - - Future noContent() => _server.write(_response, 204); -} diff --git a/lib/src/server/requests.dart b/lib/src/server/requests.dart new file mode 100644 index 0000000..c53aea8 --- /dev/null +++ b/lib/src/server/requests.dart @@ -0,0 +1,174 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_api/document.dart'; +import 'package:json_api/src/server/controller.dart'; +import 'package:json_api/src/server/page.dart'; +import 'package:json_api/src/server/route.dart'; +import 'package:json_api/src/server/server.dart'; + +abstract class _BaseRequest implements JsonApiRequest { + final HttpRequest request; + JsonApiServer server; + + _BaseRequest(this.request); + + Map get queryParameters => + request.requestedUri.queryParameters; + + HttpResponse get _response => request.response; + + Future get _body async => + json.decode(await request.transform(utf8.decoder).join()); + + Future call(JsonApiController controller); + + Future errorNotFound([Iterable errors = const []]) => + server.error(_response, 404, errors); + + bind(JsonApiServer s) => server = s; +} + +class FetchCollection extends _BaseRequest implements FetchCollectionRequest { + final CollectionRoute route; + + FetchCollection(HttpRequest request, this.route) : super(request); + + Future call(JsonApiController controller) => controller.fetchCollection(this); + + Future sendCollection(Iterable resources, {Page page}) => + server.collection(_response, route, resources, page: page); +} + +class FetchRelated extends _BaseRequest implements FetchRelatedRequest { + final RelatedRoute route; + + FetchRelated(HttpRequest request, this.route) : super(request); + + Future call(JsonApiController controller) => controller.fetchRelated(this); + + Future sendCollection(Iterable collection) => + server.relatedCollection(_response, route, collection); + + Future sendResource(Resource resource) => + server.relatedResource(_response, route, resource); +} + +class FetchRelationship extends _BaseRequest + implements FetchRelationshipRequest { + final RelationshipRoute route; + + FetchRelationship(HttpRequest request, this.route) : super(request); + + Future call(JsonApiController controller) => + controller.fetchRelationship(this); + + Future sendToMany(Iterable collection) => + server.toMany(_response, route, collection); + + Future sendToOne(Identifier id) => server.toOne(_response, route, id); +} + +class ReplaceRelationship extends _BaseRequest + implements ReplaceRelationshipRequest { + final RelationshipRoute route; + + ReplaceRelationship(HttpRequest request, this.route) : super(request); + + Future getRelationship() async => + Relationship.parse(await _body); + + Future call(JsonApiController controller) => + controller.replaceRelationship(this); + + Future sendNoContent() => server.write(_response, 204); + + Future sendToMany(Iterable collection) => + server.toMany(_response, route, collection); + + Future sendToOne(Identifier id) => server.toOne(_response, route, id); +} + +class AddToRelationship extends _BaseRequest + implements AddToRelationshipRequest { + final RelationshipRoute route; + + AddToRelationship(HttpRequest request, this.route) : super(request); + + Future> getIdentifiers() async => + ToMany.parse(await _body).identifiers; + + Future call(JsonApiController controller) => + controller.addToRelationship(this); + + Future sendToMany(Iterable collection) => + server.toMany(_response, route, collection); +} + +class FetchResource extends _BaseRequest implements FetchResourceRequest { + final ResourceRoute route; + + FetchResource(HttpRequest request, this.route) : super(request); + + Future call(JsonApiController controller) => controller.fetchResource(this); + + Future sendResource(Resource resource, {Iterable included}) => + server.resource(_response, route, resource, included: included); +} + +class DeleteResource extends _BaseRequest implements DeleteResourceRequest { + final ResourceRoute route; + + DeleteResource(HttpRequest request, this.route) : super(request); + + Future call(JsonApiController controller) => controller.deleteResource(this); + + Future sendNoContent() => server.write(_response, 204); + + Future sendMeta(Map meta) => + server.meta(_response, route, meta); +} + +class CreateResource extends _BaseRequest implements CreateResourceRequest { + final CollectionRoute route; + + CreateResource(HttpRequest request, this.route) : super(request); + + Future getResource() async { + return ResourceData.parse(await _body).resourceJson.toResource(); + } + + Future call(JsonApiController controller) => controller.createResource(this); + + Future sendCreated(Resource resource) => + server.created(_response, route, resource); + + Future errorConflict(Iterable errors) => + server.error(_response, 409, errors); + + Future sendNoContent() => server.write(_response, 204); +} + +class UpdateResource extends _BaseRequest implements UpdateResourceRequest { + final ResourceRoute route; + + UpdateResource(HttpRequest request, this.route) : super(request); + + Future getResource() async { + return ResourceData.parse(await _body).resourceJson.toResource(); + } + + Future call(JsonApiController controller) => controller.updateResource(this); + + Future sendUpdated(Resource resource) => + server.resource(_response, route, resource); + + Future errorConflict(Iterable errors) => + server.error(_response, 409, errors); + + Future errorForbidden(Iterable errors) => + server.error(_response, 403, errors); + + Future sendNoContent() => server.write(_response, 204); +} diff --git a/lib/src/server/route.dart b/lib/src/server/route.dart index 9679d61..aee07a6 100644 --- a/lib/src/server/route.dart +++ b/lib/src/server/route.dart @@ -1,8 +1,17 @@ +import 'dart:async'; import 'dart:io'; -import 'package:json_api/src/server/request.dart'; +import 'package:json_api/src/server/controller.dart'; +import 'package:json_api/src/server/requests.dart'; +import 'package:json_api/src/server/server.dart'; import 'package:json_api/src/server/uri_builder.dart'; +abstract class JsonApiRequest { + Future call(JsonApiController controller); + + bind(JsonApiServer server); +} + abstract class JsonApiRoute { final Uri uri; @@ -15,6 +24,20 @@ abstract class JsonApiRoute { /// URI parameters Map get parameters => uri.queryParameters; + + factory JsonApiRoute.collection(Uri uri, String type) => + CollectionRoute(uri, type); + + factory JsonApiRoute.resource(Uri uri, String type, String id) => + ResourceRoute(uri, type, id); + + factory JsonApiRoute.relationship( + Uri uri, String type, String id, String relationship) => + RelationshipRoute(uri, type, id, relationship); + + factory JsonApiRoute.related( + Uri uri, String type, String id, String relationship) => + RelatedRoute(uri, type, id, relationship); } class CollectionRoute extends JsonApiRoute { diff --git a/lib/src/server/routing.dart b/lib/src/server/routing.dart index 1fbbb12..69d52b5 100644 --- a/lib/src/server/routing.dart +++ b/lib/src/server/routing.dart @@ -1,61 +1,10 @@ -import 'package:json_api/src/server/route.dart'; import 'package:json_api/src/server/route_resolver.dart'; +import 'package:json_api/src/server/standard_routing.dart'; import 'package:json_api/src/server/uri_builder.dart'; /// Routing defines the design of URLs. -abstract class Routing implements UriBuilder, RouteResolver {} - -/// StandardRouting implements the recommended URL design schema: -/// -/// /photos - for a collection -/// /photos/1 - for a resource -/// /photos/1/relationships/author - for a relationship -/// /photos/1/author - for a related resource -/// -/// See https://jsonapi.org/recommendations/#urls -class StandardRouting implements Routing { - final Uri base; - - StandardRouting(this.base) { - ArgumentError.checkNotNull(base, 'base'); - } - - collection(String type, {Map params = const {}}) { - final combined = {} - ..addAll(base.queryParameters) - ..addAll(params); - return base.replace( - pathSegments: base.pathSegments + [type], - queryParameters: combined.isNotEmpty ? combined : null); - } - - related(String type, String id, String relationship, - {Map params = const {}}) => - base.replace(pathSegments: base.pathSegments + [type, id, relationship]); - - relationship(String type, String id, String relationship, - {Map params = const {}}) => - base.replace( - pathSegments: - base.pathSegments + [type, id, 'relationships', relationship]); - - resource(String type, String id, {Map params = const {}}) => - base.replace(pathSegments: base.pathSegments + [type, id]); - - JsonApiRoute getRoute(Uri uri) { - final segments = uri.pathSegments; - switch (segments.length) { - case 1: - return CollectionRoute(uri, segments[0]); - case 2: - return ResourceRoute(uri, segments[0], segments[1]); - case 3: - return RelatedRoute(uri, segments[0], segments[1], segments[2]); - case 4: - if (segments[2] == 'relationships') { - return RelationshipRoute(uri, segments[0], segments[1], segments[3]); - } - } - return null; // TODO: replace with a null-object +abstract class Routing implements UriBuilder, RouteResolver { + factory Routing(Uri base) { + return StandardRouting(base); } } diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index 45f3a04..a7b22e9 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -41,7 +41,7 @@ class JsonApiServer { Link(route.self(url, parameters: _.parameters))))), )); - Future error(HttpResponse response, int status, List errors) => + Future error(HttpResponse response, int status, Iterable errors) => write(response, status, document: Document.error(errors)); Future relatedCollection(HttpResponse response, RelatedRoute route, diff --git a/lib/src/server/standard_routing.dart b/lib/src/server/standard_routing.dart new file mode 100644 index 0000000..26125c3 --- /dev/null +++ b/lib/src/server/standard_routing.dart @@ -0,0 +1,58 @@ + +import 'package:json_api/src/server/route.dart'; +import 'package:json_api/src/server/routing.dart'; + +/// StandardRouting implements the recommended URL design schema: +/// +/// /photos - for a collection +/// /photos/1 - for a resource +/// /photos/1/relationships/author - for a relationship +/// /photos/1/author - for a related resource +/// +/// See https://jsonapi.org/recommendations/#urls +class StandardRouting implements Routing { + final Uri base; + + StandardRouting(this.base) { + ArgumentError.checkNotNull(base, 'base'); + } + + collection(String type, {Map params = const {}}) { + final combined = {} + ..addAll(base.queryParameters) + ..addAll(params); + return base.replace( + pathSegments: base.pathSegments + [type], + queryParameters: combined.isNotEmpty ? combined : null); + } + + related(String type, String id, String relationship, + {Map params = const {}}) => + base.replace(pathSegments: base.pathSegments + [type, id, relationship]); + + relationship(String type, String id, String relationship, + {Map params = const {}}) => + base.replace( + pathSegments: + base.pathSegments + [type, id, 'relationships', relationship]); + + resource(String type, String id, {Map params = const {}}) => + base.replace(pathSegments: base.pathSegments + [type, id]); + + JsonApiRoute getRoute(Uri uri) { + final segments = uri.pathSegments; + switch (segments.length) { + case 1: + return JsonApiRoute.collection(uri, segments[0]); + case 2: + return JsonApiRoute.resource(uri, segments[0], segments[1]); + case 3: + return JsonApiRoute.related(uri, segments[0], segments[1], segments[2]); + case 4: + if (segments[2] == 'relationships') { + return JsonApiRoute.relationship(uri, segments[0], segments[1], segments[3]); + } + } + return null; // TODO: replace with a null-object + } +} diff --git a/lib/src/server/uri_builder.dart b/lib/src/server/uri_builder.dart index 2067f41..e3a9c05 100644 --- a/lib/src/server/uri_builder.dart +++ b/lib/src/server/uri_builder.dart @@ -1,15 +1,15 @@ abstract class UriBuilder { /// Builds a URI for a resource collection - Uri collection(String type, {Map params = const {}}); + Uri collection(String type, {Map params}); /// Builds a URI for a single resource - Uri resource(String type, String id, {Map params = const {}}); + Uri resource(String type, String id, {Map params}); /// Builds a URI for a related resource Uri related(String type, String id, String relationship, - {Map params = const {}}); + {Map params}); /// Builds a URI for a relationship object Uri relationship(String type, String id, String relationship, - {Map params = const {}}); + {Map params}); } From cb3d2a8bdaaa18ad2ad03c0f60349c47b4b351fb Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sun, 17 Mar 2019 18:58:56 -0700 Subject: [PATCH 4/9] WIP --- example/cars_server.dart | 15 +- example/cars_server/controller.dart | 112 +++++---- lib/server.dart | 10 +- lib/src/document/relationship.dart | 2 +- lib/src/document/resource_json.dart | 8 +- lib/src/server/contracts/controller.dart | 134 +++++++++++ .../server/contracts/document_builder.dart | 41 ++++ lib/src/server/{ => contracts}/page.dart | 0 lib/src/server/contracts/router.dart | 47 ++++ lib/src/server/controller.dart | 125 ---------- lib/src/server/numbered_page.dart | 2 +- lib/src/server/requests.dart | 174 -------------- lib/src/server/route.dart | 132 ----------- lib/src/server/route_resolver.dart | 6 - lib/src/server/routing.dart | 10 - lib/src/server/server.dart | 105 ++------- lib/src/server/server_requests.dart | 219 ++++++++++++++++++ lib/src/server/server_routes.dart | 111 +++++++++ lib/src/server/standard_document_builder.dart | 100 ++++++++ lib/src/server/standard_router.dart | 56 +++++ lib/src/server/standard_routing.dart | 58 ----- lib/src/server/uri_builder.dart | 15 -- test/functional/fetch_test.dart | 13 +- test/functional/update_test.dart | 8 +- 24 files changed, 812 insertions(+), 691 deletions(-) create mode 100644 lib/src/server/contracts/controller.dart create mode 100644 lib/src/server/contracts/document_builder.dart rename lib/src/server/{ => contracts}/page.dart (100%) create mode 100644 lib/src/server/contracts/router.dart delete mode 100644 lib/src/server/controller.dart delete mode 100644 lib/src/server/requests.dart delete mode 100644 lib/src/server/route.dart delete mode 100644 lib/src/server/route_resolver.dart delete mode 100644 lib/src/server/routing.dart create mode 100644 lib/src/server/server_requests.dart create mode 100644 lib/src/server/server_routes.dart create mode 100644 lib/src/server/standard_document_builder.dart create mode 100644 lib/src/server/standard_router.dart delete mode 100644 lib/src/server/standard_routing.dart delete mode 100644 lib/src/server/uri_builder.dart diff --git a/example/cars_server.dart b/example/cars_server.dart index 3eb4d6c..681a0b7 100644 --- a/example/cars_server.dart +++ b/example/cars_server.dart @@ -49,22 +49,13 @@ Future createServer(InternetAddress addr, int port) async { final controller = CarsController( {'companies': companies, 'cities': cities, 'models': models}); - final routing = Routing(Uri.parse('http://localhost:$port')); + final router = StandardRouter(Uri.parse('http://localhost:$port')); - final server = JsonApiServer(routing); + final jsonApiServer = JsonApiServer(router, controller); final httpServer = await HttpServer.bind(addr, port); - httpServer.forEach((request) async { - final route = await routing.getRoute(request.requestedUri); - if (route == null) { - request.response.statusCode = 404; - return request.response.close(); - } - route.createRequest(request) - ..bind(server) - ..call(controller); - }); + httpServer.forEach(jsonApiServer.process); return httpServer; } diff --git a/example/cars_server/controller.dart b/example/cars_server/controller.dart index 6d48cb6..d5c474d 100644 --- a/example/cars_server/controller.dart +++ b/example/cars_server/controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:json_api/document.dart'; -import 'package:json_api/src/server/controller.dart'; +import 'package:json_api/src/server/contracts/controller.dart'; import 'package:json_api/src/server/numbered_page.dart'; import 'package:uuid/uuid.dart'; @@ -14,36 +14,36 @@ class CarsController implements JsonApiController { @override Future fetchCollection(FetchCollectionRequest r) async { - if (!dao.containsKey(r.route.type)) { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final page = NumberedPage.fromQueryParameters(r.route.parameters, - total: dao[r.route.type].length); + final page = NumberedPage.fromQueryParameters(r.uri.queryParameters, + total: dao[r.type].length); return r.sendCollection( - dao[r.route.type] + dao[r.type] .fetchCollection(offset: page.offset) - .map(dao[r.route.type].toResource), + .map(dao[r.type].toResource), page: page); } @override Future fetchRelated(FetchRelatedRequest r) { - if (!dao.containsKey(r.route.type)) { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final res = dao[r.route.type].fetchByIdAsResource(r.route.id); + final res = dao[r.type].fetchByIdAsResource(r.id); if (res == null) { return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } - if (res.toOne.containsKey(r.route.relationship)) { - final id = res.toOne[r.route.relationship]; + if (res.toOne.containsKey(r.relationship)) { + final id = res.toOne[r.relationship]; final resource = dao[id.type].fetchByIdAsResource(id.id); return r.sendResource(resource); } - if (res.toMany.containsKey(r.route.relationship)) { - final resources = res.toMany[r.route.relationship] + if (res.toMany.containsKey(r.relationship)) { + final resources = res.toMany[r.relationship] .map((id) => dao[id.type].fetchByIdAsResource(id.id)); return r.sendCollection(resources); } @@ -52,10 +52,10 @@ class CarsController implements JsonApiController { @override Future fetchResource(FetchResourceRequest r) { - if (!dao.containsKey(r.route.type)) { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final res = dao[r.route.type].fetchByIdAsResource(r.route.id); + final res = dao[r.type].fetchByIdAsResource(r.id); if (res == null) { return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } @@ -70,21 +70,21 @@ class CarsController implements JsonApiController { @override Future fetchRelationship(FetchRelationshipRequest r) { - if (!dao.containsKey(r.route.type)) { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final res = dao[r.route.type].fetchByIdAsResource(r.route.id); + final res = dao[r.type].fetchByIdAsResource(r.id); if (res == null) { return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } - if (res.toOne.containsKey(r.route.relationship)) { - final id = res.toOne[r.route.relationship]; + if (res.toOne.containsKey(r.relationship)) { + final id = res.toOne[r.relationship]; return r.sendToOne(id); } - if (res.toMany.containsKey(r.route.relationship)) { - final ids = res.toMany[r.route.relationship]; + if (res.toMany.containsKey(r.relationship)) { + final ids = res.toMany[r.relationship]; return r.sendToMany(ids); } return r.errorNotFound([JsonApiError(detail: 'Relationship not found')]); @@ -92,14 +92,14 @@ class CarsController implements JsonApiController { @override Future deleteResource(DeleteResourceRequest r) { - if (!dao.containsKey(r.route.type)) { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final res = dao[r.route.type].fetchByIdAsResource(r.route.id); + final res = dao[r.type].fetchByIdAsResource(r.id); if (res == null) { return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } - final dependenciesCount = dao[r.route.type].deleteById(r.route.id); + final dependenciesCount = dao[r.type].deleteById(r.id); if (dependenciesCount == 0) { return r.sendNoContent(); } @@ -107,46 +107,43 @@ class CarsController implements JsonApiController { } Future createResource(CreateResourceRequest r) async { - if (!dao.containsKey(r.route.type)) { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final resource = await r.getResource(); - if (r.route.type != resource.type) { + if (r.type != r.resource.type) { return r.errorConflict([JsonApiError(detail: 'Incompatible type')]); } - if (resource.hasId) { - if (dao[r.route.type].fetchById(resource.id) != null) { + if (r.resource.hasId) { + if (dao[r.type].fetchById(r.resource.id) != null) { return r .errorConflict([JsonApiError(detail: 'Resource already exists')]); } - final created = dao[r.route.type].create(resource); - dao[r.route.type].insert(created); + final created = dao[r.type].create(r.resource); + dao[r.type].insert(created); return r.sendNoContent(); } - final created = dao[r.route.type].create(Resource( - resource.type, Uuid().v4(), - attributes: resource.attributes, - toMany: resource.toMany, - toOne: resource.toOne)); - dao[r.route.type].insert(created); - return r.sendCreated(dao[r.route.type].toResource(created)); + final created = dao[r.type].create(Resource(r.resource.type, Uuid().v4(), + attributes: r.resource.attributes, + toMany: r.resource.toMany, + toOne: r.resource.toOne)); + dao[r.type].insert(created); + return r.sendCreated(dao[r.type].toResource(created)); } @override Future updateResource(UpdateResourceRequest r) async { - if (!dao.containsKey(r.route.type)) { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final resource = await r.getResource(); - if (r.route.type != resource.type) { + if (r.type != r.resource.type) { return r.errorConflict([JsonApiError(detail: 'Incompatible type')]); } - if (dao[r.route.type].fetchById(r.route.id) == null) { + if (dao[r.type].fetchById(r.id) == null) { return r.errorNotFound([JsonApiError(detail: 'Resource not found')]); } - final updated = dao[r.route.type].update(r.route.id, resource); + final updated = dao[r.type].update(r.id, r.resource); if (updated == null) { return r.sendNoContent(); } @@ -154,30 +151,29 @@ class CarsController implements JsonApiController { } @override - Future replaceRelationship(ReplaceRelationshipRequest r) async { - if (!dao.containsKey(r.route.type)) { + Future replaceToOne(ReplaceToOneRequest r) async { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final rel = await r.getRelationship(); - if (rel is ToOne) { - dao[r.route.type] - .replaceToOne(r.route.id, r.route.relationship, rel.toIdentifier()); - return r.sendNoContent(); - } - if (rel is ToMany) { - dao[r.route.type] - .replaceToMany(r.route.id, r.route.relationship, rel.identifiers); - return r.sendNoContent(); + dao[r.type].replaceToOne(r.id, r.relationship, r.identifier); + return r.sendNoContent(); + } + + @override + Future replaceToMany(ReplaceToManyRequest r) async { + if (!dao.containsKey(r.type)) { + return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } + dao[r.type].replaceToMany(r.id, r.relationship, r.identifiers); + return r.sendNoContent(); } @override - Future addToRelationship(AddToRelationshipRequest r) async { - if (!dao.containsKey(r.route.type)) { + Future addToMany(AddToManyRequest r) async { + if (!dao.containsKey(r.type)) { return r.errorNotFound([JsonApiError(detail: 'Unknown resource type')]); } - final result = dao[r.route.type] - .addToMany(r.route.id, r.route.relationship, await r.getIdentifiers()); + final result = dao[r.type].addToMany(r.id, r.relationship, r.identifiers); return r.sendToMany(result); } } diff --git a/lib/server.dart b/lib/server.dart index 1927d9b..ae514df 100644 --- a/lib/server.dart +++ b/lib/server.dart @@ -1,8 +1,6 @@ -export 'package:json_api/src/server/controller.dart'; +export 'package:json_api/src/server/contracts/controller.dart'; +export 'package:json_api/src/server/contracts/page.dart'; +export 'package:json_api/src/server/contracts/router.dart'; export 'package:json_api/src/server/numbered_page.dart'; -export 'package:json_api/src/server/page.dart'; -export 'package:json_api/src/server/route.dart'; -export 'package:json_api/src/server/route_resolver.dart'; -export 'package:json_api/src/server/routing.dart'; export 'package:json_api/src/server/server.dart'; -export 'package:json_api/src/server/uri_builder.dart'; +export 'package:json_api/src/server/standard_router.dart'; diff --git a/lib/src/document/relationship.dart b/lib/src/document/relationship.dart index 836d9cd..580c1da 100644 --- a/lib/src/document/relationship.dart +++ b/lib/src/document/relationship.dart @@ -135,5 +135,5 @@ class ToMany extends Relationship { /// Converts to List<[Identifier]>. /// For empty relationships returns an empty List. - Iterable get identifiers => linkage.map((_) => _.toIdentifier()); + Iterable toIdentifiers() => linkage.map((_) => _.toIdentifier()); } diff --git a/lib/src/document/resource_json.dart b/lib/src/document/resource_json.dart index ce603e9..3852717 100644 --- a/lib/src/document/resource_json.dart +++ b/lib/src/document/resource_json.dart @@ -48,7 +48,7 @@ class ResourceJson { throw 'Can not parse ResourceObject from $json'; } - static ResourceJson fromResource(Resource resource, {Link self}) { + static ResourceJson fromResource(Resource resource) { final relationships = {} ..addAll(resource.toOne.map((k, v) => MapEntry(k, ToOne(nullable(IdentifierJson.fromIdentifier)(v))))) @@ -56,9 +56,7 @@ class ResourceJson { (k, v) => MapEntry(k, ToMany(v.map(IdentifierJson.fromIdentifier))))); return ResourceJson(resource.type, resource.id, - attributes: resource.attributes, - relationships: relationships, - self: self); + attributes: resource.attributes, relationships: relationships); } /// Returns the JSON object to be used in the `data` or `included` members @@ -90,7 +88,7 @@ class ResourceJson { if (rel is ToOne) { toOne[name] = rel.toIdentifier(); } else if (rel is ToMany) { - toMany[name] = rel.identifiers.toList(); + toMany[name] = rel.toIdentifiers().toList(); } else { incomplete[name] = rel; } diff --git a/lib/src/server/contracts/controller.dart b/lib/src/server/contracts/controller.dart new file mode 100644 index 0000000..d2035e8 --- /dev/null +++ b/lib/src/server/contracts/controller.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:json_api/document.dart'; +import 'package:json_api/src/server/contracts/page.dart'; + +abstract class JsonApiController { + Future fetchCollection(FetchCollectionRequest rq); + + Future fetchRelated(FetchRelatedRequest rq); + + Future fetchResource(FetchResourceRequest rq); + + Future fetchRelationship(FetchRelationshipRequest rq); + + Future deleteResource(DeleteResourceRequest rq); + + Future createResource(CreateResourceRequest rq); + + Future updateResource(UpdateResourceRequest rq); + + Future replaceToOne(ReplaceToOneRequest rq); + + Future replaceToMany(ReplaceToManyRequest rq); + + Future addToMany(AddToManyRequest rq); +} + +abstract class JsonApiRequest { + Uri get uri; + + String get type; + + Future errorNotFound(Iterable errors); +} + +abstract class FetchCollectionRequest extends JsonApiRequest { + Future sendCollection(Iterable resources, {Page page}); +} + +abstract class FetchRelatedRequest extends JsonApiRequest { + String get id; + + String get relationship; + + Future sendCollection(Iterable collection); + + Future sendResource(Resource resource); +} + +abstract class FetchRelationshipRequest extends JsonApiRequest { + String get id; + + String get relationship; + + Future sendToMany(Iterable collection); + + Future sendToOne(Identifier id); +} + +abstract class ReplaceToOneRequest extends JsonApiRequest { + String get id; + + String get relationship; + + Identifier get identifier; + + Future sendNoContent(); + + Future sendToMany(Iterable collection); + + Future sendToOne(Identifier id); +} + +abstract class ReplaceToManyRequest extends JsonApiRequest { + String get id; + + String get relationship; + + Iterable get identifiers; + + Future sendNoContent(); + + Future sendToMany(Iterable collection); + + Future sendToOne(Identifier id); +} + +abstract class AddToManyRequest extends JsonApiRequest { + String get id; + + String get relationship; + + Iterable get identifiers; + + Future sendToMany(Iterable collection); +} + +abstract class FetchResourceRequest extends JsonApiRequest { + String get id; + + Future sendResource(Resource resource, {Iterable included}); +} + +abstract class DeleteResourceRequest extends JsonApiRequest { + String get id; + + Future sendNoContent(); + + Future sendMeta(Map meta); +} + +abstract class CreateResourceRequest extends JsonApiRequest { + Resource get resource; + + Future sendCreated(Resource resource); + + Future sendNoContent(); + + Future errorConflict(Iterable errors); +} + +abstract class UpdateResourceRequest extends JsonApiRequest { + String get id; + + Resource get resource; + + Future sendUpdated(Resource resource); + + Future sendNoContent(); + + Future errorConflict(Iterable errors); + + Future errorForbidden(Iterable errors); +} diff --git a/lib/src/server/contracts/document_builder.dart b/lib/src/server/contracts/document_builder.dart new file mode 100644 index 0000000..ba5cf12 --- /dev/null +++ b/lib/src/server/contracts/document_builder.dart @@ -0,0 +1,41 @@ +import 'package:json_api/document.dart'; +import 'package:json_api/src/server/contracts/page.dart'; + +/// The Document builder is used by JsonApiServer. It abstracts the process +/// of building response documents and is responsible for such aspects as +/// adding `meta` and `jsonapi` attributes and generating links +abstract class DocumentBuilder { + /// Given the [collection] of type [type] return a document. + /// If the collection is paginated, the [page] parameter will contain the + /// current page details. + Document collection( + Iterable collection, String type, Uri self, + {Page page, Iterable included}); + + Document relatedCollection( + Iterable collection, + String type, + String id, + String relationship, + Uri self, + {Page page, + Iterable included}); + + Document resource( + Resource resource, String type, String id, Uri self, + {Iterable included}); + + Document relatedResource( + Resource resource, String type, String id, String relationship, Uri self, + {Iterable included}); + + Document toMany(Iterable collection, String type, + String id, String relationship, Uri self); + + Document toOne(Identifier identifier, String type, String id, + String relationship, Uri self); + + Document meta(Map meta); + + Document error(Iterable errors); +} diff --git a/lib/src/server/page.dart b/lib/src/server/contracts/page.dart similarity index 100% rename from lib/src/server/page.dart rename to lib/src/server/contracts/page.dart diff --git a/lib/src/server/contracts/router.dart b/lib/src/server/contracts/router.dart new file mode 100644 index 0000000..5d8f70e --- /dev/null +++ b/lib/src/server/contracts/router.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +abstract class UriBuilder { + /// Builds a URI for a resource collection + Uri collection(String type, {Map parameters}); + + /// Builds a URI for a single resource + Uri resource(String type, String id, {Map parameters}); + + /// Builds a URI for a related resource + Uri related(String type, String id, String relationship, + {Map parameters}); + + /// Builds a URI for a relationship object + Uri relationship(String type, String id, String relationship, + {Map parameters}); +} + +/// Route resolver detects the type of the route by [Uri] +abstract class RouteResolver { + /// Resolves HTTP request to route object. + /// This function should call one of the methods of the [factory] object depending on the + /// detected route and return the result back. If the route can be matched + /// to neither Collection, Resource, Related Resource nor Relationship, + /// this method should return the Unmatched route. + FutureOr getRoute(Uri uri, RouteFactory factory); +} + +abstract class RouteFactory { + /// Returns a Resource Collection route + R collection(String type); + + /// Returns a Resource route + R resource(String type, String id); + + /// Returns a Relationship route + R relationship(String type, String id, String relationship); + + /// Returns a Related Resource route + R related(String type, String id, String relationship); + + /// Returns the Unmatched route (neither of the above) + R unmatched(); +} + +/// Routing defines the design of URLs. +abstract class Router implements UriBuilder, RouteResolver {} diff --git a/lib/src/server/controller.dart b/lib/src/server/controller.dart deleted file mode 100644 index df699cf..0000000 --- a/lib/src/server/controller.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'dart:async'; - -import 'package:json_api/document.dart'; -import 'package:json_api/src/server/page.dart'; -import 'package:json_api/src/server/route.dart'; - -abstract class JsonApiController { - Future fetchCollection(FetchCollectionRequest rq); - - Future fetchRelated(FetchRelatedRequest rq); - - Future fetchResource(FetchResourceRequest rq); - - Future fetchRelationship(FetchRelationshipRequest rq); - - Future deleteResource(DeleteResourceRequest rq); - - Future createResource(CreateResourceRequest rq); - - Future updateResource(UpdateResourceRequest rq); - - Future replaceRelationship(ReplaceRelationshipRequest rq); - - Future addToRelationship(AddToRelationshipRequest rq); -} - -abstract class FetchCollectionRequest { - CollectionRoute get route; - - Future sendCollection(Iterable resources, {Page page}); - - Future errorNotFound(Iterable errors); -} - -abstract class FetchRelatedRequest { - RelatedRoute get route; - - Future sendCollection(Iterable collection); - - Future sendResource(Resource resource); - - Future errorNotFound(Iterable errors); -} - -abstract class FetchRelationshipRequest { - RelationshipRoute get route; - - Future sendToMany(Iterable collection); - - Future sendToOne(Identifier id); - - Future errorNotFound(Iterable errors); -} - -abstract class ReplaceRelationshipRequest { - RelationshipRoute get route; - - Future getRelationship(); - - Future sendNoContent(); - - Future sendToMany(Iterable collection); - - Future sendToOne(Identifier id); - - Future errorNotFound(Iterable errors); -} - -abstract class AddToRelationshipRequest { - RelationshipRoute get route; - - Future> getIdentifiers(); - - Future sendToMany(Iterable collection); - - Future errorNotFound(Iterable errors); -} - -abstract class FetchResourceRequest { - ResourceRoute get route; - - Future sendResource(Resource resource, {Iterable included}); - - Future errorNotFound(Iterable errors); -} - -abstract class DeleteResourceRequest { - ResourceRoute get route; - - Future sendNoContent(); - - Future sendMeta(Map meta); - - Future errorNotFound(Iterable errors); -} - -abstract class CreateResourceRequest { - CollectionRoute get route; - - Future getResource(); - - Future sendCreated(Resource resource); - - Future sendNoContent(); - - Future errorConflict(Iterable errors); - - Future errorNotFound(Iterable errors); -} - -abstract class UpdateResourceRequest { - ResourceRoute get route; - - Future getResource(); - - Future sendUpdated(Resource resource); - - Future sendNoContent(); - - Future errorConflict(Iterable errors); - - Future errorForbidden(Iterable errors); - - Future errorNotFound(Iterable errors); -} diff --git a/lib/src/server/numbered_page.dart b/lib/src/server/numbered_page.dart index 2e494bf..4090c5e 100644 --- a/lib/src/server/numbered_page.dart +++ b/lib/src/server/numbered_page.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:json_api/src/server/page.dart'; +import 'package:json_api/src/server/contracts/page.dart'; class NumberedPage extends Page { final int number; diff --git a/lib/src/server/requests.dart b/lib/src/server/requests.dart deleted file mode 100644 index c53aea8..0000000 --- a/lib/src/server/requests.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:json_api/document.dart'; -import 'package:json_api/src/server/controller.dart'; -import 'package:json_api/src/server/page.dart'; -import 'package:json_api/src/server/route.dart'; -import 'package:json_api/src/server/server.dart'; - -abstract class _BaseRequest implements JsonApiRequest { - final HttpRequest request; - JsonApiServer server; - - _BaseRequest(this.request); - - Map get queryParameters => - request.requestedUri.queryParameters; - - HttpResponse get _response => request.response; - - Future get _body async => - json.decode(await request.transform(utf8.decoder).join()); - - Future call(JsonApiController controller); - - Future errorNotFound([Iterable errors = const []]) => - server.error(_response, 404, errors); - - bind(JsonApiServer s) => server = s; -} - -class FetchCollection extends _BaseRequest implements FetchCollectionRequest { - final CollectionRoute route; - - FetchCollection(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.fetchCollection(this); - - Future sendCollection(Iterable resources, {Page page}) => - server.collection(_response, route, resources, page: page); -} - -class FetchRelated extends _BaseRequest implements FetchRelatedRequest { - final RelatedRoute route; - - FetchRelated(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.fetchRelated(this); - - Future sendCollection(Iterable collection) => - server.relatedCollection(_response, route, collection); - - Future sendResource(Resource resource) => - server.relatedResource(_response, route, resource); -} - -class FetchRelationship extends _BaseRequest - implements FetchRelationshipRequest { - final RelationshipRoute route; - - FetchRelationship(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => - controller.fetchRelationship(this); - - Future sendToMany(Iterable collection) => - server.toMany(_response, route, collection); - - Future sendToOne(Identifier id) => server.toOne(_response, route, id); -} - -class ReplaceRelationship extends _BaseRequest - implements ReplaceRelationshipRequest { - final RelationshipRoute route; - - ReplaceRelationship(HttpRequest request, this.route) : super(request); - - Future getRelationship() async => - Relationship.parse(await _body); - - Future call(JsonApiController controller) => - controller.replaceRelationship(this); - - Future sendNoContent() => server.write(_response, 204); - - Future sendToMany(Iterable collection) => - server.toMany(_response, route, collection); - - Future sendToOne(Identifier id) => server.toOne(_response, route, id); -} - -class AddToRelationship extends _BaseRequest - implements AddToRelationshipRequest { - final RelationshipRoute route; - - AddToRelationship(HttpRequest request, this.route) : super(request); - - Future> getIdentifiers() async => - ToMany.parse(await _body).identifiers; - - Future call(JsonApiController controller) => - controller.addToRelationship(this); - - Future sendToMany(Iterable collection) => - server.toMany(_response, route, collection); -} - -class FetchResource extends _BaseRequest implements FetchResourceRequest { - final ResourceRoute route; - - FetchResource(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.fetchResource(this); - - Future sendResource(Resource resource, {Iterable included}) => - server.resource(_response, route, resource, included: included); -} - -class DeleteResource extends _BaseRequest implements DeleteResourceRequest { - final ResourceRoute route; - - DeleteResource(HttpRequest request, this.route) : super(request); - - Future call(JsonApiController controller) => controller.deleteResource(this); - - Future sendNoContent() => server.write(_response, 204); - - Future sendMeta(Map meta) => - server.meta(_response, route, meta); -} - -class CreateResource extends _BaseRequest implements CreateResourceRequest { - final CollectionRoute route; - - CreateResource(HttpRequest request, this.route) : super(request); - - Future getResource() async { - return ResourceData.parse(await _body).resourceJson.toResource(); - } - - Future call(JsonApiController controller) => controller.createResource(this); - - Future sendCreated(Resource resource) => - server.created(_response, route, resource); - - Future errorConflict(Iterable errors) => - server.error(_response, 409, errors); - - Future sendNoContent() => server.write(_response, 204); -} - -class UpdateResource extends _BaseRequest implements UpdateResourceRequest { - final ResourceRoute route; - - UpdateResource(HttpRequest request, this.route) : super(request); - - Future getResource() async { - return ResourceData.parse(await _body).resourceJson.toResource(); - } - - Future call(JsonApiController controller) => controller.updateResource(this); - - Future sendUpdated(Resource resource) => - server.resource(_response, route, resource); - - Future errorConflict(Iterable errors) => - server.error(_response, 409, errors); - - Future errorForbidden(Iterable errors) => - server.error(_response, 403, errors); - - Future sendNoContent() => server.write(_response, 204); -} diff --git a/lib/src/server/route.dart b/lib/src/server/route.dart deleted file mode 100644 index aee07a6..0000000 --- a/lib/src/server/route.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:json_api/src/server/controller.dart'; -import 'package:json_api/src/server/requests.dart'; -import 'package:json_api/src/server/server.dart'; -import 'package:json_api/src/server/uri_builder.dart'; - -abstract class JsonApiRequest { - Future call(JsonApiController controller); - - bind(JsonApiServer server); -} - -abstract class JsonApiRoute { - final Uri uri; - - JsonApiRoute(this.uri); - - /// Returns the `self` link uri - Uri self(UriBuilder schema, {Map parameters = const {}}); - - JsonApiRequest createRequest(HttpRequest httpRequest); - - /// URI parameters - Map get parameters => uri.queryParameters; - - factory JsonApiRoute.collection(Uri uri, String type) => - CollectionRoute(uri, type); - - factory JsonApiRoute.resource(Uri uri, String type, String id) => - ResourceRoute(uri, type, id); - - factory JsonApiRoute.relationship( - Uri uri, String type, String id, String relationship) => - RelationshipRoute(uri, type, id, relationship); - - factory JsonApiRoute.related( - Uri uri, String type, String id, String relationship) => - RelatedRoute(uri, type, id, relationship); -} - -class CollectionRoute extends JsonApiRoute { - final String type; - - CollectionRoute(Uri uri, this.type) : super(uri); - - JsonApiRequest createRequest(HttpRequest request) { - switch (request.method) { - case 'GET': - return FetchCollection(request, this); - case 'POST': - return CreateResource(request, this); - } - throw 'Unexpected method ${request.method}'; - } - - @override - Uri self(UriBuilder schema, {Map parameters = const {}}) => - schema.collection(type, params: parameters); -} - -class RelatedRoute extends JsonApiRoute { - final String type; - final String id; - final String relationship; - - RelatedRoute(Uri uri, this.type, this.id, this.relationship) : super(uri); - - JsonApiRequest createRequest(HttpRequest request) { - switch (request.method) { - case 'GET': - return FetchRelated(request, this); - } - throw 'Unexpected method ${request.method}'; - } - - @override - Uri self(UriBuilder schema, {Map parameters = const {}}) => - schema.related(type, id, relationship, params: parameters); -} - -class RelationshipRoute extends JsonApiRoute { - final String type; - final String id; - final String relationship; - - RelationshipRoute(Uri uri, this.type, this.id, this.relationship) - : super(uri); - - JsonApiRequest createRequest(HttpRequest request) { - switch (request.method) { - case 'GET': - return FetchRelationship(request, this); - case 'PATCH': - return ReplaceRelationship(request, this); - case 'POST': - return AddToRelationship(request, this); - } - throw 'Unexpected method ${request.method}'; - } - - @override - Uri self(UriBuilder builder, {Map parameters = const {}}) => - builder.relationship(type, id, relationship, params: parameters); - - Uri related(UriBuilder builder, {Map params = const {}}) => - builder.related(type, id, relationship, params: params); -} - -class ResourceRoute extends JsonApiRoute { - final String type; - final String id; - - ResourceRoute(Uri uri, this.type, this.id) : super(uri); - - JsonApiRequest createRequest(HttpRequest request) { - switch (request.method) { - case 'GET': - return FetchResource(request, this); - case 'DELETE': - return DeleteResource(request, this); - case 'PATCH': - return UpdateResource(request, this); - } - throw 'Unexpected method ${request.method}'; - } - - @override - Uri self(UriBuilder schema, {Map parameters = const {}}) => - schema.resource(type, id, params: parameters); -} diff --git a/lib/src/server/route_resolver.dart b/lib/src/server/route_resolver.dart deleted file mode 100644 index 405b907..0000000 --- a/lib/src/server/route_resolver.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:json_api/src/server/route.dart'; - -abstract class RouteResolver { - /// Resolves HTTP request to [JsonAiRequest] object - JsonApiRoute getRoute(Uri uri); -} diff --git a/lib/src/server/routing.dart b/lib/src/server/routing.dart deleted file mode 100644 index 69d52b5..0000000 --- a/lib/src/server/routing.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:json_api/src/server/route_resolver.dart'; -import 'package:json_api/src/server/standard_routing.dart'; -import 'package:json_api/src/server/uri_builder.dart'; - -/// Routing defines the design of URLs. -abstract class Routing implements UriBuilder, RouteResolver { - factory Routing(Uri base) { - return StandardRouting(base); - } -} diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index a7b22e9..8673273 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -3,90 +3,35 @@ import 'dart:convert'; import 'dart:io'; import 'package:json_api/document.dart'; -import 'package:json_api/src/document/pagination.dart'; -import 'package:json_api/src/nullable.dart'; -import 'package:json_api/src/server/page.dart'; -import 'package:json_api/src/server/route.dart'; -import 'package:json_api/src/server/uri_builder.dart'; +import 'package:json_api/src/server/contracts/controller.dart'; +import 'package:json_api/src/server/contracts/page.dart'; +import 'package:json_api/src/server/contracts/router.dart'; +import 'package:json_api/src/server/contracts/document_builder.dart'; +import 'package:json_api/src/server/standard_document_builder.dart'; + +part 'server_requests.dart'; + +part 'server_routes.dart'; class JsonApiServer { - final UriBuilder url; - final String allowOrigin; + final Router router; + final JsonApiController controller; - JsonApiServer(this.url, {this.allowOrigin = '*'}); + JsonApiServer(this.router, this.controller); - Future write(HttpResponse response, int status, - {Document document, Map headers = const {}}) { - response.statusCode = status; - headers.forEach(response.headers.add); - if (allowOrigin != null) { - response.headers.set('Access-Control-Allow-Origin', allowOrigin); - } - if (document != null) { - response.write(json.encode(document)); + Future process(HttpRequest httpRequest) async { + const factory = _JsonApiRouteFactory(); + final route = await router.getRoute(httpRequest.requestedUri, factory); + if (route == null) { + httpRequest.response.statusCode = 404; + return httpRequest.response.close(); } - return response.close(); + final request = route.createRequest(httpRequest); + final body = await httpRequest.transform(utf8.decoder).join(); + request.uri = httpRequest.requestedUri; + if (body.isNotEmpty) request.setBody(json.decode(body)); + request.docBuilder = StandardDocumentBuilder(router); + request.response = httpRequest.response; + return request.call(controller); } - - Future collection(HttpResponse response, CollectionRoute route, - Iterable resource, - {Page page}) => - write(response, 200, - document: Document( - ResourceCollectionData(resource.map(ResourceJson.fromResource), - self: Link(route.self(url, parameters: route.parameters)), - pagination: page == null - ? Pagination.empty() - : Pagination.fromLinks(page.map((_) => - Link(route.self(url, parameters: _.parameters))))), - )); - - Future error(HttpResponse response, int status, Iterable errors) => - write(response, status, document: Document.error(errors)); - - Future relatedCollection(HttpResponse response, RelatedRoute route, - Iterable collection) => - write(response, 200, - document: Document(ResourceCollectionData( - collection.map(ResourceJson.fromResource), - self: Link(route.self(url))))); - - Future relatedResource( - HttpResponse response, RelatedRoute route, Resource resource) => - write(response, 200, - document: Document(ResourceData(ResourceJson.fromResource(resource), - self: Link(route.self(url))))); - - Future resource(HttpResponse response, ResourceRoute route, Resource resource, - {Iterable included}) => - write(response, 200, - document: Document( - ResourceData(ResourceJson.fromResource(resource), - self: Link(route.self(url))), - included: included?.map(ResourceJson.fromResource))); - - Future toMany(HttpResponse response, RelationshipRoute route, - Iterable collection) => - write(response, 200, - document: Document(ToMany( - collection.map(IdentifierJson.fromIdentifier), - self: Link(route.self(url)), - related: Link(route.related(url))))); - - Future toOne(HttpResponse response, RelationshipRoute route, Identifier id) => - write(response, 200, - document: Document(ToOne(nullable(IdentifierJson.fromIdentifier)(id), - self: Link(route.self(url)), related: Link(route.related(url))))); - - Future meta(HttpResponse response, ResourceRoute route, - Map meta) => - write(response, 200, document: Document.empty(meta)); - - Future created( - HttpResponse response, CollectionRoute route, Resource resource) => - write(response, 201, - document: Document(ResourceData(ResourceJson.fromResource(resource))), - headers: { - 'Location': url.resource(resource.type, resource.id).toString() - }); } diff --git a/lib/src/server/server_requests.dart b/lib/src/server/server_requests.dart new file mode 100644 index 0000000..d6ae704 --- /dev/null +++ b/lib/src/server/server_requests.dart @@ -0,0 +1,219 @@ +part of 'server.dart'; + +abstract class _BaseRequest { + HttpResponse response; + bool allowOrigin; + Uri uri; + DocumentBuilder docBuilder; + + void setBody(Object body) {} + + Future _error(int status, Iterable errors) => + _write(status, document: docBuilder.error(errors)); + + Future call(JsonApiController controller); + + Future sendNoContent() => _write(204); + + Future errorNotFound([Iterable errors]) => _error(404, errors); + + Future errorConflict(Iterable errors) => _error(409, errors); + + Future errorForbidden(Iterable errors) => _error(403, errors); + + Future _write(int status, + {Document document, Map headers = const {}}) { + response.statusCode = status; + headers.forEach(response.headers.add); + if (allowOrigin != null) { + response.headers.set('Access-Control-Allow-Origin', allowOrigin); + } + if (document != null) { + response.write(json.encode(document)); + } + return response.close(); + } + + Future _collection(_CollectionRoute route, Iterable resource, + {Page page}) => + _write(200, + document: + docBuilder.collection(resource, route.type, uri, page: page)); + + Future _relatedCollection(_RelatedRoute route, Iterable collection, + {Page page}) => + _write(200, + document: docBuilder.relatedCollection( + collection, route.type, route.id, route.relationship, uri, + page: page)); + + Future _relatedResource(_RelatedRoute route, Resource resource) => _write(200, + document: docBuilder.relatedResource( + resource, route.type, route.id, route.relationship, uri)); + + Future _resource(_ResourceRoute route, Resource resource, + {Iterable included}) => + _write(200, + document: docBuilder.resource(resource, route.type, route.id, uri, + included: included)); + + Future _toMany(_RelationshipRoute route, Iterable collection) => + _write(200, + document: docBuilder.toMany( + collection, route.type, route.id, route.relationship, uri)); + + Future _toOne(_RelationshipRoute route, Identifier identifier) => _write(200, + document: docBuilder.toOne( + identifier, route.type, route.id, route.relationship, uri)); + + Future _meta(_ResourceRoute route, Map meta) => + _write(200, document: Document.empty(meta)); + + Future _created(_CollectionRoute route, Resource resource) { + final doc = docBuilder.resource(resource, route.type, resource.id, uri); + return _write(201, + document: doc, + headers: {'Location': doc.data.resourceJson.self.toString()}); + } +} + +abstract class _CollectionRequest extends _BaseRequest { + _CollectionRoute route; + + String get type => route.type; +} + +class _FetchCollection extends _CollectionRequest + implements FetchCollectionRequest { + Future call(JsonApiController controller) => controller.fetchCollection(this); + + Future sendCollection(Iterable resources, {Page page}) => + _collection(route, resources, page: page); +} + +class _CreateResource extends _CollectionRequest + implements CreateResourceRequest { + Resource resource; + + void setBody(Object body) { + resource = ResourceData.parse(body).toResource(); + } + + Future call(JsonApiController controller) => controller.createResource(this); + + Future sendCreated(Resource resource) => _created(route, resource); +} + +class _FetchRelated extends _BaseRequest implements FetchRelatedRequest { + _RelatedRoute route; + + String get type => route.type; + + String get id => route.id; + + String get relationship => route.relationship; + + Future call(JsonApiController controller) => controller.fetchRelated(this); + + Future sendCollection(Iterable collection) => + _relatedCollection(route, collection); + + Future sendResource(Resource resource) => _relatedResource(route, resource); +} + +abstract class _RelationshipRequest extends _BaseRequest { + _RelationshipRoute route; + + String get type => route.type; + + String get id => route.id; + + String get relationship => route.relationship; +} + +class _FetchRelationship extends _RelationshipRequest + implements FetchRelationshipRequest { + Future call(JsonApiController controller) => + controller.fetchRelationship(this); + + Future sendToMany(Iterable collection) => + _toMany(route, collection); + + Future sendToOne(Identifier id) => _toOne(route, id); +} + +class _ReplaceRelationship extends _RelationshipRequest + implements ReplaceToOneRequest, ReplaceToManyRequest { + Identifier identifier; + Iterable identifiers; + + @override + void setBody(Object body) { + final r = Relationship.parse(body); + if (r is ToOne) identifier = r.toIdentifier(); + if (r is ToMany) identifiers = r.toIdentifiers(); + } + + Future call(JsonApiController controller) { + if (identifiers != null) return controller.replaceToMany(this); + return controller.replaceToOne(this); + } + + Future sendToMany(Iterable collection) => + _toMany(route, collection); + + Future sendToOne(Identifier id) => _toOne(route, id); +} + +class _AddToMany extends _RelationshipRequest implements AddToManyRequest { + Identifier identifier; + Iterable identifiers; + + @override + void setBody(Object body) { + final r = Relationship.parse(body); + if (r is ToOne) identifier = r.toIdentifier(); + if (r is ToMany) identifiers = r.toIdentifiers(); + } + + Future call(JsonApiController controller) => controller.addToMany(this); + + Future sendToMany(Iterable collection) => + _toMany(route, collection); +} + +abstract class _ResourceRequest extends _BaseRequest { + _ResourceRoute route; + + String get type => route.type; + + String get id => route.id; +} + +class _FetchResource extends _ResourceRequest implements FetchResourceRequest { + Future call(JsonApiController controller) => controller.fetchResource(this); + + Future sendResource(Resource resource, {Iterable included}) => + _resource(route, resource, included: included); +} + +class _DeleteResource extends _ResourceRequest + implements DeleteResourceRequest { + Future call(JsonApiController controller) => controller.deleteResource(this); + + Future sendMeta(Map meta) => _meta(route, meta); +} + +class _UpdateResource extends _ResourceRequest + implements UpdateResourceRequest { + Resource resource; + + @override + void setBody(Object body) { + resource = ResourceData.parse(body).resourceJson.toResource(); + } + + Future call(JsonApiController controller) => controller.updateResource(this); + + Future sendUpdated(Resource resource) => _resource(route, resource); +} diff --git a/lib/src/server/server_routes.dart b/lib/src/server/server_routes.dart new file mode 100644 index 0000000..3db24d0 --- /dev/null +++ b/lib/src/server/server_routes.dart @@ -0,0 +1,111 @@ +part of 'server.dart'; + +class _JsonApiRouteFactory implements RouteFactory<_BaseRoute> { + const _JsonApiRouteFactory(); + + _BaseRoute collection(String type) => _CollectionRoute(type); + + _BaseRoute related(String type, String id, String relationship) => + _RelatedRoute(type, id, relationship); + + _BaseRoute relationship(String type, String id, String relationship) => + _RelationshipRoute(type, id, relationship); + + _BaseRoute resource(String type, String id) => _ResourceRoute(type, id); + + _BaseRoute unmatched() => null; +} + +abstract class _BaseRoute { + Uri self(UriBuilder builder, {Map parameters = const {}}); + + _BaseRequest createRequest(HttpRequest httpRequest); +} + +class _CollectionRoute extends _BaseRoute { + final String type; + + _CollectionRoute(this.type); + + _BaseRequest createRequest(HttpRequest request) { + switch (request.method) { + case 'GET': + return _FetchCollection()..route = this; + case 'POST': + return _CreateResource()..route = this; + } + throw 'Unexpected method ${request.method}'; + } + + @override + Uri self(UriBuilder builder, {Map parameters = const {}}) => + builder.collection(type, parameters: parameters); +} + +class _RelatedRoute extends _BaseRoute { + final String type; + final String id; + final String relationship; + + _RelatedRoute(this.type, this.id, this.relationship); + + _BaseRequest createRequest(HttpRequest request) { + switch (request.method) { + case 'GET': + return _FetchRelated()..route = this; + } + throw 'Unexpected method ${request.method}'; + } + + @override + Uri self(UriBuilder builder, {Map parameters = const {}}) => + builder.related(type, id, relationship, parameters: parameters); +} + +class _RelationshipRoute extends _BaseRoute { + final String type; + final String id; + final String relationship; + + _RelationshipRoute(this.type, this.id, this.relationship); + + _BaseRequest createRequest(HttpRequest request) { + switch (request.method) { + case 'GET': + return _FetchRelationship()..route = this; + case 'PATCH': + return _ReplaceRelationship()..route = this; + case 'POST': + return _AddToMany()..route = this; + } + throw 'Unexpected method ${request.method}'; + } + + Uri self(UriBuilder builder, {Map parameters = const {}}) => + builder.relationship(type, id, relationship, parameters: parameters); + + Uri related(UriBuilder builder, {Map params = const {}}) => + builder.related(type, id, relationship, parameters: params); +} + +class _ResourceRoute extends _BaseRoute { + final String type; + final String id; + + _ResourceRoute(this.type, this.id); + + _BaseRequest createRequest(HttpRequest request) { + switch (request.method) { + case 'GET': + return _FetchResource()..route = this; + case 'DELETE': + return _DeleteResource()..route = this; + case 'PATCH': + return _UpdateResource()..route = this; + } + throw 'Unexpected method ${request.method}'; + } + + Uri self(UriBuilder builder, {Map parameters = const {}}) => + builder.resource(type, id, parameters: parameters); +} diff --git a/lib/src/server/standard_document_builder.dart b/lib/src/server/standard_document_builder.dart new file mode 100644 index 0000000..9dd7ea3 --- /dev/null +++ b/lib/src/server/standard_document_builder.dart @@ -0,0 +1,100 @@ +import 'package:json_api/document.dart'; +import 'package:json_api/src/document/pagination.dart'; +import 'package:json_api/src/nullable.dart'; +import 'package:json_api/src/server/contracts/document_builder.dart'; +import 'package:json_api/src/server/contracts/page.dart'; +import 'package:json_api/src/server/contracts/router.dart'; + +class StandardDocumentBuilder implements DocumentBuilder { + final UriBuilder uriBuilder; + + StandardDocumentBuilder(this.uriBuilder); + + Document error(Iterable errors) => Document.error(errors); + + Document collection(Iterable resource, + String type, Uri self, + {Page page, Iterable included}) => + Document(ResourceCollectionData(resource.map( + _resourceJson), + self: Link(self), + pagination: page == null + ? Pagination.empty() + : Pagination.fromLinks(page.map((_) => + Link( + uriBuilder.collection(type, parameters: _.parameters)))))); + + Document relatedCollection( + Iterable resource, String type, + String id, String relationship, Uri self, + {Page page, Iterable included}) => + Document(ResourceCollectionData(resource.map(_resourceJson), + self: Link(self), + pagination: page == null + ? Pagination.empty() + : Pagination.fromLinks(page.map((_) => + Link( + uriBuilder.related( + type, id, relationship, parameters: _.parameters)))))); + + Document resource(Resource resource, String type, String id, + Uri self, + {Iterable included}) => + Document( + ResourceData(_resourceJson(resource), + self: Link(uriBuilder.resource(type, id))), + included: included?.map(_resourceJson)); + + Document relatedResource(Resource resource, String type, + String id, + String relationship, Uri self, + {Iterable included}) => + Document( + ResourceData(_resourceJson(resource), + self: Link(uriBuilder.related(type, id, relationship))), + included: included?.map(_resourceJson)); + + Document toMany(Iterable collection, String type, + String id, + String relationship, Uri self) => + Document(ToMany( + collection.map(_identifierJson), + self: Link(uriBuilder.relationship(type, id, relationship)), + related: Link(uriBuilder.related(type, id, relationship)))); + + Document toOne(Identifier identifier, String type, String id, + String relationship, Uri self) => + Document(ToOne( + nullable(_identifierJson)(identifier), + self: Link(uriBuilder.relationship(type, id, relationship)), + related: Link(uriBuilder.related(type, id, relationship)))); + + Document meta(Map meta) => Document.empty(meta); + + IdentifierJson _identifierJson(Identifier id) => + IdentifierJson(id.type, id.id); + + ResourceJson _resourceJson(Resource resource) { + final relationships = + {}; + relationships.addAll(resource.toOne.map((k, v) => + MapEntry( + k, ToOne(nullable(_identifierJson)(v), self: Link( + uriBuilder.relationship(resource.type, resource.id, k)), + related: Link( + uriBuilder.related(resource.type, resource.id, k)))))); + relationships.addAll( + resource.toMany.map( + (k, v) => + MapEntry( + k, ToMany(v.map(_identifierJson), self: Link( + uriBuilder.relationship(resource.type, resource.id, k)), + related: Link( + uriBuilder.related(resource.type, resource.id, k)))))); + + return ResourceJson(resource.type, resource.id, + attributes: resource.attributes, + relationships: relationships, + self: Link(uriBuilder.resource(resource.type, resource.id))); + } +} diff --git a/lib/src/server/standard_router.dart b/lib/src/server/standard_router.dart new file mode 100644 index 0000000..6779950 --- /dev/null +++ b/lib/src/server/standard_router.dart @@ -0,0 +1,56 @@ +import 'package:json_api/src/server/contracts/router.dart'; + +/// StandardRouting implements the recommended URL design schema: +/// +/// /photos - for a collection +/// /photos/1 - for a resource +/// /photos/1/relationships/author - for a relationship +/// /photos/1/author - for a related resource +/// +/// See https://jsonapi.org/recommendations/#urls +class StandardRouter implements Router { + final Uri base; + + StandardRouter(this.base) { + ArgumentError.checkNotNull(base, 'base'); + } + + Uri collection(String type, {Map parameters = const {}}) { + final combined = {} + ..addAll(base.queryParameters) + ..addAll(parameters); + return base.replace( + pathSegments: base.pathSegments + [type], + queryParameters: combined.isNotEmpty ? combined : null); + } + + Uri related(String type, String id, String relationship, + {Map parameters = const {}}) => + base.replace(pathSegments: base.pathSegments + [type, id, relationship]); + + Uri relationship(String type, String id, String relationship, + {Map parameters = const {}}) => + base.replace( + pathSegments: + base.pathSegments + [type, id, 'relationships', relationship]); + + Uri resource(String type, String id, {Map parameters = const {}}) => + base.replace(pathSegments: base.pathSegments + [type, id]); + + R getRoute(Uri uri, RouteFactory route) { + final segments = uri.pathSegments; + switch (segments.length) { + case 1: + return route.collection(segments[0]); + case 2: + return route.resource(segments[0], segments[1]); + case 3: + return route.related(segments[0], segments[1], segments[2]); + case 4: + if (segments[2] == 'relationships') { + return route.relationship(segments[0], segments[1], segments[3]); + } + } + return route.unmatched(); + } +} diff --git a/lib/src/server/standard_routing.dart b/lib/src/server/standard_routing.dart deleted file mode 100644 index 26125c3..0000000 --- a/lib/src/server/standard_routing.dart +++ /dev/null @@ -1,58 +0,0 @@ - -import 'package:json_api/src/server/route.dart'; -import 'package:json_api/src/server/routing.dart'; - -/// StandardRouting implements the recommended URL design schema: -/// -/// /photos - for a collection -/// /photos/1 - for a resource -/// /photos/1/relationships/author - for a relationship -/// /photos/1/author - for a related resource -/// -/// See https://jsonapi.org/recommendations/#urls -class StandardRouting implements Routing { - final Uri base; - - StandardRouting(this.base) { - ArgumentError.checkNotNull(base, 'base'); - } - - collection(String type, {Map params = const {}}) { - final combined = {} - ..addAll(base.queryParameters) - ..addAll(params); - return base.replace( - pathSegments: base.pathSegments + [type], - queryParameters: combined.isNotEmpty ? combined : null); - } - - related(String type, String id, String relationship, - {Map params = const {}}) => - base.replace(pathSegments: base.pathSegments + [type, id, relationship]); - - relationship(String type, String id, String relationship, - {Map params = const {}}) => - base.replace( - pathSegments: - base.pathSegments + [type, id, 'relationships', relationship]); - - resource(String type, String id, {Map params = const {}}) => - base.replace(pathSegments: base.pathSegments + [type, id]); - - JsonApiRoute getRoute(Uri uri) { - final segments = uri.pathSegments; - switch (segments.length) { - case 1: - return JsonApiRoute.collection(uri, segments[0]); - case 2: - return JsonApiRoute.resource(uri, segments[0], segments[1]); - case 3: - return JsonApiRoute.related(uri, segments[0], segments[1], segments[2]); - case 4: - if (segments[2] == 'relationships') { - return JsonApiRoute.relationship(uri, segments[0], segments[1], segments[3]); - } - } - return null; // TODO: replace with a null-object - } -} diff --git a/lib/src/server/uri_builder.dart b/lib/src/server/uri_builder.dart deleted file mode 100644 index e3a9c05..0000000 --- a/lib/src/server/uri_builder.dart +++ /dev/null @@ -1,15 +0,0 @@ -abstract class UriBuilder { - /// Builds a URI for a resource collection - Uri collection(String type, {Map params}); - - /// Builds a URI for a single resource - Uri resource(String type, String id, {Map params}); - - /// Builds a URI for a related resource - Uri related(String type, String id, String relationship, - {Map params}); - - /// Builds a URI for a relationship object - Uri relationship(String type, String id, String relationship, - {Map params}); -} diff --git a/test/functional/fetch_test.dart b/test/functional/fetch_test.dart index 1846149..aaa64a0 100644 --- a/test/functional/fetch_test.dart +++ b/test/functional/fetch_test.dart @@ -23,6 +23,12 @@ void main() async { expect(r.status, 200); expect(r.isSuccessful, true); expect(r.data.collection.first.attributes['name'], 'Tesla'); + expect(r.data.collection.first.self.uri.toString(), + 'http://localhost:8080/companies/1'); + expect(r.data.collection.first.relationships['hq'].related.uri.toString(), + 'http://localhost:8080/companies/1/hq'); + expect(r.data.collection.first.relationships['hq'].self.uri.toString(), + 'http://localhost:8080/companies/1/relationships/hq'); expect(r.data.self.uri, uri); }); @@ -93,7 +99,6 @@ void main() async { expect(r.document.included.first.attributes['name'], 'Palo Alto'); expect(r.document.included.last.type, 'models'); expect(r.document.included.last.attributes['name'], 'Model 3'); - }); test('404 on type', () async { @@ -179,7 +184,7 @@ void main() async { final r = await client.fetchToMany(uri); expect(r.status, 200); expect(r.isSuccessful, true); - expect(r.data.identifiers.first.type, 'models'); + expect(r.data.toIdentifiers().first.type, 'models'); expect(r.data.self.uri, uri); expect(r.data.related.uri.toString(), 'http://localhost:8080/companies/1/models'); @@ -190,7 +195,7 @@ void main() async { final r = await client.fetchToMany(uri); expect(r.status, 200); expect(r.isSuccessful, true); - expect(r.data.identifiers, isEmpty); + expect(r.data.toIdentifiers(), isEmpty); expect(r.data.self.uri, uri); expect(r.data.related.uri.toString(), 'http://localhost:8080/companies/3/models'); @@ -202,7 +207,7 @@ void main() async { expect(r.status, 200); expect(r.isSuccessful, true); expect(r.data, TypeMatcher()); - expect((r.data as ToMany).identifiers.first.type, 'models'); + expect((r.data as ToMany).toIdentifiers().first.type, 'models'); expect(r.data.self.uri, uri); expect(r.data.related.uri.toString(), 'http://localhost:8080/companies/1/models'); diff --git a/test/functional/update_test.dart b/test/functional/update_test.dart index c9e2390..55bf9cf 100644 --- a/test/functional/update_test.dart +++ b/test/functional/update_test.dart @@ -211,7 +211,7 @@ void main() async { test('204 No Content', () async { final url = Url.relationship('companies', '1', 'models'); final r0 = await client.fetchToMany(url); - final original = r0.data.identifiers.map((_) => _.id); + final original = r0.data.toIdentifiers().map((_) => _.id); expect(original, ['1', '2', '3', '4']); final r1 = await client.replaceToMany( @@ -219,7 +219,7 @@ void main() async { expect(r1.status, 204); final r2 = await client.fetchToMany(url); - final updated = r2.data.identifiers.map((_) => _.id); + final updated = r2.data.toIdentifiers().map((_) => _.id); expect(updated, ['5', '6']); }); }); @@ -243,14 +243,14 @@ void main() async { test('200 OK', () async { final url = Url.relationship('companies', '1', 'models'); final r0 = await client.fetchToMany(url); - final original = r0.data.identifiers.map((_) => _.id); + final original = r0.data.toIdentifiers().map((_) => _.id); expect(original, ['1', '2', '3', '4']); final r1 = await client.addToMany( url, [Identifier('models', '1'), Identifier('models', '5')]); expect(r1.status, 200); - final updated = r1.data.identifiers.map((_) => _.id); + final updated = r1.data.toIdentifiers().map((_) => _.id); expect(updated, ['1', '2', '3', '4', '5']); }); }); From c29e302b3d2ce9de95f2e49600b04d3b5a5424b6 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sun, 17 Mar 2019 19:42:19 -0700 Subject: [PATCH 5/9] WIP --- CHANGELOG.md | 7 +- lib/src/client/client.dart | 29 +-- lib/src/document/document.dart | 40 +--- lib/src/document/error.dart | 25 --- lib/src/document/identifier_json.dart | 7 - lib/src/document/link.dart | 21 -- lib/src/document/relationship.dart | 59 ------ lib/src/document/resource_json.dart | 66 +++--- lib/src/parser.dart | 191 ++++++++++++++++++ lib/src/server/server.dart | 4 +- lib/src/server/server_requests.dart | 10 +- lib/src/server/standard_document_builder.dart | 14 +- lib/src/server/standard_router.dart | 3 +- test/functional/fetch_test.dart | 10 +- test/unit/document_test.dart | 20 -- test/unit/example.json | 141 +++++++------ test/unit/parser_test.dart | 27 +++ 17 files changed, 368 insertions(+), 306 deletions(-) create mode 100644 lib/src/parser.dart create mode 100644 test/unit/parser_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 00cdedd..90c817f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- Some BC-breaking changes in the Document +- Parsing logic moved out +- Some other BC-breaking changes in the Document +- Huge changes in the Server ### Added - Compound documents support in Client (Server-side support is still very limited) +### Fixed +- Server was not setting links for resources and relationships + ## [0.3.0] - 2019-03-16 ### Changed - Huge BC-breaking refactoring in the Document model which propagated everywhere diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index b6a0c67..f3c06f1 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import 'package:json_api/document.dart'; import 'package:json_api/src/client/response.dart'; import 'package:json_api/src/nullable.dart'; +import 'package:json_api/src/parser.dart'; typedef Document ResponseParser(Object j); @@ -14,6 +15,8 @@ typedef http.Client HttpClientFactory(); class JsonApiClient { static const contentType = 'application/vnd.api+json'; + JsonApiDocumentParser _parser = const JsonApiDocumentParser(); + final HttpClientFactory _factory; /// JSON:API client uses Dart's native Http Client internally. @@ -25,31 +28,31 @@ class JsonApiClient { /// Use [headers] to pass extra HTTP headers. Future> fetchCollection(Uri uri, {Map headers}) => - _get(ResourceCollectionData.parse, uri, headers); + _get(_parser.parseResourceCollectionData, uri, headers); /// Fetches a single resource /// Use [headers] to pass extra HTTP headers. Future> fetchResource(Uri uri, {Map headers}) => - _get(ResourceData.parse, uri, headers); + _get(_parser.parseResourceData, uri, headers); /// Fetches a to-one relationship /// Use [headers] to pass extra HTTP headers. Future> fetchToOne(Uri uri, {Map headers}) => - _get(ToOne.parse, uri, headers); + _get(_parser.parseToOne, uri, headers); /// Fetches a to-many relationship /// Use [headers] to pass extra HTTP headers. Future> fetchToMany(Uri uri, {Map headers}) => - _get(ToMany.parse, uri, headers); + _get(_parser.parseToMany, uri, headers); /// Fetches a to-one or to-many relationship. /// The actual type of the relationship can be determined afterwards. /// Use [headers] to pass extra HTTP headers. Future> fetchRelationship(Uri uri, {Map headers}) => - _get(Relationship.parse, uri, headers); + _get(_parser.parseRelationship, uri, headers); /// Creates a new resource. The resource will be added to a collection /// according to its type. @@ -57,7 +60,7 @@ class JsonApiClient { /// https://jsonapi.org/format/#crud-creating Future> createResource(Uri uri, Resource resource, {Map headers}) => - _post(ResourceData.parse, uri, + _post(_parser.parseResourceData, uri, ResourceData(ResourceJson.fromResource(resource)), headers); /// Deletes the resource. @@ -71,7 +74,7 @@ class JsonApiClient { /// https://jsonapi.org/format/#crud-updating Future> updateResource(Uri uri, Resource resource, {Map headers}) => - _patch(ResourceData.parse, uri, + _patch(_parser.parseResourceData, uri, ResourceData(ResourceJson.fromResource(resource)), headers); /// Updates a to-one relationship via PATCH request @@ -79,7 +82,7 @@ class JsonApiClient { /// https://jsonapi.org/format/#crud-updating-to-one-relationships Future> replaceToOne(Uri uri, Identifier id, {Map headers}) => - _patch(ToOne.parse, uri, + _patch(_parser.parseToOne, uri, ToOne(nullable(IdentifierJson.fromIdentifier)(id)), headers); /// Removes a to-one relationship. This is equivalent to calling [replaceToOne] @@ -96,8 +99,8 @@ class JsonApiClient { /// https://jsonapi.org/format/#crud-updating-to-many-relationships Future> replaceToMany(Uri uri, List ids, {Map headers}) => - _patch(ToMany.parse, uri, ToMany(ids.map(IdentifierJson.fromIdentifier)), - headers); + _patch(_parser.parseToMany, uri, + ToMany(ids.map(IdentifierJson.fromIdentifier)), headers); /// Adds the given set of [ids] to a to-many relationship. /// @@ -119,8 +122,8 @@ class JsonApiClient { /// https://jsonapi.org/format/#crud-updating-to-many-relationships Future> addToMany(Uri uri, List ids, {Map headers}) => - _post(ToMany.parse, uri, ToMany(ids.map(IdentifierJson.fromIdentifier)), - headers); + _post(_parser.parseToMany, uri, + ToMany(ids.map(IdentifierJson.fromIdentifier)), headers); Future> _get( D parse(Object _), uri, Map headers) => @@ -177,7 +180,7 @@ class JsonApiClient { return Response(r.statusCode, r.headers); } final body = json.decode(r.body); - final document = body == null ? null : Document.parse(body, parse); + final document = body == null ? null : _parser.parseDocument(body, parse); return Response(r.statusCode, r.headers, document: document); } finally { diff --git a/lib/src/document/document.dart b/lib/src/document/document.dart index 64ba72d..bcd87fc 100644 --- a/lib/src/document/document.dart +++ b/lib/src/document/document.dart @@ -1,71 +1,33 @@ import 'package:json_api/src/document/error.dart'; import 'package:json_api/src/document/primary_data.dart'; -import 'package:json_api/src/document/resource_json.dart'; class Document { /// The Primary Data final Data data; - /// For Compound Documents this member contains the included resources - final List included; - final List errors; final Map meta; - Document(this.data, - {Map meta, Iterable included}) + Document(this.data, {Map meta}) : this.errors = null, - this.included = - (included == null || included.isEmpty ? null : List.from(included)), this.meta = (meta == null || meta.isEmpty ? null : Map.from(meta)); Document.error(Iterable errors, {Map meta}) : this.data = null, - this.included = null, this.errors = List.from(errors), this.meta = (meta == null || meta.isEmpty ? null : Map.from(meta)); Document.empty(Map meta) : this.data = null, this.errors = null, - this.included = null, this.meta = (meta == null || meta.isEmpty ? null : Map.from(meta)) { ArgumentError.checkNotNull(meta, 'meta'); } - static Document parse( - Object json, Data parsePrimaryData(Object json)) { - if (json is Map) { - // TODO: validate `meta` - if (json.containsKey('errors')) { - final errors = json['errors']; - if (errors is List) { - return Document.error(errors.map(JsonApiError.parse), - meta: json['meta']); - } - } else if (json.containsKey('data')) { - final included = json['included']; - final resources = []; - if (included is List) { - resources.addAll(included.map(ResourceJson.parse)); - } - return Document(parsePrimaryData(json), - meta: json['meta'], - included: resources.isNotEmpty ? resources : null); - } else { - return Document.empty(json['meta']); - } - } - throw 'Can not parse Document from $json'; - } - Map toJson() { Map json = {}; if (data != null) { json = data.toJson(); - if (included != null && included.isNotEmpty) { - json['included'] = included; - } } else if (errors != null) { json = {'errors': errors}; } diff --git a/lib/src/document/error.dart b/lib/src/document/error.dart index d9aacb0..ae97082 100644 --- a/lib/src/document/error.dart +++ b/lib/src/document/error.dart @@ -47,31 +47,6 @@ class JsonApiError { this.meta.addAll(meta ?? {}); } - static JsonApiError parse(Object json) { - if (json is Map) { - Link about; - if (json['links'] is Map) about = Link.parse(json['links']['about']); - - String pointer; - String parameter; - if (json['source'] is Map) { - parameter = json['source']['parameter']; - pointer = json['source']['pointer']; - } - return JsonApiError( - id: json['id'], - about: about, - status: json['status'], - code: json['code'], - title: json['title'], - detail: json['detail'], - sourcePointer: pointer, - sourceParameter: parameter, - meta: json['meta']); - } - throw 'Can not parse ErrorObject from $json'; - } - Map toJson() { final json = {}; if (id != null) json['id'] = id; diff --git a/lib/src/document/identifier_json.dart b/lib/src/document/identifier_json.dart index de78622..fa26d02 100644 --- a/lib/src/document/identifier_json.dart +++ b/lib/src/document/identifier_json.dart @@ -8,13 +8,6 @@ class IdentifierJson { IdentifierJson(this.type, this.id); - static IdentifierJson parse(Object json) { - if (json is Map) { - return IdentifierJson(json['type'], json['id']); - } - throw 'Can not parse IdentifierObject from $json'; - } - static IdentifierJson fromIdentifier(Identifier id) => IdentifierJson(id.type, id.id); diff --git a/lib/src/document/link.dart b/lib/src/document/link.dart index ed20d47..ebfcd90 100644 --- a/lib/src/document/link.dart +++ b/lib/src/document/link.dart @@ -7,27 +7,6 @@ class Link { ArgumentError.checkNotNull(uri, 'uri'); } - static Link parse(Object json) { - if (json is String) return Link(Uri.parse(json)); - if (json is Map) { - return LinkObject(Uri.parse(json['href']), meta: json['meta']); - } - throw 'Can not parse Link from $json'; - } - - /// Parses the document's `links` member into a map. - /// The retuning map does not have null values. - /// - /// Details on the `links` member: https://jsonapi.org/format/#document-links - static Map parseLinks(Object json) { - if (json == null) return {}; - if (json is Map) { - return (json..removeWhere((_, v) => v == null)) - .map((k, v) => MapEntry(k.toString(), parse(v))); - } - throw 'Can not parse links from $json'; - } - toJson() => uri.toString(); } diff --git a/lib/src/document/relationship.dart b/lib/src/document/relationship.dart index 580c1da..9615df7 100644 --- a/lib/src/document/relationship.dart +++ b/lib/src/document/relationship.dart @@ -17,34 +17,6 @@ class Relationship extends PrimaryData { Relationship({this.related, Link self}) : super(self: self); - /// Parses a JSON:API Document or the `relationship` member of a Resource object. - static Relationship parse(Object json) { - if (json is Map) { - if (json.containsKey('data')) { - final data = json['data']; - if (data == null || data is Map) { - return ToOne.parse(json); - } - if (data is List) { - return ToMany.parse(json); - } - } else { - final links = Link.parseLinks(json['links']); - return Relationship(self: links['self'], related: links['related']); - } - } - throw 'Can not parse Relationship from $json'; - } - - /// Parses the `relationships` member of a Resource Object - static Map parseRelationships(Object json) { - if (json == null) return {}; - if (json is Map) { - return json.map((k, v) => MapEntry(k.toString(), Relationship.parse(v))); - } - throw 'Can not parse Relationship map from $json'; - } - Map toLinks() => related == null ? super.toLinks() : (super.toLinks()..['related'] = related); @@ -74,23 +46,6 @@ class ToOne extends Relationship { : linkage = null, super(self: self, related: related); - static ToOne parse(Object json) { - if (json is Map) { - final links = Link.parseLinks(json['links']); - if (json.containsKey('data')) { - final data = json['data']; - if (data == null) { - return ToOne.empty(self: links['self'], related: links['related']); - } - if (data is Map) { - return ToOne(IdentifierJson.parse(data), - self: links['self'], related: links['related']); - } - } - } - throw 'Can not parse ToOne from $json'; - } - Map toJson() => super.toJson()..['data'] = linkage; /// Converts to [Identifier]. @@ -115,20 +70,6 @@ class ToMany extends Relationship { this.linkage.addAll(linkage); } - static ToMany parse(Object json) { - if (json is Map) { - final links = Link.parseLinks(json['links']); - if (json.containsKey('data')) { - final data = json['data']; - if (data is List) { - return ToMany(data.map(IdentifierJson.parse), - self: links['self'], related: links['related']); - } - } - } - throw 'Can not parse ToMany from $json'; - } - Map toLinks() => super.toLinks()..addAll(pagination.toLinks()); Map toJson() => super.toJson()..['data'] = linkage; diff --git a/lib/src/document/resource_json.dart b/lib/src/document/resource_json.dart index 3852717..27eaeb0 100644 --- a/lib/src/document/resource_json.dart +++ b/lib/src/document/resource_json.dart @@ -30,24 +30,6 @@ class ResourceJson { this.relationships.addAll(relationships ?? {}); } - /// Parses the `data` member of a JSON:API Document - static ResourceJson parse(Object json) { - final mapOrNull = (_) => _ == null || _ is Map; - if (json is Map) { - final relationships = json['relationships']; - final attributes = json['attributes']; - final links = Link.parseLinks(json['links']); - - if (mapOrNull(relationships) && mapOrNull(attributes)) { - return ResourceJson(json['type'], json['id'], - attributes: attributes, - relationships: Relationship.parseRelationships(relationships), - self: links['self']); - } - } - throw 'Can not parse ResourceObject from $json'; - } - static ResourceJson fromResource(Resource resource) { final relationships = {} ..addAll(resource.toOne.map((k, v) => @@ -108,21 +90,21 @@ class ResourceJson { class ResourceData extends PrimaryData { final ResourceJson resourceJson; - ResourceData(this.resourceJson, {Link self}) : super(self: self); + /// For Compound Documents this member contains the included resources + final List included; - /// Parse the document - static ResourceData parse(Object json) { - if (json is Map) { - final links = Link.parseLinks(json['links']); - final data = ResourceJson.parse(json['data']); - return ResourceData(data, self: links['self']); - } - throw 'Can not parse SingleResourceObject from $json'; - } + ResourceData(this.resourceJson, {Link self, Iterable included}) + : this.included = + (included == null || included.isEmpty ? null : List.from(included)), + super(self: self); @override Map toJson() { final json = {'data': resourceJson}; + if (included != null && included.isNotEmpty) { + json['included'] = included; + } + final links = toLinks(); if (links.isNotEmpty) json['links'] = links; return json; @@ -136,28 +118,26 @@ class ResourceCollectionData extends PrimaryData { final collection = []; final Pagination pagination; + /// For Compound Documents this member contains the included resources + final List included; + ResourceCollectionData(Iterable collection, - {Link self, this.pagination = const Pagination.empty()}) - : super(self: self) { + {Link self, + Iterable included, + this.pagination = const Pagination.empty()}) + : this.included = + (included == null || included.isEmpty ? null : List.from(included)), + super(self: self) { this.collection.addAll(collection); } - /// Parse the document - static ResourceCollectionData parse(Object json) { - if (json is Map) { - final links = Link.parseLinks(json['links']); - final data = json['data']; - if (data is List) { - return ResourceCollectionData(data.map(ResourceJson.parse), - self: links['self'], pagination: Pagination.fromLinks(links)); - } - } - throw 'Can not parse ResourceObjectCollection from $json'; - } - @override Map toJson() { final json = {'data': collection}; + if (included != null && included.isNotEmpty) { + json['included'] = included; + } + final links = toLinks()..addAll(pagination.toLinks()); if (links.isNotEmpty) json['links'] = links; return json; diff --git a/lib/src/parser.dart b/lib/src/parser.dart new file mode 100644 index 0000000..cd5c282 --- /dev/null +++ b/lib/src/parser.dart @@ -0,0 +1,191 @@ +import 'package:json_api/document.dart'; +import 'package:json_api/src/document/pagination.dart'; + +class JsonApiDocumentParser { + const JsonApiDocumentParser(); + + Document parseDocument( + Object json, Data parsePrimaryData(Object json)) { + if (json is Map) { + // TODO: validate `meta` + if (json.containsKey('errors')) { + final errors = json['errors']; + if (errors is List) { + return Document.error(errors.map(parseError), meta: json['meta']); + } + } else if (json.containsKey('data')) { + return Document(parsePrimaryData(json), meta: json['meta']); + } else { + return Document.empty(json['meta']); + } + } + throw 'Can not parse Document from $json'; + } + + JsonApiError parseError(Object json) { + if (json is Map) { + Link about; + if (json['links'] is Map) about = parseLink(json['links']['about']); + + String pointer; + String parameter; + if (json['source'] is Map) { + parameter = json['source']['parameter']; + pointer = json['source']['pointer']; + } + return JsonApiError( + id: json['id'], + about: about, + status: json['status'], + code: json['code'], + title: json['title'], + detail: json['detail'], + sourcePointer: pointer, + sourceParameter: parameter, + meta: json['meta']); + } + throw 'Can not parse ErrorObject from $json'; + } + + /// Parses a JSON:API Document or the `relationship` member of a Resource object. + Relationship parseRelationship(Object json) { + if (json is Map) { + if (json.containsKey('data')) { + final data = json['data']; + if (data == null || data is Map) { + return parseToOne(json); + } + if (data is List) { + return parseToMany(json); + } + } else { + final links = parseLinks(json['links']); + return Relationship(self: links['self'], related: links['related']); + } + } + throw 'Can not parse Relationship from $json'; + } + + /// Parses the `relationships` member of a Resource Object + Map parseRelationships(Object json) { + if (json == null) return {}; + if (json is Map) { + return json.map((k, v) => MapEntry(k.toString(), parseRelationship(v))); + } + throw 'Can not parse Relationship map from $json'; + } + + /// Parses the `data` member of a JSON:API Document + ResourceJson parseResourceJson(Object json) { + final mapOrNull = (_) => _ == null || _ is Map; + if (json is Map) { + final relationships = json['relationships']; + final attributes = json['attributes']; + final links = parseLinks(json['links']); + + if (mapOrNull(relationships) && mapOrNull(attributes)) { + return ResourceJson(json['type'], json['id'], + attributes: attributes, + relationships: parseRelationships(relationships), + self: links['self']); + } + } + throw 'Can not parse ResourceObject from $json'; + } + + /// Parse the document + ResourceData parseResourceData(Object json) { + if (json is Map) { + final links = parseLinks(json['links']); + final included = json['included']; + final resources = []; + if (included is List) { + resources.addAll(included.map(parseResourceJson)); + } + final data = parseResourceJson(json['data']); + return ResourceData(data, + self: links['self'], + included: resources.isNotEmpty ? resources : null); + } + throw 'Can not parse SingleResourceObject from $json'; + } + + /// Parse the document + ResourceCollectionData parseResourceCollectionData(Object json) { + if (json is Map) { + final links = parseLinks(json['links']); + final included = json['included']; + final resources = []; + if (included is List) { + resources.addAll(included.map(parseResourceJson)); + } + final data = json['data']; + if (data is List) { + return ResourceCollectionData(data.map(parseResourceJson), + self: links['self'], + pagination: Pagination.fromLinks(links), + included: resources.isNotEmpty ? resources : null); + } + } + throw 'Can not parse ResourceObjectCollection from $json'; + } + + ToOne parseToOne(Object json) { + if (json is Map) { + final links = parseLinks(json['links']); + if (json.containsKey('data')) { + final data = json['data']; + if (data == null) { + return ToOne.empty(self: links['self'], related: links['related']); + } + if (data is Map) { + return ToOne(parseIdentifierJson(data), + self: links['self'], related: links['related']); + } + } + } + throw 'Can not parse ToOne from $json'; + } + + ToMany parseToMany(Object json) { + if (json is Map) { + final links = parseLinks(json['links']); + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + return ToMany(data.map(parseIdentifierJson), + self: links['self'], related: links['related']); + } + } + } + throw 'Can not parse ToMany from $json'; + } + + IdentifierJson parseIdentifierJson(Object json) { + if (json is Map) { + return IdentifierJson(json['type'], json['id']); + } + throw 'Can not parse IdentifierObject from $json'; + } + + Link parseLink(Object json) { + if (json is String) return Link(Uri.parse(json)); + if (json is Map) { + return LinkObject(Uri.parse(json['href']), meta: json['meta']); + } + throw 'Can not parse Link from $json'; + } + + /// Parses the document's `links` member into a map. + /// The retuning map does not have null values. + /// + /// Details on the `links` member: https://jsonapi.org/format/#document-links + Map parseLinks(Object json) { + if (json == null) return {}; + if (json is Map) { + return (json..removeWhere((_, v) => v == null)) + .map((k, v) => MapEntry(k.toString(), parseLink(v))); + } + throw 'Can not parse links from $json'; + } +} diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index 8673273..fe3c208 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -3,14 +3,14 @@ import 'dart:convert'; import 'dart:io'; import 'package:json_api/document.dart'; +import 'package:json_api/src/parser.dart'; import 'package:json_api/src/server/contracts/controller.dart'; +import 'package:json_api/src/server/contracts/document_builder.dart'; import 'package:json_api/src/server/contracts/page.dart'; import 'package:json_api/src/server/contracts/router.dart'; -import 'package:json_api/src/server/contracts/document_builder.dart'; import 'package:json_api/src/server/standard_document_builder.dart'; part 'server_requests.dart'; - part 'server_routes.dart'; class JsonApiServer { diff --git a/lib/src/server/server_requests.dart b/lib/src/server/server_requests.dart index d6ae704..3626110 100644 --- a/lib/src/server/server_requests.dart +++ b/lib/src/server/server_requests.dart @@ -1,5 +1,7 @@ part of 'server.dart'; +const _parser = const JsonApiDocumentParser(); + abstract class _BaseRequest { HttpResponse response; bool allowOrigin; @@ -96,7 +98,7 @@ class _CreateResource extends _CollectionRequest Resource resource; void setBody(Object body) { - resource = ResourceData.parse(body).toResource(); + resource = _parser.parseResourceData(body).toResource(); } Future call(JsonApiController controller) => controller.createResource(this); @@ -149,7 +151,7 @@ class _ReplaceRelationship extends _RelationshipRequest @override void setBody(Object body) { - final r = Relationship.parse(body); + final r = _parser.parseRelationship(body); if (r is ToOne) identifier = r.toIdentifier(); if (r is ToMany) identifiers = r.toIdentifiers(); } @@ -171,7 +173,7 @@ class _AddToMany extends _RelationshipRequest implements AddToManyRequest { @override void setBody(Object body) { - final r = Relationship.parse(body); + final r = _parser.parseRelationship(body); if (r is ToOne) identifier = r.toIdentifier(); if (r is ToMany) identifiers = r.toIdentifiers(); } @@ -210,7 +212,7 @@ class _UpdateResource extends _ResourceRequest @override void setBody(Object body) { - resource = ResourceData.parse(body).resourceJson.toResource(); + resource = _parser.parseResourceData(body).resourceJson.toResource(); } Future call(JsonApiController controller) => controller.updateResource(this); diff --git a/lib/src/server/standard_document_builder.dart b/lib/src/server/standard_document_builder.dart index 9dd7ea3..c566e7f 100644 --- a/lib/src/server/standard_document_builder.dart +++ b/lib/src/server/standard_document_builder.dart @@ -41,18 +41,20 @@ class StandardDocumentBuilder implements DocumentBuilder { Uri self, {Iterable included}) => Document( - ResourceData(_resourceJson(resource), - self: Link(uriBuilder.resource(type, id))), - included: included?.map(_resourceJson)); + ResourceData(_resourceJson(resource), + self: Link(uriBuilder.resource(type, id)), + included: included?.map(_resourceJson)), + ); Document relatedResource(Resource resource, String type, String id, String relationship, Uri self, {Iterable included}) => Document( - ResourceData(_resourceJson(resource), - self: Link(uriBuilder.related(type, id, relationship))), - included: included?.map(_resourceJson)); + ResourceData( + _resourceJson(resource), included: included?.map(_resourceJson), + self: Link(uriBuilder.related(type, id, relationship))), + ); Document toMany(Iterable collection, String type, String id, diff --git a/lib/src/server/standard_router.dart b/lib/src/server/standard_router.dart index 6779950..c7c3ba5 100644 --- a/lib/src/server/standard_router.dart +++ b/lib/src/server/standard_router.dart @@ -34,7 +34,8 @@ class StandardRouter implements Router { pathSegments: base.pathSegments + [type, id, 'relationships', relationship]); - Uri resource(String type, String id, {Map parameters = const {}}) => + Uri resource(String type, String id, + {Map parameters = const {}}) => base.replace(pathSegments: base.pathSegments + [type, id]); R getRoute(Uri uri, RouteFactory route) { diff --git a/test/functional/fetch_test.dart b/test/functional/fetch_test.dart index aaa64a0..046c95d 100644 --- a/test/functional/fetch_test.dart +++ b/test/functional/fetch_test.dart @@ -94,11 +94,11 @@ void main() async { expect(r.isSuccessful, true); expect(r.data.toResource().attributes['name'], 'Tesla'); expect(r.data.self.uri, uri); - expect(r.document.included.length, 5); - expect(r.document.included.first.type, 'cities'); - expect(r.document.included.first.attributes['name'], 'Palo Alto'); - expect(r.document.included.last.type, 'models'); - expect(r.document.included.last.attributes['name'], 'Model 3'); + expect(r.data.included.length, 5); + expect(r.data.included.first.type, 'cities'); + expect(r.data.included.first.attributes['name'], 'Palo Alto'); + expect(r.data.included.last.type, 'models'); + expect(r.data.included.last.attributes['name'], 'Model 3'); }); test('404 on type', () async { diff --git a/test/unit/document_test.dart b/test/unit/document_test.dart index dfe3a26..04a902f 100644 --- a/test/unit/document_test.dart +++ b/test/unit/document_test.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; -import 'dart:io'; - @TestOn('vm') import 'package:json_api/document.dart'; import 'package:json_matcher/json_matcher.dart'; @@ -19,22 +16,5 @@ void main() { })); }); }); - - group('Standard compliance', () { - try { - test('Can parse the example document', () { - // This is a slightly modified example from the JSON:API site - // See: https://jsonapi.org/ - final jsonString = - new File('test/unit/example.json').readAsStringSync(); - final jsonObject = json.decode(jsonString); - final doc = Document.parse(jsonObject, ResourceCollectionData.parse); - - expect(doc, encodesToJson(jsonObject)); - }); - } catch (e, s) { - print(s); - } - }); }); } diff --git a/test/unit/example.json b/test/unit/example.json index 85ace8f..3ccb7fc 100644 --- a/test/unit/example.json +++ b/test/unit/example.json @@ -4,73 +4,94 @@ "next": "http://example.com/articles?page=2", "last": "http://example.com/articles?page=10" }, - "data": [{ - "type": "articles", - "id": "1", - "attributes": { - "title": "JSON:API paints my bikeshed!" - }, - "relationships": { - "author": { - "links": { - "self": "http://example.com/articles/1/relationships/author", - "related": "http://example.com/articles/1/author" - }, - "data": { "type": "people", "id": "9" } + "data": [ + { + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON:API paints my bikeshed!" }, - "comments": { - "links": { - "self": "http://example.com/articles/1/relationships/comments", - "related": "http://example.com/articles/1/comments" + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { + "type": "people", + "id": "9" + } }, - "data": [ - { "type": "comments", "id": "5" }, - { "type": "comments", "id": "12" } - ] + "comments": { + "links": { + "self": "http://example.com/articles/1/relationships/comments", + "related": "http://example.com/articles/1/comments" + }, + "data": [ + { + "type": "comments", + "id": "5" + }, + { + "type": "comments", + "id": "12" + } + ] + } + }, + "links": { + "self": "http://example.com/articles/1" } - }, - "links": { - "self": "http://example.com/articles/1" } - }], - "included": [{ - "type": "people", - "id": "9", - "attributes": { - "firstName": "Dan", - "lastName": "Gebhardt", - "twitter": "dgeb" - }, - "links": { - "self": "http://example.com/people/9" - } - }, { - "type": "comments", - "id": "5", - "attributes": { - "body": "First!" - }, - "relationships": { - "author": { - "data": { "type": "people", "id": "2" } + ], + "included": [ + { + "type": "people", + "id": "9", + "attributes": { + "firstName": "Dan", + "lastName": "Gebhardt", + "twitter": "dgeb" + }, + "links": { + "self": "http://example.com/people/9" } }, - "links": { - "self": "http://example.com/comments/5" - } - }, { - "type": "comments", - "id": "12", - "attributes": { - "body": "I like XML better" - }, - "relationships": { - "author": { - "data": { "type": "people", "id": "9" } + { + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "relationships": { + "author": { + "data": { + "type": "people", + "id": "2" + } + } + }, + "links": { + "self": "http://example.com/comments/5" } }, - "links": { - "self": "http://example.com/comments/12" + { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "relationships": { + "author": { + "data": { + "type": "people", + "id": "9" + } + } + }, + "links": { + "self": "http://example.com/comments/12" + } } - }] + ] } \ No newline at end of file diff --git a/test/unit/parser_test.dart b/test/unit/parser_test.dart new file mode 100644 index 0000000..9d677f3 --- /dev/null +++ b/test/unit/parser_test.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:json_api/src/parser.dart'; +import 'package:json_matcher/json_matcher.dart'; +import 'package:test/test.dart'; + +void main() { + final parser = JsonApiDocumentParser(); + group('Parser', () { + try { + test('Can parse the example document', () { + // This is a slightly modified example from the JSON:API site + // See: https://jsonapi.org/ + final jsonString = + new File('test/unit/example.json').readAsStringSync(); + final jsonObject = json.decode(jsonString); + final doc = parser.parseDocument( + jsonObject, parser.parseResourceCollectionData); + + expect(doc, encodesToJson(jsonObject)); + }); + } catch (e, s) { + print(s); + } + }); +} From 9603d093925633e6a0d58c66c028ae862b2df28d Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sun, 17 Mar 2019 20:13:23 -0700 Subject: [PATCH 6/9] Release --- CHANGELOG.md | 5 +- README.md | 2 +- lib/document.dart | 6 +- lib/src/client/client.dart | 12 +-- ...ifier_json.dart => identifier_object.dart} | 10 +-- lib/src/document/relationship.dart | 8 +- .../document/resource_collection_data.dart | 35 +++++++++ lib/src/document/resource_data.dart | 32 ++++++++ ...esource_json.dart => resource_object.dart} | 78 +++---------------- lib/src/parser.dart | 58 +++++++------- lib/src/server/server_requests.dart | 6 +- lib/src/server/standard_document_builder.dart | 26 +++---- pubspec.yaml | 2 +- test/functional/create_test.dart | 2 +- test/unit/document_test.dart | 2 +- test/unit/parser_test.dart | 2 +- 16 files changed, 152 insertions(+), 134 deletions(-) rename lib/src/document/{identifier_json.dart => identifier_object.dart} (52%) create mode 100644 lib/src/document/resource_collection_data.dart create mode 100644 lib/src/document/resource_data.dart rename lib/src/document/{resource_json.dart => resource_object.dart} (50%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c817f..893e802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.4.0] - 2019-03-17 ### Changed - Parsing logic moved out - Some other BC-breaking changes in the Document @@ -34,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Client: fetch resources, collections, related resources and relationships -[Unreleased]: https://github.com/f3ath/json-api-dart/compare/0.3.0...HEAD +[Unreleased]: https://github.com/f3ath/json-api-dart/compare/0.4.0...HEAD +[0.4.0]: https://github.com/f3ath/json-api-dart/compare/0.3.0...0.4.0 [0.3.0]: https://github.com/f3ath/json-api-dart/compare/0.2.0...0.3.0 [0.2.0]: https://github.com/f3ath/json-api-dart/compare/0.1.0...0.2.0 diff --git a/README.md b/README.md index 0cfefa7..665275a 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The features here are roughly ordered by priority. Feel free to open an issue if #### Document - [x] Support relationship objects lacking the `data` member -- [ ] Compound documents +- [x] Compound documents - [ ] Support `meta` members - [ ] Support `jsonapi` members - [ ] Structural Validation including compound documents diff --git a/lib/document.dart b/lib/document.dart index 35ab4ee..6ed66f4 100644 --- a/lib/document.dart +++ b/lib/document.dart @@ -1,9 +1,11 @@ export 'package:json_api/src/document/document.dart'; export 'package:json_api/src/document/error.dart'; export 'package:json_api/src/document/identifier.dart'; -export 'package:json_api/src/document/identifier_json.dart'; +export 'package:json_api/src/document/identifier_object.dart'; export 'package:json_api/src/document/link.dart'; export 'package:json_api/src/document/primary_data.dart'; export 'package:json_api/src/document/relationship.dart'; export 'package:json_api/src/document/resource.dart'; -export 'package:json_api/src/document/resource_json.dart'; +export 'package:json_api/src/document/resource_collection_data.dart'; +export 'package:json_api/src/document/resource_data.dart'; +export 'package:json_api/src/document/resource_object.dart'; diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index f3c06f1..f14df1a 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -15,7 +15,7 @@ typedef http.Client HttpClientFactory(); class JsonApiClient { static const contentType = 'application/vnd.api+json'; - JsonApiDocumentParser _parser = const JsonApiDocumentParser(); + JsonApiParser _parser = const JsonApiParser(); final HttpClientFactory _factory; @@ -61,7 +61,7 @@ class JsonApiClient { Future> createResource(Uri uri, Resource resource, {Map headers}) => _post(_parser.parseResourceData, uri, - ResourceData(ResourceJson.fromResource(resource)), headers); + ResourceData(ResourceObject.fromResource(resource)), headers); /// Deletes the resource. /// @@ -75,7 +75,7 @@ class JsonApiClient { Future> updateResource(Uri uri, Resource resource, {Map headers}) => _patch(_parser.parseResourceData, uri, - ResourceData(ResourceJson.fromResource(resource)), headers); + ResourceData(ResourceObject.fromResource(resource)), headers); /// Updates a to-one relationship via PATCH request /// @@ -83,7 +83,7 @@ class JsonApiClient { Future> replaceToOne(Uri uri, Identifier id, {Map headers}) => _patch(_parser.parseToOne, uri, - ToOne(nullable(IdentifierJson.fromIdentifier)(id)), headers); + ToOne(nullable(IdentifierObject.fromIdentifier)(id)), headers); /// Removes a to-one relationship. This is equivalent to calling [replaceToOne] /// with id = null. @@ -100,7 +100,7 @@ class JsonApiClient { Future> replaceToMany(Uri uri, List ids, {Map headers}) => _patch(_parser.parseToMany, uri, - ToMany(ids.map(IdentifierJson.fromIdentifier)), headers); + ToMany(ids.map(IdentifierObject.fromIdentifier)), headers); /// Adds the given set of [ids] to a to-many relationship. /// @@ -123,7 +123,7 @@ class JsonApiClient { Future> addToMany(Uri uri, List ids, {Map headers}) => _post(_parser.parseToMany, uri, - ToMany(ids.map(IdentifierJson.fromIdentifier)), headers); + ToMany(ids.map(IdentifierObject.fromIdentifier)), headers); Future> _get( D parse(Object _), uri, Map headers) => diff --git a/lib/src/document/identifier_json.dart b/lib/src/document/identifier_object.dart similarity index 52% rename from lib/src/document/identifier_json.dart rename to lib/src/document/identifier_object.dart index fa26d02..2f25ca3 100644 --- a/lib/src/document/identifier_json.dart +++ b/lib/src/document/identifier_object.dart @@ -1,15 +1,15 @@ import 'package:json_api/src/document/identifier.dart'; -/// [IdentifierJson] is a JSON representation of the [Identifier]. +/// [IdentifierObject] is a JSON representation of the [Identifier]. /// It carries all JSON-related logic and the Meta-data. -class IdentifierJson { +class IdentifierObject { final String type; final String id; - IdentifierJson(this.type, this.id); + IdentifierObject(this.type, this.id); - static IdentifierJson fromIdentifier(Identifier id) => - IdentifierJson(id.type, id.id); + static IdentifierObject fromIdentifier(Identifier id) => + IdentifierObject(id.type, id.id); Identifier toIdentifier() => Identifier(type, id); diff --git a/lib/src/document/relationship.dart b/lib/src/document/relationship.dart index 9615df7..f30150d 100644 --- a/lib/src/document/relationship.dart +++ b/lib/src/document/relationship.dart @@ -1,5 +1,5 @@ import 'package:json_api/src/document/identifier.dart'; -import 'package:json_api/src/document/identifier_json.dart'; +import 'package:json_api/src/document/identifier_object.dart'; import 'package:json_api/src/document/link.dart'; import 'package:json_api/src/document/pagination.dart'; import 'package:json_api/src/document/primary_data.dart'; @@ -37,7 +37,7 @@ class ToOne extends Relationship { /// Can be null for empty relationships /// /// More on this: https://jsonapi.org/format/#document-resource-object-linkage - final IdentifierJson linkage; + final IdentifierObject linkage; ToOne(this.linkage, {Link self, Link related}) : super(self: self, related: related); @@ -60,11 +60,11 @@ class ToMany extends Relationship { /// Can be empty for empty relationships /// /// More on this: https://jsonapi.org/format/#document-resource-object-linkage - final linkage = []; + final linkage = []; final Pagination pagination; - ToMany(Iterable linkage, + ToMany(Iterable linkage, {Link self, Link related, this.pagination = const Pagination.empty()}) : super(self: self, related: related) { this.linkage.addAll(linkage); diff --git a/lib/src/document/resource_collection_data.dart b/lib/src/document/resource_collection_data.dart new file mode 100644 index 0000000..c887860 --- /dev/null +++ b/lib/src/document/resource_collection_data.dart @@ -0,0 +1,35 @@ +import 'package:json_api/src/document/link.dart'; +import 'package:json_api/src/document/pagination.dart'; +import 'package:json_api/src/document/primary_data.dart'; +import 'package:json_api/src/document/resource_object.dart'; + +/// Represents a resource collection or a collection of related resources of a to-many relationship +class ResourceCollectionData extends PrimaryData { + final collection = []; + final Pagination pagination; + + /// For Compound Documents this member contains the included resources + final List included; + + ResourceCollectionData(Iterable collection, + {Link self, + Iterable included, + this.pagination = const Pagination.empty()}) + : this.included = + (included == null || included.isEmpty ? null : List.from(included)), + super(self: self) { + this.collection.addAll(collection); + } + + @override + Map toJson() { + final json = {'data': collection}; + if (included != null && included.isNotEmpty) { + json['included'] = included; + } + + final links = toLinks()..addAll(pagination.toLinks()); + if (links.isNotEmpty) json['links'] = links; + return json; + } +} diff --git a/lib/src/document/resource_data.dart b/lib/src/document/resource_data.dart new file mode 100644 index 0000000..c0118f9 --- /dev/null +++ b/lib/src/document/resource_data.dart @@ -0,0 +1,32 @@ +import 'package:json_api/src/document/link.dart'; +import 'package:json_api/src/document/primary_data.dart'; +import 'package:json_api/src/document/resource.dart'; +import 'package:json_api/src/document/resource_object.dart'; + +/// Represents a single resource or a single related resource of a to-one relationship\\\\\\\\ +class ResourceData extends PrimaryData { + final ResourceObject resourceObject; + + /// For Compound Documents this member contains the included resources + final List included; + + ResourceData(this.resourceObject, + {Link self, Iterable included}) + : this.included = + (included == null || included.isEmpty ? null : List.from(included)), + super(self: self); + + @override + Map toJson() { + final json = {'data': resourceObject}; + if (included != null && included.isNotEmpty) { + json['included'] = included; + } + + final links = toLinks(); + if (links.isNotEmpty) json['links'] = links; + return json; + } + + Resource toResource() => resourceObject.toResource(); +} diff --git a/lib/src/document/resource_json.dart b/lib/src/document/resource_object.dart similarity index 50% rename from lib/src/document/resource_json.dart rename to lib/src/document/resource_object.dart index 27eaeb0..c385fe4 100644 --- a/lib/src/document/resource_json.dart +++ b/lib/src/document/resource_object.dart @@ -1,13 +1,11 @@ import 'package:json_api/src/document/identifier.dart'; -import 'package:json_api/src/document/identifier_json.dart'; +import 'package:json_api/src/document/identifier_object.dart'; import 'package:json_api/src/document/link.dart'; -import 'package:json_api/src/document/pagination.dart'; -import 'package:json_api/src/document/primary_data.dart'; import 'package:json_api/src/document/relationship.dart'; import 'package:json_api/src/document/resource.dart'; import 'package:json_api/src/nullable.dart'; -/// [ResourceJson] is a JSON representation of a [Resource]. +/// [ResourceObject] is a JSON representation of a [Resource]. /// /// It carries all JSON-related logic and the Meta-data. /// In a JSON:API Document it can be the value of the `data` member (a `data` @@ -15,14 +13,14 @@ import 'package:json_api/src/nullable.dart'; /// resource collection. /// /// More on this: https://jsonapi.org/format/#document-resource-objects -class ResourceJson { +class ResourceObject { final String type; final String id; final Link self; final attributes = {}; final relationships = {}; - ResourceJson(this.type, this.id, + ResourceObject(this.type, this.id, {Map attributes, Map relationships, this.self}) { @@ -30,14 +28,14 @@ class ResourceJson { this.relationships.addAll(relationships ?? {}); } - static ResourceJson fromResource(Resource resource) { + static ResourceObject fromResource(Resource resource) { final relationships = {} ..addAll(resource.toOne.map((k, v) => - MapEntry(k, ToOne(nullable(IdentifierJson.fromIdentifier)(v))))) - ..addAll(resource.toMany.map( - (k, v) => MapEntry(k, ToMany(v.map(IdentifierJson.fromIdentifier))))); + MapEntry(k, ToOne(nullable(IdentifierObject.fromIdentifier)(v))))) + ..addAll(resource.toMany.map((k, v) => + MapEntry(k, ToMany(v.map(IdentifierObject.fromIdentifier))))); - return ResourceJson(resource.type, resource.id, + return ResourceObject(resource.type, resource.id, attributes: resource.attributes, relationships: relationships); } @@ -85,61 +83,3 @@ class ResourceJson { attributes: attributes, toOne: toOne, toMany: toMany); } } - -/// Represents a single resource or a single related resource of a to-one relationship\\\\\\\\ -class ResourceData extends PrimaryData { - final ResourceJson resourceJson; - - /// For Compound Documents this member contains the included resources - final List included; - - ResourceData(this.resourceJson, {Link self, Iterable included}) - : this.included = - (included == null || included.isEmpty ? null : List.from(included)), - super(self: self); - - @override - Map toJson() { - final json = {'data': resourceJson}; - if (included != null && included.isNotEmpty) { - json['included'] = included; - } - - final links = toLinks(); - if (links.isNotEmpty) json['links'] = links; - return json; - } - - Resource toResource() => resourceJson.toResource(); -} - -/// Represents a resource collection or a collection of related resources of a to-many relationship -class ResourceCollectionData extends PrimaryData { - final collection = []; - final Pagination pagination; - - /// For Compound Documents this member contains the included resources - final List included; - - ResourceCollectionData(Iterable collection, - {Link self, - Iterable included, - this.pagination = const Pagination.empty()}) - : this.included = - (included == null || included.isEmpty ? null : List.from(included)), - super(self: self) { - this.collection.addAll(collection); - } - - @override - Map toJson() { - final json = {'data': collection}; - if (included != null && included.isNotEmpty) { - json['included'] = included; - } - - final links = toLinks()..addAll(pagination.toLinks()); - if (links.isNotEmpty) json['links'] = links; - return json; - } -} diff --git a/lib/src/parser.dart b/lib/src/parser.dart index cd5c282..748a5f0 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -1,8 +1,14 @@ import 'package:json_api/document.dart'; import 'package:json_api/src/document/pagination.dart'; -class JsonApiDocumentParser { - const JsonApiDocumentParser(); +class ParserException implements Exception { + final String message; + + ParserException(this.message); +} + +class JsonApiParser { + const JsonApiParser(); Document parseDocument( Object json, Data parsePrimaryData(Object json)) { @@ -19,7 +25,7 @@ class JsonApiDocumentParser { return Document.empty(json['meta']); } } - throw 'Can not parse Document from $json'; + throw ParserException('Can not parse Document from $json'); } JsonApiError parseError(Object json) { @@ -44,7 +50,7 @@ class JsonApiDocumentParser { sourceParameter: parameter, meta: json['meta']); } - throw 'Can not parse ErrorObject from $json'; + throw ParserException('Can not parse ErrorObject from $json'); } /// Parses a JSON:API Document or the `relationship` member of a Resource object. @@ -63,7 +69,7 @@ class JsonApiDocumentParser { return Relationship(self: links['self'], related: links['related']); } } - throw 'Can not parse Relationship from $json'; + throw ParserException('Can not parse Relationship from $json'); } /// Parses the `relationships` member of a Resource Object @@ -72,11 +78,11 @@ class JsonApiDocumentParser { if (json is Map) { return json.map((k, v) => MapEntry(k.toString(), parseRelationship(v))); } - throw 'Can not parse Relationship map from $json'; + throw ParserException('Can not parse Relationship map from $json'); } /// Parses the `data` member of a JSON:API Document - ResourceJson parseResourceJson(Object json) { + ResourceObject parseResourceObject(Object json) { final mapOrNull = (_) => _ == null || _ is Map; if (json is Map) { final relationships = json['relationships']; @@ -84,13 +90,13 @@ class JsonApiDocumentParser { final links = parseLinks(json['links']); if (mapOrNull(relationships) && mapOrNull(attributes)) { - return ResourceJson(json['type'], json['id'], + return ResourceObject(json['type'], json['id'], attributes: attributes, relationships: parseRelationships(relationships), self: links['self']); } } - throw 'Can not parse ResourceObject from $json'; + throw ParserException('Can not parse ResourceObject from $json'); } /// Parse the document @@ -98,16 +104,16 @@ class JsonApiDocumentParser { if (json is Map) { final links = parseLinks(json['links']); final included = json['included']; - final resources = []; + final resources = []; if (included is List) { - resources.addAll(included.map(parseResourceJson)); + resources.addAll(included.map(parseResourceObject)); } - final data = parseResourceJson(json['data']); + final data = parseResourceObject(json['data']); return ResourceData(data, self: links['self'], included: resources.isNotEmpty ? resources : null); } - throw 'Can not parse SingleResourceObject from $json'; + throw ParserException('Can not parse SingleResourceObject from $json'); } /// Parse the document @@ -115,19 +121,19 @@ class JsonApiDocumentParser { if (json is Map) { final links = parseLinks(json['links']); final included = json['included']; - final resources = []; + final resources = []; if (included is List) { - resources.addAll(included.map(parseResourceJson)); + resources.addAll(included.map(parseResourceObject)); } final data = json['data']; if (data is List) { - return ResourceCollectionData(data.map(parseResourceJson), + return ResourceCollectionData(data.map(parseResourceObject), self: links['self'], pagination: Pagination.fromLinks(links), included: resources.isNotEmpty ? resources : null); } } - throw 'Can not parse ResourceObjectCollection from $json'; + throw ParserException('Can not parse ResourceObjectCollection from $json'); } ToOne parseToOne(Object json) { @@ -139,12 +145,12 @@ class JsonApiDocumentParser { return ToOne.empty(self: links['self'], related: links['related']); } if (data is Map) { - return ToOne(parseIdentifierJson(data), + return ToOne(parseIdentifierObject(data), self: links['self'], related: links['related']); } } } - throw 'Can not parse ToOne from $json'; + throw ParserException('Can not parse ToOne from $json'); } ToMany parseToMany(Object json) { @@ -153,19 +159,19 @@ class JsonApiDocumentParser { if (json.containsKey('data')) { final data = json['data']; if (data is List) { - return ToMany(data.map(parseIdentifierJson), + return ToMany(data.map(parseIdentifierObject), self: links['self'], related: links['related']); } } } - throw 'Can not parse ToMany from $json'; + throw ParserException('Can not parse ToMany from $json'); } - IdentifierJson parseIdentifierJson(Object json) { + IdentifierObject parseIdentifierObject(Object json) { if (json is Map) { - return IdentifierJson(json['type'], json['id']); + return IdentifierObject(json['type'], json['id']); } - throw 'Can not parse IdentifierObject from $json'; + throw ParserException('Can not parse IdentifierObject from $json'); } Link parseLink(Object json) { @@ -173,7 +179,7 @@ class JsonApiDocumentParser { if (json is Map) { return LinkObject(Uri.parse(json['href']), meta: json['meta']); } - throw 'Can not parse Link from $json'; + throw ParserException('Can not parse Link from $json'); } /// Parses the document's `links` member into a map. @@ -186,6 +192,6 @@ class JsonApiDocumentParser { return (json..removeWhere((_, v) => v == null)) .map((k, v) => MapEntry(k.toString(), parseLink(v))); } - throw 'Can not parse links from $json'; + throw ParserException('Can not parse links from $json'); } } diff --git a/lib/src/server/server_requests.dart b/lib/src/server/server_requests.dart index 3626110..35d7b01 100644 --- a/lib/src/server/server_requests.dart +++ b/lib/src/server/server_requests.dart @@ -1,6 +1,6 @@ part of 'server.dart'; -const _parser = const JsonApiDocumentParser(); +const _parser = const JsonApiParser(); abstract class _BaseRequest { HttpResponse response; @@ -75,7 +75,7 @@ abstract class _BaseRequest { final doc = docBuilder.resource(resource, route.type, resource.id, uri); return _write(201, document: doc, - headers: {'Location': doc.data.resourceJson.self.toString()}); + headers: {'Location': doc.data.resourceObject.self.toString()}); } } @@ -212,7 +212,7 @@ class _UpdateResource extends _ResourceRequest @override void setBody(Object body) { - resource = _parser.parseResourceData(body).resourceJson.toResource(); + resource = _parser.parseResourceData(body).resourceObject.toResource(); } Future call(JsonApiController controller) => controller.updateResource(this); diff --git a/lib/src/server/standard_document_builder.dart b/lib/src/server/standard_document_builder.dart index c566e7f..44a9df4 100644 --- a/lib/src/server/standard_document_builder.dart +++ b/lib/src/server/standard_document_builder.dart @@ -16,7 +16,7 @@ class StandardDocumentBuilder implements DocumentBuilder { String type, Uri self, {Page page, Iterable included}) => Document(ResourceCollectionData(resource.map( - _resourceJson), + _ResourceObject), self: Link(self), pagination: page == null ? Pagination.empty() @@ -28,7 +28,7 @@ class StandardDocumentBuilder implements DocumentBuilder { Iterable resource, String type, String id, String relationship, Uri self, {Page page, Iterable included}) => - Document(ResourceCollectionData(resource.map(_resourceJson), + Document(ResourceCollectionData(resource.map(_ResourceObject), self: Link(self), pagination: page == null ? Pagination.empty() @@ -41,9 +41,9 @@ class StandardDocumentBuilder implements DocumentBuilder { Uri self, {Iterable included}) => Document( - ResourceData(_resourceJson(resource), + ResourceData(_ResourceObject(resource), self: Link(uriBuilder.resource(type, id)), - included: included?.map(_resourceJson)), + included: included?.map(_ResourceObject)), ); Document relatedResource(Resource resource, String type, @@ -52,7 +52,7 @@ class StandardDocumentBuilder implements DocumentBuilder { {Iterable included}) => Document( ResourceData( - _resourceJson(resource), included: included?.map(_resourceJson), + _ResourceObject(resource), included: included?.map(_ResourceObject), self: Link(uriBuilder.related(type, id, relationship))), ); @@ -60,28 +60,28 @@ class StandardDocumentBuilder implements DocumentBuilder { String id, String relationship, Uri self) => Document(ToMany( - collection.map(_identifierJson), + collection.map(_IdentifierObject), self: Link(uriBuilder.relationship(type, id, relationship)), related: Link(uriBuilder.related(type, id, relationship)))); Document toOne(Identifier identifier, String type, String id, String relationship, Uri self) => Document(ToOne( - nullable(_identifierJson)(identifier), + nullable(_IdentifierObject)(identifier), self: Link(uriBuilder.relationship(type, id, relationship)), related: Link(uriBuilder.related(type, id, relationship)))); Document meta(Map meta) => Document.empty(meta); - IdentifierJson _identifierJson(Identifier id) => - IdentifierJson(id.type, id.id); + IdentifierObject _IdentifierObject(Identifier id) => + IdentifierObject(id.type, id.id); - ResourceJson _resourceJson(Resource resource) { + ResourceObject _ResourceObject(Resource resource) { final relationships = {}; relationships.addAll(resource.toOne.map((k, v) => MapEntry( - k, ToOne(nullable(_identifierJson)(v), self: Link( + k, ToOne(nullable(_IdentifierObject)(v), self: Link( uriBuilder.relationship(resource.type, resource.id, k)), related: Link( uriBuilder.related(resource.type, resource.id, k)))))); @@ -89,12 +89,12 @@ class StandardDocumentBuilder implements DocumentBuilder { resource.toMany.map( (k, v) => MapEntry( - k, ToMany(v.map(_identifierJson), self: Link( + k, ToMany(v.map(_IdentifierObject), self: Link( uriBuilder.relationship(resource.type, resource.id, k)), related: Link( uriBuilder.related(resource.type, resource.id, k)))))); - return ResourceJson(resource.type, resource.id, + return ResourceObject(resource.type, resource.id, attributes: resource.attributes, relationships: relationships, self: Link(uriBuilder.resource(resource.type, resource.id))); diff --git a/pubspec.yaml b/pubspec.yaml index 7eb7e22..c8457d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ author: "Alexey Karapetov " description: "JSON:API v1.0 (http://jsonapi.org) Document, Client, and Server" homepage: "https://github.com/f3ath/json-api-dart" name: "json_api" -version: "0.3.0" +version: "0.4.0" dependencies: collection: "^1.14.11" http: "^0.12.0" diff --git a/test/functional/create_test.dart b/test/functional/create_test.dart index 56f27c3..a90e3b8 100644 --- a/test/functional/create_test.dart +++ b/test/functional/create_test.dart @@ -42,7 +42,7 @@ void main() async { // Make sure the resource is available final r1 = await client .fetchResource(Url.resource('models', r0.data.toResource().id)); - expect(r1.data.resourceJson.attributes['name'], 'Model Y'); + expect(r1.data.resourceObject.attributes['name'], 'Model Y'); }); /// If a POST request did include a Client-Generated ID and the requested diff --git a/test/unit/document_test.dart b/test/unit/document_test.dart index 04a902f..8aae120 100644 --- a/test/unit/document_test.dart +++ b/test/unit/document_test.dart @@ -7,7 +7,7 @@ void main() { group('Document', () { group('JSON Conversion', () { test('Can convert a single resource', () { - final doc = Document(ResourceData(ResourceJson('foo', 'bar'))); + final doc = Document(ResourceData(ResourceObject('foo', 'bar'))); expect( doc, diff --git a/test/unit/parser_test.dart b/test/unit/parser_test.dart index 9d677f3..727d6e9 100644 --- a/test/unit/parser_test.dart +++ b/test/unit/parser_test.dart @@ -6,7 +6,7 @@ import 'package:json_matcher/json_matcher.dart'; import 'package:test/test.dart'; void main() { - final parser = JsonApiDocumentParser(); + final parser = JsonApiParser(); group('Parser', () { try { test('Can parse the example document', () { From 0a5089b7bac3755cbe5d90fa8c144aa525efb39e Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sun, 17 Mar 2019 20:25:46 -0700 Subject: [PATCH 7/9] Tests --- lib/{src => }/parser.dart | 0 lib/src/server/server_requests.dart | 2 +- test/unit/parser_test.dart | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename lib/{src => }/parser.dart (100%) diff --git a/lib/src/parser.dart b/lib/parser.dart similarity index 100% rename from lib/src/parser.dart rename to lib/parser.dart diff --git a/lib/src/server/server_requests.dart b/lib/src/server/server_requests.dart index 35d7b01..c06a8a6 100644 --- a/lib/src/server/server_requests.dart +++ b/lib/src/server/server_requests.dart @@ -4,7 +4,7 @@ const _parser = const JsonApiParser(); abstract class _BaseRequest { HttpResponse response; - bool allowOrigin; + String allowOrigin = '*'; Uri uri; DocumentBuilder docBuilder; diff --git a/test/unit/parser_test.dart b/test/unit/parser_test.dart index 727d6e9..9ca342d 100644 --- a/test/unit/parser_test.dart +++ b/test/unit/parser_test.dart @@ -1,3 +1,4 @@ +@TestOn('vm') import 'dart:convert'; import 'dart:io'; From d342b552885ca2253e09bd86ee7b7c4fc29fd336 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sun, 17 Mar 2019 20:29:36 -0700 Subject: [PATCH 8/9] Tests --- lib/src/client/client.dart | 2 +- lib/src/server/server.dart | 2 +- test/unit/parser_test.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index f14df1a..663c0db 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:json_api/document.dart'; +import 'package:json_api/parser.dart'; import 'package:json_api/src/client/response.dart'; import 'package:json_api/src/nullable.dart'; -import 'package:json_api/src/parser.dart'; typedef Document ResponseParser(Object j); diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index fe3c208..eedcc22 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:json_api/document.dart'; -import 'package:json_api/src/parser.dart'; +import 'package:json_api/parser.dart'; import 'package:json_api/src/server/contracts/controller.dart'; import 'package:json_api/src/server/contracts/document_builder.dart'; import 'package:json_api/src/server/contracts/page.dart'; diff --git a/test/unit/parser_test.dart b/test/unit/parser_test.dart index 9ca342d..acf0037 100644 --- a/test/unit/parser_test.dart +++ b/test/unit/parser_test.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:json_api/src/parser.dart'; +import 'package:json_api/parser.dart'; import 'package:json_matcher/json_matcher.dart'; import 'package:test/test.dart'; From 0a6ca45ab027f3b6ac587f893bc5c19d75f1aec9 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Sun, 17 Mar 2019 20:46:12 -0700 Subject: [PATCH 9/9] formatting --- lib/src/server/standard_document_builder.dart | 140 +++++++++--------- 1 file changed, 72 insertions(+), 68 deletions(-) diff --git a/lib/src/server/standard_document_builder.dart b/lib/src/server/standard_document_builder.dart index 44a9df4..5e6af06 100644 --- a/lib/src/server/standard_document_builder.dart +++ b/lib/src/server/standard_document_builder.dart @@ -12,91 +12,95 @@ class StandardDocumentBuilder implements DocumentBuilder { Document error(Iterable errors) => Document.error(errors); - Document collection(Iterable resource, - String type, Uri self, - {Page page, Iterable included}) => - Document(ResourceCollectionData(resource.map( - _ResourceObject), - self: Link(self), - pagination: page == null - ? Pagination.empty() - : Pagination.fromLinks(page.map((_) => - Link( - uriBuilder.collection(type, parameters: _.parameters)))))); + Document collection( + Iterable resource, String type, Uri self, + {Page page, Iterable included}) { + return Document(ResourceCollectionData(resource.map(_resourceObject), + self: Link(self), + pagination: page == null + ? Pagination.empty() + : Pagination.fromLinks(page.map((_) => + Link(uriBuilder.collection(type, parameters: _.parameters)))))); + } Document relatedCollection( - Iterable resource, String type, - String id, String relationship, Uri self, - {Page page, Iterable included}) => - Document(ResourceCollectionData(resource.map(_ResourceObject), - self: Link(self), - pagination: page == null - ? Pagination.empty() - : Pagination.fromLinks(page.map((_) => - Link( - uriBuilder.related( - type, id, relationship, parameters: _.parameters)))))); - - Document resource(Resource resource, String type, String id, + Iterable resource, + String type, + String id, + String relationship, Uri self, - {Iterable included}) => - Document( - ResourceData(_ResourceObject(resource), - self: Link(uriBuilder.resource(type, id)), - included: included?.map(_ResourceObject)), - ); + {Page page, + Iterable included}) { + final pagination = _pagination(page, type, id, relationship); + return Document(ResourceCollectionData(resource.map(_resourceObject), + self: Link(self), pagination: pagination)); + } - Document relatedResource(Resource resource, String type, - String id, - String relationship, Uri self, - {Iterable included}) => - Document( - ResourceData( - _ResourceObject(resource), included: included?.map(_ResourceObject), - self: Link(uriBuilder.related(type, id, relationship))), - ); + Document resource( + Resource resource, String type, String id, Uri self, + {Iterable included}) { + return Document( + ResourceData(_resourceObject(resource), + self: Link(uriBuilder.resource(type, id)), + included: included?.map(_resourceObject)), + ); + } + + Document relatedResource( + Resource resource, String type, String id, String relationship, Uri self, + {Iterable included}) { + return Document( + ResourceData(_resourceObject(resource), + included: included?.map(_resourceObject), + self: Link(uriBuilder.related(type, id, relationship))), + ); + } Document toMany(Iterable collection, String type, - String id, - String relationship, Uri self) => - Document(ToMany( - collection.map(_IdentifierObject), - self: Link(uriBuilder.relationship(type, id, relationship)), - related: Link(uriBuilder.related(type, id, relationship)))); + String id, String relationship, Uri self) { + return Document(ToMany(collection.map(_rdentifierObject), + self: Link(uriBuilder.relationship(type, id, relationship)), + related: Link(uriBuilder.related(type, id, relationship)))); + } Document toOne(Identifier identifier, String type, String id, - String relationship, Uri self) => - Document(ToOne( - nullable(_IdentifierObject)(identifier), - self: Link(uriBuilder.relationship(type, id, relationship)), - related: Link(uriBuilder.related(type, id, relationship)))); + String relationship, Uri self) { + return Document(ToOne(nullable(_rdentifierObject)(identifier), + self: Link(uriBuilder.relationship(type, id, relationship)), + related: Link(uriBuilder.related(type, id, relationship)))); + } Document meta(Map meta) => Document.empty(meta); - IdentifierObject _IdentifierObject(Identifier id) => + IdentifierObject _rdentifierObject(Identifier id) => IdentifierObject(id.type, id.id); - ResourceObject _ResourceObject(Resource resource) { - final relationships = - {}; - relationships.addAll(resource.toOne.map((k, v) => - MapEntry( - k, ToOne(nullable(_IdentifierObject)(v), self: Link( - uriBuilder.relationship(resource.type, resource.id, k)), - related: Link( - uriBuilder.related(resource.type, resource.id, k)))))); - relationships.addAll( - resource.toMany.map( - (k, v) => - MapEntry( - k, ToMany(v.map(_IdentifierObject), self: Link( - uriBuilder.relationship(resource.type, resource.id, k)), - related: Link( - uriBuilder.related(resource.type, resource.id, k)))))); + ResourceObject _resourceObject(Resource resource) { + final relationships = {}; + relationships.addAll(resource.toOne.map((k, v) => MapEntry( + k, + ToOne(nullable(_rdentifierObject)(v), + self: Link(uriBuilder.relationship(resource.type, resource.id, k)), + related: + Link(uriBuilder.related(resource.type, resource.id, k)))))); + relationships.addAll(resource.toMany.map((k, v) => MapEntry( + k, + ToMany(v.map(_rdentifierObject), + self: Link(uriBuilder.relationship(resource.type, resource.id, k)), + related: + Link(uriBuilder.related(resource.type, resource.id, k)))))); return ResourceObject(resource.type, resource.id, attributes: resource.attributes, relationships: relationships, self: Link(uriBuilder.resource(resource.type, resource.id))); } + + Pagination _pagination( + Page page, String type, String id, String relationship) { + return page == null + ? Pagination.empty() + : Pagination.fromLinks(page.map((_) => Link(uriBuilder + .related(type, id, relationship, parameters: _.parameters)))); + } }