From 55f3fa583a535ec842db87067019ed2933f75a78 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 12 Dec 2021 17:42:32 +0100 Subject: [PATCH] feat: #307 - added 21 enum values for advanced search parameters (#313) Impacted files: * `api_searchProduct_test.dart`: additional tests on all `TagFilterType`s; refactoring * `TagFilter.dart`: added more explicit `enum TagFilterType` as filter types --- lib/model/parameter/TagFilter.dart | 75 ++++++++++ test/api_searchProducts_test.dart | 223 +++++++++++++++++++++++------ 2 files changed, 255 insertions(+), 43 deletions(-) diff --git a/lib/model/parameter/TagFilter.dart b/lib/model/parameter/TagFilter.dart index 8887d10576..dd0addd1e1 100644 --- a/lib/model/parameter/TagFilter.dart +++ b/lib/model/parameter/TagFilter.dart @@ -1,5 +1,58 @@ import 'package:openfoodfacts/interface/Parameter.dart'; +/// Filter types for advanced search parameters +enum TagFilterType { + BRANDS, + CATEGORIES, + PACKAGING, + LABELS, + ORIGINS, + MANUFACTURING_PLACES, + EMB_CODES, + PURCHASE_PLACES, + STORES, + COUNTRIES, + ADDITIVES, + ALLERGENS, + TRACES, + NUTRITION_GRADES, + STATES, + INGREDIENTS, + NOVA_GROUPS, + LANGUAGES, + CREATOR, + EDITORS, + LANG, +} + +extension TagFilterTypeExtension on TagFilterType { + static const Map _map = { + TagFilterType.BRANDS: 'brands', + TagFilterType.CATEGORIES: 'categories', + TagFilterType.PACKAGING: 'packaging', + TagFilterType.LABELS: 'labels', + TagFilterType.ORIGINS: 'origins', + TagFilterType.MANUFACTURING_PLACES: 'manufacturing_places', + TagFilterType.EMB_CODES: 'emb_codes', + TagFilterType.PURCHASE_PLACES: 'purchase_places', + TagFilterType.STORES: 'stores', + TagFilterType.COUNTRIES: 'countries', + TagFilterType.ADDITIVES: 'additives', + TagFilterType.ALLERGENS: 'allergens', + TagFilterType.TRACES: 'traces', + TagFilterType.NUTRITION_GRADES: 'nutrition_grades', + TagFilterType.STATES: 'states', + TagFilterType.INGREDIENTS: 'ingredients', + TagFilterType.NOVA_GROUPS: 'nova_groups', + TagFilterType.LANGUAGES: 'languages', + TagFilterType.CREATOR: 'creator', + TagFilterType.EDITORS: 'editors', + TagFilterType.LANG: 'lang', + }; + + String get key => _map[this]!; +} + /// Tag filter ("LIST contains/without ITEM") search API parameter /// /// Eg. 'nutrition_grades' contains 'A' @@ -25,9 +78,31 @@ class TagFilter extends Parameter { final bool contains; final String tagName; +// TODO: deprecated from 2021-12-12 (#307); remove when old enough + @Deprecated('Use TagFilter.fromType instead') const TagFilter({ + required final String tagType, + required final bool contains, + required final String tagName, + }) : this._( + tagType: tagType, + contains: contains, + tagName: tagName, + ); + + const TagFilter._({ required this.tagType, required this.contains, required this.tagName, }); + + TagFilter.fromType({ + required final TagFilterType tagFilterType, + required final String tagName, + final bool contains = true, + }) : this._( + tagType: tagFilterType.key, + contains: contains, + tagName: tagName, + ); } diff --git a/test/api_searchProducts_test.dart b/test/api_searchProducts_test.dart index 82387e475e..f431035667 100644 --- a/test/api_searchProducts_test.dart +++ b/test/api_searchProducts_test.dart @@ -16,81 +16,81 @@ void main() { group('$OpenFoodAPIClient search products', () { test('search favorite products', () async { - var parameters = [ + final parameters = [ const Page(page: 1), const PageSize(size: 10), const SortBy(option: SortOption.POPULARITY) ]; - ProductSearchQueryConfiguration configuration = + final ProductSearchQueryConfiguration configuration = ProductSearchQueryConfiguration( parametersList: parameters, fields: [ProductField.ALL], language: OpenFoodFactsLanguage.GERMAN); - SearchResult result = await OpenFoodAPIClient.searchProducts( + final SearchResult result = await OpenFoodAPIClient.searchProducts( TestConstants.TEST_USER, configuration, ); expect(result.page, 1); expect(result.pageSize, 10); - expect(result.products != null, true); + expect(result.products, isNotNull); expect(result.products!.length, 10); expect(result.products![0].runtimeType, Product); - expect(result.count! > 30000, true); + expect(result.count, greaterThan(30000)); }); test('search favorite products EN', () async { - var parameters = [ + final parameters = [ const Page(page: 14), const PageSize(size: 3), const SortBy(option: SortOption.EDIT) ]; - ProductSearchQueryConfiguration configuration = + final ProductSearchQueryConfiguration configuration = ProductSearchQueryConfiguration( parametersList: parameters, fields: [ProductField.ALL], language: OpenFoodFactsLanguage.ENGLISH); - SearchResult result = await OpenFoodAPIClient.searchProducts( + final SearchResult result = await OpenFoodAPIClient.searchProducts( TestConstants.TEST_USER, configuration, ); expect(result.page, 14); expect(result.pageSize, 3); - expect(result.products != null, true); + expect(result.products, isNotNull); expect(result.products!.length, 3); expect(result.products![0].runtimeType, Product); - expect(result.count! > 30000, true); + expect(result.count, greaterThan(30000)); }); test('type bug : ingredient percent int vs String ', () async { - var parameters = [ + final parameters = [ const Page(page: 16), const PageSize(size: 5), const SortBy(option: SortOption.POPULARITY) ]; - ProductSearchQueryConfiguration configuration = + final ProductSearchQueryConfiguration configuration = ProductSearchQueryConfiguration( parametersList: parameters, fields: [ProductField.ALL], language: OpenFoodFactsLanguage.GERMAN); - SearchResult result = await OpenFoodAPIClient.searchProducts( + final SearchResult result = await OpenFoodAPIClient.searchProducts( TestConstants.TEST_USER, configuration, ); expect(result.page, 16); expect(result.pageSize, 5); - expect(result.products != null, true); + expect(result.products, isNotNull); expect(result.products!.length, 5); expect(result.products![0].runtimeType, Product); - expect(result.count! > 30000, true); + expect(result.count, greaterThan(30000)); }); test('search products by keywords', () async { @@ -113,10 +113,10 @@ void main() { expect(result.page, 2); expect(result.pageSize, 10); - expect(result.products != null, true); + expect(result.products, isNotNull); expect(result.products!.length, 10); expect(result.products![0].runtimeType, Product); - expect(result.count! > 900, true); + expect(result.count, greaterThan(900)); }); test('search products filter additives', () async { @@ -148,47 +148,184 @@ void main() { counts[withoutAdditives] = result.count!; } - expect(counts[WITHOUT_ADDITIVES]! < counts[ADDITIVE_AGNOSTIC]!, true); + expect(counts[WITHOUT_ADDITIVES], lessThan(counts[ADDITIVE_AGNOSTIC]!)); }); test('search products with filter on tags', () async { - var parameters = [ + final parameters = [ const Page(page: 5), const PageSize(size: 10), const SortBy(option: SortOption.PRODUCT_NAME), - const TagFilter( - tagType: 'categories', - contains: true, - tagName: 'breakfast_cereals'), - const TagFilter( - tagType: 'nutrition_grades', contains: true, tagName: 'A') + TagFilter.fromType( + tagFilterType: TagFilterType.CATEGORIES, + contains: true, + tagName: 'breakfast_cereals', + ), + TagFilter.fromType( + tagFilterType: TagFilterType.NUTRITION_GRADES, + contains: true, + tagName: 'A', + ), ]; - ProductSearchQueryConfiguration configuration = + final ProductSearchQueryConfiguration configuration = ProductSearchQueryConfiguration( parametersList: parameters, fields: [ProductField.ALL], language: OpenFoodFactsLanguage.FRENCH); - SearchResult result = await OpenFoodAPIClient.searchProducts( + final SearchResult result = await OpenFoodAPIClient.searchProducts( TestConstants.TEST_USER, configuration, ); expect(result.page, 5); expect(result.pageSize, 10); - expect(result.products != null, true); + expect(result.products, isNotNull); expect(result.products!.length, 10); expect(result.products![0].runtimeType, Product); expect( - result.products![0].categoriesTags!.contains('en:breakfast-cereals'), - true); - expect(result.products![0].nutriscore!.toUpperCase() == 'A', true); + result.products![0].categoriesTags, contains('en:breakfast-cereals')); + expect(result.products![0].nutriscore!.toUpperCase(), 'A'); + }); + + test('search products with filter on all tags (part 1)', () async { + const String brands = 'Bjorg'; + const String categories = 'en:breakfast-cereals'; + const String packaging = 'fr:Sachet'; + const String labels = 'en:organic'; + const String origins = 'en:european-union-and-non-european-union'; + const String manufacturingPlaces = 'Allemagne'; + const String purchasePlaces = 'france'; + const String stores = 'franprix'; + const String countries = 'en:france'; + const String allergens = 'en:gluten'; + const String traces = 'en:nuts'; + const String nutritionGrades = 'A'; + const String states = 'en:nutrition-facts-completed'; + const String ingredients = 'en:cereal'; + const int novaGroup = 1; + const String languages = 'ar'; + const String creator = 'sebleouf'; + const String editors = 'foodrepo'; + const String lang = 'fr'; + + final parameters = [ + TagFilter.fromType( + tagFilterType: TagFilterType.BRANDS, tagName: brands), + TagFilter.fromType( + tagFilterType: TagFilterType.CATEGORIES, tagName: categories), + TagFilter.fromType( + tagFilterType: TagFilterType.PACKAGING, tagName: packaging), + TagFilter.fromType( + tagFilterType: TagFilterType.LABELS, tagName: labels), + TagFilter.fromType( + tagFilterType: TagFilterType.ORIGINS, tagName: origins), + TagFilter.fromType( + tagFilterType: TagFilterType.MANUFACTURING_PLACES, + tagName: manufacturingPlaces), + TagFilter.fromType( + tagFilterType: TagFilterType.PURCHASE_PLACES, + tagName: purchasePlaces), + TagFilter.fromType( + tagFilterType: TagFilterType.STORES, tagName: stores), + TagFilter.fromType( + tagFilterType: TagFilterType.COUNTRIES, tagName: countries), + TagFilter.fromType( + tagFilterType: TagFilterType.ALLERGENS, tagName: allergens), + TagFilter.fromType( + tagFilterType: TagFilterType.TRACES, tagName: traces), + TagFilter.fromType( + tagFilterType: TagFilterType.NUTRITION_GRADES, + tagName: nutritionGrades), + TagFilter.fromType( + tagFilterType: TagFilterType.STATES, tagName: states), + TagFilter.fromType( + tagFilterType: TagFilterType.INGREDIENTS, tagName: ingredients), + TagFilter.fromType( + tagFilterType: TagFilterType.NOVA_GROUPS, tagName: '$novaGroup'), + TagFilter.fromType( + tagFilterType: TagFilterType.LANGUAGES, tagName: languages), + TagFilter.fromType( + tagFilterType: TagFilterType.CREATOR, tagName: creator), + TagFilter.fromType( + tagFilterType: TagFilterType.EDITORS, tagName: editors), + TagFilter.fromType(tagFilterType: TagFilterType.LANG, tagName: lang), + ]; + + final ProductSearchQueryConfiguration configuration = + ProductSearchQueryConfiguration( + parametersList: parameters, + fields: [ProductField.ALL], + language: OpenFoodFactsLanguage.FRENCH, + ); + + final SearchResult result = await OpenFoodAPIClient.searchProducts( + TestConstants.TEST_USER, + configuration, + ); + + expect(result.products, isNotNull); + expect(result.products!.length, greaterThan(0)); + for (final Product product in result.products!) { + expect(product.brands!, brands); + expect(product.categoriesTags, contains(categories)); + expect(product.packaging, contains(packaging)); + expect(product.labelsTags, contains(labels)); + expect(product.storesTags, contains(stores)); + expect(product.countriesTags, contains(countries)); + expect(product.allergens!.ids, contains(allergens)); + expect(product.tracesTags, contains(traces)); + expect(product.nutriscore!.toUpperCase(), nutritionGrades); + expect(product.statesTags, contains(states)); + expect(product.ingredientsTags, contains(ingredients)); + expect(product.nutriments!.novaGroup, novaGroup); + expect(product.lang.code, lang); + // TODO(monsieurtanuki): extract the origins, manufactoringPlaces, purchasePlaces, languages, creator and editors from the product, and compare them to expected values + } + }); + + test('search products with filter on all tags (part 2)', () async { + const String embCodes = 'emb-01451a'; + const String additives = 'en:e415'; + + final parameters = [ + TagFilter.fromType( + tagFilterType: TagFilterType.EMB_CODES, + contains: true, + tagName: embCodes, + ), + TagFilter.fromType( + tagFilterType: TagFilterType.ADDITIVES, + contains: true, + tagName: additives, + ), + ]; + + final ProductSearchQueryConfiguration configuration = + ProductSearchQueryConfiguration( + parametersList: parameters, + fields: [ProductField.ALL], + language: OpenFoodFactsLanguage.FRENCH, + ); + + final SearchResult result = await OpenFoodAPIClient.searchProducts( + TestConstants.TEST_USER, + configuration, + ); + + expect(result.products!.length, greaterThan(0)); + + expect(result.products, isNotNull); + for (final Product product in result.products!) { + expect(product.additives!.ids, contains(additives)); + // TODO(monsieurtanuki): extract the emb_codes from the product, and compare it to the expected value + } }); test('search products with quotes', () async { - String barcode = '2222222222223'; - Product product = Product( + const String barcode = '2222222222223'; + final Product product = Product( barcode: barcode, productName: 'Quoted Coca "cola"', lang: OpenFoodFactsLanguage.GERMAN, @@ -199,25 +336,25 @@ void main() { product, ); - var parameters = [ + final parameters = [ const Page(page: 1), const SearchTerms(terms: ['Quoted Coca "Cola"']), ]; - ProductSearchQueryConfiguration configuration = + final ProductSearchQueryConfiguration configuration = ProductSearchQueryConfiguration( parametersList: parameters, fields: [ProductField.ALL], language: OpenFoodFactsLanguage.GERMAN); - SearchResult result = await OpenFoodAPIClient.searchProducts( + final SearchResult result = await OpenFoodAPIClient.searchProducts( TestConstants.TEST_USER, configuration, ); expect(result.products!.length, 1); - expect(result.products![0].productName, equals('Quoted Coca "cola"')); - expect(result.products![0].brands, equals('Quoted Coca "Cola"')); + expect(result.products![0].productName, 'Quoted Coca "cola"'); + expect(result.products![0].brands, 'Quoted Coca "Cola"'); }); test('multiple products', () async { @@ -250,7 +387,7 @@ void main() { language: OpenFoodFactsLanguage.FRENCH, ); - SearchResult result = await OpenFoodAPIClient.getProductList( + final SearchResult result = await OpenFoodAPIClient.getProductList( TestConstants.PROD_USER, configuration, queryType: QueryType.PROD, @@ -259,7 +396,7 @@ void main() { expect(result.page, 1); expect(result.pageSize, 24); expect(result.count, BARCODES.length - 1); - expect(result.products != null, true); + expect(result.products, isNotNull); expect(result.products!.length, BARCODES.length - 1); for (final Product product in result.products!) { final String barcode = product.barcode!; @@ -312,7 +449,7 @@ void main() { } // We want to test pagination mechanism so we expect >1 pages expect(page, greaterThan(1)); - expect(obtainedBarcodes.toSet(), equals(BARCODES.toSet())); + expect(obtainedBarcodes.toSet(), BARCODES.toSet()); }); test('query potatoes products', () async { @@ -333,10 +470,10 @@ void main() { expect(result.page, 3); expect(result.pageSize, 24); - expect(result.products != null, true); + expect(result.products, isNotNull); expect(result.products!.length, 24); expect(result.products![0].runtimeType, Product); - expect(result.count! > 1500, true); + expect(result.count, greaterThan(1500)); }); }); }