Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Improved ResourceController error handling
- Resource creation

## 0.1.0 - 2019-02-27
### Added
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
- [x] Fetching single resources and resource collections
- [x] Fetching relationships and related resources and collections
- [x] Fetching single resources
- [ ] Creating resources
- [x] Creating 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!)
- [x] Fetching single resources and resource collections
- [x] Fetching relationships and related resources and collections
- [x] Fetching single resources
- [ ] Creating resources
- [x] Creating resources
- [ ] Updating resource's attributes
- [ ] Updating resource's relationships
- [ ] Updating relationships
Expand All @@ -25,10 +26,13 @@
- [ ] Sparse fieldsets
- [ ] Sorting, pagination, filtering
- [ ] Asynchronous processing
- [ ] Optional check for `Content-Type` header in incoming requests
- [ ] Support annotations in resource mappers (?)

##### Document
- [ ] Support `meta` and `jsonapi` members
- [ ] Structure Validation
- [ ] Structure Validation including compound documents
- [ ] Support relationship objects lacking the `data` member
- [ ] Naming Validation
- [ ] JSON:API v1.1 features

Expand All @@ -48,4 +52,4 @@ import 'package:http/browser_client.dart';
final client = JsonApiClient(factory: () => BrowserClient());
```

For usage examples see a corresponding test in `test/functional`.
For usage examples see the [functional tests](https://github.com/f3ath/json-api-dart/tree/master/test/functional).
1 change: 1 addition & 0 deletions dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
concurrency: 1
4 changes: 2 additions & 2 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# JSON:API examples

## [Cars Server](./cars_server)
This is a simple JSON:API server which is used in the tests. It provides an API to a collection to car brands ad models.
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.
You can run it locally to play around.

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

**Warning: Server API is not stable yet!**

Expand Down
6 changes: 3 additions & 3 deletions example/cars_client.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'dart:async';

import 'package:json_api/client.dart';
import 'package:json_api/src/transport/collection_document.dart';
import 'package:json_api/src/transport/relationship.dart';
import 'package:json_api/src/transport/resource_object.dart';
import 'package:json_api/src/document/collection_document.dart';
import 'package:json_api/src/document/relationship.dart';
import 'package:json_api/src/document/resource_object.dart';

class CarsClient {
final JsonApiClient c;
Expand Down
44 changes: 28 additions & 16 deletions example/cars_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ void main() async {
}

SimpleServer createServer() {
final cars = CarDAO();
final models = ModelDAO();
[
Car('1', 'Roadster'),
Car('2', 'Model S'),
Car('3', 'Model X'),
Car('4', 'Model 3'),
].forEach(cars.insert);
Model('1', 'Roadster'),
Model('2', 'Model S'),
Model('3', 'Model X'),
Model('4', 'Model 3'),
].forEach(models.insert);

final cities = CityDAO();
[
Expand All @@ -29,15 +29,27 @@ SimpleServer createServer() {
City('3', 'Ingolstadt'),
].forEach(cities.insert);

final brands = BrandDAO();
final companies = CompanyDAO();
[
Brand('1', 'Tesla', headquarters: '2', models: ['1', '2', '3', '4']),
Brand('2', 'BMW', headquarters: '1'),
Brand('3', 'Audi', headquarters: '3'),
Brand('4', 'Ford'),
Brand('5', 'Toyota')
].forEach(brands.insert);

return SimpleServer(
CarsController({'brands': brands, 'cities': cities, 'cars': cars}));
Company('1', 'Tesla', headquarters: '2', models: ['1', '2', '3', '4']),
Company('2', 'BMW', headquarters: '1'),
Company('3', 'Audi'),
].forEach(companies.insert);

return SimpleServer(CarsController(
{'companies': companies, 'cities': cities, 'models': models}));
}

class Url {
static final _base = Uri.parse('http://localhost:8080');

static collection(String type) => _base.replace(path: '/$type');

static resource(String type, String id) => _base.replace(path: '/$type/$id');

static related(String type, String id, String rel) =>
_base.replace(path: '/$type/$id/$rel');

static relationship(String type, String id, String rel) =>
_base.replace(path: '/$type/$id/relationships/$rel');
}
26 changes: 25 additions & 1 deletion example/cars_server/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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/resource_controller.dart';
import 'package:uuid/uuid.dart';

import 'dao.dart';

Expand All @@ -30,7 +31,30 @@ class CarsController implements ResourceController {
Stream<Resource> fetchResources(Iterable<Identifier> ids) async* {
for (final id in ids) {
final obj = dao[id.type].fetchById(id.id);
yield obj == null ? null : dao[id.type].toResource(obj);
if (obj == null) {
throw ResourceControllerException(404, detail: 'Resource not found');
}
yield dao[id.type].toResource(obj);
}
}

@override
Future<Resource> createResource(
String type, Resource resource, Map<String, String> params) async {
if (type != resource.type) {
throw ResourceControllerException(409, detail: 'Incompatible type');
}
Object obj;
if (resource.hasId) {
if (dao[type].fetchById(resource.id) != null) {
throw ResourceControllerException(409,
detail: 'Resource already exists');
}
obj = dao[type].create(resource);
} else {
obj = dao[type].create(resource.replace(id: Uuid().v4()));
}
dao[type].insert(obj);
return dao[type].toResource(obj);
}
}
37 changes: 26 additions & 11 deletions example/cars_server/dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ abstract class DAO<T> {

Resource toResource(T t);

T create(Resource resource);

T fetchById(String id) => _collection[id];

void insert(T t); // => collection[t.id] = t;
Expand All @@ -18,30 +20,43 @@ abstract class DAO<T> {
_collection.values.skip(offset).take(limit);
}

class CarDAO extends DAO<Car> {
Resource toResource(Car _) =>
Resource('cars', _.id, attributes: {'name': _.name});
class ModelDAO extends DAO<Model> {
Resource toResource(Model _) =>
Resource('models', _.id, attributes: {'name': _.name});

void insert(Model model) => _collection[model.id] = model;

void insert(Car car) => _collection[car.id] = car;
Model create(Resource r) {
return Model(r.id, r.attributes['name']);
}
}

class CityDAO extends DAO<City> {
Resource toResource(City _) =>
Resource('cities', _.id, attributes: {'name': _.name});

void insert(City city) => _collection[city.id] = city;

City create(Resource r) {
return City(r.id, r.attributes['name']);
}
}

class BrandDAO extends DAO<Brand> {
Resource toResource(Brand brand) => Resource('brands', brand.id, attributes: {
'name': brand.name
class CompanyDAO extends DAO<Company> {
Resource toResource(Company company) =>
Resource('companies', company.id, attributes: {
'name': company.name
}, toOne: {
'hq': brand.headquarters == null
'hq': company.headquarters == null
? null
: Identifier('cities', brand.headquarters)
: Identifier('cities', company.headquarters)
}, toMany: {
'models': brand.models.map((_) => Identifier('cars', _)).toList()
'models': company.models.map((_) => Identifier('models', _)).toList()
});

void insert(Brand brand) => _collection[brand.id] = brand;
void insert(Company company) => _collection[company.id] = company;

Company create(Resource r) {
return Company(r.id, r.attributes['name']);
}
}
8 changes: 4 additions & 4 deletions example/cars_server/model.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
class Brand {
class Company {
final String id;
final String headquarters;
final List<String> models;
String name;

Brand(this.id, this.name, {this.headquarters, this.models = const []});
Company(this.id, this.name, {this.headquarters, this.models = const []});
}

class City {
Expand All @@ -14,9 +14,9 @@ class City {
City(this.id, this.name);
}

class Car {
class Model {
final String id;
String name;

Car(this.id, this.name);
Model(this.id, this.name);
}
2 changes: 2 additions & 0 deletions lib/client.dart
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export 'package:json_api/src/client/client.dart';
export 'package:json_api/src/identifier.dart';
export 'package:json_api/src/resource.dart';
2 changes: 0 additions & 2 deletions lib/core.dart

This file was deleted.

56 changes: 27 additions & 29 deletions lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:json_api/src/client/response.dart';
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/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/nullable.dart';
import 'package:json_api/src/transport/collection_document.dart';
import 'package:json_api/src/transport/document.dart';
import 'package:json_api/src/transport/error_document.dart';
import 'package:json_api/src/transport/relationship.dart';
import 'package:json_api/src/transport/resource_document.dart';
import 'package:json_api/src/resource.dart';

typedef D ResponseParser<D extends Document>(Object j);

Expand Down Expand Up @@ -56,17 +58,13 @@ class JsonApiClient {
{Map<String, String> headers}) =>
_get(Relationship.fromJson, uri, headers);

// /// Creates a new resource. The resource will be added to a collection
// /// according to its type.
// Future<Response<ResourceDocument>> createResource(Uri uri, Resource resource,
// {Map<String, String> headers}) =>
// _post(
// ResourceDocument.fromJson,
// uri,
// ResourceDocument(ResourceObject(resource.type, resource.id,
// attributes: resource.attributes)),
// headers);
//
/// Creates a new resource. The resource will be added to a collection
/// according to its type.
Future<Response<ResourceDocument>> createResource(Uri uri, Resource resource,
{Map<String, String> headers}) =>
_post(ResourceDocument.fromJson, uri,
ResourceDocument(ResourceObject.fromResource(resource)), headers);

// /// Adds the [identifiers] to a to-many relationship identified by [uri]
// Future<Response<ToMany>> addToMany(Uri uri, Iterable<Identifier> identifiers,
// {Map<String, String> headers}) =>
Expand All @@ -91,19 +89,19 @@ class JsonApiClient {
..addAll(headers ?? {})
..addAll({'Accept': contentType})));

// Future<Response<D>> _post<D extends Document>(ResponseParser<D> parse, uri,
// Document document, Map<String, String> headers) =>
// _call(
// parse,
// (_) => _.post(uri,
// body: json.encode(document),
// headers: {}
// ..addAll(headers ?? {})
// ..addAll({
// 'Accept': contentType,
// 'Content-Type': contentType,
// })));
//
Future<Response<D>> _post<D extends Document>(ResponseParser<D> parse, uri,
Document document, Map<String, String> headers) =>
_call(
parse,
(_) => _.post(uri,
body: json.encode(document),
headers: {}
..addAll(headers ?? {})
..addAll({
'Accept': contentType,
'Content-Type': contentType,
})));

// Future<Response<D>> _patch<D extends Document>(ResponseParser<D> parse, uri,
// Document document, Map<String, String> headers) =>
// _call(
Expand Down
6 changes: 4 additions & 2 deletions lib/src/client/response.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_api/src/client/status_code.dart';
import 'package:json_api/src/transport/document.dart';
import 'package:json_api/src/transport/error_document.dart';
import 'package:json_api/src/document/document.dart';
import 'package:json_api/src/document/error_document.dart';

/// A response returned by JSON:API cars_server
class Response<D extends Document> {
Expand Down Expand Up @@ -43,4 +43,6 @@ class Response<D extends Document> {
/// Any non 2** status code is considered a failed operation.
/// For failed requests, [document] is expected to contain [ErrorDocument]
bool get isFailed => StatusCode(status).isFailed;

String get location => headers['location'];
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_api/src/transport/document.dart';
import 'package:json_api/src/transport/link.dart';
import 'package:json_api/src/transport/resource_object.dart';
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 {
final List<ResourceObject> collection;
Expand All @@ -9,7 +9,7 @@ class CollectionDocument implements Document {
final Link self;
final Pagination pagination;

CollectionDocument(List<ResourceObject> collection,
CollectionDocument(Iterable<ResourceObject> collection,
{List<ResourceObject> included, this.self, this.pagination})
: collection = List.unmodifiable(collection),
included = List.unmodifiable(included ?? []);
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:json_api/src/transport/document.dart';
import 'package:json_api/src/transport/error_object.dart';
import 'package:json_api/src/document/document.dart';
import 'package:json_api/src/document/error_object.dart';

class ErrorDocument implements Document {
final errors = <ErrorObject>[];
Expand Down
Loading