From d575f78e3a5f16c925ffc4f5d77de0824dcfc43a Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Wed, 27 Feb 2019 22:08:44 -0800 Subject: [PATCH 1/3] WIP --- example/cars_server.dart | 2 + example/cars_server/controller.dart | 9 ++++ example/cars_server/dao.dart | 4 ++ lib/src/client/client.dart | 15 ++++++ lib/src/document/collection_document.dart | 2 +- lib/src/document/document.dart | 8 ++- lib/src/document/error_document.dart | 2 +- lib/src/document/meta_document.dart | 16 ++++++ lib/src/document/relationship.dart | 2 +- lib/src/document/resource_document.dart | 2 +- lib/src/server/json_api_controller.dart | 2 + lib/src/server/request.dart | 10 +++- lib/src/server/resource_controller.dart | 3 ++ lib/src/server/routing.dart | 20 +++++--- lib/src/server/server.dart | 24 +++++++-- lib/src/server/simple_server.dart | 13 ++--- test/functional/client_delete_test.dart | 61 +++++++++++++++++++++++ test/functional/client_fetch_test.dart | 2 +- 18 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 lib/src/document/meta_document.dart create mode 100644 test/functional/client_delete_test.dart diff --git a/example/cars_server.dart b/example/cars_server.dart index e187666..edb3381 100644 --- a/example/cars_server.dart +++ b/example/cars_server.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:json_api/src/document/document.dart'; +import 'package:json_api/src/server/request.dart'; import 'package:json_api/src/server/simple_server.dart'; import 'cars_server/controller.dart'; diff --git a/example/cars_server/controller.dart b/example/cars_server/controller.dart index 0116958..68da3c7 100644 --- a/example/cars_server/controller.dart +++ b/example/cars_server/controller.dart @@ -57,4 +57,13 @@ class CarsController implements ResourceController { dao[type].insert(obj); return dao[type].toResource(obj); } + + @override + Future deleteResource(String type, String id, Map params) { + if (dao[type].fetchById(id) == null) { + throw ResourceControllerException(404, detail: 'Resource not found'); + } + dao[type].deleteById(id); + return null; + } } diff --git a/example/cars_server/dao.dart b/example/cars_server/dao.dart index f8bdf67..225b914 100644 --- a/example/cars_server/dao.dart +++ b/example/cars_server/dao.dart @@ -18,6 +18,10 @@ abstract class DAO { Iterable fetchCollection({int offset = 0, int limit = 1}) => _collection.values.skip(offset).take(limit); + + void deleteById(String id) { + _collection.remove(id); + } } class ModelDAO extends DAO { diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index 4128e82..ccf0c3a 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -7,6 +7,7 @@ import 'package:json_api/src/client/status_code.dart'; import 'package:json_api/src/document/collection_document.dart'; import 'package:json_api/src/document/document.dart'; import 'package:json_api/src/document/error_document.dart'; +import 'package:json_api/src/document/meta_document.dart'; import 'package:json_api/src/document/relationship.dart'; import 'package:json_api/src/document/resource_document.dart'; import 'package:json_api/src/document/resource_object.dart'; @@ -65,6 +66,11 @@ class JsonApiClient { _post(ResourceDocument.fromJson, uri, ResourceDocument(ResourceObject.fromResource(resource)), headers); + /// Deletes the resource. + Future> deleteResource(Uri uri, + {Map headers}) => + _delete(MetaDocument.fromJson, uri, headers); + // /// Adds the [identifiers] to a to-many relationship identified by [uri] // Future> addToMany(Uri uri, Iterable identifiers, // {Map headers}) => @@ -89,6 +95,15 @@ class JsonApiClient { ..addAll(headers ?? {}) ..addAll({'Accept': contentType}))); + Future> _delete( + ResponseParser parse, uri, Map headers) => + _call( + parse, + (_) => _.delete(uri, + headers: {} + ..addAll(headers ?? {}) + ..addAll({'Accept': contentType}))); + Future> _post(ResponseParser parse, uri, Document document, Map headers) => _call( diff --git a/lib/src/document/collection_document.dart b/lib/src/document/collection_document.dart index f571f9b..665da7c 100644 --- a/lib/src/document/collection_document.dart +++ b/lib/src/document/collection_document.dart @@ -2,7 +2,7 @@ import 'package:json_api/src/document/document.dart'; import 'package:json_api/src/document/link.dart'; import 'package:json_api/src/document/resource_object.dart'; -class CollectionDocument implements Document { +class CollectionDocument extends Document { final List collection; final List included; diff --git a/lib/src/document/document.dart b/lib/src/document/document.dart index c5f2f85..8268200 100644 --- a/lib/src/document/document.dart +++ b/lib/src/document/document.dart @@ -1 +1,7 @@ -abstract class Document {} +class Document { + final meta = {}; + + Document({Map meta}) { + this.meta.addAll(meta ?? {}); + } +} diff --git a/lib/src/document/error_document.dart b/lib/src/document/error_document.dart index 8d55091..f1f0804 100644 --- a/lib/src/document/error_document.dart +++ b/lib/src/document/error_document.dart @@ -1,7 +1,7 @@ import 'package:json_api/src/document/document.dart'; import 'package:json_api/src/document/error_object.dart'; -class ErrorDocument implements Document { +class ErrorDocument extends Document { final errors = []; ErrorDocument(Iterable errors) { diff --git a/lib/src/document/meta_document.dart b/lib/src/document/meta_document.dart new file mode 100644 index 0000000..14b342c --- /dev/null +++ b/lib/src/document/meta_document.dart @@ -0,0 +1,16 @@ +import 'package:json_api/src/document/document.dart'; + +class MetaDocument extends Document { + MetaDocument(Map meta) : super(meta: meta) { + ArgumentError.checkNotNull(meta); + } + + static MetaDocument fromJson(Object json) { + if (json is Map) { + return MetaDocument(json['meta']); + } + throw 'Can not parse MetaDocument from $json'; + } + + toJson() => {'meta': meta}; +} diff --git a/lib/src/document/relationship.dart b/lib/src/document/relationship.dart index a901512..c52102b 100644 --- a/lib/src/document/relationship.dart +++ b/lib/src/document/relationship.dart @@ -11,7 +11,7 @@ import 'package:json_api/src/nullable.dart'; /// A relationship. Can be to-one or to-many. /// /// https://jsonapi.org/format/#document-resource-object-linkage -abstract class Relationship implements Document { +abstract class Relationship extends Document { final Link self; final Link related; diff --git a/lib/src/document/resource_document.dart b/lib/src/document/resource_document.dart index 78ea922..790d2db 100644 --- a/lib/src/document/resource_document.dart +++ b/lib/src/document/resource_document.dart @@ -2,7 +2,7 @@ import 'package:json_api/src/document/document.dart'; import 'package:json_api/src/document/link.dart'; import 'package:json_api/src/document/resource_object.dart'; -class ResourceDocument implements Document { +class ResourceDocument extends Document { final ResourceObject resourceObject; final List included; final Link self; diff --git a/lib/src/server/json_api_controller.dart b/lib/src/server/json_api_controller.dart index 5486389..c2bc3f2 100644 --- a/lib/src/server/json_api_controller.dart +++ b/lib/src/server/json_api_controller.dart @@ -14,4 +14,6 @@ abstract class JsonApiController { Future fetchRelated(RelatedRequest rq); Future createResource(CollectionRequest rq); + + Future deleteResource(ResourceRequest rq); } diff --git a/lib/src/server/request.dart b/lib/src/server/request.dart index 7af2b7b..8d8760c 100644 --- a/lib/src/server/request.dart +++ b/lib/src/server/request.dart @@ -8,6 +8,8 @@ abstract class JsonApiRequest { String get method; + Map get params; + Future fulfill(JsonApiController controller); } @@ -35,13 +37,16 @@ class ResourceRequest implements JsonApiRequest { final String body; final String type; final String id; + final Map params; - ResourceRequest(this.method, this.type, this.id, {this.body}); + ResourceRequest(this.method, this.type, this.id, {this.body, this.params}); Future fulfill(JsonApiController controller) async { switch (method.toUpperCase()) { case 'GET': return controller.fetchResource(this); + case 'DELETE': + return controller.deleteResource(this); // case 'PATCH': // return controller.updateResource(type, id, body); } @@ -69,9 +74,10 @@ class RelationshipRequest implements JsonApiRequest { final String type; final String id; final String relationship; + final Map params; RelationshipRequest(this.method, this.type, this.id, this.relationship, - {this.body}); + {this.body, this.params}); Future fulfill(JsonApiController controller) async { switch (method.toUpperCase()) { diff --git a/lib/src/server/resource_controller.dart b/lib/src/server/resource_controller.dart index 1303eb2..40053d3 100644 --- a/lib/src/server/resource_controller.dart +++ b/lib/src/server/resource_controller.dart @@ -22,6 +22,9 @@ abstract class ResourceController { Future createResource( String type, Resource resource, Map params); + Future deleteResource( + String type, String id, Map params); + // Future addToMany(Identifier id, String rel, Iterable ids); // Future updateResource(Identifier id, Resource resource); diff --git a/lib/src/server/routing.dart b/lib/src/server/routing.dart index 22ec66a..9b4e598 100644 --- a/lib/src/server/routing.dart +++ b/lib/src/server/routing.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:json_api/src/server/request.dart'; @@ -18,7 +20,7 @@ abstract class Routing { Uri relationship(String type, String id, String relationship); /// Resolves HTTP request to [JsonAiRequest] object - Future resolve(String method, Uri uri, String body); + Future resolve(HttpRequest httpRequest); } /// StandardRouting implements the recommended URL design schema: @@ -51,19 +53,21 @@ class StandardRouting implements Routing { resource(String type, String id) => base.replace(pathSegments: base.pathSegments + [type, id]); - Future resolve(String method, Uri uri, String body) async { - final seg = uri.pathSegments; + Future resolve(HttpRequest httpRequest) async { + final body = await httpRequest.transform(utf8.decoder).join(); + + final seg = httpRequest.uri.pathSegments; switch (seg.length) { case 1: - return CollectionRequest(method, seg[0], - body: body, params: uri.queryParameters); + return CollectionRequest(httpRequest.method, seg[0], + body: body, params: httpRequest.uri.queryParameters); case 2: - return ResourceRequest(method, seg[0], seg[1], body: body); + return ResourceRequest(httpRequest.method, seg[0], seg[1], body: body); case 3: - return RelatedRequest(method, seg[0], seg[1], seg[2]); + return RelatedRequest(httpRequest.method, seg[0], seg[1], seg[2]); case 4: if (seg[2] == 'relationships') { - return RelationshipRequest(method, seg[0], seg[1], seg[3], + return RelationshipRequest(httpRequest.method, seg[0], seg[1], seg[3], body: body); } } diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index 1791dd7..a726b80 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:json_api/src/document/collection_document.dart'; +import 'package:json_api/src/document/document.dart'; import 'package:json_api/src/document/error_document.dart'; import 'package:json_api/src/document/error_object.dart'; import 'package:json_api/src/document/identifier_object.dart'; @@ -18,14 +20,19 @@ import 'package:json_api/src/server/resource_controller.dart'; import 'package:json_api/src/server/response.dart'; import 'package:json_api/src/server/routing.dart'; +typedef void MetaDecorator(JsonApiRequest rq, Document doc); + class JsonApiServer implements JsonApiController { final ResourceController controller; final Routing routing; + final MetaDecorator meta; + + JsonApiServer(this.controller, this.routing, {MetaDecorator meta}) + : meta = meta ?? ((JsonApiRequest rq, Document doc) {}); - JsonApiServer(this.controller, this.routing); + Future handle(HttpRequest httpRequest) async { - Future handle(String method, Uri uri, String body) async { - final jsonApiRequest = await routing.resolve(method, uri, body); + final jsonApiRequest = await routing.resolve(httpRequest); if (jsonApiRequest == null) { return ServerResponse.notFound( ErrorDocument([ErrorObject(status: '404', detail: 'Unknown route')])); @@ -135,6 +142,17 @@ class JsonApiServer implements JsonApiController { } } + @override + Future deleteResource(ResourceRequest rq) async { + try { + await controller.deleteResource(rq.type, rq.id, rq.params); + return ServerResponse.noContent(); + } on ResourceControllerException catch (e) { + return ServerResponse(e.httpStatus, + ErrorDocument([ErrorObject.fromResourceControllerException(e)])); + } + } + // Future updateResource( // String type, String id, String body) async { // // TODO: check that [type] matcher [resource.type] diff --git a/lib/src/server/simple_server.dart b/lib/src/server/simple_server.dart index e612d08..649bd75 100644 --- a/lib/src/server/simple_server.dart +++ b/lib/src/server/simple_server.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:json_api/src/server/resource_controller.dart'; @@ -10,18 +9,20 @@ import 'package:json_api/src/server/server.dart'; class SimpleServer { HttpServer _httpServer; final ResourceController _controller; + final MetaDecorator meta; - SimpleServer(this._controller); + SimpleServer(this._controller, {this.meta}); Future start(InternetAddress address, int port) async { - final jsonApiServer = JsonApiServer(_controller, - StandardRouting(Uri.parse('http://${address.host}:$port'))); + final jsonApiServer = JsonApiServer( + _controller, StandardRouting(Uri.parse('http://${address.host}:$port')), + meta: meta); _httpServer = await HttpServer.bind(address, port); _httpServer.forEach((request) async { - final serverResponse = await jsonApiServer.handle(request.method, - request.uri, await request.transform(utf8.decoder).join()); + final serverResponse = await jsonApiServer.handle(request); + request.response.statusCode = serverResponse.status; serverResponse.headers.forEach(request.response.headers.set); request.response.headers.set('Access-Control-Allow-Origin', '*'); diff --git a/test/functional/client_delete_test.dart b/test/functional/client_delete_test.dart new file mode 100644 index 0000000..afe46a1 --- /dev/null +++ b/test/functional/client_delete_test.dart @@ -0,0 +1,61 @@ +@TestOn('vm') +import 'dart:io'; + +import 'package:json_api/client.dart'; +import 'package:json_api/src/server/simple_server.dart'; +import 'package:test/test.dart'; + +import '../../example/cars_server.dart'; + +void main() async { + final client = JsonApiClient(); + SimpleServer s; + setUp(() async { + s = createServer(); + return await s.start(InternetAddress.loopbackIPv4, 8080); + }); + + tearDown(() => s.stop()); + + group('resource', () { + /// https://jsonapi.org/format/#crud-deleting-responses-204 + /// + /// A server MUST return a 204 No Content status code if a deletion request + /// is successful and no content is returned. + test('204 No Content', () async { + final r0 = await client.deleteResource(Url.resource('models', '1')); + + expect(r0.status, 204); + expect(r0.isSuccessful, true); + expect(r0.document, isNull); + + // Make sure the resource is not available anymore + final r1 = await client.fetchResource(Url.resource('models', '1')); + expect(r1.status, 404); + }); + +// test('200 OK', () async { +// // Json-Api-Options header is not a part of the standard! +// final r0 = await client.deleteResource(Url.resource('models', '1'), +// headers: {'Json-Api-Options': 'meta'}); +// +// expect(r0.status, 200); +// expect(r0.isSuccessful, true); +// expect(r0.document.meta['Server'], +// 'Dart JSON:API Server. https://pub.dartlang.org/packages/json_api'); +// +// // Make sure the resource is not available anymore +// final r1 = await client.fetchResource(Url.resource('models', '1')); +// expect(r1.status, 404); +// }); + + /// https://jsonapi.org/format/#crud-deleting-responses-404 + /// + /// A server SHOULD return a 404 Not Found status code if a deletion request + /// fails due to the resource not existing. + test('404 Not Found', () async { + final r0 = await client.fetchResource(Url.resource('models', '555')); + expect(r0.status, 404); + }); + }); +} diff --git a/test/functional/client_fetch_test.dart b/test/functional/client_fetch_test.dart index d7f031f..6fae635 100644 --- a/test/functional/client_fetch_test.dart +++ b/test/functional/client_fetch_test.dart @@ -2,8 +2,8 @@ import 'dart:io'; import 'package:json_api/client.dart'; -import 'package:json_api/src/server/simple_server.dart'; import 'package:json_api/src/document/relationship.dart'; +import 'package:json_api/src/server/simple_server.dart'; import 'package:test/test.dart'; import '../../example/cars_server.dart'; From 5fcd3174cbf2d5a7811730ecc83a1e569e2e1b59 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Thu, 28 Feb 2019 23:28:02 -0800 Subject: [PATCH 2/3] WIP --- example/cars_server/controller.dart | 12 +- lib/src/server/json_api_controller.dart | 18 ++- lib/src/server/request.dart | 97 +++------------ lib/src/server/resource_controller.dart | 11 +- lib/src/server/router.dart | 155 ++++++++++++++++++++++++ lib/src/server/routing.dart | 78 ------------ lib/src/server/server.dart | 126 ++++++++----------- lib/src/server/simple_server.dart | 6 +- test/functional/client_delete_test.dart | 28 ++--- 9 files changed, 268 insertions(+), 263 deletions(-) create mode 100644 lib/src/server/router.dart delete mode 100644 lib/src/server/routing.dart diff --git a/example/cars_server/controller.dart b/example/cars_server/controller.dart index 68da3c7..6449828 100644 --- a/example/cars_server/controller.dart +++ b/example/cars_server/controller.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:json_api/src/identifier.dart'; import 'package:json_api/src/resource.dart'; import 'package:json_api/src/server/numbered_page.dart'; +import 'package:json_api/src/server/request.dart'; import 'package:json_api/src/server/resource_controller.dart'; import 'package:uuid/uuid.dart'; @@ -17,9 +18,9 @@ class CarsController implements ResourceController { bool supports(String type) => dao.containsKey(type); Future> fetchCollection( - String type, Map params) async { - final page = - NumberedPage.fromQueryParameters(params, total: dao[type].length); + String type, JsonApiHttpRequest request) async { + final page = NumberedPage.fromQueryParameters(request.uri.queryParameters, + total: dao[type].length); return Collection( dao[type] .fetchCollection(offset: page.number - 1) @@ -40,7 +41,7 @@ class CarsController implements ResourceController { @override Future createResource( - String type, Resource resource, Map params) async { + String type, Resource resource, JsonApiHttpRequest request) async { if (type != resource.type) { throw ResourceControllerException(409, detail: 'Incompatible type'); } @@ -59,7 +60,8 @@ class CarsController implements ResourceController { } @override - Future deleteResource(String type, String id, Map params) { + Future deleteResource( + String type, String id, JsonApiHttpRequest request) { if (dao[type].fetchById(id) == null) { throw ResourceControllerException(404, detail: 'Resource not found'); } diff --git a/lib/src/server/json_api_controller.dart b/lib/src/server/json_api_controller.dart index c2bc3f2..b5a63ff 100644 --- a/lib/src/server/json_api_controller.dart +++ b/lib/src/server/json_api_controller.dart @@ -5,15 +5,21 @@ import 'package:json_api/src/server/response.dart'; /// JSON:API Controller abstract class JsonApiController { - Future fetchCollection(CollectionRequest rq); + Future fetchCollection( + String type, JsonApiHttpRequest request); - Future fetchResource(ResourceRequest rq); + Future fetchResource( + String type, String id, JsonApiHttpRequest request); - Future fetchRelationship(RelationshipRequest rq); + Future fetchRelationship( + String type, String id, String relationship, JsonApiHttpRequest request); - Future fetchRelated(RelatedRequest rq); + Future fetchRelated( + String type, String id, String relationship, JsonApiHttpRequest request); - Future createResource(CollectionRequest rq); + Future createResource( + String type, JsonApiHttpRequest request); - Future deleteResource(ResourceRequest rq); + Future deleteResource( + String type, String id, JsonApiHttpRequest request); } diff --git a/lib/src/server/request.dart b/lib/src/server/request.dart index 8d8760c..6096256 100644 --- a/lib/src/server/request.dart +++ b/lib/src/server/request.dart @@ -1,91 +1,34 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; -import 'package:json_api/src/server/json_api_controller.dart'; -import 'package:json_api/src/server/response.dart'; +enum HttpMethod { get, post, put, delete } -abstract class JsonApiRequest { - String get type; +abstract class JsonApiHttpRequest { + HttpMethod get method; - String get method; + Uri get uri; - Map get params; + Future body(); - Future fulfill(JsonApiController controller); + List headers(String key); } -class CollectionRequest implements JsonApiRequest { - final String method; - final String body; - final String type; - final Map params; +class NativeHttpRequestAdapter implements JsonApiHttpRequest { + final HttpRequest request; - CollectionRequest(this.method, this.type, {this.body, this.params}); + NativeHttpRequestAdapter(this.request); - Future fulfill(JsonApiController controller) async { - switch (method.toUpperCase()) { - case 'GET': - return controller.fetchCollection(this); - case 'POST': - return controller.createResource(this); - } - return ServerResponse(405); // TODO: meaningful error - } -} - -class ResourceRequest implements JsonApiRequest { - final String method; - final String body; - final String type; - final String id; - final Map params; - - ResourceRequest(this.method, this.type, this.id, {this.body, this.params}); - - Future fulfill(JsonApiController controller) async { - switch (method.toUpperCase()) { - case 'GET': - return controller.fetchResource(this); - case 'DELETE': - return controller.deleteResource(this); -// case 'PATCH': -// return controller.updateResource(type, id, body); - } - return ServerResponse(405); // TODO: meaningful error - } -} - -class RelatedRequest implements JsonApiRequest { - final String method; - final String type; - final String id; - final String relationship; - final Map params; - - RelatedRequest(this.method, this.type, this.id, this.relationship, - {this.params}); - - Future fulfill(JsonApiController controller) => - controller.fetchRelated(this); -} + HttpMethod get method => { + 'get': HttpMethod.get, + 'post': HttpMethod.post, + 'delete': HttpMethod.delete, + 'put': HttpMethod.put + }[request.method.toLowerCase()]; -class RelationshipRequest implements JsonApiRequest { - final String method; - final String body; - final String type; - final String id; - final String relationship; - final Map params; + Uri get uri => request.uri; - RelationshipRequest(this.method, this.type, this.id, this.relationship, - {this.body, this.params}); + List headers(String key) => request.headers[key]; - Future fulfill(JsonApiController controller) async { - switch (method.toUpperCase()) { - case 'GET': - return controller.fetchRelationship(this); -// case 'POST': -// return controller.addToMany(type, id, relationship, body); - } - return ServerResponse(405); // TODO: meaningful error - } + Future body() => request.transform(utf8.decoder).join(); } diff --git a/lib/src/server/resource_controller.dart b/lib/src/server/resource_controller.dart index 40053d3..5edf472 100644 --- a/lib/src/server/resource_controller.dart +++ b/lib/src/server/resource_controller.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:json_api/src/identifier.dart'; import 'package:json_api/src/resource.dart'; import 'package:json_api/src/server/page.dart'; +import 'package:json_api/src/server/request.dart'; class Collection { Iterable elements; @@ -15,19 +16,15 @@ abstract class ResourceController { bool supports(String type); Future> fetchCollection( - String type, Map params); + String type, JsonApiHttpRequest request); Stream fetchResources(Iterable ids); Future createResource( - String type, Resource resource, Map params); + String type, Resource resource, JsonApiHttpRequest request); Future deleteResource( - String type, String id, Map params); - -// Future addToMany(Identifier id, String rel, Iterable ids); - -// Future updateResource(Identifier id, Resource resource); + String type, String id, JsonApiHttpRequest request); } class ResourceControllerException implements Exception { diff --git a/lib/src/server/router.dart b/lib/src/server/router.dart new file mode 100644 index 0000000..bbf4245 --- /dev/null +++ b/lib/src/server/router.dart @@ -0,0 +1,155 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:json_api/src/server/json_api_controller.dart'; +import 'package:json_api/src/server/request.dart'; +import 'package:json_api/src/server/response.dart'; + +/// Routing defines the design of URLs. +abstract class Router { + /// 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); + + /// Builds a URI for a related resource + Uri related(String type, String id, String relationship); + + /// Builds a URI for a relationship object + Uri relationship(String type, String id, String relationship); + + /// Resolves HTTP request to [JsonAiRequest] object + Future resolve(JsonApiHttpRequest httpRequest); +} + +/// 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 Router { + final Uri base; + + StandardRouting(this.base) { + ArgumentError.checkNotNull(base, 'base'); + } + + collection(String type, {Map params}) => base.replace( + pathSegments: base.pathSegments + [type], + queryParameters: + _nonEmpty(CombinedMapView([base.queryParameters, params ?? {}]))); + + related(String type, String id, String relationship) => + base.replace(pathSegments: base.pathSegments + [type, id, relationship]); + + relationship(String type, String id, String relationship) => base.replace( + pathSegments: + base.pathSegments + [type, id, 'relationships', relationship]); + + resource(String type, String id) => + base.replace(pathSegments: base.pathSegments + [type, id]); + + Future resolve(JsonApiHttpRequest httpRequest) async { + final seg = httpRequest.uri.pathSegments; + switch (seg.length) { + case 1: + return CollectionRoute(seg[0]); + case 2: + return ResourceRoute(seg[0], seg[1]); + case 3: + return RelatedRoute(seg[0], seg[1], seg[2]); + case 4: + if (seg[2] == 'relationships') { + return RelationshipRoute(seg[0], seg[1], seg[3]); + } + } + return null; // TODO: replace with a null-object + } + + Map _nonEmpty(Map map) => map.isEmpty ? null : map; +} + + +abstract class JsonApiRoute { + String get type; + + Future call( + JsonApiController controller, JsonApiHttpRequest request); +} + +class CollectionRoute implements JsonApiRoute { + final String type; + + CollectionRoute(this.type); + + Future call( + JsonApiController controller, JsonApiHttpRequest request) { + switch (request.method) { + case HttpMethod.get: + return controller.fetchCollection(type, request); + case HttpMethod.post: + return controller.createResource(type, request); + default: + return Future.value(ServerResponse(405)); // TODO: meaningful error + } + } +} + +class ResourceRoute implements JsonApiRoute { + final String type; + final String id; + + ResourceRoute(this.type, this.id); + + Future call( + JsonApiController controller, JsonApiHttpRequest request) { + switch (request.method) { + case HttpMethod.get: + return controller.fetchResource(type, id, request); + case HttpMethod.delete: + return controller.deleteResource(type, id, request); + default: + return Future.value(ServerResponse(405)); // TODO: meaningful error + } + } +} + +class RelatedRoute implements JsonApiRoute { + final String type; + final String id; + final String relationship; + + RelatedRoute(this.type, this.id, this.relationship); + + Future call( + JsonApiController controller, JsonApiHttpRequest request) { + switch (request.method) { + case HttpMethod.get: + return controller.fetchRelated(type, id, relationship, request); + default: + return Future.value(ServerResponse(405)); // TODO: meaningful error + } + } +} + +class RelationshipRoute implements JsonApiRoute { + final String type; + final String id; + final String relationship; + + RelationshipRoute(this.type, this.id, this.relationship); + + Future call( + JsonApiController controller, JsonApiHttpRequest request) { + switch (request.method) { + case HttpMethod.get: + return controller.fetchRelationship(type, id, relationship, request); + default: + return Future.value(ServerResponse(405)); // TODO: meaningful error + } + } +} diff --git a/lib/src/server/routing.dart b/lib/src/server/routing.dart deleted file mode 100644 index 9b4e598..0000000 --- a/lib/src/server/routing.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:json_api/src/server/request.dart'; - -/// Routing defines the design of URLs. -abstract class Routing { - /// 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); - - /// Builds a URI for a related resource - Uri related(String type, String id, String relationship); - - /// Builds a URI for a relationship object - Uri relationship(String type, String id, String relationship); - - /// Resolves HTTP request to [JsonAiRequest] object - Future resolve(HttpRequest httpRequest); -} - -/// 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}) => base.replace( - pathSegments: base.pathSegments + [type], - queryParameters: - _nonEmpty(CombinedMapView([base.queryParameters, params ?? {}]))); - - related(String type, String id, String relationship) => - base.replace(pathSegments: base.pathSegments + [type, id, relationship]); - - relationship(String type, String id, String relationship) => base.replace( - pathSegments: - base.pathSegments + [type, id, 'relationships', relationship]); - - resource(String type, String id) => - base.replace(pathSegments: base.pathSegments + [type, id]); - - Future resolve(HttpRequest httpRequest) async { - final body = await httpRequest.transform(utf8.decoder).join(); - - final seg = httpRequest.uri.pathSegments; - switch (seg.length) { - case 1: - return CollectionRequest(httpRequest.method, seg[0], - body: body, params: httpRequest.uri.queryParameters); - case 2: - return ResourceRequest(httpRequest.method, seg[0], seg[1], body: body); - case 3: - return RelatedRequest(httpRequest.method, seg[0], seg[1], seg[2]); - case 4: - if (seg[2] == 'relationships') { - return RelationshipRequest(httpRequest.method, seg[0], seg[1], seg[3], - body: body); - } - } - return null; // TODO: replace with a null-object - } - - Map _nonEmpty(Map map) => map.isEmpty ? null : map; -} diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index a726b80..4ab0ca0 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -18,49 +18,52 @@ import 'package:json_api/src/server/json_api_controller.dart'; import 'package:json_api/src/server/request.dart'; import 'package:json_api/src/server/resource_controller.dart'; import 'package:json_api/src/server/response.dart'; -import 'package:json_api/src/server/routing.dart'; +import 'package:json_api/src/server/router.dart'; -typedef void MetaDecorator(JsonApiRequest rq, Document doc); +typedef void MetaDecorator( + JsonApiRoute route, JsonApiHttpRequest request, Document doc); class JsonApiServer implements JsonApiController { final ResourceController controller; - final Routing routing; + final Router router; final MetaDecorator meta; - JsonApiServer(this.controller, this.routing, {MetaDecorator meta}) - : meta = meta ?? ((JsonApiRequest rq, Document doc) {}); + JsonApiServer(this.controller, this.router, {MetaDecorator meta}) + : meta = meta ?? + ((JsonApiRoute route, JsonApiHttpRequest request, Document doc) {}); - Future handle(HttpRequest httpRequest) async { - - final jsonApiRequest = await routing.resolve(httpRequest); - if (jsonApiRequest == null) { + Future handle(JsonApiHttpRequest request) async { + final route = await router.resolve(request); + if (route == null) { return ServerResponse.notFound( ErrorDocument([ErrorObject(status: '404', detail: 'Unknown route')])); } - if (!controller.supports(jsonApiRequest.type)) { + if (!controller.supports(route.type)) { return ServerResponse.notFound(ErrorDocument( [ErrorObject(status: '404', detail: 'Unknown resource type')])); } - return jsonApiRequest.fulfill(this); + return route.call(this, request); } - Future fetchCollection(CollectionRequest rq) async { - final collection = await controller.fetchCollection(rq.type, rq.params); + Future fetchCollection( + String type, JsonApiHttpRequest request) async { + final collection = await controller.fetchCollection(type, request); - final pagination = Pagination.fromMap(collection.page.mapPages( - (_) => Link(routing.collection(rq.type, params: _?.parameters)))); + final pagination = Pagination.fromMap(collection.page + .mapPages((_) => Link(router.collection(type, params: _?.parameters)))); final doc = CollectionDocument( collection.elements.map(ResourceObject.fromResource), - self: Link( - routing.collection(rq.type, params: collection.page?.parameters)), + self: + Link(router.collection(type, params: collection.page?.parameters)), pagination: pagination); return ServerResponse.ok(doc); } - Future fetchResource(ResourceRequest rq) async { + Future fetchResource( + String type, String id, JsonApiHttpRequest request) async { try { - final res = await _resource(rq.type, rq.id); + final res = await _resource(type, id); return ServerResponse.ok( ResourceDocument(nullable(ResourceObject.fromResource)(res))); } on ResourceControllerException catch (e) { @@ -69,21 +72,21 @@ class JsonApiServer implements JsonApiController { } } - Future fetchRelated(RelatedRequest rq) async { + Future fetchRelated(String type, String id, + String relationship, JsonApiHttpRequest request) async { try { - final res = - await controller.fetchResources([Identifier(rq.type, rq.id)]).first; + final res = await controller.fetchResources([Identifier(type, id)]).first; - if (res.toOne.containsKey(rq.relationship)) { - final id = res.toOne[rq.relationship]; + if (res.toOne.containsKey(relationship)) { + final id = res.toOne[relationship]; // TODO check if id == null final related = await controller.fetchResources([id]).first; return ServerResponse.ok( ResourceDocument(ResourceObject.fromResource(related))); } - if (res.toMany.containsKey(rq.relationship)) { - final ids = res.toMany[rq.relationship]; + if (res.toMany.containsKey(relationship)) { + final ids = res.toMany[relationship]; final related = await controller.fetchResources(ids).toList(); return ServerResponse.ok( CollectionDocument(related.map(ResourceObject.fromResource))); @@ -96,21 +99,21 @@ class JsonApiServer implements JsonApiController { } } - Future fetchRelationship(RelationshipRequest rq) async { + Future fetchRelationship(String type, String id, + String relationship, JsonApiHttpRequest request) async { try { - final res = await _resource(rq.type, rq.id); - if (res.toOne.containsKey(rq.relationship)) { + final res = await _resource(type, id); + if (res.toOne.containsKey(relationship)) { return ServerResponse.ok(ToOne( - nullable(IdentifierObject.fromIdentifier)( - res.toOne[rq.relationship]), - self: Link(routing.relationship(res.type, res.id, rq.relationship)), - related: Link(routing.related(res.type, res.id, rq.relationship)))); + nullable(IdentifierObject.fromIdentifier)(res.toOne[relationship]), + self: Link(router.relationship(res.type, res.id, relationship)), + related: Link(router.related(res.type, res.id, relationship)))); } - if (res.toMany.containsKey(rq.relationship)) { + if (res.toMany.containsKey(relationship)) { return ServerResponse.ok(ToMany( - res.toMany[rq.relationship].map(IdentifierObject.fromIdentifier), - self: Link(routing.relationship(res.type, res.id, rq.relationship)), - related: Link(routing.related(res.type, res.id, rq.relationship)))); + res.toMany[relationship].map(IdentifierObject.fromIdentifier), + self: Link(router.relationship(res.type, res.id, relationship)), + related: Link(router.related(res.type, res.id, relationship)))); } return ServerResponse(404); } on ResourceControllerException catch (e) { @@ -119,20 +122,22 @@ class JsonApiServer implements JsonApiController { } } - Future createResource(CollectionRequest rq) async { + Future createResource( + String type, JsonApiHttpRequest request) async { try { - final requestedResource = ResourceDocument.fromJson(json.decode(rq.body)) - .resourceObject - .toResource(); - final createdResource = await controller.createResource( - rq.type, requestedResource, rq.params); + final requestedResource = + ResourceDocument.fromJson(json.decode(await request.body())) + .resourceObject + .toResource(); + final createdResource = + await controller.createResource(type, requestedResource, request); if (requestedResource.hasId) { return ServerResponse.noContent(); } else { return ServerResponse.created( ResourceDocument(ResourceObject.fromResource(createdResource))) - ..headers['Location'] = routing + ..headers['Location'] = router .resource(createdResource.type, createdResource.id) .toString(); } @@ -142,10 +147,10 @@ class JsonApiServer implements JsonApiController { } } - @override - Future deleteResource(ResourceRequest rq) async { + Future deleteResource( + String type, String id, JsonApiHttpRequest request) async { try { - await controller.deleteResource(rq.type, rq.id, rq.params); + await controller.deleteResource(type, id, request); return ServerResponse.noContent(); } on ResourceControllerException catch (e) { return ServerResponse(e.httpStatus, @@ -153,33 +158,6 @@ class JsonApiServer implements JsonApiController { } } -// Future updateResource( -// String type, String id, String body) async { -// // TODO: check that [type] matcher [resource.type] -// final doc = ResourceDocument.fromJson(json.decode(body)); -// await controller.updateResource( -// Identifier(type, id), doc.resourceEnvelope.toResource()); -// return ServerResponse(204); -// } -// -// Future addToMany( -// String type, String id, String relationship, String body) async { -// final rel = Relationship.fromJson(json.decode(body)); -// if (rel is ToMany) { -// await controller.addToMany( -// Identifier(type, id), relationship, rel.identifiers); -// final res = await _resource(type, id); -// return ServerResponse.ok(ToMany( -// res.toMany[relationship] -// .map(IdentifierEnvelope.fromIdentifier) -// .toList(), -// self: Link(routing.relationship(res.type, res.id, relationship)), -// related: Link(routing.related(res.type, res.id, relationship)))); -// } -// // TODO: Return a meaningful response -// return ServerResponse.notFound(); -// } - Future _resource(String type, String id) => controller.fetchResources([Identifier(type, id)]).first; } diff --git a/lib/src/server/simple_server.dart b/lib/src/server/simple_server.dart index 649bd75..5f298ca 100644 --- a/lib/src/server/simple_server.dart +++ b/lib/src/server/simple_server.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'dart:io'; +import 'package:json_api/src/server/request.dart'; import 'package:json_api/src/server/resource_controller.dart'; -import 'package:json_api/src/server/routing.dart'; +import 'package:json_api/src/server/router.dart'; import 'package:json_api/src/server/server.dart'; /// A simple JSON:API cars_server ot top of Dart's [HttpServer] @@ -21,7 +22,8 @@ class SimpleServer { _httpServer = await HttpServer.bind(address, port); _httpServer.forEach((request) async { - final serverResponse = await jsonApiServer.handle(request); + final serverResponse = + await jsonApiServer.handle(NativeHttpRequestAdapter(request)); request.response.statusCode = serverResponse.status; serverResponse.headers.forEach(request.response.headers.set); diff --git a/test/functional/client_delete_test.dart b/test/functional/client_delete_test.dart index afe46a1..7822b75 100644 --- a/test/functional/client_delete_test.dart +++ b/test/functional/client_delete_test.dart @@ -34,20 +34,20 @@ void main() async { expect(r1.status, 404); }); -// test('200 OK', () async { -// // Json-Api-Options header is not a part of the standard! -// final r0 = await client.deleteResource(Url.resource('models', '1'), -// headers: {'Json-Api-Options': 'meta'}); -// -// expect(r0.status, 200); -// expect(r0.isSuccessful, true); -// expect(r0.document.meta['Server'], -// 'Dart JSON:API Server. https://pub.dartlang.org/packages/json_api'); -// -// // Make sure the resource is not available anymore -// final r1 = await client.fetchResource(Url.resource('models', '1')); -// expect(r1.status, 404); -// }); + test('200 OK', () async { + // Json-Api-Options header is not a part of the standard! + final r0 = await client.deleteResource(Url.resource('models', '1'), + headers: {'Json-Api-Options': 'meta'}); + + expect(r0.status, 200); + expect(r0.isSuccessful, true); + expect(r0.document.meta['Server'], + 'Dart JSON:API Server. https://pub.dartlang.org/packages/json_api'); + + // Make sure the resource is not available anymore + final r1 = await client.fetchResource(Url.resource('models', '1')); + expect(r1.status, 404); + }); /// https://jsonapi.org/format/#crud-deleting-responses-404 /// From a5672bbb00d13390c260d603d3bc50ed4742599e Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Fri, 1 Mar 2019 14:26:01 -0800 Subject: [PATCH 3/3] Resource deletion --- CHANGELOG.md | 1 + README.md | 15 ++++++------- example/cars_server.dart | 2 -- example/cars_server/controller.dart | 13 +++++++----- example/cars_server/dao.dart | 17 ++++++++++++--- lib/client.dart | 3 +-- lib/src/client/client.dart | 2 +- lib/src/document.dart | 12 +++++++++++ lib/src/{ => document}/identifier.dart | 0 lib/src/document/identifier_object.dart | 2 +- lib/src/document/relationship.dart | 2 +- lib/src/{ => document}/resource.dart | 2 +- lib/src/document/resource_object.dart | 4 ++-- lib/src/server/resource_controller.dart | 15 ++++++++++--- lib/src/server/router.dart | 1 - lib/src/server/server.dart | 28 +++++++------------------ lib/src/server/simple_server.dart | 8 +++---- test/functional/client_create_test.dart | 16 +++++++------- test/functional/client_delete_test.dart | 17 ++++++++------- 19 files changed, 90 insertions(+), 70 deletions(-) create mode 100644 lib/src/document.dart rename lib/src/{ => document}/identifier.dart (100%) rename lib/src/{ => document}/resource.dart (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5beac9a..4e76f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Improved ResourceController error handling - Resource creation +- Resource deletion ## 0.1.0 - 2019-02-27 ### Added diff --git a/README.md b/README.md index 91e9fdf..7ac7e43 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,27 @@ # Implementation of [JSON:API v1.0](http://jsonapi.org) in Dart -#### Feature roadmap -##### Client +### Feature roadmap +#### Client - [x] Fetching single resources and resource collections - [x] Fetching relationships and related resources and collections - [x] Fetching single resources - [x] Creating resources +- [x] Deleting resources - [ ] Updating resource's attributes - [ ] Updating resource's relationships - [ ] Updating relationships -- [ ] Deleting resources - [ ] Asynchronous processing - [ ] Optional check for `Content-Type` header in incoming responses -##### Server (The Server API is not stable yet!) +#### Server (The Server API is not stable yet!) - [x] Fetching single resources and resource collections - [x] Fetching relationships and related resources and collections - [x] Fetching single resources - [x] Creating resources +- [x] Deleting resources - [ ] Updating resource's attributes - [ ] Updating resource's relationships - [ ] Updating relationships -- [ ] Deleting resources - [ ] Inclusion of related resources - [ ] Sparse fieldsets - [ ] Sorting, pagination, filtering @@ -29,8 +29,9 @@ - [ ] Optional check for `Content-Type` header in incoming requests - [ ] Support annotations in resource mappers (?) -##### Document -- [ ] Support `meta` and `jsonapi` members +#### Document (The Document API is not stable yet!) +- [ ] Support `meta` members +- [ ] Support `jsonapi` members - [ ] Structure Validation including compound documents - [ ] Support relationship objects lacking the `data` member - [ ] Naming Validation diff --git a/example/cars_server.dart b/example/cars_server.dart index edb3381..e187666 100644 --- a/example/cars_server.dart +++ b/example/cars_server.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:json_api/src/document/document.dart'; -import 'package:json_api/src/server/request.dart'; import 'package:json_api/src/server/simple_server.dart'; import 'cars_server/controller.dart'; diff --git a/example/cars_server/controller.dart b/example/cars_server/controller.dart index 6449828..b3c285a 100644 --- a/example/cars_server/controller.dart +++ b/example/cars_server/controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:json_api/src/identifier.dart'; -import 'package:json_api/src/resource.dart'; +import 'package:json_api/src/document/identifier.dart'; +import 'package:json_api/src/document/resource.dart'; import 'package:json_api/src/server/numbered_page.dart'; import 'package:json_api/src/server/request.dart'; import 'package:json_api/src/server/resource_controller.dart'; @@ -60,12 +60,15 @@ class CarsController implements ResourceController { } @override - Future deleteResource( - String type, String id, JsonApiHttpRequest request) { + Future> deleteResource( + String type, String id, JsonApiHttpRequest request) async { if (dao[type].fetchById(id) == null) { throw ResourceControllerException(404, detail: 'Resource not found'); } - dao[type].deleteById(id); + final deps = dao[type].deleteById(id); + if (deps > 0) { + return {'deps': deps}; + } return null; } } diff --git a/example/cars_server/dao.dart b/example/cars_server/dao.dart index 225b914..0b6388d 100644 --- a/example/cars_server/dao.dart +++ b/example/cars_server/dao.dart @@ -1,5 +1,5 @@ -import 'package:json_api/src/identifier.dart'; -import 'package:json_api/src/resource.dart'; +import 'package:json_api/src/document/identifier.dart'; +import 'package:json_api/src/document/resource.dart'; import 'model.dart'; @@ -19,8 +19,10 @@ abstract class DAO { Iterable fetchCollection({int offset = 0, int limit = 1}) => _collection.values.skip(offset).take(limit); - void deleteById(String id) { + /// Returns the number of depending objects the entity had + int deleteById(String id) { _collection.remove(id); + return 0; } } @@ -63,4 +65,13 @@ class CompanyDAO extends DAO { Company create(Resource r) { return Company(r.id, r.attributes['name']); } + + @override + int deleteById(String id) { + final company = fetchById(id); + int deps = company.headquarters == null ? 0 : 1; + deps += company.models.length; + _collection.remove(id); + return deps; + } } diff --git a/lib/client.dart b/lib/client.dart index 3d3e8f5..15eed33 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -1,3 +1,2 @@ export 'package:json_api/src/client/client.dart'; -export 'package:json_api/src/identifier.dart'; -export 'package:json_api/src/resource.dart'; +export 'package:json_api/src/document.dart'; diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index ccf0c3a..88daf82 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -9,10 +9,10 @@ import 'package:json_api/src/document/document.dart'; import 'package:json_api/src/document/error_document.dart'; import 'package:json_api/src/document/meta_document.dart'; import 'package:json_api/src/document/relationship.dart'; +import 'package:json_api/src/document/resource.dart'; import 'package:json_api/src/document/resource_document.dart'; import 'package:json_api/src/document/resource_object.dart'; import 'package:json_api/src/nullable.dart'; -import 'package:json_api/src/resource.dart'; typedef D ResponseParser(Object j); diff --git a/lib/src/document.dart b/lib/src/document.dart new file mode 100644 index 0000000..108a14c --- /dev/null +++ b/lib/src/document.dart @@ -0,0 +1,12 @@ +export 'package:json_api/src/document/collection_document.dart'; +export 'package:json_api/src/document/document.dart'; +export 'package:json_api/src/document/error_document.dart'; +export 'package:json_api/src/document/error_object.dart'; +export 'package:json_api/src/document/identifier.dart'; +export 'package:json_api/src/document/identifier_object.dart'; +export 'package:json_api/src/document/link.dart'; +export 'package:json_api/src/document/meta_document.dart'; +export 'package:json_api/src/document/relationship.dart'; +export 'package:json_api/src/document/resource.dart'; +export 'package:json_api/src/document/resource_document.dart'; +export 'package:json_api/src/document/resource_object.dart'; diff --git a/lib/src/identifier.dart b/lib/src/document/identifier.dart similarity index 100% rename from lib/src/identifier.dart rename to lib/src/document/identifier.dart diff --git a/lib/src/document/identifier_object.dart b/lib/src/document/identifier_object.dart index 5fe49a2..f30c0f6 100644 --- a/lib/src/document/identifier_object.dart +++ b/lib/src/document/identifier_object.dart @@ -1,4 +1,4 @@ -import 'package:json_api/src/identifier.dart'; +import 'package:json_api/src/document/identifier.dart'; class IdentifierObject { final String type; diff --git a/lib/src/document/relationship.dart b/lib/src/document/relationship.dart index c52102b..a4ee855 100644 --- a/lib/src/document/relationship.dart +++ b/lib/src/document/relationship.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:json_api/src/client/client.dart'; import 'package:json_api/src/document/document.dart'; +import 'package:json_api/src/document/identifier.dart'; import 'package:json_api/src/document/identifier_object.dart'; import 'package:json_api/src/document/link.dart'; import 'package:json_api/src/document/resource_object.dart'; -import 'package:json_api/src/identifier.dart'; import 'package:json_api/src/nullable.dart'; /// A relationship. Can be to-one or to-many. diff --git a/lib/src/resource.dart b/lib/src/document/resource.dart similarity index 96% rename from lib/src/resource.dart rename to lib/src/document/resource.dart index 29a2691..58e3787 100644 --- a/lib/src/resource.dart +++ b/lib/src/document/resource.dart @@ -1,4 +1,4 @@ -import 'package:json_api/src/identifier.dart'; +import 'package:json_api/src/document/identifier.dart'; /// The core of the Resource object /// https://jsonapi.org/format/#document-resource-objects diff --git a/lib/src/document/resource_object.dart b/lib/src/document/resource_object.dart index 5b0f0d1..93da849 100644 --- a/lib/src/document/resource_object.dart +++ b/lib/src/document/resource_object.dart @@ -1,9 +1,9 @@ +import 'package:json_api/src/document/identifier.dart'; import 'package:json_api/src/document/identifier_object.dart'; import 'package:json_api/src/document/link.dart'; import 'package:json_api/src/document/relationship.dart'; -import 'package:json_api/src/identifier.dart'; +import 'package:json_api/src/document/resource.dart'; import 'package:json_api/src/nullable.dart'; -import 'package:json_api/src/resource.dart'; /// Resource object class ResourceObject { diff --git a/lib/src/server/resource_controller.dart b/lib/src/server/resource_controller.dart index 5edf472..26d0d84 100644 --- a/lib/src/server/resource_controller.dart +++ b/lib/src/server/resource_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:json_api/src/identifier.dart'; -import 'package:json_api/src/resource.dart'; +import 'package:json_api/src/document/identifier.dart'; +import 'package:json_api/src/document/resource.dart'; import 'package:json_api/src/server/page.dart'; import 'package:json_api/src/server/request.dart'; @@ -12,7 +12,13 @@ class Collection { Collection(this.elements, {this.page}); } +/// The [ResourceController] manages resources at application level. +/// It is responsible for CRUD operations over resources. +/// When the operation succeeds, the method should return the value specified in +/// its return type. It may also throw a [ResourceControllerException] which +/// will be converted to an [ErrorObject] and returned to the client in an [ErrorDocument] abstract class ResourceController { + /// Returns true if the resource type is supported by the controller bool supports(String type); Future> fetchCollection( @@ -23,7 +29,10 @@ abstract class ResourceController { Future createResource( String type, Resource resource, JsonApiHttpRequest request); - Future deleteResource( + /// This method should delete the resource specified by [type] and [id]. + /// It may return metadata to be sent back as 200 OK response. + /// If an empty map or null is returned, the server will respond with 204 No Content. + Future> deleteResource( String type, String id, JsonApiHttpRequest request); } diff --git a/lib/src/server/router.dart b/lib/src/server/router.dart index bbf4245..71b56b6 100644 --- a/lib/src/server/router.dart +++ b/lib/src/server/router.dart @@ -73,7 +73,6 @@ class StandardRouting implements Router { Map _nonEmpty(Map map) => map.isEmpty ? null : map; } - abstract class JsonApiRoute { String get type; diff --git a/lib/src/server/server.dart b/lib/src/server/server.dart index 4ab0ca0..7a87cc4 100644 --- a/lib/src/server/server.dart +++ b/lib/src/server/server.dart @@ -2,35 +2,19 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:json_api/src/document/collection_document.dart'; -import 'package:json_api/src/document/document.dart'; -import 'package:json_api/src/document/error_document.dart'; -import 'package:json_api/src/document/error_object.dart'; -import 'package:json_api/src/document/identifier_object.dart'; -import 'package:json_api/src/document/link.dart'; -import 'package:json_api/src/document/relationship.dart'; -import 'package:json_api/src/document/resource_document.dart'; -import 'package:json_api/src/document/resource_object.dart'; -import 'package:json_api/src/identifier.dart'; +import 'package:json_api/src/document.dart'; import 'package:json_api/src/nullable.dart'; -import 'package:json_api/src/resource.dart'; import 'package:json_api/src/server/json_api_controller.dart'; import 'package:json_api/src/server/request.dart'; import 'package:json_api/src/server/resource_controller.dart'; import 'package:json_api/src/server/response.dart'; import 'package:json_api/src/server/router.dart'; -typedef void MetaDecorator( - JsonApiRoute route, JsonApiHttpRequest request, Document doc); - class JsonApiServer implements JsonApiController { final ResourceController controller; final Router router; - final MetaDecorator meta; - JsonApiServer(this.controller, this.router, {MetaDecorator meta}) - : meta = meta ?? - ((JsonApiRoute route, JsonApiHttpRequest request, Document doc) {}); + JsonApiServer(this.controller, this.router); Future handle(JsonApiHttpRequest request) async { final route = await router.resolve(request); @@ -150,8 +134,12 @@ class JsonApiServer implements JsonApiController { Future deleteResource( String type, String id, JsonApiHttpRequest request) async { try { - await controller.deleteResource(type, id, request); - return ServerResponse.noContent(); + final meta = await controller.deleteResource(type, id, request); + if (meta?.isNotEmpty == true) { + return ServerResponse.ok(MetaDocument(meta)); + } else { + return ServerResponse.noContent(); + } } on ResourceControllerException catch (e) { return ServerResponse(e.httpStatus, ErrorDocument([ErrorObject.fromResourceControllerException(e)])); diff --git a/lib/src/server/simple_server.dart b/lib/src/server/simple_server.dart index 5f298ca..9fd7d25 100644 --- a/lib/src/server/simple_server.dart +++ b/lib/src/server/simple_server.dart @@ -10,14 +10,12 @@ import 'package:json_api/src/server/server.dart'; class SimpleServer { HttpServer _httpServer; final ResourceController _controller; - final MetaDecorator meta; - SimpleServer(this._controller, {this.meta}); + SimpleServer(this._controller); Future start(InternetAddress address, int port) async { - final jsonApiServer = JsonApiServer( - _controller, StandardRouting(Uri.parse('http://${address.host}:$port')), - meta: meta); + final jsonApiServer = JsonApiServer(_controller, + StandardRouting(Uri.parse('http://${address.host}:$port'))); _httpServer = await HttpServer.bind(address, port); diff --git a/test/functional/client_create_test.dart b/test/functional/client_create_test.dart index 7f72e00..249cbbb 100644 --- a/test/functional/client_create_test.dart +++ b/test/functional/client_create_test.dart @@ -18,8 +18,6 @@ void main() async { tearDown(() => s.stop()); group('resource', () { - /// https://jsonapi.org/format/#crud-creating-responses-201 - /// /// If a POST request did not include a Client-Generated ID and the requested /// resource has been created successfully, the server MUST return a 201 Created status code. /// @@ -29,6 +27,8 @@ void main() async { /// /// If the resource object returned by the response contains a self key in its links member /// and a Location header is provided, the value of the self member MUST match the value of the Location header. + /// + /// https://jsonapi.org/format/#crud-creating-responses-201 test('201 Created', () async { final modelY = Resource('models', null, attributes: {'name': 'Model Y'}); final r0 = await client.createResource(Url.collection('models'), modelY); @@ -46,12 +46,12 @@ void main() async { expect(r1.document.resourceObject.attributes['name'], 'Model Y'); }); - /// https://jsonapi.org/format/#crud-creating-responses-204 - /// /// If a POST request did include a Client-Generated ID and the requested /// resource has been created successfully, the server MUST return either /// a 201 Created status code and response document (as described above) /// or a 204 No Content status code with no response document. + /// + /// https://jsonapi.org/format/#crud-creating-responses-204 test('204 No Content', () async { final modelY = Resource('models', '555', attributes: {'name': 'Model Y'}); final r0 = await client.createResource(Url.collection('models'), modelY); @@ -65,10 +65,10 @@ void main() async { expect(r1.document.resourceObject.attributes['name'], 'Model Y'); }); - /// https://jsonapi.org/format/#crud-creating-responses-409 - /// /// A server MUST return 409 Conflict when processing a POST request to /// create a resource with a client-generated ID that already exists. + /// + /// https://jsonapi.org/format/#crud-creating-responses-409 test('409 Conflict - Resource already exists', () async { final modelY = Resource('models', '1', attributes: {'name': 'Model Y'}); final r0 = await client.createResource(Url.collection('models'), modelY); @@ -79,11 +79,11 @@ void main() async { expect(r0.errorDocument.errors.first.detail, 'Resource already exists'); }); - /// https://jsonapi.org/format/#crud-creating-responses-409 - /// /// A server MUST return 409 Conflict when processing a POST request in /// which the resource object’s type is not among the type(s) that /// constitute the collection represented by the endpoint. + /// + /// https://jsonapi.org/format/#crud-creating-responses-409 test('409 Conflict - Incompatible type', () async { final modelY = Resource('models', '555', attributes: {'name': 'Model Y'}); final r0 = diff --git a/test/functional/client_delete_test.dart b/test/functional/client_delete_test.dart index 7822b75..9fa9917 100644 --- a/test/functional/client_delete_test.dart +++ b/test/functional/client_delete_test.dart @@ -18,10 +18,10 @@ void main() async { tearDown(() => s.stop()); group('resource', () { - /// https://jsonapi.org/format/#crud-deleting-responses-204 - /// /// A server MUST return a 204 No Content status code if a deletion request /// is successful and no content is returned. + /// + /// https://jsonapi.org/format/#crud-deleting-responses-204 test('204 No Content', () async { final r0 = await client.deleteResource(Url.resource('models', '1')); @@ -34,18 +34,19 @@ void main() async { expect(r1.status, 404); }); + /// A server MUST return a 200 OK status code if a deletion request + /// is successful and the server responds with only top-level meta data. + /// + /// https://jsonapi.org/format/#crud-deleting-responses-200 test('200 OK', () async { - // Json-Api-Options header is not a part of the standard! - final r0 = await client.deleteResource(Url.resource('models', '1'), - headers: {'Json-Api-Options': 'meta'}); + final r0 = await client.deleteResource(Url.resource('companies', '1')); expect(r0.status, 200); expect(r0.isSuccessful, true); - expect(r0.document.meta['Server'], - 'Dart JSON:API Server. https://pub.dartlang.org/packages/json_api'); + expect(r0.document.meta['deps'], 5); // Make sure the resource is not available anymore - final r1 = await client.fetchResource(Url.resource('models', '1')); + final r1 = await client.fetchResource(Url.resource('companies', '1')); expect(r1.status, 404); });