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

Update to SendGridKit v3 #50

Merged
merged 12 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions .env.testing
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SENDGRID_API_KEY=SG.1234567890
10 changes: 7 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
name: test
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
push: { branches: [ main ] }

jobs:
unit-tests:
uses: vapor/ci/.github/workflows/run-unit-tests.yml@reusable-workflows
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
with:
with_coverage: true
with_tsan: true
with_linting: true
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
5 changes: 5 additions & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: 1
builder:
configs:
- documentation_targets: [SendGrid]
swift_version: 6.0
33 changes: 25 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
// swift-tools-version:5.2
// swift-tools-version:6.0
import PackageDescription

let package = Package(
name: "sendgrid",
platforms: [
.macOS(.v10_15)
.macOS(.v14)
],
products: [
.library(name: "SendGrid", targets: ["SendGrid"])
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "2.0.2"),
.package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "3.0.0-rc.1"),
],
targets: [
.target(name: "SendGrid", dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "SendGridKit", package: "sendgrid-kit"),
]),
.testTarget(name: "SendGridTests", dependencies: ["SendGrid"])
.target(
name: "SendGrid",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "SendGridKit", package: "sendgrid-kit"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "SendGridTests",
dependencies: [
.target(name: "SendGrid"),
.product(name: "XCTVapor", package: "vapor"),
],
swiftSettings: swiftSettings
),
]
)

var swiftSettings: [SwiftSetting] {
[
.enableUpcomingFeature("ExistentialAny")
]
}
83 changes: 46 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,71 @@
# SendGrid Provider for Vapor

<p align="center">
<a href="https://github.com/vapor-community/sendgrid/actions">
<img src="https://github.com/vapor-community/sendgrid/workflows/test/badge.svg" alt="Continuous Integration">
<div align="center">
<img src="https://avatars.githubusercontent.com/u/26165732?s=200&v=4" width="100" height="100" alt="avatar" />
<h1>SendGrid</h1>
<a href="https://swiftpackageindex.com/vapor-community/sendgrid/documentation">
<img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation">
</a>
<a href="https://discord.gg/vapor"><img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat"></a>
<a href="LICENSE"><img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License"></a>
<a href="https://github.com/vapor-community/sendgrid/actions/workflows/test.yml">
<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">
</a>
<a href="https://codecov.io/github/vapor-community/sendgrid">
<img src="https://img.shields.io/codecov/c/github/vapor-community/sendgrid?style=plastic&logo=codecov&label=codecov">
</a>
<a href="https://swift.org">
<img src="http://img.shields.io/badge/swift-5.2-brightgreen.svg" alt="Swift 5.2">
<img src="https://design.vapor.codes/images/swift60up.svg" alt="Swift 6.0+">
</a>
</p>
</div>
<br>

Adds a mail backend for SendGrid to the Vapor web framework. Send simple emails,
or leverage the full capabilities of SendGrid's V3 API.
📧 SendGrid library for the Vapor web framework, based on [SendGridKit](https://github.com/vapor-community/sendgrid-kit).

## Setup
Add the dependency to Package.swift:
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).

~~~~swift
### Getting Started

Use the SPM string to easily include the dependendency in your `Package.swift` file

```swift
.package(url: "https://github.com/vapor-community/sendgrid.git", from: "4.0.0")
~~~~
```

and add it to your target's dependencies:

Make sure `SENDGRID_API_KEY` is set in your environment. 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.
```swift
.product(name: "SendGrid", package: "sendgrid")
```

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

~~~~swift
app.sendgrid.initialize()
~~~~
> [!WARNING]
> Make sure that the `SENDGRID_API_KEY` variable is set in your environment.
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.
A missing API key will result in a fatal error.

Now you can access the client at any time:
~~~~swift
app.sendgrid.client
~~~~
### Using the API

## Using the API
You can use all of the available parameters here to build your `SendGridEmail`.

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

~~~~swift
```swift
import SendGrid

let email = SendGridEmail(…)
try await req.sendgrid.client.send(email)
```

try await req.application.sendgrid.client.send(email)
~~~~
### Error handling

## Error handling
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.

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:
Simply ensure you catch errors thrown like any other throwing function.

~~~~swift
```swift
do {
try await req.application.sendgrid.client.send(email)
try await req.sendgrid.client.send(email)
} catch let error as SendGridError {
req.logger.error("\(error)")
req.logger.error("\(error.errors)")
}
~~~~
```
75 changes: 49 additions & 26 deletions Sources/SendGrid/Application+SendGrid.swift
Original file line number Diff line number Diff line change
@@ -1,42 +1,65 @@
import Vapor
import NIOConcurrencyHelpers
import SendGridKit
import Vapor

extension Application {
public struct Sendgrid {
private final class Storage {
let apiKey: String

init(apiKey: String) {
self.apiKey = apiKey
public var sendgrid: SendGrid {
.init(application: self)
}

public struct SendGrid: Sendable {
private final class Storage: Sendable {
private struct SendableBox: Sendable {
var client: SendGridClient
}
}

private struct Key: StorageKey {
typealias Value = Storage
}
private let sendableBox: NIOLockedValueBox<SendableBox>

private var storage: Storage {
if self.application.storage[Key.self] == nil {
self.initialize()
var client: SendGridClient {
get {
self.sendableBox.withLockedValue { box in
box.client
}
}
set {
self.sendableBox.withLockedValue { box in
box.client = newValue
}
}
}
return self.application.storage[Key.self]!
}

public func initialize() {
guard let apiKey = Environment.process.SENDGRID_API_KEY else {
fatalError("No sendgrid API key provided")

init(httpClient: HTTPClient, apiKey: String) {
let box = SendableBox(client: .init(httpClient: httpClient, apiKey: apiKey))
self.sendableBox = .init(box)
}

self.application.storage[Key.self] = .init(apiKey: apiKey)
}

private struct Key: StorageKey {
typealias Value = Storage
}

fileprivate let application: Application

public init(application: Application) {
self.application = application
}

public var client: SendGridClient {
.init(httpClient: self.application.http.client.shared, apiKey: self.storage.apiKey)
get { self.storage.client }
set { self.storage.client = newValue }
}
}

public var sendgrid: Sendgrid { .init(application: self) }
private var storage: Storage {
if let existing = self.application.storage[Key.self] {
return existing
} else {
guard let apiKey = Environment.process.SENDGRID_API_KEY else {
fatalError("No SendGrid API key provided")
}
let new = Storage(httpClient: self.application.http.client.shared, apiKey: apiKey)
self.application.storage[Key.self] = new
return new
}
}
}
}

8 changes: 8 additions & 0 deletions Sources/SendGrid/Request+SendGrid.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import SendGridKit
import Vapor

extension Request {
public var sendgrid: Application.SendGrid {
.init(application: self.application)
}
}
38 changes: 38 additions & 0 deletions Sources/SendGrid/SendGrid.docc/SendGrid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ``SendGrid``

📧 SendGrid library for the Vapor web framework, based on SendGridKit.

## Overview

Send simple emails, or leverage the full capabilities of SendGrid's V3 API.

> Warning: Make sure that the `SENDGRID_API_KEY` variable is set in your environment.
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.
A missing API key will result in a fatal error.

### Using the API

You can use all of the available parameters here to build your `SendGridEmail`.

Usage in a route closure would be as followed:

```swift
import SendGrid

let email = SendGridEmail(…)
try await req.sendgrid.client.send(email)
```

### Error handling

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.

Simply ensure you catch errors thrown like any other throwing function.

```swift
do {
try await req.sendgrid.client.send(email)
} catch let error as SendGridError {
req.logger.error("\(error.errors)")
}
```
16 changes: 0 additions & 16 deletions Tests/LinuxMain.swift

This file was deleted.

10 changes: 10 additions & 0 deletions Tests/SendGridTests/Helpers/isLoggingConfigured.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Vapor

let isLoggingConfigured: Bool = {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
handler.logLevel = .debug
return handler
}
return true
}()
9 changes: 9 additions & 0 deletions Tests/SendGridTests/Helpers/withApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Testing
import Vapor

func withApp(_ body: (Application) async throws -> Void) async throws {
let app = try await Application.make(.testing)
try #require(isLoggingConfigured == true)
try await body(app)
try await app.asyncShutdown()
}
Loading
Loading