Skip to content

Commit 53e9146

Browse files
authored
Simplify the delegates (#15)
- Make `PersonalizationJSON` a struct instead of a protocol. - It was missing only a field that we can make optional, now users directly create the struct and we handle the JSON encoding - Move all the signing information from the delegates to the actual services' classes - Now users aren't required to use specific `URL`'s initializers, they just pass the path as a String and we create the URL - The goal is to completely remove the delegates in the future and move the remaining functions to the `PassDataModel` and `OrderDataModel` protocols - Make the `template` delegate method return a String instead of a URL - The user provides the path to the directory and we initialize the `URL`
1 parent 349b450 commit 53e9146

33 files changed

+698
-1181
lines changed

.github/workflows/test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ jobs:
1111
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
1212
with:
1313
with_linting: true
14+
test_filter: --no-parallel
1415
secrets:
1516
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Package.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ let package = Package(
1111
.library(name: "Orders", targets: ["Orders"]),
1212
],
1313
dependencies: [
14-
.package(url: "https://github.com/vapor/vapor.git", from: "4.106.0"),
14+
.package(url: "https://github.com/vapor/vapor.git", from: "4.106.1"),
1515
.package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"),
1616
.package(url: "https://github.com/vapor/apns.git", from: "4.2.0"),
1717
.package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.3"),
18-
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.5.0"),
18+
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.6.1"),
1919
// used in tests
2020
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.8.0"),
2121
],

Sources/Orders/Models/Concrete Models/Order.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@ final public class Order: OrderModel, @unchecked Sendable {
2828
public var updatedAt: Date?
2929

3030
/// An identifier for the order type associated with the order.
31-
@Field(key: Order.FieldKeys.orderTypeIdentifier)
32-
public var orderTypeIdentifier: String
31+
@Field(key: Order.FieldKeys.typeIdentifier)
32+
public var typeIdentifier: String
3333

3434
/// The authentication token supplied to your web service.
3535
@Field(key: Order.FieldKeys.authenticationToken)
3636
public var authenticationToken: String
3737

3838
public required init() {}
3939

40-
public required init(orderTypeIdentifier: String, authenticationToken: String) {
41-
self.orderTypeIdentifier = orderTypeIdentifier
40+
public required init(typeIdentifier: String, authenticationToken: String) {
41+
self.typeIdentifier = typeIdentifier
4242
self.authenticationToken = authenticationToken
4343
}
4444
}
@@ -49,7 +49,7 @@ extension Order: AsyncMigration {
4949
.id()
5050
.field(Order.FieldKeys.createdAt, .datetime, .required)
5151
.field(Order.FieldKeys.updatedAt, .datetime, .required)
52-
.field(Order.FieldKeys.orderTypeIdentifier, .string, .required)
52+
.field(Order.FieldKeys.typeIdentifier, .string, .required)
5353
.field(Order.FieldKeys.authenticationToken, .string, .required)
5454
.create()
5555
}
@@ -64,7 +64,7 @@ extension Order {
6464
static let schemaName = "orders"
6565
static let createdAt = FieldKey(stringLiteral: "created_at")
6666
static let updatedAt = FieldKey(stringLiteral: "updated_at")
67-
static let orderTypeIdentifier = FieldKey(stringLiteral: "order_type_identifier")
67+
static let typeIdentifier = FieldKey(stringLiteral: "type_identifier")
6868
static let authenticationToken = FieldKey(stringLiteral: "authentication_token")
6969
}
7070
}

Sources/Orders/Models/Concrete Models/OrdersDevice.swift

+7-9
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ final public class OrdersDevice: DeviceModel, @unchecked Sendable {
2121
public var pushToken: String
2222

2323
/// The identifier Apple Wallet provides for the device.
24-
@Field(key: OrdersDevice.FieldKeys.deviceLibraryIdentifier)
25-
public var deviceLibraryIdentifier: String
24+
@Field(key: OrdersDevice.FieldKeys.libraryIdentifier)
25+
public var libraryIdentifier: String
2626

27-
public init(deviceLibraryIdentifier: String, pushToken: String) {
28-
self.deviceLibraryIdentifier = deviceLibraryIdentifier
27+
public init(libraryIdentifier: String, pushToken: String) {
28+
self.libraryIdentifier = libraryIdentifier
2929
self.pushToken = pushToken
3030
}
3131

@@ -37,10 +37,8 @@ extension OrdersDevice: AsyncMigration {
3737
try await database.schema(Self.schema)
3838
.field(.id, .int, .identifier(auto: true))
3939
.field(OrdersDevice.FieldKeys.pushToken, .string, .required)
40-
.field(OrdersDevice.FieldKeys.deviceLibraryIdentifier, .string, .required)
41-
.unique(
42-
on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.deviceLibraryIdentifier
43-
)
40+
.field(OrdersDevice.FieldKeys.libraryIdentifier, .string, .required)
41+
.unique(on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.libraryIdentifier)
4442
.create()
4543
}
4644

@@ -53,6 +51,6 @@ extension OrdersDevice {
5351
enum FieldKeys {
5452
static let schemaName = "orders_devices"
5553
static let pushToken = FieldKey(stringLiteral: "push_token")
56-
static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier")
54+
static let libraryIdentifier = FieldKey(stringLiteral: "library_identifier")
5755
}
5856
}

Sources/Orders/Models/OrderModel.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Foundation
1313
/// Uses a UUID so people can't easily guess order IDs.
1414
public protocol OrderModel: Model where IDValue == UUID {
1515
/// An identifier for the order type associated with the order.
16-
var orderTypeIdentifier: String { get set }
16+
var typeIdentifier: String { get set }
1717

1818
/// The date and time when the customer created the order.
1919
var createdAt: Date? { get set }
@@ -36,14 +36,14 @@ extension OrderModel {
3636
return id
3737
}
3838

39-
var _$orderTypeIdentifier: Field<String> {
40-
guard let mirror = Mirror(reflecting: self).descendant("_orderTypeIdentifier"),
41-
let orderTypeIdentifier = mirror as? Field<String>
39+
var _$typeIdentifier: Field<String> {
40+
guard let mirror = Mirror(reflecting: self).descendant("_typeIdentifier"),
41+
let typeIdentifier = mirror as? Field<String>
4242
else {
43-
fatalError("orderTypeIdentifier property must be declared using @Field")
43+
fatalError("typeIdentifier property must be declared using @Field")
4444
}
4545

46-
return orderTypeIdentifier
46+
return typeIdentifier
4747
}
4848

4949
var _$updatedAt: Timestamp<DefaultTimestampFormat> {

Sources/Orders/Models/OrdersRegistrationModel.swift

+3-5
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,13 @@ extension OrdersRegistrationModel {
4141
return order
4242
}
4343

44-
static func `for`(
45-
deviceLibraryIdentifier: String, orderTypeIdentifier: String, on db: any Database
46-
) -> QueryBuilder<Self> {
44+
static func `for`(deviceLibraryIdentifier: String, typeIdentifier: String, on db: any Database) -> QueryBuilder<Self> {
4745
Self.query(on: db)
4846
.join(parent: \._$order)
4947
.join(parent: \._$device)
5048
.with(\._$order)
5149
.with(\._$device)
52-
.filter(OrderType.self, \._$orderTypeIdentifier == orderTypeIdentifier)
53-
.filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier)
50+
.filter(OrderType.self, \._$typeIdentifier == typeIdentifier)
51+
.filter(DeviceType.self, \._$libraryIdentifier == deviceLibraryIdentifier)
5452
}
5553
}

Sources/Orders/Orders.docc/GettingStarted.md

+23-18
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,6 @@ struct OrderJSONData: OrderJSON.Properties {
111111
### Implement the Delegate
112112

113113
Create a delegate class that implements ``OrdersDelegate``.
114-
In the ``OrdersDelegate/sslSigningFilesDirectory`` you specify there must be the `WWDR.pem`, `ordercertificate.pem` and `orderkey.pem` files.
115-
If they are named like that you're good to go, otherwise you have to specify the custom name.
116-
117-
> Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders.
118-
119-
There are other fields available which have reasonable default values. See ``OrdersDelegate``'s documentation.
120114

121115
Because the files for your order's template and the method of encoding might vary by order type, you'll be provided the ``Order`` for those methods.
122116
In the ``OrdersDelegate/encode(order:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``OrderJSON``.
@@ -127,12 +121,8 @@ import Fluent
127121
import Orders
128122

129123
final class OrderDelegate: OrdersDelegate {
130-
let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Orders/", isDirectory: true)
131-
132-
let pemPrivateKeyPassword: String? = Environment.get("ORDERS_PEM_PRIVATE_KEY_PASSWORD")!
133-
134124
func encode<O: OrderModel>(order: O, db: Database, encoder: JSONEncoder) async throws -> Data {
135-
// The specific OrderData class you use here may vary based on the `order.orderTypeIdentifier`
125+
// The specific OrderData class you use here may vary based on the `order.typeIdentifier`
136126
// if you have multiple different types of orders, and thus multiple types of order data.
137127
guard let orderData = try await OrderData.query(on: db)
138128
.filter(\.$order.$id == order.requireID())
@@ -146,19 +136,21 @@ final class OrderDelegate: OrdersDelegate {
146136
return data
147137
}
148138

149-
func template<O: OrderModel>(for order: O, db: Database) async throws -> URL {
139+
func template<O: OrderModel>(for order: O, db: Database) async throws -> String {
150140
// The location might vary depending on the type of order.
151-
URL(fileURLWithPath: "Templates/Orders/", isDirectory: true)
141+
"Templates/Orders/"
152142
}
153143
}
154144
```
155145

156-
> Important: If you have an encrypted PEM private key, you **must** explicitly declare ``OrdersDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
157-
158146
### Initialize the Service
159147

160148
Next, initialize the ``OrdersService`` inside the `configure.swift` file.
161149
This will implement all of the routes that Apple Wallet expects to exist on your server.
150+
In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files.
151+
If they are named like that you're good to go, otherwise you have to specify the custom name.
152+
153+
> Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders.
162154
163155
```swift
164156
import Fluent
@@ -169,7 +161,11 @@ let orderDelegate = OrderDelegate()
169161

170162
public func configure(_ app: Application) async throws {
171163
...
172-
let ordersService = try OrdersService(app: app, delegate: orderDelegate)
164+
let ordersService = try OrdersService(
165+
app: app,
166+
delegate: orderDelegate,
167+
signingFilesDirectory: "Certificates/Orders/"
168+
)
173169
}
174170
```
175171

@@ -199,7 +195,16 @@ let orderDelegate = OrderDelegate()
199195

200196
public func configure(_ app: Application) async throws {
201197
...
202-
let ordersService = try OrdersServiceCustom<MyOrderType, MyDeviceType, MyOrdersRegistrationType, MyErrorLogType>(app: app, delegate: orderDelegate)
198+
let ordersService = try OrdersServiceCustom<
199+
MyOrderType,
200+
MyDeviceType,
201+
MyOrdersRegistrationType,
202+
MyErrorLogType
203+
>(
204+
app: app,
205+
delegate: orderDelegate,
206+
signingFilesDirectory: "Certificates/Orders/"
207+
)
203208
}
204209
```
205210

@@ -234,7 +239,7 @@ struct OrderDataMiddleware: AsyncModelMiddleware {
234239
// Create the `Order` and add it to the `OrderData` automatically at creation
235240
func create(model: OrderData, on db: Database, next: AnyAsyncModelResponder) async throws {
236241
let order = Order(
237-
orderTypeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!,
242+
typeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!,
238243
authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString())
239244
try await order.save(on: db)
240245
model.$order.id = try order.requireID()

Sources/Orders/OrdersDelegate.swift

+5-59
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import Foundation
1010

1111
/// The delegate which is responsible for generating the order files.
1212
public protocol OrdersDelegate: AnyObject, Sendable {
13-
/// Should return a `URL` which points to the template data for the order.
13+
/// Should return a URL path which points to the template data for the order.
1414
///
15-
/// The URL should point to a directory containing all the images and localizations for the generated `.order` archive but should *not* contain any of these items:
15+
/// The path should point to a directory containing all the images and localizations for the generated `.order` archive
16+
/// but should *not* contain any of these items:
1617
/// - `manifest.json`
1718
/// - `order.json`
1819
/// - `signature`
@@ -21,10 +22,8 @@ public protocol OrdersDelegate: AnyObject, Sendable {
2122
/// - order: The order data from the SQL server.
2223
/// - db: The SQL database to query against.
2324
///
24-
/// - Returns: A `URL` which points to the template data for the order.
25-
///
26-
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor.
27-
func template<O: OrderModel>(for order: O, db: any Database) async throws -> URL
25+
/// - Returns: A URL path which points to the template data for the order.
26+
func template<O: OrderModel>(for order: O, db: any Database) async throws -> String
2827

2928
/// Generates the SSL `signature` file.
3029
///
@@ -51,62 +50,9 @@ public protocol OrdersDelegate: AnyObject, Sendable {
5150
func encode<O: OrderModel>(
5251
order: O, db: any Database, encoder: JSONEncoder
5352
) async throws -> Data
54-
55-
/// Should return a `URL` which points to the template data for the order.
56-
///
57-
/// The URL should point to a directory containing the files specified by these keys:
58-
/// - `wwdrCertificate`
59-
/// - `pemCertificate`
60-
/// - `pemPrivateKey`
61-
///
62-
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` initializer!
63-
var sslSigningFilesDirectory: URL { get }
64-
65-
/// The location of the `openssl` command as a file URL.
66-
///
67-
/// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor.
68-
var sslBinary: URL { get }
69-
70-
/// The name of Apple's WWDR.pem certificate as contained in `sslSigningFiles` path.
71-
///
72-
/// Defaults to `WWDR.pem`
73-
var wwdrCertificate: String { get }
74-
75-
/// The name of the PEM Certificate for signing the order as contained in `sslSigningFiles` path.
76-
///
77-
/// Defaults to `ordercertificate.pem`
78-
var pemCertificate: String { get }
79-
80-
/// The name of the PEM Certificate's private key for signing the order as contained in `sslSigningFiles` path.
81-
///
82-
/// Defaults to `orderkey.pem`
83-
var pemPrivateKey: String { get }
84-
85-
/// The password to the private key file.
86-
var pemPrivateKeyPassword: String? { get }
8753
}
8854

8955
extension OrdersDelegate {
90-
public var wwdrCertificate: String {
91-
return "WWDR.pem"
92-
}
93-
94-
public var pemCertificate: String {
95-
return "ordercertificate.pem"
96-
}
97-
98-
public var pemPrivateKey: String {
99-
return "orderkey.pem"
100-
}
101-
102-
public var pemPrivateKeyPassword: String? {
103-
return nil
104-
}
105-
106-
public var sslBinary: URL {
107-
return URL(fileURLWithPath: "/usr/bin/openssl")
108-
}
109-
11056
public func generateSignatureFile(in root: URL) -> Bool {
11157
return false
11258
}

Sources/Orders/OrdersService.swift

+30-9
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,37 @@ public final class OrdersService: Sendable {
1717
/// - Parameters:
1818
/// - app: The `Vapor.Application` to use in route handlers and APNs.
1919
/// - delegate: The ``OrdersDelegate`` to use for order generation.
20+
/// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located.
21+
/// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`.
22+
/// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`.
23+
/// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`.
24+
/// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`.
25+
/// - sslBinary: The location of the `openssl` command as a file path.
2026
/// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
2127
/// - logger: The `Logger` to use.
2228
public init(
23-
app: Application, delegate: any OrdersDelegate,
24-
pushRoutesMiddleware: (any Middleware)? = nil, logger: Logger? = nil
29+
app: Application,
30+
delegate: any OrdersDelegate,
31+
signingFilesDirectory: String,
32+
wwdrCertificate: String = "WWDR.pem",
33+
pemCertificate: String = "certificate.pem",
34+
pemPrivateKey: String = "key.pem",
35+
pemPrivateKeyPassword: String? = nil,
36+
sslBinary: String = "/usr/bin/openssl",
37+
pushRoutesMiddleware: (any Middleware)? = nil,
38+
logger: Logger? = nil
2539
) throws {
26-
service = try .init(
27-
app: app, delegate: delegate, pushRoutesMiddleware: pushRoutesMiddleware, logger: logger
40+
self.service = try .init(
41+
app: app,
42+
delegate: delegate,
43+
signingFilesDirectory: signingFilesDirectory,
44+
wwdrCertificate: wwdrCertificate,
45+
pemCertificate: pemCertificate,
46+
pemPrivateKey: pemPrivateKey,
47+
pemPrivateKeyPassword: pemPrivateKeyPassword,
48+
sslBinary: sslBinary,
49+
pushRoutesMiddleware: pushRoutesMiddleware,
50+
logger: logger
2851
)
2952
}
3053

@@ -52,12 +75,10 @@ public final class OrdersService: Sendable {
5275
///
5376
/// - Parameters:
5477
/// - id: The `UUID` of the order to send the notifications for.
55-
/// - orderTypeIdentifier: The type identifier of the order.
78+
/// - typeIdentifier: The type identifier of the order.
5679
/// - db: The `Database` to use.
57-
public func sendPushNotificationsForOrder(
58-
id: UUID, of orderTypeIdentifier: String, on db: any Database
59-
) async throws {
60-
try await service.sendPushNotificationsForOrder(id: id, of: orderTypeIdentifier, on: db)
80+
public func sendPushNotificationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws {
81+
try await service.sendPushNotificationsForOrder(id: id, of: typeIdentifier, on: db)
6182
}
6283

6384
/// Sends push notifications for a given order.

0 commit comments

Comments
 (0)