Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] V9 #143

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
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
25 changes: 7 additions & 18 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,11 @@ 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()
.add(loggerMiddleware)
.call(router(controller, StandardUriDesign.pathOnly.matchTarget));

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

ProcessSignal.sigint.watch().listen((event) async {
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;
},
);
2 changes: 1 addition & 1 deletion lib/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/// representing the most common use cases of resource fetching and manipulation.
/// It can conveniently construct and parse JSON:API documents and URIs.
/// The [RoutingClient] should be your default choice.
library client;
library;

export 'package:json_api/src/client/client.dart';
export 'package:json_api/src/client/request.dart';
Expand Down
4 changes: 3 additions & 1 deletion lib/document.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/// JSON:API Document model.
library document;
library;

export 'package:json_api/src/document/error_object.dart';
export 'package:json_api/src/document/inbound_document.dart';
export 'package:json_api/src/document/json_encodable.dart';
export 'package:json_api/src/document/link.dart';
export 'package:json_api/src/document/new_identifier.dart';
export 'package:json_api/src/document/new_relationship.dart';
Expand All @@ -12,5 +13,6 @@ export 'package:json_api/src/document/new_to_one.dart';
export 'package:json_api/src/document/outbound_document.dart';
export 'package:json_api/src/document/relationship.dart';
export 'package:json_api/src/document/resource.dart';
export 'package:json_api/src/document/to_json_encodable.dart';
export 'package:json_api/src/document/to_many.dart';
export 'package:json_api/src/document/to_one.dart';
72 changes: 4 additions & 68 deletions lib/http.dart
Original file line number Diff line number Diff line change
@@ -1,69 +1,5 @@
import 'package:http_interop/http_interop.dart';
/// Common HTTP utilities for JSON:API clients and servers.
/// WARNING: This library is in beta stage. The API is subject to change.
library;

class StatusCode {
const StatusCode(this.value);

static const ok = 200;
static const created = 201;
static const accepted = 202;
static const noContent = 204;
static const badRequest = 400;
static const notFound = 404;
static const methodNotAllowed = 405;
static const unacceptable = 406;
static const unsupportedMediaType = 415;

final int value;

/// True for the requests processed asynchronously.
/// @see https://jsonapi.org/recommendations/#asynchronous-processing).
bool get isPending => value == accepted;

/// True for successfully processed requests
bool get isSuccessful => value >= ok && value < 300 && !isPending;

/// True for failed requests (i.e. neither successful nor pending)
bool get isFailed => !isSuccessful && !isPending;
}

Handler loggingMiddleware(Handler handler,
{Function(Request request)? onRequest,
Function(Response response)? onResponse}) =>
(Request request) async {
onRequest?.call(request);
final response = await handler(request);
onResponse?.call(response);
return response;
};

/// CORS middleware
Handler corsMiddleware(Handler handler) => (Request request) async {
final corsHeaders = {
'Access-Control-Allow-Origin': [request.headers.last('origin') ?? '*'],
'Access-Control-Expose-Headers': ['Location'],
};

if (request.method == 'options') {
const methods = ['POST', 'GET', 'DELETE', 'PATCH', 'OPTIONS'];
return Response(
204,
Body(),
Headers.from({
...corsHeaders,
'Access-Control-Allow-Methods':
request.headers['Access-Control-Request-Method'] ?? methods,
'Access-Control-Allow-Headers':
request.headers['Access-Control-Request-Headers'] ?? ['*'],
}));
}
final response = await handler(request);
response.headers.addAll(corsHeaders);
return response;
};

extension HeaderExt on Headers {
String? last(String key) {
final v = this[key];
return (v != null && v.isNotEmpty) ? v.last : null;
}
}
export 'package:json_api/src/http/status_code.dart';
2 changes: 1 addition & 1 deletion lib/query.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// A set of builders/parsers for special query parameters used in JSON:API.
library query;
library;

export 'package:json_api/src/query/fields.dart';
export 'package:json_api/src/query/include.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/routing.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Routing describes the design of URLs on the server.
/// See https://jsonapi.org/recommendations/#urls
library routing;
library;

export 'package:json_api/src/routing/standard_uri_design.dart';
export 'package:json_api/src/routing/target.dart';
Expand Down
10 changes: 7 additions & 3 deletions lib/server.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/// JSON:API server on top of dart:io.
library server;
/// WARNING: This library is in beta stage. The API is subject to change.
library;

export 'package:json_api/src/server/controller.dart';
export 'package:json_api/src/server/controller_router.dart';
export 'package:json_api/src/server/cors_middleware.dart';
export 'package:json_api/src/server/error_converter.dart';
export 'package:json_api/src/server/errors/collection_not_found.dart';
export 'package:json_api/src/server/errors/method_not_allowed.dart';
export 'package:json_api/src/server/errors/not_acceptable.dart';
export 'package:json_api/src/server/errors/relationship_not_found.dart';
export 'package:json_api/src/server/errors/resource_not_found.dart';
export 'package:json_api/src/server/errors/unmatched_target.dart';
export 'package:json_api/src/server/errors/unsupported_media_type.dart';
export 'package:json_api/src/server/request_validator.dart';
export 'package:json_api/src/server/response.dart';
export 'package:json_api/src/server/try_catch_middleware.dart';
export 'package:json_api/src/server/router.dart';
6 changes: 2 additions & 4 deletions lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:convert';

import 'package:http_interop/extensions.dart';
import 'package:http_interop/http_interop.dart' as i;
import 'package:json_api/http.dart';
import 'package:json_api/src/client/payload_codec.dart';
import 'package:json_api/src/client/request.dart';
import 'package:json_api/src/client/response.dart';
Expand Down Expand Up @@ -45,9 +44,8 @@ class Client {
Future<Map?> _decode(i.Response response) async {
final json = await response.body.decode(utf8);
if (json.isNotEmpty &&
response.headers
.last('Content-Type')
?.toLowerCase()
response.headers['Content-Type']?.last
.toLowerCase()
.startsWith(mediaType) ==
true) {
return await _codec.decode(json);
Expand Down
5 changes: 4 additions & 1 deletion lib/src/client/payload_codec.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:convert';

import 'package:json_api/src/document/to_json_encodable.dart';

/// Encodes/decodes JSON payload.
///
/// The methods are designed to be asynchronous to allow for conversion to be
Expand All @@ -16,5 +18,6 @@ class PayloadCodec {
}

/// Encodes a JSON:API document into a JSON string.
FutureOr<String> encode(Object document) => jsonEncode(document);
FutureOr<String> encode(Object document) =>
jsonEncode(document, toEncodable: toJsonEncodable);
}
7 changes: 0 additions & 7 deletions lib/src/client/response/collection_fetched.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:http_interop/http_interop.dart' as i;
import 'package:json_api/document.dart';
import 'package:json_api/src/client/response.dart';

Expand All @@ -12,12 +11,6 @@ class CollectionFetched {
links.addAll(document.links());
}

// coverage:ignore-start
/// The raw HTTP response
@Deprecated('Use rawResponse.httpResponse instead')
i.Response get httpResponse => rawResponse.httpResponse;
// coverage:ignore-end

/// The raw JSON:API response
final Response rawResponse;

Expand Down
7 changes: 0 additions & 7 deletions lib/src/client/response/related_resource_fetched.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:http_interop/http_interop.dart' as i;
import 'package:json_api/document.dart';
import 'package:json_api/src/client/response.dart';

Expand All @@ -15,12 +14,6 @@ class RelatedResourceFetched {
links.addAll(document.links());
}

// coverage:ignore-start
/// The raw HTTP response
@Deprecated('Use rawResponse.httpResponse instead')
i.Response get httpResponse => rawResponse.httpResponse;
// coverage:ignore-end

/// The raw JSON:API response
final Response rawResponse;

Expand Down
7 changes: 0 additions & 7 deletions lib/src/client/response/relationship_fetched.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:http_interop/http_interop.dart' as i;
import 'package:json_api/document.dart';
import 'package:json_api/src/client/response.dart';

Expand All @@ -20,12 +19,6 @@ class RelationshipFetched<R extends Relationship> {
..included.addAll(document.included());
}

// coverage:ignore-start
/// The raw HTTP response
@Deprecated('Use rawResponse.httpResponse instead')
i.Response get httpResponse => rawResponse.httpResponse;
// coverage:ignore-end

/// The raw JSON:API response
final Response rawResponse;

Expand Down
7 changes: 0 additions & 7 deletions lib/src/client/response/relationship_updated.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:http_interop/http_interop.dart' as i;
import 'package:json_api/document.dart';
import 'package:json_api/src/client/response.dart';

Expand All @@ -18,12 +17,6 @@ class RelationshipUpdated<R extends Relationship> {
response, json == null ? null : InboundDocument(json).asToOne());
}

// coverage:ignore-start
/// The raw HTTP response
@Deprecated('Use rawResponse.httpResponse instead')
i.Response get httpResponse => rawResponse.httpResponse;
// coverage:ignore-end

/// The raw JSON:API response
final Response rawResponse;

Expand Down
7 changes: 0 additions & 7 deletions lib/src/client/response/request_failure.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:http_interop/http_interop.dart' as i;
import 'package:json_api/document.dart';
import 'package:json_api/src/client/response.dart';

Expand All @@ -12,12 +11,6 @@ class RequestFailure implements Exception {
meta.addAll(document.meta());
}

// coverage:ignore-start
/// The raw HTTP response
@Deprecated('Use rawResponse.httpResponse instead')
i.Response get httpResponse => rawResponse.httpResponse;
// coverage:ignore-end

/// The raw JSON:API response
final Response rawResponse;

Expand Down
7 changes: 0 additions & 7 deletions lib/src/client/response/resource_created.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:http_interop/http_interop.dart' as i;
import 'package:json_api/document.dart';
import 'package:json_api/src/client/response.dart';

Expand All @@ -16,12 +15,6 @@ class ResourceCreated {
links.addAll(document.links());
}

// coverage:ignore-start
/// The raw HTTP response
@Deprecated('Use rawResponse.httpResponse instead')
i.Response get httpResponse => rawResponse.httpResponse;
// coverage:ignore-end

/// The raw JSON:API response
final Response rawResponse;

Expand Down
Loading
Loading