From 2dd013f0f5ca0504fc2146e559971b91f5483a28 Mon Sep 17 00:00:00 2001 From: vamsii777 Date: Sun, 3 Nov 2024 00:02:46 +0530 Subject: [PATCH] Add domain management --- .../Models/Request/Domain/DomainCreate.swift | 11 +++ .../Models/Request/Domain/DomainUpdate.swift | 13 +++ .../Domain/DomainCreateResponse.swift | 19 ++++ .../Response/Domain/DomainListResponse.swift | 33 +++++++ .../Domain/DomainVerifyResponse.swift | 6 ++ Sources/Resend/Resend/APIPath.swift | 18 ++++ Sources/Resend/Resend/DomainClient.swift | 93 +++++++++++++++++++ Sources/Resend/Resend/ResendClient.swift | 4 + Tests/SwiftResendTests/SwiftResendTests.swift | 34 ++++++- 9 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 Sources/Resend/Models/Request/Domain/DomainCreate.swift create mode 100644 Sources/Resend/Models/Request/Domain/DomainUpdate.swift create mode 100644 Sources/Resend/Models/Response/Domain/DomainCreateResponse.swift create mode 100644 Sources/Resend/Models/Response/Domain/DomainListResponse.swift create mode 100644 Sources/Resend/Models/Response/Domain/DomainVerifyResponse.swift create mode 100644 Sources/Resend/Resend/DomainClient.swift diff --git a/Sources/Resend/Models/Request/Domain/DomainCreate.swift b/Sources/Resend/Models/Request/Domain/DomainCreate.swift new file mode 100644 index 0000000..d3bf967 --- /dev/null +++ b/Sources/Resend/Models/Request/Domain/DomainCreate.swift @@ -0,0 +1,11 @@ +import Foundation + +struct DomainCreate: Encodable { + var name: String + var region: String + + enum CodingKeys: String, CodingKey { + case name + case region + } +} \ No newline at end of file diff --git a/Sources/Resend/Models/Request/Domain/DomainUpdate.swift b/Sources/Resend/Models/Request/Domain/DomainUpdate.swift new file mode 100644 index 0000000..2ccd806 --- /dev/null +++ b/Sources/Resend/Models/Request/Domain/DomainUpdate.swift @@ -0,0 +1,13 @@ +import Foundation + +struct DomainUpdate: Encodable { + var clickTracking: Bool + var openTracking: Bool + var tls: String + + enum CodingKeys: String, CodingKey { + case clickTracking = "click_tracking" + case openTracking = "open_tracking" + case tls + } +} \ No newline at end of file diff --git a/Sources/Resend/Models/Response/Domain/DomainCreateResponse.swift b/Sources/Resend/Models/Response/Domain/DomainCreateResponse.swift new file mode 100644 index 0000000..bf9b87d --- /dev/null +++ b/Sources/Resend/Models/Response/Domain/DomainCreateResponse.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct DomainCreateResponse: Decodable { + public var id: String + public var name: String + public var createdAt: Date + public var status: String + public var records: [DomainRecord] + public var region: String + + enum CodingKeys: String, CodingKey { + case id + case name + case createdAt = "created_at" + case status + case records + case region + } +} \ No newline at end of file diff --git a/Sources/Resend/Models/Response/Domain/DomainListResponse.swift b/Sources/Resend/Models/Response/Domain/DomainListResponse.swift new file mode 100644 index 0000000..7bd3b82 --- /dev/null +++ b/Sources/Resend/Models/Response/Domain/DomainListResponse.swift @@ -0,0 +1,33 @@ +import Foundation + +public struct DomainListResponse: Decodable { + public var data: [Domain] +} + +public struct Domain: Decodable { + public var id: String + public var name: String + public var status: String + public var createdAt: Date + public var region: String + public var records: [DomainRecord]? + + enum CodingKeys: String, CodingKey { + case id + case name + case status + case createdAt = "created_at" + case region + case records + } +} + +public struct DomainRecord: Decodable { + public var record: String + public var name: String + public var type: String + public var ttl: String + public var status: String + public var value: String + public var priority: Int? +} \ No newline at end of file diff --git a/Sources/Resend/Models/Response/Domain/DomainVerifyResponse.swift b/Sources/Resend/Models/Response/Domain/DomainVerifyResponse.swift new file mode 100644 index 0000000..ab02438 --- /dev/null +++ b/Sources/Resend/Models/Response/Domain/DomainVerifyResponse.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct DomainVerifyResponse: Decodable { + public var object: String + public var id: String +} \ No newline at end of file diff --git a/Sources/Resend/Resend/APIPath.swift b/Sources/Resend/Resend/APIPath.swift index 060587b..366fbbb 100644 --- a/Sources/Resend/Resend/APIPath.swift +++ b/Sources/Resend/Resend/APIPath.swift @@ -25,6 +25,12 @@ enum APIPath { case contactUpdate(contactId: String, audienceId: String) case contactDelete(contactIdOrEmail: String, audienceId: String) case contactList(audienceId: String) + case domainCreate + case domainRetrieve(domainId: String) + case domainVerify(domainId: String) + case domainUpdate(domainId: String) + case domainList + case domainDelete(domainId: String) private static func path(of path: String) -> String { APIPath.apiURL + path @@ -56,6 +62,18 @@ enum APIPath { return path(of: "/audiences/\(audienceId)/contacts/\(contactIdOrEmail)") case .contactList(let audienceId): return path(of: "/audiences/\(audienceId)/contacts") + case .domainCreate: + return path(of: "/domains") + case .domainRetrieve(let domainId): + return path(of: "/domains/\(domainId)") + case .domainVerify(let domainId): + return path(of: "/domains/\(domainId)/verify") + case .domainUpdate(let domainId): + return path(of: "/domains/\(domainId)") + case .domainList: + return path(of: "/domains") + case .domainDelete(let domainId): + return path(of: "/domains/\(domainId)") } } } diff --git a/Sources/Resend/Resend/DomainClient.swift b/Sources/Resend/Resend/DomainClient.swift new file mode 100644 index 0000000..43911ab --- /dev/null +++ b/Sources/Resend/Resend/DomainClient.swift @@ -0,0 +1,93 @@ +import AsyncHTTPClient +import NIOHTTP1 + +public class DomainClient: ResendClient { + + internal override init(httpClient: HTTPClient, apiKey: String) { + super.init(httpClient: httpClient, apiKey: apiKey) + } + + /// Create a new domain + public func create(name: String, region: String = "us-east-1") async throws -> DomainCreateResponse { + let body = DomainCreate(name: name, region: region) + let response = try await httpClient.execute( + request: .init( + url: APIPath.getPath(for: .domainCreate), + method: .POST, + headers: getAuthHeader(), + body: .data(encoder.encode(body)) + ) + ).get() + + return try parseResponse(response, to: DomainCreateResponse.self) + } + + /// Retrieve a domain + public func retrieve(domainId: String) async throws -> Domain { + let response = try await httpClient.execute( + request: .init( + url: APIPath.getPath(for: .domainRetrieve(domainId: domainId)), + method: .GET, + headers: getAuthHeader() + ) + ).get() + + return try parseResponse(response, to: Domain.self) + } + + /// Verify a domain + public func verify(domainId: String) async throws -> DomainVerifyResponse { + let response = try await httpClient.execute( + request: .init( + url: APIPath.getPath(for: .domainVerify(domainId: domainId)), + method: .POST, + headers: getAuthHeader() + ) + ).get() + + return try parseResponse(response, to: DomainVerifyResponse.self) + } + + /// Update a domain + public func update(domainId: String, clickTracking: Bool, openTracking: Bool, tls: String = "opportunistic") async throws -> Domain { + let body = DomainUpdate(clickTracking: clickTracking, openTracking: openTracking, tls: tls) + let response = try await httpClient.execute( + request: .init( + url: APIPath.getPath(for: .domainUpdate(domainId: domainId)), + method: .PATCH, + headers: getAuthHeader(), + body: .data(encoder.encode(body)) + ) + ).get() + + return try parseResponse(response, to: Domain.self) + } + + /// List all domains + public func list() async throws -> [Domain] { + let response = try await httpClient.execute( + request: .init( + url: APIPath.getPath(for: .domainList), + method: .GET, + headers: getAuthHeader() + ) + ).get() + + return try parseResponse(response, to: DomainListResponse.self).data + } + + /// Delete a domain + public func delete(domainId: String) async throws { + let response = try await httpClient.execute( + request: .init( + url: APIPath.getPath(for: .domainDelete(domainId: domainId)), + method: .DELETE, + headers: getAuthHeader() + ) + ).get() + + guard response.status == .ok else { + throw ResendError.unknownError + } + } +} \ No newline at end of file diff --git a/Sources/Resend/Resend/ResendClient.swift b/Sources/Resend/Resend/ResendClient.swift index 16cc2b7..ff423f4 100644 --- a/Sources/Resend/Resend/ResendClient.swift +++ b/Sources/Resend/Resend/ResendClient.swift @@ -54,6 +54,10 @@ public class ResendClient { ContactClient(httpClient: httpClient, apiKey: apiKey) } + public var domains: DomainClient { + DomainClient(httpClient: httpClient, apiKey: apiKey) + } + func parseResponse(_ response: HTTPClient.Response, to: T.Type) throws -> T { let byteBuffer: ByteBuffer = response.body ?? .init() diff --git a/Tests/SwiftResendTests/SwiftResendTests.swift b/Tests/SwiftResendTests/SwiftResendTests.swift index dd7afad..24fd88f 100644 --- a/Tests/SwiftResendTests/SwiftResendTests.swift +++ b/Tests/SwiftResendTests/SwiftResendTests.swift @@ -241,7 +241,6 @@ final class SwiftResendTests: XCTestCase { } - // MARK: Helper /// Delete all audiences created via tests func testDeleteAllAudience() async throws { @@ -252,9 +251,40 @@ final class SwiftResendTests: XCTestCase { _ = try await resend.audiences.delete(audienceId: audience.id) try await Task.sleep(for: .milliseconds(600)) } catch { - + } } } + // MARK: Domain Tests + func testCreateDomain() async throws { + let response = try await resend.domains.create(name: "example.com") + XCTAssertNotNil(response.id) + XCTAssertEqual(response.name, "example.com") + } + + func testRetrieveDomain() async throws { + let domain = try await resend.domains.retrieve(domainId: "d91cd9bd-1176-453e-8fc1-35364d380206") + XCTAssertEqual(domain.id, "d91cd9bd-1176-453e-8fc1-35364d380206") + } + + func testVerifyDomain() async throws { + let response = try await resend.domains.verify(domainId: "d91cd9bd-1176-453e-8fc1-35364d380206") + XCTAssertEqual(response.id, "d91cd9bd-1176-453e-8fc1-35364d380206") + } + + func testUpdateDomain() async throws { + let domain = try await resend.domains.update(domainId: "d91cd9bd-1176-453e-8fc1-35364d380206", clickTracking: true, openTracking: true) + XCTAssertEqual(domain.id, "d91cd9bd-1176-453e-8fc1-35364d380206") + } + + func testListDomains() async throws { + let domains = try await resend.domains.list() + XCTAssertGreaterThan(domains.count, 0) + } + + func testDeleteDomain() async throws { + try await resend.domains.delete(domainId: "d91cd9bd-1176-453e-8fc1-35364d380206") + } + }