diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index f8c2d35..0297fc1 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -25,4 +25,4 @@ jobs: - name: Tests run: dart test --coverage=.coverage -j1 - name: Coverage - run: dart run coverage:format_coverage -l -c -i .coverage --report-on=lib | dart run check_coverage:check_coverage 98 + run: dart run coverage:format_coverage -l -c -i .coverage --report-on=lib | dart run check_coverage:check_coverage diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..d79d8c8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,14 @@ +name: Publish to pub.dev + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + +jobs: + publish: + permissions: + id-token: write # Required for authentication using OIDC + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 + # with: + # working-directory: path/to/package/within/repository \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0707da2..679d663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0] - 2023-11-12 +### Changed +- Migrated to `http_interop` v1. + ## [6.0.1] - 2023-09-11 ### Fixed - `NewRelationship` was not exported @@ -242,6 +246,7 @@ the Document model. ### Added - Client: fetch resources, collections, related resources and relationships +[7.0.0]: https://github.com/f3ath/json-api-dart/compare/6.0.1...7.0.0 [6.0.1]: https://github.com/f3ath/json-api-dart/compare/6.0.0...6.0.1 [6.0.0]: https://github.com/f3ath/json-api-dart/compare/5.4.0...6.0.0 [5.4.0]: https://github.com/f3ath/json-api-dart/compare/5.3.0...5.4.0 diff --git a/example/server.dart b/example/server.dart index 57dccb9..bea19e0 100644 --- a/example/server.dart +++ b/example/server.dart @@ -22,11 +22,11 @@ Future main() async { ControllerRouter(controller, StandardUriDesign.matchTarget); handler = TryCatchHandler(handler, onError: ErrorConverter(onError: (e, stack) async { - stderr.writeln(e); - return Response(500, - document: OutboundErrorDocument( - [ErrorObject(title: 'Internal Server Error')])); - })); + stderr.writeln(e); + return Response(500, + document: OutboundErrorDocument( + [ErrorObject(title: 'Internal Server Error')])); + }).call); handler = LoggingHandler(handler, onRequest: (r) => print('${r.method} ${r.uri}'), onResponse: (r) => print('${r.statusCode}')); diff --git a/example/server/cors_handler.dart b/example/server/cors_handler.dart index c419a0a..073ee0d 100644 --- a/example/server/cors_handler.dart +++ b/example/server/cors_handler.dart @@ -13,12 +13,12 @@ class CorsHandler implements Handler { 'Access-Control-Expose-Headers': ['Location'], }; - if (request.method.equals('OPTIONS')) { + if (request.method == 'options') { const methods = ['POST', 'GET', 'DELETE', 'PATCH', 'OPTIONS']; return Response( 204, - Body.empty(), - Headers({ + Body(), + Headers.from({ ...headers, 'Access-Control-Allow-Methods': request.headers['Access-Control-Request-Method'] ?? methods, diff --git a/lib/http.dart b/lib/http.dart index 0cd26e6..5a190c6 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:http_interop/http_interop.dart'; class StatusCode { @@ -29,7 +27,7 @@ class StatusCode { } class Json extends Body { - Json(Map json) : super(jsonEncode(json), utf8); + Json(Map super.object) : super.json(); } class LoggingHandler implements Handler { diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index 083f05d..405267e 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -23,8 +23,8 @@ class Client { /// Sends the [request] to the given [uri]. Future send(Uri uri, Request request) async { final json = await _encode(request.document); - final body = http.Body(json, utf8); - final headers = http.Headers({ + final body = http.Body.text(json, utf8); + final headers = http.Headers.from({ 'Accept': [mediaType], if (json.isNotEmpty) 'Content-Type': [mediaType], ...request.headers @@ -32,8 +32,8 @@ class Client { final url = request.query.isEmpty ? uri : uri.replace(queryParameters: request.query.toQuery()); - final response = await _handler - .handle(http.Request(http.Method(request.method), url, body, headers)); + final response = + await _handler.handle(http.Request(request.method, url, body, headers)); final document = await _decode(response); return Response(response, document); diff --git a/lib/src/client/request.dart b/lib/src/client/request.dart index d2f30bf..0046727 100644 --- a/lib/src/client/request.dart +++ b/lib/src/client/request.dart @@ -31,5 +31,5 @@ class Request { final query = Query(); /// HTTP headers. - final headers = Headers({}); + final headers = Headers(); } diff --git a/lib/src/document/inbound_document.dart b/lib/src/document/inbound_document.dart index 962052a..4571c69 100644 --- a/lib/src/document/inbound_document.dart +++ b/lib/src/document/inbound_document.dart @@ -147,13 +147,11 @@ class _Parser { /// Decodes Link from [json]. Returns the decoded object. /// If the [json] has incorrect format, throws [FormatException]. - Link _link(Object json) { - if (json is String) return Link(Uri.parse(json)); - if (json is Map) { - return Link(Uri.parse(json['href']))..meta.addAll(meta(json)); - } - throw FormatException('Invalid JSON'); - } + Link _link(Object json) => switch (json) { + String() => Link(Uri.parse(json)), + Map() => Link(Uri.parse(json['href']))..meta.addAll(meta(json)), + _ => throw FormatException('Invalid JSON') + }; Map _getAttributes(Map json) => json.get>('attributes', orGet: () => {}); @@ -166,25 +164,19 @@ class _Parser { .get('relationships', orGet: () => {}) .map((key, value) => MapEntry(key, newRelationship(value))); - Relationship _rel(data) { - if (data == null) return ToOne.empty(); - if (data is Map) return ToOne(identifier(data)); - if (data is List) return ToMany(data.whereType().map(identifier)); - throw FormatException('Invalid relationship object'); - } - - NewRelationship _newRel(data) { - if (data == null) { - return NewToOne.empty(); - } - if (data is Map) { - return NewToOne(newIdentifier(data)); - } - if (data is List) { - return NewToMany(data.whereType().map(newIdentifier)); - } - throw FormatException('Invalid relationship object'); - } + Relationship _rel(data) => switch (data) { + null => ToOne.empty(), + Map() => ToOne(identifier(data)), + List() => ToMany(data.whereType().map(identifier)), + _ => throw FormatException('Invalid relationship object') + }; + + NewRelationship _newRel(data) => switch (data) { + null => NewToOne.empty(), + Map() => NewToOne(newIdentifier(data)), + List() => NewToMany(data.whereType().map(newIdentifier)), + _ => throw FormatException('Invalid relationship object') + }; } extension _TypedGeter on Map { diff --git a/lib/src/document/new_relationship.dart b/lib/src/document/new_relationship.dart index f985ea0..65de471 100644 --- a/lib/src/document/new_relationship.dart +++ b/lib/src/document/new_relationship.dart @@ -12,8 +12,6 @@ class NewRelationship with IterableMixin { if (meta.isNotEmpty) 'meta': meta, }; - // coverage:ignore-start @override Iterator get iterator => [].iterator; -// coverage:ignore-end } diff --git a/lib/src/document/new_resource.dart b/lib/src/document/new_resource.dart index b111ab7..3cf4be7 100644 --- a/lib/src/document/new_resource.dart +++ b/lib/src/document/new_resource.dart @@ -61,7 +61,6 @@ class NewResource { if (r is NewToMany) { return ToMany(r.map((identifier) => _toIdentifier(identifier, id))); } - // coverage:ignore-line throw StateError('Unexpected relationship type: ${r.runtimeType}'); } @@ -70,16 +69,12 @@ class NewResource { return _toIdentifier(identifier, id); } - Identifier _toIdentifier(NewIdentifier identifier, String id) { - switch (identifier) { - case Identifier(): - return identifier; - case LocalIdentifier(): - if (identifier.type == type && identifier.lid == lid) { - return identifier.toIdentifier(id); - } - throw StateError( - 'Unmatched local id: "${identifier.lid}". Expected "$lid".'); - } - } + Identifier _toIdentifier(NewIdentifier identifier, String id) => + switch (identifier) { + Identifier() => identifier, + LocalIdentifier() => (identifier.type == type && identifier.lid == lid) + ? identifier.toIdentifier(id) + : throw StateError( + 'Unmatched local id: "${identifier.lid}". Expected "$lid".') + }; } diff --git a/lib/src/routing/standard_uri_design.dart b/lib/src/routing/standard_uri_design.dart index 27d1382..2b474f6 100644 --- a/lib/src/routing/standard_uri_design.dart +++ b/lib/src/routing/standard_uri_design.dart @@ -11,22 +11,14 @@ class StandardUriDesign implements UriDesign { /// `/books`, `/books/42`, `/books/42/authors` static final pathOnly = StandardUriDesign(Uri(path: '/')); - static Target? matchTarget(Uri uri) { - final s = uri.pathSegments; - if (s.length == 1) { - return Target(s.first); - } - if (s.length == 2) { - return ResourceTarget(s.first, s.last); - } - if (s.length == 3) { - return RelatedTarget(s.first, s[1], s.last); - } - if (s.length == 4 && s[2] == 'relationships') { - return RelationshipTarget(s.first, s[1], s.last); - } - return null; - } + static Target? matchTarget(Uri uri) => switch ((uri.pathSegments)) { + [var type] => Target(type), + [var type, var id] => ResourceTarget(type, id), + [var type, var id, var rel] => RelatedTarget(type, id, rel), + [var type, var id, 'relationships', var rel] => + RelationshipTarget(type, id, rel), + _ => null + }; final Uri base; diff --git a/lib/src/server/controller_router.dart b/lib/src/server/controller_router.dart index 3f45e37..08f25a6 100644 --- a/lib/src/server/controller_router.dart +++ b/lib/src/server/controller_router.dart @@ -18,49 +18,31 @@ class ControllerRouter implements Handler { Future handle(Request request) async { _validate(request); final target = _matchTarget(request.uri); - if (target is RelationshipTarget) { - if (request.method.equals('GET')) { - return await _controller.fetchRelationship(request, target); - } - if (request.method.equals('POST')) { - return await _controller.addMany(request, target); - } - if (request.method.equals('PATCH')) { - return await _controller.replaceRelationship(request, target); - } - if (request.method.equals('DELETE')) { - return await _controller.deleteMany(request, target); - } - throw MethodNotAllowed(request.method.value); - } - if (target is RelatedTarget) { - if (request.method.equals('GET')) { - return await _controller.fetchRelated(request, target); - } - throw MethodNotAllowed(request.method.value); - } - if (target is ResourceTarget) { - if (request.method.equals('GET')) { - return await _controller.fetchResource(request, target); - } - if (request.method.equals('PATCH')) { - return await _controller.updateResource(request, target); - } - if (request.method.equals('DELETE')) { - return await _controller.deleteResource(request, target); - } - throw MethodNotAllowed(request.method.value); - } - if (target is Target) { - if (request.method.equals('GET')) { - return await _controller.fetchCollection(request, target); - } - if (request.method.equals('POST')) { - return await _controller.createResource(request, target); - } - throw MethodNotAllowed(request.method.value); - } - throw UnmatchedTarget(request.uri); + return await switch (target) { + RelationshipTarget() => switch (request.method) { + 'get' => _controller.fetchRelationship(request, target), + 'post' => _controller.addMany(request, target), + 'patch' => _controller.replaceRelationship(request, target), + 'delete' => _controller.deleteMany(request, target), + _ => throw MethodNotAllowed(request.method) + }, + RelatedTarget() => switch (request.method) { + 'get' => _controller.fetchRelated(request, target), + _ => throw MethodNotAllowed(request.method) + }, + ResourceTarget() => switch (request.method) { + 'get' => _controller.fetchResource(request, target), + 'patch' => _controller.updateResource(request, target), + 'delete' => _controller.deleteResource(request, target), + _ => throw MethodNotAllowed(request.method) + }, + Target() => switch (request.method) { + 'get' => _controller.fetchCollection(request, target), + 'post' => _controller.createResource(request, target), + _ => throw MethodNotAllowed(request.method) + }, + _ => throw UnmatchedTarget(request.uri) + }; } void _validate(Request request) { diff --git a/lib/src/server/response.dart b/lib/src/server/response.dart index b15084c..d8cabaa 100644 --- a/lib/src/server/response.dart +++ b/lib/src/server/response.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:http_interop/http_interop.dart' as http; import 'package:json_api/document.dart'; import 'package:json_api/http.dart'; @@ -10,10 +8,8 @@ class Response extends http.Response { Response(int statusCode, {D? document}) : super( statusCode, - document != null - ? http.Body(jsonEncode(document), utf8) - : http.Body.empty(), - http.Headers({})) { + document != null ? http.Body.json(document) : http.Body(), + http.Headers()) { if (document != null) { headers['Content-Type'] = [mediaType]; } diff --git a/pubspec.yaml b/pubspec.yaml index f93f095..d47cfb3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,24 +1,24 @@ name: json_api -version: 6.0.1 +version: 7.0.0 homepage: https://github.com/f3ath/json-api-dart description: A framework-agnostic implementations of JSON:API Client and Server. Supports JSON:API v1.0 (https://jsonapi.org) environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.1.0 <4.0.0' dependencies: http_parser: ^4.0.0 - http_interop: ^0.8.0 + http_interop: ^1.0.0 dev_dependencies: - lints: ^2.1.1 + lints: ^3.0.0 test: ^1.21.1 stream_channel: ^2.1.0 - uuid: ^3.0.0 + uuid: ^4.2.0 coverage: ^1.3.0 check_coverage: ^0.0.4 http: ^1.1.0 - http_interop_http: ^0.6.0 - http_interop_io: ^0.6.0 + http_interop_http: ^0.7.0 + http_interop_io: ^0.7.0 cider: diff --git a/test/contract/errors_test.dart b/test/contract/errors_test.dart index db60cff..edccf8b 100644 --- a/test/contract/errors_test.dart +++ b/test/contract/errors_test.dart @@ -26,16 +26,16 @@ void main() { } }); test('Bad request when target can not be matched', () async { - final r = await TestHandler().handle(http.Request(http.Method('get'), - Uri.parse('/a/long/prefix/'), http.Body.empty(), http.Headers({}))); + final r = await TestHandler().handle(http.Request( + 'get', Uri.parse('/a/long/prefix/'), http.Body(), http.Headers())); expect(r.statusCode, 400); }); test('Unsupported extension', () async { final r = await TestHandler().handle(http.Request( - http.Method('get'), + 'get', Uri.parse('/posts/1'), - http.Body.empty(), - http.Headers({ + http.Body(), + http.Headers.from({ 'Content-Type': ['application/vnd.api+json; ext=foobar'], 'Accept': ['application/vnd.api+json'] }))); @@ -43,10 +43,10 @@ void main() { }); test('Unacceptable', () async { final r = await TestHandler().handle(http.Request( - http.Method('get'), + 'get', Uri.parse('/posts/1'), - http.Body.empty(), - http.Headers({ + http.Body(), + http.Headers.from({ 'Content-Type': ['application/vnd.api+json'], 'Accept': ['application/vnd.api+json; ext=foobar'] }))); diff --git a/test/test_handler.dart b/test/test_handler.dart index d26214e..3b29817 100644 --- a/test/test_handler.dart +++ b/test/test_handler.dart @@ -17,11 +17,12 @@ class TestHandler extends LoggingHandler { ControllerRouter(RepositoryController(InMemoryRepo(types), _id), StandardUriDesign.matchTarget), onError: ErrorConverter( - onError: onError ?? - (err, trace) { - print(trace); - throw err; - })), + onError: onError ?? + (err, trace) { + print(trace); + throw err; + }) + .call), onRequest: onRequest, onResponse: onResponse); } diff --git a/test/unit/client/client_test.dart b/test/unit/client/client_test.dart index 6fe5120..c617682 100644 --- a/test/unit/client/client_test.dart +++ b/test/unit/client/client_test.dart @@ -44,7 +44,7 @@ void main() { expect(response.collection.single.type, 'articles'); expect(response.collection.single.id, '1'); expect(response.included, isEmpty); - expect(http.request.method.value, 'get'); + expect(http.request.method, equals('get')); expect(http.request.uri.toString(), '/articles'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'] @@ -70,7 +70,7 @@ void main() { expect(response.collection.length, 1); expect(response.included.length, 3); - expect(http.request.method.value, 'get'); + expect(http.request.method, equals('get')); expect(http.request.uri.path, '/articles'); expect(http.request.uri.queryParameters, { 'include': 'author', @@ -93,7 +93,7 @@ void main() { final response = await client.fetchRelatedCollection('people', '1', 'articles'); expect(response.collection.length, 1); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.path, '/people/1/articles'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'] @@ -119,7 +119,7 @@ void main() { expect(response.collection.length, 1); expect(response.included.length, 3); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.path, '/people/1/articles'); expect(http.request.uri.queryParameters, { 'include': 'author', @@ -142,7 +142,7 @@ void main() { http.response = mock.primaryResource(); final response = await client.fetchResource('articles', '1'); expect(response.resource.type, 'articles'); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.toString(), '/articles/1'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'] @@ -164,7 +164,7 @@ void main() { ]); expect(response.resource.type, 'articles'); expect(response.included.length, 3); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.path, '/articles/1'); expect(http.request.uri.queryParameters, {'include': 'author', 'fields[author]': 'name', 'foo': 'bar'}); @@ -184,7 +184,7 @@ void main() { await client.fetchRelatedResource('articles', '1', 'author'); expect(response.resource?.type, 'articles'); expect(response.included.length, 3); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.toString(), '/articles/1/author'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'] @@ -207,7 +207,7 @@ void main() { ]); expect(response.resource?.type, 'articles'); expect(response.included.length, 3); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.path, '/articles/1/author'); expect(http.request.uri.queryParameters, {'include': 'author', 'fields[author]': 'name', 'foo': 'bar'}); @@ -225,7 +225,7 @@ void main() { await client.fetchRelatedResource('articles', '1', 'author'); expect(response.resource, isNull); expect(response.included, isEmpty); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.toString(), '/articles/1/author'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'] @@ -238,7 +238,7 @@ void main() { http.response = mock.one(); final response = await client.fetchToOne('articles', '1', 'author'); expect(response.included.length, 3); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.toString(), '/articles/1/relationships/author'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'] @@ -256,7 +256,7 @@ void main() { }) ]); expect(response.included.length, 3); - expect(http.request.method.equals('get'), true); + expect(http.request.method, equals('get')); expect(http.request.uri.path, '/articles/1/relationships/author'); expect(http.request.uri.queryParameters, {'foo': 'bar'}); expect(http.request.headers, { @@ -274,7 +274,7 @@ void main() { expect( response.links['self'].toString(), 'http://example.com/articles/1'); expect(response.included.length, 3); - expect(http.request.method.equals('post'), true); + expect(http.request.method, equals('post')); expect(http.request.uri.toString(), '/articles'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -304,7 +304,7 @@ void main() { expect( response.links['self'].toString(), 'http://example.com/articles/1'); expect(response.included.length, 3); - expect(http.request.method.equals('post'), true); + expect(http.request.method, equals('post')); expect(http.request.uri.toString(), '/articles'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -344,7 +344,7 @@ void main() { http.response = mock.primaryResource(); final response = await client.create('articles', '1'); expect(response.resource?.type, 'articles'); - expect(http.request.method.equals('post'), true); + expect(http.request.method, equals('post')); expect(http.request.uri.toString(), '/articles'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -359,7 +359,7 @@ void main() { http.response = mock.noContent(); final response = await client.create('articles', '1'); expect(response.resource, isNull); - expect(http.request.method.equals('post'), true); + expect(http.request.method, equals('post')); expect(http.request.uri.toString(), '/articles'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -386,7 +386,7 @@ void main() { 'foo': ['bar'] }); expect(response.resource?.type, 'articles'); - expect(http.request.method.equals('post'), true); + expect(http.request.method, equals('post')); expect(http.request.uri.toString(), '/articles'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -427,7 +427,7 @@ void main() { http.response = mock.primaryResource(); final response = await client.updateResource('articles', '1'); expect(response.resource?.type, 'articles'); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -442,7 +442,7 @@ void main() { http.response = mock.noContent(); final response = await client.updateResource('articles', '1'); expect(response.resource, isNull); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -470,7 +470,7 @@ void main() { 'foo': ['bar'] }); expect(response.resource?.type, 'articles'); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -512,7 +512,7 @@ void main() { final response = await client.replaceToOne( 'articles', '1', 'author', Identifier('people', '42')); expect(response.relationship, isA()); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1/relationships/author'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -534,7 +534,7 @@ void main() { 'foo': ['bar'] }); expect(response.relationship, isA()); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1/relationships/author'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -574,7 +574,7 @@ void main() { final response = await client.deleteToOne('articles', '1', 'author'); expect(response.relationship, isA()); expect(response.relationship!.identifier, isNull); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1/relationships/author'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -591,7 +591,7 @@ void main() { }); expect(response.relationship, isA()); expect(response.relationship!.identifier, isNull); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1/relationships/author'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -625,7 +625,7 @@ void main() { final response = await client .deleteFromMany('articles', '1', 'tags', [Identifier('tags', '1')]); expect(response.relationship, isA()); - expect(http.request.method.equals('delete'), true); + expect(http.request.method, equals('delete')); expect(http.request.uri.toString(), '/articles/1/relationships/tags'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -648,7 +648,7 @@ void main() { 'foo': ['bar'] }); expect(response.relationship, isA()); - expect(http.request.method.equals('delete'), true); + expect(http.request.method, equals('delete')); expect(http.request.uri.toString(), '/articles/1/relationships/tags'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -690,7 +690,7 @@ void main() { final response = await client .replaceToMany('articles', '1', 'tags', [Identifier('tags', '1')]); expect(response.relationship, isA()); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1/relationships/tags'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -713,7 +713,7 @@ void main() { 'foo': ['bar'] }); expect(response.relationship, isA()); - expect(http.request.method.equals('patch'), true); + expect(http.request.method, equals('patch')); expect(http.request.uri.toString(), '/articles/1/relationships/tags'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -755,7 +755,7 @@ void main() { final response = await client .addMany('articles', '1', 'tags', [Identifier('tags', '1')]); expect(response.relationship, isA()); - expect(http.request.method.equals('post'), true); + expect(http.request.method, equals('post')); expect(http.request.uri.toString(), '/articles/1/relationships/tags'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], @@ -778,7 +778,7 @@ void main() { 'foo': ['bar'] }); expect(response.relationship, isA()); - expect(http.request.method.equals('post'), true); + expect(http.request.method, equals('post')); expect(http.request.uri.toString(), '/articles/1/relationships/tags'); expect(http.request.headers, { 'Accept': ['application/vnd.api+json'], diff --git a/test/unit/client/response.dart b/test/unit/client/response.dart index ce1c81e..1a64603 100644 --- a/test/unit/client/response.dart +++ b/test/unit/client/response.dart @@ -2,7 +2,7 @@ import 'package:http_interop/http_interop.dart'; import 'package:json_api/http.dart'; import 'package:json_api/src/media_type.dart'; -final headers = Headers({ +final headers = Headers.from({ 'Content-Type': [mediaType] }); @@ -255,7 +255,7 @@ many() => Response( }), headers); -noContent() => Response(204, Body.empty(), Headers({})); +noContent() => Response(204, Body(), Headers()); error422() => Response( 422, @@ -272,4 +272,4 @@ error422() => Response( }), headers); -error500() => Response(500, Body.empty(), Headers({})); +error500() => Response(500, Body(), Headers()); diff --git a/test/unit/document/inbound_document_test.dart b/test/unit/document/inbound_document_test.dart index 0123e38..d5b4c22 100644 --- a/test/unit/document/inbound_document_test.dart +++ b/test/unit/document/inbound_document_test.dart @@ -188,6 +188,20 @@ void main() { expect(() => InboundDocument(payload.many).asToOne(), throwsFormatException); }); + + test('throws on invalid new relationship', () { + expect( + () => InboundDocument(payload.newResourceInvalidRelationship) + .dataAsNewResource(), + throwsFormatException); + }); + + test('throws on incomplete new relationship', () { + expect( + () => InboundDocument(payload.newResourceIncompleteRelationship) + .dataAsNewResource(), + throwsFormatException); + }); }); }); } diff --git a/test/unit/document/new_relationship_test.dart b/test/unit/document/new_relationship_test.dart new file mode 100644 index 0000000..504d442 --- /dev/null +++ b/test/unit/document/new_relationship_test.dart @@ -0,0 +1,13 @@ +import 'package:json_api/document.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('NewRelationship', () { + test('iterator', () { + final list = []; + list.addAll(NewRelationship()); + expect(list, isEmpty); + }); + }); +} diff --git a/test/unit/document/new_resource_test.dart b/test/unit/document/new_resource_test.dart index 1e0cd9d..eba069e 100644 --- a/test/unit/document/new_resource_test.dart +++ b/test/unit/document/new_resource_test.dart @@ -5,6 +5,30 @@ import 'package:test/test.dart'; void main() { group('NewResource', () { + test('toResource throws on unmatched local id in "many"', () { + final resource = NewResource('test_type', id: 'test_id', lid: 'test_lid') + ..relationships['many'] = NewToMany([ + LocalIdentifier('test_type', 'test_lid2'), + ]); + + expect(() => resource.toResource(() => 'my-test-id'), throwsStateError); + }); + + test('toResource throws on unmatched local id in "one"', () { + final resource = NewResource('test_type', id: 'test_id', lid: 'test_lid') + ..relationships['one'] = + NewToOne(LocalIdentifier('test_type', 'test_lid2')); + + expect(() => resource.toResource(() => 'my-test-id'), throwsStateError); + }); + + test('toResource throws on invalid relationship', () { + final resource = NewResource('test_type', id: 'test_id') + ..relationships['many'] = NewRelationship(); + + expect(() => resource.toResource(() => 'my-test-id'), throwsStateError); + }); + test('json encoding', () { expect(jsonEncode(NewResource('test_type')), jsonEncode({'type': 'test_type'})); diff --git a/test/unit/document/new_to_one_test.dart b/test/unit/document/new_to_one_test.dart new file mode 100644 index 0000000..6853d3a --- /dev/null +++ b/test/unit/document/new_to_one_test.dart @@ -0,0 +1,15 @@ +import 'package:json_api/document.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('NewToOne', () { + test('can be iterated', () { + final id = Identifier('books', '123'); + final r = NewToOne(id); + final list = []; + list.addAll(r); + expect(list.single, equals(id)); + }); + }); +} diff --git a/test/unit/document/payload.dart b/test/unit/document/payload.dart index c7d6cb0..9653ac3 100644 --- a/test/unit/document/payload.dart +++ b/test/unit/document/payload.dart @@ -147,3 +147,27 @@ final resource = { } } }; + +final newResourceInvalidRelationship = { + 'data': { + 'type': 'articles', + 'attributes': {'title': 'JSON:API paints my bikeshed!'}, + 'relationships': { + 'author': { + 'data': true, + }, + } + } +}; + +final newResourceIncompleteRelationship = { + 'data': { + 'type': 'articles', + 'attributes': {'title': 'JSON:API paints my bikeshed!'}, + 'relationships': { + 'author': { + 'data': {'type': 'person'}, + }, + } + } +};