Api v2
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "45ae84ae5c5095f4d85c870c36fff7e4577f267c4a02fa550762071d2f9eca53",
|
||||
"originHash" : "a15a32e3876322ac74f426a1b87f358f2c06dfd7393379edff4f4fb556b95a3b",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swift-units",
|
||||
@@ -7,7 +7,7 @@
|
||||
"location" : "git@git.cems.dev:cdricms/swift-units.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "0962ae290044d24722610234cb215b748722f196"
|
||||
"revision" : "edb2bdd02240855675e9e2d3d7f2c51c2aeac2ff"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -4,35 +4,36 @@
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "swift-openfoodfacts-sdk",
|
||||
platforms: [
|
||||
.macOS(.v14),
|
||||
.iOS(.v15),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "OpenFoodFacts",
|
||||
targets: ["OpenFoodFacts"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: "git@git.cems.dev:cdricms/swift-units.git", branch: "master")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.target(
|
||||
name: "OpenFoodFacts",
|
||||
dependencies: [
|
||||
.product(name: "Units", package: "swift-units")
|
||||
],
|
||||
path: "Sources/OpenFoodFacts",
|
||||
),
|
||||
.testTarget(
|
||||
name: "OpenFoodFactsTests",
|
||||
dependencies: ["OpenFoodFacts"]
|
||||
),
|
||||
]
|
||||
name: "swift-openfoodfacts-sdk",
|
||||
platforms: [
|
||||
.macOS(.v12), .iOS(.v15), .tvOS(.v15), .watchOS(.v8),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "OpenFoodFactsSDK",
|
||||
targets: ["OpenFoodFactsSDK"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: "git@git.cems.dev:cdricms/swift-units.git", branch: "master")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.target(
|
||||
name: "OpenFoodFactsSDK",
|
||||
dependencies: [
|
||||
.product(name: "Units", package: "swift-units")
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("SwiftConcurrency")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "OpenFoodFactsTests",
|
||||
dependencies: ["OpenFoodFactsSDK"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
extension String {
|
||||
func camelCaseToSnakeCase() -> String {
|
||||
var result = ""
|
||||
var lastCharacterWasUppercase = false
|
||||
|
||||
for character in self {
|
||||
if character.isUppercase {
|
||||
if !result.isEmpty && !lastCharacterWasUppercase {
|
||||
result.append("_")
|
||||
}
|
||||
result.append(character.lowercased())
|
||||
lastCharacterWasUppercase = true
|
||||
} else {
|
||||
result.append(character)
|
||||
lastCharacterWasUppercase = false
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// The Swift Programming Language
|
||||
// https://docs.swift.org/swift-book
|
||||
|
||||
import Foundation
|
||||
|
||||
public class OpenFoodFactsClient {
|
||||
let version: Int = 0
|
||||
public var prod: Bool = false
|
||||
var baseURL: URL? {
|
||||
if prod {
|
||||
return URL(
|
||||
string: "https://world.openfoodfacts.org/api/v\(version)")
|
||||
} else {
|
||||
return URL(
|
||||
string: "https://world.openfoodfacts.net/api/v\(version)")
|
||||
}
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func getProductByBarcode(_ barcode: String) async throws
|
||||
-> ProductResponse
|
||||
{
|
||||
guard
|
||||
let endpoint = baseURL?.appendingPathComponent("product/\(barcode)")
|
||||
else { throw OFFError.invalidURL }
|
||||
var request = URLRequest(url: endpoint)
|
||||
request.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse,
|
||||
response.statusCode == 200
|
||||
else {
|
||||
throw OFFError.invalidResponse
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(ProductResponse.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
public struct SearchQuery {
|
||||
public let additivesTags: String?
|
||||
public let allergensTags: String?
|
||||
public let brandsTags: String?
|
||||
public let categoriesTags: String?
|
||||
public let countriesTagsEn: String?
|
||||
public let embCodesTags: String?
|
||||
public let labelsTags: String?
|
||||
public let manufacturingPlacesTags: String?
|
||||
public let nutritionGradesTags: String?
|
||||
public let originsTags: String?
|
||||
public let packagingTagsDe: String?
|
||||
public let purchasePlacesTags: String?
|
||||
}
|
||||
|
||||
// public func search(_ productName: String, queryParams: SearchQuery? = nil) async throws -> SearchResponse {
|
||||
// let qp = Mirror(reflecting: queryParams ?? {})
|
||||
// var s: String = "?product_name=\(productName)&"
|
||||
// for case let (label?, value) in qp.children {
|
||||
// s += label.camelCaseToSnakeCase() + "=" + (value as! String) + "&"
|
||||
// }
|
||||
// guard let endpoint = baseURL?.appendingPathComponent("search\(s)") else { throw OFFError.invalidURL }
|
||||
// var request = URLRequest(url: endpoint)
|
||||
// request.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
|
||||
// let (data, response) = try await URLSession.shared.data(for: request)
|
||||
// guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
|
||||
// throw OFFError.invalidResponse
|
||||
// }
|
||||
// do {
|
||||
// return try JSONDecoder().decode(SearchResponse.self, from: data)
|
||||
// }
|
||||
// }
|
||||
|
||||
//https://wiki.openfoodfacts.org/API/Read/Search#Parameters
|
||||
public func search(query: PerlSearchQuery) async throws -> SearchResponse {
|
||||
let endpoint = URL(
|
||||
string:
|
||||
"https://world.openfoodfacts.org/cgi/search.pl?\(query.makeToRequest())"
|
||||
)!
|
||||
print(endpoint)
|
||||
let request = URLRequest(url: endpoint)
|
||||
// request.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse,
|
||||
response.statusCode == 200
|
||||
else {
|
||||
throw OFFError.invalidResponse
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(SearchResponse.self, from: data)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
enum OFFError: Error {
|
||||
case invalidURL, invalidResponse
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct Images: Codable {
|
||||
public var otherData: [String: Data] = [:]
|
||||
|
||||
mutating func setDetail<T: Encodable>(key: String, value: T) throws {
|
||||
let encodedValue = try JSONEncoder().encode(value)
|
||||
otherData[key] = encodedValue
|
||||
}
|
||||
|
||||
func getDetail<T: Decodable>(key: String, type: T.Type) throws -> T? {
|
||||
guard let data = otherData[key] else { return nil }
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
public struct Ingredient: Codable, ObjectDebugger {
|
||||
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
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
public struct NutrientLevels: Codable, ObjectDebugger {
|
||||
public var fat: String? = nil
|
||||
public var salt: String? = nil
|
||||
public var saturatedFat: String? = nil
|
||||
public var sugars: String? = nil
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case fat
|
||||
case salt
|
||||
case saturatedFat = "saturated-fat"
|
||||
case sugars
|
||||
}
|
||||
|
||||
public subscript(_ key: CodingKeys) -> String? {
|
||||
switch key {
|
||||
case .fat: fat
|
||||
case .salt: salt
|
||||
case .saturatedFat: saturatedFat
|
||||
case .sugars: sugars
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
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"] }
|
||||
|
||||
public var otherNutrients: [String: Nutrient] {
|
||||
var others = nutrients
|
||||
others.removeValue(forKey: "alcohol")
|
||||
others.removeValue(forKey: "carbohydrates")
|
||||
others.removeValue(forKey: "energy")
|
||||
others.removeValue(forKey: "energy_kj")
|
||||
others.removeValue(forKey: "energy_kcal")
|
||||
others.removeValue(forKey: "fat")
|
||||
others.removeValue(
|
||||
forKey: "fruits_vegetables_nuts_estimate_from_ingredients")
|
||||
others.removeValue(forKey: "nova_group")
|
||||
others.removeValue(forKey: "proteins")
|
||||
others.removeValue(forKey: "salt")
|
||||
others.removeValue(forKey: "saturated_fat")
|
||||
others.removeValue(forKey: "sodium")
|
||||
others.removeValue(forKey: "sugars")
|
||||
others.removeValue(forKey: "calcium")
|
||||
|
||||
return others
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
public protocol ObjectDebugger: CustomStringConvertible {
|
||||
var description: String { get }
|
||||
}
|
||||
|
||||
extension ObjectDebugger {
|
||||
public var description: String {
|
||||
var description = "\(type(of: self))("
|
||||
let mirror = Mirror(reflecting: self)
|
||||
|
||||
for (label, value) in mirror.children {
|
||||
if let label = label {
|
||||
description += "\(label): \(value), "
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the trailing comma and space
|
||||
description = String(description.dropLast(2))
|
||||
description += ")"
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
private func prettyPrint(object: Any, indentation: String = "") -> String {
|
||||
var description = "\(type(of: object)) {"
|
||||
|
||||
let mirror = Mirror(reflecting: object)
|
||||
|
||||
for (label, value) in mirror.children {
|
||||
if let label = label {
|
||||
let childDescription: String
|
||||
switch value {
|
||||
case let nestedObject as CustomStringConvertible:
|
||||
childDescription = prettyPrint(
|
||||
object: nestedObject, indentation: "\(indentation) ")
|
||||
case let stringValue as String:
|
||||
childDescription = "\"\(stringValue)\""
|
||||
case let floatValue as Float:
|
||||
childDescription = "\(floatValue)"
|
||||
case let intValue as Int:
|
||||
childDescription = "\(intValue)"
|
||||
case let optionalValue as CustomStringConvertible?:
|
||||
if let unwrapped = optionalValue {
|
||||
childDescription = "\(unwrapped)"
|
||||
} else {
|
||||
childDescription = "nil"
|
||||
}
|
||||
default:
|
||||
childDescription = "\(value)"
|
||||
}
|
||||
|
||||
description += "\n\(indentation) \(label): \(childDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
description += "\n\(indentation)}"
|
||||
return description
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
public enum PerlOperator: String {
|
||||
case lt, lte, gt, gte, eq
|
||||
}
|
||||
|
||||
public enum PerlFormat: String {
|
||||
case json, xml, jqm
|
||||
}
|
||||
|
||||
public struct SearchNutrimentEntry {
|
||||
public let nutriment: SearchNutriment
|
||||
public let op: PerlOperator
|
||||
public let value: Int
|
||||
}
|
||||
|
||||
public struct SearchTagsEntry {
|
||||
public let tag: SearchTag
|
||||
public let value: String
|
||||
public let contains: Bool = true
|
||||
}
|
||||
|
||||
//https://wiki.openfoodfacts.org/API/Read/Search#Parameters
|
||||
public struct PerlSearchQuery {
|
||||
public var searchTerms: String
|
||||
public var searchTags: [SearchTagsEntry]? // tagtype_i=SearchTag — i as #element (starting from 0); (tag_contains_i|tag_does_not_contain_i)=String
|
||||
public var searchNutriment: [SearchNutrimentEntry]? // nutriment_i=SearchNutriment — i as #element (starting from 0); nutriment_compare_i=PerlOperator; nutriment_value_i=String
|
||||
public var page: Int = 1 // Pagination
|
||||
public var format: PerlFormat // json=1 | xml=1 | jqm=1
|
||||
|
||||
public init(
|
||||
searchTerms: String, searchTags: [SearchTagsEntry]? = nil,
|
||||
searchNutriment: [SearchNutrimentEntry]? = nil, page: Int = 1,
|
||||
format: PerlFormat = .json
|
||||
) {
|
||||
self.searchTerms = searchTerms
|
||||
self.searchTags = searchTags
|
||||
self.searchNutriment = searchNutriment
|
||||
self.page = page
|
||||
self.format = format
|
||||
}
|
||||
|
||||
public func makeToRequest() -> String {
|
||||
var _searchTags: String?
|
||||
var _searchNutriment: String?
|
||||
let _format: String = "\(format.rawValue)=1"
|
||||
let _page: String = "page=\(page)"
|
||||
if let tags = searchTags {
|
||||
_searchTags = tags.enumerated().map { i, v in
|
||||
return
|
||||
"tagtype_\(i)=\(v.tag.rawValue)&tag_contains_\(i)=\(v.contains ? "contains" : "does_not_contain")&tag_\(i)=\(v.value)"
|
||||
}.joined(separator: "&")
|
||||
}
|
||||
if let nutriments = searchNutriment {
|
||||
_searchNutriment = nutriments.enumerated().map { i, v in
|
||||
return
|
||||
"nutriment_\(i)=\(v.nutriment.rawValue)&nutriment_compare_\(i)=\(v.op.rawValue)&nutriment_value_\(i)=\(v.nutriment.rawValue)"
|
||||
}.joined(separator: "&")
|
||||
}
|
||||
|
||||
return
|
||||
"search_terms=\(searchTerms)\(_searchTags != nil ? "&\(_searchTags!)" : "")\(_searchNutriment != nil ? "&\(_searchNutriment!)" : "")&\(_page)&\(_format)"
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,973 +0,0 @@
|
||||
import Units
|
||||
|
||||
public class Product: Codable, ObjectDebugger {
|
||||
// public var images: Images? = Images()
|
||||
public var ingredients: [Ingredient]? = []
|
||||
public var languagesCodes: LanguagesCodes?
|
||||
public var nutrientLevels: NutrientLevels?
|
||||
public var nutriments: Nutriments? = Nutriments()
|
||||
public var selectedImages: SelectedImages?
|
||||
public var sources: [Source]? = []
|
||||
public var additivesN: Float?
|
||||
public var additivesOldN: Float?
|
||||
public var additivesOriginalTags: [String]?
|
||||
public var additivesOldTags: [String]?
|
||||
public var additivesPrevOriginalTags: [String]?
|
||||
public var additivesDebugTags: [String]?
|
||||
public var additivesTags: [String]?
|
||||
public var allergens: String?
|
||||
public var allergensFromIngredients: String?
|
||||
public var allergensFromUser: String?
|
||||
public var allergensHierarchy: [String]?
|
||||
public var allergensLc: String?
|
||||
public var allergensTags: [String]?
|
||||
public var aminoAcidsPrevTags: [String]?
|
||||
public var aminoAcidsTags: [String]?
|
||||
public var brands: String?
|
||||
public var brandsDebugTags: [String]?
|
||||
public var brandsTags: [String]?
|
||||
public var carbonFootprintPercentOfKnownIngredients: Float?
|
||||
public var carbonFootprintFromKnownIngredientsDebug: String?
|
||||
public var categories: String?
|
||||
public var categoriesHierarchy: [String]?
|
||||
public var categoriesLc: String?
|
||||
public var categoriesPropertiesTags: [String]?
|
||||
public var categoriesTags: [String]?
|
||||
public var checkersTags: [String]?
|
||||
public var citiesTags: [String]?
|
||||
public var code: String?
|
||||
public var codesTags: [String]?
|
||||
public var comparedToCategory: String?
|
||||
public var complete: Float?
|
||||
public var completedT: Float?
|
||||
public var completeness: Double?
|
||||
public var conservationConditions: String?
|
||||
public var countries: String?
|
||||
public var countriesHierarchy: [String]?
|
||||
public var countriesLc: String?
|
||||
public var countriesDebugTags: [String]?
|
||||
public var countriesTags: [String]?
|
||||
public var correctorsTags: [String]?
|
||||
public var createdT: Float?
|
||||
public var creator: String?
|
||||
public var dataQualityBugsTags: [String]?
|
||||
public var dataQualityErrorsTags: [String]?
|
||||
public var dataQualityInfoTags: [String]?
|
||||
public var dataQualityTags: [String]?
|
||||
public var dataQualityWarningsTags: [String]?
|
||||
public var dataSources: String?
|
||||
public var dataSourcesTags: [String]?
|
||||
public var debugParamSortedLangs: [String]?
|
||||
public var editorsTags: [String]?
|
||||
public var embCodes: String?
|
||||
public var embCodesDebugTags: [String]?
|
||||
public var embCodesOrig: String?
|
||||
public var embCodesTags: [String]?
|
||||
public var entryDatesTags: [String]?
|
||||
public var expirationDate: String?
|
||||
public var expirationDateDebugTags: [String]?
|
||||
public var fruitsVegetablesNuts100GEstimate: Float?
|
||||
public var genericName: String?
|
||||
public var id: String?
|
||||
public var imageFrontSmallUrl: String?
|
||||
public var imageFrontThumbUrl: String?
|
||||
public var imageFrontUrl: String?
|
||||
public var imageIngredientsUrl: String?
|
||||
public var imageIngredientsSmallUrl: String?
|
||||
public var imageIngredientsThumbUrl: String?
|
||||
public var imageNutritionSmallUrl: String?
|
||||
public var imageNutritionThumbUrl: String?
|
||||
public var imageNutritionUrl: String?
|
||||
public var imageSmallUrl: String?
|
||||
public var imageThumbUrl: String?
|
||||
public var imageUrl: String?
|
||||
public var informersTags: [String]?
|
||||
public var ingredientsAnalysisTags: [String]?
|
||||
public var ingredientsDebug: [String?]?
|
||||
public var ingredientsFromOrThatMayBeFromPalmOilN: Float?
|
||||
public var ingredientsFromPalmOilTags: [String]?
|
||||
public var ingredientsFromPalmOilN: Float?
|
||||
public var ingredientsHierarchy: [String]?
|
||||
public var ingredientsIdsDebug: [String]?
|
||||
public var ingredientsN: Float?
|
||||
public var ingredientsNTags: [String]?
|
||||
public var ingredientsOriginalTags: [String]?
|
||||
public var ingredientsTags: [String]?
|
||||
public var ingredientsText: String?
|
||||
public var ingredientsTextDebug: String?
|
||||
public var ingredientsTextWithAllergens: String?
|
||||
public var ingredientsThatMayBeFromPalmOilN: Float?
|
||||
public var ingredientsThatMayBeFromPalmOilTags: [String]?
|
||||
public var interfaceVersionCreated: String?
|
||||
public var interfaceVersionModified: String?
|
||||
public var keywords: [String]?
|
||||
public var knownIngredientsN: Float?
|
||||
public var labels: String?
|
||||
public var labelsHierarchy: [String]?
|
||||
public var labelsLc: String?
|
||||
public var labelsPrevHierarchy: [String]?
|
||||
public var labelsPrevTags: [String]?
|
||||
public var labelsTags: [String]?
|
||||
public var labelsDebugTags: [String]?
|
||||
public var lang: String?
|
||||
public var langDebugTags: [String]?
|
||||
public var languagesHierarchy: [String]?
|
||||
public var languagesTags: [String]?
|
||||
public var lastEditDatesTags: [String]?
|
||||
public var lastEditor: String?
|
||||
public var lastImageDatesTags: [String]?
|
||||
public var lastImageT: Float?
|
||||
public var lastModifiedBy: String?
|
||||
public var lastModifiedT: Float?
|
||||
public var lc: String?
|
||||
public var link: String?
|
||||
public var linkDebugTags: [String]?
|
||||
public var manufacturingPlaces: String?
|
||||
public var manufacturingPlacesDebugTags: [String]?
|
||||
public var manufacturingPlacesTags: [String]?
|
||||
public var maxImgid: String?
|
||||
public var mineralsPrevTags: [String]?
|
||||
public var mineralsTags: [String]?
|
||||
public var miscTags: [String]?
|
||||
public var netWeightUnit: String?
|
||||
public var netWeightValue: String?
|
||||
public var nutritionDataPer: String?
|
||||
public var nutritionScoreWarningNoFruitsVegetablesNuts: Float?
|
||||
public var nutriscoreGrade: String?
|
||||
public var noNutritionData: String?
|
||||
public var novaGroup: Float?
|
||||
public var novaGroups: String?
|
||||
public var novaGroupDebug: String?
|
||||
public var novaGroupTags: [String]?
|
||||
public var novaGroupsTags: [String]?
|
||||
public var nucleotidesPrevTags: [String]?
|
||||
public var nucleotidesTags: [String]?
|
||||
public var nutrientLevelsTags: [String]?
|
||||
public var nutritionData: String?
|
||||
public var nutritionDataPerDebugTags: [String]?
|
||||
public var nutritionDataPrepared: String?
|
||||
public var nutritionDataPreparedPer: String?
|
||||
public var nutritionGrades: String?
|
||||
public var nutritionScoreBeverage: Float?
|
||||
public var nutritionScoreDebug: String?
|
||||
public var nutritionScoreWarningNoFiber: Float?
|
||||
public var nutritionGradesTags: [String]?
|
||||
public var origins: String?
|
||||
public var originsDebugTags: [String]?
|
||||
public var originsTags: [String]?
|
||||
public var otherInformation: String?
|
||||
public var otherNutritionalSubstancesTags: [String]?
|
||||
public var packaging: String?
|
||||
public var packagingDebugTags: [String]?
|
||||
public var packagingTags: [String]?
|
||||
public var photographersTags: [String]?
|
||||
public var pnnsGroups1: String?
|
||||
public var pnnsGroups2: String?
|
||||
public var pnnsGroups1Tags: [String]?
|
||||
public var pnnsGroups2Tags: [String]?
|
||||
public var popularityKey: Float?
|
||||
public var producerVersionId: String?
|
||||
public var productName: String?
|
||||
public var productQuantity: Float?
|
||||
public var productQuantityUnit: String?
|
||||
public var purchasePlaces: String?
|
||||
public var purchasePlacesDebugTags: [String]?
|
||||
public var purchasePlacesTags: [String]?
|
||||
public var qualityTags: [String]?
|
||||
public var quantity: String?
|
||||
public var quantityDebugTags: [String]?
|
||||
public var recyclingInstructionsToDiscard: String?
|
||||
public var rev: Float?
|
||||
public var servingQuantity: String?
|
||||
public var servingQuantityUnit: String?
|
||||
public var servingSize: String?
|
||||
public var servingSizeDebugTags: [String]?
|
||||
public var sortkey: Float?
|
||||
public var states: String?
|
||||
public var statesHierarchy: [String]?
|
||||
public var statesTags: [String]?
|
||||
public var stores: String?
|
||||
public var storesDebugTags: [String]?
|
||||
public var storesTags: [String]?
|
||||
public var traces: String?
|
||||
public var tracesFromIngredients: String?
|
||||
public var tracesHierarchy: [String]?
|
||||
public var tracesDebugTags: [String]?
|
||||
public var tracesFromUser: String?
|
||||
public var tracesLc: String?
|
||||
public var tracesTags: [String]?
|
||||
public var unknownIngredientsN: Float?
|
||||
public var unknownNutrientsTags: [String]?
|
||||
public var updateKey: String?
|
||||
public var vitaminsPrevTags: [String]?
|
||||
public var vitaminsTags: [String]?
|
||||
|
||||
public var waterQuantity: Units.UnitValue<Double>? {
|
||||
guard let ingredient = ingredients?.first(where: { $0.isWater })
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let quantity = productQuantity,
|
||||
let estimate = ingredient.percentEstimate,
|
||||
let rawUnit = productQuantityUnit,
|
||||
let unit = Units.Unit(rawValue: rawUnit)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Double(quantity * (estimate / 100))[unit]
|
||||
}
|
||||
|
||||
public var isLiquid: Bool {
|
||||
guard let rawUnit = productQuantityUnit,
|
||||
let unit = Units.Unit(rawValue: rawUnit)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return unit.category == .volume
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
// case images
|
||||
case ingredients
|
||||
case languagesCodes = "languages_codes"
|
||||
case nutrientLevels = "nutrient_levels"
|
||||
case nutriments
|
||||
case selectedImages = "selected_images"
|
||||
case sources
|
||||
case additivesN = "additives_n"
|
||||
case additivesOldN = "additives_old_n"
|
||||
case additivesOriginalTags = "additives_original_tags"
|
||||
case additivesOldTags = "additives_old_tags"
|
||||
case additivesPrevOriginalTags = "additives_prev_original_tags"
|
||||
case additivesDebugTags = "additives_debug_tags"
|
||||
case additivesTags = "additives_tags"
|
||||
case allergens
|
||||
case allergensFromIngredients = "allergens_from_ingredients"
|
||||
case allergensFromUser = "allergens_from_user"
|
||||
case allergensHierarchy = "allergens_hierarchy"
|
||||
case allergensLc = "allergens_lc"
|
||||
case allergensTags = "allergens_tags"
|
||||
case aminoAcidsPrevTags = "amino_acids_prev_tags"
|
||||
case aminoAcidsTags = "amino_acids_tags"
|
||||
case brands
|
||||
case brandsDebugTags = "brands_debug_tags"
|
||||
case brandsTags = "brands_tags"
|
||||
case carbonFootprintPercentOfKnownIngredients =
|
||||
"carbon_footprint_percent_of_known_ingredients"
|
||||
case carbonFootprintFromKnownIngredientsDebug =
|
||||
"carbon_footprint_from_known_ingredients_debug"
|
||||
case categories
|
||||
case categoriesHierarchy = "categories_hierarchy"
|
||||
case categoriesLc = "categories_lc"
|
||||
case categoriesPropertiesTags = "categories_properties_tags"
|
||||
case categoriesTags = "categories_tags"
|
||||
case checkersTags = "checkers_tags"
|
||||
case citiesTags = "cities_tags"
|
||||
case code
|
||||
case codesTags = "codes_tags"
|
||||
case comparedToCategory = "compared_to_category"
|
||||
case complete
|
||||
case completedT = "completed_t"
|
||||
case completeness
|
||||
case conservationConditions = "conservation_conditions"
|
||||
case countries
|
||||
case countriesHierarchy = "countries_hierarchy"
|
||||
case countriesLc = "countries_lc"
|
||||
case countriesDebugTags = "countries_debug_tags"
|
||||
case countriesTags = "countries_tags"
|
||||
case correctorsTags = "correctors_tags"
|
||||
case createdT = "created_t"
|
||||
case creator
|
||||
case dataQualityBugsTags = "data_quality_bugs_tags"
|
||||
case dataQualityErrorsTags = "data_quality_errors_tags"
|
||||
case dataQualityInfoTags = "data_quality_info_tags"
|
||||
case dataQualityTags = "data_quality_tags"
|
||||
case dataQualityWarningsTags = "data_quality_warnings_tags"
|
||||
case dataSources = "data_sources"
|
||||
case dataSourcesTags = "data_sources_tags"
|
||||
case debugParamSortedLangs = "debug_param_sorted_langs"
|
||||
case editorsTags = "editors_tags"
|
||||
case embCodes = "emb_codes"
|
||||
case embCodesDebugTags = "emb_codes_debug_tags"
|
||||
case embCodesOrig = "emb_codes_orig"
|
||||
case embCodesTags = "emb_codes_tags"
|
||||
case entryDatesTags = "entry_dates_tags"
|
||||
case expirationDate = "expiration_date"
|
||||
case expirationDateDebugTags = "expiration_date_debug_tags"
|
||||
case fruitsVegetablesNuts100GEstimate =
|
||||
"fruits-vegetables-nuts_100g_estimate"
|
||||
case genericName
|
||||
case id
|
||||
case imageFrontSmallUrl = "image_front_small_url"
|
||||
case imageFrontThumbUrl = "image_front_thumb_url"
|
||||
case imageFrontUrl = "image_front_url"
|
||||
case imageIngredientsUrl = "image_ingredients_url"
|
||||
case imageIngredientsSmallUrl = "image_ingredients_small_url"
|
||||
case imageIngredientsThumbUrl = "image_ingredients_thumb_url"
|
||||
case imageNutritionSmallUrl = "image_nutrition_small_url"
|
||||
case imageNutritionThumbUrl = "image_nutrition_thumb_url"
|
||||
case imageNutritionUrl = "image_nutrition_url"
|
||||
case imageSmallUrl = "image_small_url"
|
||||
case imageThumbUrl = "image_thumb_url"
|
||||
case imageUrl = "image_url"
|
||||
case informersTags = "informers_tags"
|
||||
case ingredientsAnalysisTags = "ingredients_analysis_tags"
|
||||
case ingredientsDebug = "ingredients_debug"
|
||||
case ingredientsFromOrThatMayBeFromPalmOilN =
|
||||
"ingredients_from_or_that_may_be_from_palm_oil_n"
|
||||
case ingredientsFromPalmOilTags = "ingredients_from_palm_oil_tags"
|
||||
case ingredientsFromPalmOilN = "ingredients_from_palm_oil_n"
|
||||
case ingredientsHierarchy = "ingredients_hierarchy"
|
||||
case ingredientsIdsDebug = "ingredients_ids_debug"
|
||||
case ingredientsN = "ingredients_n"
|
||||
case ingredientsNTags = "ingredients_n_tags"
|
||||
case ingredientsOriginalTags = "ingredients_original_tags"
|
||||
case ingredientsTags = "ingredients_tags"
|
||||
case ingredientsText = "ingredients_text"
|
||||
case ingredientsTextDebug = "ingredients_text_debug"
|
||||
case ingredientsTextWithAllergens = "ingredients_text_with_allergens"
|
||||
case ingredientsThatMayBeFromPalmOilN =
|
||||
"ingredients_that_may_be_from_palm_oil_n"
|
||||
case ingredientsThatMayBeFromPalmOilTags =
|
||||
"ingredients_that_may_be_from_palm_oil_tags"
|
||||
case interfaceVersionCreated = "interface_version_created"
|
||||
case interfaceVersionModified = "interface_version_modified"
|
||||
case keywords
|
||||
case knownIngredientsN = "known_ingredients_n"
|
||||
case labels
|
||||
case labelsHierarchy = "labels_hierarchy"
|
||||
case labelsLc = "labels_lc"
|
||||
case labelsPrevHierarchy = "labels_prev_hierarchy"
|
||||
case labelsPrevTags = "labels_prev_tags"
|
||||
case labelsTags = "labels_tags"
|
||||
case labelsDebugTags = "labels_debug_tags"
|
||||
case lang
|
||||
case langDebugTags = "lang_debug_tags"
|
||||
case languagesHierarchy = "languages_hierarchy"
|
||||
case languagesTags = "languages_tags"
|
||||
case lastEditDatesTags = "last_edit_dates_tags"
|
||||
case lastEditor = "last_editor"
|
||||
case lastImageDatesTags = "last_image_dates_tags"
|
||||
case lastImageT = "last_image_t"
|
||||
case lastModifiedBy = "last_modified_by"
|
||||
case lastModifiedT = "last_modified_t"
|
||||
case lc
|
||||
case link
|
||||
case linkDebugTags = "link_debug_tags"
|
||||
case manufacturingPlaces = "manufacturing_places"
|
||||
case manufacturingPlacesDebugTags = "manufacturing_places_debug_tags"
|
||||
case manufacturingPlacesTags = "manufacturing_places_tags"
|
||||
case maxImgid = "max_imgid"
|
||||
case mineralsPrevTags = "minerals_prev_tags"
|
||||
case mineralsTags = "minerals_tags"
|
||||
case miscTags = "misc_tags"
|
||||
case netWeightUnit = "net_weight_unit"
|
||||
case netWeightValue = "net_weight_value"
|
||||
case nutritionDataPer = "nutrition_data_per"
|
||||
case nutritionScoreWarningNoFruitsVegetablesNuts =
|
||||
"nutrition_score_warning_no_fruits_vegetables_nuts"
|
||||
case nutriscoreGrade = "nutriscore_grade"
|
||||
case noNutritionData = "no_nutrition_data"
|
||||
case novaGroup = "nova_group"
|
||||
case novaGroups = "nova_groups"
|
||||
case novaGroupDebug = "nova_group_debug"
|
||||
case novaGroupTags = "nova_group_tags"
|
||||
case novaGroupsTags = "nova_groups_tags"
|
||||
case nucleotidesPrevTags = "nucleotides_prev_tags"
|
||||
case nucleotidesTags = "nucleotides_tags"
|
||||
case nutrientLevelsTags = "nutrient_levels_tags"
|
||||
case nutritionData = "nutrition_data"
|
||||
case nutritionDataPerDebugTags = "nutrition_data_per_debug_tags"
|
||||
case nutritionDataPrepared = "nutrition_data_prepared"
|
||||
case nutritionDataPreparedPer = "nutrition_data_prepared_per"
|
||||
case nutritionGrades = "nutrition_grades"
|
||||
case nutritionScoreBeverage = "nutrition_score_beverage"
|
||||
case nutritionScoreDebug = "nutrition_score_debug"
|
||||
case nutritionScoreWarningNoFiber = "nutrition_score_warning_no_fiber"
|
||||
case nutritionGradesTags = "nutrition_grades_tags"
|
||||
case origins
|
||||
case originsDebugTags = "origins_debug_tags"
|
||||
case originsTags = "origins_tags"
|
||||
case otherInformation = "other_information"
|
||||
case otherNutritionalSubstancesTags =
|
||||
"other_nutritional_substances_tags"
|
||||
case packaging
|
||||
case packagingDebugTags = "packaging_debug_tags"
|
||||
case packagingTags = "packaging_tags"
|
||||
case photographersTags = "photographers_tags"
|
||||
case pnnsGroups1 = "pnns_groups_1"
|
||||
case pnnsGroups2 = "pnns_groups_2"
|
||||
case pnnsGroups1Tags = "pnns_groups_1_tags"
|
||||
case pnnsGroups2Tags = "pnns_groups_2_tags"
|
||||
case popularityKey = "popularity_key"
|
||||
case producerVersionId = "producer_version_id"
|
||||
case productName = "product_name"
|
||||
case productQuantity = "product_quantity"
|
||||
case productQuantityUnit = "product_quantity_unit"
|
||||
case purchasePlaces = "purchase_places"
|
||||
case purchasePlacesDebugTags = "purchase_places_debug_tags"
|
||||
case purchasePlacesTags = "purchase_places_tags"
|
||||
case qualityTags = "quality_tags"
|
||||
case quantity
|
||||
case quantityDebugTags = "quantity_debug_tags"
|
||||
case recyclingInstructionsToDiscard =
|
||||
"recycling_instructions_to_discard"
|
||||
case rev
|
||||
case servingQuantity = "serving_quantity"
|
||||
case servingQuantityUnit = "serving_quantity_unit"
|
||||
case servingSize = "serving_size"
|
||||
case servingSizeDebugTags = "serving_size_debug_tags"
|
||||
case sortkey
|
||||
case states
|
||||
case statesHierarchy = "states_hierarchy"
|
||||
case statesTags = "states_tags"
|
||||
case stores
|
||||
case storesDebugTags = "stores_debug_tags"
|
||||
case storesTags = "stores_tags"
|
||||
case traces
|
||||
case tracesFromIngredients = "traces_from_ingredients"
|
||||
case tracesHierarchy = "traces_hierarchy"
|
||||
case tracesDebugTags = "traces_debug_tags"
|
||||
case tracesFromUser = "traces_from_user"
|
||||
case tracesLc = "traces_lc"
|
||||
case tracesTags = "traces_tags"
|
||||
case unknownIngredientsN = "unknown_ingredients_n"
|
||||
case unknownNutrientsTags = "unknown_nutrients_tags"
|
||||
case updateKey = "update_key"
|
||||
case vitaminsPrevTags = "vitamins_prev_tags"
|
||||
case vitaminsTags = "vitamins_tags"
|
||||
}
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
ingredients = try container.decodeIfPresent(
|
||||
[Ingredient].self, forKey: .ingredients)
|
||||
languagesCodes = try container.decodeIfPresent(
|
||||
LanguagesCodes.self, forKey: .languagesCodes)
|
||||
nutrientLevels = try container.decodeIfPresent(
|
||||
NutrientLevels.self, forKey: .nutrientLevels)
|
||||
nutriments =
|
||||
try container.decodeIfPresent(Nutriments.self, forKey: .nutriments)
|
||||
?? Nutriments()
|
||||
selectedImages = try container.decodeIfPresent(
|
||||
SelectedImages.self, forKey: .selectedImages)
|
||||
sources = try container.decodeIfPresent([Source].self, forKey: .sources)
|
||||
additivesN = try container.decodeIfPresent(
|
||||
Float.self, forKey: .additivesN)
|
||||
additivesOldN = try container.decodeIfPresent(
|
||||
Float.self, forKey: .additivesOldN)
|
||||
additivesOriginalTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .additivesOriginalTags)
|
||||
additivesOldTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .additivesOldTags)
|
||||
additivesPrevOriginalTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .additivesPrevOriginalTags)
|
||||
additivesDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .additivesDebugTags)
|
||||
additivesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .additivesTags)
|
||||
allergens = try container.decodeIfPresent(
|
||||
String.self, forKey: .allergens)
|
||||
allergensFromIngredients = try container.decodeIfPresent(
|
||||
String.self, forKey: .allergensFromIngredients)
|
||||
allergensFromUser = try container.decodeIfPresent(
|
||||
String.self, forKey: .allergensFromUser)
|
||||
allergensHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .allergensHierarchy)
|
||||
allergensLc = try container.decodeIfPresent(
|
||||
String.self, forKey: .allergensLc)
|
||||
allergensTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .allergensTags)
|
||||
aminoAcidsPrevTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .aminoAcidsPrevTags)
|
||||
aminoAcidsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .aminoAcidsTags)
|
||||
brands = try container.decodeIfPresent(String.self, forKey: .brands)
|
||||
brandsDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .brandsDebugTags)
|
||||
brandsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .brandsTags)
|
||||
carbonFootprintPercentOfKnownIngredients =
|
||||
try container.decodeIfPresent(
|
||||
Float.self, forKey: .carbonFootprintPercentOfKnownIngredients)
|
||||
carbonFootprintFromKnownIngredientsDebug =
|
||||
try container.decodeIfPresent(
|
||||
String.self, forKey: .carbonFootprintFromKnownIngredientsDebug)
|
||||
categories = try container.decodeIfPresent(
|
||||
String.self, forKey: .categories)
|
||||
categoriesHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .categoriesHierarchy)
|
||||
categoriesLc = try container.decodeIfPresent(
|
||||
String.self, forKey: .categoriesLc)
|
||||
categoriesPropertiesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .categoriesPropertiesTags)
|
||||
categoriesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .categoriesTags)
|
||||
checkersTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .checkersTags)
|
||||
citiesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .citiesTags)
|
||||
code = try container.decodeIfPresent(String.self, forKey: .code)
|
||||
codesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .codesTags)
|
||||
comparedToCategory = try container.decodeIfPresent(
|
||||
String.self, forKey: .comparedToCategory)
|
||||
complete = try container.decodeIfPresent(Float.self, forKey: .complete)
|
||||
completedT = try container.decodeIfPresent(
|
||||
Float.self, forKey: .completedT)
|
||||
completeness = try container.decodeIfPresent(
|
||||
Double.self, forKey: .completeness)
|
||||
conservationConditions = try container.decodeIfPresent(
|
||||
String.self, forKey: .conservationConditions)
|
||||
countries = try container.decodeIfPresent(
|
||||
String.self, forKey: .countries)
|
||||
countriesHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .countriesHierarchy)
|
||||
countriesLc = try container.decodeIfPresent(
|
||||
String.self, forKey: .countriesLc)
|
||||
countriesDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .countriesDebugTags)
|
||||
countriesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .countriesTags)
|
||||
correctorsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .correctorsTags)
|
||||
createdT = try container.decodeIfPresent(Float.self, forKey: .createdT)
|
||||
creator = try container.decodeIfPresent(String.self, forKey: .creator)
|
||||
dataQualityBugsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .dataQualityBugsTags)
|
||||
dataQualityErrorsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .dataQualityErrorsTags)
|
||||
dataQualityInfoTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .dataQualityInfoTags)
|
||||
dataQualityTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .dataQualityTags)
|
||||
dataQualityWarningsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .dataQualityWarningsTags)
|
||||
dataSources = try container.decodeIfPresent(
|
||||
String.self, forKey: .dataSources)
|
||||
dataSourcesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .dataSourcesTags)
|
||||
debugParamSortedLangs = try container.decodeIfPresent(
|
||||
[String].self, forKey: .debugParamSortedLangs)
|
||||
editorsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .editorsTags)
|
||||
embCodes = try container.decodeIfPresent(String.self, forKey: .embCodes)
|
||||
embCodesDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .embCodesDebugTags)
|
||||
embCodesOrig = try container.decodeIfPresent(
|
||||
String.self, forKey: .embCodesOrig)
|
||||
embCodesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .embCodesTags)
|
||||
entryDatesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .entryDatesTags)
|
||||
expirationDate = try container.decodeIfPresent(
|
||||
String.self, forKey: .expirationDate)
|
||||
expirationDateDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .expirationDateDebugTags)
|
||||
fruitsVegetablesNuts100GEstimate = try container.decodeIfPresent(
|
||||
Float.self, forKey: .fruitsVegetablesNuts100GEstimate)
|
||||
genericName = try container.decodeIfPresent(
|
||||
String.self, forKey: .genericName)
|
||||
id = try container.decodeIfPresent(String.self, forKey: .id)
|
||||
imageFrontSmallUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageFrontSmallUrl)
|
||||
imageFrontThumbUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageFrontThumbUrl)
|
||||
imageFrontUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageFrontUrl)
|
||||
imageIngredientsUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageIngredientsUrl)
|
||||
imageIngredientsSmallUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageIngredientsSmallUrl)
|
||||
imageIngredientsThumbUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageIngredientsThumbUrl)
|
||||
imageNutritionSmallUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageNutritionSmallUrl)
|
||||
imageNutritionThumbUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageNutritionThumbUrl)
|
||||
imageNutritionUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageNutritionUrl)
|
||||
imageSmallUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageSmallUrl)
|
||||
imageThumbUrl = try container.decodeIfPresent(
|
||||
String.self, forKey: .imageThumbUrl)
|
||||
imageUrl = try container.decodeIfPresent(String.self, forKey: .imageUrl)
|
||||
informersTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .informersTags)
|
||||
ingredientsAnalysisTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsAnalysisTags)
|
||||
ingredientsDebug = try container.decodeIfPresent(
|
||||
[String?].self, forKey: .ingredientsDebug)
|
||||
ingredientsFromOrThatMayBeFromPalmOilN = try container.decodeIfPresent(
|
||||
Float.self, forKey: .ingredientsFromOrThatMayBeFromPalmOilN)
|
||||
ingredientsFromPalmOilTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsFromPalmOilTags)
|
||||
ingredientsFromPalmOilN = try container.decodeIfPresent(
|
||||
Float.self, forKey: .ingredientsFromPalmOilN)
|
||||
ingredientsHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsHierarchy)
|
||||
ingredientsIdsDebug = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsIdsDebug)
|
||||
if container.contains(.ingredientsN) {
|
||||
if try container.decodeNil(forKey: .ingredientsN) {
|
||||
ingredientsN = nil
|
||||
} else {
|
||||
if let intValue = try? container.decode(
|
||||
Float.self, forKey: .ingredientsN)
|
||||
{
|
||||
ingredientsN = intValue
|
||||
} else if let stringValue = try? container.decode(
|
||||
String.self, forKey: .ingredientsN)
|
||||
{
|
||||
ingredientsN = Float(stringValue)
|
||||
} else {
|
||||
// If decoding as both Float and String fails, handle the error accordingly
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .ingredientsN,
|
||||
in: container,
|
||||
debugDescription: "Unable to decode ingredientsN"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
ingredientsN = nil
|
||||
}
|
||||
ingredientsNTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsNTags)
|
||||
ingredientsOriginalTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsOriginalTags)
|
||||
ingredientsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsTags)
|
||||
ingredientsText = try container.decodeIfPresent(
|
||||
String.self, forKey: .ingredientsText)
|
||||
ingredientsTextDebug = try container.decodeIfPresent(
|
||||
String.self, forKey: .ingredientsTextDebug)
|
||||
ingredientsTextWithAllergens = try container.decodeIfPresent(
|
||||
String.self, forKey: .ingredientsTextWithAllergens)
|
||||
ingredientsThatMayBeFromPalmOilN = try container.decodeIfPresent(
|
||||
Float.self, forKey: .ingredientsThatMayBeFromPalmOilN)
|
||||
ingredientsThatMayBeFromPalmOilTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .ingredientsThatMayBeFromPalmOilTags)
|
||||
interfaceVersionCreated = try container.decodeIfPresent(
|
||||
String.self, forKey: .interfaceVersionCreated)
|
||||
interfaceVersionModified = try container.decodeIfPresent(
|
||||
String.self, forKey: .interfaceVersionModified)
|
||||
keywords = try container.decodeIfPresent(
|
||||
[String].self, forKey: .keywords)
|
||||
knownIngredientsN = try container.decodeIfPresent(
|
||||
Float.self, forKey: .knownIngredientsN)
|
||||
labels = try container.decodeIfPresent(String.self, forKey: .labels)
|
||||
labelsHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .labelsHierarchy)
|
||||
labelsLc = try container.decodeIfPresent(String.self, forKey: .labelsLc)
|
||||
labelsPrevHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .labelsPrevHierarchy)
|
||||
labelsPrevTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .labelsPrevTags)
|
||||
labelsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .labelsTags)
|
||||
labelsDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .labelsDebugTags)
|
||||
lang = try container.decodeIfPresent(String.self, forKey: .lang)
|
||||
langDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .langDebugTags)
|
||||
languagesHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .languagesHierarchy)
|
||||
languagesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .languagesTags)
|
||||
lastEditDatesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .lastEditDatesTags)
|
||||
lastEditor = try container.decodeIfPresent(
|
||||
String.self, forKey: .lastEditor)
|
||||
lastImageDatesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .lastImageDatesTags)
|
||||
lastImageT = try container.decodeIfPresent(
|
||||
Float.self, forKey: .lastImageT)
|
||||
lastModifiedBy = try container.decodeIfPresent(
|
||||
String.self, forKey: .lastModifiedBy)
|
||||
if container.contains(.lastModifiedT) {
|
||||
if try container.decodeNil(forKey: .maxImgid) {
|
||||
lastModifiedT = nil
|
||||
} else {
|
||||
if let floatValue = try? container.decode(
|
||||
Float.self, forKey: .lastModifiedT)
|
||||
{
|
||||
lastModifiedT = floatValue
|
||||
} else if let stringValue = try? container.decode(
|
||||
String.self, forKey: .lastModifiedT)
|
||||
{
|
||||
lastModifiedT = Float(stringValue)
|
||||
} else {
|
||||
// If decoding as both Float and String fails, handle the error accordingly
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .lastModifiedT,
|
||||
in: container,
|
||||
debugDescription: "Unable to decode lastModifiedT"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
lastModifiedT = nil
|
||||
}
|
||||
lc = try container.decodeIfPresent(String.self, forKey: .lc)
|
||||
link = try container.decodeIfPresent(String.self, forKey: .link)
|
||||
linkDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .linkDebugTags)
|
||||
manufacturingPlaces = try container.decodeIfPresent(
|
||||
String.self, forKey: .manufacturingPlaces)
|
||||
manufacturingPlacesDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .manufacturingPlacesDebugTags)
|
||||
manufacturingPlacesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .manufacturingPlacesTags)
|
||||
if container.contains(.maxImgid) {
|
||||
if try container.decodeNil(forKey: .maxImgid) {
|
||||
maxImgid = nil
|
||||
} else {
|
||||
if let floatValue = try? container.decode(
|
||||
Float.self, forKey: .maxImgid)
|
||||
{
|
||||
maxImgid = "\(floatValue)"
|
||||
} else if let stringValue = try? container.decode(
|
||||
String.self, forKey: .maxImgid)
|
||||
{
|
||||
maxImgid = stringValue
|
||||
} else {
|
||||
// If decoding as both Float and String fails, handle the error accordingly
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .maxImgid,
|
||||
in: container,
|
||||
debugDescription: "Unable to decode maxImgid"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
maxImgid = nil
|
||||
}
|
||||
mineralsPrevTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .mineralsPrevTags)
|
||||
mineralsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .mineralsTags)
|
||||
miscTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .miscTags)
|
||||
netWeightUnit = try container.decodeIfPresent(
|
||||
String.self, forKey: .netWeightUnit)
|
||||
netWeightValue = try container.decodeIfPresent(
|
||||
String.self, forKey: .netWeightValue)
|
||||
nutritionDataPer = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutritionDataPer)
|
||||
nutritionScoreWarningNoFruitsVegetablesNuts =
|
||||
try container.decodeIfPresent(
|
||||
Float.self, forKey: .nutritionScoreWarningNoFruitsVegetablesNuts
|
||||
)
|
||||
noNutritionData = try container.decodeIfPresent(
|
||||
String.self, forKey: .noNutritionData)
|
||||
novaGroup = try container.decodeIfPresent(
|
||||
Float.self, forKey: .novaGroup)
|
||||
novaGroups = try container.decodeIfPresent(
|
||||
String.self, forKey: .novaGroups)
|
||||
novaGroupDebug = try container.decodeIfPresent(
|
||||
String.self, forKey: .novaGroupDebug)
|
||||
novaGroupTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .novaGroupTags)
|
||||
novaGroupsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .novaGroupsTags)
|
||||
nucleotidesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .nucleotidesTags)
|
||||
nutrientLevelsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .nutrientLevelsTags)
|
||||
nutritionData = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutritionData)
|
||||
nutriscoreGrade = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutriscoreGrade)
|
||||
nutritionDataPerDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .nutritionDataPerDebugTags)
|
||||
nutritionDataPrepared = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutritionDataPrepared)
|
||||
nutritionDataPreparedPer = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutritionDataPreparedPer)
|
||||
nutritionGrades = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutritionGrades)
|
||||
nutritionScoreBeverage = try container.decodeIfPresent(
|
||||
Float.self, forKey: .nutritionScoreBeverage)
|
||||
nutritionScoreDebug = try container.decodeIfPresent(
|
||||
String.self, forKey: .nutritionScoreDebug)
|
||||
nutritionScoreWarningNoFiber = try container.decodeIfPresent(
|
||||
Float.self, forKey: .nutritionScoreWarningNoFiber)
|
||||
nutritionGradesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .nutritionGradesTags)
|
||||
origins = try container.decodeIfPresent(String.self, forKey: .origins)
|
||||
originsDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .originsDebugTags)
|
||||
originsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .originsTags)
|
||||
otherInformation = try container.decodeIfPresent(
|
||||
String.self, forKey: .otherInformation)
|
||||
otherNutritionalSubstancesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .otherNutritionalSubstancesTags)
|
||||
packaging = try container.decodeIfPresent(
|
||||
String.self, forKey: .packaging)
|
||||
packagingDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .packagingDebugTags)
|
||||
packagingTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .packagingTags)
|
||||
photographersTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .photographersTags)
|
||||
pnnsGroups1 = try container.decodeIfPresent(
|
||||
String.self, forKey: .pnnsGroups1)
|
||||
pnnsGroups2 = try container.decodeIfPresent(
|
||||
String.self, forKey: .pnnsGroups2)
|
||||
pnnsGroups1Tags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .pnnsGroups1Tags)
|
||||
pnnsGroups2Tags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .pnnsGroups2Tags)
|
||||
popularityKey = try container.decodeIfPresent(
|
||||
Float.self, forKey: .popularityKey)
|
||||
producerVersionId = try container.decodeIfPresent(
|
||||
String.self, forKey: .producerVersionId)
|
||||
productName = try container.decodeIfPresent(
|
||||
String.self, forKey: .productName)
|
||||
purchasePlaces = try container.decodeIfPresent(
|
||||
String.self, forKey: .purchasePlaces)
|
||||
purchasePlacesDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .purchasePlacesDebugTags)
|
||||
purchasePlacesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .purchasePlacesTags)
|
||||
qualityTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .qualityTags)
|
||||
quantity = try container.decodeIfPresent(String.self, forKey: .quantity)
|
||||
quantityDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .quantityDebugTags)
|
||||
recyclingInstructionsToDiscard = try container.decodeIfPresent(
|
||||
String.self, forKey: .recyclingInstructionsToDiscard)
|
||||
rev = try container.decodeIfPresent(Float.self, forKey: .rev)
|
||||
if container.contains(.servingQuantity) {
|
||||
if try container.decodeNil(forKey: .servingQuantity) {
|
||||
servingQuantity = nil
|
||||
} else {
|
||||
if let floatValue = try? container.decode(
|
||||
Float.self, forKey: .servingQuantity)
|
||||
{
|
||||
servingQuantity = "\(floatValue)"
|
||||
} else if let stringValue = try? container.decode(
|
||||
String.self, forKey: .servingQuantity)
|
||||
{
|
||||
servingQuantity = stringValue
|
||||
} else {
|
||||
// If decoding as both Float and String fails, handle the error accordingly
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .servingQuantity,
|
||||
in: container,
|
||||
debugDescription: "Unable to decode servingQuantity"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
servingQuantity = nil
|
||||
}
|
||||
servingQuantityUnit = try container.decodeIfPresent(
|
||||
String.self,
|
||||
forKey:
|
||||
.servingQuantityUnit)
|
||||
servingSize = try container.decodeIfPresent(
|
||||
String.self, forKey: .servingSize)
|
||||
servingSizeDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .servingSizeDebugTags)
|
||||
sortkey = try container.decodeIfPresent(Float.self, forKey: .sortkey)
|
||||
states = try container.decodeIfPresent(String.self, forKey: .states)
|
||||
statesHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .statesHierarchy)
|
||||
statesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .statesTags)
|
||||
stores = try container.decodeIfPresent(String.self, forKey: .stores)
|
||||
storesDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .storesDebugTags)
|
||||
storesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .storesTags)
|
||||
traces = try container.decodeIfPresent(String.self, forKey: .traces)
|
||||
tracesFromIngredients = try container.decodeIfPresent(
|
||||
String.self, forKey: .tracesFromIngredients)
|
||||
tracesHierarchy = try container.decodeIfPresent(
|
||||
[String].self, forKey: .tracesHierarchy)
|
||||
tracesDebugTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .tracesDebugTags)
|
||||
tracesFromUser = try container.decodeIfPresent(
|
||||
String.self, forKey: .tracesFromUser)
|
||||
tracesLc = try container.decodeIfPresent(String.self, forKey: .tracesLc)
|
||||
tracesTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .tracesTags)
|
||||
if container.contains(.unknownIngredientsN) {
|
||||
if try container.decodeNil(forKey: .unknownIngredientsN) {
|
||||
unknownIngredientsN = nil
|
||||
} else {
|
||||
if let intValue = try? container.decode(
|
||||
Float.self, forKey: .unknownIngredientsN)
|
||||
{
|
||||
unknownIngredientsN = intValue
|
||||
} else if let stringValue = try? container.decode(
|
||||
String.self, forKey: .unknownIngredientsN)
|
||||
{
|
||||
unknownIngredientsN = Float(stringValue)
|
||||
} else {
|
||||
// If decoding as both Float and String fails, handle the error accordingly
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .unknownIngredientsN,
|
||||
in: container,
|
||||
debugDescription: "Unable to decode unknownIngredientsN"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
unknownIngredientsN = nil
|
||||
}
|
||||
unknownNutrientsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .unknownNutrientsTags)
|
||||
updateKey = try container.decodeIfPresent(
|
||||
String.self, forKey: .updateKey)
|
||||
vitaminsPrevTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .vitaminsPrevTags)
|
||||
vitaminsTags = try container.decodeIfPresent(
|
||||
[String].self, forKey: .vitaminsTags)
|
||||
|
||||
// Check for null value
|
||||
if container.contains(.productQuantity) {
|
||||
if try container.decodeNil(forKey: .productQuantity) {
|
||||
productQuantity = nil
|
||||
} else {
|
||||
// Try to decode as Float
|
||||
if let floatValue = try? container.decode(
|
||||
Float.self, forKey: .productQuantity)
|
||||
{
|
||||
productQuantity = floatValue
|
||||
} else if let stringValue = try? container.decode(
|
||||
String.self, forKey: .productQuantity)
|
||||
{
|
||||
// If decoding as Float fails, try to decode as String
|
||||
productQuantity = Float(stringValue)
|
||||
} else {
|
||||
// If decoding as both Float and String fails, handle the error accordingly
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .productQuantity,
|
||||
in: container,
|
||||
debugDescription: "Unable to decode productQuantity"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
productQuantity = nil
|
||||
}
|
||||
|
||||
productQuantityUnit = try container.decodeIfPresent(
|
||||
String.self,
|
||||
forKey:
|
||||
.productQuantityUnit)
|
||||
// ... (initialize other properties)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
public struct ProductResponse: Codable, ObjectDebugger {
|
||||
public var product: Product?
|
||||
public var code: String?
|
||||
public var status: Int? // or Bool, depending on your needs
|
||||
public var statusVerbose: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case product
|
||||
case code
|
||||
case status
|
||||
case statusVerbose = "status_verbose"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
public struct SearchResponse: Codable, ObjectDebugger {
|
||||
public var count: Int
|
||||
public var page: Int
|
||||
public var pageCount: Int
|
||||
public var pageSize: Int
|
||||
public var products: [Product]?
|
||||
public var skip: Int
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case count, page, products, skip
|
||||
case pageCount = "page_count"
|
||||
case pageSize = "page_size"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
extension KeyedDecodingContainer {
|
||||
public func decodeFloatOrString(forKey key: Key) throws -> Float? {
|
||||
if let floatVal = try? decode(Float.self, forKey: key) {
|
||||
return floatVal
|
||||
}
|
||||
if let stringVal = try? decode(String.self, forKey: key) {
|
||||
return Float(stringVal)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
12
Sources/OpenFoodFactsSDK/Extensions/String+Cases.swift
Normal file
12
Sources/OpenFoodFactsSDK/Extensions/String+Cases.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
public func camelCaseToSnakeCase() -> String {
|
||||
let pattern = "([a-z0-9])([A-Z])"
|
||||
let regex = try! NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(location: 0, length: self.count)
|
||||
return regex.stringByReplacingMatches(
|
||||
in: self, options: [], range: range, withTemplate: "$1_$2"
|
||||
).lowercased()
|
||||
}
|
||||
}
|
||||
137
Sources/OpenFoodFactsSDK/OpenFoodFactsClient.swift
Normal file
137
Sources/OpenFoodFactsSDK/OpenFoodFactsClient.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
import FoundationEssentials
|
||||
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
public actor OpenFoodFactsClient {
|
||||
private let config: OpenFoodFactsConfig
|
||||
private let session: URLSession
|
||||
|
||||
public init(config: OpenFoodFactsConfig, session: URLSession = .shared) {
|
||||
self.config = config
|
||||
self.session = session
|
||||
}
|
||||
|
||||
// MARK: - API V2 Implementation
|
||||
|
||||
/// Fetch a product by barcode.
|
||||
/// - Parameters:
|
||||
/// - barcode: The product barcode.
|
||||
/// - fields: Optional list of fields to fetch (optimizes network usage).
|
||||
public func product(barcode: String, fields: [ProductField]? = nil)
|
||||
async throws -> Product?
|
||||
{
|
||||
var url = config.apiURL.appendingPathComponent("/product/\(barcode)")
|
||||
|
||||
// Append fields parameter if present
|
||||
if let fields = fields, !fields.isEmpty {
|
||||
let fieldString = fields.map { $0.rawValue }.joined(separator: ",")
|
||||
if var components = URLComponents(
|
||||
url: url, resolvingAgainstBaseURL: true)
|
||||
{
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "fields", value: fieldString)
|
||||
]
|
||||
if let newUrl = components.url { url = newUrl }
|
||||
}
|
||||
}
|
||||
|
||||
let request = buildRequest(url: url)
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode)
|
||||
else {
|
||||
throw OFFError.invalidResponse
|
||||
}
|
||||
|
||||
let envelope = try JSONDecoder().decode(
|
||||
ProductResponseEnvelope.self, from: data)
|
||||
return envelope.product
|
||||
}
|
||||
|
||||
/// Search for products using V2 API.
|
||||
public func search(
|
||||
_ parameters: SearchParameter..., fields: [ProductField]? = nil
|
||||
) async throws -> [Product] {
|
||||
guard
|
||||
var components = URLComponents(
|
||||
url: config.apiURL.appendingPathComponent("/search"),
|
||||
resolvingAgainstBaseURL: true)
|
||||
else {
|
||||
throw OFFError.invalidURL
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem] = []
|
||||
|
||||
// Add requested fields
|
||||
if let fields = fields, !fields.isEmpty {
|
||||
let fieldVal = fields.map { $0.rawValue }.joined(separator: ",")
|
||||
queryItems.append(URLQueryItem(name: "fields", value: fieldVal))
|
||||
}
|
||||
|
||||
let parameters = Set(parameters)
|
||||
// Add search parameters
|
||||
for param in parameters {
|
||||
switch param {
|
||||
case .query(let q):
|
||||
queryItems.append(URLQueryItem(name: "search_terms", value: q))
|
||||
case .tag(let tag, let value):
|
||||
// V2 allows dynamic tags like `brands_tags=coca`
|
||||
queryItems.append(
|
||||
URLQueryItem(name: "\(tag.rawValue)_tags", value: value))
|
||||
case .page(let p):
|
||||
queryItems.append(URLQueryItem(name: "page", value: String(p)))
|
||||
case .pageSize(let s):
|
||||
queryItems.append(
|
||||
URLQueryItem(name: "page_size", value: String(s)))
|
||||
case .sort(let s):
|
||||
queryItems.append(
|
||||
URLQueryItem(name: "sort_by", value: s.rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
components.queryItems = queryItems
|
||||
guard let url = components.url else { throw OFFError.invalidURL }
|
||||
|
||||
let request = buildRequest(url: url)
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode)
|
||||
else {
|
||||
throw OFFError.invalidResponse
|
||||
}
|
||||
|
||||
let envelope = try JSONDecoder().decode(
|
||||
SearchResponseEnvelope.self, from: data)
|
||||
return envelope.products ?? []
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private func buildRequest(url: URL) -> URLRequest {
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(
|
||||
config.userAgent.description, forHTTPHeaderField: "User-Agent")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
public enum OFFError: Error {
|
||||
case invalidURL, invalidResponse
|
||||
}
|
||||
|
||||
// Internal Envelopes for Decoding
|
||||
struct ProductResponseEnvelope: Decodable {
|
||||
let code: String?
|
||||
let product: Product?
|
||||
let status: Int?
|
||||
}
|
||||
|
||||
struct SearchResponseEnvelope: Decodable {
|
||||
let count: Int?
|
||||
let products: [Product]?
|
||||
}
|
||||
66
Sources/OpenFoodFactsSDK/OpenFoodFactsConfig.swift
Normal file
66
Sources/OpenFoodFactsSDK/OpenFoodFactsConfig.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
import FoundationEssentials
|
||||
|
||||
public struct OpenFoodFactsConfig: Sendable {
|
||||
public let baseURL: URL
|
||||
public let userAgent: UserAgent
|
||||
public let apiURL: URL
|
||||
|
||||
public struct UserAgent: CustomStringConvertible, Sendable {
|
||||
public let appName: String
|
||||
public let version: String
|
||||
public let contactInfo: String
|
||||
|
||||
/// AppName/Version (Contact info)
|
||||
public var description: String {
|
||||
"\(appName)/\(version) (\(contactInfo))"
|
||||
}
|
||||
|
||||
public init(appName: String, version: String, contactInfo: String) {
|
||||
self.appName = appName
|
||||
self.version = version
|
||||
self.contactInfo = contactInfo
|
||||
}
|
||||
|
||||
public init?(from userAgent: String) {
|
||||
guard userAgent.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
let splitted = userAgent.split(separator: " ")
|
||||
guard splitted.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let appNameVersion = splitted[0].split(separator: "/")
|
||||
guard appNameVersion.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.appName = String(appNameVersion[0])
|
||||
self.version = String(appNameVersion[1])
|
||||
self.contactInfo = String(splitted[1])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum Environment: Sendable {
|
||||
case production
|
||||
case staging
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .production:
|
||||
return URL(string: "https://world.openfoodfacts.org")!
|
||||
case .staging:
|
||||
return URL(string: "https://world.openfoodfacts.net")!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - userAgent: Crucial for OFF. Format: "AppName/Version (Contact info)"
|
||||
public init(environment: Environment = .production, userAgent: UserAgent) {
|
||||
self.baseURL = environment.url
|
||||
self.userAgent = userAgent
|
||||
self.apiURL = self.baseURL.appendingPathComponent("/api/v2")
|
||||
}
|
||||
}
|
||||
56
Sources/OpenFoodFactsSDK/Schemas/Ingredient.swift
Normal file
56
Sources/OpenFoodFactsSDK/Schemas/Ingredient.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
public struct LanguagesCodes: Codable, ObjectDebugger {
|
||||
public struct LanguagesCodes: Sendable, Codable {
|
||||
public var en: Float? = nil
|
||||
public var fr: Float? = nil
|
||||
public var pl: Float? = nil
|
||||
28
Sources/OpenFoodFactsSDK/Schemas/NutrientLevels.swift
Normal file
28
Sources/OpenFoodFactsSDK/Schemas/NutrientLevels.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
52
Sources/OpenFoodFactsSDK/Schemas/Nutriments.swift
Normal file
52
Sources/OpenFoodFactsSDK/Schemas/Nutriments.swift
Normal 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 }
|
||||
}
|
||||
98
Sources/OpenFoodFactsSDK/Schemas/Product.swift
Normal file
98
Sources/OpenFoodFactsSDK/Schemas/Product.swift
Normal 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
20
Sources/OpenFoodFactsSDK/Schemas/ProductField.swift
Normal file
20
Sources/OpenFoodFactsSDK/Schemas/ProductField.swift
Normal 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
|
||||
}
|
||||
30
Sources/OpenFoodFactsSDK/Schemas/SearchParameter.swift
Normal file
30
Sources/OpenFoodFactsSDK/Schemas/SearchParameter.swift
Normal 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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
public class SelectedImage: Codable, ObjectDebugger {
|
||||
public class SelectedImage: Codable {
|
||||
public var display: SelectedImageItem?
|
||||
public var small: SelectedImageItem?
|
||||
public var thumb: SelectedImageItem?
|
||||
@@ -1,4 +1,4 @@
|
||||
public struct SelectedImageItem: Codable, ObjectDebugger {
|
||||
public struct SelectedImageItem: Codable {
|
||||
public var en: String?
|
||||
public var fr: String?
|
||||
public var pl: String?
|
||||
@@ -1,4 +1,4 @@
|
||||
public struct SelectedImages: Codable, ObjectDebugger {
|
||||
public struct SelectedImages: Codable {
|
||||
public var front: SelectedImage?
|
||||
public var ingredients: SelectedImage?
|
||||
public var nutrition: SelectedImage?
|
||||
@@ -1,4 +1,4 @@
|
||||
public struct Source: Codable, ObjectDebugger {
|
||||
public struct Source: Codable {
|
||||
public let fields: [String] = []
|
||||
public let id: String? = nil
|
||||
public let images: [String] = []
|
||||
@@ -1,31 +1,50 @@
|
||||
@testable import OpenFoodFacts
|
||||
import XCTest
|
||||
|
||||
final class swift_openfoodfacts_sdkTests: XCTestCase {
|
||||
func testBarcodeProcessing() async throws {
|
||||
let off = OpenFoodFactsClient()
|
||||
off.prod = true
|
||||
@testable import OpenFoodFactsSDK
|
||||
|
||||
for barcode in ["0737628064502", "0812133010036", "0849092103196", "22007377", "3033610048398", "3222473161867", "3242272260059", "3245412470929", "3502110000880", "3551100749018", "3560070805259", "3560070976867", "3596710352418", "3800020430781", "4000539770708", "4388858946739", "5010251168577", "5015821151720", "5050854517631", "5054070608074", "5201051001076", "5410228196693", "5449000179661", "5601077161035", "6194002510064", "7311041026670", "7640101710236", "8424259826051", "8585002476821", "8712000031312", "8992696419766", "9300601768226", "9300650658615", "9310155100335"] {
|
||||
do {
|
||||
let _ = try await off.getProductByBarcode(barcode)
|
||||
} catch {
|
||||
XCTFail("[BARCODE: \(barcode)] \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
final class OpenFoodFactsTests: XCTestCase {
|
||||
|
||||
func testPerlSearch() async throws {
|
||||
let off = OpenFoodFactsClient()
|
||||
do {
|
||||
// try await off.search(query: .init(searchTerms: "prince", format: .json))
|
||||
let _ = try await off.search(query: .init(
|
||||
searchTerms: "",
|
||||
searchTags: [.init(tag: .brands, value: "mondelez"), .init(tag: .countries, value: "france")],
|
||||
format: .json
|
||||
))
|
||||
} catch {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
}
|
||||
var client: OpenFoodFactsClient!
|
||||
|
||||
override func setUp() {
|
||||
let config = OpenFoodFactsConfig(
|
||||
environment: .staging,
|
||||
userAgent: .init(
|
||||
appName: "SwiftOpenFoodFactsTests", version: "1.0",
|
||||
contactInfo: "(test@test.com)")
|
||||
)
|
||||
client = OpenFoodFactsClient(config: config)
|
||||
}
|
||||
|
||||
func testProductFetch() async throws {
|
||||
// Fetch specific fields only
|
||||
let product = try await client.product(
|
||||
barcode: "3017620422003", // Nutella
|
||||
fields: [.code, .productName, .nutriscoreGrade, .nutriments]
|
||||
)
|
||||
|
||||
XCTAssertEqual(product?.code, "3017620422003")
|
||||
XCTAssertNotNil(product?.productName)
|
||||
XCTAssertNotNil(product?.nutriscoreGrade)
|
||||
// This field was NOT requested, so it should be nil (if decoding handles strict optionals)
|
||||
// or the struct just holds nil.
|
||||
}
|
||||
|
||||
func testSearch() async throws {
|
||||
let results = try await client.search(
|
||||
.query("chocolate"),
|
||||
.tag(tag: .brands, value: "milka"),
|
||||
.pageSize(5),
|
||||
.sort(.popularity),
|
||||
fields: [.nutrientLevels]
|
||||
)
|
||||
|
||||
let jsonResults = try JSONEncoder().encode(results)
|
||||
try jsonResults.write(to: .init(filePath: "./jsonResults.json"))
|
||||
|
||||
XCTAssertFalse(results.isEmpty)
|
||||
XCTAssertEqual(results.count, 5)
|
||||
// XCTAssertTrue(
|
||||
// results.first?.brands?.lowercased().contains("milka") ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user