Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
78251b1
wip
f3ath Jul 16, 2024
fc56c60
WIP
f3ath Jul 16, 2024
47575fc
WIP
f3ath Jul 17, 2024
ae8fb36
Format
f3ath Jul 17, 2024
f46e34c
alpha.2
f3ath Aug 11, 2024
9a7c6af
alpha.3
f3ath Aug 12, 2024
13a14df
alpha.4
f3ath Aug 18, 2024
e16decc
alpha.5
f3ath Aug 19, 2024
f267771
alpha.5
f3ath Aug 19, 2024
47929a5
alpha.5
f3ath Aug 19, 2024
d88151b
alpha.5
f3ath Aug 19, 2024
468d9dc
Add rawResponse (#144)
f3ath Aug 30, 2024
6560949
Remove deprecations
f3ath Aug 30, 2024
384d028
Merge branch 'master' into v9
f3ath Sep 29, 2024
3ab6ed1
update cors
f3ath Sep 29, 2024
28eff82
update cors
f3ath Sep 29, 2024
da9e033
WIP
f3ath Sep 30, 2024
18bc887
fix
f3ath Sep 30, 2024
622034f
fix
f3ath Sep 30, 2024
4f44073
Dart 3.13
f3ath Oct 4, 2026
186e7d8
format
f3ath Oct 4, 2026
0b168a4
format
f3ath Oct 4, 2026
f0761eb
wip
f3ath Oct 9, 2026
63b23fe
wip
f3ath Oct 9, 2026
4879413
Added: Filter, BaseController
f3ath Oct 11, 2026
770cf08
BaseController test
f3ath Oct 11, 2026
9d68d98
MappingController test
f3ath Oct 11, 2026
bcfdadf
Routing
f3ath Oct 12, 2026
721452d
Format
f3ath Oct 12, 2026
53681da
Format
f3ath Oct 12, 2026
4b4550b
Format
f3ath Oct 12, 2026
32635f1
Format
f3ath Oct 12, 2026
09e973b
Format
f3ath Oct 12, 2026
2a3bdd0
wip
f3ath Oct 16, 2026
6c50f0a
wip
f3ath Oct 16, 2026
458ae01
wip
f3ath Oct 16, 2026
e280eb5
wip
f3ath Oct 16, 2026
7934227
wip
f3ath Oct 16, 2026
23e6bdd
wip
f3ath Oct 17, 2026
627f44c
wip
f3ath Oct 17, 2026
551bb8e
wip
f3ath Oct 18, 2026
0abb6cb
wip
f3ath Oct 18, 2026
1c98377
wip
f3ath Oct 18, 2026
81bc5df
interop v3
f3ath Oct 24, 2026
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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ 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).

## [Unreleased]
### Added
- Automatically encode DateTime objects as an ISO-8601 string
- The "conflict" server response

### Changed
- URL Design matching now respects the base URL
- Allow null to be returned by error interceptors

### Fixed
- StandardUriDesign working inconsistently, depending on the trailing slash in the path

## [8.1.0] - 2024-08-29
### Added
- The rawResponse property to the responses of the RoutingClient
Expand All @@ -13,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [8.0.0] - 2024-07-01
### Added
- CORS middware
- CORS middleware

### Changed
- Bump http\_interop to v2.0
Expand Down Expand Up @@ -264,6 +276,7 @@ the Document model.
### Added
- Client: fetch resources, collections, related resources and relationships

[Unreleased]: https://github.com/f3ath/json-api-dart/compare/8.1.0...HEAD
[8.1.0]: https://github.com/f3ath/json-api-dart/compare/8.0.0...8.1.0
[8.0.0]: https://github.com/f3ath/json-api-dart/compare/7.0.1...8.0.0
[7.0.1]: https://github.com/f3ath/json-api-dart/compare/7.0.0...7.0.1
Expand Down
10 changes: 8 additions & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
include: package:lints/recommended.yaml
formatter:
page_width: 100

linter:
rules:
- sort_constructors_first
- sort_unnamed_constructors_first
- use_declaring_parameters
- unnecessary_primary_constructor_body
- unnecessary_type_name_in_constructor

- prefer_if_null_operators
11 changes: 3 additions & 8 deletions example/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main() async {
/// This is the interface which decouples this JSON:API implementation
/// from the HTTP client.
/// Learn more: https://pub.dev/packages/http_interop
final httpHandler = httpClient.handleInterop;
final httpHandler = httpClient.sendInterop;

/// This is the basic JSON:API client. It is flexible but not very convenient
/// to use, because you would need to remember a lot of JSON:API protocol details.
Expand All @@ -28,7 +28,7 @@ void main() async {
/// It is called routing because it routes the calls to the correct
/// URLs depending on the use case. Take a look at its methods, they cover
/// all the standard scenarios specified by the JSON:API standard.
final client = RoutingClient(uriDesign, jsonApiClient);
final client = RoutingClient(uriDesign.uri, jsonApiClient);

try {
/// Fetch the collection.
Expand All @@ -38,12 +38,7 @@ void main() async {
/// The fetched collection allows us to iterate over the resources
/// and to look into their attributes
for (final resource in response.collection) {
final {
'name': name,
'red': red,
'green': green,
'blue': blue,
} = resource.attributes;
final {'name': name, 'red': red, 'green': green, 'blue': blue} = resource.attributes;
print('${resource.type}:${resource.id}');
print('$name - $red:$green:$blue');
}
Expand Down
53 changes: 25 additions & 28 deletions example/server.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import 'dart:io';

import 'package:http_interop/http_interop.dart' as interop;
import 'package:json_api/document.dart';
import 'package:json_api/http.dart';
import 'package:http_interop_middleware/http_interop_middleware.dart';
import 'package:json_api/routing.dart';
import 'package:json_api/server.dart';
import 'package:uuid/uuid.dart';

import 'server/in_memory_repo.dart';
import 'server/json_api_server.dart';
import 'server/logger_middleware.dart';
import 'server/repository.dart';
import 'server/repository_controller.dart';

Expand All @@ -18,21 +17,16 @@ Future<void> main() async {
final repo = InMemoryRepo(['colors']);
await initRepo(repo);
final controller = RepositoryController(repo, Uuid().v4);
interop.Handler handler =
ControllerRouter(controller, StandardUriDesign.matchTarget).handle;

handler = tryCatchMiddleware(handler,
onError: ErrorConverter(onError: (e, stack) async {
stderr.writeln(e);
stderr.writeln(stack);
return response(500,
document: OutboundErrorDocument(
[ErrorObject(title: 'Internal Server Error')]));
}).call);

handler = loggingMiddleware(handler,
onRequest: (r) => print('${r.method} ${r.uri}'),
onResponse: (r) => print('${r.statusCode}'));

final handler = errorConverter()
.wrapIn(loggerMiddleware)
.call(
router(
getTarget: (rq) => StandardUriDesign.pathOnly.target(rq.uri),
getController: (_, _) => controller,
),
);

final server = JsonApiServer(handler, host: host, port: port);

ProcessSignal.sigint.watch().listen((event) async {
Expand All @@ -46,16 +40,19 @@ Future<void> main() async {
}

Future initRepo(Repository repo) async {
final models = {
{'name': 'Salmon', 'r': 250, 'g': 128, 'b': 114},
{'name': 'Pink', 'r': 255, 'g': 192, 'b': 203},
{'name': 'Lime', 'r': 0, 'g': 255, 'b': 0},
{'name': 'Peru', 'r': 205, 'g': 133, 'b': 63},
}.map((color) => Model(Uuid().v4())
..attributes['name'] = color['name']
..attributes['red'] = color['r']
..attributes['green'] = color['g']
..attributes['blue'] = color['b']);
final models =
{
{'name': 'Salmon', 'r': 250, 'g': 128, 'b': 114},
{'name': 'Pink', 'r': 255, 'g': 192, 'b': 203},
{'name': 'Lime', 'r': 0, 'g': 255, 'b': 0},
{'name': 'Peru', 'r': 205, 'g': 133, 'b': 63},
}.map(
(color) => Model(Uuid().v4())
..attributes['name'] = color['name']
..attributes['red'] = color['r']
..attributes['green'] = color['g']
..attributes['blue'] = color['b'],
);
for (final model in models) {
await repo.persist('colors', model);
}
Expand Down
19 changes: 7 additions & 12 deletions example/server/in_memory_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import 'package:json_api/src/nullable.dart';

import 'repository.dart';

class InMemoryRepo implements Repository {
InMemoryRepo(Iterable<String> types) {
class InMemoryRepo(Iterable<String> types) implements Repository {
this {
for (var type in types) {
_storage[type] = {};
}
Expand All @@ -29,8 +29,7 @@ class InMemoryRepo implements Repository {
}

@override
Stream<Identifier> addMany(
String type, String id, String rel, Iterable<Identifier> ids) {
Stream<Identifier> addMany(String type, String id, String rel, Iterable<Identifier> ids) {
final many = _many(type, id, rel);
many.addAll(ids.map(Reference.of));
return Stream.fromIterable(many).map((e) => e.toIdentifier());
Expand All @@ -47,21 +46,17 @@ class InMemoryRepo implements Repository {
}

@override
Future<void> replaceOne(
String type, String id, String rel, Identifier? one) async {
Future<void> replaceOne(String type, String id, String rel, Identifier? one) async {
_model(type, id).one[rel] = nullable(Reference.of)(one);
}

@override
Stream<Identifier> deleteMany(
String type, String id, String rel, Iterable<Identifier> many) =>
Stream.fromIterable(
_many(type, id, rel)..removeAll(many.map(Reference.of)))
Stream<Identifier> deleteMany(String type, String id, String rel, Iterable<Identifier> many) =>
Stream.fromIterable(_many(type, id, rel)..removeAll(many.map(Reference.of)))
.map((it) => it.toIdentifier());

@override
Stream<Identifier> replaceMany(
String type, String id, String rel, Iterable<Identifier> many) {
Stream<Identifier> replaceMany(String type, String id, String rel, Iterable<Identifier> many) {
final set = _many(type, id, rel);
set.clear();
set.addAll(many.map(Reference.of));
Expand Down
15 changes: 5 additions & 10 deletions example/server/json_api_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,15 @@ import 'dart:io';
import 'package:http_interop/http_interop.dart';
import 'package:http_interop_io/http_interop_io.dart';

class JsonApiServer {
JsonApiServer(
this._handler, {
this.host = 'localhost',
this.port = 8080,
});
class JsonApiServer(
final Handler _handler, {

/// Server host name
final String host;
final String host = 'localhost',

/// Server port
final int port;

final Handler _handler;
final int port = 8080,
}) {
HttpServer? _server;

/// Server uri
Expand Down
20 changes: 20 additions & 0 deletions example/server/logger_middleware.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:io';

import 'package:http_interop_middleware/http_interop_middleware.dart';

/// Middleware that logs all requests and responses to stderr.
final Middleware loggerMiddleware = middleware(
onRequest: (r) async {
stderr.writeln(r);
return null;
},
onResponse: (r, _) async {
stderr.writeln(r);
return null;
},
onError: (e, t, _) async {
stderr.writeln(e);
stderr.writeln(t);
return null;
},
);
14 changes: 5 additions & 9 deletions example/server/relationship_node.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
/// Relationship tree node
class RelationshipNode {
RelationshipNode(this.name);

class RelationshipNode(
/// The name of the relationship
final String name,
) {
static Iterable<RelationshipNode> forest(Iterable<String> relationships) {
final root = RelationshipNode('');
relationships
.map((rel) => rel.trim().split('.').map((e) => e.trim()))
.forEach(root.add);
relationships.map((rel) => rel.trim().split('.').map((e) => e.trim())).forEach(root.add);
return root.children;
}

/// The name of the relationship
final String name;

Iterable<RelationshipNode> get children => _map.values;

final _map = <String, RelationshipNode>{};
Expand Down
32 changes: 9 additions & 23 deletions example/server/repository.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:json_api/document.dart';
import 'package:json_api/src/nullable.dart';

abstract class Repository {
abstract class Repository() {
/// Fetches a collection.
/// Throws [CollectionNotFound].
Stream<Model> fetchCollection(String type);
Expand All @@ -16,8 +16,7 @@ abstract class Repository {
/// Throws [CollectionNotFound].
/// Throws [ResourceNotFound].
/// Throws [RelationshipNotFound].
Stream<Identifier> addMany(
String type, String id, String rel, Iterable<Identifier> many);
Stream<Identifier> addMany(String type, String id, String rel, Iterable<Identifier> many);

/// Delete the resource
Future<void> delete(String type, String id);
Expand All @@ -29,33 +28,25 @@ abstract class Repository {

/// Deletes refs from the to-many relationship.
/// Returns the new actual refs.
Stream<Identifier> deleteMany(
String type, String id, String rel, Iterable<Identifier> many);
Stream<Identifier> deleteMany(String type, String id, String rel, Iterable<Identifier> many);

/// Replaces refs in the to-many relationship.
/// Returns the new actual refs.
Stream<Identifier> replaceMany(
String type, String id, String rel, Iterable<Identifier> many);
Stream<Identifier> replaceMany(String type, String id, String rel, Iterable<Identifier> many);
}

class Reference {
Reference(this.type, this.id);

class Reference(final String type, final String id) {
static Reference of(Identifier id) => Reference(id.type, id.id);

Identifier toIdentifier() => Identifier(type, id);
final String type;
final String id;

@override
final hashCode = 0;

@override
bool operator ==(Object other) =>
other is Reference && type == other.type && id == other.id;
bool operator ==(Object other) => other is Reference && type == other.type && id == other.id;
}

class ModelProps {
class ModelProps() {
static ModelProps fromResource(Resource res) {
final props = ModelProps();
res.attributes.forEach((key, value) {
Expand Down Expand Up @@ -92,19 +83,14 @@ class ModelProps {
}

/// A model of a resource. Essentially, this is the core of a resource object.
class Model extends ModelProps {
Model(this.id);

final String id;

class Model(final String id) extends ModelProps {
Resource toResource(String type) {
final res = Resource(type, id);
attributes.forEach((key, value) {
res.attributes[key] = value;
});
one.forEach((key, value) {
res.relationships[key] =
(value == null ? ToOne.empty() : ToOne(value.toIdentifier()));
res.relationships[key] = (value == null ? ToOne.empty() : ToOne(value.toIdentifier()));
});
many.forEach((key, value) {
res.relationships[key] = ToMany(value.map((it) => it.toIdentifier()));
Expand Down
Loading
Loading