diff --git a/lib/openfoodfacts.dart b/lib/openfoodfacts.dart index 81434e9334..ddbc5404df 100644 --- a/lib/openfoodfacts.dart +++ b/lib/openfoodfacts.dart @@ -97,9 +97,12 @@ 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_level.dart'; +export 'src/search/fuzziness.dart'; export 'src/search/taxonomy_name.dart'; +export 'src/search/taxonomy_name_autocompleter.dart'; export 'src/utils/abstract_query_configuration.dart'; +export 'src/utils/autocomplete_manager.dart'; +export 'src/utils/autocompleter.dart'; export 'src/utils/barcodes_validator.dart'; export 'src/utils/country_helper.dart'; export 'src/utils/http_helper.dart'; @@ -119,6 +122,7 @@ export 'src/model/robotoff_question_order.dart'; export 'src/utils/server_type.dart'; export 'src/utils/suggestion_manager.dart'; export 'src/utils/tag_type.dart'; +export 'src/utils/tag_type_autocompleter.dart'; export 'src/utils/unit_helper.dart'; export 'src/utils/uri_helper.dart'; export 'src/utils/uri_reader.dart'; diff --git a/lib/src/open_food_search_api_client.dart b/lib/src/open_food_search_api_client.dart index 2e68a10e7a..f298c896c0 100644 --- a/lib/src/open_food_search_api_client.dart +++ b/lib/src/open_food_search_api_client.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart'; @@ -8,7 +7,7 @@ import 'utils/http_helper.dart'; import 'utils/language_helper.dart'; import 'utils/open_food_api_configuration.dart'; import 'search/autocomplete_search_result.dart'; -import 'search/fuzziness_level.dart'; +import 'search/fuzziness.dart'; import 'search/taxonomy_name.dart'; import 'utils/uri_helper.dart'; diff --git a/lib/src/search/fuzziness_level.dart b/lib/src/search/fuzziness.dart similarity index 100% rename from lib/src/search/fuzziness_level.dart rename to lib/src/search/fuzziness.dart diff --git a/lib/src/search/taxonomy_name_autocompleter.dart b/lib/src/search/taxonomy_name_autocompleter.dart new file mode 100644 index 0000000000..9a57e2c9ea --- /dev/null +++ b/lib/src/search/taxonomy_name_autocompleter.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import '../model/user.dart'; +import '../open_food_search_api_client.dart'; +import '../utils/autocompleter.dart'; +import '../utils/language_helper.dart'; +import '../utils/open_food_api_configuration.dart'; +import '../utils/uri_helper.dart'; +import 'autocomplete_search_result.dart'; +import 'autocomplete_single_result.dart'; +import 'fuzziness.dart'; +import 'taxonomy_name.dart'; + +/// Autocomplete suggestions for [TaxonomyName]s. +class TaxonomyNameAutocompleter implements Autocompleter { + const TaxonomyNameAutocompleter({ + required this.taxonomyNames, + required this.language, + this.limit = 25, + this.uriHelper = uriHelperFoodProd, + this.user, + this.fuzziness = Fuzziness.none, + }); + + final List taxonomyNames; + final OpenFoodFactsLanguage language; + final int limit; + final UriProductHelper uriHelper; + final User? user; + final Fuzziness fuzziness; + + @override + Future> getSuggestions( + final String input, + ) async { + final AutocompleteSearchResult results = + await OpenFoodSearchAPIClient.autocomplete( + language: language, + query: input, + taxonomyNames: taxonomyNames, + size: limit, + user: user, + uriHelper: uriHelper, + fuzziness: fuzziness, + ); + final List result = []; + if (results.options == null) { + return result; + } + for (final AutocompleteSingleResult item in results.options!) { + result.add(item.text); + } + return result; + } +} diff --git a/lib/src/utils/autocomplete_manager.dart b/lib/src/utils/autocomplete_manager.dart new file mode 100644 index 0000000000..335db63712 --- /dev/null +++ b/lib/src/utils/autocomplete_manager.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'autocompleter.dart'; + +/// Manager that returns the suggestions for the latest input. +/// +/// Typical use case: the user types one character (which triggers a call to +/// suggestions), then another character (which triggers another call). +/// What if the second call is much faster than the first one, for server or +/// connection reasons? The autocomplete widget will get the second suggestions, +/// then the first suggestions will override them. +/// And the user should get the suggestions that match the latest input. +class AutocompleteManager implements Autocompleter { + AutocompleteManager(this.autocompleter); + + final Autocompleter autocompleter; + + final List _inputs = []; + final Map> _cache = >{}; + + @override + Future> getSuggestions( + final String input, + ) async { + _inputs.add(input); + final List? cached = _cache[input]; + if (cached != null) { + return cached; + } + await waitForTestPurpose(); + _cache[input] = await autocompleter.getSuggestions(input); + // meanwhile there might have been some calls to this method, adding inputs. + for (final String latestInput in _inputs.reversed) { + final List? cached = _cache[latestInput]; + if (cached != null) { + return cached; + } + } + // not supposed to happen, as we should have downloaded for "input". + return []; + } + + @protected + @visibleForTesting + Future waitForTestPurpose() async {} +} diff --git a/lib/src/utils/autocompleter.dart b/lib/src/utils/autocompleter.dart new file mode 100644 index 0000000000..5c7755569e --- /dev/null +++ b/lib/src/utils/autocompleter.dart @@ -0,0 +1,4 @@ +/// Interface that provides autocomplete suggestions. +abstract class Autocompleter { + Future> getSuggestions(final String input); +} diff --git a/lib/src/utils/suggestion_manager.dart b/lib/src/utils/suggestion_manager.dart index bbc20ab46a..a3f667964b 100644 --- a/lib/src/utils/suggestion_manager.dart +++ b/lib/src/utils/suggestion_manager.dart @@ -1,14 +1,14 @@ import 'dart:async'; -import 'package:meta/meta.dart'; - import '../model/user.dart'; -import '../open_food_api_client.dart'; +import 'autocomplete_manager.dart'; +import 'autocompleter.dart'; import 'country_helper.dart'; import 'language_helper.dart'; import 'open_food_api_configuration.dart'; import 'uri_helper.dart'; import 'tag_type.dart'; +import 'tag_type_autocompleter.dart'; /// Manager that returns the suggestions for the latest input. /// @@ -18,63 +18,36 @@ import 'tag_type.dart'; /// connection reasons? The autocomplete widget will get the second suggestions, /// then the first suggestions will override them. /// And the user should get the suggestions that match the latest input. -class SuggestionManager { +// TODO: deprecated from 2023-12-06; remove when old enough +@Deprecated('Use TagTypeAutocompleter and AutocompleteManager instead') +class SuggestionManager implements Autocompleter { SuggestionManager( - this.taxonomyType, { - required this.language, - this.country, - this.categories, - this.shape, - this.limit = 25, - this.uriHelper = uriHelperFoodProd, - this.user, - }); - - final TagType taxonomyType; - final OpenFoodFactsLanguage language; - final OpenFoodFactsCountry? country; - final String? categories; - final String? shape; - final int limit; - final UriProductHelper uriHelper; - final User? user; + final TagType taxonomyType, { + required final OpenFoodFactsLanguage language, + final OpenFoodFactsCountry? country, + final String? categories, + final String? shape, + final int limit = 25, + final UriProductHelper uriHelper = uriHelperFoodProd, + final User? user, + }) : manager = AutocompleteManager( + TagTypeAutocompleter( + tagType: taxonomyType, + language: language, + country: country, + categories: categories, + shape: shape, + limit: limit, + uriHelper: uriHelper, + user: user, + ), + ); - final List _inputs = []; - final Map> _cache = >{}; + final AutocompleteManager manager; - /// Returns suggestions about the latest input. + @override Future> getSuggestions( final String input, - ) async { - _inputs.add(input); - final List? cached = _cache[input]; - if (cached != null) { - return cached; - } - await waitForTestPurpose(); - _cache[input] = await OpenFoodAPIClient.getSuggestions( - taxonomyType, - input: input, - language: language, - country: country, - categories: categories, - shape: shape, - limit: limit, - uriHelper: uriHelper, - user: user, - ); - // meanwhile there might have been some calls to this method, adding inputs. - for (final String latestInput in _inputs.reversed) { - final List? cached = _cache[latestInput]; - if (cached != null) { - return cached; - } - } - // not supposed to happen, as we should have downloaded for "input". - return []; - } - - @protected - @visibleForTesting - Future waitForTestPurpose() async {} + ) async => + manager.getSuggestions(input); } diff --git a/lib/src/utils/tag_type_autocompleter.dart b/lib/src/utils/tag_type_autocompleter.dart new file mode 100644 index 0000000000..f2349a6d1a --- /dev/null +++ b/lib/src/utils/tag_type_autocompleter.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import '../model/user.dart'; +import '../open_food_api_client.dart'; +import 'autocompleter.dart'; +import 'country_helper.dart'; +import 'language_helper.dart'; +import 'open_food_api_configuration.dart'; +import 'uri_helper.dart'; +import 'tag_type.dart'; + +/// Autocomplete suggestions for [TagType]. +class TagTypeAutocompleter implements Autocompleter { + const TagTypeAutocompleter({ + required this.tagType, + required this.language, + this.country, + this.categories, + this.shape, + this.limit = 25, + this.uriHelper = uriHelperFoodProd, + this.user, + }); + + final TagType tagType; + final OpenFoodFactsLanguage language; + final OpenFoodFactsCountry? country; + final String? categories; + final String? shape; + final int limit; + final UriProductHelper uriHelper; + final User? user; + + @override + Future> getSuggestions( + final String input, + ) async => + OpenFoodAPIClient.getSuggestions( + tagType, + input: input, + language: language, + country: country, + categories: categories, + shape: shape, + limit: limit, + uriHelper: uriHelper, + user: user, + ); +} diff --git a/test/api_suggestion_manager_test.dart b/test/api_suggestion_manager_test.dart index b07f131796..0ca2681d84 100644 --- a/test/api_suggestion_manager_test.dart +++ b/test/api_suggestion_manager_test.dart @@ -5,10 +5,9 @@ import 'package:test/test.dart'; import 'test_constants.dart'; -class _SuggestionManagerTest extends SuggestionManager { +class _SuggestionManagerTest extends AutocompleteManager { _SuggestionManagerTest( - super.taxonomyType, { - required super.language, + super.autocompleter, { required this.milliSecondWaits, }); @@ -25,7 +24,7 @@ class _SuggestionManagerTest extends SuggestionManager { void main() { OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; - OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; + OpenFoodAPIConfiguration.globalUser = TestConstants.TEST_USER; const TagType tagType = TagType.COUNTRIES; const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH; @@ -36,17 +35,12 @@ void main() { const String input2 = 'fr'; Future> getSlowSuggestions( - final TagType tagType, { + final Autocompleter autoCompleter, { required final int milliseconds, - required final OpenFoodFactsLanguage language, required final String input, }) async { await Future.delayed(Duration(milliseconds: milliseconds)); - return OpenFoodAPIClient.getSuggestions( - tagType, - language: language, - input: input, - ); + return autoCompleter.getSuggestions(input); } /// Returns the result of the longest future. @@ -70,19 +64,15 @@ void main() { } group('$OpenFoodAPIClient suggestion manager', () { - test('countries', () async { - final SuggestionManager manager = SuggestionManager( - tagType, - language: language, - ); + Future testToto(final Autocompleter autocompleter) async { + final AutocompleteManager manager = AutocompleteManager(autocompleter); final List countries1 = await manager.getSuggestions(input1); final List countries2 = await manager.getSuggestions(input2); expect(countries1, isNot(equals(countries2))); // Here we have the second call that takes longer (at least starts later). - final SuggestionManager fastThenSlowManager = _SuggestionManagerTest( - tagType, - language: language, + final AutocompleteManager fastThenSlowManager = _SuggestionManagerTest( + autocompleter, // the second will start later milliSecondWaits: [0, millisecondsWait], ); @@ -95,9 +85,8 @@ void main() { expect(countriesFastThenSlow, countries2); // Here we have the first call that takes longer (at least starts later). - final SuggestionManager slowThenFastManager = _SuggestionManagerTest( - tagType, - language: language, + final AutocompleteManager slowThenFastManager = _SuggestionManagerTest( + autocompleter, // the first will start later milliSecondWaits: [millisecondsWait, 0], ); @@ -114,19 +103,36 @@ void main() { final List countriesNormal = await last( [ getSlowSuggestions( - tagType, - language: language, + autocompleter, input: input1, milliseconds: millisecondsWait, ), - OpenFoodAPIClient.getSuggestions( - tagType, - language: language, - input: input2, - ), + autocompleter.getSuggestions(input2), ], ); expect(countriesNormal, countries1); - }); + } + + test( + 'countries as TagType', + () async => testToto( + TagTypeAutocompleter( + tagType: tagType, + language: language, + uriHelper: uriHelperFoodTest, + ), + ), + ); + + test( + 'countries as TaxonomyName', + () async => testToto( + TaxonomyNameAutocompleter( + taxonomyNames: [TaxonomyName.country], + language: language, + uriHelper: uriHelperFoodTest, + ), + ), + ); }); }