Fixed some type issues + made request methods
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,40 @@
|
|||||||
public struct AbridgedFoodItem: Codable {
|
import Foundation
|
||||||
public let dataType: String
|
|
||||||
public let description: String
|
public class AbridgedFoodItem: Codable {
|
||||||
public let fdcId: Int
|
public var dataType: String = ""
|
||||||
public let foodNutrients: [AbridgedFoodNutrient]
|
public var description: String = ""
|
||||||
public let publicationDate: String?
|
public var fdcId: Int = 0
|
||||||
public let brandOwner: String?
|
public var foodNutrients: [AbridgedFoodNutrient] = []
|
||||||
public let gtinUpc: String?
|
public var publicationDate: String?
|
||||||
public let ndbNumber: Int?
|
public var brandOwner: String?
|
||||||
public let foodCode: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
Sources/USDA_FDC/types/FoodCriteria.swift
Normal file
9
Sources/USDA_FDC/types/FoodCriteria.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user