Search code examples
swiftstring-interpolationswift-stringcustomstringconvertible

Is it possible to change Swift String Interpolation for the String typealias?


I have a lot of DTOs in my app which log some field. That field should not be logged because the data is kind of sensitive. The model looks like this:

typealias HiddenFieldType = String

struct DTO1 {
    var field1_1: String
    var fieldToHide: HiddenFieldType
    var field1_2: String
    var field1_3: String
}

struct DTO2 {
    var field2_1: String
    var field2_2: String
    var fieldToHide: HiddenFieldType
    var field2_3: String
}

the code which outputs the data is like this (actually it's os_log in a real app):

func test() {
    let dto1 = DTO1(field1_1: "1_1", fieldToHide: "super-secret-1", field1_2: "1_2", field1_3: "1_3")
    let dto2 = DTO2(field2_1: "2_1", field2_2: "2_2", fieldToHide: "super-secret-2", field2_3: "2_3")

    print("Test1: dto1=\(dto1) dto2=\(dto2)")
}

Approach #1

It seems the field can be hidden in DTO1 with such code:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: DTO1) {
        appendInterpolation("field1_1: \(value.field1_1), fieldToHide: 🤷‍♀️, field1_2: \(value.field1_2), field1_3: \(value.field1_3)")
    }
}

However, the solution is neither scalable nor maintainable:

  • the same extension should be added for each DTO
  • each field should be included into appendInterpolation - a lot of boilerplate
  • if a new field is added to some DTO, we may forget to update appendInterpolation etc

Approach #2

I tried to add interpolation for HiddenFieldType (assuming it's a type, just like DTO1...):

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: HiddenFieldType) {
        appendInterpolation("🤷‍♀️")
    }
} 

But this solution doesn't work at all:

  • the compiler says "Function call causes an infinite recursion"
  • and it actually causes an infinite recursion
  • when changing appendInterpolation to appendLiteral, there's no recursion, but "super-secret-1" is not hidden

Approach #3

I tried overriding DefaultStringInterpolation, conforming to ExpressibleByStringLiteral/ExpressibleByStringInterpolation, but it doesn't work: the compiler says that HiddenFieldType is String, and Conformance of 'String' to protocol 'ExpressibleByStringLiteral' was already stated in the type's module 'Swift'

The only approach I can imagine is changing typealias HiddenFieldType = String to struct HiddenFieldType { let value: String }, so the HiddenFieldType becomes a "real" type.


Approach #4

Then such code doesn't cause an infinite recursion anymore, but doesn't works either (the value is unhidden)

struct HiddenFieldType { 
    let value: String 
}

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: HiddenFieldType) {
        appendInterpolation("🤷‍♀️")
    }
}

Approach #5

This code finally works:

struct HiddenFieldType { 
    let value: String 
}

extension HiddenFieldType: CustomStringConvertible {
    var description: String {
        "🤷‍♀️"
    }
}

As I can't imagine any better, for now I'd use this approach, but it also has some slight scalability issues, as I must update each DTO's initializing point: from

let dto1 = DTO1(field1_1: "1_1", fieldToHide: "super-secret-1", field1_2: "1_2", field1_3: "1_3")

to

let dto1 = DTO1(field1_1: "1_1", fieldToHide: .init(value: "super-secret-1"), field1_2: "1_2", field1_3: "1_3")

and I hoped to only add some extension in the file which contains typealias HiddenFieldType = String, and not to update the entire code.


The questions

  • Is it possible to hide the value of HiddenFieldType without changing it from typealias to struct, and without updating each DTO?
  • Is there any better approach than 5?

Thanks in advance


Solution

  • Is it possible to hide the value of HiddenFieldType without changing it from typealias to struct

    I think you're attempting to use the wrong tool for the job here. A typealias is just a name change, and it sounds like you want something that acts fundamentally different than a String (i.e. one gets printed when passed into an os_log call and one doesn't). You won't be able to write logic that treats a String different from its typealias; the compiler doesn't differentiate between them.

    Is it possible to make your DTOs classes instead of structs? (EDIT: see below, you can keep them as structs and just use a protocol extension) If so, you could use reflection on a superclass to accomplish this without having to manually specify the description for every different DTO.

    struct HiddenFieldType {
        let value: String
    }
    
    open class DTO: CustomStringConvertible {
        public var description: String {
            Mirror(reflecting: self).children.compactMap { $0.value as? String }.joined(separator: "\n")
        }
    }
    
    final class DTO1: DTO {
        let field1_1: String
        let field1_2: String
        let fieldToHide: HiddenFieldType
    
        init(field1_1: String, field1_2: String, fieldToHide: HiddenFieldType) {
            self. field1_1 = field1_1
            self. field1_2 = field1_2
            self. fieldToHide = fieldToHide
        }
    }
    

    Note that I'm including all Strings in the description but, if you have types other than String and HiddenFieldType that you want to log, you could always just filter out the HiddenFieldTypes specifically.

    Personally, I'd be hesitant to rely on reflection for any critical code but other people have more tolerance for it so its a judgement call.

    EDIT: You don't need to use inheritance to accomplish this. Instead of a superclass, DTO should be a protocol that conforms to CustomStringConvertible:

    protocol DTO: CustomStringConvertible {}
    extension DTO {
        public var description: String {
            Mirror(reflecting: self).children.compactMap { $0.value as? String }.joined(separator: "\n")
        }
    }