Fixed some type issues + made request methods

This commit is contained in:
cdricms
2024-01-13 18:07:46 +01:00
parent 70e0e7c417
commit b73b9dcd29
10 changed files with 321 additions and 54 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ DerivedData/
.swiftpm/configuration/registries.json .swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc .netrc
.env

View File

@@ -5,6 +5,9 @@ import PackageDescription
let package = Package( let package = Package(
name: "USDA_FDC", name: "USDA_FDC",
platforms: [
.macOS(.v14),
],
products: [ products: [
// Products define the executables and libraries a package produces, making them visible to other packages. // Products define the executables and libraries a package produces, making them visible to other packages.
.library( .library(

View File

@@ -1,6 +1,129 @@
// The Swift Programming Language // The Swift Programming Language
// https://docs.swift.org/swift-book // https://docs.swift.org/swift-book
final class USDA_FDC_Client { import Foundation
public final class USDA_FDC_Client {
var version = 1
var baseURL: URL {
URL(string: "https://api.nal.usda.gov/fdc/v\(version)")!
}
public init() {}
public var apiKey: String = ""
public func getFood(_ foodCriteria: FoodCriteria) async throws -> AbridgedFoodItem {
var endpoint = baseURL.appendingPathComponent("/food/\(foodCriteria.fdcId)")
endpoint.append(queryItems: [.init(name: "api_key", value: apiKey)])
// if criteria != nil {
// for (key, value) in Mirror(reflecting: criteria!).children {
// if value as Any? != nil {
// if let l = value as? [String] {
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: l.joined(separator: ",")))])
// }
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: value))])
// }
// }
// }
print(endpoint)
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 FDCError.invalidResponse
}
do {
return try JSONDecoder().decode(AbridgedFoodItem.self, from: data)
} catch {
throw error
}
}
// TODO:
public func getFoods(_ foodsCriteria: FoodsCriteria) async throws -> [AbridgedFoodItem] {
var endpoint = baseURL.appendingPathComponent("/food")
endpoint.append(queryItems: [.init(name: "api_key", value: apiKey)])
// if criteria != nil {
// for (key, value) in Mirror(reflecting: criteria!).children {
// if value as Any? != nil {
// if let l = value as? [String] {
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: l.joined(separator: ",")))])
// }
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: value))])
// }
// }
// }
print(endpoint)
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 FDCError.invalidResponse
}
do {
return try JSONDecoder().decode([AbridgedFoodItem].self, from: data)
} catch {
throw error
}
}
public func getFoodsSearch(_ search: FoodSearchCriteria) async throws -> SearchResult {
var endpoint = baseURL.appendingPathComponent("/foods/search")
endpoint.append(queryItems: [.init(name: "api_key", value: apiKey)])
endpoint.append(queryItems: [.init(name: "query", value: search.query)])
// if criteria != nil {
// for (key, value) in Mirror(reflecting: criteria!).children {
// if value as Any? != nil {
// if let l = value as? [String] {
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: l.joined(separator: ",")))])
// }
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: value))])
// }
// }
// }
print(endpoint)
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 FDCError.invalidResponse
}
do {
return try JSONDecoder().decode(SearchResult.self, from: data)
} catch {
throw error
}
}
public func getFoodList(_: FoodListCriteria? = nil) async throws -> [AbridgedFoodItem] {
var endpoint = baseURL.appendingPathComponent("/foods/list")
endpoint.append(queryItems: [.init(name: "api_key", value: apiKey)])
// if criteria != nil {
// for (key, value) in Mirror(reflecting: criteria!).children {
// if value as Any? != nil {
// if let l = value as? [String] {
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: l.joined(separator: ",")))])
// }
// endpoint.append(queryItems: [.init(name: key!, value: String(describing: value))])
// }
// }
// }
print(endpoint)
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 FDCError.invalidResponse
}
do {
return try JSONDecoder().decode([AbridgedFoodItem].self, from: data)
} catch {
throw error
}
}
}
public enum FDCError: Error {
case invalidResponse
} }

View File

@@ -1,12 +1,40 @@
public struct AbridgedFoodItem: Codable { import Foundation
public let dataType: String
public let description: String
public let fdcId: Int
public let foodNutrients: [AbridgedFoodNutrient]
public let publicationDate: String?
public let brandOwner: String?
public let gtinUpc: String?
public let ndbNumber: Int?
public let foodCode: String?
}
public class AbridgedFoodItem: Codable {
public var dataType: String = ""
public var description: String = ""
public var fdcId: Int = 0
public var foodNutrients: [AbridgedFoodNutrient] = []
public var publicationDate: String?
public var brandOwner: String?
public var gtinUpc: String?
public var ndbNumber: Int?
public var foodCode: String?
private enum CodingKeys: String, CodingKey {
case dataType, description, fdcId, foodNutrients, publicationDate, brandOwner, gtinUpc, ndbNumber, foodCode
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dataType = try container.decode(String.self, forKey: .dataType)
description = try container.decode(String.self, forKey: .description)
fdcId = try container.decode(Int.self, forKey: .fdcId)
foodNutrients = try container.decode([AbridgedFoodNutrient].self, forKey: .foodNutrients)
publicationDate = try container.decodeIfPresent(String.self, forKey: .publicationDate)
brandOwner = try container.decodeIfPresent(String.self, forKey: .brandOwner)
gtinUpc = try container.decodeIfPresent(String.self, forKey: .gtinUpc)
foodCode = try container.decodeIfPresent(String.self, forKey: .foodCode)
// Only manually handle the special case for ndbNumber
if let stringValue = try? container.decode(String.self, forKey: .ndbNumber),
let intValue = Int(stringValue) {
// If the value is a string, attempt to convert it to Int
ndbNumber = intValue
} else {
// Let other properties initialize themselves
ndbNumber = nil // Or any other default value or leave it as nil
}
}
}

View File

@@ -1,9 +1,9 @@
public struct AbridgedFoodNutrient: Codable { public struct AbridgedFoodNutrient: Codable {
public let number: Int public let number: String?
public let name: String public let name: String?
public let amount: Float public let amount: Float?
public let unitName: String public let unitName: String?
public let derivationCode: String public let derivationCode: String?
public let derivationDescription: String public let derivationDescription: String?
} }

View File

@@ -0,0 +1,9 @@
public struct FoodCriteria: Codable {
public var fdcId: Int // Max : 20 items
public var format: [Format]?
public var nutrients: [Int]? // Max : 25 items
public enum Format: String, Codable {
case abridged, full
}
}

View File

@@ -1,7 +1,7 @@
public struct FoodListCriteria: Codable { public struct FoodListCriteria: Codable {
public let dataType: [DataType]? public var dataType: [DataType]?
public let pageSize: Int? // 1..=200, default 50 public var pageSize: Int? // 1..=200, default 50
public let pageNumber: Int? public var pageNumber: Int?
public let sortBy: SortBy? public var sortBy: SortBy?
public let sortOrder: SortOrder? public var sortOrder: SortOrder?
} }

View File

@@ -1,12 +1,12 @@
public struct FoodSearchCriteria: Codable { public struct FoodSearchCriteria: Codable {
public let query: String public var query: String
public let dataType: DataType? public var dataType: DataType?
public let pageSize: Int? public var pageSize: Int?
public let pageNumber: Int? public var pageNumber: Int?
public let sortBy: SortBy? public var sortBy: SortBy?
public let sortOrder: SortOrder? public var sortOrder: SortOrder?
public let brandOwner: String? public var brandOwner: String?
public let tradeChannel: [TradeChannel]? // Max items: 3 public var tradeChannel: [TradeChannel]? // Max items: 3
public let startDate: String? // Format: YYYY-MM-DD public var startDate: String? // Format: YYYY-MM-DD
public let endDate: String? // Format: YYYY-MM-DD public var endDate: String? // Format: YYYY-MM-DD
} }

View File

@@ -1,17 +1,70 @@
public struct SearchResultFood: Codable { public struct SearchResultFood: Codable {
public let fdcId: Int public var fdcId: Int = 0
public let dataType: String? public var dataType: String?
public let description: String public var description: String = ""
public let foodCode: String? public var foodCode: String?
public let foodNutrients: [AbridgedFoodNutrient] public var foodNutrients: [AbridgedFoodNutrient] = []
public let publicationDate: String? public var publicationDate: String?
public let scientificName: String? public var scientificName: String?
public let brandOwner: String? public var brandOwner: String?
public let gtinUpc: String? public var gtinUpc: String?
public let ingredients: String? public var ingredients: String?
public let ndbNumber: Int? public var ndbNumber: Int?
public let additionalDescriptions: String? public var additionalDescriptions: String?
public let allHighlightFields: String? public var allHighlightFields: String?
public let score: Float? public var score: Float?
private enum CodingKeys: String, CodingKey {
case fdcId
case dataType
case description
case foodCode
case foodNutrients
case publicationDate
case scientificName
case brandOwner
case gtinUpc
case ingredients
case ndbNumber
case additionalDescriptions
case allHighlightFields
case score
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
fdcId = try container.decode(Int.self, forKey: .fdcId)
dataType = try container.decodeIfPresent(String.self, forKey: .dataType)
description = try container.decode(String.self, forKey: .description)
foodNutrients = try container.decodeIfPresent([AbridgedFoodNutrient].self, forKey: .foodNutrients) ?? []
publicationDate = try container.decodeIfPresent(String.self, forKey: .publicationDate)
scientificName = try container.decodeIfPresent(String.self, forKey: .scientificName)
brandOwner = try container.decodeIfPresent(String.self, forKey: .brandOwner)
gtinUpc = try container.decodeIfPresent(String.self, forKey: .gtinUpc)
ingredients = try container.decodeIfPresent(String.self, forKey: .ingredients)
additionalDescriptions = try container.decodeIfPresent(String.self, forKey: .additionalDescriptions)
allHighlightFields = try container.decodeIfPresent(String.self, forKey: .allHighlightFields)
score = try container.decodeIfPresent(Float.self, forKey: .score)
if let intValue = try? container.decode(Int.self, forKey: .ndbNumber) {
// If the value can be directly decoded as Int, use it
ndbNumber = intValue
} else if let stringValue = try? container.decode(String.self, forKey: .ndbNumber),
let intValue = Int(stringValue) {
// If the value is a string, attempt to convert it to Int
ndbNumber = intValue
} else {
// Handle other cases or throw an error if needed
ndbNumber = nil
}
if let stringValue = try? container.decode(String.self, forKey: .foodCode) {
foodCode = stringValue
} else if let intValue = try? container.decode(Int.self, forKey: .foodCode),
let stringValue = Optional(String(intValue)) {
foodCode = stringValue
} else {
foodCode = nil
}
}
} }

View File

@@ -1,12 +1,62 @@
import XCTest import XCTest
@testable import USDA @testable import USDA_FDC
func loadEnvironment() -> [String: String] {
var dic: [String: String] = [:]
if FileManager.default.fileExists(atPath: ".env") {
do {
let contents = try String(contentsOfFile: ".env", encoding: .utf8)
let lines = contents.components(separatedBy: .newlines)
for line in lines {
let components = line.components(separatedBy: "=")
if components.count == 2 {
let key = components[0].trimmingCharacters(in: .whitespacesAndNewlines)
let value = components[1].trimmingCharacters(in: .whitespacesAndNewlines)
dic[key] = value
}
}
} catch {
print("Error reading .env file: \(error)")
}
}
return dic
}
final class USDATests: XCTestCase { final class USDATests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest
// Defining Test Cases and Test Methods var env: [String: String] {
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods loadEnvironment()
}
func testFoodList() async throws {
let fdc = USDA_FDC_Client()
fdc.apiKey = env["API_KEY"] ?? ""
do {
let _ = try await fdc.getFoodList()
} catch {
throw error
}
}
func testFoodItem() async throws {
let fdc = USDA_FDC_Client()
fdc.apiKey = env["API_KEY"] ?? ""
do {
let _ = try await fdc.getFood(.init(fdcId: 167782))
} catch {
throw error
}
}
func testFoodSearch() async throws {
let fdc = USDA_FDC_Client()
fdc.apiKey = env["API_KEY"] ?? ""
do {
let _ = try await fdc.getFoodsSearch(.init(query: "cheddar cheese"))
} catch {
throw error
}
} }
} }