diff --git a/core/src/main/scala/higherkindness/mu/rpc/srcgen/Model.scala b/core/src/main/scala/higherkindness/mu/rpc/srcgen/Model.scala index 2ab35741..fd0cd00c 100644 --- a/core/src/main/scala/higherkindness/mu/rpc/srcgen/Model.scala +++ b/core/src/main/scala/higherkindness/mu/rpc/srcgen/Model.scala @@ -76,6 +76,12 @@ object Model { } } + sealed trait ExecutionMode extends Product with Serializable + object ExecutionMode { + case object Compendium extends ExecutionMode + case object Local extends ExecutionMode + } + sealed trait IdlType extends Product with Serializable object IdlType { diff --git a/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumClient.scala b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumClient.scala new file mode 100644 index 00000000..11c1bcef --- /dev/null +++ b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumClient.scala @@ -0,0 +1,48 @@ +package higherkindness.mu.rpc.srcgen.compendium + +import cats.effect.Sync +import org.http4s._ +import org.http4s.circe._ +import cats.implicits._ +import org.http4s.client.{Client, UnexpectedStatus} + +trait CompendiumClient[F[_]] { + + /** Retrieve a Protocol by its id + * + * @param identifier the protocol identifier + * @param version optional protocol version number + * @return a protocol + */ + def retrieveProtocol(identifier: String, version: Option[Int]): F[Option[RawProtocol]] + +} + +object CompendiumClient { + + def apply[F[_]: Sync]( + clientF: Client[F], + clientConfig: HttpConfig + ): CompendiumClient[F] = + new CompendiumClient[F] { + + override def retrieveProtocol( + identifier: String, + version: Option[Int] + ): F[Option[RawProtocol]] = { + val versionParam = version.fold("")(v => s"?version=${v.show}") + val connectionUrl = s"${clientConfig.serverUrl}/v0/protocol/$identifier$versionParam" + + implicit val rawEntityDecoder = jsonOf[F, RawProtocol] + + clientF.get(connectionUrl)(res => + res.status match { + case Status.Ok => res.as[RawProtocol].map(Option(_)) + case Status.NotFound => Sync[F].pure(None) + case s => Sync[F].raiseError(UnexpectedStatus(s)) + } + ) + } + } + +} diff --git a/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumError.scala b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumError.scala new file mode 100644 index 00000000..e73ae55a --- /dev/null +++ b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumError.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2019-2020 47 Degrees, LLC. + * + * 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 higherkindness.mu.rpc.srcgen.compendium + +abstract class CompendiumError(error: String) extends Exception(error) + +final case class ProtocolNotFound(msg: String) extends CompendiumError(msg) +final case class SchemaError(msg: String) extends CompendiumError(msg) +final case class UnknownError(msg: String) extends CompendiumError(msg) diff --git a/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumMode.scala b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumMode.scala new file mode 100644 index 00000000..f61cec67 --- /dev/null +++ b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/CompendiumMode.scala @@ -0,0 +1,68 @@ +package higherkindness.mu.rpc.srcgen.compendium + +import java.io.{File, PrintWriter} + +import cats.effect.{ConcurrentEffect, Resource} + +import scala.util.Try +import cats.implicits._ +import org.http4s.client.blaze._ + +import scala.concurrent.ExecutionContext.global + +final case class ProtocolAndVersion(name: String, version: Option[String]) +final case class FilePrintWriter(file: File, pw: PrintWriter) + +final case class CompendiumMode[F[_]: ConcurrentEffect]( + protocols: List[ProtocolAndVersion], + fileType: String, + httpConfig: HttpConfig, + path: String +) { + + val httpClient = BlazeClientBuilder[F](global).resource + + def run(): F[List[File]] = + protocols.traverse(protocolAndVersion => + httpClient.use(client => { + for { + protocol <- CompendiumClient(client, httpConfig) + .retrieveProtocol( + protocolAndVersion.name, + safeInt(protocolAndVersion.version) + ) + file <- protocol match { + case Some(raw) => + writeTempFile( + raw.raw, + extension = fileType, + identifier = protocolAndVersion.name, + path = path + ) + case None => + ProtocolNotFound(s"Protocol ${protocolAndVersion.name} not found in Compendium. ") + .raiseError[F, File] + } + } yield file + }) + ) + + private def safeInt(s: Option[String]): Option[Int] = s.flatMap(str => Try(str.toInt).toOption) + + private def writeTempFile( + msg: String, + extension: String, + identifier: String, + path: String + ): F[File] = + Resource + .make(ConcurrentEffect[F].delay { + if (!new File(path).exists()) new File(path).mkdirs() + val file = new File(path + s"/$identifier.$extension") + file.deleteOnExit() + FilePrintWriter(file, new PrintWriter(file)) + }) { fpw: FilePrintWriter => ConcurrentEffect[F].delay(fpw.pw.close()) } + .use((fpw: FilePrintWriter) => ConcurrentEffect[F].delay(fpw.pw.write(msg)).as(fpw)) + .map(_.file) + +} diff --git a/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/ErrorResponse.scala b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/ErrorResponse.scala new file mode 100644 index 00000000..79e83167 --- /dev/null +++ b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/ErrorResponse.scala @@ -0,0 +1,11 @@ +package higherkindness.mu.rpc.srcgen.compendium + +import io.circe.{Decoder, Encoder} +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} + +final case class ErrorResponse(message: String) + +object ErrorResponse { + implicit val decoder: Decoder[ErrorResponse] = deriveDecoder[ErrorResponse] + implicit val encoder: Encoder[ErrorResponse] = deriveEncoder[ErrorResponse] +} diff --git a/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/HttpConfig.scala b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/HttpConfig.scala new file mode 100644 index 00000000..81e804ed --- /dev/null +++ b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/HttpConfig.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2019-2020 47 Degrees, LLC. + * + * 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 higherkindness.mu.rpc.srcgen.compendium + +final case class HttpConfig(serverUrl: String) diff --git a/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/RawProtocol.scala b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/RawProtocol.scala new file mode 100644 index 00000000..f6efdbc3 --- /dev/null +++ b/core/src/main/scala/higherkindness/mu/rpc/srcgen/compendium/RawProtocol.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2019-2020 47 Degrees, LLC. + * + * 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 higherkindness.mu.rpc.srcgen.compendium + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + +final case class RawProtocol(raw: String) + +object RawProtocol { + implicit val decoder: Decoder[RawProtocol] = deriveDecoder[RawProtocol] + implicit val encoder: Encoder[RawProtocol] = deriveEncoder[RawProtocol] + +} diff --git a/plugin/src/main/scala/higherkindness/mu/rpc/srcgen/SrcGenPlugin.scala b/plugin/src/main/scala/higherkindness/mu/rpc/srcgen/SrcGenPlugin.scala index fa3171f7..d1213ad9 100644 --- a/plugin/src/main/scala/higherkindness/mu/rpc/srcgen/SrcGenPlugin.scala +++ b/plugin/src/main/scala/higherkindness/mu/rpc/srcgen/SrcGenPlugin.scala @@ -18,13 +18,17 @@ package higherkindness.mu.rpc.srcgen import java.io.File +import cats.effect.{ContextShift, IO => IOCats} +import higherkindness.mu.rpc.srcgen.Model.ExecutionMode._ import sbt.Keys._ -import sbt._ +import sbt.{Def, settingKey, _} import sbt.io.{Path, PathFinder} - import higherkindness.mu.rpc.srcgen.Model._ +import higherkindness.mu.rpc.srcgen.compendium.{CompendiumMode, HttpConfig, ProtocolAndVersion} import higherkindness.mu.rpc.srcgen.openapi.OpenApiSrcGenerator.HttpImpl +import scala.concurrent.ExecutionContext.global + object SrcGenPlugin extends AutoPlugin { override def trigger: PluginTrigger = allRequirements @@ -105,6 +109,18 @@ object SrcGenPlugin extends AutoPlugin { "By default, the streaming implementation is FS2 Stream." ) + lazy val muSrcGenExecutionMode = settingKey[ExecutionMode]( + "Execution mode of the plugin. If Compendium, it's required a compendium instance where IDL files are saved. `Local` by default." + ) + + lazy val muSrcGenCompendiumProtocolIdentifiers: SettingKey[Seq[ProtocolAndVersion]] = + settingKey[Seq[ProtocolAndVersion]]( + "Protocol identifiers (and version) to be retrieved from compendium server. By default is an empty list." + ) + + lazy val muSrcGenCompendiumServerUrl: SettingKey[String] = + settingKey[String]("Url of the compendium server. By default, `http://localhost:47047`.") + } import autoImport._ @@ -141,7 +157,10 @@ object SrcGenPlugin extends AutoPlugin { muSrcGenCompressionType := NoCompressionGen, muSrcGenIdiomaticEndpoints := false, muSrcGenOpenApiHttpImpl := HttpImpl.Http4sV20, - muSrcGenStreamingImplementation := Fs2Stream + muSrcGenStreamingImplementation := Fs2Stream, + muSrcGenExecutionMode := Local, + muSrcGenCompendiumProtocolIdentifiers := Nil, + muSrcGenCompendiumServerUrl := "http://localhost:47047" ) lazy val taskSettings: Seq[Def.Setting[_]] = { @@ -159,16 +178,30 @@ object SrcGenPlugin extends AutoPlugin { ) }, Def.task { - muSrcGenSourceDirs.value.toSet.foreach { f: File => - IO.copyDirectory( - f, - muSrcGenIdlTargetDir.value, - CopyOptions( - overwrite = true, - preserveLastModified = true, - preserveExecutable = true - ) - ) + muSrcGenExecutionMode.value match { + case Compendium => + implicit val cs: ContextShift[IOCats] = IOCats.contextShift(global) + CompendiumMode[IOCats]( + muSrcGenCompendiumProtocolIdentifiers.value.toList, + muSrcGenIdlExtension.value, + HttpConfig( + muSrcGenCompendiumServerUrl.value + ), + muSrcGenIdlTargetDir.value.getAbsolutePath + ).run() + .unsafeRunSync() + case Local => + muSrcGenSourceDirs.value.toSet.foreach { f: File => + IO.copyDirectory( + f, + muSrcGenIdlTargetDir.value, + CopyOptions( + overwrite = true, + preserveLastModified = true, + preserveExecutable = true + ) + ) + } } }, Def.task { diff --git a/plugin/src/sbt-test/sbt-mu-srcgen/compendium/build.sbt b/plugin/src/sbt-test/sbt-mu-srcgen/compendium/build.sbt new file mode 100644 index 00000000..0be6b6d7 --- /dev/null +++ b/plugin/src/sbt-test/sbt-mu-srcgen/compendium/build.sbt @@ -0,0 +1,5 @@ +version := sys.props("version") + +libraryDependencies ++= Seq( + "io.higherkindness" %% "mu-rpc-server" % sys.props("mu") +) diff --git a/plugin/src/sbt-test/sbt-mu-srcgen/compendium/project/plugins.sbt b/plugin/src/sbt-test/sbt-mu-srcgen/compendium/project/plugins.sbt new file mode 100644 index 00000000..2e452245 --- /dev/null +++ b/plugin/src/sbt-test/sbt-mu-srcgen/compendium/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("io.higherkindness" %% "sbt-mu-srcgen" % sys.props("version")) diff --git a/plugin/src/sbt-test/sbt-mu-srcgen/compendium/test b/plugin/src/sbt-test/sbt-mu-srcgen/compendium/test new file mode 100644 index 00000000..ecdf572c --- /dev/null +++ b/plugin/src/sbt-test/sbt-mu-srcgen/compendium/test @@ -0,0 +1,7 @@ +> 'set muSrcGenExecutionMode := higherkindness.mu.rpc.srcgen.Model.ExecutionMode.Compendium' +> 'set muSrcGenIdlType := higherkindness.mu.rpc.srcgen.Model.IdlType.Proto' +> 'set muSrcGenCompendiumProtocolIdentifiers := Seq(higherkindness.mu.rpc.srcgen.compendium.ProtocolAndVersion.apply("shop",None))' +> 'set muSrcGenCompendiumServerUrl := "http://localhost:8080"' +# > compile +# $ exists target/scala-2.12/resource_managed/main/proto/shop.proto + diff --git a/project/ProjectPlugin.scala b/project/ProjectPlugin.scala index 83b03b04..ab3c1620 100644 --- a/project/ProjectPlugin.scala +++ b/project/ProjectPlugin.scala @@ -24,6 +24,7 @@ object ProjectPlugin extends AutoPlugin { val scalatestplusScheck: String = "3.1.0.0-RC2" val skeuomorph: String = "0.0.23" val slf4j: String = "1.7.30" + val http4s: String = "0.21.4" } lazy val srcGenSettings: Seq[Def.Setting[_]] = Seq( @@ -33,6 +34,8 @@ object ProjectPlugin extends AutoPlugin { "io.higherkindness" %% "skeuomorph" % V.skeuomorph, "com.julianpeeters" %% "avrohugger-core" % V.avrohugger, "io.circe" %% "circe-generic" % V.circe, + "org.http4s" %% "http4s-blaze-client" % V.http4s, + "org.http4s" %% "http4s-circe" % V.http4s, "org.scalatest" %% "scalatest" % V.scalatest % Test, "org.scalacheck" %% "scalacheck" % V.scalacheck % Test, "org.scalatestplus" %% "scalatestplus-scalacheck" % V.scalatestplusScheck % Test,