From 0b968615bae84965595529bf9b03884f9b5671cc Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 29 Mar 2024 19:00:15 +0100 Subject: [PATCH] feat: 831 - latest "prices" API changes Deleted files: * `get_prices_results.dart` * `validation_error.dart` * `validation_error.g.dart` * `validation_errors.dart` * `validation_errors.g.dart` New files: * `get_locations_order.dart`: Field for the "order by" clause of "get locations". * `get_locations_parameters.dart`: Parameters for the "get locations" API query. * `get_locations_result.dart`: List of location objects returned by the "get locations" method. * `get_locations_result.g.dart`: generated * `order_by.dart`: "Order by" clause for "get list" methods (e.g. "get prices") Impacted files: * `api_prices_test.dart`: impacted by `MaybeError` refactoring; tests for new methods `getOSMLocation`, `getLocations`, `getPriceProductByCode` * `get_prices_order.dart`: refactored using new class `OrderByField` * `get_prices_parameters.dart`: refactored using new class `OrderBy` * `open_prices_api_client.dart`: refactored using more systematically `MaybeError`; renamed `getPriceProduct` as `getPriceProductById`; added methods `getOSMLocation`, `getLocations`, `getPriceProductByCode` * `openfoodfacts.dart`: added the new files; removed the deleted files --- lib/openfoodfacts.dart | 7 +- lib/src/open_prices_api_client.dart | 154 +++++++++++-- lib/src/prices/get_locations_order.dart | 12 + lib/src/prices/get_locations_parameters.dart | 39 ++++ lib/src/prices/get_locations_result.dart | 33 +++ lib/src/prices/get_locations_result.g.dart | 26 +++ lib/src/prices/get_prices_order.dart | 18 +- lib/src/prices/get_prices_parameters.dart | 7 +- lib/src/prices/get_prices_results.dart | 13 -- lib/src/prices/order_by.dart | 18 ++ lib/src/prices/validation_error.dart | 23 -- lib/src/prices/validation_error.g.dart | 18 -- lib/src/prices/validation_errors.dart | 21 -- lib/src/prices/validation_errors.g.dart | 18 -- test/api_prices_test.dart | 229 ++++++++++++++++--- 15 files changed, 468 insertions(+), 168 deletions(-) create mode 100644 lib/src/prices/get_locations_order.dart create mode 100644 lib/src/prices/get_locations_parameters.dart create mode 100644 lib/src/prices/get_locations_result.dart create mode 100644 lib/src/prices/get_locations_result.g.dart delete mode 100644 lib/src/prices/get_prices_results.dart create mode 100644 lib/src/prices/order_by.dart delete mode 100644 lib/src/prices/validation_error.dart delete mode 100644 lib/src/prices/validation_error.g.dart delete mode 100644 lib/src/prices/validation_errors.dart delete mode 100644 lib/src/prices/validation_errors.g.dart diff --git a/lib/openfoodfacts.dart b/lib/openfoodfacts.dart index 024a0008f2..89aa078813 100644 --- a/lib/openfoodfacts.dart +++ b/lib/openfoodfacts.dart @@ -89,22 +89,23 @@ export 'src/personalized_search/preference_importance.dart'; export 'src/personalized_search/product_preferences_manager.dart'; export 'src/personalized_search/product_preferences_selection.dart'; export 'src/prices/currency.dart'; +export 'src/prices/get_locations_order.dart'; +export 'src/prices/get_locations_parameters.dart'; +export 'src/prices/get_locations_result.dart'; // export 'src/prices/get_parameters_helper.dart'; // uncomment if really needed export 'src/prices/get_prices_order.dart'; export 'src/prices/get_prices_parameters.dart'; export 'src/prices/get_prices_result.dart'; -export 'src/prices/get_prices_results.dart'; export 'src/prices/location.dart'; export 'src/prices/location_osm_type.dart'; export 'src/prices/maybe_error.dart'; +export 'src/prices/order_by.dart'; export 'src/prices/price.dart'; export 'src/prices/price_per.dart'; export 'src/prices/price_product.dart'; export 'src/prices/proof.dart'; export 'src/prices/proof_type.dart'; export 'src/prices/session.dart'; -export 'src/prices/validation_error.dart'; -export 'src/prices/validation_errors.dart'; export 'src/search/autocomplete_search_result.dart'; export 'src/search/autocomplete_single_result.dart'; export 'src/search/fuzziness.dart'; diff --git a/lib/src/open_prices_api_client.dart b/lib/src/open_prices_api_client.dart index c1b00b9f91..1bc05bc701 100644 --- a/lib/src/open_prices_api_client.dart +++ b/lib/src/open_prices_api_client.dart @@ -6,15 +6,16 @@ import 'package:path/path.dart'; import 'prices/maybe_error.dart'; import 'prices/price.dart'; import 'prices/proof.dart'; +import 'prices/get_locations_parameters.dart'; +import 'prices/get_locations_result.dart'; import 'prices/get_parameters_helper.dart'; import 'prices/get_prices_parameters.dart'; import 'prices/get_prices_result.dart'; -import 'prices/get_prices_results.dart'; import 'prices/location.dart'; +import 'prices/location_osm_type.dart'; import 'prices/price_product.dart'; import 'prices/proof_type.dart'; import 'prices/session.dart'; -import 'prices/validation_errors.dart'; import 'utils/http_helper.dart'; import 'utils/open_food_api_configuration.dart'; import 'utils/uri_helper.dart'; @@ -36,7 +37,7 @@ class OpenPricesAPIClient { static String _getHost(final UriProductHelper uriHelper) => uriHelper.getHost(_subdomain); - static Future getPrices( + static Future> getPrices( final GetPricesParameters parameters, { final UriProductHelper uriHelper = uriHelperFoodProd, final String? bearerToken, @@ -51,14 +52,72 @@ class OpenPricesAPIClient { uriHelper: uriHelper, bearerToken: bearerToken, ); - dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); if (response.statusCode == 200) { - return GetPricesResults.result(GetPricesResult.fromJson(decodedResponse)); + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value( + GetPricesResult.fromJson(decodedResponse), + ); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } + + static Future> getOSMLocation({ + required final int locationOSMId, + required final LocationOSMType locationOSMType, + final UriProductHelper uriHelper = uriHelperFoodProd, + }) async { + final Uri uri = uriHelper.getUri( + path: '/api/v1/locations/osm/${locationOSMType.offTag}/$locationOSMId', + forcedHost: _getHost(uriHelper), + ); + final Response response = await HttpHelper().doGetRequest( + uri, + uriHelper: uriHelper, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(Location.fromJson(decodedResponse)); + } catch (e) { + // + } } - return GetPricesResults.error(ValidationErrors.fromJson(decodedResponse)); + return MaybeError.responseError(response); } - static Future getLocation( + static Future> getLocations( + final GetLocationsParameters parameters, { + final UriProductHelper uriHelper = uriHelperFoodProd, + final String? bearerToken, + }) async { + final Uri uri = uriHelper.getUri( + path: '/api/v1/locations', + queryParameters: parameters.getQueryParameters(), + forcedHost: _getHost(uriHelper), + ); + final Response response = await HttpHelper().doGetRequest( + uri, + uriHelper: uriHelper, + bearerToken: bearerToken, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value( + GetLocationsResult.fromJson(decodedResponse), + ); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } + + static Future> getLocation( final int locationId, { final UriProductHelper uriHelper = uriHelperFoodProd, }) async { @@ -70,14 +129,18 @@ class OpenPricesAPIClient { uri, uriHelper: uriHelper, ); - dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); if (response.statusCode == 200) { - return Location.fromJson(decodedResponse); + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(Location.fromJson(decodedResponse)); + } catch (e) { + // + } } - return null; + return MaybeError.responseError(response); } - static Future getPriceProduct( + static Future> getPriceProductById( final int productId, { final UriProductHelper uriHelper = uriHelperFoodProd, }) async { @@ -89,11 +152,42 @@ class OpenPricesAPIClient { uri, uriHelper: uriHelper, ); - dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); if (response.statusCode == 200) { - return PriceProduct.fromJson(decodedResponse); + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value( + PriceProduct.fromJson(decodedResponse), + ); + } catch (e) { + // + } + } + return MaybeError.responseError(response); + } + + static Future> getPriceProductByCode( + final String productCode, { + final UriProductHelper uriHelper = uriHelperFoodProd, + }) async { + final Uri uri = uriHelper.getUri( + path: '/api/v1/products/code/$productCode', + forcedHost: _getHost(uriHelper), + ); + final Response response = await HttpHelper().doGetRequest( + uri, + uriHelper: uriHelper, + ); + if (response.statusCode == 200) { + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value( + PriceProduct.fromJson(decodedResponse), + ); + } catch (e) { + // + } } - return null; + return MaybeError.responseError(response); } /// Returns the status of the server. @@ -147,9 +241,13 @@ class OpenPricesAPIClient { 'password': password, }, ); - dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); if (response.statusCode == 200) { - return MaybeError.value(decodedResponse['access_token']); + try { + final dynamic decodedResponse = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(decodedResponse['access_token']); + } catch (e) { + // + } } return MaybeError.responseError(response); } @@ -168,9 +266,13 @@ class OpenPricesAPIClient { uriHelper: uriHelper, bearerToken: bearerToken, ); - final Map json = HttpHelper().jsonDecodeUtf8(response); if (response.statusCode == 200) { - return MaybeError.value(Session.fromJson(json)); + try { + final Map json = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(Session.fromJson(json)); + } catch (e) { + // + } } return MaybeError.responseError(response); } @@ -247,9 +349,13 @@ class OpenPricesAPIClient { uriHelper: uriHelper, bearerToken: bearerToken, ); - final Map json = HttpHelper().jsonDecodeUtf8(response); if (response.statusCode == 201) { - return MaybeError.value(Price.fromJson(json)); + try { + final Map json = HttpHelper().jsonDecodeUtf8(response); + return MaybeError.value(Price.fromJson(json)); + } catch (e) { + // + } } return MaybeError.responseError(response); } @@ -317,8 +423,12 @@ class OpenPricesAPIClient { response, ); if (response.statusCode == 201) { - final Map json = HttpHelper().jsonDecode(responseBody); - return MaybeError.value(Proof.fromJson(json)); + try { + final Map json = HttpHelper().jsonDecode(responseBody); + return MaybeError.value(Proof.fromJson(json)); + } catch (e) { + // + } } return MaybeError.error( error: responseBody, diff --git a/lib/src/prices/get_locations_order.dart b/lib/src/prices/get_locations_order.dart new file mode 100644 index 0000000000..69886dbb89 --- /dev/null +++ b/lib/src/prices/get_locations_order.dart @@ -0,0 +1,12 @@ +import 'order_by.dart'; + +/// Field for the "order by" clause of "get locations". +enum GetLocationsOrderField implements OrderByField { + created(offTag: 'created'), + updated(offTag: 'updated'); + + const GetLocationsOrderField({required this.offTag}); + + @override + final String offTag; +} diff --git a/lib/src/prices/get_locations_parameters.dart b/lib/src/prices/get_locations_parameters.dart new file mode 100644 index 0000000000..8305439a1a --- /dev/null +++ b/lib/src/prices/get_locations_parameters.dart @@ -0,0 +1,39 @@ +import 'get_locations_order.dart'; +import 'order_by.dart'; +import 'get_parameters_helper.dart'; + +/// Parameters for the "get locations" API query. +/// +/// cf. https://prices.openfoodfacts.org/api/docs +class GetLocationsParameters extends GetParametersHelper { + String? osmNameLike; + String? osmCityLike; + String? osmPostcodeLike; + String? osmCountryLike; + int? priceCount; + int? priceCountGte; + int? priceCountLte; + List>? orderBy; + + @override + Map getQueryParameters() { + super.getQueryParameters(); + addNonNullString(osmNameLike, 'osm_name__like'); + addNonNullString(osmCityLike, 'osm_address_city__like'); + addNonNullString(osmPostcodeLike, 'osm_address_postcode__like'); + addNonNullString(osmCountryLike, 'osm_address_country__like'); + addNonNullInt(priceCount, 'price_count'); + addNonNullInt(priceCountGte, 'price_count__gte'); + addNonNullInt(priceCountLte, 'price_count__lte'); + if (orderBy != null) { + final List orders = []; + for (final OrderBy order in orderBy!) { + orders.add(order.offTag); + } + if (orders.isNotEmpty) { + addNonNullString(orders.join(','), 'order_by'); + } + } + return result; + } +} diff --git a/lib/src/prices/get_locations_result.dart b/lib/src/prices/get_locations_result.dart new file mode 100644 index 0000000000..40f1b3ced6 --- /dev/null +++ b/lib/src/prices/get_locations_result.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; + +import '../interface/json_object.dart'; + +part 'get_locations_result.g.dart'; + +/// List of location objects returned by the "get locations" method. +@JsonSerializable() +class GetLocationsResult extends JsonObject { + @JsonKey() + List? items; + + @JsonKey() + int? total; + + @JsonKey(name: 'page') + int? pageNumber; + + @JsonKey(name: 'size') + int? pageSize; + + @JsonKey(name: 'pages') + int? numberOfPages; + + GetLocationsResult(); + + factory GetLocationsResult.fromJson(Map json) => + _$GetLocationsResultFromJson(json); + + @override + Map toJson() => _$GetLocationsResultToJson(this); +} diff --git a/lib/src/prices/get_locations_result.g.dart b/lib/src/prices/get_locations_result.g.dart new file mode 100644 index 0000000000..78c6078dbe --- /dev/null +++ b/lib/src/prices/get_locations_result.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_locations_result.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GetLocationsResult _$GetLocationsResultFromJson(Map json) => + GetLocationsResult() + ..items = (json['items'] as List?) + ?.map((e) => Location.fromJson(e as Map)) + .toList() + ..total = json['total'] as int? + ..pageNumber = json['page'] as int? + ..pageSize = json['size'] as int? + ..numberOfPages = json['pages'] as int?; + +Map _$GetLocationsResultToJson(GetLocationsResult instance) => + { + 'items': instance.items, + 'total': instance.total, + 'page': instance.pageNumber, + 'size': instance.pageSize, + 'pages': instance.numberOfPages, + }; diff --git a/lib/src/prices/get_prices_order.dart b/lib/src/prices/get_prices_order.dart index 7b0d45ab64..ca06266336 100644 --- a/lib/src/prices/get_prices_order.dart +++ b/lib/src/prices/get_prices_order.dart @@ -1,7 +1,7 @@ -import 'package:openfoodfacts/src/model/off_tagged.dart'; +import 'order_by.dart'; /// Field for the "order by" clause of "get prices". -enum GetPricesOrderField implements OffTagged { +enum GetPricesOrderField implements OrderByField { created(offTag: 'created'), date(offTag: 'date'), price(offTag: 'price'); @@ -11,17 +11,3 @@ enum GetPricesOrderField implements OffTagged { @override final String offTag; } - -/// Order clause for "get prices". -class GetPricesOrder implements OffTagged { - const GetPricesOrder({ - required this.field, - required this.ascending, - }); - - final GetPricesOrderField field; - final bool ascending; - - @override - String get offTag => '${ascending ? '' : '-'}${field.offTag}'; -} diff --git a/lib/src/prices/get_prices_parameters.dart b/lib/src/prices/get_prices_parameters.dart index f4685a24fc..82017212fd 100644 --- a/lib/src/prices/get_prices_parameters.dart +++ b/lib/src/prices/get_prices_parameters.dart @@ -1,6 +1,7 @@ import 'currency.dart'; import 'get_parameters_helper.dart'; import 'get_prices_order.dart'; +import 'order_by.dart'; import 'location_osm_type.dart'; /// Parameters for the "get prices" API query. @@ -24,7 +25,7 @@ class GetPricesParameters extends GetParametersHelper { DateTime? dateLte; String? owner; DateTime? createdGte; - List? getPricesOrder; + List>? orderBy; @override Map getQueryParameters() { @@ -46,9 +47,9 @@ class GetPricesParameters extends GetParametersHelper { addNonNullDate(dateLte, 'date__lte', dayOnly: true); addNonNullString(owner, 'owner'); addNonNullDate(createdGte, 'created__gte', dayOnly: false); - if (getPricesOrder != null) { + if (orderBy != null) { final List orders = []; - for (final GetPricesOrder order in getPricesOrder!) { + for (final OrderBy order in orderBy!) { orders.add(order.offTag); } if (orders.isNotEmpty) { diff --git a/lib/src/prices/get_prices_results.dart b/lib/src/prices/get_prices_results.dart deleted file mode 100644 index 3828ad9bbf..0000000000 --- a/lib/src/prices/get_prices_results.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'get_prices_result.dart'; -import 'validation_errors.dart'; - -/// Either successful "get prices" result, or request validation errors. -class GetPricesResults { - final GetPricesResult? result; - - final ValidationErrors? error; - - GetPricesResults.result(this.result) : error = null; - - GetPricesResults.error(this.error) : result = null; -} diff --git a/lib/src/prices/order_by.dart b/lib/src/prices/order_by.dart new file mode 100644 index 0000000000..017ba09222 --- /dev/null +++ b/lib/src/prices/order_by.dart @@ -0,0 +1,18 @@ +import '../model/off_tagged.dart'; + +/// "Order by" field for "get list" methods (e.g. "get prices") +abstract class OrderByField implements OffTagged {} + +/// "Order by" clause for "get list" methods (e.g. "get prices") +class OrderBy implements OffTagged { + const OrderBy({ + required this.field, + required this.ascending, + }); + + final T field; + final bool ascending; + + @override + String get offTag => '${ascending ? '' : '-'}${field.offTag}'; +} diff --git a/lib/src/prices/validation_error.dart b/lib/src/prices/validation_error.dart deleted file mode 100644 index 71501424ef..0000000000 --- a/lib/src/prices/validation_error.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import '../interface/json_object.dart'; - -part 'validation_error.g.dart'; - -/// Single API request validation error. -@JsonSerializable() -class ValidationError extends JsonObject { - @JsonKey() - String? msg; - - @JsonKey() - String? type; - - ValidationError(); - - factory ValidationError.fromJson(Map json) => - _$ValidationErrorFromJson(json); - - @override - Map toJson() => _$ValidationErrorToJson(this); -} diff --git a/lib/src/prices/validation_error.g.dart b/lib/src/prices/validation_error.g.dart deleted file mode 100644 index a387f4a1d8..0000000000 --- a/lib/src/prices/validation_error.g.dart +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'validation_error.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ValidationError _$ValidationErrorFromJson(Map json) => - ValidationError() - ..msg = json['msg'] as String? - ..type = json['type'] as String?; - -Map _$ValidationErrorToJson(ValidationError instance) => - { - 'msg': instance.msg, - 'type': instance.type, - }; diff --git a/lib/src/prices/validation_errors.dart b/lib/src/prices/validation_errors.dart deleted file mode 100644 index 8596506dc4..0000000000 --- a/lib/src/prices/validation_errors.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:openfoodfacts/src/prices/validation_error.dart'; - -import '../interface/json_object.dart'; - -part 'validation_errors.g.dart'; - -/// List of single API request validation errors. -@JsonSerializable() -class ValidationErrors extends JsonObject { - @JsonKey() - List? detail; - - ValidationErrors(); - - factory ValidationErrors.fromJson(Map json) => - _$ValidationErrorsFromJson(json); - - @override - Map toJson() => _$ValidationErrorsToJson(this); -} diff --git a/lib/src/prices/validation_errors.g.dart b/lib/src/prices/validation_errors.g.dart deleted file mode 100644 index c2468e32e6..0000000000 --- a/lib/src/prices/validation_errors.g.dart +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'validation_errors.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ValidationErrors _$ValidationErrorsFromJson(Map json) => - ValidationErrors() - ..detail = (json['detail'] as List?) - ?.map((e) => ValidationError.fromJson(e as Map)) - .toList(); - -Map _$ValidationErrorsToJson(ValidationErrors instance) => - { - 'detail': instance.detail, - }; diff --git a/test/api_prices_test.dart b/test/api_prices_test.dart index 97aa2c8828..09312b1930 100644 --- a/test/api_prices_test.dart +++ b/test/api_prices_test.dart @@ -159,16 +159,18 @@ void main() { const int pageNumber = 1; const int pageSize = 20; + late GetPricesResult result; + // oldest first GetPricesParameters parameters = GetPricesParameters() - ..getPricesOrder = [ - GetPricesOrder(field: GetPricesOrderField.created, ascending: true), + ..orderBy = >[ + OrderBy(field: GetPricesOrderField.created, ascending: true), ] ..pageSize = pageSize ..pageNumber = pageNumber; - GetPricesResults results; + MaybeError maybeResults; try { - results = await OpenPricesAPIClient.getPrices( + maybeResults = await OpenPricesAPIClient.getPrices( parameters, uriHelper: uriHelper, ); @@ -178,8 +180,8 @@ void main() { } rethrow; } - expect(results.result, isNotNull); - GetPricesResult result = results.result!; + expect(maybeResults.isError, isFalse); + result = maybeResults.value; expect(result.pageSize, pageSize); expect(result.pageNumber, pageNumber); expect(result.total, isNotNull); @@ -201,13 +203,13 @@ void main() { // newest first parameters = GetPricesParameters() - ..getPricesOrder = [ - GetPricesOrder(field: GetPricesOrderField.created, ascending: false), + ..orderBy = >[ + OrderBy(field: GetPricesOrderField.created, ascending: false), ] ..pageSize = pageSize ..pageNumber = pageNumber; try { - results = await OpenPricesAPIClient.getPrices( + maybeResults = await OpenPricesAPIClient.getPrices( parameters, uriHelper: uriHelper, ); @@ -217,8 +219,8 @@ void main() { } rethrow; } - expect(results.result, isNotNull); - result = results.result!; + expect(maybeResults.isError, isFalse); + result = maybeResults.value; expect(result.pageSize, pageSize); expect(result.pageNumber, pageNumber); expect(result.total, isNotNull); @@ -263,15 +265,15 @@ void main() { ..dateLte = price.date ..owner = price.owner ..createdGte = price.created - ..getPricesOrder = null + ..orderBy = null ..pageNumber = pageNumber ..pageSize = pageSize; - results = await OpenPricesAPIClient.getPrices( + maybeResults = await OpenPricesAPIClient.getPrices( parameters, uriHelper: uriHelper, ); - expect(results.result, isNotNull); - result = results.result!; + expect(maybeResults.isError, isFalse); + result = maybeResults.value; expect(result.total, 1); expect(result.items, isNotNull); expect(result.items, hasLength(1)); @@ -281,44 +283,209 @@ void main() { group('$OpenPricesAPIClient Locations', () { test('get existing location', () async { const int locationId = 1; - final Location? location = await OpenPricesAPIClient.getLocation( + final MaybeError maybeLocation = + await OpenPricesAPIClient.getLocation( locationId, uriHelper: uriHelper, ); - expect(location, isNotNull); - expect(location!.locationId, locationId); + expect(maybeLocation.isError, isFalse); + final Location location = maybeLocation.value; + expect(location.locationId, locationId); expect(location.osmId, greaterThan(0)); expect(location.type, isNotNull); + + final MaybeError maybeSameOSMLocation = + await OpenPricesAPIClient.getOSMLocation( + locationOSMType: location.type, + locationOSMId: location.osmId, + uriHelper: uriHelper, + ); + expect(maybeSameOSMLocation.isError, isFalse); + final Location sameOSMLocation = maybeSameOSMLocation.value; + expect(sameOSMLocation.locationId, location.locationId); + expect(sameOSMLocation.osmId, location.osmId); + expect(sameOSMLocation.type, location.type); }); test('get non-existing location', () async { - final Location? location = await OpenPricesAPIClient.getLocation( - -1, + const int locationId = -1; + final MaybeError location = + await OpenPricesAPIClient.getLocation( + locationId, + uriHelper: uriHelper, + ); + expect(location.isError, isTrue); + expect( + location.detailError, + 'Location with id $locationId not found', + ); + }); + + test('get non-existing OSM location', () async { + const int locationOSMId = -1; + const LocationOSMType locationOSMType = LocationOSMType.way; + final MaybeError location = + await OpenPricesAPIClient.getOSMLocation( + locationOSMId: locationOSMId, + locationOSMType: LocationOSMType.way, + uriHelper: uriHelper, + ); + expect(location.isError, isTrue); + expect( + location.detailError, + 'Location with type ${locationOSMType.offTag} & id $locationOSMId not found', + ); + }); + + test('get locations', () async { + const int pageNumber = 1; + const int pageSize = 20; + + late GetLocationsResult result; + + // oldest first + GetLocationsParameters parameters = GetLocationsParameters() + ..orderBy = >[ + OrderBy(field: GetLocationsOrderField.created, ascending: true), + ] + ..pageSize = pageSize + ..pageNumber = pageNumber; + MaybeError maybeResults; + try { + maybeResults = await OpenPricesAPIClient.getLocations( + parameters, + uriHelper: uriHelper, + ); + } catch (e) { + if (e.toString().contains(TestConstants.badGatewayError)) { + return; + } + rethrow; + } + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.pageSize, pageSize); + expect(result.pageNumber, pageNumber); + expect(result.total, isNotNull); + expect(result.numberOfPages, (result.total! / result.pageSize!).ceil()); + expect(result.items, isNotNull); + expect(result.items, hasLength(pageSize)); + final DateTime oldestDate = result.items!.first.created; + + // newest first + parameters = GetLocationsParameters() + ..orderBy = >[ + OrderBy(field: GetLocationsOrderField.created, ascending: false), + ] + ..pageSize = pageSize + ..pageNumber = pageNumber; + try { + maybeResults = await OpenPricesAPIClient.getLocations( + parameters, + uriHelper: uriHelper, + ); + } catch (e) { + if (e.toString().contains(TestConstants.badGatewayError)) { + return; + } + rethrow; + } + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.pageSize, pageSize); + expect(result.pageNumber, pageNumber); + expect(result.total, isNotNull); + expect(result.numberOfPages, (result.total! / result.pageSize!).ceil()); + expect(result.items, isNotNull); + expect(result.items, hasLength(pageSize)); + final DateTime newestDate = result.items!.first.created; + + expect( + newestDate.millisecondsSinceEpoch, + greaterThan(oldestDate.millisecondsSinceEpoch), + ); + + parameters = GetLocationsParameters() + ..osmNameLike = 'Monoprix' + ..osmCityLike = 'Grenoble' + ..osmPostcodeLike = '38000' + ..osmCountryLike = 'France'; + maybeResults = await OpenPricesAPIClient.getLocations( + parameters, + uriHelper: uriHelper, + ); + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.total, greaterThanOrEqualTo(1)); + expect(result.items, isNotNull); + + const String city = 'Grenoble'; + parameters = GetLocationsParameters()..osmCityLike = city; + maybeResults = await OpenPricesAPIClient.getLocations( + parameters, uriHelper: uriHelper, ); - expect(location, isNull); + expect(maybeResults.isError, isFalse); + result = maybeResults.value; + expect(result.total, greaterThanOrEqualTo(1)); + expect(result.items, isNotNull); }); + }); - test('get existing product', () async { + group('$OpenPricesAPIClient Products', () { + test('get existing product by ID', () async { const int productId = 1; - final PriceProduct? priceProduct = - await OpenPricesAPIClient.getPriceProduct( + final MaybeError maybePriceProduct = + await OpenPricesAPIClient.getPriceProductById( productId, uriHelper: uriHelper, ); - expect(priceProduct, isNotNull); - expect(priceProduct!.productId, productId); + expect(maybePriceProduct.isError, isFalse); + final PriceProduct priceProduct = maybePriceProduct.value; + expect(priceProduct.productId, productId); expect(priceProduct.code.length, greaterThanOrEqualTo(1)); expect(priceProduct.created, isNotNull); }); - test('get non-existing product', () async { - final PriceProduct? priceProduct = - await OpenPricesAPIClient.getPriceProduct( - -1, + test('get non-existing product by ID', () async { + const int productId = -1; + final MaybeError maybePriceProduct = + await OpenPricesAPIClient.getPriceProductById( + productId, uriHelper: uriHelper, ); - expect(priceProduct, isNull); + expect(maybePriceProduct.isError, isTrue); + expect( + maybePriceProduct.detailError, + 'Product with id $productId not found', + ); + }); + + test('get existing product by CODE', () async { + const String productCode = '3760121210609'; + final MaybeError maybePriceProduct = + await OpenPricesAPIClient.getPriceProductByCode( + productCode, + uriHelper: uriHelper, + ); + expect(maybePriceProduct.isError, isFalse); + final PriceProduct priceProduct = maybePriceProduct.value; + expect(priceProduct.code, productCode); + expect(priceProduct.created, isNotNull); + }); + + test('get non-existing product by CODE', () async { + const String productCode = 'not a code'; + final MaybeError maybePriceProduct = + await OpenPricesAPIClient.getPriceProductByCode( + productCode, + uriHelper: uriHelper, + ); + expect(maybePriceProduct.isError, isTrue); + expect( + maybePriceProduct.detailError, + 'Product with code $productCode not found', + ); }); });