构建自定义记录器:自定义 Swift 所有类型的字符串插值

Building a custom logger: Custom Swift String interpolation for all types

我想重现新的 Swift 日志记录功能的行为,其中字符串中的内插值显示为 <private> 而不是我在我的自定义记录器中使用的实际值应用

例如:

let accountNumber = 12345
log("User account number: \(accountNumber)")

// User account number: 12345  ← leaking personal information 

相反,我想要这样的结果:

// User account number: <private>

我查看了扩展 String.StringInterpolation,它非常适合自定义类型,但我不知道如何连接到默认插值,因为我需要捕获任何插值类型。

这是一个带有自定义参数名称的示例,但很容易忘记指定:

extension String.StringInterpolation
{
    mutating func appendInterpolation<T>(private value:T) {
        let literal = "<private>"
        appendLiteral(literal)
    }
}

log("User account number: \(private: accountNumber)") // Too easy to forget "private:"...

// User account number: <private> 

您可以创建一个自定义类型 - 让我们称它为 SecureMessage - 即 ExpressibleByStringInterpolation(和 CustomStringConvertible 更好的衡量标准)并让您的 log 函数接受它作为其消息参数:

func log(_ message: SecureMessage) {
   print("\(message)")
}

let accountNumber = 12345
log("User account number: \(accountNumber)")

现在,我们可以定义SecureMessage:

struct SecureMessage: ExpressibleByStringInterpolation, 
                      CustomStringConvertible {

   struct StringInterpolation: StringInterpolationProtocol {
      var output = ""

      init(literalCapacity: Int, interpolationCount: Int) { }
        
      mutating func appendLiteral(_ literal: String) {
         output.append(literal)
      }

      mutating func appendInterpolation<T>(_ str: T) {
         output.append("<private>")
      }
   }

   let description: String

   init(stringLiteral value: String) {
      description = value
   }

   init(stringInterpolation: StringInterpolation) {
      description = stringInterpolation.output
   }
}

当然,这有点没用,因为它会隐藏所有插值,而不仅仅是您认为的私有插值。

因此,您或许可以定义一个隐私级别,以便在每次插值时应用它:

enum PrivacyLevel {
   case `public`, `private`
}

并更改 appendInterpolation 方法以获取具有某些默认隐私值的隐私参数:

mutating func appendInterpolation<T: LosslessStringConvertible>(_ str: T, privacy: PrivacyLevel = .private) {
   switch privacy {
      case .private:
         output.append("<private>")
      case .public:
         output.append(String(str))
   }
}
log("Hidden account number:  \(accountNumber)") // private by default
log("Visible account number: \(accountNumber, privacy: .public)")