138 lines
3.7 KiB
Swift
138 lines
3.7 KiB
Swift
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]?
|
|
}
|