Files
swift-openfoodfacts-sdk/Sources/OpenFoodFactsSDK/Schemas/Product.swift
2025-12-06 17:28:17 +01:00

237 lines
7.6 KiB
Swift

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<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 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)
}
}