Files
swift-openfoodfacts-sdk/Sources/OpenFoodFacts/Schemas/Nutriments.swift
2025-09-13 13:55:31 +02:00

279 lines
7.6 KiB
Swift

import Foundation
import Units
@dynamicMemberLookup
public struct Nutriments: Codable, ObjectDebugger {
private var nutrients: [String: Nutrient] = [:]
private var iterator: Dictionary<String, Nutrient>.Iterator? = nil
private var keys: [String] = []
public init() {}
public subscript(dynamicMember member: String) -> Nutrient? {
nutrients[member]
}
// MARK: - Decoding
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
for key in container.allKeys {
let keyStr = key.stringValue
let parts = keyStr.components(separatedBy: "_")
var nutrientName = "" // Original with -
var field = ""
var isPrepared = false
if let preparedIdx = parts.firstIndex(of: "prepared") {
isPrepared = true
nutrientName = parts[0..<preparedIdx].joined(separator: "_")
let afterParts = Array(parts[(preparedIdx + 1)...])
field = afterParts.joined(separator: "_")
} else {
nutrientName = parts.dropLast().joined(separator: "_")
field = parts.last ?? ""
}
let accessName = nutrientName.replacingOccurrences(
of: "-", with: "_")
if nutrients[accessName] == nil {
nutrients[accessName] = Nutrient(name: nutrientName)
}
let valDouble: Double? = try? container.decode(
Double.self, forKey: key)
let valString: String? = try? container.decode(
String.self, forKey: key)
let currentNutrient = nutrients[accessName]!
switch field {
case "":
if isPrepared {
currentNutrient.preparedValue = valDouble
} else {
currentNutrient.value = valDouble
}
case "value":
if isPrepared {
currentNutrient.preparedValueComputed = valDouble
} else {
currentNutrient.valueComputed = valDouble
}
case "100g":
if isPrepared {
currentNutrient.preparedPer100g = valDouble
} else {
currentNutrient.per100g = valDouble
}
case "serving":
if isPrepared {
currentNutrient.preparedPerServing = valDouble
} else {
currentNutrient.perServing = valDouble
}
case "unit":
if isPrepared {
currentNutrient.preparedUnit = valString
} else {
currentNutrient.unit = valString
}
default:
// Skip unknown fields
break
}
}
}
// MARK: - Encoding
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyCodingKey.self)
for (_, nutrient) in nutrients {
// Base value
if let v = nutrient.value {
let key = AnyCodingKey(stringValue: nutrient.name)
try container.encode(v, forKey: key)
}
// _value
if let v = nutrient.valueComputed {
let key = AnyCodingKey(stringValue: "\(nutrient.name)_value")
try container.encode(v, forKey: key)
}
// _unit
if let u = nutrient.unit {
let key = AnyCodingKey(stringValue: "\(nutrient.name)_unit")
try container.encode(u, forKey: key)
}
// _100g
if let v = nutrient.per100g {
let key = AnyCodingKey(stringValue: "\(nutrient.name)_100g")
try container.encode(v, forKey: key)
}
// _serving
if let v = nutrient.perServing {
let key = AnyCodingKey(stringValue: "\(nutrient.name)_serving")
try container.encode(v, forKey: key)
}
// _prepared
if let v = nutrient.preparedValue {
let key = AnyCodingKey(stringValue: "\(nutrient.name)_prepared")
try container.encode(v, forKey: key)
}
// _prepared_value
if let v = nutrient.preparedValueComputed {
let key = AnyCodingKey(
stringValue: "\(nutrient.name)_prepared_value")
try container.encode(v, forKey: key)
}
// _prepared_unit
if let u = nutrient.preparedUnit {
let key = AnyCodingKey(
stringValue: "\(nutrient.name)_prepared_unit")
try container.encode(u, forKey: key)
}
// _prepared_100g
if let v = nutrient.preparedPer100g {
let key = AnyCodingKey(
stringValue: "\(nutrient.name)_prepared_100g")
try container.encode(v, forKey: key)
}
// _prepared_serving
if let v = nutrient.preparedPerServing {
let key = AnyCodingKey(
stringValue: "\(nutrient.name)_prepared_serving")
try container.encode(v, forKey: key)
}
}
}
}
// MARK: - Nutrient Struct with Units Integration
public class Nutrient { // Class for mutability in decoding
public let name: String // Original name with -
public var value: Double?
public var valueComputed: Double?
public var per100g: Double?
public var perServing: Double?
public var unit: String?
public var preparedValue: Double?
public var preparedValueComputed: Double?
public var preparedPer100g: Double?
public var preparedPerServing: Double?
public var preparedUnit: String?
init(name: String) {
self.name = name
}
// MARK: - Integration with Units.swift
// Computed UnitValue for per100g (fallback to value or valueComputed if per100g is nil)
public var per100gUnitValue: UnitValue<Double>? {
guard let rawValue = per100g ?? value ?? valueComputed,
let unitString = unit,
let unitEnum = Unit(rawValue: unitString)
else {
return nil
}
return UnitValue(value: rawValue, unit: unitEnum)
}
// Computed UnitValue for perServing
public var perServingUnitValue: UnitValue<Double>? {
guard let rawValue = perServing,
let unitString = unit,
let unitEnum = Unit(rawValue: unitString)
else {
return nil
}
return UnitValue(value: rawValue, unit: unitEnum)
}
// Computed UnitValue for preparedPer100g (fallback similar)
public var preparedPer100gUnitValue: UnitValue<Double>? {
guard
let rawValue = preparedPer100g ?? preparedValue
?? preparedValueComputed,
let unitString = preparedUnit ?? unit, // Fallback to main unit if preparedUnit nil
let unitEnum = Unit(rawValue: unitString)
else {
return nil
}
return UnitValue(value: rawValue, unit: unitEnum)
}
// Computed UnitValue for preparedPerServing
public var preparedPerServingUnitValue: UnitValue<Double>? {
guard let rawValue = preparedPerServing,
let unitString = preparedUnit ?? unit,
let unitEnum = Unit(rawValue: unitString)
else {
return nil
}
return UnitValue(value: rawValue, unit: unitEnum)
}
}
// MARK: - Helper for Dynamic Coding Keys
struct AnyCodingKey: CodingKey, Hashable {
var stringValue: String
var intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init(intValue: Int) {
self.intValue = intValue
self.stringValue = String(intValue)
}
}
extension Nutriments {
public var alcohol: Nutrient? { nutrients["alcohol"] }
public var carbohydrates: Nutrient? { nutrients["carbohydrates"] }
public var energy: Nutrient? { nutrients["energy"] }
public var energyKj: Nutrient? { nutrients["energy_kj"] }
public var energyKcal: Nutrient? { nutrients["energy_kcal"] }
public var fat: Nutrient? { nutrients["fat"] }
public var fruitsVegetablesNuts: Nutrient? {
nutrients["fruits_vegetables_nuts_estimate_from_ingredients"]
}
public var novaGroup: Nutrient? { nutrients["nova_group"] }
public var proteins: Nutrient? { nutrients["proteins"] }
public var salt: Nutrient? { nutrients["salt"] }
public var saturatedFat: Nutrient? { nutrients["saturated_fat"] }
public var sodium: Nutrient? { nutrients["sodium"] }
public var sugars: Nutrient? { nutrients["sugars"] }
public var calcium: Nutrient? { nutrients["calcium"] }
}
extension Nutriments: Sequence, IteratorProtocol {
public mutating func next() -> Element? {
if iterator == nil {
iterator = nutrients.makeIterator()
keys = Array(nutrients.keys).sorted()
}
guard let nextElement = iterator?.next() else {
iterator = nil
return nil
}
return (item: nextElement.key, nutrient: nextElement.value)
}
public typealias Element = (item: String, nutrient: Nutrient)
}