Metaprogramming: Compile-time check whether a String matches a regex

I am trying to validate whether the given String is a valid version, in this cases major.minor.patch

The following code works at the moment, but fails with 3.3.0-RC2. Is there an obvious replacement?

case class Version(major: Int, minor: Int, patch: Int):
    def render: String = s"$major.$minor.$patch"

object Version:
    import scala.compiletime.*
    import scala.compiletime.ops.string.*

    private val VersionPattern      = raw"(\d+)\.(\d+)\.(\d+)".r

    inline def apply(inline versionNo: String): Version =
      inline if constValue[Matches[versionNo.type, "\\d+\\.\\d+.\\d+"]] then
        val VersionPattern(major, minor, patch) = versionNo: @unchecked
        Version(major.toInt, minor.toInt, patch.toInt)
      else error("Not a valid version of the form <major>.<minor>.<patch>")

See this scastie scastie with the code that works as expected in 3.2.2.

(The change seems intentional, that’s why I’m hoping for an obvious replacement: see Github Issue 16804 )

Hmm, I found that this works:

inline def apply[V <: String]: Version =
   inline if constValue[Matches[V, "\\d+\\.\\d+.\\d+"]] then
      val VersionPattern(major, minor, patch) = constValue[V]: @unchecked
      Version(major.toInt, minor.toInt, patch.toInt)
   else error("Not valid")

But the call-site isn’t very intuitive anymore: Version[“1.2.0”] vs Version(“1.2.0”), so it’s more a workaround than a solution

1 Like

You can add a Singleton upper bound:

inline def apply[V <: String & Singleton](v: V): Version
2 Likes

Thanks a lot, works perfectly for me!

For completeness sake: This is the full version I have now - I’m not sure if there is a more elegant way for reusing the ErrorMsg :sweat_smile: (scastie)

case class Version(major: Int, minor: Int, patch: Int):
    def render: String = s"$major.$minor.$patch"

object Version:
    import scala.compiletime.*
    import scala.compiletime.ops.string.*

    private val VersionPattern      = raw"(\d+)\.(\d+)\.(\d+)".r
    private type Error = "Not a valid version of the form <major>.<minor>.<patch>"

    private val ErrorMsg = constValue[Error]

    inline def apply[V <: String & Singleton](versionNo: V): Version =
      inline if constValue[Matches[V, "\\d+\\.\\d+.\\d+"]] then
        val Right(value) = parse(versionNo): @unchecked
        value
      else error(constValue[Error])

    def parse(v: String): Either[String, Version] = v match
      case VersionPattern(major, minor, patch) =>
        Right(Version(major.toInt, minor.toInt, patch.toInt))
      case invalid => 
        Left(s"$ErrorMsg: $invalid")

(edit: removed unnecessary inline from versionNo parameter)

private transparent inline def ErrorMsg = "Not a valid version of the form <major>.<minor>.<patch>"

For some reason it needs to be a transparent def. With only inline I get this error, which is a bit silly IMHO.

A literal string is expected as an argument to `compiletime.error`. Got "Not a valid version of the form <major>.<minor>.<patch>":String

Oh, I had only tried the variant without transparent and then given up. Thanks a lot!

With transparent the compiler doesn’t add the upcast : String when he inlines the method. That upcast keeps him from recognizing the expression as a literal.

1 Like