Better handling of Nutriments
This commit is contained in:
239
Sources/OpenFoodFacts/Schemas/Nutriments.swift
Normal file
239
Sources/OpenFoodFacts/Schemas/Nutriments.swift
Normal file
@@ -0,0 +1,239 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user