diff --git a/SampleContribution/.gitignore b/CongressDotGov/.gitignore similarity index 100% rename from SampleContribution/.gitignore rename to CongressDotGov/.gitignore diff --git a/SampleContribution/Package.swift b/CongressDotGov/Package.swift similarity index 69% rename from SampleContribution/Package.swift rename to CongressDotGov/Package.swift index 31ae560..909a986 100644 --- a/SampleContribution/Package.swift +++ b/CongressDotGov/Package.swift @@ -4,20 +4,20 @@ import PackageDescription let package = Package( - name: "SampleContribution", + name: "CongressDotGov", products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( - name: "SampleContribution", - targets: ["SampleContribution"]), + name: "CongressDotGov", + targets: ["CongressDotGov"]), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "SampleContribution"), + name: "CongressDotGov"), .testTarget( - name: "SampleContributionTests", - dependencies: ["SampleContribution"]), + name: "CongressDotGovTests", + dependencies: ["CongressDotGov"]), ] ) diff --git a/CongressDotGov/Sources/CongressDotGov/CongressDotGov.swift b/CongressDotGov/Sources/CongressDotGov/CongressDotGov.swift new file mode 100644 index 0000000..d405425 --- /dev/null +++ b/CongressDotGov/Sources/CongressDotGov/CongressDotGov.swift @@ -0,0 +1,267 @@ +import SwiftUI + +@available(macOS 12.0, *) +public class CongressDotGov { + static let scheme = "https" + static let host = "api.congress.gov" + static let version_path = "/v3" + static let api_key = "29Qv1YrP9bpnYMG9VrRUCYTzU2akRc8Bl1mxPqjL" + static let decoder = JSONDecoder() + + /** + * Get bill(s) filtered by optional parameters. + * Parameters congress, billType, and billNumber must be given in order, i.e. If billType is given without congress also being given, and error will be thrown + */ + public static func getBillsAsync(congress: Int? = nil, billType: String? = nil, billNumber: Int? = nil, + fromDateTime: String? = nil, toDateTime: String? = nil, count: Int? = nil +// ,onCompletion: @escaping ([Bill]) -> () + ) async -> [Bill] { + + var bills: [Bill] = [] + var urlComponents = CongressDotGov.buildURL() + + // Build URL Path + urlComponents.path += "/bill" + if congress != nil { + urlComponents.path += "/" + String(congress!) + + if billType != nil { + urlComponents.path += "/" + billType! + + if billNumber != nil { + urlComponents.path += "/" + String(billNumber!) + } + + } else if billNumber != nil { + print("Can't filter by billNumber without billType.") // Should I throw an error here? + } + } else if billType != nil || billNumber != nil { + print("Can't filter by billType or billNumber without congress.") // Should I throw an error here? + } + + // Add URL Query Items + if fromDateTime != nil { + urlComponents.queryItems! += [URLQueryItem(name: "fromDateTime", value: fromDateTime)] + } + + if toDateTime != nil { + urlComponents.queryItems! += [URLQueryItem(name: "toDateTime", value: toDateTime)] + } + + // TODO If count is higher than 250, we need to do multiple requests + if count != nil && count! > 0 && count! < 251 { + urlComponents.queryItems! += [URLQueryItem(name: "limit", value: String(count!))] + } + + print(urlComponents.url!) + + do { + let (data, response) = try await URLSession.shared.data(from: urlComponents.url!) + let responseData = try CongressDotGov.decoder.decode(BillResponse.self, from: data) + bills = responseData.bills + } catch { + CongressDotGov.logJSONError(error: error) + } + + // This was the @escaping version, changed to async/await +// let task = URLSession.shared.dataTask(with: urlComponents.url!) { data, response, error in +// if let data = data { +// do { +// let responseData = try CongressDotGov.decoder.decode(BillResponse.self, from: data) +// bills = responseData.bills +// } catch { +// CongressDotGov.logJSONError(error: error) +// } +// } +// } +// +// task.resume() + + return bills + } + + public static func printBills(congress: Int? = nil, billType: String? = nil, billNumber: Int? = nil, + fromDateTime: String? = nil, toDateTime: String? = nil, count: Int? = nil) { + Task { + let bills = await CongressDotGov.getBillsAsync() + print(bills) + } + } + + /** + * Get member(s) filtered by optional parameters. + */ + public static func getMembersAsync(bioGuideId: Int? = nil, + fromDateTime: String? = nil, toDateTime: String? = nil, count: Int? = nil +// ,onCompletion: @escaping ([Bill]) -> () + ) async -> [Member] { + + var members: [Member] = [] + var urlComponents = CongressDotGov.buildURL() + + // Build URL Path + urlComponents.path += "/member" + if bioGuideId != nil { + urlComponents.path += "/" + String(bioGuideId!) + } + + // Add URL Query Items + if fromDateTime != nil { + urlComponents.queryItems! += [URLQueryItem(name: "fromDateTime", value: fromDateTime)] + } + + if toDateTime != nil { + urlComponents.queryItems! += [URLQueryItem(name: "toDateTime", value: toDateTime)] + } + + // TODO If count is higher than 250, we need to do multiple requests + if count != nil && count! > 0 && count! < 251 { + urlComponents.queryItems! += [URLQueryItem(name: "limit", value: String(count!))] + } + + do { + let (data, response) = try await URLSession.shared.data(from: urlComponents.url!) + let responseData = try CongressDotGov.decoder.decode(MemberResponse.self, from: data) + members = responseData.members + } catch { + CongressDotGov.logJSONError(error: error) + } + + // This was the @escaping version, changed to async/await +// let task = URLSession.shared.dataTask(with: urlComponents.url!) { data, response, error in +// if let data = data { +// do { +// let responseData = try CongressDotGov.decoder.decode(BillResponse.self, from: data) +// bills = responseData.bills +// } catch { +// CongressDotGov.logJSONError(error: error) +// } +// } +// } +// +// task.resume() + + return members + } + + public static func printMembers(bioGuideId: Int? = nil, + fromDateTime: String? = nil, toDateTime: String? = nil, count: Int? = nil) { + Task { + let members = await CongressDotGov.getMembersAsync() + print(members) + } + } + + private static func buildURL() -> URLComponents { + var components = URLComponents() + components.scheme = scheme + components.host = host + components.path = version_path + components.queryItems = [ + URLQueryItem(name: "api_key", value: api_key) + ] + + return components + } + + private static func logJSONError(error: Error) { + print("Failed to decode JSON: \(error.localizedDescription)") + if let decodingError = error as? DecodingError { + switch decodingError { + case .typeMismatch(let type, let context): + print("Type '\(type)' mismatch:", context.debugDescription) + print("codingPath:", context.codingPath) + case .valueNotFound(let type, let context): + print("Value '\(type)' not found:", context.debugDescription) + print("codingPath:", context.codingPath) + case .keyNotFound(let key, let context): + print("Key '\(key)' not found:", context.debugDescription) + print("codingPath:", context.codingPath) + case .dataCorrupted(let context): + print("Data corrupted:", context.debugDescription) + print("codingPath:", context.codingPath) + default: + print("Decoding error:", error.localizedDescription) + } + } + } +} + + +public struct BillResponse: Codable { + let pagination: Pagination + let request: Request + let bills: [Bill] +} + +public struct Pagination: Codable { + let count: Int + let next: String +} + +public struct Request: Codable { + let contentType: String + let format: String +} + +public struct Bill: Codable { + let congress: Int + let latestAction: LatestAction + let number: String + let originChamber: String + let originChamberCode: String + let title: String + let type: String + let updateDate: String + let updateDateIncludingText: String + let url: String +} + +public struct LatestAction: Codable { + let actionDate: String + let text: String +} + +public struct MemberResponse: Codable { + let members: [Member] + let pagination: Pagination + let request: Request +} + +public struct Member: Codable { + let bioguideId: String + let state: String + let district: Int? + let partyName: String + let terms: Terms + let depiction: Depiction? + let updateDate: String + let name: String +} + +public struct Terms: Codable { + let item: [Item] +} + +public struct Item: Codable { + let startYear: Int? + let endYear: Int? + let chamber: String +} + +public struct Depiction: Codable { + let imageUrl: String + let attribution: String? +} + +//public func getBillInfo(billNumber: String) { +// get_all_bill_info(onCompletion: { +// bills in +// +// for bill in bills { +//// if (bill.number == billNumber) { +// print(bill.updateDate) +//// } +// } +// }) +//} diff --git a/SampleContribution/Tests/SampleContributionTests/SampleContributionTests.swift b/CongressDotGov/Tests/CongressDotGovTests/CongressDotGovTests.swift similarity index 86% rename from SampleContribution/Tests/SampleContributionTests/SampleContributionTests.swift rename to CongressDotGov/Tests/CongressDotGovTests/CongressDotGovTests.swift index aaa30be..e871e69 100644 --- a/SampleContribution/Tests/SampleContributionTests/SampleContributionTests.swift +++ b/CongressDotGov/Tests/CongressDotGovTests/CongressDotGovTests.swift @@ -1,7 +1,7 @@ import XCTest @testable import SampleContribution -final class SampleContributionTests: XCTestCase { +final class CongressDotGovTests: XCTestCase { func testExample() throws { // XCTest Documentation // https://developer.apple.com/documentation/xctest diff --git a/SampleContribution/Sources/SampleContribution/SampleContribution.swift b/SampleContribution/Sources/SampleContribution/SampleContribution.swift deleted file mode 100644 index 08b22b8..0000000 --- a/SampleContribution/Sources/SampleContribution/SampleContribution.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book