From 044b6f281a426606aaefe7a73ad891b177af7c4f Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 17 Sep 2024 19:50:56 +0200 Subject: [PATCH 1/7] Swift 6 and Swift Testing --- Package.swift | 19 ++++----------- Sources/SendGridKit/SendGridClient.swift | 4 ++-- Tests/SendGridKitTests/SendGridTestsKit.swift | 24 +++++++------------ 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Package.swift b/Package.swift index 609aaff..c02ed21 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,10 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "sendgrid-kit", platforms: [ - .macOS(.v13), + .macOS(.v14), ], products: [ .library(name: "SendGridKit", targets: ["SendGridKit"]), @@ -17,24 +17,13 @@ let package = Package( name: "SendGridKit", dependencies: [ .product(name: "AsyncHTTPClient", package: "async-http-client"), - ], - swiftSettings: swiftSettings + ] ), .testTarget( name: "SendGridKitTests", dependencies: [ .target(name: "SendGridKit"), - ], - swiftSettings: swiftSettings + ] ), ] ) - -var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("StrictConcurrency"), - .enableExperimentalFeature("StrictConcurrency=complete"), -] } diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift index 53b1e7a..ef8ac0c 100644 --- a/Sources/SendGridKit/SendGridClient.swift +++ b/Sources/SendGridKit/SendGridClient.swift @@ -1,4 +1,4 @@ -@preconcurrency import Foundation +import Foundation import NIO import AsyncHTTPClient import NIOHTTP1 @@ -41,7 +41,7 @@ public struct SendGridClient: Sendable { ).get() // If the request was accepted, simply return - guard response.status != .ok && response.status != .accepted else { return } + if response.status == .ok || response.status == .accepted { return } // JSONDecoder will handle empty body by throwing decoding error let byteBuffer = response.body ?? ByteBuffer(.init()) diff --git a/Tests/SendGridKitTests/SendGridTestsKit.swift b/Tests/SendGridKitTests/SendGridTestsKit.swift index 1f9c17f..d55cb92 100644 --- a/Tests/SendGridKitTests/SendGridTestsKit.swift +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -1,22 +1,16 @@ -import XCTest +import Testing import AsyncHTTPClient @testable import SendGridKit -class SendGridKitTests: XCTestCase { - private var httpClient: HTTPClient! - private var client: SendGridClient! +struct SendGridKitTests { + var client: SendGridClient - override func setUp() { - httpClient = HTTPClient(eventLoopGroupProvider: .singleton) - // TODO: Replace with your API key to test! - client = SendGridClient(httpClient: httpClient, apiKey: "YOUR-API-KEY") - } - - override func tearDown() async throws { - try await httpClient.shutdown() + init() { + // TODO: Replace with a valid API key to test + client = SendGridClient(httpClient: HTTPClient.shared, apiKey: "YOUR-API-KEY") } - func testSendEmail() async throws { + @Test func sendEmail() async throws { // TODO: Replace to address with the email address you'd like to recieve your test email let emailAddress = EmailAddress("TO-ADDRESS") // TODO: Replace from address with the email address associated with your verified Sender Identity @@ -40,8 +34,8 @@ class SendGridKitTests: XCTestCase { attachments: [attachment] ) - do { + await withKnownIssue { try await client.send(email: email) - } catch {} + } } } From 5b6a119a9456fd1b47b394cde36c1d8e00e2ed97 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 18 Sep 2024 19:13:31 +0200 Subject: [PATCH 2/7] Improve testing and DocC --- .../Models/AdvancedSuppressionManager.swift | 12 ++++-- .../SendGridKit/Models/EmailAttachment.swift | 27 ++++++++----- .../SendGridKit/Models/SendGridEmail.swift | 29 +++++++++----- .../SendGridKit/Models/SendGridError.swift | 8 ++++ Sources/SendGridKit/SendGridClient.swift | 31 +++++++------- Tests/SendGridKitTests/SendGridTestsKit.swift | 40 ++++++++++++++++++- 6 files changed, 107 insertions(+), 40 deletions(-) diff --git a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift index 9235b9e..575abb1 100644 --- a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift +++ b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift @@ -2,21 +2,25 @@ import Foundation public struct AdvancedSuppressionManager: Codable, Sendable { /// The unsubscribe group to associate with this email. - public var groupId: Int + /// + /// See the Suppressions API to manage unsubscribe group IDs. + public var groupID: Int /// An array containing the unsubscribe groups that you would like to be displayed on the unsubscribe preferences page. + /// + /// This page is displayed in the recipient's browser when they click the unsubscribe link in your message. public var groupsToDisplay: [String]? public init( - groupId: Int, + groupID: Int, groupsToDisplay: [String]? = nil ) { - self.groupId = groupId + self.groupID = groupID self.groupsToDisplay = groupsToDisplay } private enum CodingKeys: String, CodingKey { - case groupId = "group_id" + case groupID = "group_id" case groupsToDisplay = "groups_to_display" } } diff --git a/Sources/SendGridKit/Models/EmailAttachment.swift b/Sources/SendGridKit/Models/EmailAttachment.swift index a81a61f..8e13788 100644 --- a/Sources/SendGridKit/Models/EmailAttachment.swift +++ b/Sources/SendGridKit/Models/EmailAttachment.swift @@ -6,33 +6,42 @@ public struct EmailAttachment: Codable, Sendable { /// The MIME type of the content you are attaching. /// - /// For example, `“text/plain”` or `“text/html”`. + /// For example, `image/jpeg`, `text/html` or `application/pdf`. public var type: String? - /// The filename of the attachment. + /// The attachment's filename, including the file extension. public var filename: String - /// The content-disposition of the attachment specifying how you would like the attachment to be displayed. - public var disposition: String? + /// The attachment's content-disposition specifies how you would like the attachment to be displayed. + /// + /// For example, inline results in the attached file being displayed automatically within the message + /// while attachment results in the attached file requiring some action to be taken before it is displayed + /// such as opening or downloading the file. + public var disposition: Disposition? + + public enum Disposition: String, Codable, Sendable { + case inline + case attachment + } /// The content ID for the attachment. /// /// This is used when the disposition is set to “inline” and the attachment is an image, /// allowing the file to be displayed within the body of your email. - public var contentId: String? + public var contentID: String? public init( content: String, type: String? = nil, filename: String, - disposition: String? = nil, - contentId: String? = nil + disposition: Disposition? = nil, + contentID: String? = nil ) { self.content = content self.type = type self.filename = filename self.disposition = disposition - self.contentId = contentId + self.contentID = contentID } private enum CodingKeys: String, CodingKey { @@ -40,6 +49,6 @@ public struct EmailAttachment: Codable, Sendable { case type case filename case disposition - case contentId = "content_id" + case contentID = "content_id" } } diff --git a/Sources/SendGridKit/Models/SendGridEmail.swift b/Sources/SendGridKit/Models/SendGridEmail.swift index 186af8f..bdc57d6 100644 --- a/Sources/SendGridKit/Models/SendGridEmail.swift +++ b/Sources/SendGridKit/Models/SendGridEmail.swift @@ -11,12 +11,19 @@ public struct SendGridEmail: Codable, Sendable { public var replyTo: EmailAddress? - /// An array of recipients who will receive replies and/or bounces. + /// An array of recipients to whom replies will be sent. + /// + /// Each object in this array must contain a recipient's email address. + /// Each object in the array may optionally contain a recipient's name. + /// You can use either the `reply_to property` or `reply_to_list` property but not both. public var replyToList: [EmailAddress]? - /// The global, or “message level”, subject of your email. + /// The global or _message level_ subject of your email. + /// + /// Subject lines set in personalizations objects will override this global subject line. + /// See line length limits specified in RFC 2822 for guidance on subject line character limits. /// - /// > Note: This may be overridden by `personalizations[x].subject`. + /// > Note: Min length: 1. public var subject: String? /// An array in which you may specify the content of your email. @@ -29,7 +36,7 @@ public struct SendGridEmail: Codable, Sendable { /// /// > Note: If you use a template that contains a subject and content (either text or HTML), /// you do not need to specify those at the personalizations nor message level. - public var templateId: String? + public var templateID: String? /// An object containing key/value pairs of header names and the value to substitute for them. /// @@ -57,7 +64,7 @@ public struct SendGridEmail: Codable, Sendable { /// /// Including a `batch_id` in your request allows you include this email in that batch, /// and also enables you to cancel or pause the delivery of that batch. - public var batchId: String? + public var batchID: String? /// An object allowing you to specify how to handle unsubscribes. public var asm: AdvancedSuppressionManager? @@ -79,12 +86,12 @@ public struct SendGridEmail: Codable, Sendable { subject: String? = nil, content: [EmailContent]? = nil, attachments: [EmailAttachment]? = nil, - templateId: String? = nil, + templateID: String? = nil, headers: [String: String]? = nil, categories: [String]? = nil, customArgs: [String: String]? = nil, sendAt: Date? = nil, - batchId: String? = nil, + batchID: String? = nil, asm: AdvancedSuppressionManager? = nil, ipPoolName: String? = nil, mailSettings: MailSettings? = nil, @@ -97,12 +104,12 @@ public struct SendGridEmail: Codable, Sendable { self.subject = subject self.content = content self.attachments = attachments - self.templateId = templateId + self.templateID = templateID self.headers = headers self.categories = categories self.customArgs = customArgs self.sendAt = sendAt - self.batchId = batchId + self.batchID = batchID self.asm = asm self.ipPoolName = ipPoolName self.mailSettings = mailSettings @@ -117,12 +124,12 @@ public struct SendGridEmail: Codable, Sendable { case subject case content case attachments - case templateId = "template_id" + case templateID = "template_id" case headers case categories case customArgs = "custom_args" case sendAt = "send_at" - case batchId = "batch_id" + case batchID = "batch_id" case asm case ipPoolName = "ip_pool_name" case mailSettings = "mail_settings" diff --git a/Sources/SendGridKit/Models/SendGridError.swift b/Sources/SendGridKit/Models/SendGridError.swift index 7e0766c..98ace4b 100644 --- a/Sources/SendGridKit/Models/SendGridError.swift +++ b/Sources/SendGridKit/Models/SendGridError.swift @@ -2,10 +2,18 @@ import Foundation public struct SendGridError: Error, Decodable, Sendable { public var errors: [SendGridErrorResponse]? + + /// When applicable, this property value will be an error ID. + public var ids: String? } public struct SendGridErrorResponse: Decodable, Sendable { + /// An error message. public var message: String? + + /// When applicable, this property value will be the field that generated the error. public var field: String? + + /// When applicable, this property value will be helper text or a link to documentation to help you troubleshoot the error. public var help: String? } diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift index ef8ac0c..59dab94 100644 --- a/Sources/SendGridKit/SendGridClient.swift +++ b/Sources/SendGridKit/SendGridClient.swift @@ -5,7 +5,7 @@ import NIOHTTP1 import NIOFoundationCompat public struct SendGridClient: Sendable { - let apiURL = "https://api.sendgrid.com/v3/mail/send" + let apiURL: String let httpClient: HTTPClient let apiKey: String @@ -21,31 +21,34 @@ public struct SendGridClient: Sendable { return decoder }() - public init(httpClient: HTTPClient, apiKey: String) { + /// Initialize a new `SendGridClient` + /// + /// - Parameters: + /// - httpClient: The `HTTPClient` to use for sending requests + /// - apiKey: The SendGrid API key + /// - forEU: Whether to use the API endpoint for global users and subusers or for EU regional subusers + public init(httpClient: HTTPClient, apiKey: String, forEU: Bool = false) { self.httpClient = httpClient self.apiKey = apiKey + self.apiURL = forEU ? "https://api.eu.sendgrid.com/v3/mail/send" : "https://api.sendgrid.com/v3/mail/send" } public func send(email: SendGridEmail) async throws { var headers = HTTPHeaders() headers.add(name: "Authorization", value: "Bearer \(apiKey)") headers.add(name: "Content-Type", value: "application/json") + + var request = HTTPClientRequest(url: apiURL) + request.method = .POST + request.headers = headers + request.body = try HTTPClientRequest.Body.bytes(encoder.encode(email)) - let response = try await httpClient.execute( - request: .init( - url: apiURL, - method: .POST, - headers: headers, - body: .data(encoder.encode(email)) - ) - ).get() + let response = try await httpClient.execute(request, timeout: .seconds(30)) // If the request was accepted, simply return if response.status == .ok || response.status == .accepted { return } - // JSONDecoder will handle empty body by throwing decoding error - let byteBuffer = response.body ?? ByteBuffer(.init()) - - throw try decoder.decode(SendGridError.self, from: byteBuffer) + // JSONDecoder will handle empty body by throwing decoding error + throw try await decoder.decode(SendGridError.self, from: response.body.collect(upTo: 1024 * 1024)) } } diff --git a/Tests/SendGridKitTests/SendGridTestsKit.swift b/Tests/SendGridKitTests/SendGridTestsKit.swift index d55cb92..6f0fdca 100644 --- a/Tests/SendGridKitTests/SendGridTestsKit.swift +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -27,15 +27,51 @@ struct SendGridKitTests { let emailContent = EmailContent("This email was sent using SendGridKit!") + let setting = Setting(enable: true) + let mailSettings = MailSettings( + bypassListManagement: setting, + bypassSpamManagement: setting, + bypassBounceManagement: setting, + footer: Footer(enable: true, text: "footer", html: "footer"), + sandboxMode: setting + ) + + let trackingSettings = TrackingSettings( + clickTracking: ClickTracking(enable: true, enableText: true), + openTracking: OpenTracking(enable: true, substitutionTag: "open_tracking"), + subscriptionTracking: SubscriptionTracking( + enable: true, + text: "sub_text", + html: "sub_html", + substitutionTag: "sub_tag" + ), + ganalytics: GoogleAnalytics( + enable: true, + utmSource: "utm_source", + utmMedium: "utm_medium", + utmTerm: "utm_term", + utmContent: "utm_content", + utmCampaign: "utm_campaign" + ) + ) + + let asm = AdvancedSuppressionManager(groupID: 21, groupsToDisplay: ["group1", "group2"]) + let email = SendGridEmail( personalizations: [personalization], from: fromEmailAddress, content: [emailContent], - attachments: [attachment] + attachments: [attachment], + asm: asm, + mailSettings: mailSettings, + trackingSettings: trackingSettings ) - await withKnownIssue { + try await withKnownIssue { try await client.send(email: email) + } when: { + // TODO: Replace with `false` when you have a valid API key + true } } } From c93aac2ab2b0a4809b9c4f676dd83531bdfec95a Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 18 Sep 2024 19:14:18 +0200 Subject: [PATCH 3/7] Fix test --- Tests/SendGridKitTests/SendGridTestsKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SendGridKitTests/SendGridTestsKit.swift b/Tests/SendGridKitTests/SendGridTestsKit.swift index 6f0fdca..972fa94 100644 --- a/Tests/SendGridKitTests/SendGridTestsKit.swift +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -22,7 +22,7 @@ struct SendGridKitTests { content: "Hello, World!".data(using: .utf8)!.base64EncodedString(), type: "text/plain", filename: "hello.txt", - disposition: "attachment" + disposition: .attachment ) let emailContent = EmailContent("This email was sent using SendGridKit!") From e8461974b6172e7007e05545d2ede1a692672bc2 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 20 Sep 2024 19:23:31 +0200 Subject: [PATCH 4/7] Update SPI manifest file --- .spi.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.spi.yml b/.spi.yml index b5ab714..8a3fa63 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,5 @@ version: 1 builder: configs: - - documentation_targets: [SendGridKit] \ No newline at end of file + - documentation_targets: [SendGridKit] + swift_version: 6.0 \ No newline at end of file From 2f5119dbb0c01e168654fea1caf4cf5fb6f5c52c Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 20 Sep 2024 19:37:40 +0200 Subject: [PATCH 5/7] Update badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ceba67..728c175 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - Swift 5.10+ + Swift 6.0+
From e281fe154df1ab3d86bd9830837f885ea8105628 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 20 Sep 2024 20:06:06 +0200 Subject: [PATCH 6/7] Fix error and Swift settings --- Package.swift | 10 ++++++++-- Sources/SendGridKit/Models/SendGridError.swift | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index c02ed21..d22cae6 100644 --- a/Package.swift +++ b/Package.swift @@ -17,13 +17,19 @@ let package = Package( name: "SendGridKit", dependencies: [ .product(name: "AsyncHTTPClient", package: "async-http-client"), - ] + ], + swiftSettings: swiftSettings ), .testTarget( name: "SendGridKitTests", dependencies: [ .target(name: "SendGridKit"), - ] + ], + swiftSettings: swiftSettings ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ExistentialAny"), +] } \ No newline at end of file diff --git a/Sources/SendGridKit/Models/SendGridError.swift b/Sources/SendGridKit/Models/SendGridError.swift index 98ace4b..64a1fdf 100644 --- a/Sources/SendGridKit/Models/SendGridError.swift +++ b/Sources/SendGridKit/Models/SendGridError.swift @@ -4,7 +4,7 @@ public struct SendGridError: Error, Decodable, Sendable { public var errors: [SendGridErrorResponse]? /// When applicable, this property value will be an error ID. - public var ids: String? + public var id: String? } public struct SendGridErrorResponse: Decodable, Sendable { From a87da0d1a39c1aafb8ad8d6b94d0dc59031d4ca9 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 20 Sep 2024 21:45:05 +0200 Subject: [PATCH 7/7] Check for all 2xx response status code --- Sources/SendGridKit/SendGridClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift index 59dab94..21a8a2c 100644 --- a/Sources/SendGridKit/SendGridClient.swift +++ b/Sources/SendGridKit/SendGridClient.swift @@ -46,7 +46,7 @@ public struct SendGridClient: Sendable { let response = try await httpClient.execute(request, timeout: .seconds(30)) // If the request was accepted, simply return - if response.status == .ok || response.status == .accepted { return } + if (200...299).contains(response.status.code) { return } // JSONDecoder will handle empty body by throwing decoding error throw try await decoder.decode(SendGridError.self, from: response.body.collect(upTo: 1024 * 1024))