Skip to content

Commit 72513b9

Browse files
committed
Async operations
1 parent 93bb8c9 commit 72513b9

File tree

16 files changed

+240
-80
lines changed

16 files changed

+240
-80
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Changed
99
- More BC-breaking changes in the Server
1010

11+
### Fixed
12+
- Location headers were incorrectly generated by Server
13+
1114
### Added
1215
- Related collection pagination
16+
- Async operations support
1317

1418
## [0.4.0] - 2019-03-17
1519
### Changed

README.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
# Implementation of [JSON:API v1.0](http://jsonapi.org) in Dart
1+
# Implementation of [{json:api} v1.0](http://jsonapi.org) in Dart
2+
[{json:api} v1.0](http://jsonapi.org) is a specification for building APIs in JSON. This library implements
3+
a Client (VM, Flutter, Web), and a Server (VM only).
24

3-
## Warning! This is a work-in-progress. While at v0, the API is changing rapidly.
45

5-
### Feature roadmap
6-
The features here are roughly ordered by priority. Feel free to open an issue if you want to add another feature.
7-
8-
#### Client
6+
## Client
97
- [x] Fetching single resources and resource collections
108
- [x] Collection pagination
119
- [x] Fetching relationships and related resources and collections
@@ -17,8 +15,7 @@ The features here are roughly ordered by priority. Feel free to open an issue if
1715
- [x] Updating relationships
1816
- [x] Compound documents
1917
- [x] Related collection pagination
20-
- [ ] Asynchronous processing
21-
- [ ] Optional check for `Content-Type` header in incoming responses
18+
- [x] Asynchronous processing
2219

2320
#### Server
2421
- [x] Fetching single resources and resource collections
@@ -30,21 +27,18 @@ The features here are roughly ordered by priority. Feel free to open an issue if
3027
- [x] Updating resource's attributes
3128
- [x] Updating resource's relationships
3229
- [x] Updating relationships
33-
- [ ] Compound documents
34-
- [ ] Sparse fieldsets
35-
- [ ] Sorting, filtering
36-
- [ ] Related collection pagination
37-
- [ ] Asynchronous processing
38-
- [ ] Optional check for `Content-Type` header in incoming requests
39-
- [ ] Support annotations in resource mappers (?)
30+
- [x] Related collection pagination
31+
- [x] Compound documents
32+
- [x] Asynchronous processing
4033

4134
#### Document
4235
- [x] Support relationship objects lacking the `data` member
4336
- [x] Compound documents
4437
- [ ] Support `meta` members
4538
- [ ] Support `jsonapi` members
46-
- [ ] Structural Validation including compound documents
39+
- [ ] Structural Validation including compound documents and sparse fieldsets
4740
- [ ] Naming Validation
41+
- [ ] Meaningful parsing exceptions
4842
- [ ] JSON:API v1.1 features
4943

5044
### Usage

example/README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# JSON:API examples
22

33
## [Cars Server](./cars_server)
4-
This is a simple JSON:API server which is used in the tests. It provides an API to a collection to car companies ad models.
4+
This is a simple JSON:API server which is used in the tests. It provides an API to a collection to car companies and models.
55
You can run it locally to play around.
66

77
- In you console run `dart example/cars_server.dart`, this will start the server at port 8080.
88
- Open http://localhost:8080/companies in the browser.
99

10-
**Warning: Server API is not stable yet!**
11-
1210
## [Cars Client](./cars_client.dart)
1311
A simple client for Cars Server API. It is also used in the tests.

example/cars_server.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ Future<HttpServer> createServer(InternetAddress addr, int port) async {
4646
Company('4')..name = 'Toyota',
4747
].forEach(companies.insert);
4848

49-
final controller = CarsController(
50-
{'companies': companies, 'cities': cities, 'models': models});
49+
final controller = CarsController({
50+
'companies': companies,
51+
'cities': cities,
52+
'models': models,
53+
'jobs': JobDAO()
54+
});
5155

5256
final urlDesign = StandardURLDesign(Uri.parse('http://localhost:$port'));
5357

@@ -64,14 +68,15 @@ Future<HttpServer> createServer(InternetAddress addr, int port) async {
6468
class Url {
6569
static final _design = StandardURLDesign(Uri.parse('http://localhost:8080'));
6670

67-
static collection(String type) => _design.collection(CollectionTarget(type));
71+
static Uri collection(String type) =>
72+
_design.collection(CollectionTarget(type));
6873

69-
static resource(String type, String id) =>
74+
static Uri resource(String type, String id) =>
7075
_design.resource(ResourceTarget(type, id));
7176

72-
static related(String type, String id, String relationship) =>
77+
static Uri related(String type, String id, String relationship) =>
7378
_design.related(RelatedTarget(type, id, relationship));
7479

75-
static relationship(String type, String id, String relationship) =>
80+
static Uri relationship(String type, String id, String relationship) =>
7681
_design.relationship(RelationshipTarget(type, id, relationship));
7782
}

example/cars_server/controller.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:json_api/src/server/request_target.dart';
77
import 'package:uuid/uuid.dart';
88

99
import 'dao.dart';
10+
import 'job_queue.dart';
1011

1112
class CarsController implements JsonApiController {
1213
final Map<String, DAO> _dao;
@@ -75,14 +76,19 @@ class CarsController implements JsonApiController {
7576
return response
7677
.errorNotFound([JsonApiError(detail: 'Unknown resource type')]);
7778
}
78-
final res =
79-
_dao[request.target.type].fetchByIdAsResource(request.target.id);
80-
if (res == null) {
79+
final obj = _dao[request.target.type].fetchById(request.target.id);
80+
81+
if (obj == null) {
8182
return response
8283
.errorNotFound([JsonApiError(detail: 'Resource not found')]);
8384
}
85+
if (obj is Job && obj.resource != null) {
86+
return response.sendSeeOther(obj.resource);
87+
}
88+
8489
final fetchById = (Identifier _) => _dao[_.type].fetchByIdAsResource(_.id);
8590

91+
final res = _dao[request.target.type].toResource(obj);
8692
final children = res.toOne.values
8793
.map(fetchById)
8894
.followedBy(res.toMany.values.expand((_) => _.map(fetchById)));
@@ -167,7 +173,19 @@ class CarsController implements JsonApiController {
167173
attributes: request.payload.attributes,
168174
toMany: request.payload.toMany,
169175
toOne: request.payload.toOne));
176+
177+
if (request.target.type == 'models') {
178+
// Insertion is artificially delayed
179+
final job = Job(Future.delayed(Duration(milliseconds: 100), () {
180+
_dao[request.target.type].insert(created);
181+
return _dao[request.target.type].toResource(created);
182+
}));
183+
_dao['jobs'].insert(job);
184+
return response.sendAccepted(_dao['jobs'].toResource(job));
185+
}
186+
170187
_dao[request.target.type].insert(created);
188+
171189
return response.sendCreated(_dao[request.target.type].toResource(created));
172190
}
173191

example/cars_server/dao.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:json_api/document.dart';
22
import 'package:json_api/src/nullable.dart';
33

4+
import 'job_queue.dart';
45
import 'model.dart';
56

67
abstract class DAO<T> {
@@ -162,3 +163,16 @@ class CompanyDAO extends DAO<Company> {
162163
throw ArgumentError();
163164
}
164165
}
166+
167+
class JobDAO extends DAO<Job> {
168+
@override
169+
Job create(Resource resource) {
170+
throw UnsupportedError('Jobs are created internally');
171+
}
172+
173+
@override
174+
void insert(Job job) => _collection[job.id] = job;
175+
176+
@override
177+
Resource toResource(Job job) => Resource('jobs', job.id);
178+
}

example/cars_server/job_queue.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'dart:async';
2+
3+
import 'package:json_api/document.dart';
4+
import 'package:uuid/uuid.dart';
5+
6+
class Job {
7+
final String id;
8+
9+
String get status => resource == null ? 'pending' : 'complete';
10+
Resource resource;
11+
12+
Job(Future<Resource> create) : id = Uuid().v4() {
13+
create.then((_) => this.resource = _);
14+
}
15+
}

lib/src/client/client.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:http/http.dart' as http;
55
import 'package:json_api/document.dart';
66
import 'package:json_api/parser.dart';
77
import 'package:json_api/src/client/response.dart';
8+
import 'package:json_api/src/client/status_code.dart';
89
import 'package:json_api/src/nullable.dart';
910

1011
typedef Document ResponseParser(Object j);
@@ -180,8 +181,14 @@ class JsonApiClient {
180181
return Response(r.statusCode, r.headers);
181182
}
182183
final body = json.decode(r.body);
183-
final document = body == null ? null : _parser.parseDocument(body, parse);
184-
return Response(r.statusCode, r.headers, document: document);
184+
if (StatusCode(r.statusCode).isPending) {
185+
return Response(r.statusCode, r.headers,
186+
asyncDocument: body == null
187+
? null
188+
: _parser.parseDocument(body, _parser.parseResourceData));
189+
}
190+
return Response(r.statusCode, r.headers,
191+
document: body == null ? null : _parser.parseDocument(body, parse));
185192
} finally {
186193
client.close();
187194
}

lib/src/client/response.dart

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import 'package:json_api/document.dart';
22
import 'package:json_api/src/client/status_code.dart';
3+
import 'package:json_api/src/nullable.dart';
34

4-
/// A response returned by JSON:API cars_server
5+
/// A response returned by JSON:API client
56
class Response<Data extends PrimaryData> {
67
/// HTTP status code
78
final int status;
@@ -10,32 +11,46 @@ class Response<Data extends PrimaryData> {
1011
/// May be null.
1112
final Document<Data> document;
1213

14+
/// The document received with `202 Accepted` response (if any)
15+
/// https://jsonapi.org/recommendations/#asynchronous-processing
16+
final Document<ResourceData> asyncDocument;
17+
1318
/// Headers returned by the server.
1419
final Map<String, String> headers;
1520

16-
Response(this.status, this.headers, {this.document}) {
17-
// TODO: Check for null and content-type
18-
}
21+
Response(this.status, this.headers, {this.document, this.asyncDocument});
1922

2023
/// Primary Data from the document (if any)
2124
Data get data => document.data;
2225

26+
/// Primary Data from the async document (if any)
27+
ResourceData get asyncData => asyncDocument.data;
28+
2329
/// Was the request successful?
2430
///
25-
/// For pending (202 Accepted) requests [isSuccessful] is always false.
31+
/// For pending (202 Accepted) requests both [isSuccessful] and [isFailed]
32+
/// are always false.
2633
bool get isSuccessful => StatusCode(status).isSuccessful;
2734

28-
/// Is a request is accepted but not finished yet (e.g. queued) [isPending] is true.
29-
/// HTTP Status 202 Accepted should be returned for pending requests.
30-
/// The "Content-Location" header should have a link to the job queue and
31-
/// [document] should contain a queued job resource object.
35+
/// This property is an equivalent of `202 Accepted` HTTP status.
36+
/// It indicates that the request is accepted but not finished yet (e.g. queued).
37+
/// The [contentLocation] should have a link to the job queue resource and
38+
/// [asyncData] may contain a queued job resource object.
3239
///
3340
/// See: https://jsonapi.org/recommendations/#asynchronous-processing
34-
bool get isPending => StatusCode(status).isPending;
41+
bool get isAsync => StatusCode(status).isPending;
3542

3643
/// Any non 2** status code is considered a failed operation.
3744
/// For failed requests, [document] is expected to contain [ErrorDocument]
3845
bool get isFailed => StatusCode(status).isFailed;
3946

40-
String get location => headers['location'];
47+
/// The `Location` HTTP header value. For `201 Created` responses this property
48+
/// contains the location of a newly created resource.
49+
Uri get location => nullable(Uri.parse)(headers['location']);
50+
51+
/// The `Content-Location` HTTP header value. For `202 Accepted` responses
52+
/// this property contains the location of the Job Queue resource.
53+
///
54+
/// More details: https://jsonapi.org/recommendations/#asynchronous-processing
55+
Uri get contentLocation => nullable(Uri.parse)(headers['content-location']);
4156
}

lib/src/server/controller.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ abstract class ControllerResponse {
6262
final headers = <String, String>{};
6363

6464
Future<void> errorNotFound(Iterable<JsonApiError> errors);
65+
66+
Future<void> errorBadRequest(Iterable<JsonApiError> errors);
6567
}
6668

6769
abstract class FetchCollectionResponse extends ControllerResponse {
@@ -74,9 +76,14 @@ abstract class CreateResourceResponse extends ControllerResponse {
7476
Future<void> sendNoContent();
7577

7678
Future<void> errorConflict(Iterable<JsonApiError> errors);
79+
80+
Future<void> sendAccepted(Resource asyncJob);
7781
}
7882

7983
abstract class FetchResourceResponse extends ControllerResponse {
84+
/// https://jsonapi.org/recommendations/#asynchronous-processing
85+
Future<void> sendSeeOther(Resource resource);
86+
8087
Future<void> sendResource(Resource resource, {Iterable<Resource> included});
8188
}
8289

@@ -91,6 +98,8 @@ abstract class UpdateResourceResponse extends ControllerResponse {
9198

9299
Future<void> sendNoContent();
93100

101+
Future<void> sendAccepted(Resource asyncJob);
102+
94103
Future<void> errorConflict(Iterable<JsonApiError> errors);
95104

96105
Future<void> errorForbidden(Iterable<JsonApiError> errors);
@@ -105,6 +114,8 @@ abstract class FetchRelationshipResponse extends ControllerResponse {
105114
abstract class ReplaceToOneResponse extends ControllerResponse {
106115
Future<void> sendNoContent();
107116

117+
Future<void> sendAccepted(Resource asyncJob);
118+
108119
Future<void> sendToMany(Iterable<Identifier> collection);
109120

110121
Future<void> sendToOne(Identifier id);
@@ -113,12 +124,16 @@ abstract class ReplaceToOneResponse extends ControllerResponse {
113124
abstract class ReplaceToManyResponse extends ControllerResponse {
114125
Future<void> sendNoContent();
115126

127+
Future<void> sendAccepted(Resource asyncJob);
128+
116129
Future<void> sendToMany(Iterable<Identifier> collection);
117130

118131
Future<void> sendToOne(Identifier id);
119132
}
120133

121134
abstract class AddToManyResponse extends ControllerResponse {
135+
Future<void> sendAccepted(Resource asyncJob);
136+
122137
Future<void> sendToMany(Iterable<Identifier> collection);
123138
}
124139

0 commit comments

Comments
 (0)