10

I'm working on a RESTful app in Kotlin and for the router, I'm using a when statement, as it's the most readable and good looking conditional.

Is there a way to use Regex or a wildcard in the when statement for a string?

(So that URIs like "/article/get/" would all be passed to the same controller)

The structure of my router is as follows:

when(uri) {
    "some/url" -> return SomeController(config).someAction(session)
}
Jake S.
  • 568
  • 6
  • 25
  • 1. There is no wildcard in "/article/get/" so it only matches itself which makes it a bit hard to understand your question. (i.e. what kind of URI matching do you really want/need?) 2. Putting verbs like "get" in RESTful APIs is typically frowned upon; the HTTP methods are the verbs and resources should be referenceable with the same address regardless of the verb. 3. Have you considered using a RESTful framework with routing built-in? e.g. Jersey. – mfulton26 Dec 22 '16 at 15:48

4 Answers4

11

Yes.

import kotlin.text.regex

val regex1 = Regex( /* pattern */ )
val regex2 = Regex( /* pattern */ )
/* etc */

when {
    regex1.matches(uri) -> /* do stuff */
    regex2.matches(uri) -> /* do stuff */
    /* etc */
}

You could also use containsMatchIn if that suits your needs better than matches.

Explanation:

The test expression of a when statement is optional. If no test expression is included, then the when statement functions like an if-else if chain, where the whenCondition of each whenEntry shall independently evaluate to a boolean.


EDIT:

So I thought about it for awhile, and I came up with a different approach that might be closer to what you want.

import kotlin.text.regex

when (RegexWhenArgument(uri)) {
    Regex(/* pattern */) -> /* do stuff */
    Regex(/* pattern */) -> /* do stuff */
    /* etc */
}

Where RegexWhenArgument is minimally defined as:

class RegexWhenArgument (val whenArgument: CharSequence) {
    operator fun equals(whenEntry: Regex) = whenEntry.matches(whenArgument)
    override operator fun equals(whenEntry: Any?) = (whenArgument == whenEntry)
}

This approach lets you get as close as possible to the "argument-ful" when expression syntax. I think it's about as streamlined and readable as it can be (assuming that you define the RegexWhenArgument class elsewhere).

This approach uses something similar to the visitor design pattern in combination with Kotlin's operator overloading to redefine what constitutes a "match" between a when expression argument and a whenEntry. If you really wanted to, I suppose you could take this approach a step further and generify RegexWhenArgument into a general-purpose WhenArgument and WhenArgumentDecorator that allows you to specify custom "match" criteria in a when expression for any sort of type, not just Regex.

Graham
  • 7,431
  • 18
  • 59
  • 84
Travis
  • 2,135
  • 17
  • 29
  • Thanks mate! This is not perfect, but I guess it's the best option for me right now. – Jake S. Dec 22 '16 at 14:33
  • @JacobS You could use anonymous instances of `Regex` if you don't want the pattern to be visually separated from the `return SomeController` logic, but that might start taking up a lot of horizontal line space inside the `when` block... – Travis Dec 22 '16 at 14:59
  • @JacobS. I just edited in another approach that might be closer to what you're looking for... – Travis Dec 22 '16 at 22:13
  • This is great! Thank you. – Jake S. Dec 26 '16 at 19:36
  • Anyone else have a problem with with using import kotlin.text.regex? Like I wonder if it was moved – user3505901 Nov 16 '17 at 04:05
  • 1
    Unfortunately, the second approach to have a more elegant syntax does not work with Kotlin 1.5. There is a typing issue (RegexWhenArgument is not a Regex). Anyway, that trick may be error prone in other contexts because RegexWhenArgument::equals was not symmetric. – mcoolive Oct 28 '22 at 10:53
2

The typing of the when statement enforces to have compatible types between the whenSubject and the whenEntries. So we cannot compare a String whenSubject with a Regex directly.

We can use when with no subject, then branch conditions may be simply boolean expressions.

fun main() {
  val uri: String? = "http://my.site.com/a/b/c"

  val res = when {
    uri == null -> "NULL"
    uri == "http://my.site.com/" -> "ROOT"
    uri.startsWith("http://my.site.com/a/") -> "A STUFF"
    uri.matches(Regex("http://my.site.com/b/.*")) -> "B STUFF"
    else -> "DEFAULT"
  }
  /* do stuff */
}

Alternatively, we can emulate a kind of when+regex with a dedicated class and few helper functions.

fun main() {
  val uri: String? = "http://my.site.com/a/b/c"

  val res2 = when(matching(uri)) {
    null -> "NULL"
    matchesLiteral("http://my.site.com/") -> "ROOT"
    matchesRegex("http://my.site.com/a/.*") -> "A STUFF"
    else -> "DEFAULT"
  }
  /* do stuff */
}

class MatchLiteralOrPattern(val value: String, val isPattern: Boolean) {
    override fun hashCode(): Int = value.hashCode()
    override fun equals(other: Any?): Boolean {
        if (other !is MatchLiteralOrPattern) return false
        if (isPattern && !other.isPattern) return Regex(this.value).matches(other.value)
        if (!isPattern && other.isPattern) return Regex(other.value).matches(this.value)
        return value == other.value
    }
}
fun matching(whenSubject: String?) = whenSubject?.let { MatchLiteralOrPattern(it, false) }
fun matchesLiteral(value: String) = MatchLiteralOrPattern(value, false)
fun matchesRegex(value: String) = MatchLiteralOrPattern(value, true)
mcoolive
  • 3,805
  • 1
  • 27
  • 32
  • In your alternative solution you didn't override hashCode(). Can you update your answer? – Cyber Avater Mar 23 '23 at 07:25
  • I added an implementation of hashCode(). But I am not sure it is really useful here. I don't see any scenario where it would be used. I hope my answer can help people to figure how the when syntax works in Kotlin 1.5. But in my case, I used when with no subject because I think my solution based on MatchLiteralOrPattern was too complex. – mcoolive Mar 23 '23 at 11:39
1
operator fun Regex.contains(text: CharSequence): Boolean = this.matches(text)

when("Alex") {
  in Regex(".+lex") -> { println("ends with lex") }
}

Copied from here: https://discuss.kotlinlang.org/t/using-regex-in-a-when/1794/6?u=cyberavater

Cyber Avater
  • 1,494
  • 2
  • 9
  • 25
0

I tried the following on the kotlin playground and it seems to work as expected.

class WhenArgument (val whenArg: CharSequence) {
    override operator fun equals(other: Any?): Boolean {
        return when (other) {
            is Regex -> other.matches(whenArg)
            else -> whenArg.equals(other)
        }
    }
}

fun what(target: String): String {
    return when (WhenArgument(target) as Any) {
        Regex("source-.*") -> "${target} is-a-source"
        Regex(".*-foo") -> "${target} is-a-foo"
        "target-fool" -> "${target} is-the-target-fool"
        else -> "nothing"
    }
}

fun main() {
    println(what("target-foo"))
    println(what("source-foo"))
    println(what("target-bar"))
    println(what("target-fool"))
}

It works around the type compatibility problem by making the 'when' argument of type Any.

phreed
  • 1,759
  • 1
  • 15
  • 30