This commit is contained in:
cdricms
2025-12-06 16:33:17 +01:00
parent 7be99711e1
commit 043de16a16
31 changed files with 594 additions and 1674 deletions

View File

@@ -0,0 +1,56 @@
public struct Ingredient: Sendable, Codable {
public var fromPalmOil: String? = nil
public var id: String? = nil
public var origin: String? = nil
public var percent: Float? = nil
public var rank: Float? = 0
public var text: String? = nil
public var vegan: String? = nil
public var vegetarian: String? = nil
public var ciqualFoodCode: String? = nil
public var ecobalyseCode: String? = nil
public var percentEstimate: Float? = nil // String or number
private enum CodingKeys: String, CodingKey {
case fromPalmOil = "from_palm_oil"
case id
case origin
case percent
case rank
case text
case vegan
case vegetarian
case ciqualFoodCode = "ciqual_food_code"
case ecobalyseCode = "ecobalyse_code"
case percentEstimate = "percent_estimate"
}
public var isWater: Bool {
ciqualFoodCode == "18066"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Standard String fields
id = try container.decodeIfPresent(String.self, forKey: .id)
text = try container.decodeIfPresent(String.self, forKey: .text)
vegan = try container.decodeIfPresent(String.self, forKey: .vegan)
vegetarian = try container.decodeIfPresent(
String.self, forKey: .vegetarian)
fromPalmOil = try container.decodeIfPresent(
String.self, forKey: .fromPalmOil)
origin = try container.decodeIfPresent(String.self, forKey: .origin)
ciqualFoodCode = try container.decodeIfPresent(
String.self, forKey: .ciqualFoodCode)
ecobalyseCode = try container.decodeIfPresent(
String.self, forKey: .ecobalyseCode)
// Polymorphic Fields (String or Number)
percentEstimate = try container.decodeFloatOrString(
forKey: .percentEstimate)
percent = try container.decodeFloatOrString(forKey: .percent)
rank = try container.decodeFloatOrString(forKey: .rank)
}
}

View File

@@ -0,0 +1,5 @@
public struct LanguagesCodes: Sendable, Codable {
public var en: Float? = nil
public var fr: Float? = nil
public var pl: Float? = nil
}

View File

@@ -0,0 +1,28 @@
public struct NutrientLevels: Sendable, Codable {
public enum Level: String, Sendable, Codable {
case high, moderate, low
}
public var fat: Level? = nil
public var salt: Level? = nil
public var saturatedFat: Level? = nil
public var sugars: Level? = nil
public enum CodingKeys: String, CodingKey {
case fat
case salt
case saturatedFat = "saturated-fat"
case sugars
}
public subscript(_ key: CodingKeys) -> Level? {
switch key {
case .fat: fat
case .salt: salt
case .saturatedFat: saturatedFat
case .sugars: sugars
}
}
}

View File

@@ -0,0 +1,52 @@
import FoundationEssentials
@dynamicMemberLookup
public struct Nutriments: Codable, Sendable {
private var storage: [String: Double]
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
var dict = [String: Double]()
for key in container.allKeys {
if let val = try? container.decode(Double.self, forKey: key) {
dict[key.stringValue] = val
} else if let valStr = try? container.decode(
String.self, forKey: key), let val = Double(valStr)
{
dict[key.stringValue] = val
}
}
self.storage = dict
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyCodingKey.self)
for (key, value) in storage {
try container.encode(value, forKey: AnyCodingKey(stringValue: key))
}
}
/// Access any nutrient dynamically (e.g. `nutriments.energyKcal`)
public subscript(dynamicMember member: String) -> Double? {
// Convert camelCase "energyKcal" to snake_case "energy-kcal" or "energy_kcal" logic if needed
// For V2, OFF often returns "energy-kcal_100g"
let snake = member.camelCaseToSnakeCase()
return storage[snake] ?? storage["\(snake)_100g"]
?? storage["\(snake)_value"]
}
// Specific standard getters
public var energyKcal: Double? { self.storage["energy-kcal_100g"] }
public var carbohydrates: Double? { self.storage["carbohydrates_100g"] }
public var fat: Double? { self.storage["fat_100g"] }
public var proteins: Double? { self.storage["proteins_100g"] }
public var salt: Double? { self.storage["salt_100g"] }
}
// Helper for String extension used above
struct AnyCodingKey: CodingKey {
var stringValue: String
var intValue: Int?
init(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
}

View File

@@ -0,0 +1,98 @@
import FoundationEssentials
public struct Product: Codable, Sendable, Identifiable {
public let code: String?
public let productName: String?
public let brands: String?
public let quantity: String?
public let imageFrontUrl: String?
public let imageSmallUrl: String?
public let ingredientsText: String?
// Grades
public let nutriscoreGrade: String?
public let novaGroup: Int?
// Nutriments
public let nutriments: Nutriments?
public let ingredients: [Ingredient]?
// MARK: - Product-Misc
public let additivesN: Int?
public let checked: String?
public let complete: Int?
public let completeness: Float?
public let ecoscoreGrade: String?
// ...
public let nutrientLevels: NutrientLevels?
public var id: String {
guard let code = code else {
return UUID().uuidString
}
return code
}
private enum CodingKeys: String, CodingKey {
case code
case productName = "product_name"
case brands
case quantity
case imageFrontUrl = "image_front_url"
case imageSmallUrl = "image_small_url"
case ingredientsText = "ingredients_text"
case nutriscoreGrade = "nutriscore_grade"
case ecoscoreGrade = "ecoscore_grade"
case novaGroup = "nova_group"
case nutriments
case ingredients
case complete
case completeness
case additivesN = "additives_n"
case checked
case nutrientLevels = "nutrient_levels"
}
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)
brands = try container.decodeIfPresent(String.self, forKey: .brands)
quantity = try container.decodeIfPresent(String.self, forKey: .quantity)
imageFrontUrl = try container.decodeIfPresent(
String.self, forKey: .imageFrontUrl)
imageSmallUrl = try container.decodeIfPresent(
String.self, forKey: .imageSmallUrl)
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)
// MARK: - Product-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)
}
}

View File

@@ -0,0 +1,20 @@
import FoundationEssentials
public enum ProductField: String, Sendable, Codable {
case code
case productName = "product_name"
case brands
case quantity
case nutriments
case ingredients
case nutrientLevels = "nutrient_levels"
case ingredientsText = "ingredients_text"
case imageFrontUrl = "image_front_url"
case imageSmallUrl = "image_small_url"
case categories
case ecoscoreGrade = "ecoscore_grade"
case nutriscoreGrade = "nutriscore_grade"
case novaGroup = "nova_group"
case stores
// Add other specific fields as needed
}

View File

@@ -0,0 +1,98 @@
public enum SearchNutriment: String, Codable {
case energy // Energy
case energyFromFat = "energy-from-fat" // Energy from fat
case fat // Fat
case saturatedFat = "saturated-fat" // Saturated fat
case butyricAcid = "butyric-acid" // Butyric acid (4:0)
case caproicAcid = "caproic-acid" // Caproic acid (6:0)
case caprylicAcid = "caprylic-acid" // Caprylic acid (8:0)
case capricAcid = "capric-acid" // Capric acid (10:0)
case lauricAcid = "lauric-acid" // Lauric acid (12:0)
case myristicAcid = "myristic-acid" // Myristic acid (14:0)
case palmiticAcid = "palmitic-acid" // Palmitic acid (16:0)
case stearicAcid = "stearic-acid" // Stearic acid (18:0)
case arachidicAcid = "arachidic-acid" // Arachidic acid (20:0)
case behenicAcid = "behenic-acid" // Behenic acid (22:0)
case lignocericAcid = "lignoceric-acid" // Lignoceric acid (24:0)
case ceroticAcid = "cerotic-acid" // Cerotic acid (26:0)
case montanicAcid = "montanic-acid" // Montanic acid (28:0)
case melissicAcid = "melissic-acid" // Melissic acid (30:0)
case monounsaturatedFat = "monounsaturated-fat" // Monounsaturated fat
case polyunsaturatedFat = "polyunsaturated-fat" // Polyunsaturated fat
case omega3Fat = "omega-3-fat" // Omega 3 fatty acids
case alphaLinolenicAcid = "alpha-linolenic-acid" // Alpha-linolenic acid / ALA (18:3 n-3)
case eicosapentaenoicAcid = "eicosapentaenoic-acid" // Eicosapentaenoic acid / EPA (20:5 n-3)
case docosahexaenoicAcid = "docosahexaenoic-acid" // Docosahexaenoic acid / DHA (22:6 n-3)
case omega6Fat = "omega-6-fat" // Omega 6 fatty acids
case linoleicAcid = "linoleic-acid" // Linoleic acid / LA (18:2 n-6)
case arachidonicAcid = "arachidonic-acid" // Arachidonic acid / AA / ARA (20:4 n-6)
case gammaLinolenicAcid = "gamma-linolenic-acid" // Gamma-linolenic acid / GLA (18:3 n-6)
case dihomoGammaLinolenicAcid = "dihomo-gamma-linolenic-acid" // Dihomo-gamma-linolenic acid / DGLA (20:3 n-6)
case omega9Fat = "omega-9-fat" // Omega 9 fatty acids
case oleicAcid = "oleic-acid" // Oleic acid (18:1 n-9)
case elaidicAcid = "elaidic-acid" // Elaidic acid (18:1 n-9)
case gondoicAcid = "gondoic-acid" // Gondoic acid (20:1 n-9)
case meadAcid = "mead-acid" // Mead acid (20:3 n-9)
case erucicAcid = "erucic-acid" // Erucic acid (22:1 n-9)
case nervonicAcid = "nervonic-acid" // Nervonic acid (24:1 n-9)
case transFat = "trans-fat" // Trans fat
case cholesterol // Cholesterol
case carbohydrates // Carbohydrate
case sugars // Sugars
case sucrose // Sucrose
case glucose // Glucose
case fructose // Fructose
case lactose // Lactose
case maltose // Maltose
case maltodextrins // Maltodextrins
case starch // Starch
case polyols // Sugar alcohols (Polyols)
case fiber // Dietary fiber
case proteins // Proteins
case casein // Casein
case serumProteins = "serum-proteins" // Serum proteins
case nucleotides // Nucleotides
case salt // Salt
case sodium // Sodium
case alcohol // Alcohol
case vitaminA = "vitamin-a" // Vitamin A
case betaCarotene = "beta-carotene" // Beta carotene
case vitaminD = "vitamin-d" // Vitamin D
case vitaminE = "vitamin-e" // Vitamin E
case vitaminK = "vitamin-k" // Vitamin K
case vitaminC = "vitamin-c" // Vitamin C (ascorbic acid)
case vitaminB1 = "vitamin-b1" // Vitamin B1 (Thiamin)
case vitaminB2 = "vitamin-b2" // Vitamin B2 (Riboflavin)
case vitaminPP = "vitamin-pp" // Vitamin B3 / Vitamin PP (Niacin)
case vitaminB6 = "vitamin-b6" // Vitamin B6 (Pyridoxin)
case vitaminB9 = "vitamin-b9" // Vitamin B9 (Folic acid / Folates)
case vitaminB12 = "vitamin-b12" // Vitamin B12 (Cobalamin)
case biotin // Biotin
case pantothenicAcid = "pantothenic-acid" // Pantothenic acid / Pantothenate (Vitamin B5)
case silica // Silica
case bicarbonate // Bicarbonate
case potassium // Potassium
case chloride // Chloride
case calcium // Calcium
case phosphorus // Phosphorus
case iron // Iron
case magnesium // Magnesium
case zinc // Zinc
case copper // Copper
case manganese // Manganese
case fluoride // Fluoride
case selenium // Selenium
case chromium // Chromium
case molybdenum // Molybdenum
case iodine // Iodine
case caffeine // Caffeine
case taurine // Taurine
case pH // pH
case fruitsVegetablesNuts = "fruits-vegetables-nuts" // Fruits, vegetables, and nuts (minimum)
case collagenMeatProteinRatio = "collagen-meat-protein-ratio" // Collagen/Meat protein ratio (maximum)
case cocoa // Cocoa (minimum)
case chlorophyll = "chlorophyll" // Chlorophyll
case carbonFootprint = "carbon-footprint" // Carbon footprint / CO2 emissions
case nutritionScoreFR = "nutrition-score-fr" // Experimental nutrition score
case nutritionScoreUK = "nutrition-score-uk" // Nutrition score - UK
}

View File

@@ -0,0 +1,30 @@
import FoundationEssentials
public enum SearchParameter: Sendable, Hashable {
case query(String)
case tag(tag: SearchTagType, value: String)
case page(Int)
case pageSize(Int)
case sort(SearchSort)
}
public enum SearchTagType: String, Sendable {
case brands
case categories
case packaging
case labels
case origins
case manufacturingPlaces = "manufacturing_places"
case countries
case additives
case allergens
case traces
case states
}
public enum SearchSort: String, Sendable {
case popularity
case productName = "product_name"
case created
case edited
}

View File

@@ -0,0 +1,17 @@
public enum SearchTag: String, Codable {
case brands // Brands
case categories // Categories
case packaging // Packaging
case labels // Labels
case origins // Origins of ingredients
case manufacturingPlaces = "manufacturing_places" // Manufacturing or processing places
case embCodes = "emb_codes" // Packager codes
case purchasePlaces = "purchase_places" // Purchase places
case stores // Stores
case countries // Countries
case additives // Additives
case allergens // Allergens
case traces // Traces
case nutritionGrades = "nutrition_grades" // Nutrition grades
case states // States
}

View File

@@ -0,0 +1,14 @@
public class SelectedImage: Codable {
public var display: SelectedImageItem?
public var small: SelectedImageItem?
public var thumb: SelectedImageItem?
public init(
display: SelectedImageItem?, small: SelectedImageItem?,
thumb: SelectedImageItem?
) {
self.display = display
self.small = small
self.thumb = thumb
}
}

View File

@@ -0,0 +1,15 @@
public struct SelectedImageItem: Codable {
public var en: String?
public var fr: String?
public var pl: String?
public var url: String {
[en, fr, pl].compactMap { $0 }.first ?? ""
}
public init(en: String?, fr: String?, pl: String?) {
self.en = en
self.fr = fr
self.pl = pl
}
}

View File

@@ -0,0 +1,14 @@
public struct SelectedImages: Codable {
public var front: SelectedImage?
public var ingredients: SelectedImage?
public var nutrition: SelectedImage?
public init(
front: SelectedImage?, ingredients: SelectedImage?,
nutrition: SelectedImage?
) {
self.front = front
self.ingredients = ingredients
self.nutrition = nutrition
}
}

View File

@@ -0,0 +1,19 @@
public struct Source: Codable {
public let fields: [String] = []
public let id: String? = nil
public let images: [String] = []
public let importT: Int = 0
public let manufacturer: String? = nil
public let name: String? = nil
public let url: String? = nil
private enum CodingKeys: String, CodingKey {
case fields
case id
case images
case importT = "import_t"
case manufacturer
case name
case url
}
}