Created
November 22, 2023 11:07
-
-
Save Obbut/285c2662c4081c3a15e6ed41c23549d3 to your computer and use it in GitHub Desktop.
Dependencies
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum ConfigKey: String, CaseIterable { | |
/// Either "SES" or "MOCK". | |
case mailProvider = "MAIL_PROVIDER" | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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