Files
swift-openfoodfacts-sdk/Sources/OpenFoodFacts/Schemas/Nutriments.swift
2025-09-13 11:25:35 +02:00

240 lines
6.2 KiB
Swift

import Foundation
import Units
@dynamicMemberLookup
public struct Nutriments: Codable, ObjectDebugger {
private var nutrients: [String: Nutrient] = [:] // Key is accessName ( - replaced with _ )
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)
}
}