Skip to content

Instantly share code, notes, and snippets.

@Obbut
Created November 22, 2023 11:07
Show Gist options
  • Save Obbut/285c2662c4081c3a15e6ed41c23549d3 to your computer and use it in GitHub Desktop.
Save Obbut/285c2662c4081c3a15e6ed41c23549d3 to your computer and use it in GitHub Desktop.
Dependencies
import Dependencies
import SotoCore
struct AWSClientDependencyKey: DependencyKey {
typealias value = AWSClient
static var liveValue: AWSClient {
@Dependency(\.httpClient) var httpClient
return AWSClient(httpClientProvider: .shared(httpClient))
}
}
extension DependencyValues {
var awsClient: AWSClient {
get { self[AWSClientDependencyKey.self] }
set { self[AWSClientDependencyKey.self] = newValue }
}
}
import Dependencies
import Foundation
import Logging
/// Usually environment variables.
struct Config {
let log = Logger(label: "Config")
private let _resolve: (ConfigKey) -> Result<String, MissingConfigValueError>
init(resolve: @escaping (ConfigKey) -> Result<String, MissingConfigValueError>) {
self._resolve = resolve
}
subscript(key: ConfigKey, file: String = #file, line: Int = #line) -> String? {
switch resolve(
key,
reason: "Optional Config subscript",
failureLevel: .debug,
file: file,
line: line
) {
case .success(let value): return value
case .failure: return nil
}
}
/// Calls `_resolve` and logs a message at the given level if the value is missing.
/// Also enriches the error with the reason, file and line.
private func resolve(
_ key: ConfigKey,
reason: String?,
failureLevel: Logger.Level,
file: String,
line: Int
) -> Result<String, MissingConfigValueError> {
switch _resolve(key) {
case .success(let value): return .success(value)
case .failure(var error):
if error.reason == nil {
error.reason = reason
}
error.file = file
error.line = line
log.log(
level: failureLevel,
"Missing config value \(key.rawValue) in \(file):\(line)",
metadata: [
"key": .string(key.rawValue),
"reason": .string(reason ?? "nil"),
"file": .string(file),
"line": .string("\(line)"),
]
)
return .failure(error)
}
}
func require(
_ key: ConfigKey,
reason: String? = nil,
file: String = #file,
line: Int = #line
) throws -> String {
try resolve(key, reason: reason, failureLevel: .error, file: file, line: line).get()
}
func critical(
_ key: ConfigKey,
reason: String? = nil,
file: String = #file,
line: Int = #line
) -> String {
switch resolve(key, reason: reason, failureLevel: .critical, file: file, line: line) {
case .success(let value): return value
case .failure(let error): fatalError(error.description)
}
}
}
struct MissingConfigValueError: Error, CustomStringConvertible {
var key: ConfigKey
var reason: String?
var file: String = #file
var line: Int = #line
var description: String {
var message = "Missing config value \(key.rawValue) in \(file):\(line)"
if let reason = reason {
message += " \(reason)"
}
return message
}
}
import Dependencies
import Foundation
enum ConfigDependencyKey: DependencyKey {
typealias Value = Config
static let liveValue: Config = Config { key in
guard let value = ProcessInfo.processInfo.environment[key.rawValue] else {
return .failure(.init(key: key))
}
return .success(value)
}
static let testValue: Config = Config { key in
.failure(.init(key: key, reason: "Config not specified in test environment"))
}
}
extension DependencyValues {
var config: Config {
get { self[ConfigDependencyKey.self] }
set { self[ConfigDependencyKey.self] = newValue }
}
}
enum ConfigKey: String, CaseIterable {
/// Either "SES" or "MOCK".
case mailProvider = "MAIL_PROVIDER"
}
import Dependencies
/// A shortcut to access one or more failable dependency, useful in endpoints.
/// It can be used with both failable dependencies (that return a `Result`) and non-failable dependencies.
///
/// Get one dependency: `let httpClient = try dependencies(\.httpClient)`
/// Get multiple dependencies: `let (httpClient, meow) = try dependencies(\.httpClient, \.meow)`
// TODO: protocol FailableDependencyKey
func dependencies<each Value, each E>(
_ keyPath: repeat KeyPath<_DependencyValues, Result<each Value, each E>>,
file: StaticString = #file,
fileID: StaticString = #fileID,
line: UInt = #line
) throws -> (repeat each Value) {
let values = _DependencyValues(file: file, fileID: fileID, line: line)
return (repeat try values[keyPath: each keyPath].get())
}
/// A helper type to allow accessing both throwing and non-throwing dependencies.
@dynamicMemberLookup
struct _DependencyValues {
var file: StaticString
var fileID: StaticString
var line: UInt
fileprivate init(file: StaticString, fileID: StaticString, line: UInt) {
self.file = file
self.fileID = fileID
self.line = line
}
subscript<V>(
dynamicMember keyPath: KeyPath<DependencyValues, V>
) -> Result<V, Never> {
@Dependency(keyPath, file: file, fileID: fileID, line: line) var dependency
return .success(dependency)
}
subscript<V, E>(
dynamicMember keyPath: KeyPath<DependencyValues, Result<V, E>>
) -> Result<V, E> {
@Dependency(keyPath, file: file, fileID: fileID, line: line) var dependency
return dependency
}
}
import Dependencies
import Vapor
enum ImageServiceDependencyKey: DependencyKey {
typealias Value = Result<ImageService, Error>
static var liveValue: Value {
Result {
let (config, cloudflare) = try dependencies(\.config, \.cloudflare)
return try CloudflareImageService(
cloudflare: cloudflare.images(
accountHash: config.require(.cloudflareImagesAccountHash)
)
)
}
}
}
extension DependencyValues {
var imageService: Result<ImageService, Error> {
get { self[ImageServiceDependencyKey.self] }
set { self[ImageServiceDependencyKey.self] = newValue }
}
}
import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
// @freestanding(expression)
// public macro unimplementedEndpoint<Response>() -> Response =
// #externalMacro(module: "...", type: "UnimplementedEndpointMacro")
/// Useful for stubbing endpoints that are specified in OpenAPI but you aren't ready to implement yet
public struct UnimplementedEndpointMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
context.diagnose(
Diagnostic(
node: node,
message: SimpleDiagnosticMessage(
message: "Unimplemented endpoint",
diagnosticID: MessageID(domain: "unimplementedEndpoint", id: "warning"),
severity: .warning
)
)
)
return ".undocumented(statusCode: 500, UndocumentedPayload())"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment