Skip to content

feat: Nutrient modifiers (Get/Send product APIs) #1042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

g123k
Copy link
Contributor

@g123k g123k commented Feb 14, 2025

Hi everyone!

Finally, a working solution for nutrient modifiers, when we get / save a product.

On the GET API, we have _modifier fields.
On the POST one, we must include the modifier character within the value.

@g123k g123k requested a review from a team as a code owner February 14, 2025 09:22
Copy link
Contributor

@monsieurtanuki monsieurtanuki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @g123k!

I have a doubt about the relevancy of using the same modifier for all PerSizes of a given nutrient:

  • I'm not sure it's always true
  • That makes the code harder to read (the keys of the private maps being different), with no added value

Please also read my other comments.

Comment on lines 106 to 107
_valueMap[_getTag(nutrient, perSize)] = value;
_modifierMap[nutrient.offTag] = modifier;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could a modifier be different for different PerSizes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but I can support it

Comment on lines 1449 to 1456
expect(nutriments.getModifier(Nutrient.energyKJ), isNull);
expect(nutriments.getModifier(Nutrient.energyKCal), isNull);
expect(nutriments.getModifier(Nutrient.sugars), isNull);
expect(nutriments.getModifier(Nutrient.salt), isNull);
expect(nutriments.getModifier(Nutrient.fiber), isNull);
expect(nutriments.getModifier(Nutrient.fat), isNull);
expect(nutriments.getModifier(Nutrient.saturatedFat), isNull);
expect(nutriments.getModifier(Nutrient.proteins), isNull);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be more fun if at least one modifier was not null. Or get rid of that test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal was to test a product with no modifier.
But I can remove it, no pb

@@ -1430,4 +1432,81 @@ void main() {
expect(productResult.product!.dataQualityWarningsTags, isNull);
});
});

test('check nutrients modifier (set)', () async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't understand the title, more specifically: "(set)".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to include a few changes.
Indeed, it wasn't understandable

expect(nutriments.getModifier(Nutrient.proteins), isNull);
});

test('check nutrients modifier (without)', () async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't understand the title, more specifically: "(without)".

@@ -513,5 +518,97 @@ Like that:
expect(product.noNutritionData, isFalse);
expect(product.nutriments, isNotNull);
});

test('Nutriments modifiers', () async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test makes sense, but as you don't save anything (to the server) I guess it should be somewhere else. Or commented as "not really saving anything".
And another test actually saving to the server would make sense.

@g123k
Copy link
Contributor Author

g123k commented Feb 15, 2025

Hi @monsieurtanuki!

Thanks for your review.
We can send a different modifier per 100g or per serving.
However, we only receive one for the two.
I've changed the code to allow this.

Copy link
Contributor

@monsieurtanuki monsieurtanuki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @g123k for your work!
My bad: unfortunately I was a bit wrong in my review, as I didn't get that the server provided only a single modifier for a nutrient, regardless of the PerSize.
Therefore it would make much more sense to remove the PerSize parameter from the _getModifierTag method. Good thing that this method does exist, though, as we just have to remove the parameter and let the syntactic analyzer detect the related errors.

Please have a look at my comments regarding tests: in one case you play the same tests on the same product with the same modifiers. As a consequence you don't really test the "save modifier' feature, you test that it did work once. I suggest that you always change the modifier values, and that you include the null modifier in the "save modifier" test, as it's really a use case.

Comment on lines +65 to +68
/// Returns the modifier key for a [nutrient]
String _getModifierTag(final Nutrient nutrient, final PerSize perSize) =>
'${nutrient.offTag}_${perSize.offTag}';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I'm sorry, I've just had a look again at the data provided by the server, I didn't get that the modifier was meant for a nutrient, not for nutrient + persize.
Anyway, that's better to use a method, the impact would be minor on the rest of the code.

Suggested change
/// Returns the modifier key for a [nutrient]
String _getModifierTag(final Nutrient nutrient, final PerSize perSize) =>
'${nutrient.offTag}_${perSize.offTag}';
/// Returns the modifier local key map for a [nutrient]
String _getModifierTag(final Nutrient nutrient) => nutrient.offTag;

Comment on lines +20 to +22
_modifierMap[_getModifierTag(nutrient, PerSize.serving)] = modifier;
_modifierMap[_getModifierTag(nutrient, PerSize.oneHundredGrams)] =
modifier;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_modifierMap[_getModifierTag(nutrient, PerSize.serving)] = modifier;
_modifierMap[_getModifierTag(nutrient, PerSize.oneHundredGrams)] =
modifier;
_modifierMap[_getModifierTag(nutrient] = modifier;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that the server may have a different value per serving AND 100g.
In this case, the last one will win.

Solution 1: keep the current behavior
Solution 2: save the value depending on the PerSize value of the array.

Comment on lines +55 to +56
final Map<String, NutrientModifier> _modifierMap =
<String, NutrientModifier>{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I did it again: if I read the comment above it makes sense to store null values.

Suggested change
final Map<String, NutrientModifier> _modifierMap =
<String, NutrientModifier>{};
final Map<String, NutrientModifier?> _modifierMap =
<String, NutrientModifier?>{};

Comment on lines +76 to +78
NutrientModifier? getModifier(
final Nutrient nutrient, final PerSize perSize) =>
_modifierMap[_getModifierTag(nutrient, perSize)];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
NutrientModifier? getModifier(
final Nutrient nutrient, final PerSize perSize) =>
_modifierMap[_getModifierTag(nutrient, perSize)];
NutrientModifier? getModifier(final Nutrient nutrient) =>
_modifierMap[_getModifierTag(nutrient)];

Comment on lines +96 to +97
_valueMap.remove(_getTag(nutrient, perSize));
_modifierMap[_getModifierTag(nutrient, perSize)] = modifier!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_valueMap.remove(_getTag(nutrient, perSize));
_modifierMap[_getModifierTag(nutrient, perSize)] = modifier!;
_valueMap[_getTag(nutrient, perSize)] = null;
_modifierMap[_getModifierTag(nutrient)] = modifier;

Comment on lines +1483 to +1490
expect(
nutriments.getModifier(Nutrient.salt, PerSize.oneHundredGrams),
NutrientModifier.lessThan,
);
expect(
nutriments.getModifier(Nutrient.sodium, PerSize.oneHundredGrams),
NutrientModifier.lessThan,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(
nutriments.getModifier(Nutrient.salt, PerSize.oneHundredGrams),
NutrientModifier.lessThan,
);
expect(
nutriments.getModifier(Nutrient.sodium, PerSize.oneHundredGrams),
NutrientModifier.lessThan,
);
expect(nutriments.getModifier(Nutrient.salt), NutrientModifier.lessThan);
expect(nutriments.getModifier(Nutrient.sodium), NutrientModifier.lessThan);

Comment on lines +1508 to +1519
expect(
nutriments.getModifier(Nutrient.fiber, PerSize.oneHundredGrams),
NutrientModifier.notProvided,
);
expect(
nutriments.getValue(Nutrient.fiber, PerSize.oneHundredGrams),
isNull,
);
expect(
nutriments.getValue(Nutrient.fiber, PerSize.serving),
isNull,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(
nutriments.getModifier(Nutrient.fiber, PerSize.oneHundredGrams),
NutrientModifier.notProvided,
);
expect(
nutriments.getValue(Nutrient.fiber, PerSize.oneHundredGrams),
isNull,
);
expect(
nutriments.getValue(Nutrient.fiber, PerSize.serving),
isNull,
);
expect(nutriments.getModifier(Nutrient.fiber), NutrientModifier.notProvided);
expect(nutriments.getValue(Nutrient.fiber), isNull);
expect(nutriments.getValue(Nutrient.fiber), isNull);

@@ -406,6 +406,11 @@ Like that:
nutriments.getValue(nutrient, perSize),
reason: 'should be the same values for $nutrient',
);
expect(
nutriments.getModifier(nutrient, perSize),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
nutriments.getModifier(nutrient, perSize),
nutriments.getModifier(nutrient),


test('Nutriments modifiers (server call)', () async {
Product product = Product(
barcode: '1234567890123',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that you're always setting the same values for the same product. So if you're lucky it may work the first time, and if you're not lucky for some reason it doesn't work anymore (e.g. server somehow changed) and you won't be able to see that.
You may rather start with a getProductV3, and change each modifier.

Comment on lines +575 to +579
result.product!.nutriments?.getModifier(
Nutrient.proteins,
PerSize.oneHundredGrams,
),
NutrientModifier.notProvided,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather keeps the nutrient x modifier in a map, then save the product, then get the product, then check that the product matches the map.

@monsieurtanuki
Copy link
Contributor

Hi @g123k!

I've just run some tests in PROD on the website, and I don't think there's such thing as "modifier for serving" - it looks like there's only one modifier for both serving and 100g, something like '${nutrient.offTag}_modifier'.
A bit like "unit".

The test was on "salt" for https://world.openfoodfacts.org/api/v3/product/8712000050368/?fields=nutriments

  • salt "< 0.01" for 100g
      "salt": 0.01,
      "salt_100g": 0.01,
      "salt_modifier": "\u003C",
      "salt_serving": 0.025,
      "salt_unit": "g",
      "salt_value": 0.01,
  • salt "< 0.026" for serving
      "salt": 0.026,
      "salt_100g": 0.0104,
      "salt_modifier": "\u003C",
      "salt_serving": 0.026,
      "salt_unit": "g",
      "salt_value": 0.026,
  • salt "> 0.027" for serving
      "salt": 0.027,
      "salt_100g": 0.0108,
      "salt_modifier": "\u003E",
      "salt_serving": 0.027,
      "salt_unit": "g",
      "salt_value": 0.027,
  • salt "< 0.01" for 100g
      "salt": 0.01,
      "salt_100g": 0.01,
      "salt_modifier": "\u003C",
      "salt_serving": 0.025,
      "salt_unit": "g",
      "salt_value": 0.01,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants