import Foundation @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 } }