diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27de380..85bd56d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - "platform=iOS Simulator,name=iPhone 16" - "platform=tvOS Simulator,name=Apple TV" - "platform=watchOS Simulator,name=Apple Watch Series 10 (42mm)" - - "platform=visionOS Simulator,name=Apple Vision Pro" + - "platform=visionOS Simulator,name=Apple Vision Pro,OS=26.1" steps: - uses: actions/checkout@v4 - name: Test platform ${{ matrix.destination }} diff --git a/Package.resolved b/Package.resolved index 1670f17..d968434 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "79772beb13b2d5e9ae03d91ea73aefd861946eb487105353f86977b6c2920fa2", + "originHash" : "96e0d45ae443bfd2b83137c042febcda2762013a24905f862d8634380a95d3c6", "pins" : [ { "identity" : "swift-async-dns-resolver", diff --git a/Package.swift b/Package.swift index 0a792a3..8646a57 100644 --- a/Package.swift +++ b/Package.swift @@ -18,14 +18,18 @@ let package = Package( targets: ["ATResolve"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-async-dns-resolver.git", from: "0.4.0") + .package(url: "https://github.com/apple/swift-async-dns-resolver.git", from: "0.4.0"), ], targets: [ .target( name: "ATResolve", dependencies: [ - .product(name: "AsyncDNSResolver", package: "swift-async-dns-resolver"), - ]), + .product( + name: "AsyncDNSResolver", + package: "swift-async-dns-resolver" + ), + ] + ), .testTarget( name: "ATResolveTests", dependencies: ["ATResolve"] diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index d1e81b2..77a0933 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -32,21 +32,11 @@ public struct PLCDirectoryResolveDidResponse: Codable, Hashable, Sendable { } } -public protocol ResponseProviding { - func decodeJSON(at urlString: String, queryItems: [(String, String)]) async throws -> T -} - -extension ResponseProviding { - public func decodeJSON(at urlString: String) async throws -> T { - try await decodeJSON(at: urlString, queryItems: []) - } -} - -public struct ATResolver { - public let provider: Provider +public struct ATResolver { + public let requester: Requester - public init(provider: Provider) { - self.provider = provider + public init(requester: Requester) { + self.requester = requester } public func didForDomain(_ name: String) async throws -> String? { @@ -75,15 +65,21 @@ public struct ATResolver { } public func blueskyGetProfile(_ actor: String) async throws -> BlueskyProfile { - try await provider.decodeJSON( - at: "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile", + try await requester.decodeJSON( + host: "public.api.bsky.app", + path: "/xrpc/app.bsky.actor.getProfile", + headers: ["Accept": "application/json"], queryItems: [("actor", actor)] ) } - public func plcDirectoryQuery(_ did: String) async throws -> PLCDirectoryResolveDidResponse { - try await provider.decodeJSON( - at: "https://plc.directory/\(did)" + public func plcDirectoryQuery( + _ did: String + ) async throws -> PLCDirectoryResolveDidResponse { + try await requester.decodeJSON( + host: "plc.directory", + path: "/\(did)", + headers: ["Accept": "application/json"] ) } @@ -99,7 +95,7 @@ public struct ATResolver { } } -extension ATResolver: Sendable where Provider: Sendable {} +extension ATResolver: Sendable where Requester: Sendable {} #if canImport(Foundation) import Foundation diff --git a/Sources/ATResolve/Networking.swift b/Sources/ATResolve/Networking.swift index 729ca40..3b9f823 100644 --- a/Sources/ATResolve/Networking.swift +++ b/Sources/ATResolve/Networking.swift @@ -1,28 +1,29 @@ +#if canImport(Foundation) import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif extension URLSession: ResponseProviding { - public func decodeJSON(at urlString: String, queryItems: [(String, String)]) async throws -> T where T : Decodable { - guard var components = URLComponents(string: urlString) else { - throw ATResolverError.urlInvalid - } - - components.queryItems = queryItems.map({ pair in + public func data(for request: Request) async throws -> Data { + var components = URLComponents() + components.scheme = "https" + components.host = request.host + components.path = request.path + components.queryItems = request.queryItems.map({ pair in URLQueryItem(name: pair.0, value: pair.1) }) - + guard let url = components.url else { - throw ATResolverError.urlInvalid + throw URLError(.badURL) } - - var request = URLRequest(url: url) - - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Accept") - - let (data, response) = try await URLSession.shared.data(for: request) + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = request.method.rawValue + for (key, value) in request.headers { + urlRequest.addValue(value, forHTTPHeaderField: key) + } + urlRequest.addValue("text/plain;charset=UTF-8", forHTTPHeaderField: "Accept") + let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse, @@ -33,7 +34,7 @@ extension URLSession: ResponseProviding { throw ATResolverError.requestFailed } - - return try JSONDecoder().decode(T.self, from: data) + return data } } +#endif diff --git a/Sources/ATResolve/ResponseProviding.swift b/Sources/ATResolve/ResponseProviding.swift new file mode 100644 index 0000000..7839f24 --- /dev/null +++ b/Sources/ATResolve/ResponseProviding.swift @@ -0,0 +1,56 @@ +// +// ResponseProviding.swift +// ATResolve +// +// Created by Mark @ Germ on 12/1/25. +// + +import Foundation + +///Allows the client to choose between URLSession (Foundation) or AsyncHTTPClient, +///Or another networking library of their choice + +//from Dave Delong +public struct HTTPMethod: Hashable, Sendable { + public static let get = HTTPMethod(rawValue: "GET") + public static let post = HTTPMethod(rawValue: "POST") + public static let put = HTTPMethod(rawValue: "PUT") + public static let delete = HTTPMethod(rawValue: "DELETE") + + public let rawValue: String +} + +public struct Request { + public let host: String + public let path: String + public let method: HTTPMethod + public let headers: [String: String] + public let queryItems: [(String, String?)] +} + +public protocol ResponseProviding { + func data(for: Request) async throws -> Data +} + +extension ResponseProviding { + func decodeJSON( + host: String, + path: String, + method: HTTPMethod = .get, + headers: [String: String] = [:], + queryItems: [(String, String)] = [] + ) async throws -> T { + let result = try await data( + for: .init( + host: host, + path: path, + method: method, + headers: headers, + queryItems: queryItems + ) + ) + + return try JSONDecoder().decode(T.self, from: result) + } + +} diff --git a/Tests/ATResolveTests/ATResolveAsyncHTTPTests.swift b/Tests/ATResolveTests/ATResolveAsyncHTTPTests.swift new file mode 100644 index 0000000..fcbc61f --- /dev/null +++ b/Tests/ATResolveTests/ATResolveAsyncHTTPTests.swift @@ -0,0 +1,27 @@ +// +// ATResolveAsyncHTTPTests.swift +// ATResolve +// +// Created by Mark @ Germ on 12/1/25. +// + +#if canImport(AsyncHTTPClient) +import ATResolve +import Foundation +import AsyncHTTPClient +import Testing + +//Test API with AsyncHTTPClient +struct ATResolveAsyncHTTPTests { + @Test + func resolveHandle() async throws { + let resolver = ATResolver(requester: HTTPClient.shared) + + let data = try await resolver.resolveHandle("massicotte.org") + + #expect(data?.did == "did:plc:klsh7edzj3jmxucibyjqstb3") + #expect(data?.handle == "massicotte.org") + #expect(data?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") + } +} +#endif diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index 2b89692..f720956 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -8,7 +8,7 @@ import FoundationNetworking struct ATResolveTests { @Test func resolveHandle() async throws { - let resolver = ATResolver(provider: URLSession.shared) + let resolver = ATResolver(requester: URLSession.shared) let data = try await resolver.resolveHandle("massicotte.org") @@ -19,7 +19,7 @@ struct ATResolveTests { @Test func didForDomain() async throws { - let resolver = ATResolver(provider: URLSession.shared) + let resolver = ATResolver(requester: URLSession.shared) let did = try await resolver.didForDomain("massicotte.org") @@ -28,7 +28,7 @@ struct ATResolveTests { @Test func blueskyGetProfile() async throws { - let resolver = ATResolver(provider: URLSession.shared) + let resolver = ATResolver(requester: URLSession.shared) let profile = try await resolver.blueskyGetProfile("massicotte.org") @@ -36,7 +36,7 @@ struct ATResolveTests { } @Test func bskySocialHandle() async throws { - let resolver = ATResolver(provider: URLSession.shared) + let resolver = ATResolver(requester: URLSession.shared) let profile = try await resolver.resolveHandle("cjrdev.bsky.social") @@ -49,15 +49,15 @@ struct ATResolveTests { {"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1","https://w3id.org/security/suites/secp256k1-2019/v1"],"id":"did:plc:klsh7edzj3jmxucibyjqstb3","alsoKnownAs":["at://massicotte.org"],"verificationMethod":[{"id":"did:plc:klsh7edzj3jmxucibyjqstb3#atproto","type":"Multikey","controller":"did:plc:klsh7edzj3jmxucibyjqstb3","publicKeyMultibase":"zQ3shP3NvazgSaEFpryzuyx8Q4MHho2KC2MNobAuQX3gdKAPW"}],"service":[{"id":"#atproto_pds","type":"AtprotoPersonalDataServer","serviceEndpoint":"https://milkcap.us-west.host.bsky.network"}]} """ - func decodeJSON(at urlString: String, queryItems: [(String, String)]) async throws -> T where T : Decodable { - try JSONDecoder().decode(T.self, from: Data(content.utf8)) + func data(for: Request) async throws -> Data { + Data(content.utf8) } } - let resolver = ATResolver(provider: CustomProvider()) - + let resolver = ATResolver(requester: CustomProvider()) + let response = try await resolver.plcDirectoryQuery("did:plc:klsh7edzj3jmxucibyjqstb3") - + #expect(response.pds?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") } }