feat: 1055 - new experimental class "FlexibleProduct"#1056
feat: 1055 - new experimental class "FlexibleProduct"#1056monsieurtanuki wants to merge 16 commits intoopenfoodfacts:masterfrom
Conversation
New files: * `api_get_flexible_product.dart`: tests around the new class `FlexibleProduct`. * `flexible_map.dart`: (experimental) helper class around json and maps. * `flexible_product.dart`: (experimental) Product without predefined structure, relying mainly on the json. * `flexible_product_result.dart`: (experimental) API answer from a call to /api/v???/product/$barcode, in a flexible manner. * `flexible_search_result.dart`: (experimental) API answer from a call product search, in a flexible manner. Impacted files: * `language_helper.dart`: added 3 experimental methods for `FlexibleProduct` * `open_food_api_client.dart`: added 2 experimental methods for `FlexibleProduct` - `getFlexibleProductResult` and `searchFlexibleProducts` * `openfoodfacts.dart`: added the new 4 class files * `product_image.dart`: added an experimental method to compute the image URLs * `product_query_configurations.dart`: made it possible to call API get product method with a "double" API version number - and not only an "int" * `product_result_v3.dart`: minor refactoring * `product_result_v3.g.dart`: generated
|
@teolemon ping |
ping |
ping |
|
Sorry @monsieurtanuki , underwater on non mobile things. I asked for a technical quick look from @PrimaelQuemerais @g123k (who also seem to be a little bit underwater). |
|
@teolemon For the record it is related to the app, cf. openfoodfacts/smooth-app#6502 |
|
Closed as stale. |
|
Ping |
|
Hello @monsieurtanuki |
|
@PrimaelQuemerais The typical use case is that 'brands' are a comma-separated String in OxF, and a List in Searchalicious. Another use case is fields that we download but ignore forever because at the time of the download they weren't part of the Product structure. And with an 'extends' it's easy to override or add fields. |
To answer that point more specifically, we've been asked to be more reactive, e.g. for new experimental fields. |
|
I understand, thank you for the prompt reply. |
| (json['attribute_groups'] as List<dynamic>?) | ||
| ?.map(AttributeGroup.fromJson); | ||
|
|
||
| String? get brands => json['brands'] as String?; |
There was a problem hiding this comment.
Following on your example with brands.
Since FlexibleProduct is supposed to be used with both the OxF API and Searchalicious, wouldn't this throw if json['brands'] is a List instead of String?
There was a problem hiding this comment.
@PrimaelQuemerais That's correct. But it's not relevant for the moment to implement anything specific to Searchalicious.
That first PR - that already started more than 3 months ago - is only about creating a FlexibleProduct that works like a Product.
There was a problem hiding this comment.
But if it doesn't implement this kind of logic I don't understand where this model stands between Product and storing the product in a Map. Do you already know how this would be handled in FlexibleProduct and why it couldn't be implemented in Product directly?
JSONSerializable still allows us to implement multiple parsing logics as long as the returning type remains the same, and wouldn't you face the same issue with FlexibleProduct?
Let me know if there's something I'm not getting here.
There was a problem hiding this comment.
as long as the returning type remains the same
That's the problem for "brands".
No format problem will be faced with FlexibleProduct, assuming it's correct, JSON-wise.
What we cannot do today with Product is accepting a priori data that don't match exactly requirements decided months ago.
Again, currently when we download products, we first check if they match the expected Product structure. If they don't, we ignore them by throwing an exception.
With FlexibleProduct, we won't have this problem because as long as it's JSON, we accept the data. We'll still have to decide later how to retrieve this or that field - e.g. for brands - but the data won't be rejected a priori. And extending FlexibleProduct e.g. in Smoothie will make it possible to be flexible about the data.
Basically that would be something like
@override
String? get brands {
dynamic result = json['brands'];
if (result == null) {
return null;
}
try {
return result as String;
} catch(e) {
return (result as List<String>).join(',');
}
}There was a problem hiding this comment.
We can implement the same try catch in the Product deserialization logic which I think is what we should do.
This FlexibleProduct approach seems to introduce a lot of runtime errors as no-one expects a getter to throw.
If we want to manipulate products from the server without flexibly I think it is wiser to keep it as a Map until we are sure of the structure, at which point it should be deserialization into an object, but then I don't see what prevents us from implementing everything into Product directly.
That's my take but it would be nice to get more opinions @g123k @teolemon
There was a problem hiding this comment.
Okay let's reset and let me try to understand you better.
Cool
Is the goal of
FlexibleProductto provide developers a way to get products from the API regardless of the structure of what the API sends back?
Correct. In order to provide flexibility to API structure changes.
And it is better than a function returning a Map<String, dynamic> because it offers default getters?
Correct. So that developers don't have to reinvent the wheel for the 99% for fields that don't change.
There was a problem hiding this comment.
I'll take a bit more time to ponder over this but here's a few thoughts :
One improvement I would make then is to allow developers to pass their own type to getFlexibleProductResult() like this : static Future<FlexibleProductResult> getFlexibleProductResult<T extends FlexibleProduct>(...). This way FlexibleProductResult can contain a list of their own type. And same for searchFlexibleProducts().
One concern is if I extend FlexibleProduct to let's say fix the issue with brands being Strings or Lists, I won't be able to use the brand attribute as dart doesn't allow me to both override and change the type at the same time. How to we expect developers to work around this?
I still don't really like having getters which can throw, but that's a design choice and no solution will be perfect for this problem anyway, but maybe we can find a way to let developers know that these getters can be unsafe? I can take some time to think about it.
There was a problem hiding this comment.
One improvement I would make then is to allow developers to pass their own type to
getFlexibleProductResult()like this :static Future<FlexibleProductResult> getFlexibleProductResult<T extends FlexibleProduct>(...). This wayFlexibleProductResultcan contain a list of their own type. And same forsearchFlexibleProducts().
I think that would be over-engineering the code:
- we already have access to
json, so we can build whatever class on top of that json if needed - in
dartwe can extendFlexibleProductwhenever we want, including in Smoothie, so I can't think of anything we couldn't fix with anextension
One concern is if I extend
FlexibleProductto let's say fix the issue withbrandsbeing Strings or Lists, I won't be able to use the brand attribute as dart doesn't allow me to both override and change the type at the same time. How to we expect developers to work around this?I still don't really like having getters which can throw, but that's a design choice and no solution will be perfect for this problem anyway, but maybe we can find a way to let developers know that these getters can be unsafe? I can take some time to think about it.
In order to solve this issue forever, the idea would be to deprecate brands and to create 2 new getters, approximately coded like that:
String? get brandsAsString {
final result = json['brands'];
if (result == null) {
return null;
}
if (result is String) {
return result;
}
return (result as List<String>).join(',');
}
List<String>? get brandsAsList {
final result = json['brands'];
if (result == null) {
return null;
}
if (result is List<String>) {
return result;
}
return (result as String).split(',');
}What do you think?
There was a problem hiding this comment.
Correct me if I'm wrong but in order to deprecate brands, one would need to create a new class extending from FlexibleProduct. Thus an extension wouldn't be enough?
There was a problem hiding this comment.
@PrimaelQuemerais Indeed, we don't need brands after all. With brandsAsString and brandsAsList we're good.
Impacted files: * `abstract_query_configuration.dart`: added `flexibleFields`, for more... flexibility * `api_get_flexible_product.dart`: tested `flexibleFields` * `flexible_product.dart`: changed getter `brands` into less ambiguous getters `brandsAsString` and `brandsAsList` * `product_fields.dart`: added field SCHEMA_VERSION * `product_query_configurations.dart`: minor refactoring * `product_search_query_configuration.dart`: minor refactoring
|
@PrimaelQuemerais With my latest push:
|
|
@PrimaelQuemerais ping |
|
Closed as stale. Again. |
What
This PR adds flexibility:
3.2Productbefore anything elseWith that PR we can easily accept new product fields and fields changing format, as we can just store the json regardless of the current or future product format.
Fixes bug(s)
Part of
Files
New files:
api_get_flexible_product.dart: tests around the new classFlexibleProduct.flexible_map.dart: (experimental) helper class around json and maps.flexible_product.dart: (experimental) Product without predefined structure, relying mainly on the json.flexible_product_result.dart: (experimental) API answer from a call to /api/v???/product/$barcode, in a flexible manner.flexible_search_result.dart: (experimental) API answer from a call product search, in a flexible manner.Impacted files:
language_helper.dart: added 3 experimental methods forFlexibleProductopen_food_api_client.dart: added 2 experimental methods forFlexibleProduct-getFlexibleProductResultandsearchFlexibleProductsopenfoodfacts.dart: added the new 4 class filesproduct_image.dart: added an experimental method to compute the image URLsproduct_query_configurations.dart: made it possible to call API get product method with a "double" API version number - and not only an "int"product_result_v3.dart: minor refactoringproduct_result_v3.g.dart: generated