Skip to content

Commit

Permalink
added comments explaining the macros
Browse files Browse the repository at this point in the history
  • Loading branch information
lbialy committed Feb 8, 2024
1 parent cb857b5 commit faa74d9
Showing 1 changed file with 36 additions and 11 deletions.
47 changes: 36 additions & 11 deletions core/src/main/scala/besom/util/NonEmptyString.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,43 +55,65 @@ object NonEmptyString:
end fromStringOutputImpl

/** @param s
* a [[String]] to be converted to a [[NonEmptyString]].
* a [[String]] to be inferred to be a [[NonEmptyString]].
* @return
* a [[NonEmptyString]] if the given [[String]] is not empty or blank.
* a [[NonEmptyString]] if the given [[String]] is not empty or blank and can be looked up as a static constant, otherwise a
* compile-time error.
*/
inline def from(inline s: String): NonEmptyString = ${ fromImpl('s) }

private def fromImpl(expr: Expr[String])(using quotes: Quotes): Expr[NonEmptyString] =
import quotes.reflect.*

// utility to validate the string and convert it to a NonEmptyString
def toValidNesOrFail(s: String, strExpr: Expr[String]): Expr[NonEmptyString] =
if s.isEmpty then report.errorAndAbort("Empty string is not allowed here!")
else if s.isBlank then report.errorAndAbort("Blank string is not allowed here!")
else strExpr.asExprOf[NonEmptyString]

// kudos to @Kordyjan for most of this implementation
// this function traverses the tree of the given expression and tries to extract the constant string value from it in Right
// if it fails, it returns a Left with the final tree that couldn't be extracted from
def downTheRabbitHole(tree: Term): Either[Term, String] =
tree match
// it's a val x = "y" situation, we take the rhs
case Inlined(_, _, inner) => downTheRabbitHole(inner)

// it's a constant string, yay, we're done
case Literal(StringConstant(value)) =>
Right(value.toString)

// it's a `"x" * 5` situation, we verify that multiplier is > 0 (otherwise this operation returns an empty string)
// and extract the string constant that is being multiplied, if the string can't be extracted we return a Left
case augment @ Apply(Select(Apply(Ident("augmentString"), List(augmented)), "*"), List(multiplier)) =>
val opt = for
multiplierValue <- multiplier.asExprOf[Int].value if multiplierValue > 0
augmentedValue <- augmented.asExprOf[String].value
yield augmentedValue

opt.toRight(augment)

// it's a `"x" + "y"` situation, we try to extract both sides, if they can't be extracted we replace their value
// with an empty string and concatenate them, final logic checks if the string is empty or blank anyway
case Apply(Select(left, "+"), right :: Nil) =>
for
l <- downTheRabbitHole(left).orElse(Right(""))
r <- downTheRabbitHole(right).orElse(Right(""))
yield l + r

// it's a `val x = someObject.x` situation, we take the rhs and go deeper
case t if t.tpe.termSymbol != Symbol.noSymbol && t.tpe <:< TypeRepr.of[String] =>
t.tpe.termSymbol.tree match
case ValDef(_, _, Some(rhs)) => downTheRabbitHole(rhs)
case _ =>
report.errorAndAbort(t.tpe.termSymbol.tree.toString())
Left(t)

// uh, can't match, fail the whole thing
case other =>
Left(other)

expr match
// if it's a string interpolation, we check if at least one segment is non-empty and non-blank
case '{ scala.StringContext.apply(${ Varargs(parts) }: _*).s(${ Varargs(_) }: _*) } =>
val atLeastOneSegmentIsNonEmptyAndNonBlank =
parts
Expand All @@ -100,6 +122,8 @@ object NonEmptyString:

if atLeastOneSegmentIsNonEmptyAndNonBlank then expr.asExprOf[NonEmptyString]
else report.errorAndAbort("This interpolated string is possibly empty, empty strings are not allowed here!")

// if it's a formatted string interpolation, we check if at least one segment is non-empty and non-blank
case '{ scala.StringContext.apply(${ Varargs(parts) }: _*).f(${ Varargs(_) }: _*) } =>
val atLeastOneSegmentIsNonEmptyAndNonBlank =
parts
Expand All @@ -108,6 +132,8 @@ object NonEmptyString:

if atLeastOneSegmentIsNonEmptyAndNonBlank then expr.asExprOf[NonEmptyString]
else report.errorAndAbort("This interpolated string is possibly empty, empty strings are not allowed here!")

// if it's a raw string interpolation, we check if at least one segment is non-empty and non-blank
case '{ scala.StringContext.apply(${ Varargs(parts) }: _*).raw(${ Varargs(_) }: _*) } =>
val atLeastOneSegmentIsNonEmptyAndNonBlank =
parts
Expand All @@ -116,25 +142,24 @@ object NonEmptyString:

if atLeastOneSegmentIsNonEmptyAndNonBlank then expr.asExprOf[NonEmptyString]
else report.errorAndAbort("This interpolated string is possibly empty, empty strings are not allowed here!")

// oh, it's just an expression of type String, let's try to extract the constant value from it directly
// if it's not a constant value let's see if we can infer it using the tree traversal function `downTheRabbitHole`
case '{ $str: String } =>
str.value match
case Some(s) =>
if s.isEmpty then report.errorAndAbort("Empty string is not allowed here!")
else if s.isBlank then report.errorAndAbort("Blank string is not allowed here!")
else str.asExprOf[NonEmptyString]
case Some(s) => toValidNesOrFail(s, str)
case None =>
downTheRabbitHole(expr.asTerm) match
case Right(value) =>
if value.isEmpty then report.errorAndAbort("Empty string is not allowed here!")
else if value.isBlank then report.errorAndAbort("Blank string is not allowed here!")
else Expr(value)
case Right(s) => toValidNesOrFail(s, str)
case Left(tree) =>
report.errorAndAbort(
"Only constant strings or string interpolations are allowed here, use NonEmptyString.apply instead! " + tree.show
)
end match
end fromImpl

// these implicit conversions allow inference of NonEmptyString from String literals

implicit inline def str2NonEmptyString(inline s: String): NonEmptyString = NonEmptyString.from(s)

import besom.internal.{Output, Context}
Expand Down

0 comments on commit faa74d9

Please sign in to comment.