Skip to content

Commit

Permalink
Open meteo example (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
SandroMaglione authored Dec 16, 2022
2 parents 872f1b5 + 44884d7 commit 201b3d0
Show file tree
Hide file tree
Showing 33 changed files with 1,812 additions and 60 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.dart_tool/
.packages

# Generated files
**.g.dart
.idea/
28 changes: 26 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down Expand Up @@ -35,7 +35,31 @@ final either = Either<String, int>.fromNullable(value, (r) => 'none');
final either = Either<String, int>.fromNullable(value, () => 'none');
```
- Added article about [Option type and Null Safety in dart](https://www.sandromaglione.com/techblog/option_type_and_null_safety_dart)
- 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<int>' in type cast
final waitWhat = intValue as List<int>;
final first = waitWhat.first;
/// Safe 🎯
final wellYeah = Either<String, List<int>>.safeCast(
intValue,
(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 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))
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,38 @@ 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
### [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.

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.

### [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`
Expand Down Expand Up @@ -315,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
Expand Down
2 changes: 1 addition & 1 deletion example/json_serializable/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ packages:
path: "../.."
relative: true
source: path
version: "0.3.0"
version: "0.3.1"
frontend_server_client:
dependency: transitive
description:
Expand Down
26 changes: 26 additions & 0 deletions example/open_meteo_api/README.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions example/open_meteo_api/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions example/open_meteo_api/build.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions example/open_meteo_api/lib/open_meteo_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +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';
61 changes: 61 additions & 0 deletions example/open_meteo_api/lib/src/fpdart/location_failure.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:http/http.dart' as http;

/// Abstract class which represents a failure in the `locationSearch` request.
abstract class OpenMeteoApiFpdartLocationFailure {}

/// [OpenMeteoApiFpdartLocationFailure] when **http request** fails
class LocationHttpRequestFpdartFailure
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 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 {
const LocationFormattingFpdartFailure(this.object, this.stackTrace);
final Object object;
final StackTrace stackTrace;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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';

class OpenMeteoApiClientFpdart {
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<OpenMeteoApiFpdartLocationFailure, Location> locationSearch(
String query) =>
TaskEither<OpenMeteoApiFpdartLocationFailure, http.Response>.tryCatch(
() => _httpClient.get(
Uri.https(
_baseUrlGeocoding,
'/v1/search',
{'name': query, 'count': '1'},
),
),
LocationHttpRequestFpdartFailure.new,
)
.chainEither(
(response) =>
_validResponseBody(response, LocationRequestFpdartFailure.new),
)
.chainEither(
(body) => Either.tryCatch(
() => jsonDecode(body),
(_, __) => LocationInvalidJsonDecodeFpdartFailure(body),
),
)
.chainEither(
(json) => Either<OpenMeteoApiFpdartLocationFailure,
Map<dynamic, dynamic>>.safeCast(
json,
LocationInvalidMapFpdartFailure.new,
),
)
.chainEither(
(body) => body
.lookup('results')
.toEither(LocationKeyNotFoundFpdartFailure.new),
)
.chainEither(
(currentWeather) => Either<OpenMeteoApiFpdartLocationFailure,
List<dynamic>>.safeCast(
currentWeather,
LocationInvalidListFpdartFailure.new,
),
)
.chainEither(
(results) =>
results.head.toEither(LocationDataNotFoundFpdartFailure.new),
)
.chainEither(
(weather) => Either.tryCatch(
() => Location.fromJson(weather as Map<String, dynamic>),
LocationFormattingFpdartFailure.new,
),
);

/// Fetches [Weather] for a given [latitude] and [longitude].
TaskEither<OpenMeteoApiFpdartWeatherFailure, Weather> getWeather({
required double latitude,
required double longitude,
}) =>
TaskEither<OpenMeteoApiFpdartWeatherFailure, http.Response>.tryCatch(
() async => _httpClient.get(
Uri.https(
_baseUrlWeather,
'v1/forecast',
{
'latitude': '$latitude',
'longitude': '$longitude',
'current_weather': 'true'
},
),
),
WeatherHttpRequestFpdartFailure.new,
)
.chainEither(
(response) =>
_validResponseBody(response, WeatherRequestFpdartFailure.new),
)
.chainEither(
(body) => Either.safeCastStrict<
OpenMeteoApiFpdartWeatherFailure,
Map<dynamic, dynamic>,
String>(body, WeatherInvalidMapFpdartFailure.new),
)
.chainEither(
(body) => body
.lookup('current_weather')
.toEither(WeatherKeyNotFoundFpdartFailure.new),
)
.chainEither(
(currentWeather) => Either<OpenMeteoApiFpdartWeatherFailure,
List<dynamic>>.safeCast(
currentWeather,
WeatherInvalidListFpdartFailure.new,
),
)
.chainEither(
(results) =>
results.head.toEither(WeatherDataNotFoundFpdartFailure.new),
)
.chainEither(
(weather) => Either.tryCatch(
() => Weather.fromJson(weather as Map<String, dynamic>),
WeatherFormattingFpdartFailure.new,
),
);

/// Verify that the response status code is 200,
/// and extract the response's body.
Either<E, String> _validResponseBody<E>(
http.Response response,
E Function(http.Response) onError,
) =>
Either<E, http.Response>.fromPredicate(
response,
(r) => r.statusCode == 200,
onError,
).map((r) => r.body);
}
Loading

0 comments on commit 201b3d0

Please sign in to comment.