Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt swift-format #21

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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:
70 changes: 70 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 10 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"),
] }
var swiftSettings: [SwiftSetting] {
[
.enableUpcomingFeature("ExistentialAny")
]
}
10 changes: 5 additions & 5 deletions Sources/SendGridKit/Models/AdvancedSuppressionManager.swift
Original file line number Diff line number Diff line change
@@ -2,23 +2,23 @@ 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
) {
self.groupID = groupID
self.groupsToDisplay = groupsToDisplay
}

private enum CodingKeys: String, CodingKey {
case groupID = "group_id"
case groupsToDisplay = "groups_to_display"
4 changes: 2 additions & 2 deletions Sources/SendGridKit/Models/EmailAddress.swift
Original file line number Diff line number Diff line change
@@ -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
18 changes: 9 additions & 9 deletions Sources/SendGridKit/Models/EmailAttachment.swift
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions Sources/SendGridKit/Models/EmailContent.swift
Original file line number Diff line number Diff line change
@@ -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

20 changes: 10 additions & 10 deletions Sources/SendGridKit/Models/MailSettings.swift
Original file line number Diff line number Diff line change
@@ -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,
22 changes: 11 additions & 11 deletions Sources/SendGridKit/Models/Personalization.swift
Original file line number Diff line number Diff line change
@@ -2,17 +2,17 @@ import Foundation

public struct Personalization<DynamicTemplateData: Codable & Sendable>: 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<DynamicTemplateData: Codable & Sendable>: 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<DynamicTemplateData: Codable & Sendable>: Codable,
self.customArgs = customArgs
self.sendAt = sendAt
}

private enum CodingKeys: String, CodingKey {
case to
case cc
@@ -71,8 +71,8 @@ public struct Personalization<DynamicTemplateData: Codable & Sendable>: 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,
40 changes: 20 additions & 20 deletions Sources/SendGridKit/Models/SendGridEmail.swift
Original file line number Diff line number Diff line change
@@ -2,66 +2,66 @@ import Foundation

public struct SendGridEmail<DynamicTemplateData: Codable & Sendable>: 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<DynamicTemplateData>]

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<DynamicTemplateData: Codable & Sendable>: 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<DynamicTemplateData>],
from: EmailAddress,
@@ -115,7 +115,7 @@ public struct SendGridEmail<DynamicTemplateData: Codable & Sendable>: Codable, S
self.mailSettings = mailSettings
self.trackingSettings = trackingSettings
}

private enum CodingKeys: String, CodingKey {
case personalizations
case from
@@ -137,8 +137,8 @@ public struct SendGridEmail<DynamicTemplateData: Codable & Sendable>: 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,
58 changes: 29 additions & 29 deletions Sources/SendGridKit/Models/TrackingSettings.swift
Original file line number Diff line number Diff line change
@@ -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,15 +41,15 @@ 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

public init(enable: Bool, enableText: Bool) {
self.enable = enable
self.enableText = enableText
}

private enum CodingKeys: String, CodingKey {
case enable
case enableText = "enable_text"
@@ -59,20 +59,20 @@ 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
) {
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"
29 changes: 17 additions & 12 deletions Sources/SendGridKit/SendGridClient.swift
Original file line number Diff line number Diff line change
@@ -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<DynamicTemplateData: Codable & Sendable>(email: SendGridEmail<DynamicTemplateData>) async throws {

public func send<DynamicTemplateData: Codable & Sendable>(
email: SendGridEmail<DynamicTemplateData>
) 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))
}
}
21 changes: 13 additions & 8 deletions Tests/SendGridKitTests/SendGridTestsKit.swift
Original file line number Diff line number Diff line change
@@ -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)