Skip to content

Commit

Permalink
feat: 617 - new "save packagings V3" feature (#640)
Browse files Browse the repository at this point in the history
New files:
* `api_saveProductV3_test.dart`: Integration tests around the "save packagings V3" feature.
* `ProductPackaging.dart`: Packaging component for a product, e.g. recyclable bottle made of glass.
* `ProductPackaging.g.dart`: generated

Impacted files:
* `AbstractQueryConfiguration.dart`: populated parameter `tags_lc`
* `api_getProduct_test.dart`: removed references to now deprecated field `packaging`; added a test about the new packagings field
* `api_saveProduct_test.dart`: skipped one 504 test; removed references to deprecated field `packaging`
* `HttpHelper.dart`: new method `doPatchRequest`; minor refactoring
* `openfoodfacts.dart`: new method `temporarySaveProductV3`
* `Product.dart`: deprecated field `packaging`; added field `packagings`
* `Product.g.dart`: generated
* `ProductFields.dart`: deprecated `PACKAGING`; added `PACKAGINGS` and `RAW`
* `ProductSearchQueryConfiguration.dart`: added api version parameter
* `UriHelper.dart`: new method `getPatchUri`
  • Loading branch information
monsieurtanuki authored Dec 16, 2022
1 parent 8d54c3e commit 55ffbf2
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 29 deletions.
10 changes: 9 additions & 1 deletion lib/model/Product.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:openfoodfacts/model/Attribute.dart';
import 'package:openfoodfacts/model/AttributeGroup.dart';
import 'package:openfoodfacts/model/KnowledgePanels.dart';
import 'package:openfoodfacts/model/ProductImage.dart';
import 'package:openfoodfacts/model/ProductPackaging.dart';
import 'package:openfoodfacts/utils/JsonHelper.dart';
import 'package:openfoodfacts/utils/LanguageHelper.dart';
import 'package:openfoodfacts/utils/ProductFields.dart';
Expand Down Expand Up @@ -311,8 +312,14 @@ class Product extends JsonObject {
includeIfNull: false)
Map<OpenFoodFactsLanguage, List<String>>? labelsTagsInLanguages;

// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('User packagingS instead')
@JsonKey(name: 'packaging', includeIfNull: false)
String? packaging;

@JsonKey(name: 'packagings', includeIfNull: false)
List<ProductPackaging>? packagings;

@JsonKey(name: 'packaging_tags', includeIfNull: false)
List<String>? packagingTags;
@JsonKey(
Expand Down Expand Up @@ -468,7 +475,8 @@ class Product extends JsonObject {
this.labels,
this.labelsTags,
this.labelsTagsInLanguages,
this.packaging,
// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('Use packagingS field instead') this.packaging,
this.packagingTags,
this.miscTags,
this.statesTags,
Expand Down
4 changes: 4 additions & 0 deletions lib/model/Product.g.dart

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

38 changes: 38 additions & 0 deletions lib/model/ProductPackaging.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:openfoodfacts/model/LocalizedTag.dart';
import '../interface/JsonObject.dart';

part 'ProductPackaging.g.dart';

/// Packaging component for a product, e.g. recyclable bottle made of glass.
@JsonSerializable()
class ProductPackaging extends JsonObject {
@JsonKey(includeIfNull: false)
LocalizedTag? shape;

@JsonKey(includeIfNull: false)
LocalizedTag? material;

@JsonKey(includeIfNull: false)
LocalizedTag? recycling;

@JsonKey(
name: 'number_of_units',
includeIfNull: false,
fromJson: JsonObject.parseInt,
)
int? numberOfUnits;

ProductPackaging();

factory ProductPackaging.fromJson(dynamic json) =>
_$ProductPackagingFromJson(json);

@override
Map<String, dynamic> toJson() => _$ProductPackagingToJson(this);

Map<String, String> toServerData() => JsonObject.toDataStatic(toJson());

@override
String toString() => toServerData().toString();
}
36 changes: 36 additions & 0 deletions lib/model/ProductPackaging.g.dart

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

43 changes: 43 additions & 0 deletions lib/openfoodfacts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:developer';

import 'package:http/http.dart';
import 'package:openfoodfacts/interface/JsonObject.dart';
import 'package:openfoodfacts/model/ProductPackaging.dart';
import 'package:openfoodfacts/model/ProductResultV3.dart';
import 'package:openfoodfacts/model/KnowledgePanels.dart';
import 'package:openfoodfacts/model/LoginStatus.dart';
Expand Down Expand Up @@ -147,6 +148,48 @@ class OpenFoodAPIClient {
return Status.fromApiResponse(response.body);
}

/// Temporary: saves product packagings V3 style.
///
/// For the moment that's the only field supported in WRITE by API V3.
/// Long term target is of course more something like [saveProduct].
static Future<ProductResultV3> temporarySaveProductV3(
final User user,
final String barcode, {
final List<ProductPackaging>? packagings,
final QueryType? queryType,
final OpenFoodFactsCountry? country,
final OpenFoodFactsLanguage? language,
}) async {
final Map<String, dynamic> parameterMap = <String, dynamic>{};
parameterMap.addAll(user.toData());
if (packagings == null) {
// For the moment it's the only purpose of this method: saving packagings.
throw Exception('packagings cannot be null');
}
parameterMap['product'] = {};
parameterMap['product']['packagings'] = packagings;
if (language != null) {
parameterMap['lc'] = language.offTag;
parameterMap['tags_lc'] = language.offTag;
}
if (country != null) {
parameterMap['cc'] = country.offTag;
}

var productUri = UriHelper.getPatchUri(
path: '/api/v3/product/$barcode',
queryType: queryType,
);

final Response response = await HttpHelper().doPatchRequest(
productUri,
parameterMap,
user,
queryType: queryType,
);
return ProductResultV3.fromJson(jsonDecode(response.body));
}

/// Send one image to the server.
/// The image will be added to the product specified in the SendImage
/// Returns a Status object as result.
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/AbstractQueryConfiguration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ abstract class AbstractQueryConfiguration {
if (queryLanguages.isNotEmpty) {
result.putIfAbsent(
'lc', () => queryLanguages.map((e) => e.offTag).join(','));
// the result looks like crap if we put several languages
result.putIfAbsent('tags_lc', () => queryLanguages.first.offTag);
}

final String? countryCode = computeCountryCode();
Expand Down
46 changes: 32 additions & 14 deletions lib/utils/HttpHelper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ class HttpHelper {
static const String FROM = 'anonymous';

/// Adds user agent data to parameters, for statistics purpose
static Map<String, String>? addUserAgentParameters(
Map<String, String>? map,
static Map<String, dynamic>? addUserAgentParameters(
Map<String, dynamic>? map,
) {
map ??= <String, String>{};
map ??= <String, dynamic>{};
if (OpenFoodAPIConfiguration.userAgent?.name != null) {
map['app_name'] = OpenFoodAPIConfiguration.userAgent!.name!;
}
Expand Down Expand Up @@ -70,9 +70,7 @@ class HttpHelper {
headers: _buildHeaders(
user: user,
isTestModeActive:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? false
: true,
OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD,
),
);

Expand Down Expand Up @@ -105,16 +103,37 @@ class HttpHelper {
headers: _buildHeaders(
user: user,
isTestModeActive:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? false
: true,
OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD,
addCredentialsToHeader: addCredentialsToHeader,
),
body: addUserAgentParameters(body),
);
return response;
}

static const String userInfoForTest = 'off:off';

/// Send a http PATCH request to the specified uri.
///
/// The data / body of the request has to be provided as map. (key, value)
/// The result of the request will be returned as string.
Future<http.Response> doPatchRequest(
final Uri uri,
final Map<String, dynamic> body,
final User? user, {
final QueryType? queryType,
}) async =>
http.patch(
uri,
headers: _buildHeaders(
user: user,
isTestModeActive: OpenFoodAPIConfiguration.getQueryType(queryType) !=
QueryType.PROD,
addCredentialsToHeader: false,
),
body: jsonEncode(addUserAgentParameters(body)),
);

/// Send a multipart post request to the specified uri.
/// The data / body of the request has to be provided as map. (key, value)
/// The files to send have to be provided as map containing the source file uri.
Expand All @@ -132,14 +151,13 @@ class HttpHelper {
_buildHeaders(
user: user,
isTestModeActive:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? false
: true,
OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD,
) as Map<String, String>,
);

request.headers.addAll({'Content-Type': 'multipart/form-data'});
request.fields.addAll(addUserAgentParameters(body)!);
addUserAgentParameters(body);
request.fields.addAll(body);
if (user != null) {
request.fields.addAll(user.toData());
}
Expand Down Expand Up @@ -192,7 +210,7 @@ class HttpHelper {
});

if (isTestModeActive && !addCredentialsToHeader) {
var token = 'Basic ${base64Encode(utf8.encode('off:off'))}';
var token = 'Basic ${base64Encode(utf8.encode(userInfoForTest))}';
headers.addAll({'Authorization': token});
}

Expand Down
6 changes: 6 additions & 0 deletions lib/utils/ProductFields.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ enum ProductField implements OffTagged {
LABELS(offTag: 'labels'),
LABELS_TAGS(offTag: 'labels_tags'),
LABELS_TAGS_IN_LANGUAGES(offTag: 'labels_tags_'),
// TODO: deprecated from 2022-12-16; remove when old enough
@Deprecated('Use packagingS field instead')
PACKAGING(offTag: 'packaging'),
PACKAGINGS(offTag: 'packagings'),
PACKAGING_TAGS(offTag: 'packaging_tags'),
PACKAGING_TEXT_IN_LANGUAGES(offTag: 'packaging_text_'),
PACKAGING_TEXT_ALL_LANGUAGES(offTag: 'packaging_text_languages'),
Expand Down Expand Up @@ -85,6 +88,9 @@ enum ProductField implements OffTagged {
ORIGINS(offTag: 'origins'),
NOVA_GROUP(offTag: 'nova_group'),
WEBSITE(offTag: 'link'),

/// All data as RAW from the server. E.g. packagings are only Strings there.
RAW(offTag: 'raw'),
ALL(offTag: '');

const ProductField({
Expand Down
8 changes: 5 additions & 3 deletions lib/utils/ProductSearchQueryConfiguration.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'package:openfoodfacts/interface/Parameter.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/utils/AbstractQueryConfiguration.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:openfoodfacts/utils/LanguageHelper.dart';
import 'package:openfoodfacts/utils/ProductFields.dart';

/// Query Configuration for search parameters
class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
Expand All @@ -14,6 +12,7 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
final OpenFoodFactsCountry? country,
final List<ProductField>? fields,
required List<Parameter> parametersList,
this.version = ProductQueryVersion.v3,
}) : super(
language: language,
languages: languages,
Expand All @@ -22,6 +21,8 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
additionalParameters: parametersList,
);

final ProductQueryVersion version;

List<String> getFieldsKeys() {
List<String> result = [];

Expand All @@ -38,6 +39,7 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration {
result.putIfAbsent('search_terms', () => '');
// explicit json output
result.putIfAbsent('json', () => '1');
result.putIfAbsent('api_version', () => '${version.version}');
return result;
}

Expand Down
16 changes: 16 additions & 0 deletions lib/utils/UriHelper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class UriHelper {
final Map<String, String>? queryParameters,
final QueryType? queryType,
final bool addUserAgentParameters = true,
final String? userInfo,
}) =>
Uri(
scheme: OpenFoodAPIConfiguration.uriScheme,
Expand All @@ -28,6 +29,7 @@ class UriHelper {
queryParameters: addUserAgentParameters
? HttpHelper.addUserAgentParameters(queryParameters)
: queryParameters,
userInfo: userInfo,
);

static Uri getPostUri({
Expand All @@ -36,6 +38,20 @@ class UriHelper {
}) =>
getUri(path: path, queryType: queryType, addUserAgentParameters: false);

static Uri getPatchUri({
required final String path,
final QueryType? queryType,
}) =>
getUri(
path: path,
queryType: queryType,
addUserAgentParameters: false,
userInfo:
OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? null
: HttpHelper.userInfoForTest,
);

///Returns a OFF-Robotoff uri with the in the [OpenFoodAPIConfiguration] specified settings
static Uri getRobotoffUri({
required final String path,
Expand Down
Loading

0 comments on commit 55ffbf2

Please sign in to comment.