diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf05755..740c8dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,15 @@ on: push: { branches: [ main ] } jobs: + lint: + runs-on: ubuntu-latest + container: swift:jammy + steps: + - name: Check out SendGridKit + uses: actions/checkout@v4 + - name: Run format lint check + run: swift format lint --strict --recursive --parallel . + unit-tests: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main secrets: diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..360ca2c --- /dev/null +++ b/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentation": { + "spaces": 4 + }, + "indentConditionalCompilationBlocks": true, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineLength": 100, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": false, + "tabWidth": 8, + "version": 1 +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index d22cae6..5f10c6d 100644 --- a/Package.swift +++ b/Package.swift @@ -4,32 +4,34 @@ import PackageDescription let package = Package( name: "sendgrid-kit", platforms: [ - .macOS(.v14), + .macOS(.v14) ], products: [ - .library(name: "SendGridKit", targets: ["SendGridKit"]), + .library(name: "SendGridKit", targets: ["SendGridKit"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.22.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.22.0") ], targets: [ .target( name: "SendGridKit", dependencies: [ - .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "AsyncHTTPClient", package: "async-http-client") ], swiftSettings: swiftSettings ), .testTarget( name: "SendGridKitTests", dependencies: [ - .target(name: "SendGridKit"), + .target(name: "SendGridKit") ], swiftSettings: swiftSettings ), ] ) -var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("ExistentialAny"), -] } \ No newline at end of file +var swiftSettings: [SwiftSetting] { + [ + .enableUpcomingFeature("ExistentialAny") + ] +} diff --git a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift index 575abb1..5a213db 100644 --- a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift +++ b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift @@ -2,15 +2,15 @@ import Foundation public struct AdvancedSuppressionManager: Codable, Sendable { /// The unsubscribe group to associate with this email. - /// + /// /// 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, groupsToDisplay: [String]? = nil @@ -18,7 +18,7 @@ public struct AdvancedSuppressionManager: Codable, Sendable { self.groupID = groupID self.groupsToDisplay = groupsToDisplay } - + private enum CodingKeys: String, CodingKey { case groupID = "group_id" case groupsToDisplay = "groups_to_display" diff --git a/Sources/SendGridKit/Models/EmailAddress.swift b/Sources/SendGridKit/Models/EmailAddress.swift index d3cbfad..439e7f7 100644 --- a/Sources/SendGridKit/Models/EmailAddress.swift +++ b/Sources/SendGridKit/Models/EmailAddress.swift @@ -3,10 +3,10 @@ import Foundation public struct EmailAddress: Codable, Sendable { /// The email address of the person to whom you are sending an email. public var email: String - + /// The name of the person to whom you are sending an email. public var name: String? - + public init( email: String, name: String? = nil diff --git a/Sources/SendGridKit/Models/EmailAttachment.swift b/Sources/SendGridKit/Models/EmailAttachment.swift index 8e13788..c2c8027 100644 --- a/Sources/SendGridKit/Models/EmailAttachment.swift +++ b/Sources/SendGridKit/Models/EmailAttachment.swift @@ -3,17 +3,17 @@ import Foundation public struct EmailAttachment: Codable, Sendable { /// The Base64 encoded content of the attachment. public var content: String - + /// The MIME type of the content you are attaching. - /// + /// /// For example, `image/jpeg`, `text/html` or `application/pdf`. public var type: String? - + /// The attachment's filename, including the file extension. public var filename: 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. @@ -23,13 +23,13 @@ public struct EmailAttachment: 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 init( content: String, type: String? = nil, @@ -43,7 +43,7 @@ public struct EmailAttachment: Codable, Sendable { self.disposition = disposition self.contentID = contentID } - + private enum CodingKeys: String, CodingKey { case content case type diff --git a/Sources/SendGridKit/Models/EmailContent.swift b/Sources/SendGridKit/Models/EmailContent.swift index 63d3989..c0d5f41 100644 --- a/Sources/SendGridKit/Models/EmailContent.swift +++ b/Sources/SendGridKit/Models/EmailContent.swift @@ -2,12 +2,12 @@ import Foundation public struct EmailContent: Codable, Sendable { /// The MIME type of the content you are including in your email. - /// + /// /// For example, `“text/plain”` or `“text/html”`. public var type: String /// The actual content of the specified MIME type that you are including in your email. - /// + /// /// > Important: The minimum length is 1. public var value: String diff --git a/Sources/SendGridKit/Models/MailSettings.swift b/Sources/SendGridKit/Models/MailSettings.swift index de1463c..7233d48 100644 --- a/Sources/SendGridKit/Models/MailSettings.swift +++ b/Sources/SendGridKit/Models/MailSettings.swift @@ -2,28 +2,28 @@ import Foundation public struct MailSettings: Codable, Sendable { /// Allows you to bypass all unsubscribe groups and suppressions to ensure that the email is delivered to every single recipient. - /// + /// /// > Important: This should only be used in emergencies when it is absolutely necessary that every recipient receives your email. public var bypassListManagement: Setting? - + /// Allows you to bypass the spam report list to ensure that the email is delivered to recipients. - /// + /// /// > Note: Bounce and unsubscribe lists will still be checked; /// addresses on these other lists will not receive the message. public var bypassSpamManagement: Setting? /// Allows you to bypass the bounce list to ensure that the email is delivered to recipients. - /// + /// /// > Note: Spam report and unsubscribe lists will still be checked; /// addresses on these other lists will not receive the message. public var bypassBounceManagement: Setting? /// The default footer that you would like included on every email. public var footer: Footer? - + /// This allows you to send a test email to ensure that your request body is valid and formatted correctly. public var sandboxMode: Setting? - + public init( bypassListManagement: Setting? = nil, bypassSpamManagement: Setting? = nil, @@ -37,7 +37,7 @@ public struct MailSettings: Codable, Sendable { self.footer = footer self.sandboxMode = sandboxMode } - + private enum CodingKeys: String, CodingKey { case bypassListManagement = "bypass_list_management" case bypassSpamManagement = "bypass_spam_management" @@ -59,13 +59,13 @@ public struct Setting: Codable, Sendable { public struct Footer: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool - + /// The plain text content of your footer. public var text: String? - + /// The HTML content of your footer. public var html: String? - + public init( enable: Bool, text: String? = nil, diff --git a/Sources/SendGridKit/Models/Personalization.swift b/Sources/SendGridKit/Models/Personalization.swift index ede38bf..66abe9e 100644 --- a/Sources/SendGridKit/Models/Personalization.swift +++ b/Sources/SendGridKit/Models/Personalization.swift @@ -2,17 +2,17 @@ import Foundation public struct Personalization: Codable, Sendable { /// An array of recipients. - /// + /// /// > Important: Each object within this array may contain the name, but must always contain the email, of a recipient. public var to: [EmailAddress]? /// An array of recipients who will receive a copy of your email. - /// + /// /// > Important: Each object within this array may contain the name, but must always contain the email, of a recipient. public var cc: [EmailAddress]? /// An array of recipients who will receive a blind carbon copy of your email. - /// + /// /// > Important: Each object within this array may contain the name, but must always contain the email, of a recipient. public var bcc: [EmailAddress]? @@ -24,18 +24,18 @@ public struct Personalization: Codable, /// A collection of key/value pairs following the pattern `"substitution_tag":"value to substitute"`. public var substitutions: [String: String]? - + /// A collection of key/value pairs following the pattern `"key":"value"` to substitute handlebar template data. public var dynamicTemplateData: DynamicTemplateData? - + /// Values that are specific to this personalization that will be carried along with the email and its activity data. public var customArgs: [String: String]? - + /// A UNIX timestamp allowing you to specify when you want your email to be delivered. - /// + /// /// > Important: Scheduling more than 72 hours in advance is forbidden. public var sendAt: Date? - + public init( to: [EmailAddress]? = nil, cc: [EmailAddress]? = nil, @@ -57,7 +57,7 @@ public struct Personalization: Codable, self.customArgs = customArgs self.sendAt = sendAt } - + private enum CodingKeys: String, CodingKey { case to case cc @@ -71,8 +71,8 @@ public struct Personalization: Codable, } } -public extension Personalization where DynamicTemplateData == [String: String] { - init( +extension Personalization where DynamicTemplateData == [String: String] { + public init( to: [EmailAddress]? = nil, cc: [EmailAddress]? = nil, bcc: [EmailAddress]? = nil, diff --git a/Sources/SendGridKit/Models/SendGridEmail.swift b/Sources/SendGridKit/Models/SendGridEmail.swift index 897fcbd..aff8657 100644 --- a/Sources/SendGridKit/Models/SendGridEmail.swift +++ b/Sources/SendGridKit/Models/SendGridEmail.swift @@ -2,7 +2,7 @@ import Foundation public struct SendGridEmail: Codable, Sendable { /// An array of messages and their metadata. - /// + /// /// Each object within `personalizations` can be thought of as an envelope - /// it defines who should receive an individual message and how that message should be handled. public var personalizations: [Personalization] @@ -10,58 +10,58 @@ public struct SendGridEmail: Codable, S public var from: EmailAddress public var replyTo: EmailAddress? - + /// 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. - /// + /// /// 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: Min length: 1. public var subject: String? - + /// An array in which you may specify the content of your email. public var content: [EmailContent]? /// An array of objects in which you can specify any attachments you want to include. public var attachments: [EmailAttachment]? - + /// The ID of a template that you would like to use. - /// + /// /// > 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? - + /// An object containing key/value pairs of header names and the value to substitute for them. - /// + /// /// > Important: You must ensure these are properly encoded if they contain unicode characters. - /// + /// /// > Important: Must not be one of the reserved headers. public var headers: [String: String]? /// An array of category names for this message. - /// + /// /// > Important: Each category name may not exceed 255 characters. public var categories: [String]? /// Values that are specific to the entire send that will be carried along with the email and its activity data. public var customArgs: [String: String]? - + /// A UNIX timestamp allowing you to specify when you want your email to be delivered. - /// + /// /// > Note: This may be overridden by the `personalizations[x].send_at` parameter. - /// + /// /// > Important: You can't schedule more than 72 hours in advance. public var sendAt: Date? /// This ID represents a batch of emails to be sent at the same time. - /// + /// /// 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? @@ -77,7 +77,7 @@ public struct SendGridEmail: Codable, S /// Settings to determine how you would like to track the metrics of how your recipients interact with your email. public var trackingSettings: TrackingSettings? - + public init( personalizations: [Personalization], from: EmailAddress, @@ -115,7 +115,7 @@ public struct SendGridEmail: Codable, S self.mailSettings = mailSettings self.trackingSettings = trackingSettings } - + private enum CodingKeys: String, CodingKey { case personalizations case from @@ -137,8 +137,8 @@ public struct SendGridEmail: Codable, S } } -public extension SendGridEmail where DynamicTemplateData == [String: String] { - init( +extension SendGridEmail where DynamicTemplateData == [String: String] { + public init( personalizations: [Personalization<[String: String]>], from: EmailAddress, replyTo: EmailAddress? = nil, diff --git a/Sources/SendGridKit/Models/TrackingSettings.swift b/Sources/SendGridKit/Models/TrackingSettings.swift index 4a1bb34..7cc1537 100644 --- a/Sources/SendGridKit/Models/TrackingSettings.swift +++ b/Sources/SendGridKit/Models/TrackingSettings.swift @@ -3,21 +3,21 @@ import Foundation public struct TrackingSettings: Codable, Sendable { /// Allows you to track whether a recipient clicked a link in your email. public var clickTracking: ClickTracking? - + /// Allows you to track whether the email was opened or not, /// but including a single pixel image in the body of the content. - /// + /// /// When the pixel is loaded, we can log that the email was opened. public var openTracking: OpenTracking? - + /// Allows you to insert a subscription management link at the bottom of the text and HTML bodies of your email. - /// + /// /// > Tip: If you would like to specify the location of the link within your email, you may use the ``SubscriptionTracking/substitutionTag``. public var subscriptionTracking: SubscriptionTracking? - + /// Allows you to enable tracking provided by Google Analytics. public var ganalytics: GoogleAnalytics? - + public init( clickTracking: ClickTracking? = nil, openTracking: OpenTracking? = nil, @@ -29,7 +29,7 @@ public struct TrackingSettings: Codable, Sendable { self.subscriptionTracking = subscriptionTracking self.ganalytics = ganalytics } - + private enum CodingKeys: String, CodingKey { case clickTracking = "click_tracking" case openTracking = "open_tracking" @@ -41,7 +41,7 @@ public struct TrackingSettings: Codable, Sendable { public struct ClickTracking: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool - + /// Indicates if this setting should be included in the text/plain portion of your email. public var enableText: Bool @@ -49,7 +49,7 @@ public struct ClickTracking: Codable, Sendable { self.enable = enable self.enableText = enableText } - + private enum CodingKeys: String, CodingKey { case enable case enableText = "enable_text" @@ -59,12 +59,12 @@ public struct ClickTracking: Codable, Sendable { public struct OpenTracking: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool - + /// Allows you to specify a substitution tag that you can insert in the body of your email at a location that you desire. - /// + /// /// > Note: This tag will be replaced by the open tracking pixel. public var substitutionTag: String? - + public init( enable: Bool, substitutionTag: String? = nil @@ -72,7 +72,7 @@ public struct OpenTracking: Codable, Sendable { self.enable = enable self.substitutionTag = substitutionTag } - + private enum CodingKeys: String, CodingKey { case enable case substitutionTag = "substitution_tag" @@ -82,25 +82,25 @@ public struct OpenTracking: Codable, Sendable { public struct SubscriptionTracking: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool - + /// Text to be appended to the email, with the subscription tracking link. - /// + /// /// > Tip: You may control where the link is by using the tag `<% %>`. public var text: String? - + /// HTML to be appended to the email, with the subscription tracking link. - /// + /// /// > Tip: You may control where the link is by using the tag `<% %>`. public var html: String? - + /// A tag that will be replaced with the unsubscribe URL. - /// + /// /// For example: `[unsubscribe_url]`. - /// + /// /// If this parameter is used, it will override both the ``SubscriptionTracking/text`` and ``SubscriptionTracking/html`` parameters. /// The URL of the link will be placed at the substitution tag’s location, with no additional formatting. public var substitutionTag: String? - + public init( enable: Bool, text: String? = nil, @@ -112,7 +112,7 @@ public struct SubscriptionTracking: Codable, Sendable { self.html = html self.substitutionTag = substitutionTag } - + private enum CodingKeys: String, CodingKey { case enable case text @@ -124,22 +124,22 @@ public struct SubscriptionTracking: Codable, Sendable { public struct GoogleAnalytics: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool - + /// Name of the referrer source. (e.g. Google, SomeDomain.com, or Marketing Email) public var utmSource: String? - + /// Name of the marketing medium. (e.g. Email) public var utmMedium: String? - + /// Used to identify any paid keywords. public var utmTerm: String? - + /// Used to differentiate your campaign from advertisements. public var utmContent: String? - + /// The name of the campaign. public var utmCampaign: String? - + public init( enable: Bool, utmSource: String? = nil, @@ -155,7 +155,7 @@ public struct GoogleAnalytics: Codable, Sendable { self.utmContent = utmContent self.utmCampaign = utmCampaign } - + private enum CodingKeys: String, CodingKey { case enable case utmSource = "utm_source" diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift index 9d395ac..bbdcc75 100644 --- a/Sources/SendGridKit/SendGridClient.swift +++ b/Sources/SendGridKit/SendGridClient.swift @@ -1,20 +1,20 @@ +import AsyncHTTPClient import Foundation import NIO -import AsyncHTTPClient -import NIOHTTP1 import NIOFoundationCompat +import NIOHTTP1 public struct SendGridClient: Sendable { let apiURL: String let httpClient: HTTPClient let apiKey: String - + private let encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .secondsSince1970 return encoder }() - + private let decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .secondsSince1970 @@ -30,10 +30,14 @@ public struct SendGridClient: Sendable { 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" + 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 { + + 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") @@ -42,13 +46,14 @@ public struct SendGridClient: Sendable { request.method = .POST request.headers = headers request.body = try HTTPClientRequest.Body.bytes(encoder.encode(email)) - + let response = try await httpClient.execute(request, timeout: .seconds(30)) - + // If the request was accepted, simply 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)) + + // `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 3c23cdc..9081339 100644 --- a/Tests/SendGridKitTests/SendGridTestsKit.swift +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -1,15 +1,15 @@ -import Testing import AsyncHTTPClient import SendGridKit +import Testing struct SendGridKitTests { var client: SendGridClient - + init() { // TODO: Replace with a valid API key to test client = SendGridClient(httpClient: HTTPClient.shared, apiKey: "YOUR-API-KEY") } - + @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") @@ -66,7 +66,7 @@ struct SendGridKitTests { mailSettings: mailSettings, trackingSettings: trackingSettings ) - + try await withKnownIssue { try await client.send(email: email) } when: { @@ -81,11 +81,16 @@ struct SendGridKitTests { let integer: Int let double: Double } - let dynamicTemplateData = DynamicTemplateData(text: "Hello, World!", integer: 42, double: 3.14) - + let dynamicTemplateData = DynamicTemplateData( + text: "Hello, World!", integer: 42, double: 3.14) + // TODO: Replace the addresses with real email addresses - let personalization = Personalization(to: [EmailAddress("TO-ADDRESS")], subject: "Test Email", dynamicTemplateData: dynamicTemplateData) - let email = SendGridEmail(personalizations: [personalization], from: EmailAddress("FROM-ADDRESS"), content: [EmailContent("Hello, World!")]) + let personalization = Personalization( + to: [EmailAddress("TO-ADDRESS")], subject: "Test Email", + dynamicTemplateData: dynamicTemplateData) + let email = SendGridEmail( + personalizations: [personalization], from: EmailAddress("FROM-ADDRESS"), + content: [EmailContent("Hello, World!")]) try await withKnownIssue { try await client.send(email: email)