Skip to content

Commit ec87aa6

Browse files
fpseverinoMahdiBM
andauthored
Update to SendGridKit v3 (#50)
- Update to SendGridKit v3 - Improve (hopefully) the storage to the `Application` - I've removed the `initialize()` function since it only really served to check if the environment key was set. The user wasn't even required to call it. - Add ability to access the SendGrid client from the `Request` - Adopt `swift-format` with `vapor/ci` linting - Switch from XCTest to Swift Testing --------- Co-authored-by: Mahdi Bahrami <github@mahdibm.com>
1 parent 9ed05f6 commit ec87aa6

File tree

12 files changed

+263
-105
lines changed

12 files changed

+263
-105
lines changed

.env.testing

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SENDGRID_API_KEY=SG.1234567890

.github/workflows/test.yml

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
name: test
2+
concurrency:
3+
group: ${{ github.workflow }}-${{ github.ref }}
4+
cancel-in-progress: true
25
on:
36
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
47
push: { branches: [ main ] }
58

69
jobs:
710
unit-tests:
8-
uses: vapor/ci/.github/workflows/run-unit-tests.yml@reusable-workflows
11+
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
912
with:
10-
with_coverage: true
11-
with_tsan: true
13+
with_linting: true
14+
secrets:
15+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.spi.yml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
version: 1
2+
builder:
3+
configs:
4+
- documentation_targets: [SendGrid]
5+
swift_version: 6.0

Package.swift

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:6.0
22
import PackageDescription
33

44
let package = Package(
55
name: "sendgrid",
66
platforms: [
7-
.macOS(.v10_15)
7+
.macOS(.v14)
88
],
99
products: [
1010
.library(name: "SendGrid", targets: ["SendGrid"])
1111
],
1212
dependencies: [
1313
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
14-
.package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "2.0.2"),
14+
.package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "3.0.0-rc.1"),
1515
],
1616
targets: [
17-
.target(name: "SendGrid", dependencies: [
18-
.product(name: "Vapor", package: "vapor"),
19-
.product(name: "SendGridKit", package: "sendgrid-kit"),
20-
]),
21-
.testTarget(name: "SendGridTests", dependencies: ["SendGrid"])
17+
.target(
18+
name: "SendGrid",
19+
dependencies: [
20+
.product(name: "Vapor", package: "vapor"),
21+
.product(name: "SendGridKit", package: "sendgrid-kit"),
22+
],
23+
swiftSettings: swiftSettings
24+
),
25+
.testTarget(
26+
name: "SendGridTests",
27+
dependencies: [
28+
.target(name: "SendGrid"),
29+
.product(name: "XCTVapor", package: "vapor"),
30+
],
31+
swiftSettings: swiftSettings
32+
),
2233
]
2334
)
35+
36+
var swiftSettings: [SwiftSetting] {
37+
[
38+
.enableUpcomingFeature("ExistentialAny")
39+
]
40+
}

README.md

+47-38
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,71 @@
1-
# SendGrid Provider for Vapor
2-
3-
<p align="center">
4-
<a href="https://github.com/vapor-community/sendgrid/actions">
5-
<img src="https://github.com/vapor-community/sendgrid/workflows/test/badge.svg" alt="Continuous Integration">
1+
<div align="center">
2+
<img src="https://avatars.githubusercontent.com/u/26165732?s=200&v=4" width="100" height="100" alt="avatar" />
3+
<h1>SendGrid</h1>
4+
<a href="https://swiftpackageindex.com/vapor-community/sendgrid/documentation">
5+
<img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation">
6+
</a>
7+
<a href="https://discord.gg/vapor"><img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat"></a>
8+
<a href="LICENSE"><img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License"></a>
9+
<a href="https://github.com/vapor-community/sendgrid/actions/workflows/test.yml">
10+
<img src="https://img.shields.io/github/actions/workflow/status/vapor-community/sendgrid/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration">
11+
</a>
12+
<a href="https://codecov.io/github/vapor-community/sendgrid">
13+
<img src="https://img.shields.io/codecov/c/github/vapor-community/sendgrid?style=plastic&logo=codecov&label=codecov">
614
</a>
715
<a href="https://swift.org">
8-
<img src="http://img.shields.io/badge/swift-5.2-brightgreen.svg" alt="Swift 5.2">
16+
<img src="https://design.vapor.codes/images/swift60up.svg" alt="Swift 6.0+">
917
</a>
10-
</p>
18+
</div>
19+
<br>
20+
21+
📧 SendGrid library for the Vapor web framework, based on [SendGridKit](https://github.com/vapor-community/sendgrid-kit).
22+
23+
Send simple emails, or leverage the full capabilities of [SendGrid's V3 API](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send).
24+
25+
### Getting Started
1126

12-
Adds a mail backend for SendGrid to the Vapor web framework. Send simple emails,
13-
or leverage the full capabilities of SendGrid's V3 API.
27+
Use the SPM string to easily include the dependendency in your `Package.swift` file
1428

15-
## Setup
16-
Add the dependency to Package.swift:
29+
```swift
30+
.package(url: "https://github.com/vapor-community/sendgrid.git", from: "6.0.0-rc.1")
31+
```
1732

18-
~~~~swift
19-
.package(url: "https://github.com/vapor-community/sendgrid.git", from: "4.0.0")
20-
~~~~
33+
and add it to your target's dependencies:
2134

22-
Make sure `SENDGRID_API_KEY` is set in your environment. This can be set in the
23-
Xcode scheme, or specified in your `docker-compose.yml`, or even provided as
24-
part of a `swift run` command.
35+
```swift
36+
.product(name: "SendGrid", package: "sendgrid")
37+
```
2538

26-
Optionally, explicitly initialize the provider (this is strongly recommended, as
27-
otherwise a missing API key will cause a fatal error some time later in your
28-
application):
39+
## Overview
2940

30-
~~~~swift
31-
app.sendgrid.initialize()
32-
~~~~
41+
> [!WARNING]
42+
> Make sure that the `SENDGRID_API_KEY` variable is set in your environment.
43+
This can be set in the Xcode scheme, or specified in your `docker-compose.yml`, or even provided as part of a `swift run` command.
44+
A missing API key will result in a fatal error.
3345

34-
Now you can access the client at any time:
35-
~~~~swift
36-
app.sendgrid.client
37-
~~~~
46+
### Using the API
3847

39-
## Using the API
48+
You can use all of the available parameters here to build your `SendGridEmail`.
4049

41-
You can use all of the available parameters here to build your `SendGridEmail`
4250
Usage in a route closure would be as followed:
4351

44-
~~~~swift
52+
```swift
4553
import SendGrid
4654

4755
let email = SendGridEmail()
56+
try await req.sendgrid.client.send(email)
57+
```
4858

49-
try await req.application.sendgrid.client.send(email)
50-
~~~~
59+
### Error handling
5160

52-
## Error handling
61+
If the request to the API failed for any reason a `SendGridError` is thrown, which has an `errors` property that contains an array of errors returned by the API.
5362

54-
If the request to the API failed for any reason a `SendGridError` is thrown and has an `errors` property that contains an array of errors returned by the API:
63+
Simply ensure you catch errors thrown like any other throwing function.
5564

56-
~~~~swift
65+
```swift
5766
do {
58-
try await req.application.sendgrid.client.send(email)
67+
try await req.sendgrid.client.send(email)
5968
} catch let error as SendGridError {
60-
req.logger.error("\(error)")
69+
req.logger.error("\(error.errors)")
6170
}
62-
~~~~
71+
```

Sources/SendGrid/Application+SendGrid.swift

+49-26
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,65 @@
1-
import Vapor
1+
import NIOConcurrencyHelpers
22
import SendGridKit
3+
import Vapor
34

45
extension Application {
5-
public struct Sendgrid {
6-
private final class Storage {
7-
let apiKey: String
8-
9-
init(apiKey: String) {
10-
self.apiKey = apiKey
6+
public var sendgrid: SendGrid {
7+
.init(application: self)
8+
}
9+
10+
public struct SendGrid: Sendable {
11+
private final class Storage: Sendable {
12+
private struct SendableBox: Sendable {
13+
var client: SendGridClient
1114
}
12-
}
1315

14-
private struct Key: StorageKey {
15-
typealias Value = Storage
16-
}
16+
private let sendableBox: NIOLockedValueBox<SendableBox>
1717

18-
private var storage: Storage {
19-
if self.application.storage[Key.self] == nil {
20-
self.initialize()
18+
var client: SendGridClient {
19+
get {
20+
self.sendableBox.withLockedValue { box in
21+
box.client
22+
}
23+
}
24+
set {
25+
self.sendableBox.withLockedValue { box in
26+
box.client = newValue
27+
}
28+
}
2129
}
22-
return self.application.storage[Key.self]!
23-
}
24-
25-
public func initialize() {
26-
guard let apiKey = Environment.process.SENDGRID_API_KEY else {
27-
fatalError("No sendgrid API key provided")
30+
31+
init(httpClient: HTTPClient, apiKey: String) {
32+
let box = SendableBox(client: .init(httpClient: httpClient, apiKey: apiKey))
33+
self.sendableBox = .init(box)
2834
}
29-
30-
self.application.storage[Key.self] = .init(apiKey: apiKey)
35+
}
36+
37+
private struct Key: StorageKey {
38+
typealias Value = Storage
3139
}
3240

3341
fileprivate let application: Application
3442

43+
public init(application: Application) {
44+
self.application = application
45+
}
46+
3547
public var client: SendGridClient {
36-
.init(httpClient: self.application.http.client.shared, apiKey: self.storage.apiKey)
48+
get { self.storage.client }
49+
set { self.storage.client = newValue }
3750
}
38-
}
3951

40-
public var sendgrid: Sendgrid { .init(application: self) }
52+
private var storage: Storage {
53+
if let existing = self.application.storage[Key.self] {
54+
return existing
55+
} else {
56+
guard let apiKey = Environment.process.SENDGRID_API_KEY else {
57+
fatalError("No SendGrid API key provided")
58+
}
59+
let new = Storage(httpClient: self.application.http.client.shared, apiKey: apiKey)
60+
self.application.storage[Key.self] = new
61+
return new
62+
}
63+
}
64+
}
4165
}
42-
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import SendGridKit
2+
import Vapor
3+
4+
extension Request {
5+
public var sendgrid: Application.SendGrid {
6+
.init(application: self.application)
7+
}
8+
}
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ``SendGrid``
2+
3+
📧 SendGrid library for the Vapor web framework, based on SendGridKit.
4+
5+
## Overview
6+
7+
Send simple emails, or leverage the full capabilities of SendGrid's V3 API.
8+
9+
> Warning: Make sure that the `SENDGRID_API_KEY` variable is set in your environment.
10+
This can be set in the Xcode scheme, or specified in your `docker-compose.yml`, or even provided as part of a `swift run` command.
11+
A missing API key will result in a fatal error.
12+
13+
### Using the API
14+
15+
You can use all of the available parameters here to build your `SendGridEmail`.
16+
17+
Usage in a route closure would be as followed:
18+
19+
```swift
20+
import SendGrid
21+
22+
let email = SendGridEmail()
23+
try await req.sendgrid.client.send(email)
24+
```
25+
26+
### Error handling
27+
28+
If the request to the API failed for any reason a `SendGridError` is thrown, which has an `errors` property that contains an array of errors returned by the API.
29+
30+
Simply ensure you catch errors thrown like any other throwing function.
31+
32+
```swift
33+
do {
34+
try await req.sendgrid.client.send(email)
35+
} catch let error as SendGridError {
36+
req.logger.error("\(error.errors)")
37+
}
38+
```

Tests/LinuxMain.swift

-16
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Vapor
2+
3+
let isLoggingConfigured: Bool = {
4+
LoggingSystem.bootstrap { label in
5+
var handler = StreamLogHandler.standardOutput(label: label)
6+
handler.logLevel = .debug
7+
return handler
8+
}
9+
return true
10+
}()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Testing
2+
import Vapor
3+
4+
func withApp(_ body: (Application) async throws -> Void) async throws {
5+
let app = try await Application.make(.testing)
6+
try #require(isLoggingConfigured)
7+
try await body(app)
8+
try await app.asyncShutdown()
9+
}

0 commit comments

Comments
 (0)