Skip to content

Instantly share code, notes, and snippets.

@twittemb
Created April 4, 2023 09:26
Show Gist options
  • Save twittemb/c1f9bdb5bf4a6ae349e23de6020724f3 to your computer and use it in GitHub Desktop.
Save twittemb/c1f9bdb5bf4a6ae349e23de6020724f3 to your computer and use it in GitHub Desktop.
import Foundation
var shouldApplyPrivacy: Bool = {
#if targetEnvironment(simulator) || DEBUG
return false
#else
return true
#endif
}()
/// A type that conforms to this protocol has to provide a `privacyDescription`. This description
/// can use `PrivacyString` privacy policies such as `.public`, `.private(.redacted)` or `.private(.hashed)`.
public protocol CustomPrivacyStringConvertible: CustomStringConvertible {
var privacyDescription: PrivacyString { get }
}
/// We can derive a default description from the PrivacyString. Doing so, when printing
/// the object, we will automatically benefit from the privacy policies.
public extension CustomPrivacyStringConvertible {
var description: String {
self.privacyDescription.output
}
}
/// We can derive a default errorDescription from the PrivacyString. Doing so, when printing
/// the error, we will automatically benefit from the privacy policies.
public extension LocalizedError where Self: CustomPrivacyStringConvertible {
var errorDescription: String? {
self.localizedDescription
}
var localizedDescription: String {
self.privacyDescription.output
}
}
/// A PrivacyString allows to include privacy instructions in an interpolated string.
/// By default, the privacy is set to `.private(.redacted)`.
/// The privacy policy will be applied only in a production environment (Simulator, Xcode and TestFlight are excluded)
///
/// ```
/// let id = 12345
/// let name = "Joe"
/// let age = 21
/// let message: PrivacyString = "id=\(id, privacy: .private(.hashed)), name=\(name), age=\(age, privacy: .public)"
///
/// print(message) // will print "id=-4514200681978012441, name=[redacted], age=21"
/// ```
///
/// We can also build a PrivacyString from an interpolated string that embeds PrivacyString.
///
/// ```
/// let idA = 12345
/// let nameA = "Joe"
/// let ageA = 21
///
/// let idB = 54321
/// let nameB = "Jane"
/// let ageB = 20
///
/// let userAMessage: PrivacyString = "id=\(idA, privacy: .private(.hashed)), name=\(nameA), age=\(ageA, privacy: .public)"
/// let userBMessage: PrivacyString = "id=\(idB, privacy: .private(.hashed)), name=\(nameB), age=\(ageB, privacy: .public)"
/// let message: PrivacyString = "userA: \(userAMessage), userB: \(userBMessage)"
///
/// print(message) // will print "userA: id=-4514200681978012441, name=[redacted], age=21, userB: id=-1558397100237974017, name=[redacted], age=20"
/// ```
///
/// Finally, it is also possible to make a type conform to CustomPrivacyStringConvertible.
/// Doing so, it will allow to build a PrivacyString from the object interpolation.
///
/// ```
/// struct User: CustomPrivacyStringConvertible {
/// let id: Int
/// let name: String
/// let age: Int
///
/// var privacyDescription: PrivacyString {
/// "id=\(self.id, privacy: .private(.hashed)), name=\(self.name), age=\(self.age, privacy: .public)"
/// }
/// }
///
/// let user = User(id: 12345, name: "Joe", age: 21)
/// let message: PrivacyString = "User: \(user)"
/// print(message) // will print "User: id=-4514200681978012441, name=[redacted], age=21"
/// ```
public struct PrivacyString: ExpressibleByStringInterpolation, CustomStringConvertible, Equatable {
/// The level of privacy to apply to a value inside an interpolated string
public enum Privacy {
/// the value will be integrated "as is" in the resulting string
case `public`
/// the value will be hidden (applying a Mask) in the resulting string
case `private`(Mask)
func apply(on value: Any) -> String {
switch self {
case .public: return "\(value)"
case let .private(mask): return mask.apply(on: value)
}
}
}
/// The mask to apply to private values inside an interpolated string
public enum Mask {
static let redactedString = "[redacted]"
/// the value will be replaced by a `[redacted]` placeholder
case redacted
/// the value will be replaced by its hash counterpart
case hashed
func apply(on value: Any) -> String {
switch self {
case .redacted:
return Mask.redactedString
case .hashed:
guard let anyHashable = value as? AnyHashable else {
return "\(String(reflecting: value).hashValue)"
}
return "\(anyHashable.hashValue)"
}
}
}
/// Converts chucks of an interpolated string into the final string according to the privacy policy
public struct StringInterpolation: StringInterpolationProtocol {
var output = ""
public init(literalCapacity: Int, interpolationCount: Int) {}
public mutating func appendLiteral(_ literal: String) {
self.output += literal
}
public mutating func appendInterpolation(_ value: Any, privacy: Privacy = .private(.redacted)) {
guard shouldApplyPrivacy else {
self.output += "\(value)"
return
}
let processedString = privacy.apply(on: value)
self.output += processedString
}
public mutating func appendInterpolation(_ value: PrivacyString) {
self.output += value.output
}
public mutating func appendInterpolation<Value: CustomPrivacyStringConvertible>(_ value: Value) {
self.appendInterpolation(value.privacyDescription)
}
}
/// The string value obtained with the string interpolation including the privacy requirements.
public let output: StringLiteralType
/// Instantiate from a string literal (like "this is my message")
/// - Parameter literal: the string literal
public init(stringLiteral literal: String) {
self.output = literal
}
/// Instantiate from an interpolated string (like "this is my \(message, privacy: .public)")
/// - Parameter stringInterpolation: the interpolated string
public init(stringInterpolation: StringInterpolation) {
self.output = stringInterpolation.output
}
public var description: String {
self.output
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment