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

kalix.springsdk.impl.RestKalixClientImpl.scala Maven / Gradle / Ivy

/*
 * Copyright 2021 Lightbend Inc.
 *
 * 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 kalix.springsdk.impl

import java.util.concurrent.CompletionStage

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.jdk.CollectionConverters.CollectionHasAsScala
import scala.jdk.FutureConverters._

import akka.http.scaladsl.model.HttpMethod
import akka.http.scaladsl.model.HttpMethods
import akka.http.scaladsl.model.Uri
import com.google.protobuf.Descriptors
import com.google.protobuf.DynamicMessage
import com.google.protobuf.any.Any
import kalix.javasdk.DeferredCall
import kalix.javasdk.impl.AnySupport
import kalix.javasdk.impl.MetadataImpl
import kalix.javasdk.impl.RestDeferredCall
import kalix.springsdk.KalixClient
import kalix.springsdk.impl.http.HttpEndpointMethodDefinition
import kalix.springsdk.impl.http.HttpEndpointMethodDefinition.ANY_METHOD
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.web.reactive.function.client.WebClient

/**
 * INTERNAL API
 */
final class RestKalixClientImpl(messageCodec: SpringSdkMessageCodec) extends KalixClient {

  private val logger: Logger = LoggerFactory.getLogger(getClass)

  private var services: Seq[HttpEndpointMethodDefinition] = Seq.empty

  // At the time of creation, Proxy Discovery has not happened yet
  // and we need the ProxyInfo to build the WebClient, so we need a Promise[WebClient]
  private val promisedWebClient: Promise[WebClient] = Promise[WebClient]()

  def setWebClient(localWebClient: WebClient) = promisedWebClient.trySuccess(localWebClient)

  private val webClient: Future[WebClient] = promisedWebClient.future

  def registerComponent(descriptor: Descriptors.ServiceDescriptor): Unit = {
    services ++= HttpEndpointMethodDefinition.extractForService(descriptor)
  }

  private def buildWrappedBody[P](
      httpDef: HttpEndpointMethodDefinition,
      inputBuilder: DynamicMessage.Builder,
      body: Option[P] = None): Any = {
    if (body.isDefined && httpDef.rule.body.nonEmpty) {
      val bodyField = httpDef.methodDescriptor.getInputType.getFields.asScala
        .find(_.getName == httpDef.rule.body)
        .getOrElse(
          throw new IllegalArgumentException("Could not find a matching body field with name: " + httpDef.rule.body))

      inputBuilder.setField(bodyField, messageCodec.encodeJava(body.get))
    }
    Any(
      AnySupport.DefaultTypeUrlPrefix + "/" + inputBuilder.getDescriptorForType.getFullName,
      inputBuilder.build().toByteString)
  }

  override def get[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.GET, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        body = None,
        httpDef,
        () =>
          webClient
            .flatMap(
              _.get()
                .uri(uriStr)
                .retrieve()
                .bodyToMono(returnType)
                .toFuture
                .asScala)
            .asJava)
    }
  }

  override def post[P, R](uriStr: String, body: P, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.POST, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        Some(body),
        httpDef,
        () =>
          webClient.flatMap {
            _.post()
              .uri(uriStr)
              .bodyValue(body)
              .retrieve()
              .bodyToMono(returnType)
              .toFuture
              .asScala
          }.asJava)
    }
  }

  override def post[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.POST, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        None,
        httpDef,
        () =>
          webClient.flatMap {
            _.post()
              .uri(uriStr)
              .retrieve()
              .bodyToMono(returnType)
              .toFuture
              .asScala
          }.asJava)
    }
  }

  override def put[P, R](uriStr: String, body: P, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.PUT, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        Some(body),
        httpDef,
        () =>
          webClient.flatMap {
            _.put()
              .uri(uriStr)
              .bodyValue(body)
              .retrieve()
              .bodyToMono(returnType)
              .toFuture
              .asScala
          }.asJava)
    }
  }

  override def put[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.PUT, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        None,
        httpDef,
        () =>
          webClient.flatMap {
            _.put()
              .uri(uriStr)
              .retrieve()
              .bodyToMono(returnType)
              .toFuture
              .asScala
          }.asJava)
    }
  }

  override def patch[P, R](uriStr: String, body: P, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.PATCH, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        Some(body),
        httpDef,
        () =>
          webClient.flatMap {
            _.patch()
              .uri(uriStr)
              .bodyValue(body)
              .retrieve()
              .bodyToMono(returnType)
              .toFuture
              .asScala
          }.asJava)
    }
  }

  override def patch[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.PATCH, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        None,
        httpDef,
        () =>
          webClient.flatMap {
            _.patch()
              .uri(uriStr)
              .retrieve()
              .bodyToMono(returnType)
              .toFuture
              .asScala
          }.asJava)
    }
  }

  override def delete[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = {
    matchMethodOrThrow(HttpMethods.DELETE, uriStr) { httpDef =>
      requestToRestDefCall(
        uriStr,
        None,
        httpDef,
        () =>
          webClient.flatMap {
            _.delete()
              .uri(uriStr)
              .retrieve()
              .bodyToMono(returnType)
              .toFuture
              .asScala
          }.asJava)
    }
  }

  private def matchMethodOrThrow[R](httpMethod: HttpMethod, uriStr: String)(
      createDefCall: => HttpEndpointMethodDefinition => RestDeferredCall[Any, R]) = {
    val uri = Uri(uriStr)
    services
      .find(d => (d.methodPattern == ANY_METHOD || httpMethod == d.methodPattern) && d.matches(uri.path))
      .map { createDefCall(_) }
      .getOrElse(throw HttpMethodNotFoundException(httpMethod, uri.path.toString()))
  }

  private def requestToRestDefCall[P, R](
      uriStr: String,
      body: Option[P],
      httpDef: HttpEndpointMethodDefinition,
      asyncCall: () => CompletionStage[R]): RestDeferredCall[Any, R] = {

    val uri = Uri(uriStr)
    val inputBuilder = DynamicMessage.newBuilder(httpDef.methodDescriptor.getInputType)

    httpDef.parsePathParametersInto(uri.path, inputBuilder)
    httpDef.parseRequestParametersInto(uri.query().toMultiMap, inputBuilder)

    val wrappedBody = buildWrappedBody(httpDef, inputBuilder, body)

    RestDeferredCall[Any, R](
      message = wrappedBody,
      metadata = MetadataImpl.Empty,
      fullServiceName = httpDef.methodDescriptor.getService.getFullName,
      methodName = httpDef.methodDescriptor.getName,
      asyncCall = asyncCall)
  }

}

final case class HttpMethodNotFoundException(httpMethod: HttpMethod, uriStr: String)
    extends RuntimeException(s"No matching service for method=$httpMethod path=$uriStr")




© 2015 - 2025 Weber Informatics LLC | Privacy Policy