I misused ExpressibleByArray
in order to coerce [Any]
to my implementation. I also faked a #function
defaulted parameter to report the call site function. See this thread for a discussion of #file
#function
literal defaults.
Here's the crufty code:
public final class LogElement: CustomStringConvertible, ExpressibleByStringInterpolation, ExpressibleByArrayLiteral {
let elements: [Any]
public let description: String
public init(arrayLiteral elements: Any...) {
self.elements = elements
switch elements.count {
case 0: self.description = "()"
case 1:
if let element = elements[0] as? String {
self.description = "\"\(element)\""
} else {
self.description = "\(elements[0])"
}
case _: self.description =
"Warning: please only use a single element in a LogElement array literal!\n\(elements)"
}
}
public init(stringLiteral value: String) {
self.description = "\"\(value)\""
self.elements = [value]
}
public init(stringInterpolation: StringInterpolation) {
description = stringInterpolation.output
self.elements = [stringInterpolation.output]
}
final public class StringInterpolation: StringInterpolationProtocol {
var output = ""
public init(literalCapacity: Int, interpolationCount: Int) {
output.reserveCapacity(literalCapacity * 2)
}
public func appendLiteral(_ literal: String) {
output.append(literal)
}
public func appendInterpolation(_ value: Any, label: String = "") {
if label.isEmpty == false {
output.append("\(label): ")
}
if let string = value as? String {
output.append("\"\(string)\"")
} else {
output.append("\(value)")
}
}
}
}
@dynamicCallable
public struct Logger {
public init() { }
private (set) public var doPrintFunction: Bool = false
public mutating func enableFunctionHeader() {
doPrintFunction = true
}
public mutating func disableFunctionHeader() {
doPrintFunction = false
}
public func dynamicallyCall(withArguments args: [LogElement]) {
printFunctionName()
let output = args.map { "\($0)" }.joined(separator: ", ")
print("ℹ️", output)
}
public func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, LogElement>) {
printFunctionName()
var args = args.toArray()
if let fileIndex = args.firstIndex(where: { $0.key == "file" }),
let filePath = args[fileIndex].value.elements.first as? String {
let url = URL(fileURLWithPath: filePath)
args[fileIndex] = ("file", "\(url.lastPathComponent)")
}
let output = args.map { "\($0.key): \($0.value)" }.joined(separator: ", ")
print("ℹ️", output)
}
private let guessCallSiteStackDistance = 3
@inline(__always) // Doesn't seem to work in Debug.
private func printFunctionName() {
guard doPrintFunction else { return }
if let callSiteSymbol = Thread.callStackSymbols.prefix(guessCallSiteStackDistance).last,
let mangledSignature = callSiteSymbol.split(separator: " ").prefix(4).last.map({ String($0) }) {
if let functionName = try? parseMangledSwiftSymbol(mangledSignature, isType: false).print(using: .simplified),
let preceding = functionName.lastIndex(of: "."),
let successor = functionName.firstIndex(of: "(") {
let start = functionName.index(after: preceding)
let end = functionName.index(before: successor)
print("🔹", functionName[start...end], "(): ", separator: "", terminator: " ")
}
}
}
}
class DynamicLoggerTests: XCTestCase {
func testDynamicLogger() {
var log = Logger()
log("simply literal")
log("interp\(0)lat\(1)\(0)n")
oneLevelDeeper(passing: log)
log(file: #file, isEmpty: [], isArray: [[1,2,3]])
log.enableFunctionHeader()
log(isNested: "\("nesting") \("works")", butOnlyOneLevelDeep: "🤯") // "\("This \("fails")")")
}
func oneLevelDeeper(passing log: Logger) {
var log = log
log("\(0...1, label: "closed")", "\(2..<3, label: "halfOpen")")
log(partialUpTo: [...4], partialFrom: [5...])
log.enableFunctionHeader()
log(shouldPrintCallingFunction: "Shame we can't have #file:#line propogated")
}
}
// prints to console:
// ℹ️ "simply literal"
// ℹ️ interp0lat10n
// ℹ️ closed: 0...1, halfOpen: 2..<3
// ℹ️ partialUpTo: PartialRangeThrough<Int>(upperBound: 4), partialFrom: PartialRangeFrom<Int>(lowerBound: 5)
// 🔹oneLevelDeeper(): ℹ️ shouldPrintCallingFunction: "Shame we can't have #file:#line propogated"
// ℹ️ file: "ChangesetRegressionData.swift", isEmpty: (), isArray: [1, 2, 3]
// 🔹testDynamicLogger(): ℹ️ isNested: "nesting" "works", butOnlyOneLevelDeep: "🤯"