Skip to content

Bridging legacy api through armadillo

Kasper Kondzielski edited this page Nov 26, 2022 · 1 revision

Assuming that your legacy api is no the following form:

jsonRpcRouter: JsonRpcRequest => IO[JsonRpcResponse]

you can wire your legacy api through armadillo using following interceptor:

import cats.effect.IO
import io.iohk.armadillo.server.JsonSupport.Json
import io.iohk.armadillo.server.ServerInterpreter.{ResponseHandlingStatus, ServerResponse}
import io.iohk.armadillo.server.{EndpointInterceptor, JsonSupport, MethodHandler, MethodInterceptor, Responder}
import io.iohk.armadillo.{JsonRpcId, JsonRpcResponse => ArmadilloJsonRpcResponse}
import legacy.api.{JsonRpcError, JsonRpcRequest, JsonRpcResponse}
import org.json4s.JsonAST.{JArray, JValue}
import sttp.monad.MonadError

class LegacyGlueRequestInterceptor(jsonRpcRouter: JsonRpcRequest => IO[JsonRpcResponse])
    extends MethodInterceptor[IO, JValue] {

  private val jsonRpcErrorCodes: List[Int] =
    List(JsonRpcError.InvalidRequest.code, JsonRpcError.ParseError.code, JsonRpcError.InvalidParams().code)

  override def apply(
      responder: Responder[IO, JValue],
      jsonSupport: JsonSupport[JValue],
      methodHandler: EndpointInterceptor[IO, JValue] => MethodHandler[IO, JValue]
  ): MethodHandler[IO, JValue] = {
    val next = methodHandler(EndpointInterceptor.noop)
    new MethodHandler[IO, JValue] {
      override def onDecodeSuccess[I](ctx: MethodHandler.DecodeSuccessContext[IO, JValue])(implicit
          monad: MonadError[IO]
      ): IO[ResponseHandlingStatus[JValue]] =
        next.onDecodeSuccess(ctx).flatMap {
          case ResponseHandlingStatus.Unhandled =>
            ctx.request.id match {
              case Some(_) =>
                ctx.request.params match {
                  case Some(Json.JsonArray(params)) => callLegacyEndpoints(jsonSupport, ctx, params)
                  case None                         => callLegacyEndpoints(jsonSupport, ctx, Vector.empty)
                  case _                            => monad.unit(ResponseHandlingStatus.Unhandled)
                }
              case None => monad.unit(ResponseHandlingStatus.Unhandled)
            }
          case actionTaken => monad.unit(actionTaken)
        }

      override def onDecodeFailure(ctx: MethodHandler.DecodeFailureContext[IO, JValue])(implicit
          monad: MonadError[IO]
      ): IO[ResponseHandlingStatus[JValue]] =
        next.onDecodeFailure(ctx)
    }
  }

  private def callLegacyEndpoints(
      jsonSupport: JsonSupport[JValue],
      ctx: MethodHandler.DecodeSuccessContext[IO, JValue],
      params: Vector[JValue]
  ): IO[ResponseHandlingStatus.Handled[JValue]] = {
    val legacyResponse = jsonRpcRouter(
      JsonRpcRequest(
        ctx.request.jsonrpc,
        ctx.request.method,
        if (params.isEmpty) None else Some(JArray(params.toList)),
        ctx.request.id.flatMap {
          case JsonRpcId.IntId(value)    => JsonRpcRequest.toId(value)
          case JsonRpcId.StringId(value) => JsonRpcRequest.toId(value)
        }
      )
    )
    convertResponse(legacyResponse, jsonSupport)
  }

  private def convertResponse(
      legacyResponse: IO[domain.JsonRpcResponse],
      jsonSupport: JsonSupport[JValue]
  ): IO[ResponseHandlingStatus.Handled[JValue]] =
    legacyResponse
      .map { response =>
        (response.result, response.error) match {
          case (Some(result), None) =>
            ServerResponse.Success(
              jsonSupport.encodeResponse(
                ArmadilloJsonRpcResponse.v2(
                  result,
                  response.id
                    .map {
                      case Left(value)  => JsonRpcId.IntId(value)
                      case Right(value) => JsonRpcId.StringId(value)
                    }
                    .getOrElse(throw new IllegalStateException("cannot happen"))
                )
              )
            )
          case (None, Some(error)) =>
            val json = jsonSupport.encodeResponse(
              ArmadilloJsonRpcResponse.error_v2(
                JsonRpcError.jsonRpcErrorEncoder.encodeJson(error),
                response.id
                  .map {
                    case Left(value)  => JsonRpcId.IntId(value)
                    case Right(value) => JsonRpcId.StringId(value)
                  }
              )
            )
            if (jsonRpcErrorCodes.contains(error.code)) {
              ServerResponse.Failure(json)
            } else {
              ServerResponse.Success(json)
            }
          case _ => throw new IllegalStateException("cannot happen")
        }
      }
      .map(serverResponse => ResponseHandlingStatus.Handled(Some(serverResponse)))
}

Thanks to that trick your legacy endpoints will work together with new armadillo endpoints (also when called from batch requests!)

Clone this wiki locally