Skip to content

Commit

Permalink
feat: 1011 - new "getPriceProducts" method (openfoodfacts#1012)
Browse files Browse the repository at this point in the history
New files:
* `get_price_products_order.dart`: Field for the "order by" clause of "get price products".
* `get_price_products_parameters.dart`: Parameters for the "get price products" API query.
* `get_price_products_result.dart`: List of price product objects returned by the "get price products" method.
* `get_price_products_result.g.dart`: generated

Impacted files:
* `api_prices_test.dart`: new tests around new `getPriceProducts` method; minor refactoring
* `get_parameters_helper.dart`: minor refactoring
* `open_prices_api_client.dart`: new `getPriceProducts` method
* `openfoodfacts.dart`: exported the new 3 files
  • Loading branch information
monsieurtanuki authored Jan 3, 2025
1 parent 8dc052d commit c583648
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 14 deletions.
3 changes: 3 additions & 0 deletions lib/openfoodfacts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ 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_price_products_order.dart';
export 'src/prices/get_price_products_parameters.dart';
export 'src/prices/get_price_products_result.dart';
export 'src/prices/get_prices_result.dart';
export 'src/prices/get_price_count_parameters_helper.dart';
export 'src/prices/get_proofs_order.dart';
Expand Down
30 changes: 30 additions & 0 deletions lib/src/open_prices_api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ 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_price_products_parameters.dart';
import 'prices/get_price_products_result.dart';
import 'prices/get_prices_parameters.dart';
import 'prices/get_prices_result.dart';
import 'prices/get_proofs_parameters.dart';
Expand Down Expand Up @@ -163,6 +165,34 @@ class OpenPricesAPIClient {
return MaybeError<Location>.responseError(response);
}

static Future<MaybeError<GetPriceProductsResult>> getPriceProducts(
final GetPriceProductsParameters parameters, {
final UriProductHelper uriHelper = uriHelperFoodProd,
final String? bearerToken,
}) async {
final Uri uri = getUri(
path: '/api/v1/products',
queryParameters: parameters.getQueryParameters(),
uriHelper: 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<GetPriceProductsResult>.value(
GetPriceProductsResult.fromJson(decodedResponse),
);
} catch (e) {
//
}
}
return MaybeError<GetPriceProductsResult>.responseError(response);
}

static Future<MaybeError<PriceProduct>> getPriceProductById(
final int productId, {
final UriProductHelper uriHelper = uriHelperFoodProd,
Expand Down
8 changes: 4 additions & 4 deletions lib/src/prices/get_parameters_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ abstract class GetParametersHelper<T extends OrderByField> {

/// Returns the parameters as a query parameter map.
Map<String, String> getQueryParameters() {
_checkIntValue('page_number', pageSize, min: 1);
_checkIntValue('page_number', pageNumber, min: 1);
_checkIntValue('page_size', pageSize, min: 1, max: 100);
_result.clear();
addNonNullInt(pageNumber, 'page');
Expand All @@ -32,7 +32,7 @@ abstract class GetParametersHelper<T extends OrderByField> {
}

void _checkIntValue(
final String field,
final String fieldDescription,
final int? value, {
final int? min,
final int? max,
Expand All @@ -43,14 +43,14 @@ abstract class GetParametersHelper<T extends OrderByField> {
if (min != null) {
if (value < min) {
throw Exception(
'$field minimum value is $min (actual value is $value)',
'$fieldDescription minimum value is $min (actual value is $value)',
);
}
}
if (max != null) {
if (value > max) {
throw Exception(
'$field maximum value is $max (actual value is $value)',
'$fieldDescription maximum value is $max (actual value is $value)',
);
}
}
Expand Down
13 changes: 13 additions & 0 deletions lib/src/prices/get_price_products_order.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'order_by.dart';

/// Field for the "order by" clause of "get price products".
enum GetPriceProductsOrderField implements OrderByField {
priceCount(offTag: 'price_count'),
created(offTag: 'created'),
updated(offTag: 'updated');

const GetPriceProductsOrderField({required this.offTag});

@override
final String offTag;
}
37 changes: 37 additions & 0 deletions lib/src/prices/get_price_products_parameters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:openfoodfacts/src/prices/flavor.dart';

import 'get_price_count_parameters_helper.dart';
import 'get_price_products_order.dart';

/// Parameters for the "get price products" API query.
class GetPriceProductsParameters
extends GetPriceCountParametersHelper<GetPriceProductsOrderField> {
String? brandsLike;
String? brandsTagsContains;
String? categoriesTagsContains;
String? code;
String? ecoscoreGrade;
String? labelsTagsContains;
String? novaGroup;
String? nutriscoreGrade;
String? productNameLike;
int? uniqueScansNGte;
Flavor? source;

@override
Map<String, String> getQueryParameters() {
super.getQueryParameters();
addNonNullString(brandsLike, 'brands__like');
addNonNullString(brandsTagsContains, 'brands_tags__contains');
addNonNullString(categoriesTagsContains, 'categories_tags__contains');
addNonNullString(code, 'code');
addNonNullString(ecoscoreGrade, 'ecoscore_grade');
addNonNullString(labelsTagsContains, 'labels_tags__contains');
addNonNullString(novaGroup, 'nova_group');
addNonNullString(nutriscoreGrade, 'nutriscore_grade');
addNonNullString(productNameLike, 'product_name__like');
addNonNullInt(uniqueScansNGte, 'unique_scans_n__gte');
addNonNullString(source?.offTag, 'source');
return result;
}
}
33 changes: 33 additions & 0 deletions lib/src/prices/get_price_products_result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:openfoodfacts/openfoodfacts.dart';

import '../interface/json_object.dart';

part 'get_price_products_result.g.dart';

/// List of price product objects returned by the "get price products" method.
@JsonSerializable()
class GetPriceProductsResult extends JsonObject {
@JsonKey()
List<PriceProduct>? items;

@JsonKey()
int? total;

@JsonKey(name: 'page')
int? pageNumber;

@JsonKey(name: 'size')
int? pageSize;

@JsonKey(name: 'pages')
int? numberOfPages;

GetPriceProductsResult();

factory GetPriceProductsResult.fromJson(Map<String, dynamic> json) =>
_$GetPriceProductsResultFromJson(json);

@override
Map<String, dynamic> toJson() => _$GetPriceProductsResultToJson(this);
}
28 changes: 28 additions & 0 deletions lib/src/prices/get_price_products_result.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

140 changes: 130 additions & 10 deletions test/api_prices_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import 'test_constants.dart';

void main() {
OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT;
const String invalidBearerToken = 'invalid bearer token';
const int HTTP_OK = 200;
const int pageNumber = 1;
const int pageSize = 20;

group('$OpenPricesAPIClient default', () {
const UriProductHelper uriHelper = uriHelperFoodProd;
Expand Down Expand Up @@ -173,6 +174,7 @@ void main() {
..priceWithoutDiscount = 13
..priceIsDiscounted = true;

const String invalidBearerToken = 'invalid bearer token';
String bearerToken = invalidBearerToken;

// failing price creation with invalid token
Expand Down Expand Up @@ -372,8 +374,6 @@ void main() {

test('get prices', () async {
const UriProductHelper uriHelper = uriHelperFoodProd;
const int pageNumber = 1;
const int pageSize = 20;

late GetPricesResult result;

Expand Down Expand Up @@ -556,9 +556,6 @@ void main() {
});

test('get locations', () async {
const int pageNumber = 1;
const int pageSize = 20;

late GetLocationsResult result;

// oldest first
Expand Down Expand Up @@ -707,6 +704,133 @@ void main() {
'No Product matches the given query.',
);
});

test('get products', () async {
late GetPriceProductsResult result;

// oldest first
GetPriceProductsParameters parameters = GetPriceProductsParameters()
..orderBy = <OrderBy<GetPriceProductsOrderField>>[
OrderBy(field: GetPriceProductsOrderField.created, ascending: true),
]
..pageSize = pageSize
..pageNumber = pageNumber;
MaybeError<GetPriceProductsResult> maybeResults;
try {
maybeResults = await OpenPricesAPIClient.getPriceProducts(
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 = GetPriceProductsParameters()
..orderBy = <OrderBy<GetPriceProductsOrderField>>[
OrderBy(field: GetPriceProductsOrderField.created, ascending: false),
]
..pageSize = pageSize
..pageNumber = pageNumber;
try {
maybeResults = await OpenPricesAPIClient.getPriceProducts(
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),
);

// most prices first
parameters = GetPriceProductsParameters()
..orderBy = <OrderBy<GetPriceProductsOrderField>>[
OrderBy(
field: GetPriceProductsOrderField.priceCount,
ascending: false,
),
]
..pageSize = pageSize
..pageNumber = pageNumber;
try {
maybeResults = await OpenPricesAPIClient.getPriceProducts(
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));
// value as of 2024-12-05
expect(result.items!.first.priceCount, greaterThanOrEqualTo(107));

parameters = GetPriceProductsParameters()..brandsLike = 'ferrero';
maybeResults = await OpenPricesAPIClient.getPriceProducts(
parameters,
uriHelper: uriHelper,
);
expect(maybeResults.isError, isFalse);
result = maybeResults.value;
// value as of 2024-12-05
expect(result.total, greaterThanOrEqualTo(2040));

// values as of 2024-12-05
const Map<Flavor?, int> expectedMinCounts = <Flavor?, int>{
Flavor.openFoodFacts: 3625952,
Flavor.openBeautyFacts: 31463,
Flavor.openPetFoodFacts: 9955,
Flavor.openProductFacts: 15741,
null: 3688608,
};
for (final Flavor? flavor in expectedMinCounts.keys) {
parameters = GetPriceProductsParameters()..source = flavor;
maybeResults = await OpenPricesAPIClient.getPriceProducts(
parameters,
uriHelper: uriHelper,
);
expect(maybeResults.isError, isFalse);
result = maybeResults.value;
expect(result.total, greaterThanOrEqualTo(expectedMinCounts[flavor]!));
}
});
});

group('$OpenPricesAPIClient Proofs', () {
Expand All @@ -729,8 +853,6 @@ void main() {
});

test('get proofs', () async {
const int pageNumber = 1;
const int pageSize = 20;
const GetProofsOrderField orderField = GetProofsOrderField.created;
const ProofType proofType = ProofType.receipt;

Expand Down Expand Up @@ -857,8 +979,6 @@ void main() {
const UriProductHelper uriHelper = uriHelperFoodProd;

test('get users', () async {
const int pageNumber = 1;
const int pageSize = 20;
const GetUsersOrderField orderField = GetUsersOrderField.priceCount;

late GetUsersResult result;
Expand Down

0 comments on commit c583648

Please sign in to comment.