Api v2
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
5
Sources/OpenFoodFactsSDK/Schemas/LanguagesCodes.swift
Normal file
5
Sources/OpenFoodFactsSDK/Schemas/LanguagesCodes.swift
Normal file
@@ -0,0 +1,5 @@
|
||||
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
|
||||
}
|
||||
98
Sources/OpenFoodFactsSDK/Schemas/SearchNutriment.swift
Normal file
98
Sources/OpenFoodFactsSDK/Schemas/SearchNutriment.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
public enum SearchNutriment: String, Codable {
|
||||
case energy // Energy
|
||||
case energyFromFat = "energy-from-fat" // Energy from fat
|
||||
case fat // Fat
|
||||
case saturatedFat = "saturated-fat" // Saturated fat
|
||||
case butyricAcid = "butyric-acid" // Butyric acid (4:0)
|
||||
case caproicAcid = "caproic-acid" // Caproic acid (6:0)
|
||||
case caprylicAcid = "caprylic-acid" // Caprylic acid (8:0)
|
||||
case capricAcid = "capric-acid" // Capric acid (10:0)
|
||||
case lauricAcid = "lauric-acid" // Lauric acid (12:0)
|
||||
case myristicAcid = "myristic-acid" // Myristic acid (14:0)
|
||||
case palmiticAcid = "palmitic-acid" // Palmitic acid (16:0)
|
||||
case stearicAcid = "stearic-acid" // Stearic acid (18:0)
|
||||
case arachidicAcid = "arachidic-acid" // Arachidic acid (20:0)
|
||||
case behenicAcid = "behenic-acid" // Behenic acid (22:0)
|
||||
case lignocericAcid = "lignoceric-acid" // Lignoceric acid (24:0)
|
||||
case ceroticAcid = "cerotic-acid" // Cerotic acid (26:0)
|
||||
case montanicAcid = "montanic-acid" // Montanic acid (28:0)
|
||||
case melissicAcid = "melissic-acid" // Melissic acid (30:0)
|
||||
case monounsaturatedFat = "monounsaturated-fat" // Monounsaturated fat
|
||||
case polyunsaturatedFat = "polyunsaturated-fat" // Polyunsaturated fat
|
||||
case omega3Fat = "omega-3-fat" // Omega 3 fatty acids
|
||||
case alphaLinolenicAcid = "alpha-linolenic-acid" // Alpha-linolenic acid / ALA (18:3 n-3)
|
||||
case eicosapentaenoicAcid = "eicosapentaenoic-acid" // Eicosapentaenoic acid / EPA (20:5 n-3)
|
||||
case docosahexaenoicAcid = "docosahexaenoic-acid" // Docosahexaenoic acid / DHA (22:6 n-3)
|
||||
case omega6Fat = "omega-6-fat" // Omega 6 fatty acids
|
||||
case linoleicAcid = "linoleic-acid" // Linoleic acid / LA (18:2 n-6)
|
||||
case arachidonicAcid = "arachidonic-acid" // Arachidonic acid / AA / ARA (20:4 n-6)
|
||||
case gammaLinolenicAcid = "gamma-linolenic-acid" // Gamma-linolenic acid / GLA (18:3 n-6)
|
||||
case dihomoGammaLinolenicAcid = "dihomo-gamma-linolenic-acid" // Dihomo-gamma-linolenic acid / DGLA (20:3 n-6)
|
||||
case omega9Fat = "omega-9-fat" // Omega 9 fatty acids
|
||||
case oleicAcid = "oleic-acid" // Oleic acid (18:1 n-9)
|
||||
case elaidicAcid = "elaidic-acid" // Elaidic acid (18:1 n-9)
|
||||
case gondoicAcid = "gondoic-acid" // Gondoic acid (20:1 n-9)
|
||||
case meadAcid = "mead-acid" // Mead acid (20:3 n-9)
|
||||
case erucicAcid = "erucic-acid" // Erucic acid (22:1 n-9)
|
||||
case nervonicAcid = "nervonic-acid" // Nervonic acid (24:1 n-9)
|
||||
case transFat = "trans-fat" // Trans fat
|
||||
case cholesterol // Cholesterol
|
||||
case carbohydrates // Carbohydrate
|
||||
case sugars // Sugars
|
||||
case sucrose // Sucrose
|
||||
case glucose // Glucose
|
||||
case fructose // Fructose
|
||||
case lactose // Lactose
|
||||
case maltose // Maltose
|
||||
case maltodextrins // Maltodextrins
|
||||
case starch // Starch
|
||||
case polyols // Sugar alcohols (Polyols)
|
||||
case fiber // Dietary fiber
|
||||
case proteins // Proteins
|
||||
case casein // Casein
|
||||
case serumProteins = "serum-proteins" // Serum proteins
|
||||
case nucleotides // Nucleotides
|
||||
case salt // Salt
|
||||
case sodium // Sodium
|
||||
case alcohol // Alcohol
|
||||
case vitaminA = "vitamin-a" // Vitamin A
|
||||
case betaCarotene = "beta-carotene" // Beta carotene
|
||||
case vitaminD = "vitamin-d" // Vitamin D
|
||||
case vitaminE = "vitamin-e" // Vitamin E
|
||||
case vitaminK = "vitamin-k" // Vitamin K
|
||||
case vitaminC = "vitamin-c" // Vitamin C (ascorbic acid)
|
||||
case vitaminB1 = "vitamin-b1" // Vitamin B1 (Thiamin)
|
||||
case vitaminB2 = "vitamin-b2" // Vitamin B2 (Riboflavin)
|
||||
case vitaminPP = "vitamin-pp" // Vitamin B3 / Vitamin PP (Niacin)
|
||||
case vitaminB6 = "vitamin-b6" // Vitamin B6 (Pyridoxin)
|
||||
case vitaminB9 = "vitamin-b9" // Vitamin B9 (Folic acid / Folates)
|
||||
case vitaminB12 = "vitamin-b12" // Vitamin B12 (Cobalamin)
|
||||
case biotin // Biotin
|
||||
case pantothenicAcid = "pantothenic-acid" // Pantothenic acid / Pantothenate (Vitamin B5)
|
||||
case silica // Silica
|
||||
case bicarbonate // Bicarbonate
|
||||
case potassium // Potassium
|
||||
case chloride // Chloride
|
||||
case calcium // Calcium
|
||||
case phosphorus // Phosphorus
|
||||
case iron // Iron
|
||||
case magnesium // Magnesium
|
||||
case zinc // Zinc
|
||||
case copper // Copper
|
||||
case manganese // Manganese
|
||||
case fluoride // Fluoride
|
||||
case selenium // Selenium
|
||||
case chromium // Chromium
|
||||
case molybdenum // Molybdenum
|
||||
case iodine // Iodine
|
||||
case caffeine // Caffeine
|
||||
case taurine // Taurine
|
||||
case pH // pH
|
||||
case fruitsVegetablesNuts = "fruits-vegetables-nuts" // Fruits, vegetables, and nuts (minimum)
|
||||
case collagenMeatProteinRatio = "collagen-meat-protein-ratio" // Collagen/Meat protein ratio (maximum)
|
||||
case cocoa // Cocoa (minimum)
|
||||
case chlorophyll = "chlorophyll" // Chlorophyll
|
||||
case carbonFootprint = "carbon-footprint" // Carbon footprint / CO2 emissions
|
||||
case nutritionScoreFR = "nutrition-score-fr" // Experimental nutrition score
|
||||
case nutritionScoreUK = "nutrition-score-uk" // Nutrition score - UK
|
||||
}
|
||||
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
|
||||
}
|
||||
17
Sources/OpenFoodFactsSDK/Schemas/SearchTag.swift
Normal file
17
Sources/OpenFoodFactsSDK/Schemas/SearchTag.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
public enum SearchTag: String, Codable {
|
||||
case brands // Brands
|
||||
case categories // Categories
|
||||
case packaging // Packaging
|
||||
case labels // Labels
|
||||
case origins // Origins of ingredients
|
||||
case manufacturingPlaces = "manufacturing_places" // Manufacturing or processing places
|
||||
case embCodes = "emb_codes" // Packager codes
|
||||
case purchasePlaces = "purchase_places" // Purchase places
|
||||
case stores // Stores
|
||||
case countries // Countries
|
||||
case additives // Additives
|
||||
case allergens // Allergens
|
||||
case traces // Traces
|
||||
case nutritionGrades = "nutrition_grades" // Nutrition grades
|
||||
case states // States
|
||||
}
|
||||
14
Sources/OpenFoodFactsSDK/Schemas/SelectedImage.swift
Normal file
14
Sources/OpenFoodFactsSDK/Schemas/SelectedImage.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
public class SelectedImage: Codable {
|
||||
public var display: SelectedImageItem?
|
||||
public var small: SelectedImageItem?
|
||||
public var thumb: SelectedImageItem?
|
||||
|
||||
public init(
|
||||
display: SelectedImageItem?, small: SelectedImageItem?,
|
||||
thumb: SelectedImageItem?
|
||||
) {
|
||||
self.display = display
|
||||
self.small = small
|
||||
self.thumb = thumb
|
||||
}
|
||||
}
|
||||
15
Sources/OpenFoodFactsSDK/Schemas/SelectedImageItem.swift
Normal file
15
Sources/OpenFoodFactsSDK/Schemas/SelectedImageItem.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
public struct SelectedImageItem: Codable {
|
||||
public var en: String?
|
||||
public var fr: String?
|
||||
public var pl: String?
|
||||
|
||||
public var url: String {
|
||||
[en, fr, pl].compactMap { $0 }.first ?? ""
|
||||
}
|
||||
|
||||
public init(en: String?, fr: String?, pl: String?) {
|
||||
self.en = en
|
||||
self.fr = fr
|
||||
self.pl = pl
|
||||
}
|
||||
}
|
||||
14
Sources/OpenFoodFactsSDK/Schemas/SelectedImages.swift
Normal file
14
Sources/OpenFoodFactsSDK/Schemas/SelectedImages.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
public struct SelectedImages: Codable {
|
||||
public var front: SelectedImage?
|
||||
public var ingredients: SelectedImage?
|
||||
public var nutrition: SelectedImage?
|
||||
|
||||
public init(
|
||||
front: SelectedImage?, ingredients: SelectedImage?,
|
||||
nutrition: SelectedImage?
|
||||
) {
|
||||
self.front = front
|
||||
self.ingredients = ingredients
|
||||
self.nutrition = nutrition
|
||||
}
|
||||
}
|
||||
19
Sources/OpenFoodFactsSDK/Schemas/Source.swift
Normal file
19
Sources/OpenFoodFactsSDK/Schemas/Source.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
public struct Source: Codable {
|
||||
public let fields: [String] = []
|
||||
public let id: String? = nil
|
||||
public let images: [String] = []
|
||||
public let importT: Int = 0
|
||||
public let manufacturer: String? = nil
|
||||
public let name: String? = nil
|
||||
public let url: String? = nil
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case fields
|
||||
case id
|
||||
case images
|
||||
case importT = "import_t"
|
||||
case manufacturer
|
||||
case name
|
||||
case url
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user