Skip to content

Commit

Permalink
feat: Add OpenAPI generation (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlemaitre authored Mar 13, 2024
1 parent 3a656c8 commit a6a482f
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 15 deletions.
32 changes: 18 additions & 14 deletions modules/core/src/main/scala/pillars/app.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package pillars

import cats.data.Validated
import cats.effect.*
import cats.effect.std.Console
import com.monovore.decline.Argument
import com.monovore.decline.Command
import com.monovore.decline.Opts
import fs2.io.file.Path
Expand All @@ -13,11 +11,13 @@ import pillars.App.Description
import pillars.App.Name
import pillars.App.Version
import pillars.probes.Probe
import sttp.tapir.AnyEndpoint

trait App[F[_]]:
def infos: AppInfo
def probes: List[Probe[F]] = Nil
def adminControllers: List[Controller[F]] = Nil
def endpoints: List[AnyEndpoint]
def run: Run[F, F[Unit]]
end App

Expand Down Expand Up @@ -47,18 +47,22 @@ trait BuildInfo:
end BuildInfo

trait EntryPoint extends IOApp:
given Argument[Path] with
def read(string: String) = Validated.valid(Path(string))
def defaultMetavar = "path"

import pillars.given
def app: App[IO]
override final def run(args: List[String]): IO[ExitCode] =
Command(app.infos.name, app.infos.description)(
Opts.option[Path]("config", "Path to the configuration file")
).parse(args, sys.env) match
case Left(help) => Console[IO].errorln(help).as(ExitCode.Error)
case Right(configPath) =>
Pillars(configPath).use: pillars =>
given Pillars[IO] = pillars
app.run.as(ExitCode.Success)
val command = Command(app.infos.name, app.infos.description):
val configFile =
Opts.option[Path]("config", "Path to the configuration file").map: configPath =>
Pillars(configPath).use: pillars =>
given Pillars[IO] = pillars
app.run.as(ExitCode.Success)
val openAPICmd =
Opts.subcommand(openapi.command).map: args =>
openapi.Generator(app).generate(args).as(ExitCode.Success)
configFile orElse openAPICmd

command.parse(args, sys.env) match
case Left(help) => Console[IO].errorln(help).as(ExitCode.Error)
case Right(prog) => prog
end run
end EntryPoint
9 changes: 9 additions & 0 deletions modules/core/src/main/scala/pillars/commands.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pillars

import com.monovore.decline.Command

val openApiGenerator = Command(
name = "openapi-generator",
header = "Generates a client from an OpenAPI specification"
):
???
42 changes: 42 additions & 0 deletions modules/core/src/main/scala/pillars/openapi.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package pillars

import cats.effect.IO
import cats.syntax.all.*
import com.monovore.decline.*
import fs2.Stream
import fs2.io.file.Files
import fs2.io.file.Path
import fs2.text
import java.net.URI
import sttp.apispec.openapi.circe.yaml.*
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter

object openapi:

val command = Command("openapi", "Generate OpenAPI documentation"):
val output = Opts.option[Path]("output", "Output file").orNone
val servers: Opts[List[URI]] =
Opts.options[URI]("server", "Server URL").map(_.toList).withDefault(Nil)
(output, servers).mapN(Generator.Args.apply)
final case class Generator(app: App[IO]):
def generate(args: Generator.Args): IO[Unit] =
val yaml = OpenAPIDocsInterpreter()
.toOpenAPI(app.endpoints, app.infos.name, app.infos.version)
.toYaml
println(yaml)
args.output match
case Some(path) =>
Stream.emit(yaml)
.covary[IO]
.through(text.utf8.encode)
.through(Files[IO].writeAll(path))
.compile
.drain
case None => IO.println(yaml)
end match
end generate
end Generator

object Generator:
final case class Args(output: Option[Path], servers: List[URI])
end openapi
9 changes: 9 additions & 0 deletions modules/core/src/main/scala/pillars/package.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import cats.data.Validated
import com.monovore.decline.Argument
import fs2.io.file.Path

package object pillars:
given Argument[Path] with
def read(string: String) = Validated.valid(Path(string))

def defaultMetavar = "path"

/**
* Type alias for a Pillars[F] context bound.
*
Expand Down
2 changes: 2 additions & 0 deletions modules/example/src/main/scala/example/app.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ object app extends pillars.EntryPoint: // // <1>
def app: pillars.App[IO] = new: // // <2>
def infos: AppInfo = BuildInfo.toAppInfo // // <3>

def endpoints = TodoController().endpoints.map(_.endpoint)

def run: Run[IO, IO[Unit]] = // // <4>
for
_ <- Logger[IO].info(s"📚 Welcome to ${Config[IO].name}!")
Expand Down
2 changes: 2 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ object Dependencies {
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.9.11",
"com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-metrics" % "1.9.11",
"com.softwaremill.sttp.tapir" %% "tapir-iron" % "1.9.11",
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.9.11",
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.7.4",
"com.softwaremill.sttp.tapir" %% "tapir-http4s-client" % "1.9.11" % Test,
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.9.11" % Test,
"com.softwaremill.sttp.client3" %% "core" % "3.9.4" % Test
Expand Down
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0")
addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.0.1")

addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")

0 comments on commit a6a482f

Please sign in to comment.