com-lihaoyi / cask

Cask: a Scala HTTP micro-framework. Cask makes it easy to set up a website, backend server, or REST API using Scala
https://com-lihaoyi.github.io/cask/
Other
525 stars 55 forks source link

Support Request deserialized into case class in the JsonEndpoint #136

Closed RecursionTaoist closed 1 month ago

RecursionTaoist commented 1 month ago

like @RequestBody annotation to read the request body and deserialized into an Object in springbot

RecursionTaoist commented 1 month ago

My attempt ...

code:

/**
 * Builds a implicit request body json parser.
 *
 * @tparam T the type of deserialize.
 * @return request body json parser.
 * @note This parser read bytes from request body directly.
 * @note Do not support custom [[FieldSerializer]] (field rename to / from
 *       and ignore).
 * @note Setting the arity value to `2` prompts the
 *       [[formula.router.Runtime.argParse]] to serialize the request body.
 */
implicit def defaultJsonParser[T <: Product : Manifest]: BasicParser[T] =
  new BasicParser[T] {
    /**
     * Indicates the json request body, need to deserialize.
     */
    override def arity: Int = 2
    /**
     * @inheritdoc
     */
    override def parse(
      req: Request,
      name: String,
      input: Seq[String]
    ): T = {
      implicit val formats: Formats = DefaultFormats
      read[T](req.data.text)
    }
  }
/**
 * Parses the argument of the given `signature` to the target type by i
 * input value, additional parameters or raw [[Request]] (decided by th
 * `arity`) via the corresponding `parse` function.
 *
 * @param args arguments map for obtaining the input value.
 * @param req [[Request]].
 * @param default function to build default value.
 * @param signature argument signature.
 * @tparam IN the type of input.
 * @return parse result in [[Any]] if successfully, a sequence of
 *         [[Result.ParamError]] otherwise.
 * @note Invoked by macro-generated code, used for [[Entry]] building.
 */
def argParse[IN](
  args: Map[String, IN],
  req: Request,
  default: => Option[Any],
  signature: ArgSig[IN, _, _]
): Either[Seq[Result.ParamError], Any] = {
  signature.parser.arity match {
    case 0 =>
      // query parameters
      tryEither(
        signature.parser.parse(
          req,
          signature.name,
          null.asInstanceOf[IN]
        ),
        Result.ParamError.DefaultFailed(signature, _)
      ).left.map(Seq(_))
    case 1 =>
      // normal argument list or its default value if input is absent
      args.get(signature.name) match {
        case None =>
          tryEither(
            default.get,
            Result.ParamError.DefaultFailed(signature, _)
          ).left.map(Seq(_))
        case Some(x) =>
          tryEither(
            signature.parser.parse(
              req,
              signature.name,
              x
            ),
            Result.ParamError.Invalid(signature, x.toString, _)
          ).left.map(Seq(_))
      }
    case 2 =>
      // only for json parse from request body, see BasicParser
      tryEither(
        signature.parser.parse(
          req,
          signature.name,
          null.asInstanceOf[IN]
        ),
        Result.ParamError.DefaultFailed(signature, _)
      ).left.map(Seq(_))
  }
}

results show:

package example

import scala.util.{Failure, Success, Try}

/**
 * Example application.
 */
object HelloWorld extends formula.MainRoutes {
  /**
   * Json post demo, mixed use of path variable, query parameters and json
   * request body.
   *
   * @param skill path variable.
   * @param score path variable.
   * @param params query parameters.
   * @param profile request body.
   * @return response.
   */
  @formula.json.post("/refine/:skill/with/:score")
  def refine(
    skill: String,
    score: Float,
    params: formula.Params,
    profile: Profile
  ): Profile = {
    val expertise = params.map({
      case (k, vs) =>
        Try(vs.head.toFloat) match {
          case Success(s) => k -> s
          case Failure(_) => k -> 0.0f
        }
    })

    Profile(
      name = profile.name,
      age = profile.age,
      level = profile.level,
      expertise = profile.expertise + (skill -> score) ++ expertise
    )
  }

  initialize()
}

/**
 * For test.
 */
case class Profile(
  name: String,
  age: Int,
  level: String,
  expertise: Map[String, Float]
)