add factory functions #26

This commit is contained in:
Konrad `ktoso` Malawski 2020-09-10 11:14:02 +09:00
parent 9be28a8450
commit 920644b013
7 changed files with 127 additions and 21 deletions

View File

@ -22,7 +22,7 @@
/// typealias Value = String
/// }
///
/// var context = BaggageContext()
/// var context = BaggageContext.empty
/// // set a new value
/// context[TestIDKey.self] = "abc"
/// // retrieve a stored value
@ -47,8 +47,11 @@
public struct BaggageContext: BaggageContextProtocol {
private var _storage = [AnyBaggageContextKey: Any]()
/// Create an empty `BaggageContext`.
public init() {}
/// Internal on purpose, please use `TODO` or `.background` to create an "empty" context,
/// which carries more meaning to other developers why an empty context was used.
///
/// Frameworks and libraries which create a new baggage to populate it right away can start
init() {}
public subscript<Key: BaggageContextKey>(_ key: Key.Type) -> Key.Value? {
get {
@ -159,3 +162,79 @@ extension AnyBaggageContextKey: Hashable {
hasher.combine(ObjectIdentifier(self.keyType))
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: Background BaggageContext
extension BaggageContextProtocol {
/// An empty baggage context intended as the "root" or "initial" baggage context background processing tasks, or as the "root" baggage context.
///
/// It is never canceled, has no values, and has no deadline.
/// It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.
///
/// ### Usage in frameworks and libraries
/// This function is really only intended to be used frameworks and libraries, at the "top-level" where a request's,
/// message's or task's processing is initiated. For example, a framework handling requests, should create an empty
/// context when handling a request only to immediately populate it with useful trace information extracted from e.g.
/// request headers.
///
/// ### Usage in applications
/// More often than not application code should never have to create an empty context.
///
/// Usually, a framework such as an HTTP server or similar "request handler" would already provide users
/// with a context to be passed along through subsequent calls.
///
/// If unsure where to obtain a context from, prefer using `TODO("Not sure where I should get a context from here?")`,
/// such that other developers are informed that the lack of context was not done on purpose, but rather because either
/// not being sure where to obtain a context from, or other framework limitations -- e.g. the outer framework not being
/// context aware just yet.
public static var empty: BaggageContext {
return BaggageContext()
}
}
// ==== ----------------------------------------------------------------------------------------------------------------
// MARK: "TO DO" BaggageContext
extension BaggageContextProtocol {
/// A baggage context intended as a placeholder until a real value can be passed through a function call.
///
/// It should ONLY be used while prototyping or when the passing of the proper context is not yet possible,
/// e.g. because an external library did not pass it correctly and has to be fixed before the proper context
/// can be obtained where the TO-DO is currently used.
///
/// ### Crashing on TO-DO context creation
/// You may set the `BAGGAGE_CRASH_TODOS` variable while compiling a project in order to make calls to this function crash
/// with a fatal error, indicating where a to-do baggage context was used. This comes in handy when wanting to ensure that
/// a project never ends up using with code which initially was written as "was lazy, did not pass context", yet the
/// project requires context passing to be done correctly throughout the application. Similar checks can be performed
/// at compile time easily using linters (not yet implemented), since it is always valid enough to detect a to-do context
/// being passed as illegal and warn or error when spotted.
///
/// - Parameters:
/// - reason: Informational reason for developers, why a placeholder context was used instead of a proper one,
/// - Returns: Empty "to-do" baggage context which should be eventually replaced with a carried through one, or `background`.
public static func TODO(_ reason: String, function: String = #function, file: String = #file, line: UInt = #line) -> BaggageContext {
var context = BaggageContext.empty
#if BAGGAGE_CRASH_TODOS
fatalError("BAGGAGE_CRASH_TODOS: at \(file):\(line) (function \(function))")
#else
context[TODOKey.self] = .init(file: file, line: line)
return context
#endif
}
}
internal enum TODOKey: BaggageContextKey {
typealias Value = TODOLocation
static var name: String? {
return "todo-loc"
}
}
/// Carried automatically by a "to do" baggage context.
/// It can be used to track where a context originated and which "to do" context must be fixed into a real one to avoid this.
public struct TODOLocation {
let file: String
let line: UInt
}

View File

@ -219,7 +219,7 @@ class ArgumentParser<U> {
) {
self.arguments.append(
Argument(name: name, help: help)
{ try self.parseArgument(name, property, defaultValue, parser) }
{ try self.parseArgument(name, property, defaultValue, parser) }
)
}

View File

@ -21,7 +21,7 @@ public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_async_empty_100_000 ",
runFunction: { _ in
let context = BaggageContext()
let context = BaggageContext.empty
pass_async(context: context, times: 100_000)
},
tags: [],
@ -31,7 +31,7 @@ public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_async_smol_100_000 ",
runFunction: { _ in
var context = BaggageContext()
var context = BaggageContext.empty
context.k1 = "one"
context.k2 = "two"
context.k3 = "three"
@ -45,7 +45,7 @@ public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_async_small_nonconst_100_000",
runFunction: { _ in
var context = BaggageContext()
var context = BaggageContext.empty
context.k1 = "\(Int.random(in: 1 ... Int.max))"
context.k2 = "\(Int.random(in: 1 ... Int.max))"
context.k3 = "\(Int.random(in: 1 ... Int.max))"
@ -65,7 +65,7 @@ public let BaggagePassingBenchmarks: [BenchmarkInfo] = [
BenchmarkInfo(
name: "BaggagePassingBenchmarks.pass_mut_async_small_100_000 ",
runFunction: { _ in
var context = BaggageContext()
var context = BaggageContext.empty
context.k1 = "\(Int.random(in: 1 ... Int.max))"
context.k2 = "\(Int.random(in: 1 ... Int.max))"
context.k3 = "\(Int.random(in: 1 ... Int.max))"

View File

@ -18,7 +18,7 @@ import XCTest
final class LoggingBaggageContextCarrierTests: XCTestCase {
func test_ContextWithLogger_dumpBaggage() throws {
let baggage = BaggageContext()
let baggage = BaggageContext.empty
let logger = Logger(label: "TheLogger")
var context: LoggingBaggageContextCarrier = ExampleFrameworkContext(context: baggage, logger: logger)
@ -43,7 +43,7 @@ final class LoggingBaggageContextCarrierTests: XCTestCase {
}
func test_ContextWithLogger_log_withBaggage() throws {
let baggage = BaggageContext()
let baggage = BaggageContext.empty
let logging = TestLogging()
let logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) })
@ -66,7 +66,7 @@ final class LoggingBaggageContextCarrierTests: XCTestCase {
}
func test_ContextWithLogger_log_prefersBaggageContextOverExistingLoggerMetadata() {
let baggage = BaggageContext()
let baggage = BaggageContext.empty
let logging = TestLogging()
var logger = Logger(label: "TheLogger", factory: { label in logging.make(label: label) })
logger[metadataKey: "secondIDExplicitlyNamed"] = "set on logger"
@ -103,7 +103,7 @@ struct CoolFrameworkContext: LoggingBaggageContextCarrier {
return self._logger.with(context: self.baggage)
}
var baggage: BaggageContext = .init()
var baggage: BaggageContext = .empty
// framework context defines other values as well
let frameworkField: String = ""

View File

@ -194,8 +194,7 @@ extension History {
metadata: Logger.Metadata? = nil,
source: String? = nil,
file: StaticString = #file,
line: UInt = #line)
{
line: UInt = #line) {
let source = source ?? Logger.currentModule(filePath: "\(file)")
let entry = self.find(level: level, message: message, metadata: metadata, source: source)
XCTAssertNotNil(
@ -215,8 +214,7 @@ extension History {
metadata: Logger.Metadata? = nil,
source: String? = nil,
file: StaticString = #file,
line: UInt = #line)
{
line: UInt = #line) {
let source = source ?? Logger.currentModule(filePath: "\(file)")
let entry = self.find(level: level, message: message, metadata: metadata, source: source)
XCTAssertNil(

View File

@ -30,6 +30,8 @@ extension BaggageContextTests {
("testEmptyBaggageDescription", testEmptyBaggageDescription),
("testSingleKeyBaggageDescription", testSingleKeyBaggageDescription),
("testMultiKeysBaggageDescription", testMultiKeysBaggageDescription),
("test_todo_context", test_todo_context),
("test_todo_empty", test_todo_empty),
]
}
}

View File

@ -18,7 +18,7 @@ final class BaggageContextTests: XCTestCase {
func testSubscriptAccess() {
let testID = 42
var baggage = BaggageContext()
var baggage = BaggageContext.empty
XCTAssertNil(baggage[TestIDKey.self])
baggage[TestIDKey.self] = testID
@ -31,7 +31,7 @@ final class BaggageContextTests: XCTestCase {
func testRecommendedConvenienceExtension() {
let testID = 42
var baggage = BaggageContext()
var baggage = BaggageContext.empty
XCTAssertNil(baggage.testID)
baggage.testID = testID
@ -42,18 +42,18 @@ final class BaggageContextTests: XCTestCase {
}
func testEmptyBaggageDescription() {
XCTAssertEqual(String(describing: BaggageContext()), "BaggageContext(keys: [])")
XCTAssertEqual(String(describing: BaggageContext.empty), "BaggageContext(keys: [])")
}
func testSingleKeyBaggageDescription() {
var baggage = BaggageContext()
var baggage = BaggageContext.empty
baggage.testID = 42
XCTAssertEqual(String(describing: baggage), #"BaggageContext(keys: ["TestIDKey"])"#)
}
func testMultiKeysBaggageDescription() {
var baggage = BaggageContext()
var baggage = BaggageContext.empty
baggage.testID = 42
baggage[SecondTestIDKey.self] = "test"
@ -63,6 +63,33 @@ final class BaggageContextTests: XCTestCase {
XCTAssert(description.contains("TestIDKey"))
XCTAssert(description.contains("ExplicitKeyName"))
}
// ==== ------------------------------------------------------------------------------------------------------------
// MARK: Factories
func test_todo_context() {
// the to-do context can be used to record intentions for why a context could not be passed through
let context = BaggageContext.TODO("#1245 Some other library should be adjusted to pass us context")
_ = context // avoid "not used" warning
// TODO: Can't work with protocols; re-consider the entire carrier approach... Context being a Baggage + Logger, and a specific type.
// func take(context: BaggageContextProtocol) {
// _ = context // ignore
// }
// take(context: .TODO("pass from request instead"))
}
func test_todo_empty() {
let context = BaggageContext.empty
_ = context // avoid "not used" warning
// TODO: Can't work with protocols; re-consider the entire carrier approach... Context being a Baggage + Logger, and a specific type.
// static member 'empty' cannot be used on protocol metatype 'BaggageContextProtocol.Protocol'
// func take(context: BaggageContextProtocol) {
// _ = context // ignore
// }
// take(context: .empty)
}
}
private enum TestIDKey: BaggageContextKey {