From cc94d6f4e3f9ff667b979b8aaad131da5c84449a Mon Sep 17 00:00:00 2001 From: Hossein Naderi Date: Sat, 24 Sep 2022 19:49:10 +0330 Subject: [PATCH 1/2] Added sttp client --- .github/workflows/ci.yml | 4 +- build.sbt | 20 +++- example/src/main/scala/SttpMain.scala | 36 +++++++ .../src/main/scala/SttpKubernetesClient.scala | 102 ++++++++++++++++++ 4 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 example/src/main/scala/SttpMain.scala create mode 100644 modules/sttp/src/main/scala/SttpKubernetesClient.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 068028f3..942d65bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,11 +119,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p modules/json4s/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target unidocs/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target .jvm/target .native/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/jawn/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/scalacheck/.js/target project/target + run: mkdir -p modules/json4s/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target unidocs/target modules/sttp/.native/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target .jvm/target .native/target modules/sttp/.js/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/sttp/.jvm/target modules/jawn/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/scalacheck/.js/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar modules/json4s/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target unidocs/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target .jvm/target .native/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/jawn/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/scalacheck/.js/target project/target + run: tar cf targets.tar modules/json4s/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target unidocs/target modules/sttp/.native/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target .jvm/target .native/target modules/sttp/.js/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/sttp/.jvm/target modules/jawn/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/scalacheck/.js/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') diff --git a/build.sbt b/build.sbt index 663dbaef..7abc02c5 100644 --- a/build.sbt +++ b/build.sbt @@ -36,6 +36,7 @@ lazy val root = client, http4s, zio, + sttp, codecTest, circe, `spray-json`, @@ -96,6 +97,18 @@ lazy val http4s = module("http4s") { .dependsOn(client, jawn) } +lazy val sttp = module("sttp") { + crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .settings( + description := "sttp based client for kubernetes", + libraryDependencies ++= Seq( + "com.softwaremill.sttp.client3" %%% "core" % "3.8.0" + ) + ) + .dependsOn(client, jawn) +} + lazy val zio = module("zio") { crossProject(JVMPlatform) .crossType(CrossType.Pure) @@ -253,6 +266,7 @@ lazy val docs = project "ZIO" -> url("https://github.com/zio/zio"), "ZIO-http" -> url("https://github.com/zio/zio-http"), "ZIO-json" -> url("https://github.com/zio/zio-json"), + "sttp" -> url("https://sttp.softwaremill.com"), "Circe" -> url("https://github.com/circe/circe"), "Spray json" -> url("https://github.com/spray/spray-json"), "Play json" -> url("https://github.com/playframework/play-json"), @@ -276,6 +290,7 @@ lazy val unidocs = project client.jvm, http4s.jvm, zio.jvm, + sttp.jvm, circe.jvm, `spray-json`.jvm, `play-json`.jvm, @@ -291,10 +306,11 @@ lazy val example = crossProject(JVMPlatform) .crossType(CrossType.Pure) .settings( libraryDependencies ++= Seq( - "org.http4s" %%% "http4s-circe" % "0.23.16" + "org.http4s" %%% "http4s-circe" % "0.23.16", + "com.softwaremill.sttp.client3" %%% "circe" % "3.8.0" ) ) - .dependsOn(http4s, circe, zio) + .dependsOn(http4s, circe, zio, sttp) .enablePlugins(NoPublishPlugin) def addAlias(name: String)(tasks: String*) = diff --git a/example/src/main/scala/SttpMain.scala b/example/src/main/scala/SttpMain.scala new file mode 100644 index 00000000..7d90c2a7 --- /dev/null +++ b/example/src/main/scala/SttpMain.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Hossein Naderi + * + * 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 test + +import dev.hnaderi.k8s.circe._ +import dev.hnaderi.k8s.client.APIs +import dev.hnaderi.k8s.client.SttpKubernetesClient +import io.circe.Json +import sttp.client3._ +import sttp.client3.circe._ + +object SttpMain extends App { + val simpleBackend = HttpURLConnectionBackend() + + val client = new SttpKubernetesClient[Identity, Json]( + "http://localhost:8001", + simpleBackend + ) + + val nodes = APIs.nodes.list().send(client) + nodes.body.items.flatMap(_.metadata).flatMap(_.name).foreach(println) +} diff --git a/modules/sttp/src/main/scala/SttpKubernetesClient.scala b/modules/sttp/src/main/scala/SttpKubernetesClient.scala new file mode 100644 index 00000000..4b306262 --- /dev/null +++ b/modules/sttp/src/main/scala/SttpKubernetesClient.scala @@ -0,0 +1,102 @@ +/* + * Copyright 2022 Hossein Naderi + * + * 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 dev.hnaderi.k8s.client + +import dev.hnaderi.k8s.jawn.jawnFacade +import dev.hnaderi.k8s.utils._ +import org.typelevel.jawn.Facade +import org.typelevel.jawn.Parser.parseFromByteArray +import sttp.client3._ +import sttp.model.Uri + +import SttpKubernetesClient._ + +final class SttpKubernetesClient[F[_], T: Builder: Reader]( + serverUrl: String, + client: SttpBackend[F, Any] +)(implicit serializer: BodySerializer[T]) + extends HttpClient[SttpF[F, *]] { + + private implicit val jawn: Facade.SimpleFacade[T] = jawnFacade[T] + private val ra: ResponseAs[Either[Throwable, T], Any] = + ResponseAsByteArray.map(parseFromByteArray[T](_).toEither) + private def respAs[O: Decoder]: ResponseAs[O, Any] = + ra.map(_.flatMap(_.decodeTo[O].left.map(DecodeError(_)))).getRight + private def urlFor(url: String, params: Seq[(String, String)]) = + Uri + .parse(s"$serverUrl$url") + .fold(err => throw InvalidURL(err), identity) + .addParams(params: _*) + + private implicit def bodyEncoder[O: Encoder]: BodySerializer[O] = + serializer.compose(_.encodeTo) + + override def get[O: Decoder]( + url: String, + params: (String, String)* + ): F[Response[O]] = + basicRequest + .get(urlFor(url, params)) + .response(respAs[O]) + .send(client) + + override def post[I: Encoder, O: Decoder]( + url: String, + params: (String, String)* + )(body: I): F[Response[O]] = + basicRequest + .post(urlFor(url, params)) + .body(body) + .response(respAs[O]) + .send(client) + + override def put[I: Encoder, O: Decoder]( + url: String, + params: (String, String)* + )(body: I): F[Response[O]] = + basicRequest + .put(urlFor(url, params)) + .body(body) + .response(respAs[O]) + .send(client) + + override def patch[I: Encoder, O: Decoder]( + url: String, + params: (String, String)* + )(body: I): F[Response[O]] = + basicRequest + .patch(urlFor(url, params)) + .body(body) + .response(respAs[O]) + .send(client) + + override def delete[O: Decoder]( + url: String, + params: (String, String)* + ): F[Response[O]] = + basicRequest + .delete(urlFor(url, params)) + .response(respAs[O]) + .send(client) + +} + +object SttpKubernetesClient { + type SttpF[F[_], T] = F[Response[T]] + final case class DecodeError(msg: String) extends Exception(msg) + final case class InvalidURL(msg: String) extends Exception(msg) +} From eb41f98bb8b8f41e32f6e4fa44e840ce1a2e8745 Mon Sep 17 00:00:00 2001 From: Hossein Naderi Date: Mon, 26 Sep 2022 15:38:19 +0330 Subject: [PATCH 2/2] Added sttp docs and examples --- README.md | 3 ++- build.sbt | 5 +++-- docs/index.md | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a33491db..e88f0cd8 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ The following integrations are currently available: libraryDependencies ++= Seq( "dev.hnaderi" %% "scala-k8s-http4s" % "@VERSION@", // JVM, JS, Native ; http4s and fs2 integration "dev.hnaderi" %% "scala-k8s-zio" % "@VERSION@", // JVM ; ZIO native integration using zio-http and zio-json + "dev.hnaderi" %% "scala-k8s-sttp" % "@VERSION@", // JVM, JS, Native ; sttp integration using jawn parser "dev.hnaderi" %% "scala-k8s-circe" % "@VERSION@", // JVM, JS ; circe integration "dev.hnaderi" %% "scala-k8s-json4s" % "@VERSION@", // JVM, JS, Native; json4s integration "dev.hnaderi" %% "scala-k8s-spray-json" % "@VERSION@", // JVM ; spray-json integration @@ -68,6 +69,6 @@ visit [project site](https://projects.hnaderi.dev/scala-k8s) to see more tutoria see [this project](https://github.com/hnaderi/sbt-k8s) ## Future plans -- more integrations (ZIO, sttp, akka-http)! +- more integrations (akka-http, ...)! - more requests and options - direct TLS support diff --git a/build.sbt b/build.sbt index 7abc02c5..5cd28ccf 100644 --- a/build.sbt +++ b/build.sbt @@ -274,10 +274,11 @@ lazy val docs = project "Jawn" -> url("https://github.com/typelevel/jawn") ), libraryDependencies ++= Seq( - "org.http4s" %%% "http4s-circe" % "0.23.16" + "org.http4s" %%% "http4s-circe" % "0.23.16", + "com.softwaremill.sttp.client3" %%% "circe" % "3.8.0" ) ) - .dependsOn(http4s.jvm, circe.jvm, manifests.jvm) + .dependsOn(http4s.jvm, sttp.jvm, circe.jvm, manifests.jvm) lazy val unidocs = project .in(file("unidocs")) diff --git a/docs/index.md b/docs/index.md index b8091999..bce9fe9f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,7 @@ The following integrations are currently available: libraryDependencies ++= Seq( "dev.hnaderi" %% "scala-k8s-http4s" % "@VERSION@", // JVM, JS, Native ; http4s and fs2 integration "dev.hnaderi" %% "scala-k8s-zio" % "@VERSION@", // JVM ; ZIO native integration using zio-http and zio-json + "dev.hnaderi" %% "scala-k8s-sttp" % "@VERSION@", // JVM, JS, Native ; sttp integration using jawn parser "dev.hnaderi" %% "scala-k8s-circe" % "@VERSION@", // JVM, JS ; circe integration "dev.hnaderi" %% "scala-k8s-json4s" % "@VERSION@", // JVM, JS, Native; json4s integration "dev.hnaderi" %% "scala-k8s-spray-json" % "@VERSION@", // JVM ; spray-json integration @@ -213,6 +214,26 @@ val client = ZIOKubernetesClient.make("http://localhost:8001") val nodes = ZIOKubernetesClient.send(APIs.nodes.list()) ``` +### Sttp based client +```scala mdoc:compile-only +import dev.hnaderi.k8s.circe._ +import dev.hnaderi.k8s.client.APIs +import dev.hnaderi.k8s.client.SttpKubernetesClient +import io.circe.Json +import sttp.client3._ +import sttp.client3.circe._ + +val simpleBackend = HttpURLConnectionBackend() + +val client = new SttpKubernetesClient[Identity, Json]( + "http://localhost:8001", + simpleBackend +) + +val nodes = APIs.nodes.list().send(client) +nodes.body.items.flatMap(_.metadata).flatMap(_.name).foreach(println) +``` + ### Working with requests Requests are plain data, so you can manipulate or pass them like any normal data