Skip to content

Commit

Permalink
feat: autocomplete manager for both TagTypes and TaxonomyNames (#851)
Browse files Browse the repository at this point in the history
* feat: autocomplete manager for both TagTypes and TaxonomyNames

New files:
* `autocomplete_manager.dart`: Manager that returns the suggestions for the latest input. Code was largely moved from `suggestion_manager.dart` as refactoring.
Manager that returns the suggestions for the latest input.
* `autocompleter.dart`: Interface that provides autocomplete suggestions.
* `tag_type_autocompleter.dart`: Autocomplete suggestions for TagType.
* `taxonomy_name_autocompleter.dart`: Autocomplete suggestions for TaxonomyNames.

Impacted files:
* `api_suggestion_manager_test.dart`: added test for elastic search
* `fuzziness.dart`: renamed from `fuzziness_level.dart`
* `open_food_search_api_client.dart`: minor refactoring
* `openfoodfacts.dart`: added the new 4 files and impacted the file renaming
* `suggestion_manager.dart`: deprecated it in favor of new classes `TagTypeAutocompleter` and `AutocompleteManager`

* fix after self-review
  • Loading branch information
monsieurtanuki authored Feb 4, 2024
1 parent 7a1f4e6 commit 6bb8deb
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 90 deletions.
6 changes: 5 additions & 1 deletion lib/openfoodfacts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
3 changes: 1 addition & 2 deletions lib/src/open_food_search_api_client.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart';
Expand All @@ -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';

Expand Down
File renamed without changes.
55 changes: 55 additions & 0 deletions lib/src/search/taxonomy_name_autocompleter.dart
Original file line number Diff line number Diff line change
@@ -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<TaxonomyName> taxonomyNames;
final OpenFoodFactsLanguage language;
final int limit;
final UriProductHelper uriHelper;
final User? user;
final Fuzziness fuzziness;

@override
Future<List<String>> 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<String> result = <String>[];
if (results.options == null) {
return result;
}
for (final AutocompleteSingleResult item in results.options!) {
result.add(item.text);
}
return result;
}
}
47 changes: 47 additions & 0 deletions lib/src/utils/autocomplete_manager.dart
Original file line number Diff line number Diff line change
@@ -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<String> _inputs = <String>[];
final Map<String, List<String>> _cache = <String, List<String>>{};

@override
Future<List<String>> getSuggestions(
final String input,
) async {
_inputs.add(input);
final List<String>? 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<String>? cached = _cache[latestInput];
if (cached != null) {
return cached;
}
}
// not supposed to happen, as we should have downloaded for "input".
return <String>[];
}

@protected
@visibleForTesting
Future<void> waitForTestPurpose() async {}
}
4 changes: 4 additions & 0 deletions lib/src/utils/autocompleter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Interface that provides autocomplete suggestions.
abstract class Autocompleter {
Future<List<String>> getSuggestions(final String input);
}
87 changes: 30 additions & 57 deletions lib/src/utils/suggestion_manager.dart
Original file line number Diff line number Diff line change
@@ -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.
///
Expand All @@ -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<String> _inputs = <String>[];
final Map<String, List<String>> _cache = <String, List<String>>{};
final AutocompleteManager manager;

/// Returns suggestions about the latest input.
@override
Future<List<String>> getSuggestions(
final String input,
) async {
_inputs.add(input);
final List<String>? 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<String>? cached = _cache[latestInput];
if (cached != null) {
return cached;
}
}
// not supposed to happen, as we should have downloaded for "input".
return <String>[];
}

@protected
@visibleForTesting
Future<void> waitForTestPurpose() async {}
) async =>
manager.getSuggestions(input);
}
49 changes: 49 additions & 0 deletions lib/src/utils/tag_type_autocompleter.dart
Original file line number Diff line number Diff line change
@@ -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<List<String>> getSuggestions(
final String input,
) async =>
OpenFoodAPIClient.getSuggestions(
tagType,
input: input,
language: language,
country: country,
categories: categories,
shape: shape,
limit: limit,
uriHelper: uriHelper,
user: user,
);
}
Loading

0 comments on commit 6bb8deb

Please sign in to comment.