From 74ae43e1549b9ad653d3ec769ca11cf123312a68 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Fri, 14 Oct 2022 06:25:49 +0200 Subject: [PATCH 01/18] setup open meteo api example --- .gitignore | 3 + example/open_meteo_api/analysis_options.yaml | 8 + example/open_meteo_api/build.yaml | 12 + .../open_meteo_api/lib/open_meteo_api.dart | 4 + .../lib/src/models/location.dart | 21 + .../open_meteo_api/lib/src/models/models.dart | 2 + .../lib/src/models/weather.dart | 15 + .../lib/src/open_meteo_api_client.dart | 84 +++ .../lib/src/open_meteo_api_client_fpdart.dart | 143 +++++ example/open_meteo_api/pubspec.lock | 488 ++++++++++++++++++ example/open_meteo_api/pubspec.yaml | 19 + .../open_meteo_api/test/location_test.dart | 26 + .../test/open_meteo_api_client_test.dart | 202 ++++++++ example/open_meteo_api/test/weather_test.dart | 19 + 14 files changed, 1046 insertions(+) create mode 100644 example/open_meteo_api/analysis_options.yaml create mode 100644 example/open_meteo_api/build.yaml create mode 100644 example/open_meteo_api/lib/open_meteo_api.dart create mode 100644 example/open_meteo_api/lib/src/models/location.dart create mode 100644 example/open_meteo_api/lib/src/models/models.dart create mode 100644 example/open_meteo_api/lib/src/models/weather.dart create mode 100644 example/open_meteo_api/lib/src/open_meteo_api_client.dart create mode 100644 example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart create mode 100644 example/open_meteo_api/pubspec.lock create mode 100644 example/open_meteo_api/pubspec.yaml create mode 100644 example/open_meteo_api/test/location_test.dart create mode 100644 example/open_meteo_api/test/open_meteo_api_client_test.dart create mode 100644 example/open_meteo_api/test/weather_test.dart diff --git a/.gitignore b/.gitignore index 9bddde4c..b1eebb57 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .dart_tool/ .packages + +# Generated files +**.g.dart .idea/ \ No newline at end of file diff --git a/example/open_meteo_api/analysis_options.yaml b/example/open_meteo_api/analysis_options.yaml new file mode 100644 index 00000000..1d5ab95c --- /dev/null +++ b/example/open_meteo_api/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:very_good_analysis/analysis_options.3.0.2.yaml +analyzer: + exclude: + - lib/**/*.g.dart + +linter: + rules: + public_member_api_docs: false diff --git a/example/open_meteo_api/build.yaml b/example/open_meteo_api/build.yaml new file mode 100644 index 00000000..917f4561 --- /dev/null +++ b/example/open_meteo_api/build.yaml @@ -0,0 +1,12 @@ +targets: + $default: + builders: + source_gen|combining_builder: + options: + ignore_for_file: + - implicit_dynamic_parameter + json_serializable: + options: + field_rename: snake + create_to_json: false + checked: true diff --git a/example/open_meteo_api/lib/open_meteo_api.dart b/example/open_meteo_api/lib/open_meteo_api.dart new file mode 100644 index 00000000..06a738f5 --- /dev/null +++ b/example/open_meteo_api/lib/open_meteo_api.dart @@ -0,0 +1,4 @@ +library open_meteo_api; + +export 'src/models/models.dart'; +export 'src/open_meteo_api_client.dart'; diff --git a/example/open_meteo_api/lib/src/models/location.dart b/example/open_meteo_api/lib/src/models/location.dart new file mode 100644 index 00000000..f11c46c0 --- /dev/null +++ b/example/open_meteo_api/lib/src/models/location.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'location.g.dart'; + +@JsonSerializable() +class Location { + const Location({ + required this.id, + required this.name, + required this.latitude, + required this.longitude, + }); + + factory Location.fromJson(Map json) => + _$LocationFromJson(json); + + final int id; + final String name; + final double latitude; + final double longitude; +} diff --git a/example/open_meteo_api/lib/src/models/models.dart b/example/open_meteo_api/lib/src/models/models.dart new file mode 100644 index 00000000..4f0d8637 --- /dev/null +++ b/example/open_meteo_api/lib/src/models/models.dart @@ -0,0 +1,2 @@ +export 'location.dart'; +export 'weather.dart'; diff --git a/example/open_meteo_api/lib/src/models/weather.dart b/example/open_meteo_api/lib/src/models/weather.dart new file mode 100644 index 00000000..bb2fc687 --- /dev/null +++ b/example/open_meteo_api/lib/src/models/weather.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'weather.g.dart'; + +@JsonSerializable() +class Weather { + const Weather({required this.temperature, required this.weatherCode}); + + factory Weather.fromJson(Map json) => + _$WeatherFromJson(json); + + final double temperature; + @JsonKey(name: 'weathercode') + final double weatherCode; +} diff --git a/example/open_meteo_api/lib/src/open_meteo_api_client.dart b/example/open_meteo_api/lib/src/open_meteo_api_client.dart new file mode 100644 index 00000000..103eb517 --- /dev/null +++ b/example/open_meteo_api/lib/src/open_meteo_api_client.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:open_meteo_api/open_meteo_api.dart'; + +/// Exception thrown when locationSearch fails. +class LocationRequestFailure implements Exception {} + +/// Exception thrown when the provided location is not found. +class LocationNotFoundFailure implements Exception {} + +/// Exception thrown when getWeather fails. +class WeatherRequestFailure implements Exception {} + +/// Exception thrown when weather for provided location is not found. +class WeatherNotFoundFailure implements Exception {} + +/// {@template open_meteo_api_client} +/// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). +/// {@endtemplate} +class OpenMeteoApiClient { + /// {@macro open_meteo_api_client} + OpenMeteoApiClient({http.Client? httpClient}) + : _httpClient = httpClient ?? http.Client(); + + static const _baseUrlWeather = 'api.open-meteo.com'; + static const _baseUrlGeocoding = 'geocoding-api.open-meteo.com'; + + final http.Client _httpClient; + + /// Finds a [Location] `/v1/search/?name=(query)`. + Future locationSearch(String query) async { + final locationRequest = Uri.https( + _baseUrlGeocoding, + '/v1/search', + {'name': query, 'count': '1'}, + ); + + final locationResponse = await _httpClient.get(locationRequest); + + if (locationResponse.statusCode != 200) { + throw LocationRequestFailure(); + } + + final locationJson = jsonDecode(locationResponse.body) as Map; + + if (!locationJson.containsKey('results')) throw LocationNotFoundFailure(); + + final results = locationJson['results'] as List; + + if (results.isEmpty) throw LocationNotFoundFailure(); + + return Location.fromJson(results.first as Map); + } + + /// Fetches [Weather] for a given [latitude] and [longitude]. + Future getWeather({ + required double latitude, + required double longitude, + }) async { + final weatherRequest = Uri.https(_baseUrlWeather, 'v1/forecast', { + 'latitude': '$latitude', + 'longitude': '$longitude', + 'current_weather': 'true' + }); + + final weatherResponse = await _httpClient.get(weatherRequest); + + if (weatherResponse.statusCode != 200) { + throw WeatherRequestFailure(); + } + + final bodyJson = jsonDecode(weatherResponse.body) as Map; + + if (!bodyJson.containsKey('current_weather')) { + throw WeatherNotFoundFailure(); + } + + final weatherJson = bodyJson['current_weather'] as Map; + + return Weather.fromJson(weatherJson); + } +} diff --git a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart new file mode 100644 index 00000000..71710226 --- /dev/null +++ b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fpdart/fpdart.dart'; +import 'package:http/http.dart' as http; +import 'package:open_meteo_api/open_meteo_api.dart'; + +abstract class OpenMeteoApiFailure implements Exception {} + +/// [OpenMeteoApiFailure] when **http request** fails +class LocationHttpRequestFailure implements OpenMeteoApiFailure {} + +/// [OpenMeteoApiFailure] when request is not successful (`status != 200`) +class LocationRequestFailure implements OpenMeteoApiFailure {} + +/// [OpenMeteoApiFailure] when the provided location is not found +class LocationNotFoundFailure implements OpenMeteoApiFailure {} + +/// [OpenMeteoApiFailure] when the response is not a valid [Location] +class LocationFormattingFailure implements OpenMeteoApiFailure {} + +/// [OpenMeteoApiFailure] when getWeather fails +class WeatherRequestFailure implements OpenMeteoApiFailure {} + +/// [OpenMeteoApiFailure] when weather for provided location is not found +class WeatherNotFoundFailure implements OpenMeteoApiFailure {} + +/// {@template open_meteo_api_client} +/// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). +/// {@endtemplate} +class OpenMeteoApiClient { + /// {@macro open_meteo_api_client} + OpenMeteoApiClient({http.Client? httpClient}) + : _httpClient = httpClient ?? http.Client(); + + static const _baseUrlWeather = 'api.open-meteo.com'; + static const _baseUrlGeocoding = 'geocoding-api.open-meteo.com'; + + final http.Client _httpClient; + + TaskEither locationSearch2(String query) => + TaskEither.tryCatch( + () => _httpClient.get( + Uri.https( + _baseUrlGeocoding, + '/v1/search', + {'name': query, 'count': '1'}, + ), + ), + (error, stackTrace) => LocationHttpRequestFailure(), + ) + .flatMap( + (response) => (response.statusCode != 200 + ? Either.left( + LocationRequestFailure(), + ) + : Either.of(response.body)) + .toTaskEither(), + ) + .flatMap( + (r) { + final locationJson = jsonDecode(r) as Map; + return (!locationJson.containsKey('results') + ? Either.left( + LocationNotFoundFailure(), + ) + : Either.of( + locationJson['results'], + )) + .toTaskEither(); + }, + ).flatMap( + (r) { + final results = r as List; + return (results.isEmpty + ? Either.left( + LocationNotFoundFailure(), + ) + : Either.of( + results.first, + )) + .toTaskEither(); + }, + ).flatMap( + (r) => Either.tryCatch( + () => Location.fromJson(r as Map), + (o, s) => LocationFormattingFailure(), + ).toTaskEither(), + ); + + /// Finds a [Location] `/v1/search/?name=(query)`. + Future locationSearch(String query) async { + final locationRequest = Uri.https( + _baseUrlGeocoding, + '/v1/search', + {'name': query, 'count': '1'}, + ); + + final locationResponse = await _httpClient.get(locationRequest); + + if (locationResponse.statusCode != 200) { + throw LocationRequestFailure(); + } + + final locationJson = jsonDecode(locationResponse.body) as Map; + + if (!locationJson.containsKey('results')) throw LocationNotFoundFailure(); + + final results = locationJson['results'] as List; + + if (results.isEmpty) throw LocationNotFoundFailure(); + + return Location.fromJson(results.first as Map); + } + + /// Fetches [Weather] for a given [latitude] and [longitude]. + Future getWeather({ + required double latitude, + required double longitude, + }) async { + final weatherRequest = Uri.https(_baseUrlWeather, 'v1/forecast', { + 'latitude': '$latitude', + 'longitude': '$longitude', + 'current_weather': 'true' + }); + + final weatherResponse = await _httpClient.get(weatherRequest); + + if (weatherResponse.statusCode != 200) { + throw WeatherRequestFailure(); + } + + final bodyJson = jsonDecode(weatherResponse.body) as Map; + + if (!bodyJson.containsKey('current_weather')) { + throw WeatherNotFoundFailure(); + } + + final weatherJson = bodyJson['current_weather'] as Map; + + return Weather.fromJson(weatherJson); + } +} diff --git a/example/open_meteo_api/pubspec.lock b/example/open_meteo_api/pubspec.lock new file mode 100644 index 00000000..78b7bfe9 --- /dev/null +++ b/example/open_meteo_api/pubspec.lock @@ -0,0 +1,488 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "49.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.4" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.4" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + fpdart: + dependency: "direct main" + description: + name: fpdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.5" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.7.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.12" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + mocktail: + dependency: "direct dev" + description: + name: mocktail + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.6" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.10" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.21.6" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.14" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.18" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "9.4.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.18.0 <3.0.0" diff --git a/example/open_meteo_api/pubspec.yaml b/example/open_meteo_api/pubspec.yaml new file mode 100644 index 00000000..b4defb22 --- /dev/null +++ b/example/open_meteo_api/pubspec.yaml @@ -0,0 +1,19 @@ +name: open_meteo_api +description: A Dart API Client for the Open-Meteo API. +version: 1.0.0+1 + +environment: + sdk: ">=2.18.0 <3.0.0" + +dependencies: + fpdart: ^0.3.0 + + http: ^0.13.0 + json_annotation: ^4.6.0 + +dev_dependencies: + build_runner: ^2.0.0 + json_serializable: ^6.3.1 + mocktail: ^0.3.0 + test: ^1.16.4 + very_good_analysis: ^3.0.2 diff --git a/example/open_meteo_api/test/location_test.dart b/example/open_meteo_api/test/location_test.dart new file mode 100644 index 00000000..d5c1f35e --- /dev/null +++ b/example/open_meteo_api/test/location_test.dart @@ -0,0 +1,26 @@ +import 'package:open_meteo_api/open_meteo_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Location', () { + group('fromJson', () { + test('returns correct Location object', () { + expect( + Location.fromJson( + { + 'id': 4887398, + 'name': 'Chicago', + 'latitude': 41.85003, + 'longitude': -87.65005, + }, + ), + isA() + .having((w) => w.id, 'id', 4887398) + .having((w) => w.name, 'name', 'Chicago') + .having((w) => w.latitude, 'latitude', 41.85003) + .having((w) => w.longitude, 'longitude', -87.65005), + ); + }); + }); + }); +} diff --git a/example/open_meteo_api/test/open_meteo_api_client_test.dart b/example/open_meteo_api/test/open_meteo_api_client_test.dart new file mode 100644 index 00000000..60a36aa6 --- /dev/null +++ b/example/open_meteo_api/test/open_meteo_api_client_test.dart @@ -0,0 +1,202 @@ +// ignore_for_file: prefer_const_constructors +import 'package:http/http.dart' as http; +import 'package:mocktail/mocktail.dart'; +import 'package:open_meteo_api/open_meteo_api.dart'; +import 'package:test/test.dart'; + +class MockHttpClient extends Mock implements http.Client {} + +class MockResponse extends Mock implements http.Response {} + +class FakeUri extends Fake implements Uri {} + +void main() { + group('OpenMeteoApiClient', () { + late http.Client httpClient; + late OpenMeteoApiClient apiClient; + + setUpAll(() { + registerFallbackValue(FakeUri()); + }); + + setUp(() { + httpClient = MockHttpClient(); + apiClient = OpenMeteoApiClient(httpClient: httpClient); + }); + + group('constructor', () { + test('does not require an httpClient', () { + expect(OpenMeteoApiClient(), isNotNull); + }); + }); + + group('locationSearch', () { + const query = 'mock-query'; + test('makes correct http request', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + try { + await apiClient.locationSearch(query); + } catch (_) {} + verify( + () => httpClient.get( + Uri.https( + 'geocoding-api.open-meteo.com', + '/v1/search', + {'name': query, 'count': '1'}, + ), + ), + ).called(1); + }); + + test('throws LocationRequestFailure on non-200 response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(400); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + expect( + () async => apiClient.locationSearch(query), + throwsA(isA()), + ); + }); + + test('throws LocationNotFoundFailure on error response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + await expectLater( + apiClient.locationSearch(query), + throwsA(isA()), + ); + }); + + test('throws LocationNotFoundFailure on empty response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{"results": []}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + await expectLater( + apiClient.locationSearch(query), + throwsA(isA()), + ); + }); + + test('returns Location on valid response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn( + ''' +{ + "results": [ + { + "id": 4887398, + "name": "Chicago", + "latitude": 41.85003, + "longitude": -87.65005 + } + ] +}''', + ); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + final actual = await apiClient.locationSearch(query); + expect( + actual, + isA() + .having((l) => l.name, 'name', 'Chicago') + .having((l) => l.id, 'id', 4887398) + .having((l) => l.latitude, 'latitude', 41.85003) + .having((l) => l.longitude, 'longitude', -87.65005), + ); + }); + }); + + group('getWeather', () { + const latitude = 41.85003; + const longitude = -87.6500; + + test('makes correct http request', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + try { + await apiClient.getWeather(latitude: latitude, longitude: longitude); + } catch (_) {} + verify( + () => httpClient.get( + Uri.https('api.open-meteo.com', 'v1/forecast', { + 'latitude': '$latitude', + 'longitude': '$longitude', + 'current_weather': 'true' + }), + ), + ).called(1); + }); + + test('throws WeatherRequestFailure on non-200 response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(400); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + expect( + () async => apiClient.getWeather( + latitude: latitude, + longitude: longitude, + ), + throwsA(isA()), + ); + }); + + test('throws WeatherNotFoundFailure on empty response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + expect( + () async => apiClient.getWeather( + latitude: latitude, + longitude: longitude, + ), + throwsA(isA()), + ); + }); + + test('returns weather on valid response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn( + ''' +{ +"latitude": 43, +"longitude": -87.875, +"generationtime_ms": 0.2510547637939453, +"utc_offset_seconds": 0, +"timezone": "GMT", +"timezone_abbreviation": "GMT", +"elevation": 189, +"current_weather": { +"temperature": 15.3, +"windspeed": 25.8, +"winddirection": 310, +"weathercode": 63, +"time": "2022-09-12T01:00" +} +} + ''', + ); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + final actual = await apiClient.getWeather( + latitude: latitude, + longitude: longitude, + ); + expect( + actual, + isA() + .having((w) => w.temperature, 'temperature', 15.3) + .having((w) => w.weatherCode, 'weatherCode', 63.0), + ); + }); + }); + }); +} diff --git a/example/open_meteo_api/test/weather_test.dart b/example/open_meteo_api/test/weather_test.dart new file mode 100644 index 00000000..4edfbbf8 --- /dev/null +++ b/example/open_meteo_api/test/weather_test.dart @@ -0,0 +1,19 @@ +import 'package:open_meteo_api/open_meteo_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Weather', () { + group('fromJson', () { + test('returns correct Weather object', () { + expect( + Weather.fromJson( + {'temperature': 15.3, 'weathercode': 63}, + ), + isA() + .having((w) => w.temperature, 'temperature', 15.3) + .having((w) => w.weatherCode, 'weatherCode', 63), + ); + }); + }); + }); +} From fcb1b484cc40f47b04d00d235b94db51cd8c5efd Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Fri, 14 Oct 2022 06:31:38 +0200 Subject: [PATCH 02/18] `chainEither` to TaskEither --- lib/src/task_either.dart | 8 ++++++++ test/src/task_either_test.dart | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/src/task_either.dart b/lib/src/task_either.dart index 1f14bc3b..4b48ba75 100644 --- a/lib/src/task_either.dart +++ b/lib/src/task_either.dart @@ -44,6 +44,14 @@ class TaskEither extends HKT2<_TaskEitherHKT, L, R> TaskEither bindEither(Either either) => flatMap((_) => either.toTaskEither()); + /// Chain a function that takes the current value `R` inside this [TaskEither] + /// and returns [Either]. + /// + /// Similar to `flatMap`, but `f` returns [Either] instead of [TaskEither]. + TaskEither chainEither(Either Function(R r) f) => flatMap( + (r) => f(r).toTaskEither(), + ); + /// Returns a [TaskEither] that returns a `Right(a)`. @override TaskEither pure(C a) => TaskEither(() async => Right(a)); diff --git a/test/src/task_either_test.dart b/test/src/task_either_test.dart index 0d4ea320..e3333bf1 100644 --- a/test/src/task_either_test.dart +++ b/test/src/task_either_test.dart @@ -99,6 +99,22 @@ void main() { }); }); + group('chainEither', () { + test('Right', () async { + final task = TaskEither(() async => Either.of(10)); + final ap = task.chainEither((r) => Either.of(r + 10)); + final r = await ap.run(); + r.matchTestRight((r) => expect(r, 20)); + }); + + test('Left', () async { + final task = TaskEither(() async => Either.left('abc')); + final ap = task.chainEither((r) => Either.of(r + 10)); + final r = await ap.run(); + r.matchTestLeft((l) => expect(l, 'abc')); + }); + }); + group('bindEither', () { test('Right', () async { final task = TaskEither(() async => Either.of(10)); From 4e9d3c4c56737943ac0c4cb48b5b4fbf50daabf4 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Fri, 14 Oct 2022 06:45:26 +0200 Subject: [PATCH 03/18] fpdart's `locationSearch` function --- .../lib/src/open_meteo_api_client_fpdart.dart | 96 ++++++------------- example/open_meteo_api/pubspec.lock | 6 +- example/open_meteo_api/pubspec.yaml | 4 +- 3 files changed, 37 insertions(+), 69 deletions(-) diff --git a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart index 71710226..c6ed730e 100644 --- a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart +++ b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart @@ -38,7 +38,8 @@ class OpenMeteoApiClient { final http.Client _httpClient; - TaskEither locationSearch2(String query) => + /// Finds a [Location] `/v1/search/?name=(query)`. + TaskEither locationSearch(String query) => TaskEither.tryCatch( () => _httpClient.get( Uri.https( @@ -47,71 +48,36 @@ class OpenMeteoApiClient { {'name': query, 'count': '1'}, ), ), - (error, stackTrace) => LocationHttpRequestFailure(), - ) - .flatMap( - (response) => (response.statusCode != 200 - ? Either.left( - LocationRequestFailure(), - ) - : Either.of(response.body)) - .toTaskEither(), + (_, __) => LocationHttpRequestFailure(), ) - .flatMap( - (r) { - final locationJson = jsonDecode(r) as Map; - return (!locationJson.containsKey('results') - ? Either.left( - LocationNotFoundFailure(), - ) - : Either.of( - locationJson['results'], - )) - .toTaskEither(); - }, - ).flatMap( - (r) { - final results = r as List; - return (results.isEmpty - ? Either.left( - LocationNotFoundFailure(), - ) - : Either.of( - results.first, - )) - .toTaskEither(); - }, - ).flatMap( - (r) => Either.tryCatch( - () => Location.fromJson(r as Map), - (o, s) => LocationFormattingFailure(), - ).toTaskEither(), - ); - - /// Finds a [Location] `/v1/search/?name=(query)`. - Future locationSearch(String query) async { - final locationRequest = Uri.https( - _baseUrlGeocoding, - '/v1/search', - {'name': query, 'count': '1'}, - ); - - final locationResponse = await _httpClient.get(locationRequest); - - if (locationResponse.statusCode != 200) { - throw LocationRequestFailure(); - } - - final locationJson = jsonDecode(locationResponse.body) as Map; - - if (!locationJson.containsKey('results')) throw LocationNotFoundFailure(); - - final results = locationJson['results'] as List; - - if (results.isEmpty) throw LocationNotFoundFailure(); - - return Location.fromJson(results.first as Map); - } + .chainEither( + (response) => + Either.fromPredicate( + response, + (r) => r.statusCode != 200, + (r) => LocationRequestFailure(), + ).map((r) => r.body), + ) + .chainEither( + (body) => Either>.fromPredicate( + jsonDecode(body) as Map, + (r) => r.containsKey('results'), + (r) => LocationRequestFailure(), + ).map((r) => r['results']), + ) + .chainEither( + (results) => Either.tryCatch( + () => (results as List).first, + (_, __) => LocationNotFoundFailure(), + ), + ) + .chainEither( + (r) => Either.tryCatch( + () => Location.fromJson(r as Map), + (_, __) => LocationFormattingFailure(), + ), + ); /// Fetches [Weather] for a given [latitude] and [longitude]. Future getWeather({ diff --git a/example/open_meteo_api/pubspec.lock b/example/open_meteo_api/pubspec.lock index 78b7bfe9..fc0af430 100644 --- a/example/open_meteo_api/pubspec.lock +++ b/example/open_meteo_api/pubspec.lock @@ -158,9 +158,9 @@ packages: fpdart: dependency: "direct main" description: - name: fpdart - url: "https://pub.dartlang.org" - source: hosted + path: "../.." + relative: true + source: path version: "0.3.0" frontend_server_client: dependency: transitive diff --git a/example/open_meteo_api/pubspec.yaml b/example/open_meteo_api/pubspec.yaml index b4defb22..a8f88e64 100644 --- a/example/open_meteo_api/pubspec.yaml +++ b/example/open_meteo_api/pubspec.yaml @@ -1,12 +1,14 @@ name: open_meteo_api description: A Dart API Client for the Open-Meteo API. version: 1.0.0+1 +publish_to: "none" environment: sdk: ">=2.18.0 <3.0.0" dependencies: - fpdart: ^0.3.0 + fpdart: + path: ../../. http: ^0.13.0 json_annotation: ^4.6.0 From 02b80b2b16bcab20c0e51a3abd64838ffb947f2a Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sat, 15 Oct 2022 06:14:58 +0200 Subject: [PATCH 04/18] weather request and curry2 generics names --- .../lib/src/open_meteo_api_client_fpdart.dart | 128 ++++++++++++------ lib/src/function.dart | 16 ++- 2 files changed, 97 insertions(+), 47 deletions(-) diff --git a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart index c6ed730e..82e65b98 100644 --- a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart +++ b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart @@ -1,29 +1,36 @@ -import 'dart:async'; import 'dart:convert'; import 'package:fpdart/fpdart.dart'; import 'package:http/http.dart' as http; import 'package:open_meteo_api/open_meteo_api.dart'; -abstract class OpenMeteoApiFailure implements Exception {} +abstract class OpenMeteoApiLocationFailure implements Exception {} -/// [OpenMeteoApiFailure] when **http request** fails -class LocationHttpRequestFailure implements OpenMeteoApiFailure {} +abstract class OpenMeteoApiWeatherFailure implements Exception {} -/// [OpenMeteoApiFailure] when request is not successful (`status != 200`) -class LocationRequestFailure implements OpenMeteoApiFailure {} +/// [OpenMeteoApiLocationFailure] when **http request** fails +class LocationHttpRequestFailure implements OpenMeteoApiLocationFailure {} -/// [OpenMeteoApiFailure] when the provided location is not found -class LocationNotFoundFailure implements OpenMeteoApiFailure {} +/// [OpenMeteoApiLocationFailure] when request is not successful (`status != 200`) +class LocationRequestFailure implements OpenMeteoApiLocationFailure {} -/// [OpenMeteoApiFailure] when the response is not a valid [Location] -class LocationFormattingFailure implements OpenMeteoApiFailure {} +/// [OpenMeteoApiLocationFailure] when the provided location is not found +class LocationNotFoundFailure implements OpenMeteoApiLocationFailure {} -/// [OpenMeteoApiFailure] when getWeather fails -class WeatherRequestFailure implements OpenMeteoApiFailure {} +/// [OpenMeteoApiLocationFailure] when the response is not a valid [Location] +class LocationFormattingFailure implements OpenMeteoApiLocationFailure {} -/// [OpenMeteoApiFailure] when weather for provided location is not found -class WeatherNotFoundFailure implements OpenMeteoApiFailure {} +/// [OpenMeteoApiWeatherFailure] when **http request** fails +class WeatherHttpRequestFailure implements OpenMeteoApiWeatherFailure {} + +/// [OpenMeteoApiWeatherFailure] when getWeather fails +class WeatherRequestFailure implements OpenMeteoApiWeatherFailure {} + +/// [OpenMeteoApiWeatherFailure] when weather for provided location is not found +class WeatherNotFoundFailure implements OpenMeteoApiWeatherFailure {} + +/// [OpenMeteoApiLocationFailure] when the response is not a valid [Weather] +class WeatherFormattingFailure implements OpenMeteoApiWeatherFailure {} /// {@template open_meteo_api_client} /// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). @@ -39,8 +46,9 @@ class OpenMeteoApiClient { final http.Client _httpClient; /// Finds a [Location] `/v1/search/?name=(query)`. - TaskEither locationSearch(String query) => - TaskEither.tryCatch( + TaskEither locationSearch( + String query) => + TaskEither.tryCatch( () => _httpClient.get( Uri.https( _baseUrlGeocoding, @@ -51,15 +59,15 @@ class OpenMeteoApiClient { (_, __) => LocationHttpRequestFailure(), ) .chainEither( - (response) => - Either.fromPredicate( + (response) => Either.fromPredicate( response, (r) => r.statusCode != 200, (r) => LocationRequestFailure(), ).map((r) => r.body), ) .chainEither( - (body) => Either Either>.fromPredicate( jsonDecode(body) as Map, (r) => r.containsKey('results'), @@ -67,7 +75,7 @@ class OpenMeteoApiClient { ).map((r) => r['results']), ) .chainEither( - (results) => Either.tryCatch( + (results) => Either.tryCatch( () => (results as List).first, (_, __) => LocationNotFoundFailure(), ), @@ -80,30 +88,64 @@ class OpenMeteoApiClient { ); /// Fetches [Weather] for a given [latitude] and [longitude]. - Future getWeather({ + TaskEither getWeather({ required double latitude, required double longitude, - }) async { - final weatherRequest = Uri.https(_baseUrlWeather, 'v1/forecast', { - 'latitude': '$latitude', - 'longitude': '$longitude', - 'current_weather': 'true' - }); - - final weatherResponse = await _httpClient.get(weatherRequest); - - if (weatherResponse.statusCode != 200) { - throw WeatherRequestFailure(); - } - - final bodyJson = jsonDecode(weatherResponse.body) as Map; - - if (!bodyJson.containsKey('current_weather')) { - throw WeatherNotFoundFailure(); - } - - final weatherJson = bodyJson['current_weather'] as Map; + }) => + TaskEither.tryCatch( + () async => _httpClient.get( + Uri.https( + _baseUrlWeather, + 'v1/forecast', + { + 'latitude': '$latitude', + 'longitude': '$longitude', + 'current_weather': 'true' + }, + ), + ), + (_, __) => WeatherHttpRequestFailure(), + ) + .chainEither( + _validResponseBodyCurry( + (response) => WeatherRequestFailure(), + ), + ) + .chainEither( + (body) => + (jsonDecode(body) as Map).lookup('current_weather').toEither( + WeatherRequestFailure.new, + ), + ) + .chainEither( + (results) => (results as List).head.toEither( + WeatherNotFoundFailure.new, + ), + ) + .chainEither( + (weather) => Either.tryCatch( + () => Weather.fromJson(weather as Map), + (_, __) => WeatherFormattingFailure(), + ), + ); - return Weather.fromJson(weatherJson); - } + /// Verify that the response status code is 200, + /// and extract the response's body. + Either _validResponseBody( + E Function(http.Response) onError, + http.Response response, + ) => + Either.fromPredicate( + response, + (r) => r.statusCode != 200, + onError, + ).map((r) => r.body); + + /// Use `curry2` to make the function more concise. + final _validResponseBodyCurry = curry2< + OpenMeteoApiWeatherFailure Function(http.Response), + http.Response, + Either>( + _validResponseBody, + ); } diff --git a/lib/src/function.dart b/lib/src/function.dart index 29f4e6f8..4f63ee69 100644 --- a/lib/src/function.dart +++ b/lib/src/function.dart @@ -91,15 +91,23 @@ A Function(dynamic b) constF(A a) => (dynamic b) => a; /// /// Enables the use of partial application of functions. /// This often leads to more concise function declarations. +/// +/// The generic types are in this order: +/// 1. Type of the first function parameter +/// 2. Type of the second function parameter +/// 3. Return type of the function /// ```dart /// final addFunction = (int a, int b) => a + b; -/// final add = curry(addFunction); +/// final add = curry2(addFunction); /// /// [1, 2, 3].map(add(1)) // returns [2, 3, 4] /// ``` -C Function(B b) Function(A a) curry2(C Function(A a, B b) function) { - return (A a) => (B b) => function(a, b); -} +ReturnType Function(SecondParameter second) Function(FirstParameter first) + curry2( + ReturnType Function(FirstParameter first, SecondParameter second) function, +) => + (FirstParameter first) => + (SecondParameter second) => function(first, second); /// Converts a ternary function into unary functions. D Function(C c) Function(B b) Function(A a) curry3( From 6c4ec7abe29f0884637ff93ed22570af1734871f Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sat, 15 Oct 2022 06:35:15 +0200 Subject: [PATCH 05/18] improve weather request implementation --- .../lib/src/open_meteo_api_client_fpdart.dart | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart index 82e65b98..22fcf2b8 100644 --- a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart +++ b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart @@ -21,16 +21,44 @@ class LocationNotFoundFailure implements OpenMeteoApiLocationFailure {} class LocationFormattingFailure implements OpenMeteoApiLocationFailure {} /// [OpenMeteoApiWeatherFailure] when **http request** fails -class WeatherHttpRequestFailure implements OpenMeteoApiWeatherFailure {} +class WeatherHttpRequestFailure implements OpenMeteoApiWeatherFailure { + const WeatherHttpRequestFailure(this.object, this.stackTrace); + final Object object; + final StackTrace stackTrace; +} /// [OpenMeteoApiWeatherFailure] when getWeather fails -class WeatherRequestFailure implements OpenMeteoApiWeatherFailure {} +class WeatherRequestFailure implements OpenMeteoApiWeatherFailure { + const WeatherRequestFailure(this.response); + final http.Response response; +} + +/// [OpenMeteoApiWeatherFailure] when weather response is not a valid [Map]. +class WeatherInvalidMapFailure implements OpenMeteoApiWeatherFailure { + const WeatherInvalidMapFailure(this.body); + final String body; +} + +/// [OpenMeteoApiWeatherFailure] when weather information for provided location +/// is not found (missing expected key). +class WeatherKeyNotFoundFailure implements OpenMeteoApiWeatherFailure {} + +/// [OpenMeteoApiWeatherFailure] when weather data is not a valid [List]. +class WeatherInvalidListFailure implements OpenMeteoApiWeatherFailure { + const WeatherInvalidListFailure(this.value); + final dynamic value; +} -/// [OpenMeteoApiWeatherFailure] when weather for provided location is not found -class WeatherNotFoundFailure implements OpenMeteoApiWeatherFailure {} +/// [OpenMeteoApiWeatherFailure] when weather for provided location +/// is not found (missing data). +class WeatherDataNotFoundFailure implements OpenMeteoApiWeatherFailure {} /// [OpenMeteoApiLocationFailure] when the response is not a valid [Weather] -class WeatherFormattingFailure implements OpenMeteoApiWeatherFailure {} +class WeatherFormattingFailure implements OpenMeteoApiWeatherFailure { + const WeatherFormattingFailure(this.object, this.stackTrace); + final Object object; + final StackTrace stackTrace; +} /// {@template open_meteo_api_client} /// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). @@ -104,31 +132,40 @@ class OpenMeteoApiClient { }, ), ), - (_, __) => WeatherHttpRequestFailure(), + WeatherHttpRequestFailure.new, ) + .chainEither(_validResponseBodyCurry(WeatherRequestFailure.new)) .chainEither( - _validResponseBodyCurry( - (response) => WeatherRequestFailure(), - ), + _safeCast, + String>(WeatherInvalidMapFailure.new), ) .chainEither( - (body) => - (jsonDecode(body) as Map).lookup('current_weather').toEither( - WeatherRequestFailure.new, - ), + (body) => body + .lookup('current_weather') + .toEither(WeatherKeyNotFoundFailure.new), ) .chainEither( - (results) => (results as List).head.toEither( - WeatherNotFoundFailure.new, - ), + _safeCast, dynamic>( + WeatherInvalidListFailure.new, + ), + ) + .chainEither( + (results) => results.head.toEither(WeatherDataNotFoundFailure.new), ) .chainEither( (weather) => Either.tryCatch( () => Weather.fromJson(weather as Map), - (_, __) => WeatherFormattingFailure(), + WeatherFormattingFailure.new, ), ); + Either Function(S) _safeCast( + E Function(S value) onError, + ) => + (S value) => value is T + ? Either.of(value) + : Either.left(onError(value)); + /// Verify that the response status code is 200, /// and extract the response's body. Either _validResponseBody( From 609f2b0abd019c1712cf088b66e3d631f2f713d6 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sat, 15 Oct 2022 06:53:54 +0200 Subject: [PATCH 06/18] `safeCast` in Either --- lib/src/either.dart | 13 +++++++++++++ test/src/either_test.dart | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/src/either.dart b/lib/src/either.dart index d7e2b391..f0a8313d 100644 --- a/lib/src/either.dart +++ b/lib/src/either.dart @@ -411,6 +411,19 @@ abstract class Either extends HKT2<_EitherHKT, L, R> } } + /// Safely cast a [dynamic] value to type `R`. + /// + /// If `value` is not of type `R`, then return a [Left] + /// containing the result of `onError`. + /// + /// **Note**: Make sure to specify the types of [Either] (`Either.safeCast` + /// instead of `Either.safeCast`), otherwise this will always return [Right]! + factory Either.safeCast( + dynamic value, + L Function(dynamic value) onError, + ) => + value is R ? Either.of(value) : Either.left(onError(value)); + /// Try to execute `run`. If no error occurs, then return [Right]. /// Otherwise return [Left] containing the result of `onError`. /// diff --git a/test/src/either_test.dart b/test/src/either_test.dart index 31b7a900..f1a0652b 100644 --- a/test/src/either_test.dart +++ b/test/src/either_test.dart @@ -4,7 +4,18 @@ import './utils/utils.dart'; void main() { group('Either', () { - group('[Property-based testing]', () {}); + group('[Property-based testing]', () { + group("safeCast", () { + Glados2(any.int, any.letterOrDigits) + .test('always returns Right without typed parameters', + (intValue, stringValue) { + final castInt = Either.safeCast(intValue, (value) => 'Error'); + final castString = Either.safeCast(stringValue, (value) => 'Error'); + expect(castInt, isA>()); + expect(castString, isA>()); + }); + }); + }); group('is a', () { final either = Either.of(10); @@ -1090,4 +1101,27 @@ void main() { expect(result.first, ['a', 'b']); expect(result.second, [1, 2, 3]); }); + + group('safeCast', () { + test('dynamic', () { + final castInt = Either.safeCast(10, (value) => 'Error'); + final castString = Either.safeCast('abc', (value) => 'Error'); + expect(castInt, isA>()); + expect(castString, isA>()); + }); + + test('Right', () { + final cast = Either.safeCast(10, (value) => 'Error'); + cast.matchTestRight((r) { + expect(r, 10); + }); + }); + + test('Left', () { + final cast = Either.safeCast('abc', (value) => 'Error'); + cast.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); } From c8e82f4ac2a0338213aa1b1a619aa3b613603c3a Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sat, 15 Oct 2022 07:23:47 +0200 Subject: [PATCH 07/18] cast example for Either --- example/src/either/cast.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 example/src/either/cast.dart diff --git a/example/src/either/cast.dart b/example/src/either/cast.dart new file mode 100644 index 00000000..c1ad2b05 --- /dev/null +++ b/example/src/either/cast.dart @@ -0,0 +1,25 @@ +// ignore_for_file: implicit_dynamic_parameter +import 'package:fpdart/fpdart.dart'; + +void main() { + int intValue = 10; + + /// Unhandled exception: type 'int' is not a subtype of type 'List' in type cast + final waitWhat = intValue as List; + final first = waitWhat.first; + print(first); + + /// Safe 🎯 + final wellYeah = + Either>.safeCast(intValue, (value) => 'Not an List!'); + final firstEither = wellYeah.map((list) => list.first); + print(firstEither); + + /// Verify using `is` + dynamic locationJson = 0; + + if (locationJson is List) { + final first = locationJson.first; + print(first); + } +} From 5c9597ccd5936d34882dd1793526d84e8d804d6f Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sun, 16 Oct 2022 06:12:57 +0200 Subject: [PATCH 08/18] `safeCastStrict` to Either --- .../lib/src/open_meteo_api_client_fpdart.dart | 17 +++++++---------- example/src/either/cast.dart | 7 ++++--- lib/src/either.dart | 17 ++++++++++++++++- test/src/either_test.dart | 18 ++++++++++++++++++ 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart index 22fcf2b8..79aa5f32 100644 --- a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart +++ b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart @@ -136,8 +136,10 @@ class OpenMeteoApiClient { ) .chainEither(_validResponseBodyCurry(WeatherRequestFailure.new)) .chainEither( - _safeCast, - String>(WeatherInvalidMapFailure.new), + (body) => Either.safeCastStrict< + OpenMeteoApiWeatherFailure, + Map, + String>(body, WeatherInvalidMapFailure.new), ) .chainEither( (body) => body @@ -145,7 +147,9 @@ class OpenMeteoApiClient { .toEither(WeatherKeyNotFoundFailure.new), ) .chainEither( - _safeCast, dynamic>( + (currentWeather) => + Either>.safeCast( + currentWeather, WeatherInvalidListFailure.new, ), ) @@ -159,13 +163,6 @@ class OpenMeteoApiClient { ), ); - Either Function(S) _safeCast( - E Function(S value) onError, - ) => - (S value) => value is T - ? Either.of(value) - : Either.left(onError(value)); - /// Verify that the response status code is 200, /// and extract the response's body. Either _validResponseBody( diff --git a/example/src/either/cast.dart b/example/src/either/cast.dart index c1ad2b05..f6dc0aca 100644 --- a/example/src/either/cast.dart +++ b/example/src/either/cast.dart @@ -1,4 +1,3 @@ -// ignore_for_file: implicit_dynamic_parameter import 'package:fpdart/fpdart.dart'; void main() { @@ -10,8 +9,10 @@ void main() { print(first); /// Safe 🎯 - final wellYeah = - Either>.safeCast(intValue, (value) => 'Not an List!'); + final wellYeah = Either>.safeCast( + intValue, + (dynamic value) => 'Not an List!', + ); final firstEither = wellYeah.map((list) => list.first); print(firstEither); diff --git a/lib/src/either.dart b/lib/src/either.dart index f0a8313d..554ed873 100644 --- a/lib/src/either.dart +++ b/lib/src/either.dart @@ -411,10 +411,15 @@ abstract class Either extends HKT2<_EitherHKT, L, R> } } - /// Safely cast a [dynamic] value to type `R`. + /// {@template fpdart_safe_cast_either} + /// Safely cast a value to type `R`. /// /// If `value` is not of type `R`, then return a [Left] /// containing the result of `onError`. + /// {@endtemplate} + /// + /// Less strict version of `Either.safeCastStrict`, since `safeCast` + /// assumes the value to be `dynamic`. /// /// **Note**: Make sure to specify the types of [Either] (`Either.safeCast` /// instead of `Either.safeCast`), otherwise this will always return [Right]! @@ -424,6 +429,16 @@ abstract class Either extends HKT2<_EitherHKT, L, R> ) => value is R ? Either.of(value) : Either.left(onError(value)); + /// {@macro fpdart_safe_cast_either} + /// + /// More strict version of `Either.safeCast`, in which also the **input value + /// type** must be specified (while in `Either.safeCast` the type is `dynamic`). + static Either safeCastStrict( + V value, + L Function(V value) onError, + ) => + value is R ? Either.of(value) : Either.left(onError(value)); + /// Try to execute `run`. If no error occurs, then return [Right]. /// Otherwise return [Left] containing the result of `onError`. /// diff --git a/test/src/either_test.dart b/test/src/either_test.dart index f1a0652b..d617eb68 100644 --- a/test/src/either_test.dart +++ b/test/src/either_test.dart @@ -1124,4 +1124,22 @@ void main() { }); }); }); + + group('safeCastStrict', () { + test('Right', () { + final cast = + Either.safeCastStrict(10, (value) => 'Error'); + cast.matchTestRight((r) { + expect(r, 10); + }); + }); + + test('Left', () { + final cast = + Either.safeCastStrict('abc', (value) => 'Error'); + cast.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); } From a1671297313b5db787f8af54b52d2eae53cd6ad9 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sun, 16 Oct 2022 06:23:22 +0200 Subject: [PATCH 09/18] `safeCast`, `safeCastStrict` in Option --- lib/src/either.dart | 2 +- lib/src/option.dart | 21 ++++++++++++++++++ test/src/option_test.dart | 46 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/lib/src/either.dart b/lib/src/either.dart index 554ed873..a9381e19 100644 --- a/lib/src/either.dart +++ b/lib/src/either.dart @@ -427,7 +427,7 @@ abstract class Either extends HKT2<_EitherHKT, L, R> dynamic value, L Function(dynamic value) onError, ) => - value is R ? Either.of(value) : Either.left(onError(value)); + Either.safeCastStrict(value, onError); /// {@macro fpdart_safe_cast_either} /// diff --git a/lib/src/option.dart b/lib/src/option.dart index a684e5d7..96eacf5f 100644 --- a/lib/src/option.dart +++ b/lib/src/option.dart @@ -401,6 +401,27 @@ abstract class Option extends HKT<_OptionHKT, T> static Option fromEither(Either either) => either.match((_) => Option.none(), (r) => Some(r)); + /// {@template fpdart_safe_cast_option} + /// Safely cast a value to type `T`. + /// + /// If `value` is not of type `T`, then return a [None]. + /// {@endtemplate} + /// + /// Less strict version of `Option.safeCastStrict`, since `safeCast` + /// assumes the value to be `dynamic`. + /// + /// **Note**: Make sure to specify the type of [Option] (`Option.safeCast` + /// instead of `Option.safeCast`), otherwise this will always return [Some]! + factory Option.safeCast(dynamic value) => + Option.safeCastStrict(value); + + /// {@macro fpdart_safe_cast_option} + /// + /// More strict version of `Option.safeCast`, in which also the **input value + /// type** must be specified (while in `Option.safeCast` the type is `dynamic`). + static Option safeCastStrict(V value) => + value is T ? Option.of(value) : Option.none(); + /// Return [Some] of `value` when `predicate` applied to `value` returns `true`, /// [None] otherwise. factory Option.fromPredicate(T value, bool Function(T t) predicate) => diff --git a/test/src/option_test.dart b/test/src/option_test.dart index f7b93ce4..11073e77 100644 --- a/test/src/option_test.dart +++ b/test/src/option_test.dart @@ -5,6 +5,17 @@ import './utils/utils.dart'; void main() { group('Option', () { group('[Property-based testing]', () { + group("safeCast", () { + Glados2(any.int, any.letterOrDigits) + .test('always returns Some without typed parameter', + (intValue, stringValue) { + final castInt = Option.safeCast(intValue); + final castString = Option.safeCast(stringValue); + expect(castInt, isA>()); + expect(castString, isA>()); + }); + }); + group('map', () { Glados(any.optionInt).test('should keep the same type (Some or None)', (option) { @@ -740,4 +751,39 @@ void main() { expect(map1 == map4, false); }); }); + + group('safeCast', () { + test('dynamic', () { + final castInt = Option.safeCast(10); + final castString = Option.safeCast('abc'); + expect(castInt, isA>()); + expect(castString, isA>()); + }); + + test('Some', () { + final cast = Option.safeCast(10); + cast.matchTestSome((r) { + expect(r, 10); + }); + }); + + test('None', () { + final cast = Option.safeCast('abc'); + expect(cast, isA>()); + }); + }); + + group('safeCastStrict', () { + test('Some', () { + final cast = Option.safeCastStrict(10); + cast.matchTestSome((r) { + expect(r, 10); + }); + }); + + test('None', () { + final cast = Option.safeCastStrict('abc'); + expect(cast, isA>()); + }); + }); } From 553c2afc26b58af482ffaa3bf7102818f1006489 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sun, 16 Oct 2022 06:40:37 +0200 Subject: [PATCH 10/18] tests for open meteo api --- .../open_meteo_api/lib/open_meteo_api.dart | 1 + .../lib/src/fpdart/location_failure.dart | 19 ++ .../fpdart/open_meteo_api_client_fpdart.dart | 133 ++++++++++++ .../lib/src/fpdart/weather_failure.dart | 49 +++++ .../lib/src/open_meteo_api_client_fpdart.dart | 185 ---------------- .../open_meteo_api_client_test_fpdart.dart | 202 ++++++++++++++++++ 6 files changed, 404 insertions(+), 185 deletions(-) create mode 100644 example/open_meteo_api/lib/src/fpdart/location_failure.dart create mode 100644 example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart create mode 100644 example/open_meteo_api/lib/src/fpdart/weather_failure.dart delete mode 100644 example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart create mode 100644 example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart diff --git a/example/open_meteo_api/lib/open_meteo_api.dart b/example/open_meteo_api/lib/open_meteo_api.dart index 06a738f5..90cc854d 100644 --- a/example/open_meteo_api/lib/open_meteo_api.dart +++ b/example/open_meteo_api/lib/open_meteo_api.dart @@ -1,4 +1,5 @@ library open_meteo_api; +export 'src/fpdart/open_meteo_api_client_fpdart.dart'; export 'src/models/models.dart'; export 'src/open_meteo_api_client.dart'; diff --git a/example/open_meteo_api/lib/src/fpdart/location_failure.dart b/example/open_meteo_api/lib/src/fpdart/location_failure.dart new file mode 100644 index 00000000..c4e4fcc9 --- /dev/null +++ b/example/open_meteo_api/lib/src/fpdart/location_failure.dart @@ -0,0 +1,19 @@ +abstract class OpenMeteoApiFpdartLocationFailure implements Exception {} + +/// [OpenMeteoApiFpdartLocationFailure] when **http request** fails +class LocationHttpRequestFpdartFailure + implements OpenMeteoApiFpdartLocationFailure {} + +/// [OpenMeteoApiFpdartLocationFailure] when request is not successful +/// (`status != 200`) +class LocationRequestFpdartFailure + implements OpenMeteoApiFpdartLocationFailure {} + +/// [OpenMeteoApiFpdartLocationFailure] when the provided location is not found +class LocationNotFoundFpdartFailure + implements OpenMeteoApiFpdartLocationFailure {} + +/// [OpenMeteoApiFpdartLocationFailure] when the response is not +/// a valid [Location] +class LocationFormattingFpdartFailure + implements OpenMeteoApiFpdartLocationFailure {} diff --git a/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart new file mode 100644 index 00000000..18d88794 --- /dev/null +++ b/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +import 'package:fpdart/fpdart.dart'; +import 'package:http/http.dart' as http; +import 'package:open_meteo_api/open_meteo_api.dart'; +import 'package:open_meteo_api/src/fpdart/location_failure.dart'; +import 'package:open_meteo_api/src/fpdart/weather_failure.dart'; + +/// {@template open_meteo_api_client} +/// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). +/// {@endtemplate} +class OpenMeteoApiClientFpdart { + /// {@macro open_meteo_api_client} + OpenMeteoApiClientFpdart({http.Client? httpClient}) + : _httpClient = httpClient ?? http.Client(); + + static const _baseUrlWeather = 'api.open-meteo.com'; + static const _baseUrlGeocoding = 'geocoding-api.open-meteo.com'; + + final http.Client _httpClient; + + /// Finds a [Location] `/v1/search/?name=(query)`. + TaskEither locationSearch( + String query) => + TaskEither.tryCatch( + () => _httpClient.get( + Uri.https( + _baseUrlGeocoding, + '/v1/search', + {'name': query, 'count': '1'}, + ), + ), + (_, __) => LocationHttpRequestFpdartFailure(), + ) + .chainEither( + (response) => Either.fromPredicate( + response, + (r) => r.statusCode != 200, + (r) => LocationRequestFpdartFailure(), + ).map((r) => r.body), + ) + .chainEither( + (body) => Either>.fromPredicate( + jsonDecode(body) as Map, + (r) => r.containsKey('results'), + (r) => LocationRequestFpdartFailure(), + ).map((r) => r['results']), + ) + .chainEither( + (results) => + Either.tryCatch( + () => (results as List).first, + (_, __) => LocationNotFoundFpdartFailure(), + ), + ) + .chainEither( + (r) => Either.tryCatch( + () => Location.fromJson(r as Map), + (_, __) => LocationFormattingFpdartFailure(), + ), + ); + + /// Fetches [Weather] for a given [latitude] and [longitude]. + TaskEither getWeather({ + required double latitude, + required double longitude, + }) => + TaskEither.tryCatch( + () async => _httpClient.get( + Uri.https( + _baseUrlWeather, + 'v1/forecast', + { + 'latitude': '$latitude', + 'longitude': '$longitude', + 'current_weather': 'true' + }, + ), + ), + WeatherHttpRequestFpdartFailure.new, + ) + .chainEither(_validResponseBodyCurry(WeatherRequestFpdartFailure.new)) + .chainEither( + (body) => Either.safeCastStrict< + OpenMeteoApiFpdartWeatherFailure, + Map, + String>(body, WeatherInvalidMapFpdartFailure.new), + ) + .chainEither( + (body) => body + .lookup('current_weather') + .toEither(WeatherKeyNotFoundFpdartFailure.new), + ) + .chainEither( + (currentWeather) => Either>.safeCast( + currentWeather, + WeatherInvalidListFpdartFailure.new, + ), + ) + .chainEither( + (results) => + results.head.toEither(WeatherDataNotFoundFpdartFailure.new), + ) + .chainEither( + (weather) => Either.tryCatch( + () => Weather.fromJson(weather as Map), + WeatherFormattingFpdartFailure.new, + ), + ); + + /// Verify that the response status code is 200, + /// and extract the response's body. + Either _validResponseBody( + E Function(http.Response) onError, + http.Response response, + ) => + Either.fromPredicate( + response, + (r) => r.statusCode != 200, + onError, + ).map((r) => r.body); + + /// Use `curry2` to make the function more concise. + final _validResponseBodyCurry = curry2< + OpenMeteoApiFpdartWeatherFailure Function(http.Response), + http.Response, + Either>( + _validResponseBody, + ); +} diff --git a/example/open_meteo_api/lib/src/fpdart/weather_failure.dart b/example/open_meteo_api/lib/src/fpdart/weather_failure.dart new file mode 100644 index 00000000..1f88c3c4 --- /dev/null +++ b/example/open_meteo_api/lib/src/fpdart/weather_failure.dart @@ -0,0 +1,49 @@ +import 'package:http/http.dart' as http; + +abstract class OpenMeteoApiFpdartWeatherFailure implements Exception {} + +/// [OpenMeteoApiFpdartWeatherFailure] when **http request** fails +class WeatherHttpRequestFpdartFailure + implements OpenMeteoApiFpdartWeatherFailure { + const WeatherHttpRequestFpdartFailure(this.object, this.stackTrace); + final Object object; + final StackTrace stackTrace; +} + +/// [OpenMeteoApiFpdartWeatherFailure] when getWeather fails +class WeatherRequestFpdartFailure implements OpenMeteoApiFpdartWeatherFailure { + const WeatherRequestFpdartFailure(this.response); + final http.Response response; +} + +/// [OpenMeteoApiFpdartWeatherFailure] when weather response is not a valid [Map]. +class WeatherInvalidMapFpdartFailure + implements OpenMeteoApiFpdartWeatherFailure { + const WeatherInvalidMapFpdartFailure(this.body); + final String body; +} + +/// [OpenMeteoApiFpdartWeatherFailure] when weather information for provided location +/// is not found (missing expected key). +class WeatherKeyNotFoundFpdartFailure + implements OpenMeteoApiFpdartWeatherFailure {} + +/// [OpenMeteoApiFpdartWeatherFailure] when weather data is not a valid [List]. +class WeatherInvalidListFpdartFailure + implements OpenMeteoApiFpdartWeatherFailure { + const WeatherInvalidListFpdartFailure(this.value); + final dynamic value; +} + +/// [OpenMeteoApiFpdartWeatherFailure] when weather for provided location +/// is not found (missing data). +class WeatherDataNotFoundFpdartFailure + implements OpenMeteoApiFpdartWeatherFailure {} + +/// [OpenMeteoApiFpdartWeatherFailure] when the response is not a valid [Weather] +class WeatherFormattingFpdartFailure + implements OpenMeteoApiFpdartWeatherFailure { + const WeatherFormattingFpdartFailure(this.object, this.stackTrace); + final Object object; + final StackTrace stackTrace; +} diff --git a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart deleted file mode 100644 index 79aa5f32..00000000 --- a/example/open_meteo_api/lib/src/open_meteo_api_client_fpdart.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'dart:convert'; - -import 'package:fpdart/fpdart.dart'; -import 'package:http/http.dart' as http; -import 'package:open_meteo_api/open_meteo_api.dart'; - -abstract class OpenMeteoApiLocationFailure implements Exception {} - -abstract class OpenMeteoApiWeatherFailure implements Exception {} - -/// [OpenMeteoApiLocationFailure] when **http request** fails -class LocationHttpRequestFailure implements OpenMeteoApiLocationFailure {} - -/// [OpenMeteoApiLocationFailure] when request is not successful (`status != 200`) -class LocationRequestFailure implements OpenMeteoApiLocationFailure {} - -/// [OpenMeteoApiLocationFailure] when the provided location is not found -class LocationNotFoundFailure implements OpenMeteoApiLocationFailure {} - -/// [OpenMeteoApiLocationFailure] when the response is not a valid [Location] -class LocationFormattingFailure implements OpenMeteoApiLocationFailure {} - -/// [OpenMeteoApiWeatherFailure] when **http request** fails -class WeatherHttpRequestFailure implements OpenMeteoApiWeatherFailure { - const WeatherHttpRequestFailure(this.object, this.stackTrace); - final Object object; - final StackTrace stackTrace; -} - -/// [OpenMeteoApiWeatherFailure] when getWeather fails -class WeatherRequestFailure implements OpenMeteoApiWeatherFailure { - const WeatherRequestFailure(this.response); - final http.Response response; -} - -/// [OpenMeteoApiWeatherFailure] when weather response is not a valid [Map]. -class WeatherInvalidMapFailure implements OpenMeteoApiWeatherFailure { - const WeatherInvalidMapFailure(this.body); - final String body; -} - -/// [OpenMeteoApiWeatherFailure] when weather information for provided location -/// is not found (missing expected key). -class WeatherKeyNotFoundFailure implements OpenMeteoApiWeatherFailure {} - -/// [OpenMeteoApiWeatherFailure] when weather data is not a valid [List]. -class WeatherInvalidListFailure implements OpenMeteoApiWeatherFailure { - const WeatherInvalidListFailure(this.value); - final dynamic value; -} - -/// [OpenMeteoApiWeatherFailure] when weather for provided location -/// is not found (missing data). -class WeatherDataNotFoundFailure implements OpenMeteoApiWeatherFailure {} - -/// [OpenMeteoApiLocationFailure] when the response is not a valid [Weather] -class WeatherFormattingFailure implements OpenMeteoApiWeatherFailure { - const WeatherFormattingFailure(this.object, this.stackTrace); - final Object object; - final StackTrace stackTrace; -} - -/// {@template open_meteo_api_client} -/// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). -/// {@endtemplate} -class OpenMeteoApiClient { - /// {@macro open_meteo_api_client} - OpenMeteoApiClient({http.Client? httpClient}) - : _httpClient = httpClient ?? http.Client(); - - static const _baseUrlWeather = 'api.open-meteo.com'; - static const _baseUrlGeocoding = 'geocoding-api.open-meteo.com'; - - final http.Client _httpClient; - - /// Finds a [Location] `/v1/search/?name=(query)`. - TaskEither locationSearch( - String query) => - TaskEither.tryCatch( - () => _httpClient.get( - Uri.https( - _baseUrlGeocoding, - '/v1/search', - {'name': query, 'count': '1'}, - ), - ), - (_, __) => LocationHttpRequestFailure(), - ) - .chainEither( - (response) => Either.fromPredicate( - response, - (r) => r.statusCode != 200, - (r) => LocationRequestFailure(), - ).map((r) => r.body), - ) - .chainEither( - (body) => Either>.fromPredicate( - jsonDecode(body) as Map, - (r) => r.containsKey('results'), - (r) => LocationRequestFailure(), - ).map((r) => r['results']), - ) - .chainEither( - (results) => Either.tryCatch( - () => (results as List).first, - (_, __) => LocationNotFoundFailure(), - ), - ) - .chainEither( - (r) => Either.tryCatch( - () => Location.fromJson(r as Map), - (_, __) => LocationFormattingFailure(), - ), - ); - - /// Fetches [Weather] for a given [latitude] and [longitude]. - TaskEither getWeather({ - required double latitude, - required double longitude, - }) => - TaskEither.tryCatch( - () async => _httpClient.get( - Uri.https( - _baseUrlWeather, - 'v1/forecast', - { - 'latitude': '$latitude', - 'longitude': '$longitude', - 'current_weather': 'true' - }, - ), - ), - WeatherHttpRequestFailure.new, - ) - .chainEither(_validResponseBodyCurry(WeatherRequestFailure.new)) - .chainEither( - (body) => Either.safeCastStrict< - OpenMeteoApiWeatherFailure, - Map, - String>(body, WeatherInvalidMapFailure.new), - ) - .chainEither( - (body) => body - .lookup('current_weather') - .toEither(WeatherKeyNotFoundFailure.new), - ) - .chainEither( - (currentWeather) => - Either>.safeCast( - currentWeather, - WeatherInvalidListFailure.new, - ), - ) - .chainEither( - (results) => results.head.toEither(WeatherDataNotFoundFailure.new), - ) - .chainEither( - (weather) => Either.tryCatch( - () => Weather.fromJson(weather as Map), - WeatherFormattingFailure.new, - ), - ); - - /// Verify that the response status code is 200, - /// and extract the response's body. - Either _validResponseBody( - E Function(http.Response) onError, - http.Response response, - ) => - Either.fromPredicate( - response, - (r) => r.statusCode != 200, - onError, - ).map((r) => r.body); - - /// Use `curry2` to make the function more concise. - final _validResponseBodyCurry = curry2< - OpenMeteoApiWeatherFailure Function(http.Response), - http.Response, - Either>( - _validResponseBody, - ); -} diff --git a/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart b/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart new file mode 100644 index 00000000..3e72673d --- /dev/null +++ b/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart @@ -0,0 +1,202 @@ +// ignore_for_file: prefer_const_constructors +import 'package:fpdart/fpdart.dart'; +import 'package:http/http.dart' as http; +import 'package:mocktail/mocktail.dart'; +import 'package:open_meteo_api/open_meteo_api.dart'; +import 'package:open_meteo_api/src/fpdart/location_failure.dart'; +import 'package:test/test.dart'; + +class MockHttpClient extends Mock implements http.Client {} + +class MockResponse extends Mock implements http.Response {} + +class FakeUri extends Fake implements Uri {} + +void main() { + group('OpenMeteoApiClientFpdart', () { + late http.Client httpClient; + late OpenMeteoApiClientFpdart apiClient; + + setUpAll(() { + registerFallbackValue(FakeUri()); + }); + + setUp(() { + httpClient = MockHttpClient(); + apiClient = OpenMeteoApiClientFpdart(httpClient: httpClient); + }); + + group('constructor', () { + test('does not require an httpClient', () { + expect(OpenMeteoApiClientFpdart(), isNotNull); + }); + }); + + group('locationSearch', () { + const query = 'mock-query'; + test('makes correct http request', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + try { + await apiClient.locationSearch(query).run(); + } catch (_) {} + verify( + () => httpClient.get( + Uri.https( + 'geocoding-api.open-meteo.com', + '/v1/search', + {'name': query, 'count': '1'}, + ), + ), + ).called(1); + }); + + test('throws LocationRequestFailure on non-200 response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(400); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + expect(result, isA>()); + }); + + test('throws LocationNotFoundFailure on error response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + expect(result, isA>()); + }); + + test('throws LocationNotFoundFailure on empty response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{"results": []}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + expect(result, isA>()); + }); + + test('returns Location on valid response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn( + ''' +{ + "results": [ + { + "id": 4887398, + "name": "Chicago", + "latitude": 41.85003, + "longitude": -87.65005 + } + ] +}''', + ); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + expect( + result, + isA>() + .having((l) => l.value.name, 'name', 'Chicago') + .having((l) => l.value.id, 'id', 4887398) + .having((l) => l.value.latitude, 'latitude', 41.85003) + .having((l) => l.value.longitude, 'longitude', -87.65005), + ); + }); + }); + +// group('getWeather', () { +// const latitude = 41.85003; +// const longitude = -87.6500; + +// test('makes correct http request', () async { +// final response = MockResponse(); +// when(() => response.statusCode).thenReturn(200); +// when(() => response.body).thenReturn('{}'); +// when(() => httpClient.get(any())).thenAnswer((_) async => response); +// try { +// await apiClient.getWeather(latitude: latitude, longitude: longitude); +// } catch (_) {} +// verify( +// () => httpClient.get( +// Uri.https('api.open-meteo.com', 'v1/forecast', { +// 'latitude': '$latitude', +// 'longitude': '$longitude', +// 'current_weather': 'true' +// }), +// ), +// ).called(1); +// }); + +// test('throws WeatherRequestFailure on non-200 response', () async { +// final response = MockResponse(); +// when(() => response.statusCode).thenReturn(400); +// when(() => httpClient.get(any())).thenAnswer((_) async => response); +// expect( +// () async => apiClient.getWeather( +// latitude: latitude, +// longitude: longitude, +// ), +// throwsA(isA()), +// ); +// }); + +// test('throws WeatherNotFoundFailure on empty response', () async { +// final response = MockResponse(); +// when(() => response.statusCode).thenReturn(200); +// when(() => response.body).thenReturn('{}'); +// when(() => httpClient.get(any())).thenAnswer((_) async => response); +// expect( +// () async => apiClient.getWeather( +// latitude: latitude, +// longitude: longitude, +// ), +// throwsA(isA()), +// ); +// }); + +// test('returns weather on valid response', () async { +// final response = MockResponse(); +// when(() => response.statusCode).thenReturn(200); +// when(() => response.body).thenReturn( +// ''' +// { +// "latitude": 43, +// "longitude": -87.875, +// "generationtime_ms": 0.2510547637939453, +// "utc_offset_seconds": 0, +// "timezone": "GMT", +// "timezone_abbreviation": "GMT", +// "elevation": 189, +// "current_weather": { +// "temperature": 15.3, +// "windspeed": 25.8, +// "winddirection": 310, +// "weathercode": 63, +// "time": "2022-09-12T01:00" +// } +// } +// ''', +// ); +// when(() => httpClient.get(any())).thenAnswer((_) async => response); +// final actual = await apiClient.getWeather( +// latitude: latitude, +// longitude: longitude, +// ); +// expect( +// actual, +// isA() +// .having((w) => w.temperature, 'temperature', 15.3) +// .having((w) => w.weatherCode, 'weatherCode', 63.0), +// ); +// }); +// }); + }); +} From 7d69212c4a5c664b99e0b6c1771981d419b98e33 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Mon, 17 Oct 2022 06:43:21 +0200 Subject: [PATCH 11/18] valid tests for fpdart open meteo api --- .../lib/src/fpdart/location_failure.dart | 49 ++++++- .../fpdart/open_meteo_api_client_fpdart.dart | 67 ++++----- .../lib/src/fpdart/weather_failure.dart | 2 +- .../open_meteo_api_client_test_fpdart.dart | 131 ++++++++++++++++-- 4 files changed, 204 insertions(+), 45 deletions(-) diff --git a/example/open_meteo_api/lib/src/fpdart/location_failure.dart b/example/open_meteo_api/lib/src/fpdart/location_failure.dart index c4e4fcc9..09fe8dab 100644 --- a/example/open_meteo_api/lib/src/fpdart/location_failure.dart +++ b/example/open_meteo_api/lib/src/fpdart/location_failure.dart @@ -1,19 +1,60 @@ +import 'package:http/http.dart' as http; + abstract class OpenMeteoApiFpdartLocationFailure implements Exception {} /// [OpenMeteoApiFpdartLocationFailure] when **http request** fails class LocationHttpRequestFpdartFailure - implements OpenMeteoApiFpdartLocationFailure {} + implements OpenMeteoApiFpdartLocationFailure { + const LocationHttpRequestFpdartFailure(this.object, this.stackTrace); + final Object object; + final StackTrace stackTrace; +} /// [OpenMeteoApiFpdartLocationFailure] when request is not successful /// (`status != 200`) class LocationRequestFpdartFailure + implements OpenMeteoApiFpdartLocationFailure { + const LocationRequestFpdartFailure(this.response); + final http.Response response; +} + +/// [OpenMeteoApiFpdartLocationFailure] when location response +/// cannot be decoded from json. +class LocationInvalidJsonDecodeFpdartFailure + implements OpenMeteoApiFpdartLocationFailure { + const LocationInvalidJsonDecodeFpdartFailure(this.body); + final String body; +} + +/// [OpenMeteoApiFpdartLocationFailure] when location response is not a valid [Map]. +class LocationInvalidMapFpdartFailure + implements OpenMeteoApiFpdartLocationFailure { + const LocationInvalidMapFpdartFailure(this.json); + final dynamic json; +} + +/// [OpenMeteoApiFpdartLocationFailure] when location information +/// is not found (missing expected key). +class LocationKeyNotFoundFpdartFailure implements OpenMeteoApiFpdartLocationFailure {} -/// [OpenMeteoApiFpdartLocationFailure] when the provided location is not found -class LocationNotFoundFpdartFailure +/// [OpenMeteoApiFpdartLocationFailure] when location data is not a valid [List]. +class LocationInvalidListFpdartFailure + implements OpenMeteoApiFpdartLocationFailure { + const LocationInvalidListFpdartFailure(this.value); + final dynamic value; +} + +/// [OpenMeteoApiFpdartLocationFailure] when location for provided location +/// is not found (missing data). +class LocationDataNotFoundFpdartFailure implements OpenMeteoApiFpdartLocationFailure {} /// [OpenMeteoApiFpdartLocationFailure] when the response is not /// a valid [Location] class LocationFormattingFpdartFailure - implements OpenMeteoApiFpdartLocationFailure {} + implements OpenMeteoApiFpdartLocationFailure { + const LocationFormattingFpdartFailure(this.object, this.stackTrace); + final Object object; + final StackTrace stackTrace; +} diff --git a/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart index 18d88794..b8c9332d 100644 --- a/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart +++ b/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart @@ -30,35 +30,45 @@ class OpenMeteoApiClientFpdart { {'name': query, 'count': '1'}, ), ), - (_, __) => LocationHttpRequestFpdartFailure(), + LocationHttpRequestFpdartFailure.new, ) .chainEither( - (response) => Either.fromPredicate( - response, - (r) => r.statusCode != 200, - (r) => LocationRequestFpdartFailure(), - ).map((r) => r.body), + (response) => + _validResponseBody(response, LocationRequestFpdartFailure.new), ) .chainEither( - (body) => Either>.fromPredicate( - jsonDecode(body) as Map, - (r) => r.containsKey('results'), - (r) => LocationRequestFpdartFailure(), - ).map((r) => r['results']), + (body) => Either.tryCatch( + () => jsonDecode(body), + (_, __) => LocationInvalidJsonDecodeFpdartFailure(body), + ), ) .chainEither( - (results) => - Either.tryCatch( - () => (results as List).first, - (_, __) => LocationNotFoundFpdartFailure(), + (json) => Either>.safeCast( + json, + LocationInvalidMapFpdartFailure.new, ), ) .chainEither( - (r) => Either.tryCatch( - () => Location.fromJson(r as Map), - (_, __) => LocationFormattingFpdartFailure(), + (body) => body + .lookup('results') + .toEither(LocationKeyNotFoundFpdartFailure.new), + ) + .chainEither( + (currentWeather) => Either>.safeCast( + currentWeather, + LocationInvalidListFpdartFailure.new, + ), + ) + .chainEither( + (results) => + results.head.toEither(LocationDataNotFoundFpdartFailure.new), + ) + .chainEither( + (weather) => Either.tryCatch( + () => Location.fromJson(weather as Map), + LocationFormattingFpdartFailure.new, ), ); @@ -81,7 +91,10 @@ class OpenMeteoApiClientFpdart { ), WeatherHttpRequestFpdartFailure.new, ) - .chainEither(_validResponseBodyCurry(WeatherRequestFpdartFailure.new)) + .chainEither( + (response) => + _validResponseBody(response, WeatherRequestFpdartFailure.new), + ) .chainEither( (body) => Either.safeCastStrict< OpenMeteoApiFpdartWeatherFailure, @@ -114,20 +127,12 @@ class OpenMeteoApiClientFpdart { /// Verify that the response status code is 200, /// and extract the response's body. Either _validResponseBody( - E Function(http.Response) onError, http.Response response, + E Function(http.Response) onError, ) => Either.fromPredicate( response, - (r) => r.statusCode != 200, + (r) => r.statusCode == 200, onError, ).map((r) => r.body); - - /// Use `curry2` to make the function more concise. - final _validResponseBodyCurry = curry2< - OpenMeteoApiFpdartWeatherFailure Function(http.Response), - http.Response, - Either>( - _validResponseBody, - ); } diff --git a/example/open_meteo_api/lib/src/fpdart/weather_failure.dart b/example/open_meteo_api/lib/src/fpdart/weather_failure.dart index 1f88c3c4..7b984850 100644 --- a/example/open_meteo_api/lib/src/fpdart/weather_failure.dart +++ b/example/open_meteo_api/lib/src/fpdart/weather_failure.dart @@ -23,7 +23,7 @@ class WeatherInvalidMapFpdartFailure final String body; } -/// [OpenMeteoApiFpdartWeatherFailure] when weather information for provided location +/// [OpenMeteoApiFpdartWeatherFailure] when weather information /// is not found (missing expected key). class WeatherKeyNotFoundFpdartFailure implements OpenMeteoApiFpdartWeatherFailure {} diff --git a/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart b/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart index 3e72673d..3266d8ea 100644 --- a/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart +++ b/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart @@ -12,6 +12,17 @@ class MockResponse extends Mock implements http.Response {} class FakeUri extends Fake implements Uri {} +void _isLeftOfType( + Either result, { + dynamic Function(TypeMatcher)? typeMatch, +}) { + expect(result, isA>()); + result.match( + (l) => expect(l, typeMatch != null ? typeMatch(isA()) : isA()), + (_) => fail('should not be right'), + ); +} + void main() { group('OpenMeteoApiClientFpdart', () { late http.Client httpClient; @@ -39,9 +50,10 @@ void main() { when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); - try { - await apiClient.locationSearch(query).run(); - } catch (_) {} + + /// No need of try/catch + await apiClient.locationSearch(query).run(); + verify( () => httpClient.get( Uri.https( @@ -53,33 +65,134 @@ void main() { ).called(1); }); - test('throws LocationRequestFailure on non-200 response', () async { + test('returns LocationHttpRequestFpdartFailure when http request fails', + () async { + when(() => httpClient.get(any())).thenThrow(Exception()); + + final result = await apiClient.locationSearch(query).run(); + + _isLeftOfType( + result, + typeMatch: (m) => m.having( + (failure) => failure.object, + 'Exception', + isA(), + ), + ); + }); + + test('returns LocationRequestFpdartFailure on non-200 response', + () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(400); when(() => httpClient.get(any())).thenAnswer((_) async => response); final result = await apiClient.locationSearch(query).run(); - expect(result, isA>()); + + _isLeftOfType( + result, + typeMatch: (m) => m.having( + (failure) => failure.response, + 'MockResponse', + response, + ), + ); + }); + + test( + 'returns LocationInvalidJsonDecodeFpdartFailure when response is invalid', + () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('_{}_'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + + _isLeftOfType(result); }); - test('throws LocationNotFoundFailure on error response', () async { + test('returns LocationInvalidMapFpdartFailure when response is not a Map', + () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('[]'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + + _isLeftOfType(result); + }); + + test( + 'returns LocationKeyNotFoundFpdartFailure when the response is missing the correct key', + () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); final result = await apiClient.locationSearch(query).run(); - expect(result, isA>()); + + _isLeftOfType(result); }); - test('throws LocationNotFoundFailure on empty response', () async { + test( + 'returns LocationInvalidListFpdartFailure when Map key does not contain a valid List', + () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{"results": {}}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + + _isLeftOfType(result); + }); + + test('returns LocationDataNotFoundFpdartFailure on empty response', + () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{"results": []}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); final result = await apiClient.locationSearch(query).run(); - expect(result, isA>()); + + _isLeftOfType(result); + }); + + test( + 'returns LocationFormattingFpdartFailure when response is not a correct Location object', + () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn( + ''' +{ + "results": [ + { + "_id": 4887398, + "_name": "Chicago", + "_latitude": 41.85003, + "_longitude": -87.65005 + } + ] +}''', + ); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final result = await apiClient.locationSearch(query).run(); + + _isLeftOfType(result); }); test('returns Location on valid response', () async { From 7a83ab688a0eb3b2ae99f8a8b42a8ead331d2800 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Tue, 18 Oct 2022 05:46:25 +0200 Subject: [PATCH 12/18] completed tests for meteo location --- .../open_meteo_api_client_test_fpdart.dart | 114 ++++-------------- 1 file changed, 24 insertions(+), 90 deletions(-) diff --git a/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart b/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart index 3266d8ea..207867bd 100644 --- a/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart +++ b/example/open_meteo_api/test/open_meteo_api_client_test_fpdart.dart @@ -112,7 +112,14 @@ void main() { final result = await apiClient.locationSearch(query).run(); _isLeftOfType(result); + LocationInvalidJsonDecodeFpdartFailure>( + result, + typeMatch: (m) => m.having( + (failure) => failure.body, + 'body', + '_{}_', + ), + ); }); test('returns LocationInvalidMapFpdartFailure when response is not a Map', @@ -125,7 +132,14 @@ void main() { final result = await apiClient.locationSearch(query).run(); _isLeftOfType(result); + LocationInvalidMapFpdartFailure>( + result, + typeMatch: (m) => m.having( + (failure) => failure.json, + 'json', + [], + ), + ); }); test( @@ -153,7 +167,14 @@ void main() { final result = await apiClient.locationSearch(query).run(); _isLeftOfType(result); + LocationInvalidListFpdartFailure>( + result, + typeMatch: (m) => m.having( + (failure) => failure.value, + 'value', + {}, + ), + ); }); test('returns LocationDataNotFoundFpdartFailure on empty response', @@ -224,92 +245,5 @@ void main() { ); }); }); - -// group('getWeather', () { -// const latitude = 41.85003; -// const longitude = -87.6500; - -// test('makes correct http request', () async { -// final response = MockResponse(); -// when(() => response.statusCode).thenReturn(200); -// when(() => response.body).thenReturn('{}'); -// when(() => httpClient.get(any())).thenAnswer((_) async => response); -// try { -// await apiClient.getWeather(latitude: latitude, longitude: longitude); -// } catch (_) {} -// verify( -// () => httpClient.get( -// Uri.https('api.open-meteo.com', 'v1/forecast', { -// 'latitude': '$latitude', -// 'longitude': '$longitude', -// 'current_weather': 'true' -// }), -// ), -// ).called(1); -// }); - -// test('throws WeatherRequestFailure on non-200 response', () async { -// final response = MockResponse(); -// when(() => response.statusCode).thenReturn(400); -// when(() => httpClient.get(any())).thenAnswer((_) async => response); -// expect( -// () async => apiClient.getWeather( -// latitude: latitude, -// longitude: longitude, -// ), -// throwsA(isA()), -// ); -// }); - -// test('throws WeatherNotFoundFailure on empty response', () async { -// final response = MockResponse(); -// when(() => response.statusCode).thenReturn(200); -// when(() => response.body).thenReturn('{}'); -// when(() => httpClient.get(any())).thenAnswer((_) async => response); -// expect( -// () async => apiClient.getWeather( -// latitude: latitude, -// longitude: longitude, -// ), -// throwsA(isA()), -// ); -// }); - -// test('returns weather on valid response', () async { -// final response = MockResponse(); -// when(() => response.statusCode).thenReturn(200); -// when(() => response.body).thenReturn( -// ''' -// { -// "latitude": 43, -// "longitude": -87.875, -// "generationtime_ms": 0.2510547637939453, -// "utc_offset_seconds": 0, -// "timezone": "GMT", -// "timezone_abbreviation": "GMT", -// "elevation": 189, -// "current_weather": { -// "temperature": 15.3, -// "windspeed": 25.8, -// "winddirection": 310, -// "weathercode": 63, -// "time": "2022-09-12T01:00" -// } -// } -// ''', -// ); -// when(() => httpClient.get(any())).thenAnswer((_) async => response); -// final actual = await apiClient.getWeather( -// latitude: latitude, -// longitude: longitude, -// ); -// expect( -// actual, -// isA() -// .having((w) => w.temperature, 'temperature', 15.3) -// .having((w) => w.weatherCode, 'weatherCode', 63.0), -// ); -// }); -// }); }); } From 17dd4bacfee98ad775eb4bfe06319c1eb32c8fd6 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Wed, 19 Oct 2022 05:46:15 +0200 Subject: [PATCH 13/18] refactoring and removed unneeded code --- .../lib/src/fpdart/location_failure.dart | 3 ++- .../fpdart/open_meteo_api_client_fpdart.dart | 4 --- .../lib/src/fpdart/weather_failure.dart | 3 ++- .../open_meteo_api/test/location_test.dart | 26 ------------------- example/open_meteo_api/test/weather_test.dart | 19 -------------- 5 files changed, 4 insertions(+), 51 deletions(-) delete mode 100644 example/open_meteo_api/test/location_test.dart delete mode 100644 example/open_meteo_api/test/weather_test.dart diff --git a/example/open_meteo_api/lib/src/fpdart/location_failure.dart b/example/open_meteo_api/lib/src/fpdart/location_failure.dart index 09fe8dab..b428044b 100644 --- a/example/open_meteo_api/lib/src/fpdart/location_failure.dart +++ b/example/open_meteo_api/lib/src/fpdart/location_failure.dart @@ -1,6 +1,7 @@ import 'package:http/http.dart' as http; -abstract class OpenMeteoApiFpdartLocationFailure implements Exception {} +/// Abstract class which represents a failure in the `locationSearch` request. +abstract class OpenMeteoApiFpdartLocationFailure {} /// [OpenMeteoApiFpdartLocationFailure] when **http request** fails class LocationHttpRequestFpdartFailure diff --git a/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart b/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart index b8c9332d..161ae0df 100644 --- a/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart +++ b/example/open_meteo_api/lib/src/fpdart/open_meteo_api_client_fpdart.dart @@ -6,11 +6,7 @@ import 'package:open_meteo_api/open_meteo_api.dart'; import 'package:open_meteo_api/src/fpdart/location_failure.dart'; import 'package:open_meteo_api/src/fpdart/weather_failure.dart'; -/// {@template open_meteo_api_client} -/// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). -/// {@endtemplate} class OpenMeteoApiClientFpdart { - /// {@macro open_meteo_api_client} OpenMeteoApiClientFpdart({http.Client? httpClient}) : _httpClient = httpClient ?? http.Client(); diff --git a/example/open_meteo_api/lib/src/fpdart/weather_failure.dart b/example/open_meteo_api/lib/src/fpdart/weather_failure.dart index 7b984850..a6cbbcd4 100644 --- a/example/open_meteo_api/lib/src/fpdart/weather_failure.dart +++ b/example/open_meteo_api/lib/src/fpdart/weather_failure.dart @@ -1,6 +1,7 @@ import 'package:http/http.dart' as http; -abstract class OpenMeteoApiFpdartWeatherFailure implements Exception {} +/// Abstract class which represents a failure in the `getWeather` request. +abstract class OpenMeteoApiFpdartWeatherFailure {} /// [OpenMeteoApiFpdartWeatherFailure] when **http request** fails class WeatherHttpRequestFpdartFailure diff --git a/example/open_meteo_api/test/location_test.dart b/example/open_meteo_api/test/location_test.dart deleted file mode 100644 index d5c1f35e..00000000 --- a/example/open_meteo_api/test/location_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:open_meteo_api/open_meteo_api.dart'; -import 'package:test/test.dart'; - -void main() { - group('Location', () { - group('fromJson', () { - test('returns correct Location object', () { - expect( - Location.fromJson( - { - 'id': 4887398, - 'name': 'Chicago', - 'latitude': 41.85003, - 'longitude': -87.65005, - }, - ), - isA() - .having((w) => w.id, 'id', 4887398) - .having((w) => w.name, 'name', 'Chicago') - .having((w) => w.latitude, 'latitude', 41.85003) - .having((w) => w.longitude, 'longitude', -87.65005), - ); - }); - }); - }); -} diff --git a/example/open_meteo_api/test/weather_test.dart b/example/open_meteo_api/test/weather_test.dart deleted file mode 100644 index 4edfbbf8..00000000 --- a/example/open_meteo_api/test/weather_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:open_meteo_api/open_meteo_api.dart'; -import 'package:test/test.dart'; - -void main() { - group('Weather', () { - group('fromJson', () { - test('returns correct Weather object', () { - expect( - Weather.fromJson( - {'temperature': 15.3, 'weathercode': 63}, - ), - isA() - .having((w) => w.temperature, 'temperature', 15.3) - .having((w) => w.weatherCode, 'weatherCode', 63), - ); - }); - }); - }); -} From beec642e594e3af415086ddb61ec3a9d131297e0 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Wed, 19 Oct 2022 06:06:57 +0200 Subject: [PATCH 14/18] README for open meteo api example --- example/open_meteo_api/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 example/open_meteo_api/README.md diff --git a/example/open_meteo_api/README.md b/example/open_meteo_api/README.md new file mode 100644 index 00000000..18412255 --- /dev/null +++ b/example/open_meteo_api/README.md @@ -0,0 +1,26 @@ +# Open Meteo API - `fpdart` +This is a re-implementation using `fpdart` and functional programming of the [Open Meteo API](https://github.com/felangel/bloc/tree/master/examples/flutter_weather/packages/open_meteo_api) from the [flutter_weather](https://bloclibrary.dev/#/flutterweathertutorial) app example in the [bloc](https://pub.dev/packages/bloc) package. + +The goal is to show a comparison between usual dart code and functional code written using `fpdart`. + +## Structure +The example is simple but comprehensive. + +The Open Meteo API implementation is only 1 file. The original source is [open_meteo_api_client.dart](./lib/src/open_meteo_api_client.dart) (copy of the [bloc package implementation](https://github.com/felangel/bloc/blob/master/examples/flutter_weather/packages/open_meteo_api/lib/src/open_meteo_api_client.dart)). + +Inside [lib/src/fpdart](./lib/src/fpdart/) you can then find the refactoring using functional programming and `fpdart`: +- [open_meteo_api_client_fpdart.dart](./lib/src/fpdart/open_meteo_api_client_fpdart.dart): implementation of the Open Meteo API with `fpdart` +- [location_failure.dart](./lib/src/fpdart/location_failure.dart): failure classes for the `locationSearch` request +- [weather_failure.dart](./lib/src/fpdart/weather_failure.dart): failure classes for the `getWeather` request + +### Test +Also the [test](./test/) has been rewritten based on the `fpdart` refactoring: +- [open_meteo_api_client_test.dart](./test/open_meteo_api_client_test.dart): Original Open Meteo API test implementation +- [open_meteo_api_client_test_fpdart.dart](./test/open_meteo_api_client_test_fpdart.dart): Testing for the new implementation using `fpdart` and functional programming + +## Types used from `fpdart` +- `TaskEither`: Used instead of `Future` to make async request that may fail +- `Either`: Used to validate the response from the API with either an error or a valid value +- `Option`: Used to get values that may be missing + - `lookup` in a `Map`: getting a value by key in a `Map` may return nothing if the key is not found + - `head` in a `List`: The list may be empty, so getting the first element (called _"head"_) may return nothing \ No newline at end of file From 458f673537b8ac09e011f711c08e061cc45a9dec Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Mon, 24 Oct 2022 06:09:30 +0200 Subject: [PATCH 15/18] updated CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c9334d1..42a1b1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,24 @@ final either = Either.fromNullable(value, (r) => 'none'); final either = Either.fromNullable(value, () => 'none'); ``` +- Added `chainEither` to `TaskEither` +- Added `safeCast` (`Either` and `Option`) +- Added `safeCastStrict` (`Either` and `Option`) +```dart +int intValue = 10; + +/// Unhandled exception: type 'int' is not a subtype of type 'List' in type cast +final waitWhat = intValue as List; +final first = waitWhat.first; + +/// Safe 🎯 +final wellYeah = Either>.safeCast( + intValue, + (dynamic value) => 'Not an List!', +); +final firstEither = wellYeah.map((list) => list.first); +``` +- Added [**Open API Meteo example**](./example/open_meteo_api/) (from imperative to functional programming) - Added article about [Option type and Null Safety in dart](https://www.sandromaglione.com/techblog/option_type_and_null_safety_dart) # v0.3.0 - 11 October 2022 From ebe22ad7d6cee6809d52b779a5f40a479084e291 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Mon, 24 Oct 2022 06:16:16 +0200 Subject: [PATCH 16/18] added examples to README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index f66b9124..1c52a373 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,19 @@ dependencies: ## ✨ Examples +### [Pokeapi](./example/pokeapi_functional/) +Flutter app that lets you search and view your favorite Pokemon: +- API request +- Response validation +- JSON conversion +- State management ([riverpod](https://pub.dev/packages/riverpod)) + +### [Open Meteo API](./example/open_meteo_api/) +Re-implementation using `fpdart` and functional programming of the [Open Meteo API](https://github.com/felangel/bloc/tree/master/examples/flutter_weather/packages/open_meteo_api) from the [flutter_weather](https://bloclibrary.dev/#/flutterweathertutorial) app example in the [bloc](https://pub.dev/packages/bloc) package. + +### [Read/Write local file](./example/read_write_file/) +Example of how to read and write a local file using functional programming. + ### [Option](./lib/src/option.dart) Used when a return value can be missing. > For example, when parsing a `String` to `int`, since not all `String` From 2bcda296da3718d72900881f8ef2643913d579d0 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Tue, 8 Nov 2022 06:16:37 +0100 Subject: [PATCH 17/18] examples of tasks and futures --- example/read_write_file/main.dart | 66 +++++++------ example/src/task/task_and_future.dart | 92 +++++++++++++++++++ .../src/task_option/future_task_option.dart | 30 ++++++ 3 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 example/src/task/task_and_future.dart create mode 100644 example/src/task_option/future_task_option.dart diff --git a/example/read_write_file/main.dart b/example/read_write_file/main.dart index 0397ce90..3f17ec7e 100644 --- a/example/read_write_file/main.dart +++ b/example/read_write_file/main.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:fpdart/fpdart.dart'; /** @@ -35,41 +36,35 @@ void main() async { /// /// Since we are using [TaskEither], until we call the `run` method, /// no actual reading is performed. - final task = readFileAsync('./assets/source_ita.txt').flatMap( - (linesIta) => readFileAsync('./assets/source_eng.txt').map( - (linesEng) => linesIta.zip(linesEng), - ), - ); - - /// Run the reading process - final result = await task.run(); - - /// Check for possible error in the reading process - result.fold( - /// Print error in the reading process + final task = readFileAsync('./assets/source_ita.txt') + .flatMap( + (linesIta) => readFileAsync('./assets/source_eng.txt').map( + (linesEng) => linesIta.zip(linesEng), + ), + ) + .map( + (iterable) => iterable.flatMapWithIndex( + (tuple, index) => searchWords.foldLeftWithIndex>( + [], + (acc, word, wordIndex) => + tuple.second.toLowerCase().split(' ').contains(word) + ? [ + ...acc, + FoundWord( + index, + word, + wordIndex, + tuple.second.replaceAll(word, '<\$>'), + tuple.first, + ), + ] + : acc, + ), + ), + ) + .match( (l) => print(l), - (r) { - /// If no error occurs, search the words inside each sentence - /// and compute list of sentence in which you found a word. - final list = r.flatMapWithIndex((tuple, index) { - return searchWords.foldLeftWithIndex>( - [], - (acc, word, wordIndex) => - tuple.second.toLowerCase().split(' ').contains(word) - ? [ - ...acc, - FoundWord( - index, - word, - wordIndex, - tuple.second.replaceAll(word, '<\$>'), - tuple.first, - ), - ] - : acc, - ); - }); - + (list) { /// Print all the found [FoundWord] list.forEach( (e) => print( @@ -77,6 +72,9 @@ void main() async { ); }, ); + + /// Run the reading process + await task.run(); } /// Read file content in `source` directory using [TaskEither] diff --git a/example/src/task/task_and_future.dart b/example/src/task/task_and_future.dart new file mode 100644 index 00000000..1678ced8 --- /dev/null +++ b/example/src/task/task_and_future.dart @@ -0,0 +1,92 @@ +import 'package:fpdart/fpdart.dart'; + +/// Helper functions ⚙️ (sync) +String addNamePrefix(String name) => "Mr. $name"; +String addEmailPrefix(String email) => "mailto:$email"; +String decodeName(int code) => "$code"; + +/// API functions 🔌 (async) +Future getUsername() => Future.value("Sandro"); +Future getEncodedName() => Future.value(10); + +Future getEmail() => Future.value("@"); + +Future sendInformation(String usernameOrName, String email) => + Future.value(true); + +Future withFuture() async { + late String usernameOrName; + late String email; + + try { + usernameOrName = await getUsername(); + } catch (e) { + try { + usernameOrName = decodeName(await getEncodedName()); + } catch (e) { + throw Exception("Missing both username and name"); + } + } + + try { + email = await getEmail(); + } catch (e) { + throw Exception("Missing email"); + } + + try { + final usernameOrNamePrefix = addNamePrefix(usernameOrName); + final emailPrefix = addEmailPrefix(email); + return await sendInformation(usernameOrNamePrefix, emailPrefix); + } catch (e) { + throw Exception("Error when sending information"); + } +} + +TaskEither withTask() => TaskEither.tryCatch( + getUsername, + (_, __) => "Missing username", + ) + .alt( + () => TaskEither.tryCatch( + getEncodedName, + (_, __) => "Missing name", + ).map( + decodeName, + ), + ) + .map( + addNamePrefix, + ) + .flatMap( + (usernameOrNamePrefix) => TaskEither.tryCatch( + getEmail, + (_, __) => "Missing email", + ) + .map( + addEmailPrefix, + ) + .flatMap( + (emailPrefix) => TaskEither.tryCatch( + () => sendInformation(usernameOrNamePrefix, emailPrefix), + (_, __) => "Error when sending information", + ), + ), + ); + +Task getTask() => Task(() async { + print("I am running [Task]..."); + return 10; + }); + +Future getFuture() async { + print("I am running [Future]..."); + return 10; +} + +void main() { + Task taskInt = getTask(); + Future futureInt = getFuture(); + + // Future taskRun = taskInt.run(); +} diff --git a/example/src/task_option/future_task_option.dart b/example/src/task_option/future_task_option.dart new file mode 100644 index 00000000..830fe73f --- /dev/null +++ b/example/src/task_option/future_task_option.dart @@ -0,0 +1,30 @@ +import 'package:fpdart/fpdart.dart'; + +late Future?> example; + +final taskOp = TaskOption.flatten( + (TaskOption.fromTask( + Task?>( + () => example, + ).map( + (ex) => Option.fromNullable(ex).toTaskOption(), + ), + )), +); + +/// New API `toOption`: from `Map?` to `Option>` +final taskOpNew = TaskOption>( + () async => (await example).toOption(), +); + +/// Using `Option.fromNullable`, the [Future] cannot fail +final taskOpNoFail = TaskOption>( + () async => Option.fromNullable(await example), +); + +/// Using `Option.fromNullable` when the [Future] can fail +final taskOpFail = TaskOption?>.tryCatch( + () => example, +).flatMap>( + (r) => Option.fromNullable(r).toTaskOption(), +); From 44884d77276fd674ee4783c6be84aa47771daa43 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Fri, 16 Dec 2022 05:45:21 +0100 Subject: [PATCH 18/18] release v0.4.0 --- CHANGELOG.md | 12 ++++++++--- README.md | 11 +++++++++- example/json_serializable/pubspec.lock | 2 +- example/open_meteo_api/pubspec.lock | 2 +- example/pokeapi_functional/pubspec.lock | 28 ++++++++++++------------- example/read_write_file/pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a1b1a7..144894fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# v0.4.0 - Soon +# v0.4.0 - 16 December 2022 - Added extension methods to work with nullable types (`T?`) - From `T?` to `fpdart`'s types - `toOption` @@ -48,12 +48,18 @@ final first = waitWhat.first; /// Safe 🎯 final wellYeah = Either>.safeCast( intValue, - (dynamic value) => 'Not an List!', + (dynamic value) => 'Not a List!', ); final firstEither = wellYeah.map((list) => list.first); ``` - Added [**Open API Meteo example**](./example/open_meteo_api/) (from imperative to functional programming) -- Added article about [Option type and Null Safety in dart](https://www.sandromaglione.com/techblog/option_type_and_null_safety_dart) +- Added new articles + - [Option type and Null Safety in dart](https://www.sandromaglione.com/techblog/option_type_and_null_safety_dart) + - [Either - Error Handling in Functional Programming](https://www.sandromaglione.com/techblog/either-error-handling-functional-programming) + - [Future & Task: asynchronous Functional Programming](https://www.sandromaglione.com/techblog/async-requests-future-and-task-dart) + - [Flutter Supabase Functional Programming with fpdart](https://www.sandromaglione.com/techblog/flutter-dart-functional-programming-fpdart-supabase-app) + - [Open Meteo API - Functional programming with fpdart (Part 1)](https://www.sandromaglione.com/techblog/real_example_fpdart_open_meteo_api_part_1) + - [Open Meteo API - Functional programming with fpdart (Part 2)](https://www.sandromaglione.com/techblog/real_example_fpdart_open_meteo_api_part_2) # v0.3.0 - 11 October 2022 - Inverted `onSome` and `onNone` functions parameters in `match` method of `Option` [⚠️ **BREAKING CHANGE**] (*Read more on why* 👉 [#56](https://github.com/SandroMaglione/fpdart/pull/56)) diff --git a/README.md b/README.md index 1c52a373..760a3a89 100644 --- a/README.md +++ b/README.md @@ -74,13 +74,17 @@ Check out also this series of articles about functional programming with `fpdart - [How to use TaskEither in fpdart](https://www.sandromaglione.com/techblog/how-to-use-task-either-fpdart-functional-programming) - [How to map an Either to a Future in fpdart](https://blog.sandromaglione.com/techblog/from-sync-to-async-functional-programming) - [Option type and Null Safety in dart](https://www.sandromaglione.com/techblog/option_type_and_null_safety_dart) +- [Either - Error Handling in Functional Programming](https://www.sandromaglione.com/techblog/either-error-handling-functional-programming) +- [Future & Task: asynchronous Functional Programming](https://www.sandromaglione.com/techblog/async-requests-future-and-task-dart) +- [Flutter Supabase Functional Programming with fpdart](https://www.sandromaglione.com/techblog/flutter-dart-functional-programming-fpdart-supabase-app) + ## 💻 Installation ```yaml # pubspec.yaml dependencies: - fpdart: ^0.3.0 # Check out the latest version + fpdart: ^0.4.0 # Check out the latest version ``` ## ✨ Examples @@ -95,6 +99,10 @@ Flutter app that lets you search and view your favorite Pokemon: ### [Open Meteo API](./example/open_meteo_api/) Re-implementation using `fpdart` and functional programming of the [Open Meteo API](https://github.com/felangel/bloc/tree/master/examples/flutter_weather/packages/open_meteo_api) from the [flutter_weather](https://bloclibrary.dev/#/flutterweathertutorial) app example in the [bloc](https://pub.dev/packages/bloc) package. +A 2 parts series explains step by step the Open Meteo API code: +- [Open Meteo API - Functional programming with fpdart (Part 1)](https://www.sandromaglione.com/techblog/real_example_fpdart_open_meteo_api_part_1) +- [Open Meteo API - Functional programming with fpdart (Part 2)](https://www.sandromaglione.com/techblog/real_example_fpdart_open_meteo_api_part_2) + ### [Read/Write local file](./example/read_write_file/) Example of how to read and write a local file using functional programming. @@ -328,6 +336,7 @@ In general, **any contribution or feedback is welcome** (and encouraged!). ## 📃 Versioning +- **v0.4.0** - 16 December 2022 - **v0.3.0** - 11 October 2022 - **v0.2.0** - 16 July 2022 - **v0.1.0** - 17 June 2022 diff --git a/example/json_serializable/pubspec.lock b/example/json_serializable/pubspec.lock index f07cb66a..fe9c83c0 100644 --- a/example/json_serializable/pubspec.lock +++ b/example/json_serializable/pubspec.lock @@ -175,7 +175,7 @@ packages: path: "../.." relative: true source: path - version: "0.3.0" + version: "0.3.1" frontend_server_client: dependency: transitive description: diff --git a/example/open_meteo_api/pubspec.lock b/example/open_meteo_api/pubspec.lock index fc0af430..51316fbe 100644 --- a/example/open_meteo_api/pubspec.lock +++ b/example/open_meteo_api/pubspec.lock @@ -161,7 +161,7 @@ packages: path: "../.." relative: true source: path - version: "0.3.0" + version: "0.4.0" frontend_server_client: dependency: transitive description: diff --git a/example/pokeapi_functional/pubspec.lock b/example/pokeapi_functional/pubspec.lock index 7392d411..6a2e2699 100644 --- a/example/pokeapi_functional/pubspec.lock +++ b/example/pokeapi_functional/pubspec.lock @@ -28,7 +28,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -98,7 +98,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -126,7 +126,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" file: dependency: transitive description: @@ -213,7 +213,7 @@ packages: path: "../.." relative: true source: path - version: "0.2.1" + version: "0.3.1" freezed: dependency: "direct main" description: @@ -311,21 +311,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -346,7 +346,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" pedantic: dependency: transitive description: @@ -414,7 +414,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -449,21 +449,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: @@ -507,5 +507,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=1.20.0" diff --git a/example/read_write_file/pubspec.lock b/example/read_write_file/pubspec.lock index 0b634edb..4f977987 100644 --- a/example/read_write_file/pubspec.lock +++ b/example/read_write_file/pubspec.lock @@ -7,7 +7,7 @@ packages: path: "../.." relative: true source: path - version: "0.2.1" + version: "0.3.1" lint: dependency: "direct dev" description: @@ -16,4 +16,4 @@ packages: source: hosted version: "1.5.3" sdks: - dart: ">=2.16.0 <3.0.0" + dart: ">=2.17.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 74a0c55d..281c8c86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: fpdart -version: 0.3.0 +version: 0.4.0 homepage: https://www.sandromaglione.com/ repository: https://github.com/SandroMaglione/fpdart description: Functional programming in Dart and Flutter. All the main functional programming types and patterns fully documented, tested, and with examples.