/ 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  }