Skip to content

Commit 511cc36

Browse files
committed
v0.0.1
1 parent 52e453b commit 511cc36

File tree

7 files changed

+149
-20
lines changed

7 files changed

+149
-20
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
9+
## 0.0.1 - 2019-01-08
10+
### Added
11+
- Initial client implementation
12+
13+
[Unreleased]: https://github.com/f3ath/json-api-dart/compare/0.0.1...HEAD

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# [JSON:API](http://jsonapi.org) v1.0 HTTP Client
22

3-
This is a work-in-progress. Feel free to join.
3+
This is a super simple implementation of a

example/example.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'package:json_api_document/json_api_document.dart';
33

44
/// This is a simple example of JsonApiClient.
55
///
6-
/// Do not forget to start server.dart first!
6+
/// Don't forget to start server.dart first!
77
void main() async {
88
final client = JsonApiClient(baseUrl: 'http://localhost:8888');
99
final response = await client.fetchResource('/example');

lib/src/json_api_client.dart

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,26 @@ class JsonApiClient {
2424
final api = Api('1.0');
2525

2626
/// Fetches a [Document] containing resource(s) from the given [url].
27-
/// Pass a [Map] of [headers] to add extra headers to the request.
2827
///
28+
/// Pass a [Map] of [headers] to add extra headers to the request.
2929
/// More details: https://jsonapi.org/format/#fetching-resources
3030
Future<Response> fetchResource(String url,
3131
{Map<String, String> headers = const {}}) async =>
3232
Response(await _exec((_) => _.get(_url(url), headers: _headers(headers))),
3333
preferResource: true);
3434

3535
/// Fetches a [Document] containing identifier(s) from the given [url].
36-
/// Pass a [Map] of [headers] to add extra headers to the request.
3736
///
37+
/// Pass a [Map] of [headers] to add extra headers to the request.
3838
/// More details: https://jsonapi.org/format/#fetching-relationships
3939
Future<Response> fetchRelationship(String url,
4040
{Map<String, String> headers = const {}}) async =>
4141
Response(
4242
await _exec((_) => _.get(_url(url), headers: _headers(headers))));
4343

4444
/// Creates a new [resource] sending a POST request to the [url].
45-
/// Pass a [Map] of [headers] to add extra headers to the request.
4645
///
46+
/// Pass a [Map] of [headers] to add extra headers to the request.
4747
/// More details: https://jsonapi.org/format/#crud-creating
4848
Future<Response> createResource(String url, Resource resource,
4949
{Map<String, String> headers = const {}}) async =>
@@ -54,17 +54,17 @@ class JsonApiClient {
5454
preferResource: true);
5555

5656
/// Deletes the resource sending a DELETE request to the [url].
57-
/// Pass a [Map] of [headers] to add extra headers to the request.
5857
///
58+
/// Pass a [Map] of [headers] to add extra headers to the request.
5959
/// More details: https://jsonapi.org/format/#crud-deleting
6060
Future<Response> deleteResource(String url,
6161
{Map<String, String> headers = const {}}) async =>
6262
Response(
6363
await _exec((_) => _.delete(_url(url), headers: _headers(headers))));
6464

6565
/// Updates the [resource] sending a PATCH request to the [url].
66-
/// Pass a [Map] of [headers] to add extra headers to the request.
6766
///
67+
/// Pass a [Map] of [headers] to add extra headers to the request.
6868
/// More details: https://jsonapi.org/format/#crud-updating
6969
Future<Response> updateResource(String url, Resource resource,
7070
{Map<String, String> headers = const {}}) async =>
@@ -76,8 +76,8 @@ class JsonApiClient {
7676

7777
/// Creates or updates a to-one relationship sending a corresponding
7878
/// [identifier] via PATCH request to the [url].
79-
/// Pass a [Map] of [headers] to add extra headers to the request.
8079
///
80+
/// Pass a [Map] of [headers] to add extra headers to the request.
8181
/// More details: https://jsonapi.org/format/#crud-updating-to-one-relationships
8282
Future<Response> setToOne(String url, Identifier identifier,
8383
{Map<String, String> headers = const {}}) async =>
@@ -87,27 +87,58 @@ class JsonApiClient {
8787

8888
/// Removes a to-one relationship sending PATCH request with "null" data
8989
/// to the [url].
90-
/// Pass a [Map] of [headers] to add extra headers to the request.
9190
///
91+
/// Pass a [Map] of [headers] to add extra headers to the request.
9292
/// More details: https://jsonapi.org/format/#crud-updating-to-one-relationships
9393
Future<Response> deleteToOne(String url,
9494
{Map<String, String> headers = const {}}) async =>
9595
Response(await _exec((_) => _.patch(_url(url),
9696
body: json.encode(DataDocument.fromNull(api: api)),
9797
headers: _headers(headers, withContentType: true))));
9898

99-
/// Updates a to-many relationship sending the
99+
/// Updates (replaces!) a to-many relationship sending the
100100
/// [identifiers] via PATCH request to the [url].
101-
/// Pass a [Map] of [headers] to add extra headers to the request.
102101
///
102+
/// This call **completely replaces** the set of relationships. E.g. when given
103+
/// an empty array, it will remove all relationships.
104+
/// Pass a [Map] of [headers] to add extra headers to the request.
103105
/// More details: https://jsonapi.org/format/#crud-updating-to-many-relationships
104-
setToMany(String url, List<Identifier> identifiers,
106+
Future<Response> setToMany(String url, List<Identifier> identifiers,
105107
{Map<String, String> headers = const {}}) async =>
106108
Response(await _exec((_) => _.patch(_url(url),
107109
body: json
108110
.encode(DataDocument.fromIdentifierList(identifiers, api: api)),
109111
headers: _headers(headers, withContentType: true))));
110112

113+
/// Adds the [identifiers] to the to-many relationship via POST request to the [url].
114+
///
115+
/// The existing members of the relationships will be kept.
116+
/// Pass a [Map] of [headers] to add extra headers to the request.
117+
/// More details: https://jsonapi.org/format/#crud-updating-to-many-relationships
118+
Future<Response> addToMany(String url, List<Identifier> identifiers,
119+
{Map<String, String> headers = const {}}) async =>
120+
Response(await _exec((_) => _.post(_url(url),
121+
body: json
122+
.encode(DataDocument.fromIdentifierList(identifiers, api: api)),
123+
headers: _headers(headers, withContentType: true))));
124+
125+
/// Deletes the [identifiers] from the to-many relationship via DELETE request to the [url].
126+
///
127+
/// The other existing members of the relationships will be kept.
128+
/// Pass a [Map] of [headers] to add extra headers to the request.
129+
/// More details: https://jsonapi.org/format/#crud-updating-to-many-relationships
130+
Future<Response> deleteToMany(String url, List<Identifier> identifiers,
131+
{Map<String, String> headers = const {}}) async {
132+
// http.Client at version 0.12.0 does not support delete() with a body
133+
// So we will have to make the Request object
134+
final request = http.Request('DELETE', Uri.parse(_url(url)));
135+
request.body =
136+
json.encode(DataDocument.fromIdentifierList(identifiers, api: api));
137+
request.headers.addAll(_headers(headers, withContentType: true));
138+
return Response(await _exec(
139+
(_) async => http.Response.fromStream(await _.send(request))));
140+
}
141+
111142
String _url(String url) => '${baseUrl}${url}';
112143

113144
Map<String, String> _headers(Map<String, String> headers,

lib/src/response.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ import 'package:http/http.dart' as http;
44
import 'package:json_api/src/exceptions.dart';
55
import 'package:json_api_document/json_api_document.dart';
66

7-
/// A result of a fetch() request.
7+
/// A response from a JSON:API server.
8+
///
9+
/// The client wraps the responses from JSON:API server in a [Response] class.
10+
/// This class contains the decoded JSON:API document (if any) and other details
11+
/// such as HTTP status code and HTTP headers.
812
class Response {
13+
/// JSON:API document returned by the server.
14+
///
15+
/// May be null if the body is empty.
916
final Document document;
17+
/// HTTP status code
1018
final int status;
19+
/// HTTP headers
1120
final Map<String, String> headers;
1221

1322
Response(http.Response r, {bool preferResource = false})
@@ -24,5 +33,9 @@ class Response {
2433
throw InvalidContentTypeException(headers[contentType]);
2534
}
2635

36+
/// The "Location:" headers (if provided).
37+
///
38+
/// This header comes with "201 Created" responses".
39+
/// More details: https://jsonapi.org/format/#crud-creating-responses-201
2740
String get location => headers['location'];
2841
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ author: "Alexey Karapetov <karapetov@gmail.com>"
22
description: "JSON:API v1.0 (http://jsonapi.org) HTTP Client for Flutter, Server and Browsers"
33
homepage: "https://github.com/f3ath/json-api-dart"
44
name: "json_api"
5-
version: "0.0.1-dev3"
5+
version: "0.0.1-dev5"
66
dependencies:
77
http: "^0.12.0"
88
json_api_document: "^0.3.9"

test/json_api_client_test.dart

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,8 @@ void main() {
278278
rq.response.close();
279279
});
280280

281-
final result = await client
282-
.setToOne('/update', identifier, headers: {'foo': 'bar'});
281+
final result =
282+
await client.setToOne('/update', identifier, headers: {'foo': 'bar'});
283283

284284
expect(result.status, HttpStatus.ok);
285285
}, tags: ['vm-only']);
@@ -296,7 +296,6 @@ void main() {
296296
///
297297
298298
group('deleteToOne()', () {
299-
300299
test('200', () async {
301300
server.listen((rq) async {
302301
expect(rq.method, 'PATCH');
@@ -312,8 +311,8 @@ void main() {
312311
rq.response.close();
313312
});
314313

315-
final result = await client
316-
.deleteToOne('/update', headers: {'foo': 'bar'});
314+
final result =
315+
await client.deleteToOne('/update', headers: {'foo': 'bar'});
317316

318317
expect(result.status, HttpStatus.ok);
319318
}, tags: ['vm-only']);
@@ -329,7 +328,7 @@ void main() {
329328

330329
///
331330
332-
group('setToMany()', () {
331+
group('replaceToMany()', () {
333332
final apple1 = Identifier('apples', '1');
334333
final apple2 = Identifier('apples', '2');
335334

@@ -367,5 +366,83 @@ void main() {
367366
});
368367
}, tags: ['vm-only']);
369368

369+
///
370+
371+
group('addToMany()', () {
372+
final apple1 = Identifier('apples', '1');
373+
final apple2 = Identifier('apples', '2');
374+
375+
test('200', () async {
376+
server.listen((rq) async {
377+
expect(rq.method, 'POST');
378+
expect(rq.headers['foo'], ['bar']);
379+
expect(rq.uri.path, '/add');
380+
expect(rq.headers.host, 'localhost');
381+
expect(rq.headers.port, 4041);
382+
final doc = Document.fromJson(json.decode(await utf8.decodeStream(rq)));
383+
expect(doc, TypeMatcher<DataDocument>());
384+
expect((doc as DataDocument).data, TypeMatcher<IdentifierListData>());
385+
expect((doc as DataDocument).data.identifies(Resource('apples', '1')),
386+
true);
387+
expect((doc as DataDocument).data.identifies(Resource('apples', '2')),
388+
true);
389+
390+
rq.response.headers.contentType = ContentType.parse(Document.mediaType);
391+
rq.response.close();
392+
});
393+
394+
final result = await client
395+
.addToMany('/add', [apple1, apple2], headers: {'foo': 'bar'});
370396

397+
expect(result.status, HttpStatus.ok);
398+
}, tags: ['vm-only']);
399+
400+
test('invalid Content-Type', () async {
401+
server.listen((rq) {
402+
rq.response.close();
403+
});
404+
expect(() async => await client.addToMany('/add', [apple1, apple2]),
405+
throwsA(TypeMatcher<InvalidContentTypeException>()));
406+
});
407+
}, tags: ['vm-only']);
408+
409+
///
410+
411+
group('deleteToMany()', () {
412+
final apple1 = Identifier('apples', '1');
413+
final apple2 = Identifier('apples', '2');
414+
415+
test('200', () async {
416+
server.listen((rq) async {
417+
expect(rq.method, 'DELETE');
418+
expect(rq.headers['foo'], ['bar']);
419+
expect(rq.uri.path, '/add');
420+
expect(rq.headers.host, 'localhost');
421+
expect(rq.headers.port, 4041);
422+
final doc = Document.fromJson(json.decode(await utf8.decodeStream(rq)));
423+
expect(doc, TypeMatcher<DataDocument>());
424+
expect((doc as DataDocument).data, TypeMatcher<IdentifierListData>());
425+
expect((doc as DataDocument).data.identifies(Resource('apples', '1')),
426+
true);
427+
expect((doc as DataDocument).data.identifies(Resource('apples', '2')),
428+
true);
429+
430+
rq.response.headers.contentType = ContentType.parse(Document.mediaType);
431+
rq.response.close();
432+
});
433+
434+
final result = await client
435+
.deleteToMany('/add', [apple1, apple2], headers: {'foo': 'bar'});
436+
437+
expect(result.status, HttpStatus.ok);
438+
}, tags: ['vm-only']);
439+
440+
test('invalid Content-Type', () async {
441+
server.listen((rq) {
442+
rq.response.close();
443+
});
444+
expect(() async => await client.deleteToMany('/add', [apple1, apple2]),
445+
throwsA(TypeMatcher<InvalidContentTypeException>()));
446+
});
447+
}, tags: ['vm-only']);
371448
}

0 commit comments

Comments
 (0)