swiftswift-regexbuilder

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


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)?


Solution

  • 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.