Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parsing JSON body in advance. Solves #391 #392

Merged
merged 9 commits into from
Apr 5, 2018

Conversation

amarrella
Copy link
Collaborator

No description provided.

@amarrella amarrella requested review from gheine and fiadliel April 4, 2018 15:53
@amarrella
Copy link
Collaborator Author

cc @mchimirev

@amarrella
Copy link
Collaborator Author

Example code generated:

package io.apibuilder.generator.v0 {
  import cats.effect._
  import cats.implicits._

  object Constants {

    val BaseUrl = "http://www.apidoc.me"
    val Namespace = "io.apibuilder.generator.v0"
    val UserAgent = "apibuilder-play_2x_client-unknown"
    val Version = "0.11.17"
    val VersionMajor = 0

  }

  class Client[F[_]: Sync](
                            val baseUrl: org.http4s.Uri,
                            auth: scala.Option[io.apibuilder.generator.v0.Authorization] = None,
                            defaultHeaders: Seq[(String, String)] = Nil,
                            httpClient: org.http4s.client.Client[F]
                          ) extends interfaces.Client[F] {
    import org.http4s.Response
    import io.apibuilder.common.v0.models.json._
    import io.apibuilder.generator.v0.models.json._
    import io.apibuilder.spec.v0.models.json._


    def generators: Generators[F] = Generators

    def healthchecks: Healthchecks[F] = Healthchecks

    def invocations: Invocations[F] = Invocations

    object Generators extends Generators[F] {
      override def get(
                        key: _root_.scala.Option[String] = None,
                        limit: Int = 100,
                        offset: Int = 0,
                        requestHeaders: Seq[(String, String)] = Nil
                      ): F[Seq[io.apibuilder.generator.v0.models.Generator]] = {
        val urlPath = Seq("generators")

        val queryParameters = Seq(
          key.map("key" -> _),
          Some("limit" -> limit.toString),
          Some("offset" -> offset.toString)
        ).flatten

        _executeRequest[Unit, Seq[io.apibuilder.generator.v0.models.Generator]]("GET", path = urlPath, queryParameters = queryParameters, requestHeaders = requestHeaders) {
          case r if r.status.code == 200 => _root_.io.apibuilder.generator.v0.Client.parseJson[F, Seq[io.apibuilder.generator.v0.models.Generator]]("Seq[io.apibuilder.generator.v0.models.Generator]", r)
          case r => Sync[F].raiseError(new io.apibuilder.generator.v0.errors.FailedRequest(r.status.code, s"Unsupported response code[${r.status.code}]. Expected: 200"))
        }
      }

      override def getByKey(
                             key: String,
                             requestHeaders: Seq[(String, String)] = Nil
                           ): F[io.apibuilder.generator.v0.models.Generator] = {
        val urlPath = Seq("generators", key)

        _executeRequest[Unit, io.apibuilder.generator.v0.models.Generator]("GET", path = urlPath, requestHeaders = requestHeaders) {
          case r if r.status.code == 200 => _root_.io.apibuilder.generator.v0.Client.parseJson[F, io.apibuilder.generator.v0.models.Generator]("io.apibuilder.generator.v0.models.Generator", r)
          case r if r.status.code == 404 => Sync[F].raiseError(new errors.UnitResponse(r.status.code))
          case r => Sync[F].raiseError(new io.apibuilder.generator.v0.errors.FailedRequest(r.status.code, s"Unsupported response code[${r.status.code}]. Expected: 200, 404"))
        }
      }
    }

    object Healthchecks extends Healthchecks[F] {
      override def get(
                        requestHeaders: Seq[(String, String)] = Nil
                      ): F[io.apibuilder.generator.v0.models.Healthcheck] = {
        val urlPath = Seq("_internal_", "healthcheck")

        _executeRequest[Unit, io.apibuilder.generator.v0.models.Healthcheck]("GET", path = urlPath, requestHeaders = requestHeaders) {
          case r if r.status.code == 200 => _root_.io.apibuilder.generator.v0.Client.parseJson[F, io.apibuilder.generator.v0.models.Healthcheck]("io.apibuilder.generator.v0.models.Healthcheck", r)
          case r => Sync[F].raiseError(new io.apibuilder.generator.v0.errors.FailedRequest(r.status.code, s"Unsupported response code[${r.status.code}]. Expected: 200"))
        }
      }
    }

    object Invocations extends Invocations[F] {
      override def postByKey(
                              key: String,
                              invocationForm: io.apibuilder.generator.v0.models.InvocationForm,
                              requestHeaders: Seq[(String, String)] = Nil
                            ): F[io.apibuilder.generator.v0.models.Invocation] = {
        val urlPath = Seq("invocations", key)

        val payload = invocationForm

        _executeRequest[io.apibuilder.generator.v0.models.InvocationForm, io.apibuilder.generator.v0.models.Invocation]("POST", path = urlPath, body = Some(payload), requestHeaders = requestHeaders) {
          case r if r.status.code == 200 => _root_.io.apibuilder.generator.v0.Client.parseJson[F, io.apibuilder.generator.v0.models.Invocation]("io.apibuilder.generator.v0.models.Invocation", r)
          case r if r.status.code == 409 => _root_.io.apibuilder.generator.v0.Client.parseJson[F, Seq[io.apibuilder.generator.v0.models.Error]]("Seq[io.apibuilder.generator.v0.models.Error]", r).flatMap(body => Sync[F].raiseError(new errors.ErrorsResponse(r, None, body)))
          case r => Sync[F].raiseError(new io.apibuilder.generator.v0.errors.FailedRequest(r.status.code, s"Unsupported response code[${r.status.code}]. Expected: 200, 409"))
        }
      }
    }

    private lazy val defaultApiHeaders = Seq(
      ("User-Agent", Constants.UserAgent),
      ("X-Apidoc-Version", Constants.Version),
      ("X-Apidoc-Version-Major", Constants.VersionMajor.toString)
    )

    def apiHeaders: Seq[(String, String)] = defaultApiHeaders

    def modifyRequest(request: F[org.http4s.Request[F]]): F[org.http4s.Request[F]] = request

    implicit def circeJsonEncoder[F[_]: Sync, A](implicit encoder: io.circe.Encoder[A]) = org.http4s.circe.jsonEncoderOf[F, A]

    def _executeRequest[T, U](
                               method: String,
                               path: Seq[String],
                               queryParameters: Seq[(String, String)] = Nil,
                               requestHeaders: Seq[(String, String)] = Nil,
                               body: Option[T] = None
                             )(handler: org.http4s.Response[F] => F[U]
                             )(implicit encoder: io.circe.Encoder[T]): F[U] = {
      import org.http4s.QueryParamEncoder._

      val m = org.http4s.Method.fromString(method) match {
        case Right(m) => m
        case Left(e) => sys.error(e.toString)
      }

      val headers = org.http4s.Headers((
        apiHeaders ++
          defaultHeaders ++
          requestHeaders
        ).toList.map { case (k, v) => org.http4s.Header(k, v) })

      val queryMap = queryParameters.groupBy(_._1).map { case (k, v) => k -> v.map(_._2) }
      val uri = path.foldLeft(baseUrl){ case (uri, segment) => uri / segment }.setQueryParams(queryMap)

      val request = org.http4s.Request[F](method = m,
        uri = uri,
        headers = headers)

      val authReq = auth.fold(request) {
        case Authorization.Basic(username, passwordOpt) => {
          val userpass = s"$username:${passwordOpt.getOrElse("")}"
          val token = java.util.Base64.getEncoder.encodeToString(userpass.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1))
          request.putHeaders(org.http4s.Header("Authorization", s"Basic $token"))
        }
        case a => sys.error("Invalid authorization scheme[" + a.getClass + "]")
      }

      val authBody = body.fold(Sync[F].pure(authReq))(authReq.withBody)

      httpClient.fetch(modifyRequest(authBody))(handler)
    }

    object errors {

      case class ErrorsResponse(
                                 response: org.http4s.Response[F],
                                 message: Option[String] = None,
                                 errors: Seq[io.apibuilder.generator.v0.models.Error]
                               ) extends Exception(message.getOrElse(response.status.code + ": " + response.body))

      case class UnitResponse(status: Int) extends Exception(s"HTTP $status")
    }
  }

  object Client {
    import cats.effect._


    implicit def circeJsonDecoder[F[_]: Sync, A](implicit decoder: io.circe.Decoder[A]) = org.http4s.circe.jsonOf[F, A]


    def parseJson[F[_]: Sync, T](
                                  className: String,
                                  r: org.http4s.Response[F]
                                )(implicit decoder: io.circe.Decoder[T]): F[T] = r.attemptAs[T].value.flatMap {
      case Right(value) => Sync[F].pure(value)
      case Left(error) => Sync[F].raiseError(new io.apibuilder.generator.v0.errors.FailedRequest(r.status.code, s"Invalid json for class[" + className + "]", None, error))
    }
  }

  sealed trait Authorization extends _root_.scala.Product with _root_.scala.Serializable
  object Authorization {
    case class Basic(username: String, password: Option[String] = None) extends Authorization
  }

  package interfaces {

    trait Client[F[_]] {
      def baseUrl: org.http4s.Uri
      def generators: io.apibuilder.generator.v0.Generators[F]
      def healthchecks: io.apibuilder.generator.v0.Healthchecks[F]
      def invocations: io.apibuilder.generator.v0.Invocations[F]
    }

  }

  trait Generators[F[_]] {
    /**
      * Get all available generators
      *
      * @param key Filter generators with this key
      * @param limit The number of records to return
      * @param offset Used to paginate. First page of results is 0.
      */
    def get(
             key: _root_.scala.Option[String] = None,
             limit: Int = 100,
             offset: Int = 0,
             requestHeaders: Seq[(String, String)] = Nil
           ): F[Seq[io.apibuilder.generator.v0.models.Generator]]

    /**
      * Get generator with this key
      */
    def getByKey(
                  key: String,
                  requestHeaders: Seq[(String, String)] = Nil
                ): F[io.apibuilder.generator.v0.models.Generator]
  }

  trait Healthchecks[F[_]] {
    def get(
             requestHeaders: Seq[(String, String)] = Nil
           ): F[io.apibuilder.generator.v0.models.Healthcheck]
  }

  trait Invocations[F[_]] {
    /**
      * Invoke a generator
      */
    def postByKey(
                   key: String,
                   invocationForm: io.apibuilder.generator.v0.models.InvocationForm,
                   requestHeaders: Seq[(String, String)] = Nil
                 ): F[io.apibuilder.generator.v0.models.Invocation]
  }

  package errors {

    case class FailedRequest(responseCode: Int, message: String, requestUri: Option[_root_.java.net.URI] = None, parent: Exception = null) extends _root_.java.lang.Exception(s"HTTP $responseCode: $message", parent)

  }
}

@amarrella
Copy link
Collaborator Author

Ok this introduces a small breaking change since now the body value is not wrapped in F. I might need for backward compatibility to wrap in F with pure, but I will also make the decoded response body available unwrapped (maybe with the name "responseBody"), since it's way nicer to handle (see #391)

Copy link
Collaborator

@gheine gheine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, few comments inline.

@@ -184,7 +184,7 @@ private lazy val defaultAsyncHttpClient = PooledHttp1Client()
""")
override val canSerializeUuid = true
override val implicitArgs: Option[String] = None
override val asyncSuccess: String = "now"
override def asyncSuccess: String = "now"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be a def?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gets overridden

@@ -107,7 +107,13 @@ class ScalaClientMethodGenerator (
if (response.isUnit) {
s"case r => ${http4sConfig.wrappedAsyncType("Sync").getOrElse(http4sConfig.asyncType)}.${http4sConfig.asyncFailure}(new errors.${response.errorClassName}(r.${config.responseStatusMethod}))"
} else {
s"case r => ${http4sConfig.wrappedAsyncType("Sync").getOrElse(http4sConfig.asyncType)}.${http4sConfig.asyncFailure}(new errors.${response.errorClassName}(r))"
config match {
case _@(_: ScalaClientMethodConfigs.Http4s017 | _: ScalaClientMethodConfigs.Http4s015) =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks it can be simplified to

case _: ScalaClientMethodConfigs.Http4s017 | _: ScalaClientMethodConfigs.Http4s015 =>

@@ -145,7 +151,13 @@ class ScalaClientMethodGenerator (
Some(s"case r if r.${config.responseStatusMethod} == $statusCode => ${http4sConfig.wrappedAsyncType("Sync").getOrElse(http4sConfig.asyncType)}.${http4sConfig.asyncFailure}(new errors.${response.errorClassName}(r.${config.responseStatusMethod}))")

} else {
Some(s"case r if r.${config.responseStatusMethod} == $statusCode => ${http4sConfig.wrappedAsyncType("Sync").getOrElse(http4sConfig.asyncType)}.${http4sConfig.asyncFailure}(new errors.${response.errorClassName}(r))")
config match {
case _@(_: ScalaClientMethodConfigs.Http4s017 | _: ScalaClientMethodConfigs.Http4s015) =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}
}.distinct.sorted

private def exceptionClass(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only called from one place (line 190) so could be inlined there.

@amarrella
Copy link
Collaborator Author

@fiadliel @gheine this is ready now

@mchimirev I slightly changed the new API, to avoid doing attempt now you have to use the body attribute from the case class

@amarrella amarrella merged commit cb18441 into apicollective:master Apr 5, 2018
anadoba pushed a commit to anadoba/apibuilder-generator that referenced this pull request Mar 14, 2019
…#392)

* Moved error models to client class and removed effect

* Added errors object within the client to avoid namespace conflicts

* Formatting

* Fixed error scope in client

* Parsing json body before returning. Solves apicollective#391

* Lifted body into F for backward compatibility and renamed decoded body to 'body'

* Inlined exceptionclass and simplified pattern match
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants