All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.greenfossil.thorium.AnnotatedPathMacroSupport.scala Maven / Gradle / Ivy

/*
 * Copyright 2022 Greenfossil Pte Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.greenfossil.thorium

import java.nio.charset.StandardCharsets

/**
 * Note: annotations for glob is not supported
 */
object AnnotatedPathMacroSupport extends MacroSupport(globalDebug = false):

  def urlEncode(str: String): String =
    java.net.URLEncoder.encode(Option(str).getOrElse(""), StandardCharsets.UTF_8).replaceAll("\\+", "%20")

  import scala.quoted.*

  def computeActionAnnotatedPath[A : Type, R : Type](epExpr: Expr[A],
                                                     onSuccessCallback: (Expr[String], Expr[List[Any]], Expr[List[String]], Expr[List[Any]], Expr[String]) =>  Expr[R],
                                                     supportQueryStringPost: Boolean = true
                                                    )(using Quotes): Expr[R] =
    import quotes.reflect.*
    searchForAnnotations(epExpr.asTerm, 1) match
      case applyTerm: Apply =>
        show("Apply Term", applyTerm)
        val paramSymss: List[List[Symbol]] = applyTerm.symbol.paramSymss

        //Get all param names annotated with @Param from all paramList
        import com.linecorp.armeria.server.annotation.Param
        val annotatedParamNames: List[String] = paramSymss.flatten.collect{
          case sym: Symbol if sym.annotations.exists(_.symbol.fullName.startsWith(classOf[Param].getName)) =>
            sym.name
        }

        //Get all param names from all paramList
        val paramNames: List[String] = paramSymss.flatten.map(_.name)

        //Removed the params that are not annotated with @Param
        val paramValues = getFlattenedParamValues(applyTerm)
        val paramNameValueLookup: Map[String, Term] =
          paramNames.zip(paramValues)
            .toMap
            .filter((key, _) => annotatedParamNames.contains(key))

        val annList = applyTerm.symbol.annotations //Get Endpoint annotations
        getAnnotatedPath(epExpr, annList, paramNameValueLookup, onSuccessCallback, supportQueryStringPost)

      case typedTerm: Typed =>
        report.errorAndAbort(s"Check function body for '???' code", epExpr)

      case methodTerm if methodTerm.symbol.flags.is(Flags.Method) =>
        //Handle - Method
        show("Method Term", methodTerm)
        val annList = methodTerm.symbol.annotations
        getAnnotatedPath(epExpr, annList, Map.empty[String, Term], onSuccessCallback, supportQueryStringPost)

      case Literal(c) if c.value.toString.startsWith("/") =>
        onSuccessCallback(Expr("Get"), Expr.ofList(List(Expr(c.value.toString))), Expr[List[String]](Nil), Expr.ofList(List.empty[Expr[Any]]), Expr(""))

      case idTerm : Ident =>
        onSuccessCallback(Expr("Get"), Expr.ofList(List(idTerm.asExpr)), Expr[List[String]](Nil), Expr.ofList(List.empty[Expr[Any]]), Expr(""))

      case otherTerm =>
        show("otherTerm", otherTerm)
        val ref = Ref(otherTerm.symbol)
        val term = searchForAnnotations(ref, 1)
        show("search term", term)
        report.errorAndAbort("Unable to find any annotated path")

  /**
   * Get a flatten list of param values from the a list of list of param values
   * @param Quotes
   * @param applyTerm
   * @return
   */
  private def getFlattenedParamValues(using Quotes)(applyTerm: quotes.reflect.Apply): List[quotes.reflect.Term] =
    import quotes.reflect.*
    applyTerm match
      case Apply(aTerm: Apply, paramValues) => getFlattenedParamValues(aTerm) ++ paramValues
      case Apply(_, paramValues) => paramValues

  /**
   * Extract params from the annotated path
   * @param Quotes
   * @param epExpr
   * @param annList
   * @param paramNameValueLookup
   * @param successCallback - on when the params/values length matched.
   *                        Query Params are those not found in the annotated path
   * @tparam A - Expr of an Annotated Essential Action or its subtye
   * @tparam P - Computed annotated path
   * @return
   */
  private def getAnnotatedPath[A : Type, P : Type](using Quotes)(
    epExpr: Expr[A],
    annList: List[quotes.reflect.Term],
    paramNameValueLookup: Map[String, quotes.reflect.Term],
    successCallback: (Expr[String], Expr[List[Any]], Expr[List[String]], Expr[List[Any]], Expr[String]) =>  Expr[P],
    supportQueryStringPost: Boolean
  ): Expr[P] =
    import quotes.reflect.*
    extractAnnotations(annList) match
      case None =>
        //Return epExpr
        successCallback(Expr("Get"), Expr.ofList(List(epExpr)), Expr[List[String]](Nil), Expr.ofList(List.empty[Expr[Any]]), Expr(""))

      case Some((method: String, pathPattern: String)) =>
        /*
         * update the del
         */
        val (computedPath: List[Expr[Any]], queryParamKeys: List[String], queryParamValues: List[Expr[Any]]) =
          getComputedPathExpr(epExpr, paramNameValueLookup, pathPattern)
        if !supportQueryStringPost && method.equalsIgnoreCase("Post") && queryParamKeys.nonEmpty
        then report.errorAndAbort("Query String for Post method is not supported")
        else ()

        successCallback(Expr(method), Expr.ofList(computedPath), Expr[List[String]](queryParamKeys), Expr.ofList(queryParamValues), Expr(pathPattern))

  /**
   * Search for the inner most Term, skip all the outer Inlined
   * @param Quotes
   * @param term
   * @param level
   * @return
   */
  private def searchForAnnotations(using Quotes)(term: quotes.reflect.Term, level: Int): quotes.reflect.Term =
    import quotes.reflect.*
    //  show(s"SearchForAnnotations level: $level", term)
    term match
      case inlined @ Inlined(_, _, owner) =>
        show(s"Inlined...search level ${level}", inlined)
        searchForAnnotations(owner, level + 1)

      case foundTerm =>
        show("Term found", foundTerm)
        foundTerm

  /**
   *
   * @param Quotes
   * @param annList
   * @return Option[(GET|POST|PATH, path),
   */
  private def extractAnnotations(using Quotes)(annList: List[quotes.reflect.Term]): Option[(String, String)] =
    import quotes.reflect.*
    show("AnnotationList", annList)
    val xs = annList.collect {
      case Apply(Select(New(annMethod), _), args) =>
        show("Annotation HttpMethod", annMethod)
        show("Annotation Path Parts", args)
        args.collectFirst {
          case Literal(c) => (annMethod.symbol.name, c.value.toString)
          case Wildcard() => (annMethod.symbol.name, "")
        }
    }.flatten
    if xs.isEmpty then None
    else
      val tup = xs.foldLeft(("", "")){(res, tup2) =>
        tup2 match 
          case ("Path", path) if path.nonEmpty => (res._1, path)
          case (method, path) if path.isEmpty => (method, res._2)
          case (method, path) => (method, path)
      }
      Some(tup)

  private def getComputedPathExpr[A : Type](using Quotes)(
    actionExpr: Expr[A],
    paramNameValueLookup: Map[String, quotes.reflect.Term],
    pathPattern: String): (List[Expr[Any]], List[String], List[Expr[Any]])  =

    import quotes.reflect.*

    //Parameterized Path
    var usedPathParamNames: List[String] = Nil

    def getPathParamExpr(name: String): Expr[Any] =
      paramNameValueLookup.get(name) match
        case Some(value) =>
          usedPathParamNames = usedPathParamNames :+ name
          value match {
            case Literal(c: StringConstant)  =>
              //UrlEncode for all String value
              Expr(urlEncode(c.value.asInstanceOf[String]))

            case valOrDef if valOrDef.symbol.isValDef || valOrDef.symbol.isDefDef =>
              //UrlEncode all the Idents of type String
              valOrDef.tpe.asType match {
                case '[String] =>
                  '{urlEncode(${valOrDef.asExprOf[String]})}
                case _ =>
                  valOrDef.asExpr
              }

            case x =>
              x.asExpr
          }
        case None =>
          report.errorAndAbort(s"Path param [$name] of pathPattern [$pathPattern] cannot be found in method's param names [${paramNameValueLookup.keySet.mkString(",")}]  ", actionExpr)

    def paramPathExtractor(path: String): List[Expr[Any]] =
      val parts = path.split("/:")
      parts.tail.foldLeft(List[Expr[Any]](Expr(parts.head))) { (accPath, part) =>
        val newParts = part.split("/").toList match
          case Nil =>  Nil
          case xs => getPathParamExpr(xs.head) +: xs.tail.map(p => Expr(p))

        accPath ++ newParts
      }

    //compute Path
    val computedPath: List[Expr[Any]] =
      pathPattern match
        case path if path.startsWith("prefix:/") =>
          List(Expr(path.replaceFirst("prefix:","")))
        case path if path.matches("regex:\\^?.+") =>
          val paramPath =
            path
              .replaceAll("(\\(\\?-?[idmsuxU]\\))", "") //remove all regex 'on' and 'off' modifiers
              .replaceFirst("regex:\\^?([^$]+)\\$?", "$1") //remove 'regex:' + optional '^' + optional '?'
              .replaceAll("/\\(\\?<(\\w+)>.+?\\)", "/:$1") //replace regex-param with :param

          paramPathExtractor(paramPath)
        case path =>
          //convert all braced params to colon params
          val _path=path.replaceAll("/\\{(\\w+)}", "/:$1")
          paramPathExtractor(_path)

    //compute QueryString
    val queryParamKeys: List[String] = paramNameValueLookup.keys.toList diff usedPathParamNames
    val queryParamValues: List[Expr[Any]] = queryParamKeys.map{k =>
      paramNameValueLookup(k) match
        case Literal(c: StringConstant) =>
          //UrlEncode for all String value
          Expr(urlEncode(c.value.asInstanceOf[String]))

        case valOrDef if valOrDef.symbol.isValDef || valOrDef.symbol.isDefDef =>
          //UrlEncode all the Idents of type String
          valOrDef.tpe.asType match {
            case '[String] =>
              '{urlEncode($ {valOrDef.asExprOf[String]})}

            case _ =>
              valOrDef.asExpr
          }

        case x =>
          x.asExpr
    }

    (computedPath, queryParamKeys, queryParamValues)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy