Use CustomDebugStringConvertible to produce enum case name as String

Hello,

I am trying to find a convenient and reusable way to be able to create a string based on an enum's case.
Here is a very simple example:

enum MyEnum: Int {
  case one
  case two
 // imagine a lot of cases...
}

struct SomeStruct {
  let myEnum: MyEnum
}

let test = SomeStruct(myEnum: .one)
print(test)

When printing this (in a playground) i get this result in the console
SomeStruct(myEnum: __lldb_expr_17.MyEnum.one)

instead of __lldb_expr_17.MyEnum.one I would like to print just ".one"

The example above is quite simple however this is not only limited to playgrounds.
The main reason I want to have such a simple print is for better console / file logging and unit test assert messages.
If say MyEnum was part of another library, it will always have the Type.case format and in some cases have Module.Type.case format. This verbose description makes reading logs and unit tests much harder than a short name like ".one".
I am working on a quite large code base with many such enums that contain a lot of cases. Writing manual debugDescription for all of them would be cumbersome and since it is based on strings it can be easy to write incorrect implementation especially after refactoring some cases without the compiler warning me.
I tried experimenting and made a simple function using String(reflecting:)

func enumCaseName<T>(for `case`: T) -> String {
  if let caseName = String(reflecting: `case`).split(separator: ".").last {
    return ".\(String(caseName))"
  } else {
    return String(reflecting: `case`)
  }
}

extension MyEnum: CustomDebugStringConvertible {
  var debugDescription: String {
    enumCaseName(for: self)
  }
}

When trying to call this within debugDescription implementation I was surprised to find out that it produced an infinite call stack loop and overflowed.
After reading the documentation for String(reflecting:) it was obvious why I had the problem, because if the parameter that was passed conformed to CustomDebugStringConvertible than this initializer would use its debugDescription instead, thus creating the problem described above.

I would like to hear any suggestions, how to solve this without manually implementing debugDescription. Additionally many of the enums are of type Int, so I can not make them of type String and use their raw value unfortunately. This does not include enums with associated values.

Thank you,
Filip

Generally, you can use String(describing:) to do what you want:

enum Key1: String, CaseIterable {
    case foo
    case bar = "foobar"
}
print(Key1.allCases.map { String(describing: $0) })
// ["foo", "bar"]

I say generally, because I just today hit a really annoying problem with this approach, which is: if your enum is also your CodingKey for Codable conformance, the CodingKey protocol hijacks the product of String(describing:) resulting in:

enum Key2: String, CodingKey, CaseIterable {
    case foo
    case bar = "foobar"
}
print(Key2.allCases.map { String(describing: $0) })
// ["Key2(stringValue: \"foo\", intValue: nil)", "Key2(stringValue: \"foobar\", intValue: nil)"]

Thanks for the reply @karim but as I have stated in my post I can not use enums of type String because many of them are Int and Unts , they are decoded from data arrays from bluetooth so they have to stay Int

Sorry I wasn't clear... String(describing:) works regardless of the backing type of the enum:

enum IntEnum: Int, CaseIterable {
    case foo
    case bar = 42
}
print(IntEnum.allCases.map { String(describing: $0) })
// ["foo", "bar"]

enum NopeEnum: CaseIterable {
    case foo, bar
}
print(NopeEnum.allCases.map { String(describing: $0) })
// ["foo", "bar"]

But again, it fails if the enums in question implement a protocol which supply their own CustomStringConvertible, CustomDebugStringConvertible, or TextOutputStreamable protocols.

I was hoping you could achieve this with Mirror, but it appears to only work when the case has an associated value. So it doesn't solve your original example, bummer.

// nil because children is empty
Mirror(reflecting: MyEnum.one).children.first?.label

// if there was an actual payload, it'd work
Mirror(reflecting: MyEnum.one(1)).children.first?.label // "one"

I wonder if it would be possible to have cases without associated values reflect with one child of (label: "one", value: ()) :thinking: