diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 2d8e79605..b76b60c9f 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -19,6 +19,7 @@ body: description: Which package has a problem? options: - dio + - compatibility_layer - cookie_manager - http2_adapter - native_dio_adapter diff --git a/.idea/modules.xml b/.idea/modules.xml index b5f4852fb..9b0148d30 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ + diff --git a/README-ZH.md b/README-ZH.md index b8ad3c394..1a237f1c6 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -22,6 +22,8 @@ Language: [English](README.md) | 简体中文 - cookie_manager: [链接](plugins/cookie_manager) [![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg?label=dev&include_prereleases)](https://pub.flutter-io.cn/packages/dio_cookie_manager) +- compatibility_layer: [链接](plugins/compatibility_layer) + [![Pub](https://img.shields.io/pub/v/dio_compatibility_layer.svg?label=dev&include_prereleases)](https://pub.flutter-io.cn/packages/dio_compatibility_layer) - http2_adapter: [链接](plugins/http2_adapter) [![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?label=dev&include_prereleases)](https://pub.flutter-io.cn/packages/dio_http2_adapter) - native_dio_adapter: [链接](plugins/native_dio_adapter) diff --git a/README.md b/README.md index 9e00f0025..fe977be73 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ See the [Migration Guide][] for the complete breaking changes list.** - cookie_manager: [link](plugins/cookie_manager) [![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg?label=dev&include_prereleases)](https://pub.dev/packages/dio_cookie_manager) +- compatibility_layer: [link](plugins/compatibility_layer) + [![Pub](https://img.shields.io/pub/v/dio_compatibility_layer.svg?label=dev&include_prereleases)](https://pub.dev/packages/dio_compatibility_layer) - http2_adapter: [link](plugins/http2_adapter) [![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?label=dev&include_prereleases)](https://pub.dev/packages/dio_http2_adapter) - native_dio_adapter: [link](plugins/native_dio_adapter) diff --git a/dio_workspace.iml b/dio_workspace.iml index 47f8e0a3e..135d32488 100644 --- a/dio_workspace.iml +++ b/dio_workspace.iml @@ -4,9 +4,6 @@ - - - diff --git a/plugins/compatibility_layer/.gitignore b/plugins/compatibility_layer/.gitignore new file mode 100644 index 000000000..3cceda557 --- /dev/null +++ b/plugins/compatibility_layer/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/plugins/compatibility_layer/CHANGELOG.md b/plugins/compatibility_layer/CHANGELOG.md new file mode 100644 index 000000000..2e7754e54 --- /dev/null +++ b/plugins/compatibility_layer/CHANGELOG.md @@ -0,0 +1,9 @@ +# CHANGELOG + +## Unreleased + +*None.* + +## 0.1.0 + +- Initial version. diff --git a/plugins/compatibility_layer/LICENSE b/plugins/compatibility_layer/LICENSE new file mode 100644 index 000000000..6fe7bd1ba --- /dev/null +++ b/plugins/compatibility_layer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 The CFUG Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/compatibility_layer/README.md b/plugins/compatibility_layer/README.md new file mode 100644 index 000000000..9336e563c --- /dev/null +++ b/plugins/compatibility_layer/README.md @@ -0,0 +1,47 @@ +# dio_compatibility_layer + +[![pub package](https://img.shields.io/pub/v/dio_compatibility_layer.svg)](https://pub.dev/packages/dio_compatibility_layer) +[![likes](https://img.shields.io/pub/likes/dio_compatibility_layer)](https://pub.dev/packages/dio_compatibility_layer/score) +[![popularity](https://img.shields.io/pub/popularity/dio_compatibility_layer)](https://pub.dev/packages/dio_compatibility_layer/score) +[![pub points](https://img.shields.io/pub/points/dio_compatibility_layer)](https://pub.dev/packages/dio_compatibility_layer/score) + +If you encounter bugs, consider fixing it by opening a PR or at least contribute a failing test case. + +This package contains adapters for [Dio](https://pub.dev/packages/dio) +which enables you to make use of other HTTP clients as the underlying implementation. + +Currently, it supports compatibility with +- [`http`](https://pub.dev/packages/http) + +## Get started + +### Install + +Add the `dio_compatibility_layer` package to your +[pubspec dependencies](https://pub.dev/packages/dio_compatibility_layer/install). + +### Example + +To use the `http` compatibility: + +```dart +import 'package:dio/dio.dart'; +import 'package:dio_compatibility_layer/dio_compatibility_layer.dart'; +import 'package:http/http.dart'; + +void main() async { + // Start in the `http` world. You can use `http`, `cronet_http`, + // `cupertino_http` and other `http` compatible packages. + final httpClient = Client(); + + // Make the `httpClient` compatible via the `ConversionLayerAdapter` class. + final dioAdapter = ConversionLayerAdapter(httpClient); + + // Make dio use the `httpClient` via the conversion layer. + final dio = Dio()..httpClientAdapter = dioAdapter; + + // Make a request + final response = await dio.get('https://dart.dev'); + print(response); +} +``` diff --git a/plugins/compatibility_layer/analysis_options.yaml b/plugins/compatibility_layer/analysis_options.yaml new file mode 100644 index 000000000..d11f6ec33 --- /dev/null +++ b/plugins/compatibility_layer/analysis_options.yaml @@ -0,0 +1,7 @@ +include: ../../analysis_options.yaml + +analyzer: + language: + strict-raw-types: true + strict-casts: true + strict-inference: true diff --git a/plugins/compatibility_layer/dart_test.yaml b/plugins/compatibility_layer/dart_test.yaml new file mode 100644 index 000000000..dbc48f42c --- /dev/null +++ b/plugins/compatibility_layer/dart_test.yaml @@ -0,0 +1,12 @@ +presets: + # empty placeholder required in CI scripts + all: + +override_platforms: + chrome: + settings: + headless: true + firefox: + settings: + # headless argument has to be set explicitly for non-chrome browsers + arguments: --headless diff --git a/plugins/compatibility_layer/dio_compatibility_layer.iml b/plugins/compatibility_layer/dio_compatibility_layer.iml new file mode 100644 index 000000000..04b3b5ece --- /dev/null +++ b/plugins/compatibility_layer/dio_compatibility_layer.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/plugins/compatibility_layer/example/conversion_layer_example.dart b/plugins/compatibility_layer/example/conversion_layer_example.dart new file mode 100644 index 000000000..a246b2f0b --- /dev/null +++ b/plugins/compatibility_layer/example/conversion_layer_example.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; +import 'package:dio_compatibility_layer/dio_compatibility_layer.dart'; +import 'package:http/http.dart'; + +void main() async { + // Start in the `http` world. You can use `http`, `cronet_http`, + // `cupertino_http` and other `http` compatible packages. + final httpClient = Client(); + + // Make the `httpClient` compatible via the `ConversionLayerAdapter` class. + final dioAdapter = ConversionLayerAdapter(httpClient); + + // Make dio use the `httpClient` via the conversion layer. + final dio = Dio()..httpClientAdapter = dioAdapter; + + // Make a request. + final response = await dio.get('https://dart.dev'); + print(response); +} diff --git a/plugins/compatibility_layer/lib/dio_compatibility_layer.dart b/plugins/compatibility_layer/lib/dio_compatibility_layer.dart new file mode 100644 index 000000000..41e7e91eb --- /dev/null +++ b/plugins/compatibility_layer/lib/dio_compatibility_layer.dart @@ -0,0 +1,3 @@ +library dio_compatibility_layer; + +export 'src/conversion_layer_adapter.dart'; diff --git a/plugins/compatibility_layer/lib/src/conversion_layer_adapter.dart b/plugins/compatibility_layer/lib/src/conversion_layer_adapter.dart new file mode 100644 index 000000000..87ec81e1a --- /dev/null +++ b/plugins/compatibility_layer/lib/src/conversion_layer_adapter.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:http/http.dart' as http; + +const _kIsWeb = bool.hasEnvironment('dart.library.js_util') + ? bool.fromEnvironment('dart.library.js_util') + : identical(0, 0.0); + +/// A conversion layer which translates [Dio] requests to +/// [`http`](https://pub.dev/packages/http) compatible requests. +/// This enables you to use +/// [`cronet_http`](https://pub.dev/packages/cronet_http), +/// [`cupertino_http`](https://pub.dev/packages/cupertino_http), +/// and other `http` compatible packages with [Dio]. +class ConversionLayerAdapter implements HttpClientAdapter { + ConversionLayerAdapter(this.client); + + /// The client instance from the `http` package. + final http.Client client; + + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) async { + final request = await _fromOptionsAndStream(options, requestStream); + final response = await client.send(request); + return ResponseBody( + response.stream.cast(), + response.statusCode, + statusMessage: response.reasonPhrase, + isRedirect: response.isRedirect, + headers: Map.fromEntries( + response.headers.entries.map((e) => MapEntry(e.key, [e.value])), + ), + ); + } + + @override + void close({bool force = false}) => client.close(); + + Future _fromOptionsAndStream( + RequestOptions options, + Stream? requestStream, + ) async { + final http.BaseRequest request; + if (_kIsWeb && requestStream != null) { + final normalRequest = request = http.Request( + options.method, + options.uri, + ); + final completer = Completer(); + final sink = ByteConversionSink.withCallback( + (bytes) => completer.complete( + bytes is Uint8List ? bytes : Uint8List.fromList(bytes), + ), + ); + requestStream.listen( + sink.add, + onError: completer.completeError, + onDone: sink.close, + cancelOnError: true, + ); + final bytes = await completer.future; + normalRequest.bodyBytes = bytes; + } else if (requestStream != null) { + final streamedRequest = request = http.StreamedRequest( + options.method, + options.uri, + ); + requestStream.listen( + streamedRequest.sink.add, + onError: streamedRequest.sink.addError, + onDone: streamedRequest.sink.close, + cancelOnError: true, + ); + } else { + request = http.Request(options.method, options.uri); + } + request.headers.addAll( + Map.fromEntries( + options.headers.entries.map( + (e) => MapEntry(e.key, e.value.toString().trim()), + ), + ), + ); + request + ..followRedirects = options.followRedirects + ..maxRedirects = options.maxRedirects + ..persistentConnection = options.persistentConnection; + return request; + } +} diff --git a/plugins/compatibility_layer/pubspec.yaml b/plugins/compatibility_layer/pubspec.yaml new file mode 100644 index 000000000..5916f8c36 --- /dev/null +++ b/plugins/compatibility_layer/pubspec.yaml @@ -0,0 +1,24 @@ +name: dio_compatibility_layer +version: 0.1.0 + +description: Enables dio to make use of http packages. +topics: + - dio + - http + - network + - native + - cronet +homepage: https://github.com/cfug/dio +repository: https://github.com/cfug/dio/blob/main/plugins/compatibility_layer +issue_tracker: https://github.com/cfug/dio/issues + +environment: + sdk: ^3.0.0 + +dependencies: + dio: ^5.2.0 + http: ^1.0.0 + +dev_dependencies: + lints: any + test: any diff --git a/plugins/compatibility_layer/test/client_mock.dart b/plugins/compatibility_layer/test/client_mock.dart new file mode 100644 index 000000000..184ba168b --- /dev/null +++ b/plugins/compatibility_layer/test/client_mock.dart @@ -0,0 +1,27 @@ +import 'package:http/http.dart'; + +class CloseClientMock implements Client { + bool closeWasCalled = false; + + @override + void close() { + closeWasCalled = true; + } + + @override + dynamic noSuchMethod(Invocation i) => super.noSuchMethod(i); +} + +class ClientMock implements Client { + StreamedResponse? response; + BaseRequest? request; + + @override + Future send(BaseRequest request) async { + this.request = request; + return response!; + } + + @override + dynamic noSuchMethod(Invocation i) => super.noSuchMethod(i); +} diff --git a/plugins/compatibility_layer/test/conversion_layer_adapter_test.dart b/plugins/compatibility_layer/test/conversion_layer_adapter_test.dart new file mode 100644 index 000000000..e2d956f39 --- /dev/null +++ b/plugins/compatibility_layer/test/conversion_layer_adapter_test.dart @@ -0,0 +1,64 @@ +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:dio_compatibility_layer/dio_compatibility_layer.dart'; +import 'package:http/http.dart'; +import 'package:test/test.dart'; + +import 'client_mock.dart'; + +void main() { + test('close', () { + final mock = CloseClientMock(); + final cla = ConversionLayerAdapter(mock); + + cla.close(); + + expect(mock.closeWasCalled, true); + }); + + test('close with force', () { + final mock = CloseClientMock(); + final cla = ConversionLayerAdapter(mock); + + cla.close(force: true); + + expect(mock.closeWasCalled, true); + }); + + test('headers', () async { + final mock = ClientMock()..response = StreamedResponse(Stream.empty(), 200); + final cla = ConversionLayerAdapter(mock); + + await cla.fetch( + RequestOptions(path: '', headers: {'foo': 'bar'}), + Stream.empty(), + null, + ); + + expect(mock.request?.headers, {'foo': 'bar'}); + }); + + test('download stream', () async { + final mock = ClientMock() + ..response = StreamedResponse( + Stream.fromIterable([ + Uint8List.fromList([10, 1]), + Uint8List.fromList([1, 4]), + Uint8List.fromList([5, 1]), + Uint8List.fromList([1, 1]), + Uint8List.fromList([2, 4]), + ]), + 200, + ); + final cla = ConversionLayerAdapter(mock); + + final resp = await cla.fetch( + RequestOptions(path: ''), + null, + null, + ); + + expect(await resp.stream.length, 5); + }); +}