Created
April 4, 2023 09:26
-
-
Save twittemb/c1f9bdb5bf4a6ae349e23de6020724f3 to your computer and use it in GitHub Desktop.
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 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