Should work fine
This commit is contained in:
@@ -1,98 +1,236 @@
|
||||
import Foundation
|
||||
import Units
|
||||
|
||||
public struct Product: Codable, Sendable, Identifiable {
|
||||
public let code: String?
|
||||
public let productName: String?
|
||||
public let genericName: String?
|
||||
public let brands: String?
|
||||
public let brandsTags: [String]?
|
||||
public let quantity: String?
|
||||
|
||||
// MARK: - Images
|
||||
public let imageFrontUrl: String?
|
||||
public let imageSmallUrl: String?
|
||||
public let ingredientsText: String?
|
||||
public let imageUrl: String?
|
||||
public let imageNutritionSmallUrl: String?
|
||||
public let imageNutritionThumbUrl: String?
|
||||
public let imageNutritionUrl: String?
|
||||
public let imageIngredientsSmallUrl: String?
|
||||
public let imageIngredientsThumbUrl: String?
|
||||
public let imageIngredientsUrl: String?
|
||||
|
||||
// Grades
|
||||
// MARK: - Quantities
|
||||
public let productQuantity: Float?
|
||||
public let productQuantityUnit: String?
|
||||
public let servingQuantity: String?
|
||||
public let servingQuantityUnit: String?
|
||||
public let servingSize: String?
|
||||
|
||||
// MARK: - Grades & Scores
|
||||
public let nutriscoreGrade: String?
|
||||
public let nutriscoreData: NutriscoreData?
|
||||
public let novaGroup: Int?
|
||||
public let ecoscoreGrade: String?
|
||||
|
||||
// Nutriments
|
||||
public let nutritionDataPer: String?
|
||||
public let nutriments: Nutriments?
|
||||
public let ingredients: [Ingredient]?
|
||||
public let nutrientLevels: NutrientLevels?
|
||||
|
||||
// MARK: - Ingredients & Allergens
|
||||
public let ingredientsText: String?
|
||||
public let ingredients: [Ingredient]?
|
||||
public let ingredientsHierarchy: [String]?
|
||||
public let allergens: String?
|
||||
public let allergensHierarchy: [String]?
|
||||
|
||||
// MARK: - Product-Misc
|
||||
public let additivesN: Int?
|
||||
|
||||
// MARK: - Metadata
|
||||
public let checked: String?
|
||||
public let complete: Int?
|
||||
public let completeness: Float?
|
||||
public let ecoscoreGrade: String?
|
||||
// ...
|
||||
public let nutrientLevels: NutrientLevels?
|
||||
public var categories: String?
|
||||
public var categoriesHierarchy: [String]?
|
||||
|
||||
public var id: String {
|
||||
guard let code = code else {
|
||||
return UUID().uuidString
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties (Units)
|
||||
public var waterQuantity: Units.UnitValue<Double>? {
|
||||
guard let ingredient = ingredients?.first(where: { $0.isWater }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let quantity = productQuantity,
|
||||
let estimate = ingredient.percentEstimate,
|
||||
let rawUnit = productQuantityUnit,
|
||||
let unit = Units.Unit(rawValue: rawUnit)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Double(quantity * (estimate / 100))[unit]
|
||||
}
|
||||
|
||||
public var isLiquid: Bool {
|
||||
guard let rawUnit = productQuantityUnit,
|
||||
let unit = Units.Unit(rawValue: rawUnit)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return unit.category == .volume
|
||||
}
|
||||
|
||||
// MARK: - CodingKeys
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case code
|
||||
case productName = "product_name"
|
||||
case genericName = "generic_name"
|
||||
case brands
|
||||
case brandsTags = "brands_tags"
|
||||
case quantity
|
||||
|
||||
// Images
|
||||
case imageFrontUrl = "image_front_url"
|
||||
case imageSmallUrl = "image_small_url"
|
||||
case imageUrl = "image_url"
|
||||
case imageNutritionSmallUrl = "image_nutrition_small_url"
|
||||
case imageNutritionThumbUrl = "image_nutrition_thumb_url"
|
||||
case imageNutritionUrl = "image_nutrition_url"
|
||||
case imageIngredientsSmallUrl = "image_ingredients_small_url"
|
||||
case imageIngredientsThumbUrl = "image_ingredients_thumb_url"
|
||||
case imageIngredientsUrl = "image_ingredients_url"
|
||||
|
||||
// Quantities
|
||||
case productQuantity = "product_quantity"
|
||||
case productQuantityUnit = "product_quantity_unit"
|
||||
case servingQuantity = "serving_quantity"
|
||||
case servingQuantityUnit = "serving_quantity_unit"
|
||||
case servingSize = "serving_size"
|
||||
|
||||
case ingredientsText = "ingredients_text"
|
||||
case ingredients
|
||||
case ingredientsHierarchy = "ingredients_hierarchy"
|
||||
|
||||
// Grades & Data
|
||||
case nutriscoreGrade = "nutriscore_grade"
|
||||
case nutriscoreData = "nutriscore_data"
|
||||
case ecoscoreGrade = "ecoscore_grade"
|
||||
case novaGroup = "nova_group"
|
||||
case nutriments
|
||||
case ingredients
|
||||
case nutrientLevels = "nutrient_levels"
|
||||
case nutritionDataPer = "nutrition_data_per"
|
||||
|
||||
case complete
|
||||
case completeness
|
||||
case additivesN = "additives_n"
|
||||
case checked
|
||||
case nutrientLevels = "nutrient_levels"
|
||||
|
||||
case allergens
|
||||
case allergensHierarchy = "allergens_hierarchy"
|
||||
case categories
|
||||
case categoriesHierarchy = "categories_hierarchy"
|
||||
}
|
||||
|
||||
// MARK: - Initializer
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
code = try container.decodeIfPresent(String.self, forKey: .code)
|
||||
productName = try container.decodeIfPresent(
|
||||
String.self, forKey: .productName)
|
||||
genericName = try container.decodeIfPresent(
|
||||
String.self, forKey: .genericName)
|
||||
brands = try container.decodeIfPresent(String.self, forKey: .brands)
|
||||
brandsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .brandsTags)
|
||||
quantity = try container.decodeIfPresent(String.self, forKey: .quantity)
|
||||
|
||||
// Images
|
||||
imageFrontUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageFrontUrl)
|
||||
imageSmallUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageSmallUrl)
|
||||
imageUrl = try container.decodeIfPresent(String.self, forKey: .imageUrl)
|
||||
imageNutritionSmallUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageNutritionSmallUrl)
|
||||
imageNutritionThumbUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageNutritionThumbUrl)
|
||||
imageNutritionUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageNutritionUrl)
|
||||
imageIngredientsSmallUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageIngredientsSmallUrl)
|
||||
imageIngredientsThumbUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageIngredientsThumbUrl)
|
||||
imageIngredientsUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageIngredientsUrl)
|
||||
|
||||
// Quantities (Robust decoding)
|
||||
productQuantity = try container.decodeFloatOrString(
|
||||
forKey: .productQuantity)
|
||||
productQuantityUnit = try container.decodeIfPresent(
|
||||
String.self, forKey: .productQuantityUnit)
|
||||
|
||||
// servingQuantity is String?, but API might return a number
|
||||
if let floatVal = try? container.decode(
|
||||
Float.self, forKey: .servingQuantity)
|
||||
{
|
||||
servingQuantity = String(floatVal)
|
||||
} else {
|
||||
servingQuantity = try container.decodeIfPresent(
|
||||
String.self, forKey: .servingQuantity)
|
||||
}
|
||||
|
||||
servingQuantityUnit = try container.decodeIfPresent(
|
||||
String.self, forKey: .servingQuantityUnit)
|
||||
servingSize = try container.decodeIfPresent(
|
||||
String.self, forKey: .servingSize)
|
||||
|
||||
// Ingredients & Categories
|
||||
ingredientsText = try container.decodeIfPresent(
|
||||
String.self, forKey: .ingredientsText)
|
||||
|
||||
// Grades
|
||||
nutriscoreGrade = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutriscoreGrade)
|
||||
novaGroup = try container.decodeIfPresent(
|
||||
Int.self, forKey: .novaGroup)
|
||||
|
||||
// Nutriments
|
||||
nutriments = try container.decodeIfPresent(
|
||||
Nutriments.self, forKey: .nutriments)
|
||||
ingredients = try container.decodeIfPresent(
|
||||
[Ingredient].self, forKey: .ingredients)
|
||||
ingredientsHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsHierarchy)
|
||||
|
||||
// MARK: - Product-Misc
|
||||
categories = try container.decodeIfPresent(
|
||||
String.self, forKey: .categories)
|
||||
categoriesHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .categoriesHierarchy)
|
||||
|
||||
// Grades & Nutriments
|
||||
nutriscoreGrade = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutriscoreGrade)
|
||||
nutriscoreData = try container.decodeIfPresent(
|
||||
NutriscoreData.self, forKey: .nutriscoreData)
|
||||
novaGroup = try container.decodeIfPresent(Int.self, forKey: .novaGroup)
|
||||
ecoscoreGrade = try container.decodeIfPresent(
|
||||
String.self, forKey: .ecoscoreGrade)
|
||||
|
||||
nutriments = try container.decodeIfPresent(
|
||||
Nutriments.self, forKey: .nutriments)
|
||||
nutrientLevels = try container.decodeIfPresent(
|
||||
NutrientLevels.self, forKey: .nutrientLevels)
|
||||
nutritionDataPer = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutritionDataPer)
|
||||
|
||||
// Misc
|
||||
additivesN = try container.decodeIfPresent(
|
||||
Int.self, forKey: .additivesN)
|
||||
checked = try container.decodeIfPresent(String.self, forKey: .checked)
|
||||
complete = try container.decodeIfPresent(Int.self, forKey: .complete)
|
||||
completeness = try container.decodeFloatOrString(forKey: .completeness)
|
||||
ecoscoreGrade = try container.decodeIfPresent(
|
||||
String.self, forKey: .ecoscoreGrade)
|
||||
// ...
|
||||
nutrientLevels = try container.decodeIfPresent(
|
||||
NutrientLevels.self, forKey: .nutrientLevels)
|
||||
|
||||
allergens = try container.decodeIfPresent(
|
||||
String.self, forKey: .allergens)
|
||||
allergensHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .allergensHierarchy)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user