How to build a ChoiceOf regex component from an CaseIterable enumeration of String values?

448 views Asked by At

Currently I use this workaround to pass a list of enum cases to a ChoiceOf.

enum Fruit: String, CaseIterable {
    case apple = "Apple"
    case banana = "Banana"
    case strawberry = "Strawberry"
}

let regex = Regex {
    ChoiceOf {
        try! Regex(Fruit.allCases.map(\.rawValue).joined(separator: "|"))
    }
}

Is there a more elegant way to do this, without using a hardcoded regex pattern? Something like ChoiceOf(Fruit.allCases)?

1

There are 1 answers

0
Sweeper On BEST ANSWER

This is kind of a hack too, but you can see how the regex builders work in the Swift evolution proposal:

Regex {
  regex0
  regex1
  regex2
  regex3
}

becomes

Regex {
  let e0 = RegexComponentBuilder.buildExpression(regex0)
  let e1 = RegexComponentBuilder.buildExpression(regex1)
  let e2 = RegexComponentBuilder.buildExpression(regex2)
  let e3 = RegexComponentBuilder.buildExpression(regex3)
  let r0 = RegexComponentBuilder.buildPartialBlock(first: e0)
  let r1 = RegexComponentBuilder.buildPartialBlock(accumulated: r0, next: e1)
  let r2 = RegexComponentBuilder.buildPartialBlock(accumulated: r1, next: e2)
  let r3 = RegexComponentBuilder.buildPartialBlock(accumulated: r2, next: e3)
  return r3
}

Rather than RegexComponentBuilder, we can use AlternationBuilder here to make a ChoiceOf. You can see that the way that buildExpression and buildPartialBlock are called are like a map and reduce.

let regex = Regex {
    let exps = Fruit.allCases.map { AlternationBuilder.buildExpression($0.rawValue) }

    // assuming exps is not empty
    exps.dropFirst().reduce(AlternationBuilder.buildPartialBlock(first: exps[0])) { acc, next in
        AlternationBuilder.buildPartialBlock(accumulated: acc, next: next)
    }
}

We can put this into an extension:

extension ChoiceOf where RegexOutput == Substring {
    init<S: Sequence<String>>(_ components: S) {
        let exps = components.map { AlternationBuilder.buildExpression($0) }
        
        guard !exps.isEmpty else {
            fatalError("Empty choice!")
        }
        
        self = exps.dropFirst().reduce(AlternationBuilder.buildPartialBlock(first: exps[0])) { acc, next in
            AlternationBuilder.buildPartialBlock(accumulated: acc, next: next)
        }
    }
}

Notably, this does not work when the array is empty, i.e. when there is no choice to be made. You cannot just return: Choice { }. That violates one of the constraints of that initialiser. And indeed, Choice { } doesn't make sense anyway.

I think this is also why this isn't supported out of the box - the compiler cannot determine whether Fruits.allCases, or whatever other array you give it, is empty or not.