diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 0297fc1..38a2c18 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -18,6 +18,8 @@ jobs: - uses: actions/checkout@v2 - name: Install dependencies run: dart pub get + - name: Print Dart version + run: dart --version - name: Format run: dart format --output none --set-exit-if-changed example lib test - name: Analyzer diff --git a/CHANGELOG.md b/CHANGELOG.md index a0ddbdc..302bb29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ 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). +## [5.4.0] - 2023-04-30 +### Changed +- Switch to http\_interop packages. +- Bump min SDK version to 2.19. + ## [5.3.0] - 2022-12-29 ### Added - Client MessageConverter class to control HTTP request/response conversion. @@ -46,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sound null-safety support. ### Changed -- Everything. Again. This is another major **BC-breaking** rework. Please refer to +- Everything. Again. This is another major **BC-breaking** rework. Please refer to the API documentation, examples and tests. ## [3.2.3] - 2020-08-06 @@ -87,11 +92,11 @@ is missing. Before in such cases a `FormatException` would be thrown ([pr](https ## [3.2.2] - 2020-01-07 ### Fixed -- Can not decode related resource which is null ([\#77](https://github.com/f3ath/json-api-dart/issues/77)) +- Can not decode related resource which is null ([\#77](https://github.com/f3ath/json-api-dart/issues/77)) ## [3.2.1] - 2020-01-01 ### Fixed -- Incorrect URL in the example in the Client documentation ([\#74](https://github.com/f3ath/json-api-dart/issues/74)) +- Incorrect URL in the example in the Client documentation ([\#74](https://github.com/f3ath/json-api-dart/issues/74)) ## [3.2.0] - 2019-12-30 ### Added @@ -157,14 +162,14 @@ is missing. Before in such cases a `FormatException` would be thrown ([pr](https ## [2.0.0] - 2019-07-12 ### Changed -- This package now consolidates the Client, the Server and the Document in one single library. -It does not depend on `json_api_document` and `json_api_server` anymore, please remove these packages +- This package now consolidates the Client, the Server and the Document in one single library. +It does not depend on `json_api_document` and `json_api_server` anymore, please remove these packages from your `pubspec.yaml`. - The min Dart SDK version bumped to `2.3.0` - The Client requires an instance of HttpClient to be passed to the constructor explicitly. -- Both the Document and the Server have been refactored with lots of **BREAKING CHANGES**. +- Both the Document and the Server have been refactored with lots of **BREAKING CHANGES**. See the examples and the functional tests for details. -- Meta properties are not defensively copied, but set directly. Meta property behavior is unified across +- Meta properties are not defensively copied, but set directly. Meta property behavior is unified across the Document model. ### Removed @@ -224,6 +229,7 @@ the Document model. ### Added - Client: fetch resources, collections, related resources and relationships +[5.4.0]: https://github.com/f3ath/json-api-dart/compare/5.3.0...5.4.0 [5.3.0]: https://github.com/f3ath/json-api-dart/compare/5.2.0...5.3.0 [5.2.0]: https://github.com/f3ath/json-api-dart/compare/5.1.0...5.2.0 [5.1.0]: https://github.com/f3ath/json-api-dart/compare/5.0.5...5.1.0 diff --git a/example/README.md b/example/README.md index a326488..4b5eecf 100644 --- a/example/README.md +++ b/example/README.md @@ -1,4 +1,4 @@ -The client and server examples are meant to run together. +The client and server examples are meant to run together. - Open a new terminal window and run `dart server.dart` - While the server is running, open another window and run `dart client.dart` \ No newline at end of file diff --git a/example/server/json_api_server.dart b/example/server/json_api_server.dart index f53b7e8..defdb7e 100644 --- a/example/server/json_api_server.dart +++ b/example/server/json_api_server.dart @@ -1,6 +1,6 @@ -import 'dart:convert'; import 'dart:io'; +import 'package:http_interop_io/http_interop_io.dart'; import 'package:json_api/http.dart'; class JsonApiServer { @@ -41,18 +41,7 @@ class JsonApiServer { Future _createServer() async { final server = await HttpServer.bind(host, port); - server.listen((request) async { - final headers = {}; - request.headers.forEach((k, v) => headers[k] = v.join(',')); - final response = await _handler.handle(HttpRequest( - request.method, request.requestedUri, - body: await request.cast>().transform(utf8.decoder).join()) - ..headers.addAll(headers)); - response.headers.forEach(request.response.headers.add); - request.response.statusCode = response.statusCode; - request.response.write(response.body); - await request.response.close(); - }); + server.listen(listener(_handler)); return server; } } diff --git a/lib/client.dart b/lib/client.dart index 7455c69..9f4f0ef 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -23,9 +23,8 @@ /// The [RoutingClient] should be your default choice. library client; +export 'package:http_interop_http/http_interop_http.dart'; export 'package:json_api/src/client/client.dart'; -export 'package:json_api/src/client/disposable_handler.dart'; -export 'package:json_api/src/client/message_converter.dart'; export 'package:json_api/src/client/persistent_handler.dart'; export 'package:json_api/src/client/request.dart'; export 'package:json_api/src/client/response/collection_fetched.dart'; diff --git a/lib/http.dart b/lib/http.dart index 724805b..f7c2930 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -1,12 +1,15 @@ /// This is a thin HTTP layer abstraction used by the client and the server library http; -export 'package:json_api/src/http/http_handler.dart'; -export 'package:json_api/src/http/http_headers.dart'; -export 'package:json_api/src/http/http_message.dart'; -export 'package:json_api/src/http/http_request.dart'; -export 'package:json_api/src/http/http_response.dart'; -export 'package:json_api/src/http/logging_handler.dart'; +export 'package:http_interop/http_interop.dart' + show + HttpMessage, + HttpHeaders, + HttpRequest, + HttpResponse, + HttpHandler, + LoggingHandler; +export 'package:json_api/src/http/http_response_ext.dart'; export 'package:json_api/src/http/media_type.dart'; export 'package:json_api/src/http/payload_codec.dart'; export 'package:json_api/src/http/status_code.dart'; diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index 1a126a4..be1a50d 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -1,5 +1,5 @@ +import 'package:http_interop_http/http_interop_http.dart'; import 'package:json_api/http.dart'; -import 'package:json_api/src/client/disposable_handler.dart'; import 'package:json_api/src/client/request.dart'; import 'package:json_api/src/client/response.dart'; diff --git a/lib/src/client/disposable_handler.dart b/lib/src/client/disposable_handler.dart deleted file mode 100644 index ab69d52..0000000 --- a/lib/src/client/disposable_handler.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:http/http.dart'; -import 'package:json_api/http.dart'; -import 'package:json_api/src/client/persistent_handler.dart'; - -/// This HTTP handler creates a new instance of the [Client] for every request -/// end disposes the client after the request completes. -class DisposableHandler implements HttpHandler { - const DisposableHandler(); - - @override - Future handle(HttpRequest request) async { - final client = Client(); - try { - return await PersistentHandler(client).handle(request); - } finally { - client.close(); - } - } -} diff --git a/lib/src/client/message_converter.dart b/lib/src/client/message_converter.dart deleted file mode 100644 index 9e830b5..0000000 --- a/lib/src/client/message_converter.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:json_api/http.dart'; - -/// Converts HTTP messages to and from to ones used by the `http` package. -class MessageConverter { - /// Creates an instance of the converter. - /// - /// Pass [defaultResponseEncoding] to use in cases when the server does - /// not specify the encoding. - MessageConverter({Encoding defaultResponseEncoding = utf8}) - : _defaultResponseEncoding = defaultResponseEncoding; - - final Encoding _defaultResponseEncoding; - - /// Converts [HttpRequest] to [Request]. - Request request(HttpRequest request) { - final converted = Request(request.method, request.uri); - final hasBody = !(request.isGet || request.isOptions); - if (hasBody) { - // The Request would set the content-type header if the body is assigned - // a value (even an empty string). We want to avoid this extra header for - // GET and OPTIONS requests. - // See https://github.com/dart-lang/http/issues/841 - converted.body = request.body; - } - converted.headers.addAll(request.headers); - return converted; - } - - /// Converts [Response] to [HttpResponse]. - HttpResponse response(Response response) { - final encoding = _encodingForHeaders(response.headers); - final body = encoding.decode(response.bodyBytes); - return HttpResponse(response.statusCode, body: body) - ..headers.addAll(response.headers); - } - - /// Returns the [Encoding] that corresponds to [charset]. - /// - /// Returns [defaultResponseEncoding] if [charset] is null or if no [Encoding] - /// was found that corresponds to [charset]. - Encoding _encodingForCharset(String? charset) { - if (charset == null) return _defaultResponseEncoding; - return Encoding.getByName(charset) ?? _defaultResponseEncoding; - } - - /// Returns the [MediaType] object for the given content-type. - /// - /// Defaults to `application/octet-stream`. - MediaType _mediaType(String? contentType) { - if (contentType != null) return MediaType.parse(contentType); - return MediaType('application', 'octet-stream'); - } - - /// Returns the encoding to use for a response with the given headers. - /// - /// Defaults to [defaultEncoding] if the headers don't specify the charset - /// or if that charset is unknown. - Encoding _encodingForHeaders(Map headers) { - final mediaType = _mediaType(headers['content-type']); - final charset = mediaType.parameters['charset']; - return _encodingForCharset(charset); - } -} diff --git a/lib/src/client/persistent_handler.dart b/lib/src/client/persistent_handler.dart index 7eacb9b..b6d5007 100644 --- a/lib/src/client/persistent_handler.dart +++ b/lib/src/client/persistent_handler.dart @@ -1,35 +1,25 @@ import 'dart:convert'; import 'package:http/http.dart'; -import 'package:json_api/http.dart'; -import 'package:json_api/src/client/message_converter.dart'; +import 'package:http_interop_http/http_interop_http.dart'; /// Handler which relies on the built-in Dart HTTP client. /// It is the developer's responsibility to instantiate the client and /// call `close()` on it in the end pf the application lifecycle. -class PersistentHandler implements HttpHandler { +class PersistentHandler extends HandlerWrapper { /// Creates a new instance of the handler. Do not forget to call `close()` on /// the [client] when it's not longer needed. /// /// Use [messageConverter] to fine tune the HTTP request/response conversion. PersistentHandler( - this.client, + Client client, {@Deprecated('Deprecated in favor of MessageConverter.' ' To be removed in version 6.0.0') this.defaultEncoding = utf8, MessageConverter? messageConverter}) - : _converter = messageConverter ?? - MessageConverter(defaultResponseEncoding: defaultEncoding); + : super(client, + messageConverter: messageConverter ?? + MessageConverter(defaultResponseEncoding: defaultEncoding)); - final Client client; final Encoding defaultEncoding; - final MessageConverter _converter; - - @override - Future handle(HttpRequest request) async { - final convertedRequest = _converter.request(request); - final streamedResponse = await client.send(convertedRequest); - final response = await Response.fromStream(streamedResponse); - return _converter.response(response); - } } diff --git a/lib/src/client/routing_client.dart b/lib/src/client/routing_client.dart index 108880b..f4c03c6 100644 --- a/lib/src/client/routing_client.dart +++ b/lib/src/client/routing_client.dart @@ -1,4 +1,5 @@ import 'package:json_api/document.dart'; +import 'package:json_api/http.dart'; import 'package:json_api/routing.dart'; import 'package:json_api/src/client/client.dart'; import 'package:json_api/src/client/request.dart'; diff --git a/lib/src/http/http_handler.dart b/lib/src/http/http_handler.dart deleted file mode 100644 index 234c125..0000000 --- a/lib/src/http/http_handler.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:json_api/src/http/http_request.dart'; -import 'package:json_api/src/http/http_response.dart'; - -abstract class HttpHandler { - Future handle(HttpRequest request); -} diff --git a/lib/src/http/http_headers.dart b/lib/src/http/http_headers.dart deleted file mode 100644 index a711104..0000000 --- a/lib/src/http/http_headers.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:collection'; - -mixin HttpHeaders { - /// Message headers. Case-insensitive. - final headers = LinkedHashMap( - equals: (a, b) => a.toLowerCase() == b.toLowerCase(), - hashCode: (s) => s.toLowerCase().hashCode); -} diff --git a/lib/src/http/http_message.dart b/lib/src/http/http_message.dart deleted file mode 100644 index 64142ac..0000000 --- a/lib/src/http/http_message.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:json_api/src/http/http_headers.dart'; - -/// HTTP message. Request or Response. -class HttpMessage with HttpHeaders { - HttpMessage(this.body); - - /// Message body - final String body; -} diff --git a/lib/src/http/http_request.dart b/lib/src/http/http_request.dart deleted file mode 100644 index 2eef17c..0000000 --- a/lib/src/http/http_request.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:json_api/src/http/http_message.dart'; - -/// The request which is sent by the client and received by the server -class HttpRequest extends HttpMessage { - HttpRequest(String method, this.uri, {String body = ''}) - : method = method.toLowerCase(), - super(body); - - /// Requested URI - final Uri uri; - - /// Request method, lowercase - final String method; - - bool get isGet => method == 'get'; - - bool get isPost => method == 'post'; - - bool get isDelete => method == 'delete'; - - bool get isPatch => method == 'patch'; - - bool get isOptions => method == 'options'; -} diff --git a/lib/src/http/http_response.dart b/lib/src/http/http_response_ext.dart similarity index 70% rename from lib/src/http/http_response.dart rename to lib/src/http/http_response_ext.dart index 76c3961..6259810 100644 --- a/lib/src/http/http_response.dart +++ b/lib/src/http/http_response_ext.dart @@ -1,14 +1,9 @@ -import 'package:json_api/src/http/http_message.dart'; +import 'package:http_interop/http_interop.dart' as interop; import 'package:json_api/src/http/media_type.dart'; import 'package:json_api/src/http/status_code.dart'; /// The response sent by the server and received by the client -class HttpResponse extends HttpMessage { - HttpResponse(this.statusCode, {String body = ''}) : super(body); - - /// Response status code - final int statusCode; - +extension HttpResponseExt on interop.HttpResponse { /// True if the body is not empty and the Content-Type /// is `application/vnd.api+json` bool get hasDocument => diff --git a/lib/src/http/logging_handler.dart b/lib/src/http/logging_handler.dart deleted file mode 100644 index 1ae8c38..0000000 --- a/lib/src/http/logging_handler.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:json_api/src/http/http_handler.dart'; -import 'package:json_api/src/http/http_request.dart'; -import 'package:json_api/src/http/http_response.dart'; - -/// A wrapper over [HttpHandler] which allows logging -class LoggingHandler implements HttpHandler { - LoggingHandler(this.handler, {this.onRequest, this.onResponse}); - - final HttpHandler handler; - final Function(HttpRequest request)? onRequest; - final Function(HttpResponse response)? onResponse; - - @override - Future handle(HttpRequest request) async { - onRequest?.call(request); - final response = await handler.handle(request); - onResponse?.call(response); - return response; - } -} diff --git a/lib/src/http/payload_codec.dart b/lib/src/http/payload_codec.dart index e3ed74b..54cf3aa 100644 --- a/lib/src/http/payload_codec.dart +++ b/lib/src/http/payload_codec.dart @@ -2,9 +2,6 @@ import 'dart:async'; import 'dart:convert'; /// Encodes/decodes JSON payload. -/// -/// The methods are designed to be asynchronous to allow for conversion to be -/// performed in isolates if needed. class PayloadCodec { const PayloadCodec(); diff --git a/pubspec.yaml b/pubspec.yaml index 4fdedbc..18d2116 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,15 @@ name: json_api -version: 5.3.0 +version: 5.4.0 homepage: https://github.com/f3ath/json-api-dart description: A framework-agnostic implementations of JSON:API Client and Server. Supports JSON:API v1.0 (https://jsonapi.org) environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.19.0 <3.0.0' dependencies: http: ^0.13.4 http_parser: ^4.0.0 + http_interop: ^0.1.0 + http_interop_http: ^0.1.1 dev_dependencies: lints: ^1.0.1 @@ -16,6 +18,7 @@ dev_dependencies: uuid: ^3.0.0 coverage: ^1.3.0 check_coverage: ^0.0.4 + http_interop_io: ^0.1.0 cider: link_template: diff --git a/test/unit/client/message_converter_test.dart b/test/unit/client/message_converter_test.dart index 966d718..be0b862 100644 --- a/test/unit/client/message_converter_test.dart +++ b/test/unit/client/message_converter_test.dart @@ -16,4 +16,9 @@ void main() { final r = converter.request(HttpRequest('OPTIONS', uri)); expect(r.headers, isEmpty); }); + + test('No headers are set for DELETE requests', () { + final r = converter.request(HttpRequest('DELETE', uri)); + expect(r.headers, isEmpty); + }); }