/ BalanceKit / OpenFoodFactsService.swift
OpenFoodFactsService.swift
1 // 2 // OpenFoodFactsService.swift 3 // BalanceKit 4 // 5 // Created by Gemini on 13.07.25. 6 // 7 8 import Foundation 9 10 // MARK: - Data Models for OpenFoodFacts API Response 11 // These structs are designed to decode the JSON response from the API. 12 // We only decode the fields we are interested in. 13 14 struct ProductResponse: Codable { 15 let product: Product? 16 let status: Int 17 let statusVerbose: String 18 19 enum CodingKeys: String, CodingKey { 20 case product 21 case status 22 case statusVerbose = "status_verbose" 23 } 24 } 25 26 struct Product: Codable { 27 let productName: String? 28 let nutriments: Nutriments? 29 30 enum CodingKeys: String, CodingKey { 31 case productName = "product_name_de" // Prefer German product name 32 case nutriments 33 } 34 } 35 36 struct Nutriments: Codable { 37 // Values are often per 100g 38 let energyKcal100G: Double? 39 let proteins100G: Double? 40 let carbohydrates100G: Double? 41 let fat100G: Double? 42 43 enum CodingKeys: String, CodingKey { 44 case energyKcal100G = "energy-kcal_100g" 45 case proteins100G = "proteins_100g" 46 case carbohydrates100G = "carbohydrates_100g" 47 case fat100G = "fat_100g" 48 } 49 } 50 51 52 // MARK: - OpenFoodFactsService 53 // This class handles the communication with the Open Food Facts API. 54 55 class OpenFoodFactsService { 56 57 enum ServiceError: Error { 58 case invalidURL 59 case network(Error) 60 case decoding(Error) 61 case productNotFound 62 case noNutriments 63 } 64 65 /// Fetches product information for a given barcode. 66 /// - Parameter barcode: The EAN barcode string. 67 /// - Returns: A `Product` object. 68 /// - Throws: A `ServiceError` if the request fails or the product is not found. 69 func fetchProduct(for barcode: String) async throws -> Product { 70 let urlString = "https://world.openfoodfacts.org/api/v2/product/\(barcode).json" 71 72 guard let url = URL(string: urlString) else { 73 throw ServiceError.invalidURL 74 } 75 76 do { 77 let (data, _) = try await URLSession.shared.data(from: url) 78 79 let decoder = JSONDecoder() 80 let productResponse = try decoder.decode(ProductResponse.self, from: data) 81 82 guard productResponse.status == 1, let product = productResponse.product else { 83 throw ServiceError.productNotFound 84 } 85 86 // Ensure the product has the necessary nutritional information 87 guard product.nutriments != nil else { 88 throw ServiceError.noNutriments 89 } 90 91 return product 92 93 } catch let error as DecodingError { 94 throw ServiceError.decoding(error) 95 } catch { 96 throw ServiceError.network(error) 97 } 98 } 99 }