├── version.txt ├── test ├── test_assets │ ├── front_de.jpg │ ├── front_en.jpg │ ├── front_small_en.jpg │ ├── ingredients_en.jpg │ ├── nutrition_en.jpg │ ├── front_coca_light_de.jpg │ ├── nutrition_small_en.jpg │ ├── ingredients_small_en.jpg │ ├── ingredient_3613042717385.jpg │ └── ingredients_coca_light_de.jpg ├── test_constants.dart ├── api_post_robotoff_test.dart ├── barcode_validator_test.dart ├── api_get_taxonomy_nova_server_test.dart ├── api_get_product_image_ids_test.dart ├── recommended_daily_intake_test.dart └── json_conversion_test.dart ├── analysis_options.yaml ├── lib └── src │ ├── utils │ ├── autocompleter.dart │ ├── uri_reader_stub.dart │ ├── image_helper.dart │ ├── ocr_field.dart │ ├── uri_reader_io.dart │ ├── uri_reader_js.dart │ ├── too_many_requests_exception.dart │ ├── server_type.dart │ ├── tag_type.dart │ ├── uri_reader.dart │ ├── product_helper.dart │ ├── tag_type_autocompleter.dart │ ├── product_search_query_configuration.dart │ ├── too_many_requests_manager.dart │ ├── autocomplete_manager.dart │ ├── invalid_barcodes.dart │ ├── unit_helper.dart │ ├── nutriments_helper.dart │ └── recommended_daily_intake_helper.dart │ ├── model │ ├── tag_i18n.dart │ ├── parameter │ │ ├── without_additives.dart │ │ ├── page_number.dart │ │ ├── search_terms.dart │ │ ├── page_size.dart │ │ ├── barcode_parameter.dart │ │ ├── pnns_group2_filter.dart │ │ ├── states_tags_parameter.dart │ │ ├── allergens_parameter.dart │ │ ├── sort_by.dart │ │ ├── bool_map_parameter.dart │ │ └── ingredients_analysis_parameter.dart │ ├── product_type_filter.dart │ ├── agribalyse.dart │ ├── robotoff_question_order.dart │ ├── per_size.dart │ ├── agribalyse.g.dart │ ├── packaging.dart │ ├── off_tagged.dart │ ├── value_count.g.dart │ ├── insight.g.dart │ ├── key_stats.g.dart │ ├── leaderboard_entry.g.dart │ ├── packaging.g.dart │ ├── product_list.g.dart │ ├── badge_base.g.dart │ ├── ordered_nutrients.g.dart │ ├── value_count.dart │ ├── key_stats.dart │ ├── localized_tag.g.dart │ ├── nutrient_modifier.dart │ ├── user.g.dart │ ├── ecoscore_adjustments.dart │ ├── owner_field.dart │ ├── product_list.dart │ ├── leaderboard_entry.dart │ ├── ocr_packaging_result.dart │ ├── status.g.dart │ ├── badge_base.dart │ ├── ocr_ingredients_result.dart │ ├── product_stats.g.dart │ ├── ocr_packaging_result.g.dart │ ├── events_base.g.dart │ ├── user_agent.dart │ ├── ocr_ingredients_result.g.dart │ ├── additives.dart │ ├── product_result_field_answer.dart │ ├── taxonomy_allergen.g.dart │ ├── product_result_field.g.dart │ ├── product_stats.dart │ ├── product_type.dart │ ├── ecoscore_adjustments.g.dart │ ├── product_tag.g.dart │ ├── localized_tag.dart │ ├── search_result.dart │ ├── search_result.g.dart │ ├── origins_of_ingredients.dart │ ├── send_image.dart │ ├── origins_of_ingredients.g.dart │ ├── product_result_field_answer.g.dart │ ├── ordered_nutrient.g.dart │ ├── product_tag.dart │ ├── product_result_field.dart │ ├── taxonomy_language.g.dart │ ├── events_base.dart │ ├── product_freshness.dart │ ├── ecoscore_data.dart │ ├── taxonomy_packaging.g.dart │ ├── product_result_v3.g.dart │ ├── knowledge_panels.dart │ ├── robotoff_question.dart │ ├── user.dart │ ├── ordered_nutrient.dart │ ├── product_packaging.g.dart │ ├── knowledge_panel_action.dart │ ├── ecoscore_data.g.dart │ ├── robotoff_nutrient_extraction_annotation.g.dart │ ├── spelling_corrections.dart │ ├── ordered_nutrients.dart │ ├── robotoff_nutrient_extraction_annotation.dart │ ├── allergens.dart │ ├── attribute_group.dart │ ├── spelling_corrections.g.dart │ ├── nutrient_levels.dart │ ├── ingredient.g.dart │ ├── robotoff_nutrient_extraction.g.dart │ └── product_packaging.dart │ ├── prices │ ├── get_users_parameters.dart │ ├── update_proof_parameters.dart │ ├── get_users_order.dart │ ├── get_proofs_order.dart │ ├── get_challenges_order.dart │ ├── get_prices_order.dart │ ├── create_proof_parameters.dart │ ├── get_locations_order.dart │ ├── get_price_products_order.dart │ ├── order_by.dart │ ├── contribution_kind.dart │ ├── price_type.dart │ ├── location_type.dart │ ├── price_per.dart │ ├── get_price_count_parameters_helper.dart │ ├── session.g.dart │ ├── get_prices_result.dart │ ├── get_proofs_result.dart │ ├── get_users_result.dart │ ├── get_locations_parameters.dart │ ├── get_challenges_result.dart │ ├── proof_type.dart │ ├── get_locations_result.dart │ ├── location_osm_type.dart │ ├── get_price_products_result.dart │ ├── flavor.dart │ ├── get_users_result.g.dart │ ├── get_prices_result.g.dart │ ├── get_proofs_result.g.dart │ ├── get_locations_result.g.dart │ ├── get_challenges_result.g.dart │ ├── discount_type.dart │ ├── get_price_products_result.g.dart │ ├── maybe_error.dart │ ├── session.dart │ ├── get_price_products_parameters.dart │ ├── challenge.dart │ ├── get_proofs_parameters.dart │ ├── update_price_parameters.dart │ ├── get_challenges_parameters.dart │ ├── challenge.g.dart │ ├── common_proof_parameters.dart │ ├── get_prices_parameters.dart │ ├── price_user.dart │ └── price_product.dart │ ├── interface │ ├── parameter.dart │ └── json_object.dart │ ├── search │ ├── fuzziness.dart │ ├── autocomplete_single_result.g.dart │ ├── autocomplete_search_result.dart │ ├── autocomplete_search_result.g.dart │ ├── autocomplete_single_result.dart │ ├── taxonomy_name.dart │ └── taxonomy_name_autocompleter.dart │ └── personalized_search │ ├── available_attribute_groups.dart │ ├── available_product_preferences.dart │ └── preference_importance.dart ├── .github ├── CODEOWNERS ├── workflows │ ├── auto-assign-pr.yml │ ├── semantic-pr.yml │ ├── merge-conflict-autolabel.yml │ ├── label.yml │ ├── release_to_pub.dev.yml │ ├── dartdoc.yml │ ├── top-issues.yml │ ├── test-sdk.yml │ └── release-please.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── example └── pubspec.yaml ├── .gitignore ├── pubspec.yaml ├── REUSERS.md └── MIGRATIONS.md /version.txt: -------------------------------------------------------------------------------- 1 | 2.5.2 2 | -------------------------------------------------------------------------------- /test/test_assets/front_de.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/front_de.jpg -------------------------------------------------------------------------------- /test/test_assets/front_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/front_en.jpg -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | linter: 4 | rules: 5 | constant_identifier_names: false -------------------------------------------------------------------------------- /test/test_assets/front_small_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/front_small_en.jpg -------------------------------------------------------------------------------- /test/test_assets/ingredients_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/ingredients_en.jpg -------------------------------------------------------------------------------- /test/test_assets/nutrition_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/nutrition_en.jpg -------------------------------------------------------------------------------- /test/test_assets/front_coca_light_de.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/front_coca_light_de.jpg -------------------------------------------------------------------------------- /test/test_assets/nutrition_small_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/nutrition_small_en.jpg -------------------------------------------------------------------------------- /test/test_assets/ingredients_small_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/ingredients_small_en.jpg -------------------------------------------------------------------------------- /test/test_assets/ingredient_3613042717385.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/ingredient_3613042717385.jpg -------------------------------------------------------------------------------- /test/test_assets/ingredients_coca_light_de.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-dart/HEAD/test/test_assets/ingredients_coca_light_de.jpg -------------------------------------------------------------------------------- /lib/src/utils/autocompleter.dart: -------------------------------------------------------------------------------- 1 | /// Interface that provides autocomplete suggestions. 2 | abstract class Autocompleter { 3 | Future> getSuggestions(final String input); 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/utils/uri_reader_stub.dart: -------------------------------------------------------------------------------- 1 | import 'uri_reader.dart'; 2 | 3 | /// Needed for conditional imports 4 | UriReader getUriReaderInstance() => 5 | throw UnsupportedError('Cannot create the URI reader!'); 6 | -------------------------------------------------------------------------------- /lib/src/model/tag_i18n.dart: -------------------------------------------------------------------------------- 1 | /// A tag with internationalisation. 2 | class TagI18N { 3 | String id; 4 | 5 | String? textDE; 6 | String? textEN; 7 | String? textFR; 8 | 9 | TagI18N(this.id); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/utils/image_helper.dart: -------------------------------------------------------------------------------- 1 | /// Helper class related to product pictures 2 | class ImageHelper { 3 | /// Minimum accepted width for an uploaded image. 4 | static const int minimumWidth = 640; 5 | 6 | /// Minimum accepted height for an uploaded image. 7 | static const int minimumHeight = 160; 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/prices/get_users_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'get_price_count_parameters_helper.dart'; 2 | import 'get_users_order.dart'; 3 | 4 | /// Parameters for the "get users" API query. 5 | /// 6 | /// cf. https://prices.openfoodfacts.org/api/docs 7 | class GetUsersParameters 8 | extends GetPriceCountParametersHelper {} 9 | -------------------------------------------------------------------------------- /lib/src/utils/ocr_field.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | enum OcrField implements OffTagged { 4 | TESSERACT(offTag: 'tesseract'), 5 | GOOGLE_CLOUD_VISION(offTag: 'google_cloud_vision'); 6 | 7 | const OcrField({ 8 | required this.offTag, 9 | }); 10 | 11 | @override 12 | final String offTag; 13 | } 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | 2 | # These owners will be the default owners for everything in 3 | # the repo. Unless a later match takes precedence, 4 | # review when someone opens a pull request. 5 | # For more on how to customize the CODEOWNERS file - https://help.github.com/en/articles/about-code-owners 6 | 7 | * @openfoodfacts/openfoodfacts-dart 8 | -------------------------------------------------------------------------------- /lib/src/prices/update_proof_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'common_proof_parameters.dart'; 2 | import 'proof_type.dart'; 3 | 4 | /// Parameters for the "update proof" API query. 5 | /// 6 | /// cf. https://prices.openfoodfacts.org/api/docs 7 | class UpdateProofParameters extends CommonProofParameters { 8 | @override 9 | ProofType? type; 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/model/parameter/without_additives.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | 3 | /// "Without Additives" search API parameter 4 | class WithoutAdditives extends Parameter { 5 | @override 6 | String getName() => 'additives'; 7 | 8 | @override 9 | String getValue() => 'without'; 10 | 11 | const WithoutAdditives(); 12 | } 13 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: example for this dart API 3 | version: 0.0.2 4 | homepage: https://github.com/openfoodfacts/openfoodfacts-dart 5 | publish_to: none 6 | 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | 10 | dependencies: 11 | openfoodfacts: 12 | path: ../ 13 | 14 | dev_dependencies: 15 | test: any 16 | lints: ^2.0.0 17 | -------------------------------------------------------------------------------- /lib/src/prices/get_users_order.dart: -------------------------------------------------------------------------------- 1 | import 'order_by.dart'; 2 | 3 | /// Field for the "order by" clause of "get users". 4 | enum GetUsersOrderField implements OrderByField { 5 | priceCount(offTag: 'price_count'), 6 | userId(offTag: 'user_id'); 7 | 8 | const GetUsersOrderField({required this.offTag}); 9 | 10 | @override 11 | final String offTag; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/model/parameter/page_number.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | 3 | /// "Page number" search API parameter 4 | class PageNumber extends Parameter { 5 | @override 6 | String getName() => 'page'; 7 | 8 | @override 9 | String getValue() => page.toString(); 10 | 11 | final int page; 12 | 13 | const PageNumber({required this.page}); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/prices/get_proofs_order.dart: -------------------------------------------------------------------------------- 1 | import 'order_by.dart'; 2 | 3 | /// Field for the "order by" clause of "get proofs". 4 | enum GetProofsOrderField implements OrderByField { 5 | priceCount(offTag: 'price_count'), 6 | created(offTag: 'created'); 7 | 8 | const GetProofsOrderField({required this.offTag}); 9 | 10 | @override 11 | final String offTag; 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign-pr.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/auto-author-assign.yml 2 | name: 'Auto Author Assign' 3 | 4 | on: 5 | pull_request_target: 6 | types: [opened, reopened] 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | assign-author: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: toshimaru/auto-author-assign@v2.1.1 16 | -------------------------------------------------------------------------------- /lib/src/prices/get_challenges_order.dart: -------------------------------------------------------------------------------- 1 | import 'order_by.dart'; 2 | 3 | /// Field for the "order by" clause of "get challenges". 4 | enum GetChallengesOrderField implements OrderByField { 5 | created(offTag: 'created'), 6 | updated(offTag: 'updated'); 7 | 8 | const GetChallengesOrderField({required this.offTag}); 9 | 10 | @override 11 | final String offTag; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/prices/get_prices_order.dart: -------------------------------------------------------------------------------- 1 | import 'order_by.dart'; 2 | 3 | /// Field for the "order by" clause of "get prices". 4 | enum GetPricesOrderField implements OrderByField { 5 | created(offTag: 'created'), 6 | date(offTag: 'date'), 7 | price(offTag: 'price'); 8 | 9 | const GetPricesOrderField({required this.offTag}); 10 | 11 | @override 12 | final String offTag; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/model/parameter/search_terms.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | 3 | /// Term list search API parameter 4 | class SearchTerms extends Parameter { 5 | @override 6 | String getName() => 'search_terms'; 7 | 8 | @override 9 | String getValue() => terms.join('+'); 10 | 11 | final List terms; 12 | 13 | const SearchTerms({required this.terms}); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/prices/create_proof_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'common_proof_parameters.dart'; 2 | import 'proof_type.dart'; 3 | 4 | /// Parameters for the "upload proof" API query. 5 | /// 6 | /// cf. https://prices.openfoodfacts.org/api/docs 7 | class CreateProofParameters extends CommonProofParameters { 8 | CreateProofParameters(this.type); 9 | 10 | @override 11 | final ProofType type; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/utils/uri_reader_io.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'uri_reader.dart'; 3 | 4 | UriReader getUriReaderInstance() => UriReaderIo(); 5 | 6 | /// Reader of URI data, "not web" version (i.e. supports `File`) 7 | class UriReaderIo extends UriReader { 8 | @override 9 | Future> readFileAsBytes(final Uri uri) async => 10 | await File.fromUri(uri).readAsBytes(); 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Semantic PRs" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v6 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /lib/src/interface/parameter.dart: -------------------------------------------------------------------------------- 1 | /// Abstract search API parameter, to be used in the search URI 2 | /// 3 | /// Typical use will be 'name=value' 4 | abstract class Parameter { 5 | /// URI parameter name 6 | String getName(); 7 | 8 | /// URI parameter value 9 | String getValue(); 10 | 11 | @override 12 | String toString() => '&${getName()}=${getValue()}'; 13 | 14 | const Parameter(); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/utils/uri_reader_js.dart: -------------------------------------------------------------------------------- 1 | import 'uri_reader.dart'; 2 | 3 | UriReader getUriReaderInstance() => UriReaderJs(); 4 | 5 | /// Reader of URI data, "web" version 6 | class UriReaderJs extends UriReader { 7 | @override 8 | Future> readFileAsBytes(final Uri uri) async => 9 | throw Exception('Cannot read files in web version'); 10 | 11 | @override 12 | bool get isWeb => true; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/prices/get_locations_order.dart: -------------------------------------------------------------------------------- 1 | import 'order_by.dart'; 2 | 3 | /// Field for the "order by" clause of "get locations". 4 | enum GetLocationsOrderField implements OrderByField { 5 | priceCount(offTag: 'price_count'), 6 | created(offTag: 'created'), 7 | updated(offTag: 'updated'); 8 | 9 | const GetLocationsOrderField({required this.offTag}); 10 | 11 | @override 12 | final String offTag; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/prices/get_price_products_order.dart: -------------------------------------------------------------------------------- 1 | import 'order_by.dart'; 2 | 3 | /// Field for the "order by" clause of "get price products". 4 | enum GetPriceProductsOrderField implements OrderByField { 5 | priceCount(offTag: 'price_count'), 6 | created(offTag: 'created'), 7 | updated(offTag: 'updated'); 8 | 9 | const GetPriceProductsOrderField({required this.offTag}); 10 | 11 | @override 12 | final String offTag; 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | commit-message: 9 | prefix: "chore" 10 | include: "scope" 11 | - package-ecosystem: "pub" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | commit-message: 16 | prefix: "chore" 17 | include: "scope" 18 | -------------------------------------------------------------------------------- /lib/src/model/parameter/page_size.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | 3 | /// "Page size" search API parameter 4 | /// 5 | /// Typically defaults to 50 (used to be 24). Max value seems to be 100. 6 | class PageSize extends Parameter { 7 | @override 8 | String getName() => 'page_size'; 9 | 10 | @override 11 | String getValue() => size.toString(); 12 | 13 | final int size; 14 | 15 | const PageSize({required this.size}); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/model/parameter/barcode_parameter.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | 3 | /// "Barcodes" search API parameter. 4 | class BarcodeParameter extends Parameter { 5 | @override 6 | String getName() => 'code'; 7 | 8 | @override 9 | String getValue() => codes.join(','); 10 | 11 | final List codes; 12 | 13 | BarcodeParameter(final String code) : this.list([code]); 14 | 15 | const BarcodeParameter.list(this.codes); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/model/parameter/pnns_group2_filter.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | import '../../utils/pnns_groups.dart'; 3 | 4 | /// [PnnsGroup2] search API parameter 5 | class PnnsGroup2Filter extends Parameter { 6 | @override 7 | String getName() => 'pnns_groups_2_tags'; 8 | 9 | @override 10 | String getValue() => pnnsGroup2.offTag; 11 | 12 | final PnnsGroup2 pnnsGroup2; 13 | 14 | const PnnsGroup2Filter({required this.pnnsGroup2}); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/model/product_type_filter.dart: -------------------------------------------------------------------------------- 1 | import 'off_tagged.dart'; 2 | import 'product_type.dart'; 3 | 4 | /// Filter on product type for API get product queries. 5 | class ProductTypeFilter implements OffTagged { 6 | const ProductTypeFilter._(this.offTag); 7 | 8 | ProductTypeFilter(final ProductType productType) 9 | : offTag = productType.offTag; 10 | 11 | static const ProductTypeFilter all = ProductTypeFilter._('all'); 12 | 13 | @override 14 | final String offTag; 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | openfoodfacts.iml 2 | 3 | # local files of paths to flutter and android SDK 4 | android/local.properties 5 | 6 | .DS_Store 7 | .dart_tool/ 8 | 9 | .packages 10 | .pub/ 11 | .flutter-plugins 12 | pubspec.lock 13 | 14 | 15 | # build files 16 | build/ 17 | out/ 18 | android/ 19 | ios/ 20 | 21 | # IntelliJ project files 22 | *.iml 23 | *.iws 24 | *.ipr 25 | .idea/ 26 | 27 | # VSCode 28 | .vscode/ 29 | 30 | # eclipse project file 31 | .settings/ 32 | .classpath 33 | .project 34 | 35 | doc/ 36 | 37 | -------------------------------------------------------------------------------- /lib/src/model/parameter/states_tags_parameter.dart: -------------------------------------------------------------------------------- 1 | import '../product_state.dart'; 2 | import 'bool_map_parameter.dart'; 3 | 4 | /// States Tags as completed or to-be-completed. 5 | class StatesTagsParameter extends BoolMapParameter { 6 | @override 7 | String getName() => 'states_tags'; 8 | 9 | @override 10 | String getTag(final ProductState key, final bool value) => 11 | value ? key.completedTag : key.toBeCompletedTag; 12 | 13 | const StatesTagsParameter({required super.map}); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/search/fuzziness.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/src/model/off_tagged.dart'; 2 | 3 | /// Fuzziness Level for Elastic Search API. 4 | /// 5 | /// Levenshtein distance (= number of edits). 6 | /// cf. https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness 7 | enum Fuzziness implements OffTagged { 8 | none(offTag: '0'), 9 | one(offTag: '1'), 10 | two(offTag: '2'); 11 | 12 | const Fuzziness({ 13 | required this.offTag, 14 | }); 15 | 16 | @override 17 | final String offTag; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/model/agribalyse.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'agribalyse.g.dart'; 5 | 6 | @JsonSerializable() 7 | class Agribalyse extends JsonObject { 8 | @JsonKey(includeIfNull: false, fromJson: JsonObject.parseDouble) 9 | double? score; 10 | 11 | Agribalyse({this.score}); 12 | 13 | factory Agribalyse.fromJson(Map json) => 14 | _$AgribalyseFromJson(json); 15 | 16 | @override 17 | Map toJson() => _$AgribalyseToJson(this); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/prices/order_by.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | /// "Order by" field for "get list" methods (e.g. "get prices") 4 | abstract class OrderByField implements OffTagged {} 5 | 6 | /// "Order by" clause for "get list" methods (e.g. "get prices") 7 | class OrderBy implements OffTagged { 8 | const OrderBy({ 9 | required this.field, 10 | required this.ascending, 11 | }); 12 | 13 | final T field; 14 | final bool ascending; 15 | 16 | @override 17 | String get offTag => '${ascending ? '' : '-'}${field.offTag}'; 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/merge-conflict-autolabel.yml: -------------------------------------------------------------------------------- 1 | name: '💥 Auto-Label Merge Conflicts on PRs' 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | triage: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: mschilde/auto-label-merge-conflicts@master 16 | with: 17 | CONFLICT_LABEL_NAME: "💥 Merge Conflicts" 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | MAX_RETRIES: 5 20 | WAIT_MS: 5000 21 | -------------------------------------------------------------------------------- /lib/src/model/parameter/allergens_parameter.dart: -------------------------------------------------------------------------------- 1 | import '../allergens.dart'; 2 | import 'bool_map_parameter.dart'; 3 | 4 | /// List of allergens to filter in and out. 5 | /// 6 | /// When we have several items, the results returned use a logical AND. 7 | class AllergensParameter extends BoolMapParameter { 8 | @override 9 | String getName() => 'allergens_tags'; 10 | 11 | @override 12 | String getTag(final AllergensTag key, final bool value) => 13 | value ? key.offTag : '-${key.offTag}'; 14 | 15 | const AllergensParameter({required super.map}); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/model/robotoff_question_order.dart: -------------------------------------------------------------------------------- 1 | import 'off_tagged.dart'; 2 | 3 | /// Order for Robotoff questions. 4 | enum RobotoffQuestionOrder implements OffTagged { 5 | /// order by (descending) model confidence, null confidence insights come last 6 | confidence(offTag: 'confidence'), 7 | 8 | /// random order 9 | random(offTag: 'random'), 10 | 11 | /// order by (descending) popularity (=scan count) 12 | popularity(offTag: 'popularity'); 13 | 14 | const RobotoffQuestionOrder({ 15 | required this.offTag, 16 | }); 17 | 18 | @override 19 | final String offTag; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/prices/contribution_kind.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | /// Kind of the contribution 4 | enum ContributionKind implements OffTagged { 5 | consumption(offTag: 'CONSUMPTION'), 6 | community(offTag: 'COMMUNITY'); 7 | 8 | const ContributionKind({ 9 | required this.offTag, 10 | }); 11 | @override 12 | final String offTag; 13 | 14 | /// Returns the first [ContributionKind] that matches the [offTag]. 15 | static ContributionKind? fromOffTag(final String? offTag) => 16 | OffTagged.fromOffTag(offTag, ContributionKind.values) 17 | as ContributionKind?; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/model/per_size.dart: -------------------------------------------------------------------------------- 1 | import 'off_tagged.dart'; 2 | 3 | /// Used for nutrient values: for which size of the product? 4 | enum PerSize implements OffTagged { 5 | /// Per serving of product 6 | serving(offTag: 'serving'), 7 | 8 | /// Per 100 grams of product 9 | oneHundredGrams(offTag: '100g'); 10 | 11 | const PerSize({ 12 | required this.offTag, 13 | }); 14 | 15 | @override 16 | final String offTag; 17 | 18 | /// Returns the first [PerSize] that matches the [offTag]. 19 | static PerSize? fromOffTag(final String? offTag) => 20 | OffTagged.fromOffTag(offTag, PerSize.values) as PerSize?; 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/model/agribalyse.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'agribalyse.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Agribalyse _$AgribalyseFromJson(Map json) => Agribalyse( 10 | score: JsonObject.parseDouble(json['score']), 11 | ); 12 | 13 | Map _$AgribalyseToJson(Agribalyse instance) => 14 | { 15 | if (instance.score case final value?) 'score': value, 16 | }; 17 | -------------------------------------------------------------------------------- /lib/src/prices/price_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../model/off_tagged.dart'; 3 | 4 | /// Type of a Price. 5 | enum PriceType implements OffTagged { 6 | @JsonValue('PRODUCT') 7 | product(offTag: 'PRODUCT'), 8 | 9 | @JsonValue('CATEGORY') 10 | category(offTag: 'CATEGORY'); 11 | 12 | const PriceType({ 13 | required this.offTag, 14 | }); 15 | 16 | @override 17 | final String offTag; 18 | 19 | /// Returns the first [PriceType] that matches the [offTag]. 20 | static PriceType? fromOffTag(final String? offTag) => 21 | OffTagged.fromOffTag(offTag, PriceType.values) as PriceType?; 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/model/packaging.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'packaging.g.dart'; 5 | 6 | @JsonSerializable() 7 | class Packaging extends JsonObject { 8 | @JsonKey(includeIfNull: false, fromJson: JsonObject.parseDouble) 9 | double? value; 10 | @JsonKey(includeIfNull: false, fromJson: JsonObject.parseDouble) 11 | double? score; 12 | 13 | Packaging({this.score, this.value}); 14 | 15 | factory Packaging.fromJson(Map json) => 16 | _$PackagingFromJson(json); 17 | 18 | @override 19 | Map toJson() => _$PackagingToJson(this); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/prices/location_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import '../model/off_tagged.dart'; 4 | 5 | /// Type of the Location. 6 | enum LocationType implements OffTagged { 7 | @JsonValue('OSM') 8 | osm(offTag: 'OSM'), 9 | @JsonValue('ONLINE') 10 | online(offTag: 'ONLINE'); 11 | 12 | const LocationType({ 13 | required this.offTag, 14 | }); 15 | 16 | @override 17 | final String offTag; 18 | 19 | /// Returns the first [LocationType] that matches the [offTag]. 20 | static LocationType? fromOffTag(final String? offTag) => 21 | OffTagged.fromOffTag(offTag, LocationType.values) as LocationType?; 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | type: '✨ Enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Why - Problem description 11 | 12 | 13 | ### What - Proposed solution 14 | 15 | 16 | ### Alternatives you've considered 17 | 18 | 19 | ### Additional context 20 | 21 | -------------------------------------------------------------------------------- /lib/src/model/off_tagged.dart: -------------------------------------------------------------------------------- 1 | /// Interface about OFF code unification. 2 | abstract class OffTagged { 3 | /// Identifying tag used in the rest of OFF. 4 | String get offTag; 5 | 6 | /// Returns the first [OffTagged] of [values] that matches the [offTag]. 7 | /// 8 | /// Typical use: with `enum`s that implement OffTagged. 9 | static OffTagged? fromOffTag( 10 | final String? offTag, 11 | final Iterable values, 12 | ) { 13 | if (offTag == null) { 14 | return null; 15 | } 16 | for (final OffTagged offTagged in values) { 17 | if (offTagged.offTag == offTag) { 18 | return offTagged; 19 | } 20 | } 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/model/value_count.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'value_count.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ValueCount _$ValueCountFromJson(Map json) => ValueCount( 10 | value: json['v'] as String, 11 | productCount: (json['product_count'] as num).toInt(), 12 | ); 13 | 14 | Map _$ValueCountToJson(ValueCount instance) => 15 | { 16 | 'v': instance.value, 17 | 'product_count': instance.productCount, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/src/prices/price_per.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../model/off_tagged.dart'; 3 | 4 | /// Price Per. 5 | /// 6 | /// cf. PricePerEnum in https://prices.openfoodfacts.org/api/docs 7 | enum PricePer implements OffTagged { 8 | @JsonValue('UNIT') 9 | unit(offTag: 'UNIT'), 10 | 11 | @JsonValue('KILOGRAM') 12 | kilogram(offTag: 'KILOGRAM'); 13 | 14 | const PricePer({ 15 | required this.offTag, 16 | }); 17 | 18 | @override 19 | final String offTag; 20 | 21 | /// Returns the first [PricePer] that matches the [offTag]. 22 | static PricePer? fromOffTag(final String? offTag) => 23 | OffTagged.fromOffTag(offTag, PricePer.values) as PricePer?; 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | description: 'Create a report to help us improve the Open Food Facts Dart SDK' 4 | about: 'Create a report to help us improve the Open Food Facts Dart SDK' 5 | title: '' 6 | type: '🐛 Bug' 7 | assignees: '' 8 | 9 | --- 10 | 11 | ### Description 12 | 13 | 14 | ### Expected behavior 15 | 16 | 17 | ### Stacktraces 18 | 19 | 20 | ### Package information 21 | 22 | 23 | ### Additional context 24 | 25 | -------------------------------------------------------------------------------- /lib/src/model/insight.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'insight.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | InsightsResult _$InsightsResultFromJson(Map json) => 10 | InsightsResult( 11 | status: json['status'] as String?, 12 | insights: Insight.fromJson(json['insights']), 13 | ); 14 | 15 | Map _$InsightsResultToJson(InsightsResult instance) => 16 | { 17 | 'status': instance.status, 18 | 'insights': Insight.toJson(instance.insights), 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/model/key_stats.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'key_stats.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | KeyStats _$KeyStatsFromJson(Map json) => KeyStats( 10 | key: json['k'] as String, 11 | count: (json['count'] as num).toInt(), 12 | values: (json['values'] as num).toInt(), 13 | ); 14 | 15 | Map _$KeyStatsToJson(KeyStats instance) => { 16 | 'k': instance.key, 17 | 'count': instance.count, 18 | 'values': instance.values, 19 | }; 20 | -------------------------------------------------------------------------------- /test/test_constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/openfoodfacts.dart'; 2 | 3 | class TestConstants { 4 | // ignore: non_constant_identifier_names 5 | static final UserAgent TEST_USER_AGENT = UserAgent( 6 | name: 'off-dart integration tests', 7 | ); 8 | 9 | static const User TEST_USER = User( 10 | userId: 'openfoodfacts-dart', 11 | password: 'iloveflutter', 12 | comment: 'dart API test', 13 | ); 14 | 15 | static const User PROD_USER = User( 16 | userId: 'grumpf', 17 | password: 'takeitorleaveit', 18 | comment: 'dart API test', 19 | ); 20 | 21 | static const String badGatewayError = 22 | 'Exception: JSON expected, html found: 502 Bad Gateway'; 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/model/leaderboard_entry.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'leaderboard_entry.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | LeaderboardEntry _$LeaderboardEntryFromJson(Map json) => 10 | LeaderboardEntry( 11 | score: (json['score'] as num).toInt(), 12 | userId: json['user_id'] as String?, 13 | ); 14 | 15 | Map _$LeaderboardEntryToJson(LeaderboardEntry instance) => 16 | { 17 | 'user_id': instance.userId, 18 | 'score': instance.score, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/model/packaging.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'packaging.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Packaging _$PackagingFromJson(Map json) => Packaging( 10 | score: JsonObject.parseDouble(json['score']), 11 | value: JsonObject.parseDouble(json['value']), 12 | ); 13 | 14 | Map _$PackagingToJson(Packaging instance) => { 15 | if (instance.value case final value?) 'value': value, 16 | if (instance.score case final value?) 'score': value, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/src/model/product_list.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product_list.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ProductList _$ProductListFromJson(Map json) => ProductList( 10 | barcode: json['product'] as String, 11 | key: json['k'] as String, 12 | value: json['v'] as String, 13 | ); 14 | 15 | Map _$ProductListToJson(ProductList instance) => 16 | { 17 | 'product': instance.barcode, 18 | 'k': instance.key, 19 | 'v': instance.value, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/src/prices/get_price_count_parameters_helper.dart: -------------------------------------------------------------------------------- 1 | import 'get_parameters_helper.dart'; 2 | import 'order_by.dart'; 3 | 4 | /// Helper class for API query parameters with price count filters. 5 | abstract class GetPriceCountParametersHelper 6 | extends GetParametersHelper { 7 | int? priceCount; 8 | int? priceCountGte; 9 | int? priceCountLte; 10 | 11 | /// Returns the parameters as a query parameter map. 12 | @override 13 | Map getQueryParameters() { 14 | super.getQueryParameters(); 15 | addNonNullInt(priceCount, 'price_count'); 16 | addNonNullInt(priceCountGte, 'price_count__gte'); 17 | addNonNullInt(priceCountLte, 'price_count__lte'); 18 | return result; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/utils/too_many_requests_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:http/http.dart'; 2 | 3 | /// Exception when the server returns "Too many requests". 4 | class TooManyRequestsException implements Exception { 5 | const TooManyRequestsException(); 6 | 7 | /// Start of the response body when the server received too many requests. 8 | static const String _tooManyRequestsError = 9 | '

TOO MANY REQUESTS

'; 10 | 11 | static void check(final Response response) { 12 | if (response.body.startsWith(_tooManyRequestsError)) { 13 | throw TooManyRequestsException(); 14 | } 15 | } 16 | 17 | @override 18 | String toString() => 'Too many requests'; 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/model/badge_base.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'badge_base.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | BadgeBase _$BadgeBaseFromJson(Map json) => BadgeBase( 10 | badgeName: json['badge_name'] as String, 11 | level: (json['level'] as num).toInt(), 12 | userId: json['user_id'] as String?, 13 | ); 14 | 15 | Map _$BadgeBaseToJson(BadgeBase instance) => { 16 | 'user_id': instance.userId, 17 | 'badge_name': instance.badgeName, 18 | 'level': instance.level, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/model/ordered_nutrients.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ordered_nutrients.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | OrderedNutrients _$OrderedNutrientsFromJson(Map json) => 10 | OrderedNutrients( 11 | nutrients: (json['nutrients'] as List) 12 | .map((e) => OrderedNutrient.fromJson(e as Map)) 13 | .toList(), 14 | ); 15 | 16 | Map _$OrderedNutrientsToJson(OrderedNutrients instance) => 17 | { 18 | 'nutrients': instance.nutrients, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler 7 | 8 | name: Labeler 9 | on: [pull_request] 10 | 11 | jobs: 12 | label: 13 | runs-on: ubuntu-latest 14 | if: github.event.pull_request.head.repo.full_name == github.repository 15 | #if: ${{ github.repository_owner == 'openfoodfacts' }} 16 | permissions: 17 | contents: read 18 | pull-requests: write 19 | steps: 20 | - uses: actions/labeler@v6 21 | with: 22 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 23 | -------------------------------------------------------------------------------- /.github/workflows/release_to_pub.dev.yml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | # https://github.com/dart-lang/ecosystem/tree/main/pkgs/firehose 3 | # https://github.com/dart-lang/ecosystem/wiki/Publishing-automation 4 | 5 | name: Publish 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | # release please PR, will only run "validate" 11 | - release-please--branches--master--components--openfoodfacts 12 | types: [opened, synchronize, reopened, labeled, unlabeled] 13 | push: 14 | # new version triggers validate and publish 15 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 16 | 17 | permissions: 18 | pull-requests: write 19 | id-token: write 20 | 21 | jobs: 22 | publish: 23 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 24 | -------------------------------------------------------------------------------- /lib/src/model/value_count.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'value_count.g.dart'; 5 | 6 | /// Folksonomy: statistics around a value. 7 | @JsonSerializable() 8 | class ValueCount extends JsonObject { 9 | @JsonKey(name: 'v') 10 | final String value; 11 | 12 | @JsonKey(name: 'product_count') 13 | final int productCount; 14 | 15 | ValueCount({ 16 | required this.value, 17 | required this.productCount, 18 | }); 19 | 20 | factory ValueCount.fromJson(Map json) => 21 | _$ValueCountFromJson(json); 22 | 23 | @override 24 | Map toJson() => _$ValueCountToJson(this); 25 | 26 | @override 27 | String toString() => toJson().toString(); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/utils/server_type.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | /// Type of Server (e.g. "food facts", "beauty facts", ...). 4 | enum ServerType implements OffTagged { 5 | /// Open Food Facts 6 | openFoodFacts(offTag: 'off'), 7 | 8 | /// Open Beauty Facts 9 | openBeautyFacts(offTag: 'obf'), 10 | 11 | /// Open Pet Food Facts 12 | openPetFoodFacts(offTag: 'opff'), 13 | 14 | /// Open Product Facts 15 | openProductFacts(offTag: 'opf'); 16 | 17 | const ServerType({ 18 | required this.offTag, 19 | }); 20 | 21 | @override 22 | final String offTag; 23 | 24 | /// Returns the [ServerType] that matches the [offTag]. 25 | static ServerType? fromOffTag(final String? offTag) => 26 | OffTagged.fromOffTag(offTag, ServerType.values) as ServerType?; 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/dartdoc.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages Deploy Action 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | jobs: 7 | deploy-pages: 8 | name: Deploy to GitHub Pages 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | if: github.repository_owner == 'openfoodfacts' 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v5 16 | 17 | - name: Setup Dart 18 | uses: dart-lang/setup-dart@v1 19 | 20 | - name: Run Dartdoc 21 | run: dart doc . 22 | 23 | - name: Deploy API documentation to Github Pages 24 | uses: JamesIves/github-pages-deploy-action@v4.7.4 25 | with: 26 | BRANCH: gh-pages 27 | FOLDER: doc/api/ 28 | -------------------------------------------------------------------------------- /lib/src/prices/session.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'session.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Session _$SessionFromJson(Map json) => Session() 10 | ..userId = json['user_id'] as String 11 | ..created = JsonHelper.stringTimestampToDate(json['created']) 12 | ..lastUsed = JsonHelper.nullableStringTimestampToDate(json['last_used']); 13 | 14 | Map _$SessionToJson(Session instance) => { 15 | 'user_id': instance.userId, 16 | 'created': instance.created.toIso8601String(), 17 | 'last_used': instance.lastUsed?.toIso8601String(), 18 | }; 19 | -------------------------------------------------------------------------------- /lib/src/model/key_stats.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'key_stats.g.dart'; 5 | 6 | /// Folksonomy: statistics around a tag key. 7 | @JsonSerializable() 8 | class KeyStats extends JsonObject { 9 | @JsonKey(name: 'k') 10 | final String key; 11 | @JsonKey(name: 'count') 12 | final int count; 13 | @JsonKey(name: 'values') 14 | final int values; 15 | 16 | KeyStats({ 17 | required this.key, 18 | required this.count, 19 | required this.values, 20 | }); 21 | 22 | factory KeyStats.fromJson(Map json) => 23 | _$KeyStatsFromJson(json); 24 | 25 | @override 26 | Map toJson() => _$KeyStatsToJson(this); 27 | 28 | @override 29 | String toString() => toJson().toString(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/model/localized_tag.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'localized_tag.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | LocalizedTag _$LocalizedTagFromJson(Map json) => LocalizedTag() 10 | ..id = json['id'] as String? 11 | ..name = json['name'] as String? 12 | ..lcName = json['lc_name'] as String?; 13 | 14 | Map _$LocalizedTagToJson(LocalizedTag instance) => 15 | { 16 | if (instance.id case final value?) 'id': value, 17 | if (instance.name case final value?) 'name': value, 18 | if (instance.lcName case final value?) 'lc_name': value, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/model/nutrient_modifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/src/model/off_tagged.dart'; 2 | 3 | enum NutrientModifier implements OffTagged { 4 | approximately(offTag: '~'), 5 | greaterThan(offTag: '>'), 6 | lessThan(offTag: '<'); 7 | 8 | const NutrientModifier({ 9 | required this.offTag, 10 | }); 11 | @override 12 | final String offTag; 13 | 14 | /// Returns the first [NutrientModifier] that matches the [offTag]. 15 | static NutrientModifier? fromOffTag(final String? offTag) => 16 | OffTagged.fromOffTag(offTag, NutrientModifier.values) 17 | as NutrientModifier?; 18 | 19 | static NutrientModifier? fromValue(final String? value) { 20 | if (value == null || value.isEmpty) { 21 | return null; 22 | } 23 | 24 | return fromOffTag(value.substring(0, 1)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/model/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | User _$UserFromJson(Map json) => User( 10 | comment: json['comment'] as String?, 11 | userId: json['user_id'] as String, 12 | password: json['password'] as String, 13 | cookie: json['cookie'] as String?, 14 | ); 15 | 16 | Map _$UserToJson(User instance) => { 17 | if (instance.comment case final value?) 'comment': value, 18 | 'user_id': instance.userId, 19 | 'password': instance.password, 20 | 'cookie': instance.cookie, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/src/model/ecoscore_adjustments.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | import 'origins_of_ingredients.dart'; 4 | import 'packaging.dart'; 5 | 6 | part 'ecoscore_adjustments.g.dart'; 7 | 8 | @JsonSerializable(explicitToJson: true) 9 | class EcoscoreAdjustments extends JsonObject { 10 | @JsonKey(includeIfNull: false) 11 | Packaging? packaging; 12 | @JsonKey(name: 'origins_of_ingredients', includeIfNull: false) 13 | OriginsOfIngredients? originsOfIngredients; 14 | 15 | EcoscoreAdjustments({this.packaging, this.originsOfIngredients}); 16 | 17 | factory EcoscoreAdjustments.fromJson(Map json) => 18 | _$EcoscoreAdjustmentsFromJson(json); 19 | 20 | @override 21 | Map toJson() => _$EcoscoreAdjustmentsToJson(this); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/top-issues.yml: -------------------------------------------------------------------------------- 1 | name: Top issues action. 2 | on: 3 | schedule: 4 | - cron: '0 0 */1 * *' 5 | #on: 6 | # issues: 7 | # types: [opened, transferred] 8 | #on: 9 | # push: 10 | # branches: 11 | # - top-issues 12 | jobs: 13 | ShowAndLabelTopIssues: 14 | name: Display and label top issues. 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Run top issues action 18 | uses: rickstaa/top-issues-action@v1 19 | env: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | label: true 23 | dashboard: true 24 | dashboard_title: 👍 Top Issues Dashboard 25 | dashboard_show_total_reactions: true 26 | top_issues: true 27 | top_bugs: true 28 | top_features: true 29 | top_pull_requests: true 30 | top_list_size: 20 31 | -------------------------------------------------------------------------------- /lib/src/model/owner_field.dart: -------------------------------------------------------------------------------- 1 | import '../utils/language_helper.dart'; 2 | import '../utils/product_fields.dart'; 3 | import 'nutrient.dart'; 4 | import 'off_tagged.dart'; 5 | 6 | /// Helper class to compute the Product.ownerFields tags. 7 | class OwnerField implements OffTagged { 8 | const OwnerField.raw(this.offTag); 9 | 10 | OwnerField.nutrient(final Nutrient nutrient) : this.raw(nutrient.offTag); 11 | 12 | factory OwnerField.productField( 13 | final ProductField productField, 14 | final OpenFoodFactsLanguage language, 15 | ) { 16 | final ProductField? inLanguages = productField.inLanguages; 17 | if (inLanguages == null) { 18 | return OwnerField.raw(productField.offTag); 19 | } 20 | return OwnerField.raw('${inLanguages.offTag}${language.offTag}'); 21 | } 22 | 23 | @override 24 | final String offTag; 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/model/product_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'product_list.g.dart'; 5 | 6 | /// Folksonomy: current value for a product and a tag key. 7 | @JsonSerializable() 8 | class ProductList extends JsonObject { 9 | @JsonKey(name: 'product') 10 | final String barcode; 11 | @JsonKey(name: 'k') 12 | final String key; 13 | @JsonKey(name: 'v') 14 | final String value; 15 | 16 | ProductList({ 17 | required this.barcode, 18 | required this.key, 19 | required this.value, 20 | }); 21 | 22 | factory ProductList.fromJson(Map json) => 23 | _$ProductListFromJson(json); 24 | 25 | @override 26 | Map toJson() => _$ProductListToJson(this); 27 | 28 | @override 29 | String toString() => toJson().toString(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/prices/get_prices_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'price.dart'; 4 | import '../interface/json_object.dart'; 5 | 6 | part 'get_prices_result.g.dart'; 7 | 8 | /// List of price objects returned by the "get prices" method. 9 | @JsonSerializable() 10 | class GetPricesResult extends JsonObject { 11 | @JsonKey() 12 | List? items; 13 | 14 | @JsonKey() 15 | int? total; 16 | 17 | @JsonKey(name: 'page') 18 | int? pageNumber; 19 | 20 | @JsonKey(name: 'size') 21 | int? pageSize; 22 | 23 | @JsonKey(name: 'pages') 24 | int? numberOfPages; 25 | 26 | GetPricesResult(); 27 | 28 | factory GetPricesResult.fromJson(Map json) => 29 | _$GetPricesResultFromJson(json); 30 | 31 | @override 32 | Map toJson() => _$GetPricesResultToJson(this); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/prices/get_proofs_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'proof.dart'; 4 | import '../interface/json_object.dart'; 5 | 6 | part 'get_proofs_result.g.dart'; 7 | 8 | /// List of proof objects returned by the "get proofs" method. 9 | @JsonSerializable() 10 | class GetProofsResult extends JsonObject { 11 | @JsonKey() 12 | List? items; 13 | 14 | @JsonKey() 15 | int? total; 16 | 17 | @JsonKey(name: 'page') 18 | int? pageNumber; 19 | 20 | @JsonKey(name: 'size') 21 | int? pageSize; 22 | 23 | @JsonKey(name: 'pages') 24 | int? numberOfPages; 25 | 26 | GetProofsResult(); 27 | 28 | factory GetProofsResult.fromJson(Map json) => 29 | _$GetProofsResultFromJson(json); 30 | 31 | @override 32 | Map toJson() => _$GetProofsResultToJson(this); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/prices/get_users_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'price_user.dart'; 4 | import '../interface/json_object.dart'; 5 | 6 | part 'get_users_result.g.dart'; 7 | 8 | /// List of price user objects returned by the "get users" method. 9 | @JsonSerializable() 10 | class GetUsersResult extends JsonObject { 11 | @JsonKey() 12 | List? items; 13 | 14 | @JsonKey() 15 | int? total; 16 | 17 | @JsonKey(name: 'page') 18 | int? pageNumber; 19 | 20 | @JsonKey(name: 'size') 21 | int? pageSize; 22 | 23 | @JsonKey(name: 'pages') 24 | int? numberOfPages; 25 | 26 | GetUsersResult(); 27 | 28 | factory GetUsersResult.fromJson(Map json) => 29 | _$GetUsersResultFromJson(json); 30 | 31 | @override 32 | Map toJson() => _$GetUsersResultToJson(this); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/model/leaderboard_entry.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'leaderboard_entry.g.dart'; 5 | 6 | /// Events API: leaderboard entry. 7 | @JsonSerializable() 8 | class LeaderboardEntry extends JsonObject { 9 | @JsonKey(name: 'user_id') 10 | final String? userId; 11 | 12 | @JsonKey() 13 | final int score; 14 | 15 | LeaderboardEntry({ 16 | required this.score, 17 | this.userId, 18 | }); 19 | 20 | factory LeaderboardEntry.fromJson(Map json) => 21 | _$LeaderboardEntryFromJson(json); 22 | 23 | @override 24 | Map toJson() => _$LeaderboardEntryToJson(this); 25 | 26 | @override 27 | String toString() => 'LeaderboardEntry(score: $score' 28 | '${userId == null ? ', no user id' : ', userId: $userId'}' 29 | ')'; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/model/ocr_packaging_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'ocr_packaging_result.g.dart'; 5 | 6 | /// Result from OCR applied to packaging. 7 | @JsonSerializable() 8 | class OcrPackagingResult extends JsonObject { 9 | const OcrPackagingResult({ 10 | this.status, 11 | this.textFromImageOrig, 12 | this.textFromImage, 13 | }); 14 | 15 | factory OcrPackagingResult.fromJson(Map json) => 16 | _$OcrPackagingResultFromJson(json); 17 | 18 | @override 19 | Map toJson() => _$OcrPackagingResultToJson(this); 20 | 21 | final int? status; 22 | 23 | @JsonKey(name: 'packaging_text_from_image_orig') 24 | final String? textFromImageOrig; 25 | 26 | @JsonKey(name: 'packaging_text_from_image') 27 | final String? textFromImage; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/prices/get_locations_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'get_locations_order.dart'; 2 | import 'get_price_count_parameters_helper.dart'; 3 | 4 | /// Parameters for the "get locations" API query. 5 | /// 6 | /// cf. https://prices.openfoodfacts.org/api/docs 7 | class GetLocationsParameters 8 | extends GetPriceCountParametersHelper { 9 | String? osmNameLike; 10 | String? osmCityLike; 11 | String? osmPostcodeLike; 12 | String? osmCountryLike; 13 | 14 | @override 15 | Map getQueryParameters() { 16 | super.getQueryParameters(); 17 | addNonNullString(osmNameLike, 'osm_name__like'); 18 | addNonNullString(osmCityLike, 'osm_address_city__like'); 19 | addNonNullString(osmPostcodeLike, 'osm_address_postcode__like'); 20 | addNonNullString(osmCountryLike, 'osm_address_country__like'); 21 | return result; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/prices/get_challenges_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'challenge.dart'; 4 | import '../interface/json_object.dart'; 5 | 6 | part 'get_challenges_result.g.dart'; 7 | 8 | /// List of challenge objects returned by the "get challenges" method. 9 | @JsonSerializable() 10 | class GetChallengesResult extends JsonObject { 11 | @JsonKey() 12 | List? items; 13 | 14 | @JsonKey() 15 | int? total; 16 | 17 | @JsonKey(name: 'page') 18 | int? pageNumber; 19 | 20 | @JsonKey(name: 'size') 21 | int? pageSize; 22 | 23 | @JsonKey(name: 'pages') 24 | int? numberOfPages; 25 | 26 | GetChallengesResult(); 27 | 28 | factory GetChallengesResult.fromJson(Map json) => 29 | _$GetChallengesResultFromJson(json); 30 | 31 | @override 32 | Map toJson() => _$GetChallengesResultToJson(this); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/prices/proof_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../model/off_tagged.dart'; 3 | 4 | /// Type of a Proof. 5 | /// 6 | /// cf. ProofTypeEnum in https://prices.openfoodfacts.org/api/docs 7 | enum ProofType implements OffTagged { 8 | @JsonValue('PRICE_TAG') 9 | priceTag(offTag: 'PRICE_TAG'), 10 | 11 | @JsonValue('RECEIPT') 12 | receipt(offTag: 'RECEIPT'), 13 | 14 | @JsonValue('GDPR_REQUEST') 15 | gdprRequest(offTag: 'GDPR_REQUEST'), 16 | 17 | @JsonValue('SHOP_IMPORT') 18 | shopImport(offTag: 'SHOP_IMPORT'); 19 | 20 | const ProofType({ 21 | required this.offTag, 22 | }); 23 | 24 | @override 25 | final String offTag; 26 | 27 | /// Returns the first [ProofType] that matches the [offTag]. 28 | static ProofType? fromOffTag(final String? offTag) => 29 | OffTagged.fromOffTag(offTag, ProofType.values) as ProofType?; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/model/status.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'status.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Status _$StatusFromJson(Map json) => Status( 10 | status: json['status'], 11 | statusVerbose: json['status_verbose'] as String?, 12 | body: json['body'] as String?, 13 | error: json['error'] as String?, 14 | imageId: JsonObject.parseInt(json['imgid']), 15 | ); 16 | 17 | Map _$StatusToJson(Status instance) => { 18 | 'status': instance.status, 19 | 'status_verbose': instance.statusVerbose, 20 | 'body': instance.body, 21 | 'error': instance.error, 22 | 'imgid': instance.imageId, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/src/search/autocomplete_single_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'autocomplete_single_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | AutocompleteSingleResult _$AutocompleteSingleResultFromJson( 10 | Map json) => 11 | AutocompleteSingleResult( 12 | id: json['id'] as String, 13 | text: json['text'] as String, 14 | taxonomyNameAsString: json['taxonomy_name'] as String, 15 | ); 16 | 17 | Map _$AutocompleteSingleResultToJson( 18 | AutocompleteSingleResult instance) => 19 | { 20 | 'id': instance.id, 21 | 'text': instance.text, 22 | 'taxonomy_name': instance.taxonomyNameAsString, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/src/prices/get_locations_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:openfoodfacts/openfoodfacts.dart'; 3 | 4 | import '../interface/json_object.dart'; 5 | 6 | part 'get_locations_result.g.dart'; 7 | 8 | /// List of location objects returned by the "get locations" method. 9 | @JsonSerializable() 10 | class GetLocationsResult extends JsonObject { 11 | @JsonKey() 12 | List? items; 13 | 14 | @JsonKey() 15 | int? total; 16 | 17 | @JsonKey(name: 'page') 18 | int? pageNumber; 19 | 20 | @JsonKey(name: 'size') 21 | int? pageSize; 22 | 23 | @JsonKey(name: 'pages') 24 | int? numberOfPages; 25 | 26 | GetLocationsResult(); 27 | 28 | factory GetLocationsResult.fromJson(Map json) => 29 | _$GetLocationsResultFromJson(json); 30 | 31 | @override 32 | Map toJson() => _$GetLocationsResultToJson(this); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/model/badge_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'badge_base.g.dart'; 5 | 6 | /// Events API: badge. 7 | @JsonSerializable() 8 | class BadgeBase extends JsonObject { 9 | @JsonKey(name: 'user_id') 10 | final String? userId; 11 | 12 | @JsonKey(name: 'badge_name') 13 | final String badgeName; 14 | 15 | @JsonKey() 16 | final int level; 17 | 18 | BadgeBase({ 19 | required this.badgeName, 20 | required this.level, 21 | this.userId, 22 | }); 23 | 24 | factory BadgeBase.fromJson(Map json) => 25 | _$BadgeBaseFromJson(json); 26 | 27 | @override 28 | Map toJson() => _$BadgeBaseToJson(this); 29 | 30 | @override 31 | String toString() => 'BadgeBase(badgeName: $badgeName' 32 | ', level: $level' 33 | '${userId == null ? '' : ', userId: $userId'}' 34 | ')'; 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/model/ocr_ingredients_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'ocr_ingredients_result.g.dart'; 5 | 6 | /// Result from OCR applied to ingredients. 7 | @JsonSerializable() 8 | class OcrIngredientsResult extends JsonObject { 9 | const OcrIngredientsResult({ 10 | this.status, 11 | this.ingredientsTextFromImageOrig, 12 | this.ingredientsTextFromImage, 13 | }); 14 | 15 | factory OcrIngredientsResult.fromJson(Map json) => 16 | _$OcrIngredientsResultFromJson(json); 17 | 18 | @override 19 | Map toJson() => _$OcrIngredientsResultToJson(this); 20 | 21 | final int? status; 22 | 23 | @JsonKey(name: 'ingredients_text_from_image_orig') 24 | final String? ingredientsTextFromImageOrig; 25 | 26 | @JsonKey(name: 'ingredients_text_from_image') 27 | final String? ingredientsTextFromImage; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/prices/location_osm_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import '../model/off_tagged.dart'; 4 | 5 | /// Type of the OpenStreetMap location object. 6 | /// 7 | /// Stores can be represented as nodes, ways or relations in OpenStreetMap. It is necessary to be able to fetch the correct information about the store using the ID. 8 | enum LocationOSMType implements OffTagged { 9 | @JsonValue('NODE') 10 | node(offTag: 'NODE'), 11 | @JsonValue('WAY') 12 | way(offTag: 'WAY'), 13 | @JsonValue('RELATION') 14 | relation(offTag: 'RELATION'); 15 | 16 | const LocationOSMType({ 17 | required this.offTag, 18 | }); 19 | 20 | @override 21 | final String offTag; 22 | 23 | /// Returns the first [LocationOSMType] that matches the [offTag]. 24 | static LocationOSMType? fromOffTag(final String? offTag) => 25 | OffTagged.fromOffTag(offTag, LocationOSMType.values) as LocationOSMType?; 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/model/product_stats.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product_stats.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ProductStats _$ProductStatsFromJson(Map json) => ProductStats( 10 | barcode: json['product'] as String, 11 | numberOfKeys: (json['keys'] as num).toInt(), 12 | numberOfEditors: (json['editors'] as num).toInt(), 13 | lastEdit: JsonHelper.stringTimestampToDate(json['last_edit']), 14 | ); 15 | 16 | Map _$ProductStatsToJson(ProductStats instance) => 17 | { 18 | 'product': instance.barcode, 19 | 'keys': instance.numberOfKeys, 20 | 'editors': instance.numberOfEditors, 21 | 'last_edit': instance.lastEdit.toIso8601String(), 22 | }; 23 | -------------------------------------------------------------------------------- /lib/src/model/ocr_packaging_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ocr_packaging_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | OcrPackagingResult _$OcrPackagingResultFromJson(Map json) => 10 | OcrPackagingResult( 11 | status: (json['status'] as num?)?.toInt(), 12 | textFromImageOrig: json['packaging_text_from_image_orig'] as String?, 13 | textFromImage: json['packaging_text_from_image'] as String?, 14 | ); 15 | 16 | Map _$OcrPackagingResultToJson(OcrPackagingResult instance) => 17 | { 18 | 'status': instance.status, 19 | 'packaging_text_from_image_orig': instance.textFromImageOrig, 20 | 'packaging_text_from_image': instance.textFromImage, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/src/prices/get_price_products_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:openfoodfacts/openfoodfacts.dart'; 3 | 4 | import '../interface/json_object.dart'; 5 | 6 | part 'get_price_products_result.g.dart'; 7 | 8 | /// List of price product objects returned by the "get price products" method. 9 | @JsonSerializable() 10 | class GetPriceProductsResult extends JsonObject { 11 | @JsonKey() 12 | List? items; 13 | 14 | @JsonKey() 15 | int? total; 16 | 17 | @JsonKey(name: 'page') 18 | int? pageNumber; 19 | 20 | @JsonKey(name: 'size') 21 | int? pageSize; 22 | 23 | @JsonKey(name: 'pages') 24 | int? numberOfPages; 25 | 26 | GetPriceProductsResult(); 27 | 28 | factory GetPriceProductsResult.fromJson(Map json) => 29 | _$GetPriceProductsResultFromJson(json); 30 | 31 | @override 32 | Map toJson() => _$GetPriceProductsResultToJson(this); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/model/parameter/sort_by.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | 3 | /// Sort search API parameter 4 | class SortBy extends Parameter { 5 | static const Map _VALUES = { 6 | SortOption.PRODUCT_NAME: 'product_name', 7 | SortOption.CREATED: 'created_t', 8 | SortOption.EDIT: 'last_modified_t', 9 | SortOption.POPULARITY: 'unique_scans_n', 10 | SortOption.NOTHING: 'nothing', 11 | SortOption.ECOSCORE: 'ecoscore_score', 12 | SortOption.NUTRISCORE: 'nutriscore_score', 13 | }; 14 | 15 | @override 16 | String getName() => 'sort_by'; 17 | 18 | @override 19 | String getValue() => _VALUES[option] ?? 'unique_scans_n'; 20 | 21 | final SortOption? option; 22 | 23 | const SortBy({required this.option}); 24 | } 25 | 26 | /// Possible sort options for search API 27 | enum SortOption { 28 | POPULARITY, 29 | PRODUCT_NAME, 30 | CREATED, 31 | EDIT, 32 | NOTHING, 33 | ECOSCORE, 34 | NUTRISCORE, 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/model/parameter/bool_map_parameter.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import '../../interface/parameter.dart'; 3 | 4 | /// Abstract map of [bool] as [Parameter]. 5 | /// 6 | /// Typical use-case with objects that have an on/off quality, 7 | /// like "with/without gluten". 8 | /// A query like "I want the products with eggs but without gluten" would be 9 | /// something like "{'eggs': true, 'gluten': false}". 10 | abstract class BoolMapParameter extends Parameter { 11 | const BoolMapParameter({required this.map}); 12 | 13 | final Map map; 14 | 15 | @override 16 | String getValue() { 17 | final List result = []; 18 | for (final MapEntry item in map.entries) { 19 | result.add(getTag(item.key, item.value)); 20 | } 21 | return result.join(','); 22 | } 23 | 24 | /// Returns the tag as on or off, like "gluten:with" or "gluten:without" 25 | @protected 26 | String getTag(final T key, final bool value); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/search/autocomplete_search_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'autocomplete_single_result.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'autocomplete_search_result.g.dart'; 6 | 7 | /// Result of an autocomplete search on the Elastic Search API. 8 | @JsonSerializable() 9 | class AutocompleteSearchResult extends JsonObject { 10 | @JsonKey(fromJson: JsonObject.parseInt) 11 | final int? took; 12 | 13 | @JsonKey(name: 'timed_out', fromJson: JsonObject.parseBool) 14 | final bool? timedOut; 15 | 16 | @JsonKey() 17 | final List? options; 18 | 19 | const AutocompleteSearchResult({ 20 | this.took, 21 | this.timedOut, 22 | this.options, 23 | }); 24 | 25 | factory AutocompleteSearchResult.fromJson(Map json) => 26 | _$AutocompleteSearchResultFromJson(json); 27 | 28 | @override 29 | Map toJson() => _$AutocompleteSearchResultToJson(this); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/model/events_base.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'events_base.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | EventsBase _$EventsBaseFromJson(Map json) => EventsBase( 10 | eventType: json['event_type'] as String, 11 | timestamp: JsonHelper.nullableStringTimestampToDate(json['timestamp']), 12 | userId: json['user_id'] as String?, 13 | barcode: json['barcode'] as String?, 14 | points: (json['points'] as num?)?.toInt(), 15 | ); 16 | 17 | Map _$EventsBaseToJson(EventsBase instance) => 18 | { 19 | 'event_type': instance.eventType, 20 | 'timestamp': instance.timestamp?.toIso8601String(), 21 | 'user_id': instance.userId, 22 | 'barcode': instance.barcode, 23 | 'points': instance.points, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/src/prices/flavor.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../model/off_tagged.dart'; 3 | import '../utils/server_type.dart'; 4 | 5 | /// Flavor is used to refer to a specific Open*Facts project. 6 | /// 7 | /// cf. `Flavor` in https://prices.openfoodfacts.org/api/docs 8 | /// Somehow redundant with [ServerType]. 9 | enum Flavor implements OffTagged { 10 | /// Open Food Facts 11 | @JsonValue('off') 12 | openFoodFacts(offTag: 'off'), 13 | 14 | /// Open Beauty Facts 15 | @JsonValue('obf') 16 | openBeautyFacts(offTag: 'obf'), 17 | 18 | /// Open Pet Food Facts 19 | @JsonValue('opff') 20 | openPetFoodFacts(offTag: 'opff'), 21 | 22 | /// Open Product Facts 23 | @JsonValue('opf') 24 | openProductFacts(offTag: 'opf'), 25 | 26 | /// Open Product Facts (Pro platform) 27 | @JsonValue('off-pro') 28 | openFoodProductFactsPro(offTag: 'off-pro'); 29 | 30 | const Flavor({ 31 | required this.offTag, 32 | }); 33 | 34 | @override 35 | final String offTag; 36 | } 37 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: openfoodfacts 2 | description: Dart package for the Open Food Facts API, a food products database made by everyone, for everyone. 3 | # The version is automatically, temporarily increased by the release workflow, changing it manually has no effect. 4 | version: 3.28.0 5 | homepage: https://github.com/openfoodfacts/openfoodfacts-dart 6 | 7 | environment: 8 | sdk: '>=3.5.0 <4.0.0' 9 | 10 | dependencies: 11 | json_annotation: ^4.9.0 12 | http: '>=0.13.5 <2.0.0' 13 | http_parser: ^4.0.2 14 | path: ^1.9.0 15 | meta: ^1.8.0 16 | 17 | dev_dependencies: 18 | analyzer: 7.7.0 19 | build_runner: 2.5.4 20 | json_serializable: 6.9.5 21 | lints: ">=3.0.0 <7.0.0" 22 | test: 1.26.3 23 | coverage: 1.15.0 24 | 25 | funding: 26 | - "https://donate.openfoodfacts.org/" 27 | - "https://opencollective.com/openfoodfacts-server" 28 | 29 | # 5 topics max 30 | # cf. https://dart.dev/tools/pub/pubspec#topics 31 | topics: 32 | - nutriscore 33 | - ecoscore 34 | - nutrition 35 | - food 36 | - ingredients 37 | -------------------------------------------------------------------------------- /lib/src/model/user_agent.dart: -------------------------------------------------------------------------------- 1 | import '../interface/json_object.dart'; 2 | 3 | class UserAgent extends JsonObject { 4 | /// The name of your application (eg: smooth-app) 5 | final String name; 6 | 7 | /// The version of the application (1.0.0) 8 | final String? version; 9 | 10 | /// The system running the application (eg: Android+10) 11 | final String? system; 12 | 13 | /// The url of your application (eg: https://example.com) 14 | final String? url; 15 | 16 | /// Additional information about your application 17 | final String? comment; 18 | 19 | UserAgent({ 20 | required this.name, 21 | this.version, 22 | this.system, 23 | this.url, 24 | this.comment, 25 | }) { 26 | if (name.trim().isEmpty) { 27 | throw Exception('A non empty name is required'); 28 | } 29 | } 30 | 31 | @override 32 | Map toJson() => { 33 | 'name': name, 34 | 'version': version, 35 | 'system': system, 36 | 'url': url, 37 | 'comment': comment, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/model/ocr_ingredients_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ocr_ingredients_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | OcrIngredientsResult _$OcrIngredientsResultFromJson( 10 | Map json) => 11 | OcrIngredientsResult( 12 | status: (json['status'] as num?)?.toInt(), 13 | ingredientsTextFromImageOrig: 14 | json['ingredients_text_from_image_orig'] as String?, 15 | ingredientsTextFromImage: json['ingredients_text_from_image'] as String?, 16 | ); 17 | 18 | Map _$OcrIngredientsResultToJson( 19 | OcrIngredientsResult instance) => 20 | { 21 | 'status': instance.status, 22 | 'ingredients_text_from_image_orig': instance.ingredientsTextFromImageOrig, 23 | 'ingredients_text_from_image': instance.ingredientsTextFromImage, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/src/search/autocomplete_search_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'autocomplete_search_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | AutocompleteSearchResult _$AutocompleteSearchResultFromJson( 10 | Map json) => 11 | AutocompleteSearchResult( 12 | took: JsonObject.parseInt(json['took']), 13 | timedOut: JsonObject.parseBool(json['timed_out']), 14 | options: (json['options'] as List?) 15 | ?.map((e) => 16 | AutocompleteSingleResult.fromJson(e as Map)) 17 | .toList(), 18 | ); 19 | 20 | Map _$AutocompleteSearchResultToJson( 21 | AutocompleteSearchResult instance) => 22 | { 23 | 'took': instance.took, 24 | 'timed_out': instance.timedOut, 25 | 'options': instance.options, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/utils/tag_type.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | enum TagType implements OffTagged { 4 | STATES(offTag: 'states'), 5 | LANGUAGES(offTag: 'languages'), 6 | LABELS(offTag: 'labels'), 7 | CATEGORIES(offTag: 'categories'), 8 | COUNTRIES(offTag: 'countries'), 9 | INGREDIENTS(offTag: 'ingredients'), 10 | TRACES(offTag: 'traces'), 11 | ADDITIVES(offTag: 'additives'), 12 | ALLERGENS(offTag: 'allergens'), 13 | PACKAGING(offTag: 'packaging'), 14 | ORIGINS(offTag: 'origins'), 15 | PACKAGING_SHAPES(offTag: 'packaging_shapes'), 16 | PACKAGING_MATERIALS(offTag: 'packaging_materials'), 17 | PACKAGING_RECYCLING(offTag: 'packaging_recycling'), 18 | NOVA(offTag: 'nova_groups'), 19 | EMB_CODES(offTag: 'emb_codes'); 20 | 21 | const TagType({ 22 | required this.offTag, 23 | }); 24 | 25 | @override 26 | final String offTag; 27 | 28 | /// Returns the first [TagType] that matches the [offTag]. 29 | static TagType? fromOffTag(final String? offTag) => 30 | OffTagged.fromOffTag(offTag, TagType.values) as TagType?; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/model/additives.dart: -------------------------------------------------------------------------------- 1 | class Additives { 2 | List ids; // additive id formatted as 'en:e100i' 3 | List names; // additive name formatted as 'E100i' 4 | 5 | Additives(this.ids, this.names); 6 | 7 | static Additives additivesFromJson(List? json) { 8 | List ids = []; 9 | List names = []; 10 | 11 | if (json == null) { 12 | return Additives(ids, names); 13 | } 14 | 15 | for (int i = 0; i < json.length; i++) { 16 | ids.add(json[i].toString()); 17 | String name = 18 | 'E${json[i].toString().substring(4)}'; // remove the 'en:' header and Capitalize the 'E'. 19 | names.add(name); 20 | } 21 | 22 | return Additives(ids, names); 23 | } 24 | 25 | static List? additivesToJson(Additives? additives) { 26 | List result = []; 27 | 28 | if (additives == null) { 29 | return null; 30 | } 31 | 32 | for (int i = 0; i < additives.ids.length; i++) { 33 | result.add(additives.ids[i].toString()); 34 | } 35 | 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/model/product_result_field_answer.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | import 'localized_tag.dart'; 4 | import 'product_result_field.dart'; 5 | 6 | part 'product_result_field_answer.g.dart'; 7 | 8 | /// API V3 answer for one field. 9 | @JsonSerializable() 10 | class ProductResultFieldAnswer extends JsonObject { 11 | ProductResultFieldAnswer(); 12 | 13 | /// Field on which there is a specific answer. 14 | @JsonKey(includeIfNull: false) 15 | ProductResultField? field; 16 | 17 | /// Impact, e.g. "Field ignored". 18 | @JsonKey(includeIfNull: false) 19 | LocalizedTag? impact; 20 | 21 | /// Message, e.g. "Missing field". 22 | @JsonKey(includeIfNull: false) 23 | LocalizedTag? message; 24 | 25 | factory ProductResultFieldAnswer.fromJson(Map json) => 26 | _$ProductResultFieldAnswerFromJson(json); 27 | 28 | @override 29 | Map toJson() => _$ProductResultFieldAnswerToJson(this); 30 | 31 | @override 32 | String toString() => toJson().toString(); 33 | } 34 | -------------------------------------------------------------------------------- /test/api_post_robotoff_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/openfoodfacts.dart'; 2 | import 'package:test/test.dart'; 3 | import 'test_constants.dart'; 4 | 5 | void main() { 6 | OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; 7 | const UriHelper uriHelper = uriHelperRobotoffTest; 8 | 9 | group('$OpenFoodAPIClient answer robotoff question', () { 10 | test('get questions for Noix de Saint-Jacques EN and answer', () async { 11 | RobotoffQuestionResult result = 12 | await RobotoffAPIClient.getProductQuestions( 13 | '0080868000633', 14 | OpenFoodFactsLanguage.ENGLISH, 15 | user: TestConstants.TEST_USER, 16 | count: 1, 17 | uriHelper: uriHelper, 18 | ); 19 | 20 | if (result.status == 'found') { 21 | Status postResult = await RobotoffAPIClient.postInsightAnnotation( 22 | result.questions![0].insightId, 23 | InsightAnnotation.YES, 24 | uriHelper: uriHelper, 25 | ); 26 | expect(postResult.status, 'saved'); 27 | } 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/barcode_validator_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/openfoodfacts.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'test_constants.dart'; 5 | 6 | void main() { 7 | OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; 8 | 9 | group('EAN8', () { 10 | test('Valid EAN8 barcodes', () { 11 | expect(BarcodeValidator.isValid("12345670"), true); 12 | expect(BarcodeValidator.isValid("47195127"), true); 13 | }); 14 | 15 | test('Invalid EAN8 barcodes', () { 16 | expect(BarcodeValidator.isValid("12345676"), false); 17 | expect(BarcodeValidator.isValid("56987943"), false); 18 | }); 19 | }); 20 | 21 | group('EAN13', () { 22 | test('Valid EAN13 barcodes', () { 23 | expect(BarcodeValidator.isValid("4719512002889"), true); 24 | expect(BarcodeValidator.isValid("1234567890128"), true); 25 | }); 26 | 27 | test('Invalid EAN13 barcodes', () { 28 | expect(BarcodeValidator.isValid("4719512002884"), false); 29 | expect(BarcodeValidator.isValid("1234567890127"), false); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/model/taxonomy_allergen.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'taxonomy_allergen.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TaxonomyAllergen _$TaxonomyAllergenFromJson(Map json) => 10 | TaxonomyAllergen( 11 | LanguageHelper.fromJsonStringMap(json['name']), 12 | LanguageHelper.fromJsonStringsListMap(json['synonyms']), 13 | LanguageHelper.fromJsonStringMap(json['wikidata']), 14 | ); 15 | 16 | Map _$TaxonomyAllergenToJson(TaxonomyAllergen instance) => 17 | { 18 | if (LanguageHelper.toJsonStringMap(instance.name) case final value?) 19 | 'name': value, 20 | if (LanguageHelper.toJsonStringsListMap(instance.synonyms) 21 | case final value?) 22 | 'synonyms': value, 23 | if (LanguageHelper.toJsonStringMap(instance.wikidata) case final value?) 24 | 'wikidata': value, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/src/model/product_result_field.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product_result_field.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ProductResultField _$ProductResultFieldFromJson(Map json) => 10 | ProductResultField() 11 | ..id = json['id'] as String? 12 | ..value = JsonHelper.stringFromJSON(json['value']) 13 | ..defaultValue = JsonHelper.stringFromJSON(json['default_value']) 14 | ..valuedConverted = JsonHelper.stringFromJSON(json['valued_converted']); 15 | 16 | Map _$ProductResultFieldToJson(ProductResultField instance) => 17 | { 18 | if (instance.id case final value?) 'id': value, 19 | if (instance.value case final value?) 'value': value, 20 | if (instance.defaultValue case final value?) 'default_value': value, 21 | if (instance.valuedConverted case final value?) 'valued_converted': value, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/src/prices/get_users_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'get_users_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GetUsersResult _$GetUsersResultFromJson(Map json) => 10 | GetUsersResult() 11 | ..items = (json['items'] as List?) 12 | ?.map((e) => PriceUser.fromJson(e as Map)) 13 | .toList() 14 | ..total = (json['total'] as num?)?.toInt() 15 | ..pageNumber = (json['page'] as num?)?.toInt() 16 | ..pageSize = (json['size'] as num?)?.toInt() 17 | ..numberOfPages = (json['pages'] as num?)?.toInt(); 18 | 19 | Map _$GetUsersResultToJson(GetUsersResult instance) => 20 | { 21 | 'items': instance.items, 22 | 'total': instance.total, 23 | 'page': instance.pageNumber, 24 | 'size': instance.pageSize, 25 | 'pages': instance.numberOfPages, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/model/parameter/ingredients_analysis_parameter.dart: -------------------------------------------------------------------------------- 1 | import '../../interface/parameter.dart'; 2 | import '../ingredients_analysis_tags.dart'; 3 | 4 | /// Ingredients Analysis search API parameter. 5 | class IngredientsAnalysisParameter extends Parameter { 6 | @override 7 | String getName() => 'ingredients_analysis_tags'; 8 | 9 | @override 10 | String getValue() { 11 | final List result = []; 12 | if (veganStatus != null) { 13 | result.add(veganStatus!.offTag); 14 | } 15 | if (vegetarianStatus != null) { 16 | result.add(vegetarianStatus!.offTag); 17 | } 18 | if (palmOilFreeStatus != null) { 19 | result.add(palmOilFreeStatus!.offTag); 20 | } 21 | if (result.isEmpty) { 22 | return ''; 23 | } 24 | return result.join(','); 25 | } 26 | 27 | final VeganStatus? veganStatus; 28 | final VegetarianStatus? vegetarianStatus; 29 | final PalmOilFreeStatus? palmOilFreeStatus; 30 | 31 | const IngredientsAnalysisParameter({ 32 | this.veganStatus, 33 | this.vegetarianStatus, 34 | this.palmOilFreeStatus, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/prices/get_prices_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'get_prices_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GetPricesResult _$GetPricesResultFromJson(Map json) => 10 | GetPricesResult() 11 | ..items = (json['items'] as List?) 12 | ?.map((e) => Price.fromJson(e as Map)) 13 | .toList() 14 | ..total = (json['total'] as num?)?.toInt() 15 | ..pageNumber = (json['page'] as num?)?.toInt() 16 | ..pageSize = (json['size'] as num?)?.toInt() 17 | ..numberOfPages = (json['pages'] as num?)?.toInt(); 18 | 19 | Map _$GetPricesResultToJson(GetPricesResult instance) => 20 | { 21 | 'items': instance.items, 22 | 'total': instance.total, 23 | 'page': instance.pageNumber, 24 | 'size': instance.pageSize, 25 | 'pages': instance.numberOfPages, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/prices/get_proofs_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'get_proofs_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GetProofsResult _$GetProofsResultFromJson(Map json) => 10 | GetProofsResult() 11 | ..items = (json['items'] as List?) 12 | ?.map((e) => Proof.fromJson(e as Map)) 13 | .toList() 14 | ..total = (json['total'] as num?)?.toInt() 15 | ..pageNumber = (json['page'] as num?)?.toInt() 16 | ..pageSize = (json['size'] as num?)?.toInt() 17 | ..numberOfPages = (json['pages'] as num?)?.toInt(); 18 | 19 | Map _$GetProofsResultToJson(GetProofsResult instance) => 20 | { 21 | 'items': instance.items, 22 | 'total': instance.total, 23 | 'page': instance.pageNumber, 24 | 'size': instance.pageSize, 25 | 'pages': instance.numberOfPages, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/model/product_stats.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../utils/json_helper.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'product_stats.g.dart'; 6 | 7 | /// Folksonomy: statistics about the tag keys on a product. 8 | @JsonSerializable() 9 | class ProductStats extends JsonObject { 10 | @JsonKey(name: 'product') 11 | final String barcode; 12 | @JsonKey(name: 'keys') 13 | final int numberOfKeys; 14 | @JsonKey(name: 'editors') 15 | final int numberOfEditors; 16 | @JsonKey( 17 | name: 'last_edit', 18 | fromJson: JsonHelper.stringTimestampToDate, 19 | ) 20 | final DateTime lastEdit; 21 | 22 | ProductStats({ 23 | required this.barcode, 24 | required this.numberOfKeys, 25 | required this.numberOfEditors, 26 | required this.lastEdit, 27 | }); 28 | 29 | factory ProductStats.fromJson(Map json) => 30 | _$ProductStatsFromJson(json); 31 | 32 | @override 33 | Map toJson() => _$ProductStatsToJson(this); 34 | 35 | @override 36 | String toString() => toJson().toString(); 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/test-sdk.yml: -------------------------------------------------------------------------------- 1 | name: Run sdk tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: 0 0,12 * * * 8 | 9 | concurrency: 10 | group: tests 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v5 18 | 19 | - name: Setup Dart 20 | uses: dart-lang/setup-dart@v1 21 | 22 | - name: Install dependencies 23 | run: dart pub get 24 | 25 | - name: Check for formatting issues (run "flutter format . ") 26 | run: dart format --set-exit-if-changed . 27 | 28 | - name: Analyze project 29 | run: dart analyze 30 | 31 | - name: Run tests 32 | run: dart test test/ --coverage=./.cov 33 | 34 | - name: Collect coverage 35 | run: dart pub run coverage:format_coverage --lcov --in ./.cov --out coverage.lcov 36 | 37 | - name: Upload code coverage to codecov 38 | uses: codecov/codecov-action@v5 39 | with: 40 | files: ./coverage.lcov 41 | fail_ci_if_error: false 42 | verbose: true 43 | -------------------------------------------------------------------------------- /lib/src/prices/get_locations_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'get_locations_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GetLocationsResult _$GetLocationsResultFromJson(Map json) => 10 | GetLocationsResult() 11 | ..items = (json['items'] as List?) 12 | ?.map((e) => Location.fromJson(e as Map)) 13 | .toList() 14 | ..total = (json['total'] as num?)?.toInt() 15 | ..pageNumber = (json['page'] as num?)?.toInt() 16 | ..pageSize = (json['size'] as num?)?.toInt() 17 | ..numberOfPages = (json['pages'] as num?)?.toInt(); 18 | 19 | Map _$GetLocationsResultToJson(GetLocationsResult instance) => 20 | { 21 | 'items': instance.items, 22 | 'total': instance.total, 23 | 'page': instance.pageNumber, 24 | 'size': instance.pageSize, 25 | 'pages': instance.numberOfPages, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/model/product_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'off_tagged.dart'; 3 | import '../prices/flavor.dart'; 4 | import '../utils/server_type.dart'; 5 | 6 | /// Type used at the [Product] level (e.g. "this is a pet food product"). 7 | /// 8 | /// Somehow redundant with [ServerType] and [Flavor]. 9 | enum ProductType implements OffTagged { 10 | @JsonValue('food') 11 | food(offTag: 'food', flavor: Flavor.openFoodFacts), 12 | @JsonValue('beauty') 13 | beauty(offTag: 'beauty', flavor: Flavor.openBeautyFacts), 14 | @JsonValue('petfood') 15 | petFood(offTag: 'petfood', flavor: Flavor.openPetFoodFacts), 16 | @JsonValue('product') 17 | product(offTag: 'product', flavor: Flavor.openProductFacts); 18 | 19 | const ProductType({ 20 | required this.offTag, 21 | required this.flavor, 22 | }); 23 | 24 | @override 25 | final String offTag; 26 | 27 | final Flavor flavor; 28 | 29 | /// Returns the first [ProductType] that matches the [offTag]. 30 | static ProductType? fromOffTag(final String? offTag) => 31 | OffTagged.fromOffTag(offTag, ProductType.values) as ProductType?; 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/model/ecoscore_adjustments.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ecoscore_adjustments.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | EcoscoreAdjustments _$EcoscoreAdjustmentsFromJson(Map json) => 10 | EcoscoreAdjustments( 11 | packaging: json['packaging'] == null 12 | ? null 13 | : Packaging.fromJson(json['packaging'] as Map), 14 | originsOfIngredients: json['origins_of_ingredients'] == null 15 | ? null 16 | : OriginsOfIngredients.fromJson( 17 | json['origins_of_ingredients'] as Map), 18 | ); 19 | 20 | Map _$EcoscoreAdjustmentsToJson( 21 | EcoscoreAdjustments instance) => 22 | { 23 | if (instance.packaging?.toJson() case final value?) 'packaging': value, 24 | if (instance.originsOfIngredients?.toJson() case final value?) 25 | 'origins_of_ingredients': value, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/prices/get_challenges_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'get_challenges_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GetChallengesResult _$GetChallengesResultFromJson(Map json) => 10 | GetChallengesResult() 11 | ..items = (json['items'] as List?) 12 | ?.map((e) => Challenge.fromJson(e as Map)) 13 | .toList() 14 | ..total = (json['total'] as num?)?.toInt() 15 | ..pageNumber = (json['page'] as num?)?.toInt() 16 | ..pageSize = (json['size'] as num?)?.toInt() 17 | ..numberOfPages = (json['pages'] as num?)?.toInt(); 18 | 19 | Map _$GetChallengesResultToJson( 20 | GetChallengesResult instance) => 21 | { 22 | 'items': instance.items, 23 | 'total': instance.total, 24 | 'page': instance.pageNumber, 25 | 'size': instance.pageSize, 26 | 'pages': instance.numberOfPages, 27 | }; 28 | -------------------------------------------------------------------------------- /test/api_get_taxonomy_nova_server_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/openfoodfacts.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'test_constants.dart'; 5 | 6 | /// Integration test about nova. 7 | void main() { 8 | OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; 9 | OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; 10 | OpenFoodAPIConfiguration.globalCountry = OpenFoodFactsCountry.FRANCE; 11 | OpenFoodAPIConfiguration.globalLanguages = [ 12 | OpenFoodFactsLanguage.ENGLISH, 13 | OpenFoodFactsLanguage.FRENCH, 14 | ]; 15 | 16 | final Iterable knownRootTags = TaxonomyNova.offTags.values; 17 | 18 | group('OpenFoodAPIClient getTaxonomyNova SERVER', () { 19 | test('get all nova', () async { 20 | final Map? values = 21 | await OpenFoodAPIClient.getTaxonomyNova( 22 | TaxonomyNovaQueryConfiguration.roots(), 23 | ); 24 | expect(values, isNotNull); 25 | expect(values!.keys, containsAll(knownRootTags)); 26 | for (final TaxonomyNova value in values.values) { 27 | expect(value.name, isNotNull); 28 | } 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/prices/discount_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../model/off_tagged.dart'; 3 | 4 | /// Discount Type, for Prices. 5 | /// 6 | /// cf. DiscountTypeEnum in https://prices.openfoodfacts.org/api/docs 7 | enum DiscountType implements OffTagged { 8 | @JsonValue('QUANTITY') 9 | quantity(offTag: 'QUANTITY'), 10 | @JsonValue('SALE') 11 | sale(offTag: 'SALE'), 12 | @JsonValue('SEASONAL') 13 | seasonal(offTag: 'SEASONAL'), 14 | @JsonValue('LOYALTY_PROGRAM') 15 | loyaltyProgram(offTag: 'LOYALTY_PROGRAM'), 16 | @JsonValue('EXPIRES_SOON') 17 | expiresSoon(offTag: 'EXPIRES_SOON'), 18 | @JsonValue('PICK_IT_YOURSELF') 19 | pickItYourself(offTag: 'PICK_IT_YOURSELF'), 20 | @JsonValue('SECOND_HAND') 21 | secondHand(offTag: 'SECOND_HAND'), 22 | @JsonValue('OTHER') 23 | other(offTag: 'OTHER'); 24 | 25 | const DiscountType({ 26 | required this.offTag, 27 | }); 28 | 29 | @override 30 | final String offTag; 31 | 32 | /// Returns the first [DiscountType] that matches the [offTag]. 33 | static DiscountType? fromOffTag(final String? offTag) => 34 | OffTagged.fromOffTag(offTag, DiscountType.values) as DiscountType?; 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/prices/get_price_products_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'get_price_products_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GetPriceProductsResult _$GetPriceProductsResultFromJson( 10 | Map json) => 11 | GetPriceProductsResult() 12 | ..items = (json['items'] as List?) 13 | ?.map((e) => PriceProduct.fromJson(e as Map)) 14 | .toList() 15 | ..total = (json['total'] as num?)?.toInt() 16 | ..pageNumber = (json['page'] as num?)?.toInt() 17 | ..pageSize = (json['size'] as num?)?.toInt() 18 | ..numberOfPages = (json['pages'] as num?)?.toInt(); 19 | 20 | Map _$GetPriceProductsResultToJson( 21 | GetPriceProductsResult instance) => 22 | { 23 | 'items': instance.items, 24 | 'total': instance.total, 25 | 'page': instance.pageNumber, 26 | 'size': instance.pageSize, 27 | 'pages': instance.numberOfPages, 28 | }; 29 | -------------------------------------------------------------------------------- /lib/src/model/product_tag.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product_tag.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ProductTag _$ProductTagFromJson(Map json) => ProductTag( 10 | barcode: json['product'] as String, 11 | key: json['k'] as String, 12 | value: json['v'] as String, 13 | owner: json['owner'] as String, 14 | version: (json['version'] as num).toInt(), 15 | editor: json['editor'] as String, 16 | lastEdit: JsonHelper.stringTimestampToDate(json['last_edit']), 17 | comment: json['comment'] as String, 18 | ); 19 | 20 | Map _$ProductTagToJson(ProductTag instance) => 21 | { 22 | 'product': instance.barcode, 23 | 'k': instance.key, 24 | 'v': instance.value, 25 | 'owner': instance.owner, 26 | 'version': instance.version, 27 | 'editor': instance.editor, 28 | 'last_edit': instance.lastEdit.toIso8601String(), 29 | 'comment': instance.comment, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/src/model/localized_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'localized_tag.g.dart'; 5 | 6 | /// Tag with localizations (in English and an additional language). 7 | /// 8 | /// We need to populate URL parameter `tag_lc` to get [lcName]. 9 | @JsonSerializable() 10 | class LocalizedTag extends JsonObject { 11 | /// Tag id. 12 | @JsonKey(includeIfNull: false) 13 | String? id; 14 | 15 | /// Name in English. 16 | @JsonKey(includeIfNull: false) 17 | String? name; 18 | 19 | /// Localized name - according to query parameter `tags_lc`. 20 | @JsonKey(name: 'lc_name', includeIfNull: false) 21 | String? lcName; 22 | 23 | LocalizedTag(); 24 | 25 | factory LocalizedTag.fromJson(Map json) => 26 | _$LocalizedTagFromJson(json); 27 | 28 | static Map objToJson(LocalizedTag? tag) => 29 | tag != null ? tag.toJson() : {}; 30 | 31 | @override 32 | Map toJson() => _$LocalizedTagToJson(this); 33 | 34 | Map toServerData() => JsonObject.toDataStatic(toJson()); 35 | 36 | @override 37 | String toString() => toServerData().toString(); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/model/search_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | import 'product.dart'; 4 | 5 | part 'search_result.g.dart'; 6 | 7 | @JsonSerializable() 8 | class SearchResult extends JsonObject { 9 | @JsonKey(name: 'page', fromJson: JsonObject.parseInt) 10 | final int? page; 11 | 12 | @JsonKey(name: 'page_size', fromJson: JsonObject.parseInt) 13 | final int? pageSize; 14 | 15 | @JsonKey(name: 'count', fromJson: JsonObject.parseInt) 16 | final int? count; 17 | 18 | @JsonKey(name: 'page_count', fromJson: JsonObject.parseInt) 19 | final int? pageCount; 20 | 21 | @JsonKey(name: 'skip', fromJson: JsonObject.parseInt) 22 | final int? skip; 23 | 24 | @JsonKey(name: 'products', includeIfNull: false) 25 | final List? products; 26 | 27 | const SearchResult({ 28 | this.page, 29 | this.pageSize, 30 | this.count, 31 | this.pageCount, 32 | this.skip, 33 | this.products, 34 | }); 35 | 36 | factory SearchResult.fromJson(Map json) => 37 | _$SearchResultFromJson(json); 38 | 39 | @override 40 | Map toJson() => _$SearchResultToJson(this); 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/model/search_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'search_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SearchResult _$SearchResultFromJson(Map json) => SearchResult( 10 | page: JsonObject.parseInt(json['page']), 11 | pageSize: JsonObject.parseInt(json['page_size']), 12 | count: JsonObject.parseInt(json['count']), 13 | pageCount: JsonObject.parseInt(json['page_count']), 14 | skip: JsonObject.parseInt(json['skip']), 15 | products: (json['products'] as List?) 16 | ?.map((e) => Product.fromJson(e as Map)) 17 | .toList(), 18 | ); 19 | 20 | Map _$SearchResultToJson(SearchResult instance) => 21 | { 22 | 'page': instance.page, 23 | 'page_size': instance.pageSize, 24 | 'count': instance.count, 25 | 'page_count': instance.pageCount, 26 | 'skip': instance.skip, 27 | if (instance.products case final value?) 'products': value, 28 | }; 29 | -------------------------------------------------------------------------------- /lib/src/model/origins_of_ingredients.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'origins_of_ingredients.g.dart'; 5 | 6 | @JsonSerializable() 7 | class OriginsOfIngredients extends JsonObject { 8 | @JsonKey( 9 | name: 'epi_score', includeIfNull: false, fromJson: JsonObject.parseDouble) 10 | double? epiScore; 11 | @JsonKey( 12 | name: 'epi_value', includeIfNull: false, fromJson: JsonObject.parseDouble) 13 | double? epiValue; 14 | @JsonKey( 15 | name: 'transportation_score', 16 | includeIfNull: false, 17 | fromJson: JsonObject.parseDouble) 18 | double? transportationScore; 19 | @JsonKey( 20 | name: 'transportation_value', 21 | includeIfNull: false, 22 | fromJson: JsonObject.parseDouble) 23 | double? transportationValue; 24 | 25 | OriginsOfIngredients( 26 | {this.epiScore, 27 | this.epiValue, 28 | this.transportationScore, 29 | this.transportationValue}); 30 | 31 | factory OriginsOfIngredients.fromJson(Map json) => 32 | _$OriginsOfIngredientsFromJson(json); 33 | 34 | @override 35 | Map toJson() => _$OriginsOfIngredientsToJson(this); 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/model/send_image.dart: -------------------------------------------------------------------------------- 1 | import 'product_image.dart'; 2 | import '../interface/json_object.dart'; 3 | import '../utils/language_helper.dart'; 4 | 5 | class SendImage extends JsonObject { 6 | OpenFoodFactsLanguage? lang; 7 | 8 | // ignored for json 9 | Uri imageUri; 10 | 11 | String barcode; 12 | 13 | ImageField imageField; 14 | 15 | SendImage({ 16 | this.lang, 17 | required this.barcode, 18 | required this.imageUri, 19 | this.imageField = ImageField.OTHER, 20 | }); 21 | 22 | /// the json key depending on the image field of this object. 23 | String getImageDataKey() { 24 | String imageDataKey = 'imgupload_${imageField.offTag}'; 25 | if (lang != null) { 26 | imageDataKey += '_${lang!.offTag}'; 27 | } 28 | return imageDataKey; 29 | } 30 | 31 | String _getImageFieldWithLang() { 32 | String imageFieldWithLang = imageField.offTag; 33 | if (lang != null) { 34 | imageFieldWithLang += '_${lang!.offTag}'; 35 | } 36 | return imageFieldWithLang; 37 | } 38 | 39 | @override 40 | Map toJson() { 41 | return { 42 | 'lc': lang.code, 43 | 'code': barcode, 44 | 'imagefield': _getImageFieldWithLang(), 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/prices/maybe_error.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:http/http.dart'; 4 | 5 | /// Contains a successful value OR an error. 6 | class MaybeError { 7 | const MaybeError.value( 8 | T this._value, { 9 | this.statusCode, 10 | }) : error = null, 11 | isError = false; 12 | MaybeError.responseError(final Response response) 13 | : _value = null, 14 | error = utf8.decode(response.bodyBytes), 15 | statusCode = response.statusCode, 16 | isError = true; 17 | MaybeError.unreadableResponse(final Response response) 18 | : _value = null, 19 | error = utf8.decode(response.bodyBytes), 20 | statusCode = response.statusCode, 21 | isError = false; 22 | MaybeError.error({ 23 | required String this.error, 24 | required int this.statusCode, 25 | }) : _value = null, 26 | isError = true; 27 | 28 | final T? _value; 29 | final bool isError; 30 | final String? error; 31 | final int? statusCode; 32 | 33 | T get value => _value!; 34 | 35 | String get detailError { 36 | try { 37 | final Map json = jsonDecode(error!); 38 | return json['detail']; 39 | } catch (e) { 40 | return error!; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/model/origins_of_ingredients.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'origins_of_ingredients.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | OriginsOfIngredients _$OriginsOfIngredientsFromJson( 10 | Map json) => 11 | OriginsOfIngredients( 12 | epiScore: JsonObject.parseDouble(json['epi_score']), 13 | epiValue: JsonObject.parseDouble(json['epi_value']), 14 | transportationScore: JsonObject.parseDouble(json['transportation_score']), 15 | transportationValue: JsonObject.parseDouble(json['transportation_value']), 16 | ); 17 | 18 | Map _$OriginsOfIngredientsToJson( 19 | OriginsOfIngredients instance) => 20 | { 21 | if (instance.epiScore case final value?) 'epi_score': value, 22 | if (instance.epiValue case final value?) 'epi_value': value, 23 | if (instance.transportationScore case final value?) 24 | 'transportation_score': value, 25 | if (instance.transportationValue case final value?) 26 | 'transportation_value': value, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/src/model/product_result_field_answer.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product_result_field_answer.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ProductResultFieldAnswer _$ProductResultFieldAnswerFromJson( 10 | Map json) => 11 | ProductResultFieldAnswer() 12 | ..field = json['field'] == null 13 | ? null 14 | : ProductResultField.fromJson(json['field'] as Map) 15 | ..impact = json['impact'] == null 16 | ? null 17 | : LocalizedTag.fromJson(json['impact'] as Map) 18 | ..message = json['message'] == null 19 | ? null 20 | : LocalizedTag.fromJson(json['message'] as Map); 21 | 22 | Map _$ProductResultFieldAnswerToJson( 23 | ProductResultFieldAnswer instance) => 24 | { 25 | if (instance.field case final value?) 'field': value, 26 | if (instance.impact case final value?) 'impact': value, 27 | if (instance.message case final value?) 'message': value, 28 | }; 29 | -------------------------------------------------------------------------------- /lib/src/utils/uri_reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:http/http.dart' as http; 4 | import 'uri_reader_stub.dart' 5 | if (dart.library.io) 'uri_reader_io.dart' 6 | if (dart.library.js) 'uri_reader_js.dart'; 7 | 8 | /// Abstract reader of URI data, declined in "not web" and "web" versions 9 | abstract class UriReader { 10 | static late final UriReader _instance; 11 | static bool _initialized = false; 12 | 13 | static UriReader get instance { 14 | if (!_initialized) { 15 | _initialized = true; 16 | _instance = getUriReaderInstance(); 17 | } 18 | return _instance; 19 | } 20 | 21 | Future> readAsBytes(final Uri uri) async { 22 | final Uint8List? content = uri.data?.contentAsBytes(); 23 | if (content != null) { 24 | return content; 25 | } 26 | switch (uri.scheme) { 27 | case '': 28 | case 'file': 29 | return await readFileAsBytes(uri); 30 | case 'http': 31 | case 'https': 32 | final http.Response response = await http.get(uri); 33 | return response.bodyBytes; 34 | } 35 | throw Exception('Unknown uri scheme for $uri'); 36 | } 37 | 38 | Future> readFileAsBytes(final Uri uri); 39 | 40 | bool get isWeb => false; 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/search/autocomplete_single_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'taxonomy_name.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'autocomplete_single_result.g.dart'; 6 | 7 | /// Single item result of an autocomplete search on the Elastic Search API. 8 | @JsonSerializable() 9 | class AutocompleteSingleResult extends JsonObject { 10 | /// Tag, e.g. 'en:margherita-pizza'. 11 | @JsonKey() 12 | final String id; 13 | 14 | /// Localized text, e.g. 'Pizza au fromage et aux tomates'. 15 | @JsonKey() 16 | final String text; 17 | 18 | /// Taxonomy name off tag, e.g. 'category'. 19 | @JsonKey(name: 'taxonomy_name') 20 | final String taxonomyNameAsString; 21 | 22 | /// Taxonomy name, if [taxonomyNameAsString] is valid. 23 | TaxonomyName? get taxonomyName => 24 | TaxonomyName.fromOffTag(taxonomyNameAsString); 25 | 26 | const AutocompleteSingleResult({ 27 | required this.id, 28 | required this.text, 29 | required this.taxonomyNameAsString, 30 | }); 31 | 32 | factory AutocompleteSingleResult.fromJson(Map json) => 33 | _$AutocompleteSingleResultFromJson(json); 34 | 35 | @override 36 | Map toJson() => _$AutocompleteSingleResultToJson(this); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/model/ordered_nutrient.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ordered_nutrient.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | OrderedNutrient _$OrderedNutrientFromJson(Map json) => 10 | OrderedNutrient( 11 | important: json['important'] as bool, 12 | id: json['id'] as String, 13 | unit: json['unit'] as String?, 14 | displayInEditForm: json['display_in_edit_form'] as bool, 15 | name: json['name'] as String?, 16 | subNutrients: (json['nutrients'] as List?) 17 | ?.map((e) => OrderedNutrient.fromJson(e as Map)) 18 | .toList(), 19 | ); 20 | 21 | Map _$OrderedNutrientToJson(OrderedNutrient instance) => 22 | { 23 | 'id': instance.id, 24 | if (instance.name case final value?) 'name': value, 25 | 'important': instance.important, 26 | if (instance.unit case final value?) 'unit': value, 27 | 'display_in_edit_form': instance.displayInEditForm, 28 | if (instance.subNutrients case final value?) 'nutrients': value, 29 | }; 30 | -------------------------------------------------------------------------------- /lib/src/utils/product_helper.dart: -------------------------------------------------------------------------------- 1 | import 'language_helper.dart'; 2 | import 'uri_helper.dart'; 3 | import '../model/product.dart'; 4 | import '../model/product_image.dart'; 5 | 6 | /// Helper class around [Product] fields 7 | class ProductHelper { 8 | /// Reduces the set of images of the product depending on the given language. 9 | static void removeImages(Product product, OpenFoodFactsLanguage? language) { 10 | if (product.selectedImages == null) { 11 | return; 12 | } 13 | 14 | for (var field in ImageField.values) { 15 | if (product.selectedImages! 16 | .any((i) => i.field == field && i.language == language)) { 17 | product.selectedImages! 18 | .removeWhere((i) => i.field == field && i.language != language); 19 | } 20 | } 21 | } 22 | 23 | /// Generates a image url for each product image entry 24 | static void createImageUrls( 25 | Product product, { 26 | required UriProductHelper uriHelper, 27 | }) { 28 | if (product.images == null) { 29 | return; 30 | } 31 | 32 | if (product.barcode == null) { 33 | return; 34 | } 35 | 36 | for (ProductImage image in product.images!) { 37 | image.url = image.getUrl(product.barcode!, uriHelper: uriHelper); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/personalized_search/available_attribute_groups.dart: -------------------------------------------------------------------------------- 1 | import '../model/attribute_group.dart'; 2 | import '../utils/http_helper.dart'; 3 | 4 | /// Referential of attribute groups, with loader. 5 | class AvailableAttributeGroups { 6 | /// Load constructor; may throw an exception. 7 | AvailableAttributeGroups.loadFromJSONString( 8 | final String attributeGroupsString, 9 | ) { 10 | final dynamic inputJson = HttpHelper().jsonDecode(attributeGroupsString); 11 | final List attributeGroups = []; 12 | for (final dynamic item in inputJson as List) { 13 | attributeGroups.add(AttributeGroup.fromJson(item)); 14 | } 15 | if (attributeGroups.isEmpty) { 16 | throw Exception( 17 | 'Unexpected error: empty attribute groups from json string $attributeGroupsString'); 18 | } 19 | _attributeGroups = attributeGroups; 20 | } 21 | 22 | List? _attributeGroups; 23 | 24 | List? get attributeGroups => _attributeGroups; 25 | 26 | /// Where a localized JSON file can be found. 27 | /// [languageCode] is a 2-letter language code. 28 | static String getUrl(final String languageCode) => 29 | 'https://world.openfoodfacts.org/api/v2/attribute_groups?lc=$languageCode'; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/model/product_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../utils/json_helper.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'product_tag.g.dart'; 6 | 7 | /// Folksonomy product tag: for this barcode, that value is set for that key. 8 | @JsonSerializable() 9 | class ProductTag extends JsonObject { 10 | @JsonKey(name: 'product') 11 | final String barcode; 12 | @JsonKey(name: 'k') 13 | final String key; 14 | @JsonKey(name: 'v') 15 | final String value; 16 | final String owner; 17 | final int version; 18 | final String editor; 19 | @JsonKey( 20 | name: 'last_edit', 21 | fromJson: JsonHelper.stringTimestampToDate, 22 | ) 23 | final DateTime lastEdit; 24 | final String comment; 25 | 26 | ProductTag({ 27 | required this.barcode, 28 | required this.key, 29 | required this.value, 30 | required this.owner, 31 | required this.version, 32 | required this.editor, 33 | required this.lastEdit, 34 | required this.comment, 35 | }); 36 | 37 | factory ProductTag.fromJson(Map json) => 38 | _$ProductTagFromJson(json); 39 | 40 | @override 41 | Map toJson() => _$ProductTagToJson(this); 42 | 43 | @override 44 | String toString() => toJson().toString(); 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/model/product_result_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | import '../utils/json_helper.dart'; 4 | 5 | part 'product_result_field.g.dart'; 6 | 7 | /// Field part of an API V3 answer. 8 | @JsonSerializable() 9 | class ProductResultField extends JsonObject { 10 | ProductResultField(); 11 | 12 | @JsonKey(includeIfNull: false) 13 | String? id; 14 | 15 | /// Value sent by the user, here converted to String. 16 | @JsonKey( 17 | includeIfNull: false, 18 | fromJson: JsonHelper.stringFromJSON, 19 | ) 20 | String? value; 21 | 22 | @JsonKey( 23 | name: 'default_value', 24 | includeIfNull: false, 25 | fromJson: JsonHelper.stringFromJSON, 26 | ) 27 | String? defaultValue; 28 | 29 | /// Value actually used by the server, here converted to String. 30 | @JsonKey( 31 | name: 'valued_converted', 32 | includeIfNull: false, 33 | fromJson: JsonHelper.stringFromJSON, 34 | ) 35 | String? valuedConverted; 36 | 37 | factory ProductResultField.fromJson(Map json) => 38 | _$ProductResultFieldFromJson(json); 39 | 40 | @override 41 | Map toJson() => _$ProductResultFieldToJson(this); 42 | 43 | @override 44 | String toString() => toJson().toString(); 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/model/taxonomy_language.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'taxonomy_language.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TaxonomyLanguage _$TaxonomyLanguageFromJson(Map json) => 10 | TaxonomyLanguage( 11 | LanguageHelper.fromJsonStringMap(json['language_code_2']), 12 | LanguageHelper.fromJsonStringMap(json['language_code_3']), 13 | LanguageHelper.fromJsonStringMap(json['name']), 14 | LanguageHelper.fromJsonStringMap(json['wikidata']), 15 | ); 16 | 17 | Map _$TaxonomyLanguageToJson(TaxonomyLanguage instance) => 18 | { 19 | if (LanguageHelper.toJsonStringMap(instance.languageCode2) 20 | case final value?) 21 | 'language_code_2': value, 22 | if (LanguageHelper.toJsonStringMap(instance.languageCode3) 23 | case final value?) 24 | 'language_code_3': value, 25 | if (LanguageHelper.toJsonStringMap(instance.name) case final value?) 26 | 'name': value, 27 | if (LanguageHelper.toJsonStringMap(instance.wikidata) case final value?) 28 | 'wikidata': value, 29 | }; 30 | -------------------------------------------------------------------------------- /test/api_get_product_image_ids_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/openfoodfacts.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'test_constants.dart'; 5 | 6 | void main() { 7 | OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; 8 | 9 | test('get product images (all, main and raw)', () async { 10 | const String barcode = '3019081238643'; 11 | await getProductTooManyRequestsManager.waitIfNeeded(); 12 | final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( 13 | ProductQueryConfiguration( 14 | barcode, 15 | version: ProductQueryVersion.v3, 16 | fields: [ProductField.IMAGES], 17 | ), 18 | user: TestConstants.PROD_USER, 19 | ); 20 | expect(result.product, isNotNull); 21 | expect(result.product!.images, isNotNull); 22 | final int countAll = result.product!.images!.length; 23 | final int countRaw = result.product!.getRawImages()!.length; 24 | final int countMain = result.product!.getMainImages()!.length; 25 | expect(countRaw, greaterThanOrEqualTo(102)); // was 102 on 2023-11-25 26 | expect(countMain, greaterThanOrEqualTo(16)); // was 16 on 2023-11-25 27 | expect(countAll, greaterThanOrEqualTo(118)); // was 118 on 2023-11-25 28 | expect(countAll, countRaw + countMain); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/model/events_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../utils/json_helper.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'events_base.g.dart'; 6 | 7 | /// Events API: event. 8 | @JsonSerializable() 9 | class EventsBase extends JsonObject { 10 | @JsonKey(name: 'event_type') 11 | final String eventType; 12 | 13 | @JsonKey(fromJson: JsonHelper.nullableStringTimestampToDate) 14 | final DateTime? timestamp; 15 | 16 | @JsonKey(name: 'user_id') 17 | final String? userId; 18 | 19 | @JsonKey() 20 | final String? barcode; 21 | 22 | @JsonKey() 23 | final int? points; 24 | 25 | EventsBase({ 26 | required this.eventType, 27 | this.timestamp, 28 | this.userId, 29 | this.barcode, 30 | this.points, 31 | }); 32 | 33 | factory EventsBase.fromJson(Map json) => 34 | _$EventsBaseFromJson(json); 35 | 36 | @override 37 | Map toJson() => _$EventsBaseToJson(this); 38 | 39 | @override 40 | String toString() => 'EventsBase(eventType: $eventType' 41 | '${timestamp == null ? '' : ', timestamp: $timestamp'}' 42 | '${userId == null ? '' : ', userId: $userId'}' 43 | '${barcode == null ? '' : ', barcode: $barcode'}' 44 | '${points == null ? '' : ', points: $points'}' 45 | ')'; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/personalized_search/available_product_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'available_attribute_groups.dart'; 2 | import 'available_preference_importances.dart'; 3 | 4 | /// Referential for product preferences: attribute groups and importance. 5 | class AvailableProductPreferences { 6 | /// Load constructor - may throw an exception. 7 | AvailableProductPreferences.loadFromJSONStrings({ 8 | required final String preferenceImportancesString, 9 | required final String attributeGroupsString, 10 | }) { 11 | final AvailableAttributeGroups availableAttributeGroups = 12 | AvailableAttributeGroups.loadFromJSONString(attributeGroupsString); 13 | final AvailablePreferenceImportances availablePreferenceImportances = 14 | AvailablePreferenceImportances.loadFromJSONString( 15 | preferenceImportancesString); 16 | _availableAttributeGroups = availableAttributeGroups; 17 | _availablePreferenceImportances = availablePreferenceImportances; 18 | } 19 | 20 | AvailableAttributeGroups? _availableAttributeGroups; 21 | AvailableAttributeGroups? get availableAttributeGroups => 22 | _availableAttributeGroups; 23 | 24 | AvailablePreferenceImportances? _availablePreferenceImportances; 25 | AvailablePreferenceImportances? get availablePreferenceImportances => 26 | _availablePreferenceImportances; 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/model/product_freshness.dart: -------------------------------------------------------------------------------- 1 | import 'product.dart'; 2 | 3 | /// Freshness of a [Product]. 4 | class ProductFreshness { 5 | ProductFreshness._({ 6 | required this.isEcoscoreReady, 7 | required this.isNutriscoreReady, 8 | required this.isIngredientsReady, 9 | required this.lastModified, 10 | required this.improvements, 11 | }); 12 | 13 | final bool isEcoscoreReady; 14 | final bool isNutriscoreReady; 15 | final bool isIngredientsReady; 16 | final DateTime? lastModified; 17 | final Set improvements; 18 | 19 | factory ProductFreshness.fromProduct(final Product product) => 20 | ProductFreshness._( 21 | isEcoscoreReady: product.ecoscoreScore != null, 22 | isNutriscoreReady: product.nutriscore != null, 23 | isIngredientsReady: product.ingredientsTags != null && 24 | product.ingredientsTags!.isNotEmpty, 25 | lastModified: product.lastModified, 26 | improvements: product.getProductImprovements(), 27 | ); 28 | 29 | @override 30 | String toString() => 'ProductFreshness(' 31 | 'ecoscore:$isEcoscoreReady' 32 | ',' 33 | 'nutriscore:$isNutriscoreReady' 34 | ',' 35 | 'ingredients:$isIngredientsReady' 36 | ',' 37 | 'lastModified:$lastModified' 38 | ',' 39 | 'improvements:$improvements' 40 | ')'; 41 | } 42 | -------------------------------------------------------------------------------- /test/recommended_daily_intake_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/openfoodfacts.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'test_constants.dart'; 5 | 6 | void main() { 7 | OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; 8 | 9 | group('Get Recommendations', () { 10 | test('Get', () { 11 | RecommendedDailyIntake rdi = 12 | RecommendedDailyIntake.getRecommendedDailyIntakes(); 13 | 14 | expect(rdi.energyKcal.value == 2000, true); 15 | expect(rdi.energyKcal.unit == Unit.KCAL, true); 16 | 17 | expect(rdi.energyKj.value == 8400, true); 18 | expect(rdi.energyKj.unit == Unit.KJ, true); 19 | 20 | expect(rdi.carbohydrates.value == 260, true); 21 | expect(rdi.carbohydrates.unit == Unit.G, true); 22 | 23 | expect(rdi.sodium.value == 6, true); 24 | expect(rdi.sodium.unit == Unit.G, true); 25 | 26 | expect(rdi.sugars.value == 90, true); 27 | expect(rdi.sugars.unit == Unit.G, true); 28 | 29 | expect(rdi.fluoride.value == 3.5, true); 30 | expect(rdi.fluoride.unit == Unit.MILLI_G, true); 31 | 32 | expect(rdi.chromium.value == 40, true); 33 | expect(rdi.chromium.unit == Unit.MICRO_G, true); 34 | 35 | expect(rdi.magnesium.value == 375, true); 36 | expect(rdi.magnesium.unit == Unit.MILLI_G, true); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/utils/tag_type_autocompleter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../model/user.dart'; 4 | import '../open_food_api_client.dart'; 5 | import 'autocompleter.dart'; 6 | import 'country_helper.dart'; 7 | import 'language_helper.dart'; 8 | import 'open_food_api_configuration.dart'; 9 | import 'uri_helper.dart'; 10 | import 'tag_type.dart'; 11 | 12 | /// Autocomplete suggestions for [TagType]. 13 | class TagTypeAutocompleter implements Autocompleter { 14 | const TagTypeAutocompleter({ 15 | required this.tagType, 16 | required this.language, 17 | this.country, 18 | this.categories, 19 | this.shape, 20 | this.limit = 25, 21 | this.uriHelper = uriHelperFoodProd, 22 | this.user, 23 | }); 24 | 25 | final TagType tagType; 26 | final OpenFoodFactsLanguage language; 27 | final OpenFoodFactsCountry? country; 28 | final String? categories; 29 | final String? shape; 30 | final int limit; 31 | final UriProductHelper uriHelper; 32 | final User? user; 33 | 34 | @override 35 | Future> getSuggestions( 36 | final String input, 37 | ) async => 38 | OpenFoodAPIClient.getSuggestions( 39 | tagType, 40 | input: input, 41 | language: language, 42 | country: country, 43 | categories: categories, 44 | shape: shape, 45 | limit: limit, 46 | uriHelper: uriHelper, 47 | user: user, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/model/ecoscore_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | import 'agribalyse.dart'; 4 | import 'ecoscore_adjustments.dart'; 5 | 6 | part 'ecoscore_data.g.dart'; 7 | 8 | enum EcoscoreStatus { 9 | @JsonValue('known') 10 | KNOWN, 11 | @JsonValue('unknown') 12 | UNKNOWN 13 | } 14 | 15 | @JsonSerializable(explicitToJson: true) 16 | class EcoscoreData extends JsonObject { 17 | @JsonKey(includeIfNull: false) 18 | String? grade; 19 | @JsonKey(includeIfNull: false, fromJson: JsonObject.parseDouble) 20 | double? score; 21 | @JsonKey(includeIfNull: false) 22 | EcoscoreStatus? status; 23 | @JsonKey(includeIfNull: false) 24 | Agribalyse? agribalyse; 25 | @JsonKey(includeIfNull: false) 26 | EcoscoreAdjustments? adjustments; 27 | @JsonKey( 28 | name: 'missing_data_warning', 29 | fromJson: JsonObject.parseBool, 30 | ) 31 | bool missingDataWarning; 32 | 33 | EcoscoreData({ 34 | this.grade, 35 | this.score, 36 | this.status, 37 | this.agribalyse, 38 | this.adjustments, 39 | this.missingDataWarning = false, 40 | }); 41 | 42 | factory EcoscoreData.fromJson(Map json) => 43 | _$EcoscoreDataFromJson(json); 44 | 45 | @override 46 | Map toJson() => _$EcoscoreDataToJson(this); 47 | 48 | static Map? toJsonHelper(EcoscoreData? d) => d?.toJson(); 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/model/taxonomy_packaging.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'taxonomy_packaging.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TaxonomyPackaging _$TaxonomyPackagingFromJson(Map json) => 10 | TaxonomyPackaging() 11 | ..name = LanguageHelper.fromJsonStringMap(json['name']) 12 | ..synonyms = LanguageHelper.fromJsonStringsListMap(json['synonyms']) 13 | ..wikidata = LanguageHelper.fromJsonStringMap(json['wikidata']) 14 | ..children = 15 | (json['children'] as List?)?.map((e) => e as String).toList() 16 | ..parents = 17 | (json['parents'] as List?)?.map((e) => e as String).toList(); 18 | 19 | Map _$TaxonomyPackagingToJson(TaxonomyPackaging instance) => 20 | { 21 | if (LanguageHelper.toJsonStringMap(instance.name) case final value?) 22 | 'name': value, 23 | if (LanguageHelper.toJsonStringsListMap(instance.synonyms) 24 | case final value?) 25 | 'synonyms': value, 26 | if (LanguageHelper.toJsonStringMap(instance.wikidata) case final value?) 27 | 'wikidata': value, 28 | if (instance.children case final value?) 'children': value, 29 | if (instance.parents case final value?) 'parents': value, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/src/model/product_result_v3.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product_result_v3.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ProductResultV3 _$ProductResultV3FromJson(Map json) => 10 | ProductResultV3() 11 | ..barcode = json['code'] as String? 12 | ..result = json['result'] == null 13 | ? null 14 | : LocalizedTag.fromJson(json['result'] as Map) 15 | ..status = json['status'] as String? 16 | ..errors = ProductResultV3._fromJsonListAnswerForField(json['errors']) 17 | ..warnings = ProductResultV3._fromJsonListAnswerForField(json['warnings']) 18 | ..product = json['product'] == null 19 | ? null 20 | : Product.fromJson(json['product'] as Map); 21 | 22 | Map _$ProductResultV3ToJson(ProductResultV3 instance) => 23 | { 24 | if (instance.barcode case final value?) 'code': value, 25 | if (instance.result case final value?) 'result': value, 26 | if (instance.status case final value?) 'status': value, 27 | if (instance.errors case final value?) 'errors': value, 28 | if (instance.warnings case final value?) 'warnings': value, 29 | if (instance.product case final value?) 'product': value, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/src/prices/session.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import '../interface/json_object.dart'; 4 | import '../utils/json_helper.dart'; 5 | 6 | part 'session.g.dart'; 7 | 8 | /// Price session. 9 | /// 10 | /// cf. SessionBase in https://prices.openfoodfacts.org/api/docs 11 | @JsonSerializable() 12 | class Session extends JsonObject { 13 | @JsonKey(name: 'user_id') 14 | late String userId; 15 | 16 | @JsonKey(fromJson: JsonHelper.stringTimestampToDate) 17 | late DateTime created; 18 | 19 | @JsonKey( 20 | name: 'last_used', 21 | fromJson: JsonHelper.nullableStringTimestampToDate, 22 | ) 23 | DateTime? lastUsed; 24 | 25 | Session(); 26 | 27 | factory Session.fromJson(Map json) => 28 | _$SessionFromJson(json); 29 | 30 | @override 31 | Map toJson() => _$SessionToJson(this); 32 | 33 | /// Status Code when the authentication fails. 34 | static const int invalidAuthStatusCode = 401; 35 | 36 | /// Error message when the authentication fails. 37 | static const String invalidAuthMessage = 'Invalid authentication credentials'; 38 | 39 | /// Status Code when we try an edit operation with a wrong authentication. 40 | static const int invalidActionWithAuthStatusCode = 403; 41 | 42 | /// Error message when we try an edit operation with a wrong authentication. 43 | static const String invalidActionWithAuthMessage = 44 | 'Authentication credentials were not provided.'; 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/utils/product_search_query_configuration.dart: -------------------------------------------------------------------------------- 1 | import 'abstract_query_configuration.dart'; 2 | import 'product_fields.dart'; 3 | import 'product_query_configurations.dart'; 4 | import '../interface/parameter.dart'; 5 | 6 | /// Query Configuration for search parameters 7 | class ProductSearchQueryConfiguration extends AbstractQueryConfiguration { 8 | /// See [AbstractQueryConfiguration.languages] for 9 | /// parameter's description. 10 | ProductSearchQueryConfiguration({ 11 | super.language, 12 | super.languages, 13 | super.country, 14 | super.fields, 15 | super.activateKnowledgePanelsSimplified, 16 | required List parametersList, 17 | required this.version, 18 | }) : super( 19 | additionalParameters: parametersList, 20 | ); 21 | 22 | final ProductQueryVersion version; 23 | 24 | List getFieldsKeys() { 25 | List result = []; 26 | 27 | for (ProductField field in fields!) { 28 | result.add(field.offTag); 29 | } 30 | 31 | return result; 32 | } 33 | 34 | @override 35 | Map getParametersMap() { 36 | final Map result = super.getParametersMap(); 37 | result.putIfAbsent('search_terms', () => ''); 38 | // explicit json output 39 | result.putIfAbsent('json', () => '1'); 40 | result.putIfAbsent('api_version', () => '${version.version}'); 41 | return result; 42 | } 43 | 44 | @override 45 | String getUriPath() => '/cgi/search.pl'; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/prices/get_price_products_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/src/prices/flavor.dart'; 2 | 3 | import 'get_price_count_parameters_helper.dart'; 4 | import 'get_price_products_order.dart'; 5 | 6 | /// Parameters for the "get price products" API query. 7 | class GetPriceProductsParameters 8 | extends GetPriceCountParametersHelper { 9 | String? brandsLike; 10 | String? brandsTagsContains; 11 | String? categoriesTagsContains; 12 | String? code; 13 | String? ecoscoreGrade; 14 | String? labelsTagsContains; 15 | String? novaGroup; 16 | String? nutriscoreGrade; 17 | String? productNameLike; 18 | int? uniqueScansNGte; 19 | Flavor? source; 20 | 21 | @override 22 | Map getQueryParameters() { 23 | super.getQueryParameters(); 24 | addNonNullString(brandsLike, 'brands__like'); 25 | addNonNullString(brandsTagsContains, 'brands_tags__contains'); 26 | addNonNullString(categoriesTagsContains, 'categories_tags__contains'); 27 | addNonNullString(code, 'code'); 28 | addNonNullString(ecoscoreGrade, 'ecoscore_grade'); 29 | addNonNullString(labelsTagsContains, 'labels_tags__contains'); 30 | addNonNullString(novaGroup, 'nova_group'); 31 | addNonNullString(nutriscoreGrade, 'nutriscore_grade'); 32 | addNonNullString(productNameLike, 'product_name__like'); 33 | addNonNullInt(uniqueScansNGte, 'unique_scans_n__gte'); 34 | addNonNullString(source?.offTag, 'source'); 35 | return result; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/personalized_search/preference_importance.dart: -------------------------------------------------------------------------------- 1 | /// Importance level when we match products to preferences. 2 | /// Will be loaded in JSON as a list of increasingly important items. 3 | class PreferenceImportance { 4 | PreferenceImportance({this.id, this.name, this.factor, this.minimalMatch}); 5 | 6 | factory PreferenceImportance.fromJson(dynamic json) => PreferenceImportance( 7 | id: _checkedId(json['id'] as String), 8 | name: json['name'] as String?, 9 | factor: json['factor'] as int?, 10 | minimalMatch: json['minimum_match'] as int?, 11 | ); 12 | 13 | final String? id; 14 | final String? name; 15 | final int? factor; 16 | final int? minimalMatch; 17 | 18 | /// The index of the least important, therefore 0 (which is "NOT" important). 19 | static const int INDEX_NOT_IMPORTANT = 0; 20 | 21 | static const String ID_NOT_IMPORTANT = 'not_important'; 22 | static const String ID_IMPORTANT = 'important'; 23 | static const String ID_VERY_IMPORTANT = 'very_important'; 24 | static const String ID_MANDATORY = 'mandatory'; 25 | 26 | static const List IDS = [ 27 | ID_NOT_IMPORTANT, 28 | ID_IMPORTANT, 29 | ID_VERY_IMPORTANT, 30 | ID_MANDATORY, 31 | ]; 32 | 33 | @override 34 | String toString() => 'PreferenceImportance(' 35 | 'id: $id, name: $name, factor: $factor, minimalWatch: $minimalMatch' 36 | ')'; 37 | 38 | static String _checkedId(final String id) => 39 | IDS.contains(id) ? id : throw Exception('Unknown id "$id"'); 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/model/knowledge_panels.dart: -------------------------------------------------------------------------------- 1 | import 'knowledge_panel.dart'; 2 | 3 | /// An object containing all KnowledgePanels in the form of a map. 4 | class KnowledgePanels { 5 | /// A map of the type: {"knowledge-panel-id": KnowledgePanel} 6 | final Map panelIdToPanelMap; 7 | 8 | const KnowledgePanels({required this.panelIdToPanelMap}); 9 | 10 | factory KnowledgePanels.fromJson(Map json) { 11 | Map map = {}; 12 | for (var panelId in json.keys) { 13 | map[panelId] = KnowledgePanel.fromJson(json[panelId]); 14 | } 15 | return KnowledgePanels(panelIdToPanelMap: map); 16 | } 17 | 18 | factory KnowledgePanels.empty() { 19 | return KnowledgePanels(panelIdToPanelMap: {}); 20 | } 21 | 22 | @override 23 | String toString() => 'KnowledgePanels(map: $panelIdToPanelMap)'; 24 | 25 | static KnowledgePanels? fromJsonHelper(final Map? json) => json == null 26 | ? null 27 | : KnowledgePanels.fromJson(json as Map); 28 | 29 | static Map? toJsonHelper( 30 | final KnowledgePanels? knowledgePanels) { 31 | final Map result = {}; 32 | if (knowledgePanels == null) { 33 | return null; 34 | } 35 | for (final MapEntry entry 36 | in knowledgePanels.panelIdToPanelMap.entries) { 37 | result[entry.key] = entry.value.toJson(); 38 | } 39 | return result; 40 | } 41 | 42 | static const String simplifiedRootId = 'simplified_root'; 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/model/robotoff_question.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'insight.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'robotoff_question.g.dart'; 6 | 7 | @JsonSerializable() 8 | class RobotoffQuestionResult extends JsonObject { 9 | final String? status; 10 | 11 | final List? questions; 12 | 13 | const RobotoffQuestionResult({this.status, this.questions}); 14 | 15 | factory RobotoffQuestionResult.fromJson(Map json) => 16 | _$RobotoffQuestionResultFromJson(json); 17 | 18 | @override 19 | Map toJson() => _$RobotoffQuestionResultToJson(this); 20 | } 21 | 22 | @JsonSerializable() 23 | class RobotoffQuestion extends JsonObject { 24 | final String? barcode; 25 | final String? type; 26 | final String? value; 27 | final String? question; 28 | @JsonKey(name: 'insight_id') 29 | final String? insightId; 30 | @JsonKey(name: 'insight_type', unknownEnumValue: InsightType.UNKNOWN) 31 | final InsightType? insightType; 32 | @JsonKey(name: 'source_image_url') 33 | final String? imageUrl; 34 | 35 | const RobotoffQuestion( 36 | {this.barcode, 37 | this.type, 38 | this.value, 39 | this.question, 40 | this.insightId, 41 | this.insightType, 42 | this.imageUrl}); 43 | 44 | factory RobotoffQuestion.fromJson(Map json) => 45 | _$RobotoffQuestionFromJson(json); 46 | 47 | @override 48 | Map toJson() => _$RobotoffQuestionToJson(this); 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/model/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import '../interface/json_object.dart'; 4 | 5 | part 'user.g.dart'; 6 | 7 | @JsonSerializable() 8 | class User extends JsonObject { 9 | @JsonKey(includeIfNull: false) 10 | final String? comment; 11 | 12 | @JsonKey(name: 'user_id') 13 | final String userId; 14 | @JsonKey() 15 | final String password; 16 | 17 | final String? cookie; 18 | 19 | const User({ 20 | this.comment, 21 | required this.userId, 22 | required this.password, 23 | this.cookie, 24 | }); 25 | 26 | factory User.fromJson(Map json) => _$UserFromJson(json); 27 | 28 | @override 29 | Map toJson() => _$UserToJson(this); 30 | 31 | static String getUserWikiTag(final String userId) => 32 | '${userId.substring(0, 1).toUpperCase()}${userId.substring(1)}'; 33 | 34 | static String getUserWikiPage(final String userId) => Uri( 35 | scheme: 'https', 36 | host: 'wiki.openfoodfacts.org', 37 | path: 'User:${getUserWikiTag(userId)}', 38 | ).toString(); 39 | 40 | /// Link to add a discussion topic on the OpenFoodFacts wiki page of a user. 41 | static String getUserWikiDiscussionPage(final String userId) => Uri( 42 | scheme: 'https', 43 | host: 'wiki.openfoodfacts.org', 44 | path: 'index.php', 45 | queryParameters: { 46 | 'title': 'User_talk:${getUserWikiTag(userId)}', 47 | 'action': 'edit', 48 | 'section': 'new', 49 | }, 50 | ).toString(); 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/model/ordered_nutrient.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'nutrient.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'ordered_nutrient.g.dart'; 6 | 7 | /// Nutrient, as a hierarchically ordered and localized entity. 8 | /// 9 | /// cf. https://github.com/openfoodfacts/openfoodfacts-dart/issues/210 10 | /// Example in https://fr.openfoodfacts.org/cgi/nutrients.pl 11 | /// To be compared to [OrderedNutrients], which is the root of the structure. 12 | @JsonSerializable(includeIfNull: false) 13 | class OrderedNutrient extends JsonObject { 14 | /// Nutrient ID (e.g. 'energy-kcal') 15 | @JsonKey() 16 | final String id; 17 | 18 | /// Localized nutrient name (e.g. 'Energía (kcal)' in Spanish) 19 | @JsonKey() 20 | final String? name; 21 | 22 | @JsonKey() 23 | final bool important; 24 | 25 | @JsonKey() 26 | final String? unit; 27 | 28 | @JsonKey(name: 'display_in_edit_form') 29 | final bool displayInEditForm; 30 | 31 | /// Hierarchically related nutrients 32 | @JsonKey(name: 'nutrients') 33 | final List? subNutrients; 34 | 35 | OrderedNutrient({ 36 | required this.important, 37 | required this.id, 38 | required this.unit, 39 | required this.displayInEditForm, 40 | this.name, 41 | this.subNutrients, 42 | }); 43 | 44 | factory OrderedNutrient.fromJson(Map json) => 45 | _$OrderedNutrientFromJson(json); 46 | 47 | @override 48 | Map toJson() => _$OrderedNutrientToJson(this); 49 | 50 | Nutrient? get nutrient => Nutrient.fromOffTag(id); 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/prices/challenge.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import '../interface/json_object.dart'; 4 | import '../utils/json_helper.dart'; 5 | import 'location.dart'; 6 | 7 | part 'challenge.g.dart'; 8 | 9 | /// Challenge object in the Prices API. 10 | /// 11 | /// cf. `Challenge` in https://prices.openfoodfacts.org/api/docs 12 | @JsonSerializable() 13 | class Challenge extends JsonObject { 14 | @JsonKey() 15 | int? id; 16 | 17 | @JsonKey() 18 | List? locations; 19 | 20 | @JsonKey() 21 | String? status; 22 | 23 | @JsonKey() 24 | String? tag; 25 | 26 | @JsonKey() 27 | String? title; 28 | 29 | @JsonKey() 30 | String? icon; 31 | 32 | @JsonKey() 33 | String? subtitle; 34 | 35 | @JsonKey(name: 'start_date', fromJson: JsonHelper.stringTimestampToDate) 36 | DateTime? startDate; 37 | 38 | @JsonKey(name: 'end_date', fromJson: JsonHelper.stringTimestampToDate) 39 | DateTime? endDate; 40 | 41 | @JsonKey() 42 | List? categories; 43 | 44 | @JsonKey(name: 'example_proof_url') 45 | String? exampleProofUrl; 46 | 47 | @JsonKey(name: 'is_published') 48 | bool? isPublished; 49 | 50 | @JsonKey() 51 | dynamic stats; 52 | 53 | @JsonKey(fromJson: JsonHelper.stringTimestampToDate) 54 | DateTime? created; 55 | 56 | @JsonKey(fromJson: JsonHelper.nullableStringTimestampToDate) 57 | DateTime? updated; 58 | 59 | Challenge(); 60 | 61 | factory Challenge.fromJson(Map json) => 62 | _$ChallengeFromJson(json); 63 | 64 | @override 65 | Map toJson() => _$ChallengeToJson(this); 66 | } 67 | -------------------------------------------------------------------------------- /REUSERS.md: -------------------------------------------------------------------------------- 1 | ## Applications using this SDK 2 | 3 | ### Official application 4 | 5 | Open Food Facts (Codename **Smoothie**) is the official app developed by Open Food Facts, which is available on [Android](https://play.google.com/store/apps/details?id=org.openfoodfacts.scanner) and [iOS](https://apps.apple.com/app/open-food-facts/id588797948). The source code is also available on [GitHub](https://github.com/openfoodfacts/smooth-app). 6 | 7 | ### Third party applications 8 | 9 | Feel [free to open a PR to add your application in this list](https://github.com/openfoodfacts/openfoodfacts-dart/edit/master/REUSERS.md). 10 | Please get in touch at reuse@openfoodfacts.org 11 | We are very interested in learning what the Open Food Facts data is used for. It is not mandatory, but we would very much appreciate it if you tell us about your re-uses (https://forms.gle/hwaeqBfs8ywwhbTg8) so that we can share them with the Open Food Facts community. You can also fill this form to get a chance to get your app featured: https://forms.gle/hwaeqBfs8ywwhbTg8 12 | 13 | 14 | 15 | - **Glutten Scan**. [Android](https://play.google.com/store/apps/details?id=com.healthyfood.gluten_free_app) / [iOS](https://apps.apple.com/ch/app/gluten-scanner/id1540660083) 16 | - **Halal & Healthy**. [Android](https://play.google.com/store/apps/details?id=com.TagIn.Tech.handh) / [iOS](https://apps.apple.com/ch/app/halal-healthy/id1603051382) 17 | - **Fitness Tracker**. [Android](https://play.google.com/store/apps/details?id=dk.cepk.fitness_tracker) 18 | - [All public repositories using this package](https://github.com/openfoodfacts/openfoodfacts-dart/network/dependents) 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | if: github.repository_owner == 'openfoodfacts' 10 | steps: 11 | 12 | - name: Release-Please 13 | id: release 14 | uses: GoogleCloudPlatform/release-please-action@v4 15 | with: 16 | token: ${{ secrets.OPENFOODFACTS_BOT_PAT }} 17 | release-type: dart 18 | 19 | # - name: Chekout code 20 | # if: ${{ steps.release.outputs.release_created }} 21 | # uses: actions/checkout@v3 22 | # 23 | # - name: Setup Dart 24 | # if: ${{ steps.release.outputs.release_created }} 25 | # uses: dart-lang/setup-dart@v1.3 26 | # 27 | # - name: Install dependencies 28 | # if: ${{ steps.release.outputs.release_created }} 29 | # run: dart pub get . 30 | # 31 | # - name: Set version 32 | # if: ${{ steps.release.outputs.release_created }} 33 | # run: dart pub global activate cider && cider version "${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}" 34 | # 35 | # - name: Setup credentials 36 | # if: ${{ steps.release.outputs.release_created }} 37 | # run: | 38 | # cat < $PUB_CACHE/credentials.json 39 | # ${{ secrets.PUB_DEV_CREDENTIALS_JSON }} 40 | # EOF 41 | # 42 | # - name: Publish package 43 | # if: ${{ steps.release.outputs.release_created }} 44 | # run: dart pub publish --force 45 | # 46 | -------------------------------------------------------------------------------- /lib/src/prices/get_proofs_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'contribution_kind.dart'; 2 | import 'get_price_count_parameters_helper.dart'; 3 | import 'get_proofs_order.dart'; 4 | import 'proof_type.dart'; 5 | import 'currency.dart'; 6 | import 'location_osm_type.dart'; 7 | 8 | /// Parameters for the "get proofs" API query. 9 | /// 10 | /// cf. https://prices.openfoodfacts.org/api/docs 11 | class GetProofsParameters 12 | extends GetPriceCountParametersHelper { 13 | String? owner; 14 | ProofType? type; 15 | int? locationOSMId; 16 | LocationOSMType? locationOSMType; 17 | int? locationId; 18 | Currency? currency; 19 | DateTime? date; 20 | DateTime? dateGt; 21 | DateTime? dateGte; 22 | DateTime? dateLt; 23 | DateTime? dateLte; 24 | ContributionKind? kind; 25 | 26 | /// Returns the parameters as a query parameter map. 27 | @override 28 | Map getQueryParameters() { 29 | super.getQueryParameters(); 30 | addNonNullString(owner, 'owner'); 31 | addNonNullString(type?.offTag, 'type'); 32 | addNonNullInt(locationOSMId, 'location_osm_id'); 33 | addNonNullString(locationOSMType?.offTag, 'location_osm_type'); 34 | addNonNullInt(locationId, 'location_id'); 35 | addNonNullString(currency?.name, 'currency'); 36 | addNonNullDate(date, 'date', dayOnly: true); 37 | addNonNullDate(dateGt, 'date__gt', dayOnly: true); 38 | addNonNullDate(dateGte, 'date__gte', dayOnly: true); 39 | addNonNullDate(dateLt, 'date__lt', dayOnly: true); 40 | addNonNullDate(dateLte, 'date__lte', dayOnly: true); 41 | addNonNullString(kind?.offTag, 'kind'); 42 | return result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/model/product_packaging.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'product_packaging.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ProductPackaging _$ProductPackagingFromJson(Map json) => 10 | ProductPackaging() 11 | ..shape = json['shape'] == null 12 | ? null 13 | : LocalizedTag.fromJson(json['shape'] as Map) 14 | ..material = json['material'] == null 15 | ? null 16 | : LocalizedTag.fromJson(json['material'] as Map) 17 | ..recycling = json['recycling'] == null 18 | ? null 19 | : LocalizedTag.fromJson(json['recycling'] as Map) 20 | ..numberOfUnits = JsonObject.parseInt(json['number_of_units']) 21 | ..quantityPerUnit = json['quantity_per_unit'] as String? 22 | ..weightMeasured = JsonObject.parseDouble(json['weight_measured']); 23 | 24 | Map _$ProductPackagingToJson(ProductPackaging instance) => 25 | { 26 | 'shape': LocalizedTag.objToJson(instance.shape), 27 | 'material': LocalizedTag.objToJson(instance.material), 28 | 'recycling': LocalizedTag.objToJson(instance.recycling), 29 | if (instance.numberOfUnits case final value?) 'number_of_units': value, 30 | if (instance.quantityPerUnit case final value?) 31 | 'quantity_per_unit': value, 32 | if (instance.weightMeasured case final value?) 'weight_measured': value, 33 | }; 34 | -------------------------------------------------------------------------------- /test/json_conversion_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:openfoodfacts/openfoodfacts.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('ProductPackaging bug - covers issues #752 and #765', () { 6 | const expectedProductName = 'Orange'; 7 | final testProduct = Product()..productName = expectedProductName; 8 | const expectedQuantity = '75cl'; 9 | final packing = ProductPackaging()..quantityPerUnit = expectedQuantity; 10 | const expectedId = '4'; 11 | packing.shape = LocalizedTag()..id = expectedId; 12 | testProduct.packagings = [packing]; 13 | final productJson = testProduct.toJson(); 14 | final productRestored = Product.fromJson(productJson); 15 | expect(productRestored.productName, expectedProductName); 16 | final packingRestored = productRestored.packagings!.first; 17 | expect(packingRestored.quantityPerUnit, expectedQuantity); 18 | expect(packingRestored.shape!.id, expectedId); 19 | }); 20 | 21 | test('ProductPackaging bug - covers Smoothie issue #6369 (part 1)', () { 22 | final testProduct = Product(); 23 | testProduct.packagings = []; 24 | final productJson = testProduct.toJson(); 25 | final productRestored = Product.fromJson(productJson); 26 | final packagings = productRestored.packagings; 27 | expect(packagings, isNotNull); 28 | expect(packagings, isEmpty); 29 | }); 30 | 31 | test('ProductPackaging bug - covers Smoothie issue #6369 (part 2)', () { 32 | final testProduct = Product(); 33 | testProduct.packagings = null; 34 | final productJson = testProduct.toJson(); 35 | final productRestored = Product.fromJson(productJson); 36 | final packagings = productRestored.packagings; 37 | expect(packagings, isNull); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/model/knowledge_panel_action.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | /// Possible needed contribute actions. 4 | /// 5 | /// cf. [KnowledgePanelActionElement.actions]. 6 | enum KnowledgePanelAction implements OffTagged { 7 | /// Action: add categories. 8 | addCategories(offTag: 'add_categories'), 9 | 10 | /// Action: add ingredients text. 11 | addIngredientsText(offTag: 'add_ingredients_text'), 12 | 13 | /// Action: add ingredients image. 14 | addIngredientsImage(offTag: 'add_ingredients_image'), 15 | 16 | /// Action: add packaging image. 17 | addPackagingImage(offTag: 'add_packaging_image'), 18 | 19 | /// Action: add packaging text. 20 | addPackagingText(offTag: 'add_packaging_text'), 21 | 22 | /// Action: add nutrition facts. 23 | addNutritionFacts(offTag: 'add_nutrition_facts'), 24 | 25 | /// Action: add origins. 26 | addOrigins(offTag: 'add_origins'), 27 | 28 | /// Action: add stores. 29 | addStores(offTag: 'add_stores'), 30 | 31 | /// Action: add labels. 32 | addLabels(offTag: 'add_labels'), 33 | 34 | /// Action: add countries. 35 | addCountries(offTag: 'add_countries'), 36 | 37 | /// Action: edit the product. 38 | editProduct(offTag: 'edit_product'), 39 | 40 | /// Action: report the product to Nutripatrol. 41 | reportProductToNutripatrol(offTag: 'report_product_to_nutripatrol'); 42 | 43 | const KnowledgePanelAction({ 44 | required this.offTag, 45 | }); 46 | 47 | @override 48 | final String offTag; 49 | 50 | /// Returns the first [KnowledgePanelAction] that matches the [offTag]. 51 | static KnowledgePanelAction? fromOffTag(final String? offTag) => 52 | OffTagged.fromOffTag(offTag, KnowledgePanelAction.values) 53 | as KnowledgePanelAction?; 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/utils/too_many_requests_manager.dart: -------------------------------------------------------------------------------- 1 | /// Manager dedicated to "too many requests" server response. 2 | /// 3 | /// Typically, the server may limit the number of requests to a [maxCount] 4 | /// during a specific [duration]. 5 | class TooManyRequestsManager { 6 | TooManyRequestsManager({ 7 | required this.maxCount, 8 | required this.duration, 9 | }); 10 | 11 | final int maxCount; 12 | final Duration duration; 13 | 14 | final List _requestTimestamps = []; 15 | 16 | /// Waits the needed duration in order to avoid "too many requests" error. 17 | Future waitIfNeeded() async { 18 | while (_requestTimestamps.length >= maxCount) { 19 | final int previousInMillis = _requestTimestamps.first; 20 | final int nowInMillis = DateTime.now().millisecondsSinceEpoch; 21 | final int waitingInMillis = 22 | duration.inMilliseconds - nowInMillis + previousInMillis; 23 | if (waitingInMillis > 0) { 24 | await Future.delayed(Duration(milliseconds: waitingInMillis)); 25 | } 26 | _requestTimestamps.removeAt(0); 27 | } 28 | final DateTime now = DateTime.now(); 29 | final int nowInMillis = now.millisecondsSinceEpoch; 30 | _requestTimestamps.add(nowInMillis); 31 | } 32 | } 33 | 34 | /// [TooManyRequestsManager] dedicated to "searchProducts" queries in PROD. 35 | final TooManyRequestsManager searchProductsTooManyRequestsManager = 36 | TooManyRequestsManager( 37 | maxCount: 10, 38 | duration: Duration(minutes: 1), 39 | ); 40 | 41 | /// [TooManyRequestsManager] dedicated to "getProduct" queries in PROD. 42 | final TooManyRequestsManager getProductTooManyRequestsManager = 43 | TooManyRequestsManager( 44 | maxCount: 100, 45 | duration: Duration(minutes: 1), 46 | ); 47 | -------------------------------------------------------------------------------- /lib/src/search/taxonomy_name.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | /// Taxonomy Name for Elastic Search API. 4 | /// 5 | /// cf. https://github.com/openfoodfacts/search-a-licious/blob/main/data/config/openfoodfacts.yml 6 | enum TaxonomyName implements OffTagged { 7 | category(offTag: 'category'), 8 | label(offTag: 'label'), 9 | additive(offTag: 'additive'), 10 | allergen(offTag: 'allergen'), 11 | aminoAcid(offTag: 'amino_acid'), 12 | country(offTag: 'country'), 13 | dataQuality(offTag: 'data_quality'), 14 | foodGroup(offTag: 'food_group'), 15 | improvement(offTag: 'improvement'), 16 | ingredient(offTag: 'ingredient'), 17 | ingredientAnalysis(offTag: 'ingredients_analysis'), 18 | ingredientProcessing(offTag: 'ingredients_processing'), 19 | language(offTag: 'language'), 20 | mineral(offTag: 'mineral'), 21 | misc(offTag: 'misc'), 22 | novaGroup(offTag: 'nova_group'), 23 | nucleotide(offTag: 'nucleotide'), 24 | nutrient(offTag: 'nutrient'), 25 | origin(offTag: 'origin'), 26 | otherNutritionalSubstance(offTag: 'other_nutritional_substance'), 27 | packagingMaterial(offTag: 'packaging_material'), 28 | packagingRecycling(offTag: 'packaging_recycling'), 29 | packagingShape(offTag: 'packaging_shape'), 30 | periodsAfterOpening(offTag: 'periods_after_opening'), 31 | preservation(offTag: 'preservation'), 32 | state(offTag: 'state'), 33 | vitamin(offTag: 'vitamin'), 34 | brand(offTag: 'brand'); 35 | 36 | const TaxonomyName({ 37 | required this.offTag, 38 | }); 39 | 40 | @override 41 | final String offTag; 42 | 43 | /// Returns the [TaxonomyName] that matches the [offTag]. 44 | static TaxonomyName? fromOffTag(String? offTag) => 45 | OffTagged.fromOffTag(offTag, TaxonomyName.values) as TaxonomyName?; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/search/taxonomy_name_autocompleter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../model/user.dart'; 4 | import '../open_food_search_api_client.dart'; 5 | import '../utils/autocompleter.dart'; 6 | import '../utils/language_helper.dart'; 7 | import '../utils/open_food_api_configuration.dart'; 8 | import '../utils/uri_helper.dart'; 9 | import 'autocomplete_search_result.dart'; 10 | import 'autocomplete_single_result.dart'; 11 | import 'fuzziness.dart'; 12 | import 'taxonomy_name.dart'; 13 | 14 | /// Autocomplete suggestions for [TaxonomyName]s. 15 | class TaxonomyNameAutocompleter implements Autocompleter { 16 | const TaxonomyNameAutocompleter({ 17 | required this.taxonomyNames, 18 | required this.language, 19 | this.limit = 25, 20 | this.uriHelper = uriHelperFoodProd, 21 | this.user, 22 | this.fuzziness = Fuzziness.none, 23 | }); 24 | 25 | final List taxonomyNames; 26 | final OpenFoodFactsLanguage language; 27 | final int limit; 28 | final UriProductHelper uriHelper; 29 | final User? user; 30 | final Fuzziness fuzziness; 31 | 32 | @override 33 | Future> getSuggestions( 34 | final String input, 35 | ) async { 36 | final AutocompleteSearchResult results = 37 | await OpenFoodSearchAPIClient.autocomplete( 38 | language: language, 39 | query: input, 40 | taxonomyNames: taxonomyNames, 41 | size: limit, 42 | user: user, 43 | uriHelper: uriHelper, 44 | fuzziness: fuzziness, 45 | ); 46 | final List result = []; 47 | if (results.options == null) { 48 | return result; 49 | } 50 | for (final AutocompleteSingleResult item in results.options!) { 51 | result.add(item.text); 52 | } 53 | return result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/model/ecoscore_data.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ecoscore_data.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | EcoscoreData _$EcoscoreDataFromJson(Map json) => EcoscoreData( 10 | grade: json['grade'] as String?, 11 | score: JsonObject.parseDouble(json['score']), 12 | status: $enumDecodeNullable(_$EcoscoreStatusEnumMap, json['status']), 13 | agribalyse: json['agribalyse'] == null 14 | ? null 15 | : Agribalyse.fromJson(json['agribalyse'] as Map), 16 | adjustments: json['adjustments'] == null 17 | ? null 18 | : EcoscoreAdjustments.fromJson( 19 | json['adjustments'] as Map), 20 | missingDataWarning: json['missing_data_warning'] == null 21 | ? false 22 | : JsonObject.parseBool(json['missing_data_warning']), 23 | ); 24 | 25 | Map _$EcoscoreDataToJson(EcoscoreData instance) => 26 | { 27 | if (instance.grade case final value?) 'grade': value, 28 | if (instance.score case final value?) 'score': value, 29 | if (_$EcoscoreStatusEnumMap[instance.status] case final value?) 30 | 'status': value, 31 | if (instance.agribalyse?.toJson() case final value?) 'agribalyse': value, 32 | if (instance.adjustments?.toJson() case final value?) 33 | 'adjustments': value, 34 | 'missing_data_warning': instance.missingDataWarning, 35 | }; 36 | 37 | const _$EcoscoreStatusEnumMap = { 38 | EcoscoreStatus.KNOWN: 'known', 39 | EcoscoreStatus.UNKNOWN: 'unknown', 40 | }; 41 | -------------------------------------------------------------------------------- /lib/src/utils/autocomplete_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | import 'autocompleter.dart'; 5 | 6 | /// Manager that returns the suggestions for the latest input. 7 | /// 8 | /// Typical use case: the user types one character (which triggers a call to 9 | /// suggestions), then another character (which triggers another call). 10 | /// What if the second call is much faster than the first one, for server or 11 | /// connection reasons? The autocomplete widget will get the second suggestions, 12 | /// then the first suggestions will override them. 13 | /// And the user should get the suggestions that match the latest input. 14 | class AutocompleteManager implements Autocompleter { 15 | AutocompleteManager(this.autocompleter); 16 | 17 | final Autocompleter autocompleter; 18 | 19 | final List _inputs = []; 20 | final Map> _cache = >{}; 21 | 22 | @override 23 | Future> getSuggestions( 24 | final String input, 25 | ) async { 26 | _inputs.add(input); 27 | final List? cached = _cache[input]; 28 | if (cached != null) { 29 | return cached; 30 | } 31 | await waitForTestPurpose(); 32 | _cache[input] = await autocompleter.getSuggestions(input); 33 | // meanwhile there might have been some calls to this method, adding inputs. 34 | for (final String latestInput in _inputs.reversed) { 35 | final List? cached = _cache[latestInput]; 36 | if (cached != null) { 37 | return cached; 38 | } 39 | } 40 | // not supposed to happen, as we should have downloaded for "input". 41 | return []; 42 | } 43 | 44 | @protected 45 | @visibleForTesting 46 | Future waitForTestPurpose() async {} 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/model/robotoff_nutrient_extraction_annotation.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'robotoff_nutrient_extraction_annotation.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | RobotoffNutrientAnnotationData _$RobotoffNutrientAnnotationDataFromJson( 10 | Map json) => 11 | RobotoffNutrientAnnotationData( 12 | unit: UnitHelper.stringToUnit(json['unit'] as String?), 13 | valueWithModifer: json['value'] as String, 14 | ); 15 | 16 | Map _$RobotoffNutrientAnnotationDataToJson( 17 | RobotoffNutrientAnnotationData instance) => 18 | { 19 | 'unit': UnitHelper.unitToString(instance.unit), 20 | 'value': instance.valueWithModifer, 21 | }; 22 | 23 | RobotoffNutrientAnnotation _$RobotoffNutrientAnnotationFromJson( 24 | Map json) => 25 | RobotoffNutrientAnnotation( 26 | nutrients: (json['nutrients'] as Map?)?.map( 27 | (k, e) => MapEntry(k, 28 | RobotoffNutrientAnnotationData.fromJson(e as Map)), 29 | ), 30 | servingSize: json['serving_size'] as String?, 31 | perSize: PerSize.fromOffTag(json['nutrition_data_per'] as String?), 32 | ); 33 | 34 | Map _$RobotoffNutrientAnnotationToJson( 35 | RobotoffNutrientAnnotation instance) => 36 | { 37 | 'nutrients': instance.nutrients, 38 | 'serving_size': instance.servingSize, 39 | 'nutrition_data_per': _$PerSizeEnumMap[instance.perSize], 40 | }; 41 | 42 | const _$PerSizeEnumMap = { 43 | PerSize.serving: 'serving', 44 | PerSize.oneHundredGrams: 'oneHundredGrams', 45 | }; 46 | -------------------------------------------------------------------------------- /lib/src/prices/update_price_parameters.dart: -------------------------------------------------------------------------------- 1 | import '../interface/json_object.dart'; 2 | import 'currency.dart'; 3 | import 'discount_type.dart'; 4 | import 'get_parameters_helper.dart'; 5 | import 'price_per.dart'; 6 | 7 | /// Parameters for the "update price" API query. 8 | /// 9 | /// cf. https://prices.openfoodfacts.org/api/docs 10 | class UpdatePriceParameters extends JsonObject { 11 | /// Price of the product, without its currency, taxes included. 12 | num? price; 13 | 14 | /// True if the price is discounted. 15 | bool? priceIsDiscounted; 16 | 17 | /// Price of the product, without discount, taxes included. 18 | num? priceWithoutDiscount; 19 | 20 | /// Discount Type. 21 | DiscountType? discountType; 22 | 23 | /// Price per unit, kilogram, ..? 24 | PricePer? pricePer; 25 | 26 | /// Currency of the price. 27 | Currency? currency; 28 | 29 | /// Date when the product was bought. 30 | DateTime? date; 31 | 32 | /// Receipt's price quantity (user input). 33 | int? receiptQuantity; 34 | 35 | @override 36 | Map toJson() => { 37 | if (pricePer != null) 'price_per': pricePer!.offTag, 38 | if (priceIsDiscounted != null) 'price_is_discounted': priceIsDiscounted, 39 | if (priceIsDiscounted == false) 40 | 'discount_type': null 41 | else if (discountType != null) 42 | 'discount_type': discountType!.offTag, 43 | if (priceIsDiscounted == false) 44 | 'price_without_discount': null 45 | else if (priceWithoutDiscount != null) 46 | 'price_without_discount': priceWithoutDiscount, 47 | if (price != null) 'price': price, 48 | if (currency != null) 'currency': currency!.name, 49 | if (date != null) 'date': GetParametersHelper.formatDate(date!), 50 | if (receiptQuantity != null) 'receipt_quantity': receiptQuantity, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/model/spelling_corrections.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | 4 | part 'spelling_corrections.g.dart'; 5 | 6 | @JsonSerializable() 7 | class SpellingCorrection extends JsonObject { 8 | @JsonKey(includeIfNull: false) 9 | String? corrected; 10 | @JsonKey(name: 'text') 11 | String? input; 12 | @JsonKey(name: 'corrections', includeIfNull: false) 13 | List? termCorrections; 14 | 15 | SpellingCorrection(this.corrected, this.input, this.termCorrections); 16 | 17 | factory SpellingCorrection.fromJson(Map json) => 18 | _$SpellingCorrectionFromJson(json); 19 | 20 | @override 21 | Map toJson() => _$SpellingCorrectionToJson(this); 22 | } 23 | 24 | @JsonSerializable() 25 | class TermCorrections extends JsonObject { 26 | @JsonKey(name: 'term_corrections') 27 | List? corrections; 28 | @JsonKey() 29 | double? score; 30 | 31 | TermCorrections(this.corrections, this.score); 32 | 33 | factory TermCorrections.fromJson(Map json) => 34 | _$TermCorrectionsFromJson(json); 35 | 36 | @override 37 | Map toJson() => _$TermCorrectionsToJson(this); 38 | } 39 | 40 | @JsonSerializable() 41 | class Correction extends JsonObject { 42 | @JsonKey(includeIfNull: false) 43 | String? correction; 44 | @JsonKey() 45 | String? original; 46 | @JsonKey(name: 'start_offset') 47 | int? startOffset; 48 | @JsonKey(name: 'end_offset') 49 | int? endOffset; 50 | @JsonKey(name: 'is_valid') 51 | bool? isValid; 52 | 53 | Correction(this.correction, this.original, this.startOffset, this.endOffset, 54 | this.isValid); 55 | 56 | factory Correction.fromJson(Map json) => 57 | _$CorrectionFromJson(json); 58 | 59 | @override 60 | Map toJson() => _$CorrectionToJson(this); 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/model/ordered_nutrients.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import '../interface/json_object.dart'; 3 | import 'ordered_nutrient.dart'; 4 | 5 | part 'ordered_nutrients.g.dart'; 6 | 7 | /// Nutrients, as hierarchically ordered and localized entities. 8 | /// 9 | /// cf. https://github.com/openfoodfacts/openfoodfacts-dart/issues/210 10 | /// Example in https://fr.openfoodfacts.org/cgi/nutrients.pl 11 | /// Compared to [OrderedNutrient], this is the root of the structure. 12 | @JsonSerializable() 13 | class OrderedNutrients extends JsonObject { 14 | /// Most important nutrients (level 0 in the hierarchy) 15 | @JsonKey() 16 | final List nutrients; 17 | 18 | OrderedNutrients({required this.nutrients}); 19 | 20 | factory OrderedNutrients.fromJson( 21 | Map json, { 22 | final bool excludeReadOnly = true, 23 | }) { 24 | final OrderedNutrients result = _$OrderedNutrientsFromJson(json); 25 | if (excludeReadOnly == false) { 26 | return result; 27 | } 28 | _excludeReadOnly(result.nutrients); 29 | return result; 30 | } 31 | 32 | // cf. https://github.com/openfoodfacts/openfoodfacts-dart/issues/1160 33 | static void _excludeReadOnly(final List? orderedNutrients) { 34 | if (orderedNutrients == null) { 35 | return; 36 | } 37 | final List remove = []; 38 | for (int i = 0; i < orderedNutrients.length; i++) { 39 | final OrderedNutrient orderedNutrient = orderedNutrients[i]; 40 | if (orderedNutrient.id.startsWith('nutrition')) { 41 | remove.add(i); 42 | } else { 43 | _excludeReadOnly(orderedNutrient.subNutrients); 44 | } 45 | } 46 | for (int i = remove.length - 1; i >= 0; i--) { 47 | orderedNutrients.removeAt(remove[i]); 48 | } 49 | } 50 | 51 | @override 52 | Map toJson() => _$OrderedNutrientsToJson(this); 53 | } 54 | -------------------------------------------------------------------------------- /MIGRATIONS.md: -------------------------------------------------------------------------------- 1 | ## Migrating from 2.x.x to 3.x.x (breaking changes) 2 | 3 | - Starting with version 3.0.0, we now enforce all clients to provide a valid user agent. 4 | For this, please ensure to set the SDK before using any other functionality: 5 | 6 | ```dart 7 | OpenFoodAPIConfiguration.userAgent = UserAgent( 8 | name: '', 9 | ); 10 | ``` 11 | 12 | - `QueryType` has been deleted. Now, for API calls you have to provide a `UriProductHelper` parameter. By default it will point you to openfoodfacts/prod. 13 | 14 | - For `RobotoffAPIClient.getRandomInsights` and `RobotoffAPIClient.getQuestions`, a list of countries instead of a single country as parameter. 15 | 16 | - Use `OpenFoodFactsCountry.fromOffTag` instead of `CountryHelper.fromJson`. 17 | 18 | - `OpenFoodAPIClient.getOrderedNutrients` now uses a `OpenFoodFactsCountry` parameter instead of a 2-letter country code. 19 | 20 | - Methods `getProductImageRootUrl` and `getBarcodeSubPath` are moved to `UriProductHelper` from `ImageHelper` 21 | 22 | - Method `buildUrl` renamed as `getLocalizedProductImageUrl` in `ImageHelper` 23 | 24 | - Removal of deprecated code. 25 | 26 | ## Migrating from 1.x.x to 2.x.x (breaking changes) 27 | 28 | - Now the only entry point is `import 'package:openfoodfacts/openfoodfacts.dart';` 29 | - replace all your instances of `import 'package:openfoodfacts/...';` with a single `import 'package:openfoodfacts/openfoodfacts.dart';` 30 | - If you used `State` from `product_state.dart`, you have to rename it to `ProductState` 31 | - If you used `Level` from `nutrient_levels.dart`, you have to rename it to `NutrientLevel` 32 | - Removed deprecated classes: 33 | - `Page` 34 | - `ProductListQueryConfiguration` 35 | - `ToBeCompletedConfiguration` 36 | - Removed deprecated fields and methods in `Nutriments` 37 | - _all_ the single nutrient value _fields_ were removed - use `getValue` and `setValue` instead 38 | - instead of `getUnit` use `nutrient.typicalUnit` 39 | -------------------------------------------------------------------------------- /lib/src/utils/invalid_barcodes.dart: -------------------------------------------------------------------------------- 1 | import 'http_helper.dart'; 2 | 3 | /// Invalid barcode blacklist 4 | /// 5 | /// cf. https://github.com/openfoodfacts/openfoodfacts-dart/issues/188 6 | /// The barcode scanners will return invalid barcodes due to bright reflections, 7 | /// poor lighting, etc… While those barcodes are theoretically valid EAN-8 8 | /// barcodes, they are not attributed. We are thus putting in place a server 9 | /// side blacklist of common invalid barcodes, so that the app doesn't return a 10 | /// prompt to add a blacklisted product, or return a result for an invalid 11 | /// product once someone has created it. 12 | class InvalidBarcodes { 13 | /// JSON load constructor; may throw an exception. 14 | InvalidBarcodes.loadFromJSONString(final String jsonString) { 15 | final List inputJson = HttpHelper().jsonDecode(jsonString); 16 | for (final dynamic item in inputJson) { 17 | _barcodes.add(item as String); 18 | } 19 | if (_barcodes.isEmpty) { 20 | throw Exception( 21 | 'Unexpected error: empty invalid barcode list from json string $jsonString'); 22 | } 23 | } 24 | 25 | /// Lazy but 99.9% of the time good enough constructor 26 | InvalidBarcodes.base() { 27 | _barcodes.addAll(_INVALID_BARCODES); 28 | } 29 | 30 | final List _barcodes = []; 31 | 32 | /// Checks if a barcode is blacklisted 33 | bool isBlacklisted(final String barcode) => _barcodes.contains(barcode); 34 | 35 | /// Where the JSON file can be found. 36 | static String getUrl() => 37 | 'https://world.openfoodfacts.org/data/invalid-barcodes.json'; 38 | 39 | // TODO: values from URL as of 2022-07-12; check for changes once in a while 40 | static const List _INVALID_BARCODES = [ 41 | '323673', 42 | '2575405', 43 | '10232903', 44 | '10576403', 45 | '10836813', 46 | '12562213', 47 | '13297703', 48 | '15600703', 49 | '16130357', 50 | '16256163', 51 | '16256866', 52 | ]; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/model/robotoff_nutrient_extraction_annotation.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:openfoodfacts/src/model/nutrient_modifier.dart'; 3 | import 'package:openfoodfacts/src/model/per_size.dart'; 4 | import 'package:openfoodfacts/src/utils/unit_helper.dart'; 5 | 6 | part 'robotoff_nutrient_extraction_annotation.g.dart'; 7 | 8 | @JsonSerializable() 9 | class RobotoffNutrientAnnotationData { 10 | @JsonKey(toJson: UnitHelper.unitToString, fromJson: UnitHelper.stringToUnit) 11 | Unit? unit; 12 | @JsonKey(name: 'value') 13 | String valueWithModifer; 14 | 15 | RobotoffNutrientAnnotationData({ 16 | this.unit, 17 | required this.valueWithModifer, 18 | }); 19 | 20 | NutrientModifier? get modifier => 21 | NutrientModifier.fromValue(valueWithModifer); 22 | 23 | double? get value { 24 | final String trimmed = valueWithModifer.trim(); 25 | if (trimmed.isEmpty) { 26 | return null; 27 | } 28 | return modifier == null 29 | ? double.tryParse(trimmed) 30 | : double.tryParse(trimmed.substring(1)); 31 | } 32 | 33 | factory RobotoffNutrientAnnotationData.fromJson(Map json) => 34 | _$RobotoffNutrientAnnotationDataFromJson(json); 35 | 36 | Map toJson() => _$RobotoffNutrientAnnotationDataToJson(this); 37 | } 38 | 39 | @JsonSerializable() 40 | class RobotoffNutrientAnnotation { 41 | final Map? nutrients; 42 | @JsonKey(name: 'serving_size') 43 | final String? servingSize; 44 | @JsonKey(name: 'nutrition_data_per', fromJson: PerSize.fromOffTag) 45 | final PerSize? perSize; 46 | 47 | const RobotoffNutrientAnnotation({ 48 | this.nutrients, 49 | this.servingSize, 50 | this.perSize, 51 | }); 52 | 53 | factory RobotoffNutrientAnnotation.fromJson(Map json) => 54 | _$RobotoffNutrientAnnotationFromJson(json); 55 | 56 | Map toJson() => _$RobotoffNutrientAnnotationToJson(this); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/utils/unit_helper.dart: -------------------------------------------------------------------------------- 1 | import '../model/off_tagged.dart'; 2 | 3 | /// Unit of measurement for nutrients 4 | enum Unit implements OffTagged { 5 | KCAL('kcal'), 6 | KJ('kj'), 7 | G('g'), 8 | MILLI_G('mg'), 9 | MICRO_G('mcg'), 10 | MILLI_L('ml'), 11 | L('liter'), 12 | PERCENT('percent'), 13 | // actually we don't expect a specific offTag for "unknown". 14 | UNKNOWN('unknown'), 15 | PERCENT_DV('% DV'), 16 | IU('IU'); 17 | 18 | const Unit(this.offTag); 19 | 20 | @override 21 | final String offTag; 22 | } 23 | 24 | /// Helper class for conversions to/from [Unit] 25 | class UnitHelper { 26 | /// Maps alternate unit spellings to a [Unit] 27 | static const Map _ALTERNATE_UNITS = { 28 | 'kCal': Unit.KCAL, 29 | 'KCal': Unit.KCAL, 30 | 'Kj': Unit.KJ, 31 | 'kJ': Unit.KJ, 32 | 'KJ': Unit.KJ, 33 | 'G': Unit.G, 34 | 'milli-gram': Unit.MILLI_G, 35 | 'mG': Unit.MILLI_G, 36 | 'µg': Unit.MICRO_G, 37 | 'µg': Unit.MICRO_G, 38 | 'µg': Unit.MICRO_G, 39 | 'µg': Unit.MICRO_G, 40 | 'mL': Unit.MILLI_L, 41 | 'Ml': Unit.MILLI_L, 42 | 'ML': Unit.MILLI_L, 43 | 'milli-liter': Unit.MILLI_L, 44 | 'L': Unit.L, 45 | 'l': Unit.L, 46 | '%': Unit.PERCENT, 47 | 'per cent': Unit.PERCENT, 48 | 'μg': Unit.MICRO_G, 49 | }; 50 | 51 | /// Returns a unit spelling corresponding to the type of [unit] 52 | static String? unitToString(Unit? unit) => unit?.offTag; 53 | 54 | /// Returns the [Unit] described by [s] 55 | static Unit? stringToUnit(String? s) { 56 | if (s == null || s.isEmpty) { 57 | return null; 58 | } 59 | 60 | if (s[0] == String.fromCharCode(0x03BC)) { 61 | // greek letter mu 62 | if (s.length > 1 && s.substring(1) == 'g') { 63 | return Unit.MICRO_G; 64 | } 65 | return Unit.UNKNOWN; 66 | } 67 | 68 | return OffTagged.fromOffTag(s, Unit.values) as Unit? ?? 69 | _ALTERNATE_UNITS[s] ?? 70 | Unit.UNKNOWN; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/prices/get_challenges_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'get_challenges_order.dart'; 2 | import 'get_parameters_helper.dart'; 3 | 4 | /// Parameters for the "get challenges" API query. 5 | /// 6 | /// cf. https://prices.openfoodfacts.org/api/docs 7 | class GetChallengesParameters 8 | extends GetParametersHelper { 9 | /// Possible values for [status]. 10 | static const String statusDraft = 'DRAFT'; 11 | static const String statusUpcoming = 'UPCOMING'; 12 | static const String statusOngoing = 'ONGOING'; 13 | static const String statusCompleted = 'COMPLETED'; 14 | 15 | DateTime? endDateGt; 16 | DateTime? endDateGte; 17 | DateTime? endDateLt; 18 | DateTime? endDateLte; 19 | int? endDateMonth; 20 | int? endDateYear; 21 | int? id; 22 | bool? isPublished; 23 | DateTime? startDateGt; 24 | DateTime? startDateGte; 25 | DateTime? startDateLt; 26 | DateTime? startDateLte; 27 | int? startDateMonth; 28 | int? startDateYear; 29 | String? status; 30 | 31 | @override 32 | Map getQueryParameters() { 33 | super.getQueryParameters(); 34 | addNonNullDate(endDateGt, 'end_date__gt', dayOnly: true); 35 | addNonNullDate(endDateGte, 'end_date__gte', dayOnly: true); 36 | addNonNullDate(endDateLt, 'end_date__lt', dayOnly: true); 37 | addNonNullDate(endDateLte, 'end_date__lte', dayOnly: true); 38 | addNonNullInt(endDateMonth, 'end_date__month'); 39 | addNonNullInt(endDateYear, 'end_date__year'); 40 | addNonNullInt(id, 'id'); 41 | addNonNullBool(isPublished, 'is_published'); 42 | addNonNullDate(startDateGt, 'start_date__gt', dayOnly: true); 43 | addNonNullDate(startDateGte, 'start_date__gte', dayOnly: true); 44 | addNonNullDate(startDateLt, 'start_date__lt', dayOnly: true); 45 | addNonNullDate(startDateLte, 'start_date__lte', dayOnly: true); 46 | addNonNullInt(startDateMonth, 'start_date__month'); 47 | addNonNullInt(startDateYear, 'start_date__year'); 48 | addNonNullString(status, 'status'); 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/model/allergens.dart: -------------------------------------------------------------------------------- 1 | import 'off_tagged.dart'; 2 | 3 | /// Main allergens. 4 | enum AllergensTag implements OffTagged { 5 | GLUTEN(offTag: 'en:gluten'), 6 | MILK(offTag: 'en:milk'), 7 | EGGS(offTag: 'en:eggs'), 8 | NUTS(offTag: 'en:nuts'), 9 | PEANUTS(offTag: 'en:peanuts'), 10 | SESAME_SEEDS(offTag: 'en:sesame-seeds'), 11 | SOYBEANS(offTag: 'en:soybeans'), 12 | CELERY(offTag: 'en:celery'), 13 | MUSTARD(offTag: 'en:mustard'), 14 | LUPIN(offTag: 'en:lupin'), 15 | FISH(offTag: 'en:fish'), 16 | CRUSTACEANS(offTag: 'en:crustaceans'), 17 | MOLLUSCS(offTag: 'en:molluscs'), 18 | SULPHUR_DIOXIDE_AND_SULPHITES(offTag: 'en:sulphur-dioxide-and-sulphites'); 19 | 20 | const AllergensTag({ 21 | required this.offTag, 22 | }); 23 | 24 | @override 25 | final String offTag; 26 | } 27 | 28 | /// List of known allergens for a [Product]. 29 | /// 30 | /// If we are lucky, we get values that match with [AllergensTag]. 31 | /// If we are less lucky, we have more exotic values. 32 | class Allergens { 33 | /// Allergen id formatted as 'en:gluten', like in [AllergensTag] tag. 34 | List ids; 35 | 36 | /// Allergen name formatted as 'gluten' 37 | List names; 38 | 39 | Allergens(this.ids, this.names); 40 | 41 | static Allergens allergensFromJson(List? json) { 42 | List ids = []; 43 | List names = []; 44 | 45 | if (json == null) { 46 | return Allergens(ids, names); 47 | } 48 | 49 | for (int i = 0; i < json.length; i++) { 50 | ids.add(json[i].toString()); 51 | String name = json[i].toString().substring(3); // remove the 'en:' header. 52 | names.add(name); 53 | } 54 | 55 | return Allergens(ids, names); 56 | } 57 | 58 | static List? allergensToJson(Allergens? allergens) { 59 | List result = []; 60 | 61 | if (allergens == null) { 62 | return null; 63 | } 64 | 65 | for (int i = 0; i < allergens.ids.length; i++) { 66 | result.add(allergens.ids[i].toString()); 67 | } 68 | 69 | return result; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/prices/challenge.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'challenge.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Challenge _$ChallengeFromJson(Map json) => Challenge() 10 | ..id = (json['id'] as num?)?.toInt() 11 | ..locations = (json['locations'] as List?) 12 | ?.map((e) => Location.fromJson(e as Map)) 13 | .toList() 14 | ..status = json['status'] as String? 15 | ..tag = json['tag'] as String? 16 | ..title = json['title'] as String? 17 | ..icon = json['icon'] as String? 18 | ..subtitle = json['subtitle'] as String? 19 | ..startDate = JsonHelper.stringTimestampToDate(json['start_date']) 20 | ..endDate = JsonHelper.stringTimestampToDate(json['end_date']) 21 | ..categories = 22 | (json['categories'] as List?)?.map((e) => e as String).toList() 23 | ..exampleProofUrl = json['example_proof_url'] as String? 24 | ..isPublished = json['is_published'] as bool? 25 | ..stats = json['stats'] 26 | ..created = JsonHelper.stringTimestampToDate(json['created']) 27 | ..updated = JsonHelper.nullableStringTimestampToDate(json['updated']); 28 | 29 | Map _$ChallengeToJson(Challenge instance) => { 30 | 'id': instance.id, 31 | 'locations': instance.locations, 32 | 'status': instance.status, 33 | 'tag': instance.tag, 34 | 'title': instance.title, 35 | 'icon': instance.icon, 36 | 'subtitle': instance.subtitle, 37 | 'start_date': instance.startDate?.toIso8601String(), 38 | 'end_date': instance.endDate?.toIso8601String(), 39 | 'categories': instance.categories, 40 | 'example_proof_url': instance.exampleProofUrl, 41 | 'is_published': instance.isPublished, 42 | 'stats': instance.stats, 43 | 'created': instance.created?.toIso8601String(), 44 | 'updated': instance.updated?.toIso8601String(), 45 | }; 46 | -------------------------------------------------------------------------------- /lib/src/utils/nutriments_helper.dart: -------------------------------------------------------------------------------- 1 | import '../model/nutrient.dart'; 2 | import '../model/nutriments.dart'; 3 | import '../model/per_size.dart'; 4 | 5 | // TODO: rename as NormalizedEnergyHelper or fix typo as NutrientHelper 6 | /// Helper class for energy computations and checks 7 | class NutrimentsHelper { 8 | static const double _kcalToKJFactor = 4.1868; 9 | 10 | static double fromKCalToKJ(final double kCal) => kCal * _kcalToKJFactor; 11 | 12 | static double fromKJtoKCal(final double kJ) => kJ / _kcalToKJFactor; 13 | 14 | /// Calculates the energy for 100g in kJ. 15 | /// ! should be used cautiously (might not be displayed to the end user) ! 16 | /// source : https://en.wikipedia.org/wiki/Food_energy 17 | static double? calculateEnergy( 18 | Nutriments nutriments, { 19 | PerSize perSize = PerSize.oneHundredGrams, 20 | }) { 21 | double? fat = nutriments.getValue(Nutrient.fat, perSize); 22 | double? carbs = nutriments.getValue(Nutrient.carbohydrates, perSize); 23 | double? proteins = nutriments.getValue(Nutrient.proteins, perSize); 24 | double? fiber = nutriments.getValue(Nutrient.fiber, perSize); 25 | 26 | if (fat == null || carbs == null || proteins == null || fiber == null) { 27 | return null; 28 | } 29 | 30 | return (fat * 37 + carbs * 17 + proteins * 17 + fiber * 8); 31 | } 32 | 33 | /// Checks if the stated energy value is within a margin of error 34 | /// a use case for this is before saving a product, check if the values aren't 35 | /// incoherent. 36 | static bool checkEnergyCoherence( 37 | Nutriments nutriments, 38 | double marginPercentage, { 39 | PerSize perSize = PerSize.oneHundredGrams, 40 | }) { 41 | final double statedEnergy = nutriments.getComputedKJ(perSize)!; 42 | 43 | double lowLimit = 44 | statedEnergy - (statedEnergy * (marginPercentage / 100.0)); 45 | double highLimit = 46 | statedEnergy + (statedEnergy * (marginPercentage / 100.0)); 47 | 48 | double calculatedEnergy = calculateEnergy(nutriments)!; 49 | 50 | return (calculatedEnergy >= lowLimit && calculatedEnergy <= highLimit); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/model/attribute_group.dart: -------------------------------------------------------------------------------- 1 | import '../interface/json_object.dart'; 2 | import 'attribute.dart'; 3 | 4 | class AttributeGroup extends JsonObject { 5 | AttributeGroup({ 6 | this.id, 7 | this.name, 8 | this.warning, 9 | this.attributes, 10 | }); 11 | 12 | factory AttributeGroup.fromJson(dynamic json) => AttributeGroup( 13 | id: json[_JSON_TAG_ID] as String?, 14 | name: json[_JSON_TAG_NAME] as String?, 15 | warning: json[_JSON_TAG_WARNING] as String?, 16 | attributes: (json[_JSON_TAG_ATTRIBUTES] as List?) 17 | ?.map((item) => Attribute.fromJson(item)) 18 | .toList(), 19 | ); 20 | 21 | @override 22 | Map toJson() => JsonObject.removeNullEntries({ 23 | _JSON_TAG_ID: id, 24 | _JSON_TAG_NAME: name, 25 | _JSON_TAG_WARNING: warning, 26 | _JSON_TAG_ATTRIBUTES: _listToJson(), 27 | }); 28 | 29 | static const String _JSON_TAG_ID = 'id'; 30 | static const String _JSON_TAG_NAME = 'name'; 31 | static const String _JSON_TAG_WARNING = 'warning'; 32 | static const String _JSON_TAG_ATTRIBUTES = 'attributes'; 33 | 34 | final String? id; 35 | final String? name; 36 | final String? warning; 37 | final List? attributes; 38 | 39 | @override 40 | String toString() => 'AttributeGroup(${toJson()})'; 41 | 42 | List>? _listToJson() { 43 | if (attributes == null || attributes!.isEmpty) { 44 | return null; 45 | } 46 | final List> result = []; 47 | for (final Attribute item in attributes!) { 48 | result.add(item.toJson()); 49 | } 50 | return result; 51 | } 52 | 53 | static const String ATTRIBUTE_GROUP_NUTRITIONAL_QUALITY = 54 | 'nutritional_quality'; 55 | static const String ATTRIBUTE_GROUP_PROCESSING = 'processing'; 56 | static const String ATTRIBUTE_GROUP_ALLERGENS = 'allergens'; 57 | static const String ATTRIBUTE_GROUP_INGREDIENT_ANALYSIS = 58 | 'ingredients_analysis'; 59 | static const String ATTRIBUTE_GROUP_LABELS = 'labels'; 60 | static const String ATTRIBUTE_GROUP_ENVIRONMENT = 'environment'; 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/model/spelling_corrections.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'spelling_corrections.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SpellingCorrection _$SpellingCorrectionFromJson(Map json) => 10 | SpellingCorrection( 11 | json['corrected'] as String?, 12 | json['text'] as String?, 13 | (json['corrections'] as List?) 14 | ?.map((e) => TermCorrections.fromJson(e as Map)) 15 | .toList(), 16 | ); 17 | 18 | Map _$SpellingCorrectionToJson(SpellingCorrection instance) => 19 | { 20 | if (instance.corrected case final value?) 'corrected': value, 21 | 'text': instance.input, 22 | if (instance.termCorrections case final value?) 'corrections': value, 23 | }; 24 | 25 | TermCorrections _$TermCorrectionsFromJson(Map json) => 26 | TermCorrections( 27 | (json['term_corrections'] as List?) 28 | ?.map((e) => Correction.fromJson(e as Map)) 29 | .toList(), 30 | (json['score'] as num?)?.toDouble(), 31 | ); 32 | 33 | Map _$TermCorrectionsToJson(TermCorrections instance) => 34 | { 35 | 'term_corrections': instance.corrections, 36 | 'score': instance.score, 37 | }; 38 | 39 | Correction _$CorrectionFromJson(Map json) => Correction( 40 | json['correction'] as String?, 41 | json['original'] as String?, 42 | (json['start_offset'] as num?)?.toInt(), 43 | (json['end_offset'] as num?)?.toInt(), 44 | json['is_valid'] as bool?, 45 | ); 46 | 47 | Map _$CorrectionToJson(Correction instance) => 48 | { 49 | if (instance.correction case final value?) 'correction': value, 50 | 'original': instance.original, 51 | 'start_offset': instance.startOffset, 52 | 'end_offset': instance.endOffset, 53 | 'is_valid': instance.isValid, 54 | }; 55 | -------------------------------------------------------------------------------- /lib/src/utils/recommended_daily_intake_helper.dart: -------------------------------------------------------------------------------- 1 | class RecommendedDailyIntakeHelper { 2 | // There is an issue where the plugin cannot access the assets/json/recommended_daily_intakes_source _eu.json file. This is used as a replacement while the problem is being fixed. 3 | static final Map _euRecommendations = { 4 | 'energy_kcal': {'unit': 'kcal', 'value': 2000}, 5 | 'energy_kJ': {'unit': 'kJ', 'value': 8400}, 6 | 'fat': {'unit': 'g', 'value': 70}, 7 | 'saturated-fat': {'unit': 'g', 'value': 20}, 8 | 'carbohydrates': {'unit': 'g', 'value': 260}, 9 | 'sugars': {'unit': 'g', 'value': 90}, 10 | 'proteins': {'unit': 'g', 'value': 50}, 11 | 'sodium': {'unit': 'g', 'value': 6}, 12 | 'vitamin-a': {'unit': 'μg', 'value': 800}, 13 | 'vitamin-d': {'unit': 'μg', 'value': 5}, 14 | 'vitamin-e': {'unit': 'mg', 'value': 12}, 15 | 'vitamin-k': {'unit': 'μg', 'value': 75}, 16 | 'vitamin-c': {'unit': 'mg', 'value': 80}, 17 | 'vitamin-b1': {'unit': 'mg', 'value': 1.1}, 18 | 'vitamin-b2': {'unit': 'mg', 'value': 1.4}, 19 | 'vitamin-b3': {'unit': 'mg', 'value': 16}, 20 | 'vitamin-b6': {'unit': 'mg', 'value': 1.4}, 21 | 'vitamin-b9': {'unit': 'μg', 'value': 200}, 22 | 'vitamin-b12': {'unit': 'μg', 'value': 2.5}, 23 | 'biotin': {'unit': 'μg', 'value': 50}, 24 | 'pantothenic-acid': {'unit': 'mg', 'value': 6}, 25 | 'potassium': {'unit': 'mg', 'value': 2000}, 26 | 'chloride': {'unit': 'mg', 'value': 800}, 27 | 'calcium': {'unit': 'mg', 'value': 800}, 28 | 'phosphorus': {'unit': 'mg', 'value': 700}, 29 | 'magnesium': {'unit': 'mg', 'value': 375}, 30 | 'iron': {'unit': 'mg', 'value': 14}, 31 | 'zinc': {'unit': 'mg', 'value': 10}, 32 | 'copper': {'unit': 'mg', 'value': 1}, 33 | 'manganese': {'unit': 'mg', 'value': 2}, 34 | 'fluoride': {'unit': 'mg', 'value': 3.5}, 35 | 'selenium': {'unit': 'μg', 'value': 55}, 36 | 'chromium': {'unit': 'μg', 'value': 40}, 37 | 'molybdenum': {'unit': 'μg', 'value': 50}, 38 | 'iodine': {'unit': 'μg', 'value': 150} 39 | }; 40 | 41 | static Map getEURecommendationsJson() { 42 | return _euRecommendations; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/model/nutrient_levels.dart: -------------------------------------------------------------------------------- 1 | import 'off_tagged.dart'; 2 | 3 | enum NutrientLevel implements OffTagged { 4 | LOW(offTag: 'low'), 5 | MODERATE(offTag: 'moderate'), 6 | HIGH(offTag: 'high'), 7 | UNDEFINED(offTag: 'undefined'); 8 | 9 | const NutrientLevel({ 10 | required this.offTag, 11 | }); 12 | 13 | @override 14 | final String offTag; 15 | 16 | /// Returns the first [NutrientLevel] that matches the [offTag]. 17 | static NutrientLevel? fromOffTag(final String? offTag) => 18 | OffTagged.fromOffTag(offTag, NutrientLevel.values) as NutrientLevel?; 19 | } 20 | 21 | extension NutrientLevelExtension on NutrientLevel? { 22 | String get value => (this ?? NutrientLevel.UNDEFINED).offTag; 23 | 24 | static NutrientLevel getLevel(String? s) => 25 | NutrientLevel.fromOffTag(s) ?? NutrientLevel.UNDEFINED; 26 | } 27 | 28 | class NutrientLevels { 29 | static const String NUTRIENT_SUGARS = 'sugars'; 30 | static const String NUTRIENT_FAT = 'fat'; 31 | static const String NUTRIENT_SATURATED_FAT = 'saturated-fat'; 32 | static const String NUTRIENT_SALT = 'salt'; 33 | 34 | static const List nutrients = [ 35 | NUTRIENT_SUGARS, 36 | NUTRIENT_FAT, 37 | NUTRIENT_SATURATED_FAT, 38 | NUTRIENT_SALT 39 | ]; 40 | 41 | Map levels; // "nutrient" : Level 42 | 43 | NutrientLevels(this.levels); 44 | 45 | static NutrientLevels fromJson(Map? json) { 46 | Map result = {}; 47 | 48 | if (json == null) { 49 | return NutrientLevels(result); 50 | } 51 | 52 | for (int i = 0; i < nutrients.length; i++) { 53 | var key = nutrients[i]; 54 | result[key] = NutrientLevelExtension.getLevel(json[key]); 55 | } 56 | 57 | return NutrientLevels(result); 58 | } 59 | 60 | static Map? toJson(NutrientLevels? nutrientLevels) { 61 | Map result = {}; 62 | 63 | if (nutrientLevels == null) { 64 | return null; 65 | } 66 | 67 | for (int i = 0; i < nutrientLevels.levels.length; i++) { 68 | var key = nutrients[i]; 69 | result[key] = nutrientLevels.levels[key].value; 70 | } 71 | 72 | return result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/prices/common_proof_parameters.dart: -------------------------------------------------------------------------------- 1 | import '../interface/json_object.dart'; 2 | import 'currency.dart'; 3 | import 'get_parameters_helper.dart'; 4 | import 'location_osm_type.dart'; 5 | import 'proof_type.dart'; 6 | 7 | /// Common parameters for the "upload and update proof" API queries. 8 | /// 9 | /// cf. https://prices.openfoodfacts.org/api/docs 10 | abstract class CommonProofParameters extends JsonObject { 11 | /// Proof type. 12 | ProofType? get type; 13 | 14 | /// Date when the product was bought. 15 | DateTime? date; 16 | 17 | /// Currency of the price. 18 | Currency? currency; 19 | 20 | /// ID of the location in OpenStreetMap. 21 | int? locationOSMId; 22 | 23 | /// Type of the OpenStreetMap location object. 24 | LocationOSMType? locationOSMType; 25 | 26 | /// Receipt's number of prices. 27 | int? receiptPriceCount; 28 | 29 | /// Receipt's total amount (user input). 30 | num? receiptPriceTotal; 31 | 32 | num? receiptOnlineDeliveryCosts; 33 | 34 | bool? readyForPriceTagValidation; 35 | 36 | bool? ownerConsumption; 37 | 38 | String? ownerComment; 39 | 40 | int? locationId; 41 | 42 | @override 43 | Map toJson() => { 44 | if (type != null) 'type': type!.offTag, 45 | if (date != null) 'date': GetParametersHelper.formatDate(date!), 46 | if (currency != null) 'currency': currency!.name, 47 | if (locationOSMId != null) 'location_osm_id': locationOSMId, 48 | if (locationOSMType != null) 49 | 'location_osm_type': locationOSMType!.offTag, 50 | if (receiptPriceCount != null) 'receipt_price_count': receiptPriceCount, 51 | if (receiptPriceTotal != null) 'receipt_price_total': receiptPriceTotal, 52 | if (receiptOnlineDeliveryCosts != null) 53 | 'receipt_online_delivery_costs': receiptOnlineDeliveryCosts, 54 | if (readyForPriceTagValidation != null) 55 | 'ready_for_price_tag_validation': readyForPriceTagValidation, 56 | if (ownerConsumption != null) 'owner_consumption': ownerConsumption, 57 | if (ownerComment != null) 'owner_comment': ownerComment, 58 | if (locationId != null) 'location_id': locationId, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/prices/get_prices_parameters.dart: -------------------------------------------------------------------------------- 1 | import 'contribution_kind.dart'; 2 | import 'currency.dart'; 3 | import 'get_parameters_helper.dart'; 4 | import 'get_prices_order.dart'; 5 | import 'location_osm_type.dart'; 6 | 7 | /// Parameters for the "get prices" API query. 8 | /// 9 | /// cf. https://prices.openfoodfacts.org/api/docs 10 | class GetPricesParameters extends GetParametersHelper { 11 | String? productCode; 12 | int? productId; 13 | bool? productIdIsNull; 14 | String? categoryTag; 15 | String? labelsTagsLike; 16 | String? originsTagsLike; 17 | int? locationOSMId; 18 | LocationOSMType? locationOSMType; 19 | int? locationId; 20 | Currency? currency; 21 | DateTime? date; 22 | DateTime? dateGt; 23 | DateTime? dateGte; 24 | DateTime? dateLt; 25 | DateTime? dateLte; 26 | int? proofId; 27 | String? owner; 28 | DateTime? createdGte; 29 | DateTime? createdLte; 30 | ContributionKind? kind; 31 | 32 | @override 33 | Map getQueryParameters() { 34 | super.getQueryParameters(); 35 | addNonNullString(productCode, 'product_code'); 36 | addNonNullInt(productId, 'product_id'); 37 | addNonNullBool(productIdIsNull, 'product_id__isnull'); 38 | addNonNullString(categoryTag, 'category_tag'); 39 | addNonNullString(labelsTagsLike, 'labels_tags__like'); 40 | addNonNullString(originsTagsLike, 'origins_tags__like'); 41 | addNonNullInt(locationOSMId, 'location_osm_id'); 42 | addNonNullString(locationOSMType?.offTag, 'location_osm_type'); 43 | addNonNullInt(locationId, 'location_id'); 44 | addNonNullString(currency?.name, 'currency'); 45 | addNonNullDate(date, 'date', dayOnly: true); 46 | addNonNullDate(dateGt, 'date__gt', dayOnly: true); 47 | addNonNullDate(dateGte, 'date__gte', dayOnly: true); 48 | addNonNullDate(dateLt, 'date__lt', dayOnly: true); 49 | addNonNullDate(dateLte, 'date__lte', dayOnly: true); 50 | addNonNullInt(proofId, 'proof_id'); 51 | addNonNullString(owner, 'owner'); 52 | addNonNullDate(createdGte, 'created__gte', dayOnly: false); 53 | addNonNullDate(createdLte, 'created__lte', dayOnly: false); 54 | addNonNullString(kind?.offTag, 'kind'); 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/prices/price_user.dart: -------------------------------------------------------------------------------- 1 | import '../interface/json_object.dart'; 2 | 3 | /// Price user object. 4 | /// 5 | /// cf. `User` in https://prices.openfoodfacts.org/api/docs 6 | class PriceUser extends JsonObject { 7 | final Map json; 8 | 9 | PriceUser(this.json); 10 | 11 | factory PriceUser.fromJson(Map json) => PriceUser(json); 12 | 13 | String get userId => json['user_id'] as String; 14 | 15 | /// Number of prices for this user. 16 | int? get priceCount => getInt('price_count'); 17 | 18 | /// Number of locations for this user. 19 | int? get locationCount => getInt('location_count'); 20 | 21 | /// Number of products for this user. 22 | int? get productCount => getInt('product_count'); 23 | 24 | /// Number of proofs for this user. 25 | int? get proofCount => getInt('proof_count'); 26 | 27 | /// Number of unique currencies in the user's price contributions 28 | int? get priceCurrencyCount => getInt('currency_count'); 29 | 30 | /// Number of price contributions based on category (Community or Consumption) 31 | int? get priceKindCommunityCount => getInt('price_kind_community_count'); 32 | 33 | int? get priceKindConsumptionCount => getInt('price_kind_consumption_count'); 34 | 35 | /// Number of proof contributions based on category (Community or Consumption) 36 | int? get proofKindCommunityCount => getInt('proof_kind_community_count'); 37 | 38 | int? get proofKindConsumptionCount => getInt('proof_kind_consumption_count'); 39 | 40 | int? get priceTypeProductCount => getInt('price_type_product_count'); 41 | 42 | int? get priceTypeCategoryCount => getInt('price_type_category_count'); 43 | 44 | int? get priceInProofOwnedCount => getInt('price_in_proof_owned_count'); 45 | 46 | int? get priceInProofNotOwnedCount => 47 | getInt('price_in_proof_not_owned_count'); 48 | 49 | int? get priceNotOwnedInProofOwnedCount => 50 | getInt('price_not_owned_in_proof_owned_count'); 51 | 52 | int? get locationTypeOsmCountryCount => 53 | getInt('location_type_osm_country_count'); 54 | 55 | int? getInt(String key) => json.containsKey(key) ? json[key] as int? : null; 56 | 57 | @override 58 | Map toJson() => json; 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/model/ingredient.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ingredient.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Ingredient _$IngredientFromJson(Map json) => Ingredient( 10 | rank: JsonObject.parseInt(json['rank']), 11 | id: json['id'] as String?, 12 | text: json['text'] as String?, 13 | isInTaxonomy: JsonObject.parseBool(json['is_in_taxonomy']), 14 | percent: JsonObject.parseDouble(json['percent']), 15 | percentEstimate: JsonObject.parseDouble(json['percent_estimate']), 16 | vegan: ingredientSpecialPropertyStatusFromJson(json['vegan']), 17 | vegetarian: ingredientSpecialPropertyStatusFromJson(json['vegetarian']), 18 | fromPalmOil: 19 | ingredientSpecialPropertyStatusFromJson(json['from_palm_oil']), 20 | ingredients: (json['ingredients'] as List?) 21 | ?.map((e) => Ingredient.fromJson(e as Map)) 22 | .toList(), 23 | bold: json['bold'] as bool? ?? false, 24 | ); 25 | 26 | Map _$IngredientToJson(Ingredient instance) => 27 | { 28 | if (instance.rank case final value?) 'rank': value, 29 | if (instance.id case final value?) 'id': value, 30 | 'text': instance.text, 31 | if (instance.isInTaxonomy case final value?) 'is_in_taxonomy': value, 32 | if (instance.percent case final value?) 'percent': value, 33 | if (instance.percentEstimate case final value?) 'percent_estimate': value, 34 | if (ingredientSpecialPropertyStatusToJson(instance.vegan) 35 | case final value?) 36 | 'vegan': value, 37 | if (ingredientSpecialPropertyStatusToJson(instance.vegetarian) 38 | case final value?) 39 | 'vegetarian': value, 40 | if (ingredientSpecialPropertyStatusToJson(instance.fromPalmOil) 41 | case final value?) 42 | 'from_palm_oil': value, 43 | if (JsonHelper.ingredientsToJson(instance.ingredients) case final value?) 44 | 'ingredients': value, 45 | 'bold': instance.bold, 46 | }; 47 | -------------------------------------------------------------------------------- /lib/src/model/robotoff_nutrient_extraction.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'robotoff_nutrient_extraction.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | RobotoffNutrientExtractionResult _$RobotoffNutrientExtractionResultFromJson( 10 | Map json) => 11 | RobotoffNutrientExtractionResult( 12 | status: json['status'] as String?, 13 | count: (json['count'] as num?)?.toInt(), 14 | insights: (json['insights'] as List?) 15 | ?.map((e) => RobotoffNutrientExtractionInsight.fromJson( 16 | e as Map)) 17 | .toList(), 18 | ); 19 | 20 | Map _$RobotoffNutrientExtractionResultToJson( 21 | RobotoffNutrientExtractionResult instance) => 22 | { 23 | 'status': instance.status, 24 | 'count': instance.count, 25 | 'insights': instance.insights, 26 | }; 27 | 28 | RobotoffNutrientEntity _$RobotoffNutrientEntityFromJson( 29 | Map json) => 30 | RobotoffNutrientEntity( 31 | start: (json['start'] as num?)?.toInt(), 32 | end: (json['end'] as num?)?.toInt(), 33 | text: json['text'] as String?, 34 | unit: UnitHelper.stringToUnit(json['unit'] as String?), 35 | score: (json['score'] as num?)?.toDouble(), 36 | valid: json['valid'] as bool?, 37 | value: json['value'] as String?, 38 | entity: json['entity'] as String?, 39 | charStart: (json['char_start'] as num?)?.toInt(), 40 | charEnd: (json['char_end'] as num?)?.toInt(), 41 | ); 42 | 43 | Map _$RobotoffNutrientEntityToJson( 44 | RobotoffNutrientEntity instance) => 45 | { 46 | 'start': instance.start, 47 | 'end': instance.end, 48 | 'text': instance.text, 49 | 'unit': UnitHelper.unitToString(instance.unit), 50 | 'score': instance.score, 51 | 'valid': instance.valid, 52 | 'value': instance.value, 53 | 'entity': instance.entity, 54 | 'char_start': instance.charStart, 55 | 'char_end': instance.charEnd, 56 | }; 57 | -------------------------------------------------------------------------------- /lib/src/model/product_packaging.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'localized_tag.dart'; 3 | import '../interface/json_object.dart'; 4 | 5 | part 'product_packaging.g.dart'; 6 | 7 | /// Packaging component for a product, e.g. recyclable bottle made of glass. 8 | @JsonSerializable() 9 | class ProductPackaging extends JsonObject { 10 | /// Shape, canonicalized using [TaxonomyPackagingShape]. 11 | @JsonKey( 12 | includeIfNull: false, 13 | toJson: LocalizedTag.objToJson, 14 | ) 15 | LocalizedTag? shape; 16 | 17 | /// Material, canonicalized using [TaxonomyPackagingMaterial]. 18 | @JsonKey( 19 | includeIfNull: false, 20 | toJson: LocalizedTag.objToJson, 21 | ) 22 | LocalizedTag? material; 23 | 24 | /// Recycling status, canonicalized using [TaxonomyPackagingRecycling]. 25 | @JsonKey( 26 | includeIfNull: false, 27 | toJson: LocalizedTag.objToJson, 28 | ) 29 | LocalizedTag? recycling; 30 | 31 | /// Number of units of this component contained in the product. 32 | /// 33 | /// E.g. 6 for a pack of 6 bottles. 34 | @JsonKey( 35 | name: 'number_of_units', 36 | includeIfNull: false, 37 | fromJson: JsonObject.parseInt, 38 | ) 39 | int? numberOfUnits; 40 | 41 | /// Quantity (weight or volume) of food product contained. 42 | /// 43 | /// Ee.g. 75cl for a wine bottle. 44 | @JsonKey(name: 'quantity_per_unit', includeIfNull: false) 45 | String? quantityPerUnit; 46 | 47 | /// Weight in grams as measured by a user of one unit of the empty component. 48 | /// 49 | /// E.g. for a 6 pack of 1.5l water bottles, it might be 30, the weight in 50 | /// grams of 1 empty water bottle without its cap which is a different 51 | /// component. 52 | @JsonKey( 53 | name: 'weight_measured', 54 | includeIfNull: false, 55 | fromJson: JsonObject.parseDouble, 56 | ) 57 | double? weightMeasured; 58 | 59 | ProductPackaging(); 60 | 61 | factory ProductPackaging.fromJson(dynamic json) => 62 | _$ProductPackagingFromJson(json); 63 | 64 | @override 65 | Map toJson() => _$ProductPackagingToJson(this); 66 | 67 | Map toServerData() => JsonObject.toDataStatic(toJson()); 68 | 69 | @override 70 | String toString() => toServerData().toString(); 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/interface/json_object.dart: -------------------------------------------------------------------------------- 1 | /// interface class for all serializable json model objects. 2 | abstract class JsonObject { 3 | Map toJson(); 4 | 5 | Map toData() { 6 | return toDataStatic(toJson()); 7 | } 8 | 9 | static Map toDataStatic(Map json) { 10 | var result = {}; 11 | for (MapEntry entry in json.entries) { 12 | result.putIfAbsent(entry.key, () => entry.value.toString()); 13 | } 14 | 15 | return result; 16 | } 17 | 18 | const JsonObject(); 19 | 20 | static int? parseInt(dynamic json) { 21 | if (json is String) { 22 | return int.tryParse(json); 23 | } else if (json is double) { 24 | return json.floor(); 25 | } else { 26 | return json; 27 | } 28 | } 29 | 30 | static double? parseDouble(dynamic json) { 31 | if (json is String) { 32 | return double.tryParse(json); 33 | } else if (json is int) { 34 | return json.toDouble(); 35 | } else { 36 | return json; 37 | } 38 | } 39 | 40 | static bool parseBool(dynamic json) { 41 | if (json is String) { 42 | return json == '1'; 43 | } else if (json is int) { 44 | return json == 1; 45 | } else if (json is bool) { 46 | return json; 47 | } else { 48 | return false; 49 | } 50 | } 51 | 52 | static Map removeNullEntries( 53 | final Map input) { 54 | final Map result = {}; 55 | for (var element in input.entries) { 56 | if (element.value != null) { 57 | result[element.key] = element.value; 58 | } 59 | } 60 | return result; 61 | } 62 | 63 | /// Returns all values as a String separated by a hyphen 64 | /// value1 - value2 - value3 65 | String toValueString() { 66 | return toValueStringStatic(toJson()); 67 | } 68 | 69 | /// Returns all values as a String separated by a hyphen 70 | /// value1 - value2 - value3 71 | static String toValueStringStatic(Map json) { 72 | String result = ''; 73 | for (MapEntry entry in json.entries) { 74 | result = result + (entry.value != null ? ' - ${entry.value}' : ''); 75 | } 76 | 77 | return result; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/prices/price_product.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'flavor.dart'; 4 | import '../interface/json_object.dart'; 5 | import '../utils/json_helper.dart'; 6 | 7 | part 'price_product.g.dart'; 8 | 9 | /// Product object in the Prices API. 10 | /// 11 | /// cf. `ProductFull` in https://prices.openfoodfacts.org/api/docs 12 | @JsonSerializable() 13 | class PriceProduct extends JsonObject { 14 | /// Barcode (EAN) of the product, as a string. 15 | @JsonKey() 16 | late String code; 17 | 18 | /// Number of prices for this product. 19 | @JsonKey(name: 'price_count') 20 | int? priceCount; 21 | 22 | /// Number of locations for this product. 23 | @JsonKey(name: 'location_count') 24 | int? locationCount; 25 | 26 | /// Number of users for this product. 27 | @JsonKey(name: 'user_count') 28 | int? userCount; 29 | 30 | /// Number of proofs for this product. 31 | @JsonKey(name: 'proof_count') 32 | int? proofCount; 33 | 34 | @JsonKey(name: 'id') 35 | late int productId; 36 | 37 | /// Source of data. 38 | @JsonKey() 39 | Flavor? source; 40 | 41 | @JsonKey(name: 'product_name') 42 | String? name; 43 | 44 | @JsonKey(name: 'product_quantity') 45 | int? quantity; 46 | 47 | @JsonKey(name: 'product_quantity_unit') 48 | String? quantityUnit; 49 | 50 | @JsonKey(name: 'categories_tags') 51 | late List categoriesTags; 52 | 53 | @JsonKey() 54 | String? brands; 55 | 56 | @JsonKey(name: 'brands_tags') 57 | late List brandsTags; 58 | 59 | @JsonKey(name: 'labels_tags') 60 | late List labelsTags; 61 | 62 | @JsonKey(name: 'image_url') 63 | String? imageURL; 64 | 65 | @JsonKey(name: 'nutriscore_grade') 66 | String? nutriscoreGrade; 67 | 68 | @JsonKey(name: 'ecoscore_grade') 69 | String? ecoscoreGrade; 70 | 71 | @JsonKey(name: 'nova_group') 72 | int? novaGroup; 73 | 74 | @JsonKey(name: 'unique_scans_n') 75 | late int uniqueScansNumber; 76 | 77 | @JsonKey(fromJson: JsonHelper.stringTimestampToDate) 78 | late DateTime created; 79 | 80 | @JsonKey(fromJson: JsonHelper.nullableStringTimestampToDate) 81 | DateTime? updated; 82 | 83 | PriceProduct(); 84 | 85 | factory PriceProduct.fromJson(Map json) => 86 | _$PriceProductFromJson(json); 87 | 88 | @override 89 | Map toJson() => _$PriceProductToJson(this); 90 | } 91 | --------------------------------------------------------------------------------