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 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? // 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? public let nutritionDataPer: String? public let nutriments: Nutriments? 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]? public let additivesN: Int? // MARK: - Metadata public let checked: String? public let complete: Int? public let completeness: Float? 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? { 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 nutrientLevels = "nutrient_levels" case nutritionDataPer = "nutrition_data_per" case complete case completeness case additivesN = "additives_n" case checked 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) ingredients = try container.decodeIfPresent( [Ingredient].self, forKey: .ingredients) ingredientsHierarchy = try container.decodeIfPresent( [String].self, forKey: .ingredientsHierarchy) 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) allergens = try container.decodeIfPresent( String.self, forKey: .allergens) allergensHierarchy = try container.decodeIfPresent( [String].self, forKey: .allergensHierarchy) } }