Skip to content

Commit

Permalink
Simplify the delegates (#15)
Browse files Browse the repository at this point in the history
- 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`
  • Loading branch information
fpseverino authored Oct 31, 2024
1 parent 349b450 commit 53e9146
Show file tree
Hide file tree
Showing 33 changed files with 698 additions and 1,181 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ jobs:
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
with:
with_linting: true
test_filter: --no-parallel
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ let package = Package(
.library(name: "Orders", targets: ["Orders"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.106.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.106.1"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"),
.package(url: "https://github.com/vapor/apns.git", from: "4.2.0"),
.package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.3"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.6.1"),
// used in tests
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.8.0"),
],
Expand Down
12 changes: 6 additions & 6 deletions Sources/Orders/Models/Concrete Models/Order.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ final public class Order: OrderModel, @unchecked Sendable {
public var updatedAt: Date?

/// An identifier for the order type associated with the order.
@Field(key: Order.FieldKeys.orderTypeIdentifier)
public var orderTypeIdentifier: String
@Field(key: Order.FieldKeys.typeIdentifier)
public var typeIdentifier: String

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

public required init() {}

public required init(orderTypeIdentifier: String, authenticationToken: String) {
self.orderTypeIdentifier = orderTypeIdentifier
public required init(typeIdentifier: String, authenticationToken: String) {
self.typeIdentifier = typeIdentifier
self.authenticationToken = authenticationToken
}
}
Expand All @@ -49,7 +49,7 @@ extension Order: AsyncMigration {
.id()
.field(Order.FieldKeys.createdAt, .datetime, .required)
.field(Order.FieldKeys.updatedAt, .datetime, .required)
.field(Order.FieldKeys.orderTypeIdentifier, .string, .required)
.field(Order.FieldKeys.typeIdentifier, .string, .required)
.field(Order.FieldKeys.authenticationToken, .string, .required)
.create()
}
Expand All @@ -64,7 +64,7 @@ extension Order {
static let schemaName = "orders"
static let createdAt = FieldKey(stringLiteral: "created_at")
static let updatedAt = FieldKey(stringLiteral: "updated_at")
static let orderTypeIdentifier = FieldKey(stringLiteral: "order_type_identifier")
static let typeIdentifier = FieldKey(stringLiteral: "type_identifier")
static let authenticationToken = FieldKey(stringLiteral: "authentication_token")
}
}
16 changes: 7 additions & 9 deletions Sources/Orders/Models/Concrete Models/OrdersDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ final public class OrdersDevice: DeviceModel, @unchecked Sendable {
public var pushToken: String

/// The identifier Apple Wallet provides for the device.
@Field(key: OrdersDevice.FieldKeys.deviceLibraryIdentifier)
public var deviceLibraryIdentifier: String
@Field(key: OrdersDevice.FieldKeys.libraryIdentifier)
public var libraryIdentifier: String

public init(deviceLibraryIdentifier: String, pushToken: String) {
self.deviceLibraryIdentifier = deviceLibraryIdentifier
public init(libraryIdentifier: String, pushToken: String) {
self.libraryIdentifier = libraryIdentifier
self.pushToken = pushToken
}

Expand All @@ -37,10 +37,8 @@ extension OrdersDevice: AsyncMigration {
try await database.schema(Self.schema)
.field(.id, .int, .identifier(auto: true))
.field(OrdersDevice.FieldKeys.pushToken, .string, .required)
.field(OrdersDevice.FieldKeys.deviceLibraryIdentifier, .string, .required)
.unique(
on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.deviceLibraryIdentifier
)
.field(OrdersDevice.FieldKeys.libraryIdentifier, .string, .required)
.unique(on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.libraryIdentifier)
.create()
}

Expand All @@ -53,6 +51,6 @@ extension OrdersDevice {
enum FieldKeys {
static let schemaName = "orders_devices"
static let pushToken = FieldKey(stringLiteral: "push_token")
static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier")
static let libraryIdentifier = FieldKey(stringLiteral: "library_identifier")
}
}
12 changes: 6 additions & 6 deletions Sources/Orders/Models/OrderModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
/// Uses a UUID so people can't easily guess order IDs.
public protocol OrderModel: Model where IDValue == UUID {
/// An identifier for the order type associated with the order.
var orderTypeIdentifier: String { get set }
var typeIdentifier: String { get set }

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

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

return orderTypeIdentifier
return typeIdentifier
}

var _$updatedAt: Timestamp<DefaultTimestampFormat> {
Expand Down
8 changes: 3 additions & 5 deletions Sources/Orders/Models/OrdersRegistrationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,13 @@ extension OrdersRegistrationModel {
return order
}

static func `for`(
deviceLibraryIdentifier: String, orderTypeIdentifier: String, on db: any Database
) -> QueryBuilder<Self> {
static func `for`(deviceLibraryIdentifier: String, typeIdentifier: String, on db: any Database) -> QueryBuilder<Self> {
Self.query(on: db)
.join(parent: \._$order)
.join(parent: \._$device)
.with(\._$order)
.with(\._$device)
.filter(OrderType.self, \._$orderTypeIdentifier == orderTypeIdentifier)
.filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier)
.filter(OrderType.self, \._$typeIdentifier == typeIdentifier)
.filter(DeviceType.self, \._$libraryIdentifier == deviceLibraryIdentifier)
}
}
41 changes: 23 additions & 18 deletions Sources/Orders/Orders.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,6 @@ struct OrderJSONData: OrderJSON.Properties {
### Implement the Delegate

Create a delegate class that implements ``OrdersDelegate``.
In the ``OrdersDelegate/sslSigningFilesDirectory`` you specify there must be the `WWDR.pem`, `ordercertificate.pem` and `orderkey.pem` files.
If they are named like that you're good to go, otherwise you have to specify the custom name.

> 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.
There are other fields available which have reasonable default values. See ``OrdersDelegate``'s documentation.

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.
In the ``OrdersDelegate/encode(order:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``OrderJSON``.
Expand All @@ -127,12 +121,8 @@ import Fluent
import Orders

final class OrderDelegate: OrdersDelegate {
let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Orders/", isDirectory: true)

let pemPrivateKeyPassword: String? = Environment.get("ORDERS_PEM_PRIVATE_KEY_PASSWORD")!

func encode<O: OrderModel>(order: O, db: Database, encoder: JSONEncoder) async throws -> Data {
// The specific OrderData class you use here may vary based on the `order.orderTypeIdentifier`
// The specific OrderData class you use here may vary based on the `order.typeIdentifier`
// if you have multiple different types of orders, and thus multiple types of order data.
guard let orderData = try await OrderData.query(on: db)
.filter(\.$order.$id == order.requireID())
Expand All @@ -146,19 +136,21 @@ final class OrderDelegate: OrdersDelegate {
return data
}

func template<O: OrderModel>(for order: O, db: Database) async throws -> URL {
func template<O: OrderModel>(for order: O, db: Database) async throws -> String {
// The location might vary depending on the type of order.
URL(fileURLWithPath: "Templates/Orders/", isDirectory: true)
"Templates/Orders/"
}
}
```

> 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.
### Initialize the Service

Next, initialize the ``OrdersService`` inside the `configure.swift` file.
This will implement all of the routes that Apple Wallet expects to exist on your server.
In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files.
If they are named like that you're good to go, otherwise you have to specify the custom name.

> 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.
```swift
import Fluent
Expand All @@ -169,7 +161,11 @@ let orderDelegate = OrderDelegate()

public func configure(_ app: Application) async throws {
...
let ordersService = try OrdersService(app: app, delegate: orderDelegate)
let ordersService = try OrdersService(
app: app,
delegate: orderDelegate,
signingFilesDirectory: "Certificates/Orders/"
)
}
```

Expand Down Expand Up @@ -199,7 +195,16 @@ let orderDelegate = OrderDelegate()

public func configure(_ app: Application) async throws {
...
let ordersService = try OrdersServiceCustom<MyOrderType, MyDeviceType, MyOrdersRegistrationType, MyErrorLogType>(app: app, delegate: orderDelegate)
let ordersService = try OrdersServiceCustom<
MyOrderType,
MyDeviceType,
MyOrdersRegistrationType,
MyErrorLogType
>(
app: app,
delegate: orderDelegate,
signingFilesDirectory: "Certificates/Orders/"
)
}
```

Expand Down Expand Up @@ -234,7 +239,7 @@ struct OrderDataMiddleware: AsyncModelMiddleware {
// Create the `Order` and add it to the `OrderData` automatically at creation
func create(model: OrderData, on db: Database, next: AnyAsyncModelResponder) async throws {
let order = Order(
orderTypeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!,
typeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!,
authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString())
try await order.save(on: db)
model.$order.id = try order.requireID()
Expand Down
64 changes: 5 additions & 59 deletions Sources/Orders/OrdersDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import Foundation

/// The delegate which is responsible for generating the order files.
public protocol OrdersDelegate: AnyObject, Sendable {
/// Should return a `URL` which points to the template data for the order.
/// Should return a URL path which points to the template data for the order.
///
/// 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:
/// The path should point to a directory containing all the images and localizations for the generated `.order` archive
/// but should *not* contain any of these items:
/// - `manifest.json`
/// - `order.json`
/// - `signature`
Expand All @@ -21,10 +22,8 @@ public protocol OrdersDelegate: AnyObject, Sendable {
/// - order: The order data from the SQL server.
/// - db: The SQL database to query against.
///
/// - Returns: A `URL` which points to the template data for the order.
///
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor.
func template<O: OrderModel>(for order: O, db: any Database) async throws -> URL
/// - Returns: A URL path which points to the template data for the order.
func template<O: OrderModel>(for order: O, db: any Database) async throws -> String

/// Generates the SSL `signature` file.
///
Expand All @@ -51,62 +50,9 @@ public protocol OrdersDelegate: AnyObject, Sendable {
func encode<O: OrderModel>(
order: O, db: any Database, encoder: JSONEncoder
) async throws -> Data

/// Should return a `URL` which points to the template data for the order.
///
/// The URL should point to a directory containing the files specified by these keys:
/// - `wwdrCertificate`
/// - `pemCertificate`
/// - `pemPrivateKey`
///
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` initializer!
var sslSigningFilesDirectory: URL { get }

/// The location of the `openssl` command as a file URL.
///
/// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor.
var sslBinary: URL { get }

/// The name of Apple's WWDR.pem certificate as contained in `sslSigningFiles` path.
///
/// Defaults to `WWDR.pem`
var wwdrCertificate: String { get }

/// The name of the PEM Certificate for signing the order as contained in `sslSigningFiles` path.
///
/// Defaults to `ordercertificate.pem`
var pemCertificate: String { get }

/// The name of the PEM Certificate's private key for signing the order as contained in `sslSigningFiles` path.
///
/// Defaults to `orderkey.pem`
var pemPrivateKey: String { get }

/// The password to the private key file.
var pemPrivateKeyPassword: String? { get }
}

extension OrdersDelegate {
public var wwdrCertificate: String {
return "WWDR.pem"
}

public var pemCertificate: String {
return "ordercertificate.pem"
}

public var pemPrivateKey: String {
return "orderkey.pem"
}

public var pemPrivateKeyPassword: String? {
return nil
}

public var sslBinary: URL {
return URL(fileURLWithPath: "/usr/bin/openssl")
}

public func generateSignatureFile(in root: URL) -> Bool {
return false
}
Expand Down
39 changes: 30 additions & 9 deletions Sources/Orders/OrdersService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,37 @@ public final class OrdersService: Sendable {
/// - Parameters:
/// - app: The `Vapor.Application` to use in route handlers and APNs.
/// - delegate: The ``OrdersDelegate`` to use for order generation.
/// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located.
/// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`.
/// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`.
/// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`.
/// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`.
/// - sslBinary: The location of the `openssl` command as a file path.
/// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
/// - logger: The `Logger` to use.
public init(
app: Application, delegate: any OrdersDelegate,
pushRoutesMiddleware: (any Middleware)? = nil, logger: Logger? = nil
app: Application,
delegate: any OrdersDelegate,
signingFilesDirectory: String,
wwdrCertificate: String = "WWDR.pem",
pemCertificate: String = "certificate.pem",
pemPrivateKey: String = "key.pem",
pemPrivateKeyPassword: String? = nil,
sslBinary: String = "/usr/bin/openssl",
pushRoutesMiddleware: (any Middleware)? = nil,
logger: Logger? = nil
) throws {
service = try .init(
app: app, delegate: delegate, pushRoutesMiddleware: pushRoutesMiddleware, logger: logger
self.service = try .init(
app: app,
delegate: delegate,
signingFilesDirectory: signingFilesDirectory,
wwdrCertificate: wwdrCertificate,
pemCertificate: pemCertificate,
pemPrivateKey: pemPrivateKey,
pemPrivateKeyPassword: pemPrivateKeyPassword,
sslBinary: sslBinary,
pushRoutesMiddleware: pushRoutesMiddleware,
logger: logger
)
}

Expand Down Expand Up @@ -52,12 +75,10 @@ public final class OrdersService: Sendable {
///
/// - Parameters:
/// - id: The `UUID` of the order to send the notifications for.
/// - orderTypeIdentifier: The type identifier of the order.
/// - typeIdentifier: The type identifier of the order.
/// - db: The `Database` to use.
public func sendPushNotificationsForOrder(
id: UUID, of orderTypeIdentifier: String, on db: any Database
) async throws {
try await service.sendPushNotificationsForOrder(id: id, of: orderTypeIdentifier, on: db)
public func sendPushNotificationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws {
try await service.sendPushNotificationsForOrder(id: id, of: typeIdentifier, on: db)
}

/// Sends push notifications for a given order.
Expand Down
Loading

0 comments on commit 53e9146

Please sign in to comment.