Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sbt plugin rewrite #933

Merged
merged 19 commits into from
Jun 26, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8d952c9
Updating regex in Codegen.scala to support ABI-suffixed source direct…
blast-hardcheese Jun 21, 2021
44455b3
Integrating changes to caliban-codegen-sbt
blast-hardcheese Jun 21, 2021
6c571c3
Consider both packageName and clientName in generated source path
blast-hardcheese Jun 21, 2021
167bfda
If we neglect to supply a packageName in the caliban-codegen-sbt sett…
blast-hardcheese Jun 21, 2021
1e94139
Adding support for src/test/graphql to codegen-sbt
blast-hardcheese Jun 21, 2021
4bb2567
Updating documentation to reflect new sbt plugin infra
blast-hardcheese Jun 21, 2021
68a3a6b
Auto-generate at least one scripted source file
blast-hardcheese Jun 21, 2021
1161ca3
Merging scripted tests together
blast-hardcheese Jun 21, 2021
020ee1f
Adding a deprecated alias for CodegenPlugin
blast-hardcheese Jun 21, 2021
a5aeec0
Converting schema.graphql genView test over to use the genView caliba…
blast-hardcheese Jun 21, 2021
e38c32b
Splitting CalibanSettings out into CalibanFileSettings
blast-hardcheese Jun 21, 2021
8ed045e
Adding support for calibanSetting(url(...))(...)
blast-hardcheese Jun 21, 2021
eb81c6f
Documenting URL generation
blast-hardcheese Jun 21, 2021
24a9161
Fixin' typos
blast-hardcheese Jun 22, 2021
2f56800
Trim down tests
blast-hardcheese Jun 22, 2021
c5a7c17
Rename section headers
blast-hardcheese Jun 22, 2021
4dc71eb
Reword docs
blast-hardcheese Jun 22, 2021
b777f37
Effect is not defined for clients
blast-hardcheese Jun 22, 2021
785add1
Documenting CalibanSettings fields
blast-hardcheese Jun 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import sbt.{ AutoPlugin, Command, State }
import zio.console.{ putStrLn, Console }
import zio.{ RIO, Runtime }

object CodegenPlugin extends AutoPlugin {
override lazy val projectSettings = Seq(commands ++= Seq(genSchemaCommand, genClientCommand))

object CalibanCli {
lazy val genSchemaCommand =
genCommand(
"calibanGenSchema",
Expand Down Expand Up @@ -90,4 +88,6 @@ object CodegenPlugin extends AutoPlugin {
} yield ()
case None => putStrLn(helpMsg)
}

def projectSettings = Seq(commands ++= Seq(genSchemaCommand, genClientCommand))
}
22 changes: 22 additions & 0 deletions codegen-sbt/src/main/scala/caliban/codegen/CalibanKeys.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package caliban.codegen

import sbt._
import sbt.Keys._

import java.net.URL

trait CalibanKeys {
lazy val caliban = taskKey[Seq[File]]("Generate GraphQL sources using caliban-codegen-sbt")
lazy val calibanGenerator = taskKey[Seq[File]]("Generate GraphQL sources using caliban-codegen-sbt")
lazy val calibanSources = settingKey[File]("Where to find .graphql schemas")
lazy val calibanSettings = settingKey[Seq[CalibanSettings]]("Settings that apply to individual GraphQL files")
def calibanSetting(file: File)(setting: CalibanFileSettings => CalibanFileSettings): CalibanSettings =
setting.apply(CalibanSettings.emptyFile(file))
def calibanSetting(url: URL)(setting: CalibanUrlSettings => CalibanUrlSettings): CalibanSettings =
setting.apply(CalibanSettings.emptyUrl(url))

@deprecated("CodegenPlugin has been renamed to CalibanPlugin", "1.1.0")
val CodegenPlugin: CalibanPlugin.type = CalibanPlugin
}

object CalibanKeys extends CalibanKeys
51 changes: 51 additions & 0 deletions codegen-sbt/src/main/scala/caliban/codegen/CalibanPlugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package caliban.codegen

import sbt._
import sbt.Keys._
import _root_.sbt.util.CacheImplicits._

object CalibanPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin

object autoImport extends CalibanKeys
import autoImport._

lazy val baseSettings = Seq(
caliban := (caliban / calibanGenerator).value,
(caliban / sourceManaged) := {
sourceManaged.value / "caliban-codegen-sbt"
},
(caliban / calibanSources) := {
if (Seq(Compile, Test).contains(configuration.value)) sourceDirectory.value / "graphql"
else sourceDirectory.value / "main" / "graphql"
},
caliban / calibanSettings := Seq.empty
)

lazy val calibanScopedSettings = inTask(caliban)(
Seq(
sources := (calibanSources.value ** "*.graphql").get.sorted,
clean := {
val sourceDir = sourceManaged.value
IO.delete((sourceDir ** "*").get)
IO.createDirectory(sourceDir)
},
calibanGenerator := CalibanSourceGenerator(
calibanSources.value,
sources.value,
sourceManaged.value,
streams.value.cacheDirectory,
calibanSettings.value.collect { case x: CalibanFileSettings => x },
calibanSettings.value.collect { case x: CalibanUrlSettings => x }
)
)
)

lazy val allSettings = baseSettings ++ calibanScopedSettings

override lazy val projectSettings: Seq[Def.Setting[_]] =
CalibanCli.projectSettings ++ inConfig(Compile)(allSettings) ++ inConfig(Test)(allSettings) ++ Seq(
Compile / sourceGenerators += (Compile / caliban).taskValue,
Test / sourceGenerators += (Test / caliban).taskValue
)
}
117 changes: 117 additions & 0 deletions codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package caliban.codegen

import java.io.File
import java.net.URL

sealed trait CalibanSettings {
type Type <: CalibanSettings

def clientName: Option[String]
def scalafmtPath: Option[String]
def headers: Seq[(String, String)]
def packageName: Option[String]
def genView: Option[Boolean]
def effect: Option[String]
def scalarMappings: Seq[(String, String)]
def imports: Seq[String]

def append(other: Type): Type
}

case class CalibanFileSettings(
file: File,
clientName: Option[String],
scalafmtPath: Option[String],
packageName: Option[String],
genView: Option[Boolean],
effect: Option[String],
scalarMappings: Seq[(String, String)],
imports: Seq[String]
) extends CalibanSettings {
type Type = CalibanFileSettings
val headers = Seq.empty // Not applicable for file generator

def append(other: CalibanFileSettings): CalibanFileSettings =
CalibanFileSettings(
file = file,
clientName = other.clientName.orElse(clientName),
scalafmtPath = other.scalafmtPath.orElse(scalafmtPath),
packageName = other.packageName.orElse(packageName),
genView = other.genView.orElse(genView),
effect = other.effect.orElse(effect),
scalarMappings = scalarMappings ++ other.scalarMappings,
imports = imports ++ other.imports
)

def clientName(value: String): CalibanFileSettings = this.copy(clientName = Some(value))
def scalafmtPath(path: String): CalibanFileSettings = this.copy(scalafmtPath = Some(path))
def packageName(name: String): CalibanFileSettings = this.copy(packageName = Some(name))
def genView(value: Boolean): CalibanFileSettings = this.copy(genView = Some(value))
def effect(tpe: String): CalibanFileSettings = this.copy(effect = Some(tpe))
def scalarMapping(mapping: (String, String)*): CalibanFileSettings =
this.copy(scalarMappings = this.scalarMappings ++ mapping)
def imports(values: String*): CalibanFileSettings = this.copy(imports = this.imports ++ values)
}

case class CalibanUrlSettings(
url: URL,
clientName: Option[String],
scalafmtPath: Option[String],
headers: Seq[(String, String)],
packageName: Option[String],
genView: Option[Boolean],
effect: Option[String],
scalarMappings: Seq[(String, String)],
imports: Seq[String]
) extends CalibanSettings {
type Type = CalibanUrlSettings
def append(other: CalibanUrlSettings): CalibanUrlSettings =
CalibanUrlSettings(
url = url,
clientName = other.clientName.orElse(clientName),
scalafmtPath = other.scalafmtPath.orElse(scalafmtPath),
headers = headers ++ other.headers,
packageName = other.packageName.orElse(packageName),
genView = other.genView.orElse(genView),
effect = other.effect.orElse(effect),
scalarMappings = scalarMappings ++ other.scalarMappings,
imports = imports ++ other.imports
)

def clientName(value: String): CalibanUrlSettings = this.copy(clientName = Some(value))
def scalafmtPath(path: String): CalibanUrlSettings = this.copy(scalafmtPath = Some(path))
def headers(mapping: (String, String)*): CalibanUrlSettings =
this.copy(headers = this.headers ++ mapping)
def packageName(name: String): CalibanUrlSettings = this.copy(packageName = Some(name))
def genView(value: Boolean): CalibanUrlSettings = this.copy(genView = Some(value))
def effect(tpe: String): CalibanUrlSettings = this.copy(effect = Some(tpe))
def scalarMapping(mapping: (String, String)*): CalibanUrlSettings =
this.copy(scalarMappings = this.scalarMappings ++ mapping)
def imports(values: String*): CalibanUrlSettings = this.copy(imports = this.imports ++ values)
}

object CalibanSettings {
type Transformer = CalibanSettings => CalibanSettings
def emptyFile(file: File) = CalibanFileSettings(
file = file,
clientName = Option.empty[String],
scalafmtPath = Option.empty[String],
packageName = Option.empty[String],
genView = Option.empty[Boolean],
effect = Option.empty[String],
scalarMappings = Seq.empty[(String, String)],
imports = Seq.empty[String]
)

def emptyUrl(url: URL) = CalibanUrlSettings(
url = url,
clientName = Option.empty[String],
scalafmtPath = Option.empty[String],
headers = Seq.empty[(String, String)],
packageName = Option.empty[String],
genView = Option.empty[Boolean],
effect = Option.empty[String],
scalarMappings = Seq.empty[(String, String)],
imports = Seq.empty[String]
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package caliban.codegen

import sbt._
import sbt.Keys._

import _root_.caliban.tools.Codegen.GenType
import _root_.caliban.tools._
import _root_.caliban.tools.implicits.ScalarMappings

import java.io.{ File, PrintWriter }
import java.net.URL

object CalibanSourceGenerator {
import zio._
import zio.console._

def transformFile(sourceRoot: File, managedRoot: File, settings: CalibanSettings): File => File = { graphqlFile =>
val relativePath = settings.packageName.fold(sourceRoot.toPath.relativize(graphqlFile.toPath)) { pkg =>
val components = pkg.split('.').toList.map(file(_).toPath) :+ graphqlFile.toPath.getFileName()
components.reduceLeft(_.resolve(_))
}
val interimPath = managedRoot.toPath.resolve(relativePath)
val clientName = settings.clientName.getOrElse(interimPath.getFileName().toString().stripSuffix(".graphql"))
val scalaName = clientName + ".scala"
interimPath.getParent.resolve(scalaName).toFile
}

def apply(
sourceRoot: File,
sources: Seq[File],
sourceManaged: File,
cacheDirectory: File,
fileSettings: Seq[CalibanFileSettings],
urlSettings: Seq[CalibanUrlSettings]
): List[File] = {
import sbt.util.CacheImplicits._

def collectSettingsFor(source: File): CalibanFileSettings =
// Supply a default packageName.
// If we do not, `src_managed.main.caliban-codegen-sbt` will be used,
// which is not only terrible, but invalid.
CalibanSettings
.emptyFile(source)
.packageName("caliban")
.append(
fileSettings
.collect({ case needle if source.toPath.endsWith(needle.file.toPath) => needle })
.foldLeft[CalibanFileSettings](CalibanSettings.emptyFile(source)) { case (acc, next) =>
acc.append(next)
}
)

def renderArgs(settings: CalibanSettings): List[String] = {
def singleOpt(opt: String, value: Option[String]): List[String] =
if (value.nonEmpty) {
opt :: value.toList
} else Nil
def pairList(opt: String, values: Seq[(String, String)]): List[String] =
if (values.nonEmpty) {
opt :: values.map({ case (fst, snd) => s"${fst}:${snd}" }).mkString(",") :: Nil
} else Nil
def list(opt: String, values: Seq[String]): List[String] =
if (values.nonEmpty) {
opt :: values.mkString(",") :: Nil
} else Nil
val scalafmtPath = singleOpt("--scalafmtPath", settings.scalafmtPath)
val headers = pairList("--headers", settings.headers)
val packageName = singleOpt(
"--packageName",
settings.packageName
)

val genView = singleOpt(
"--genView",
settings.genView.map(_.toString())
) // NB: Presuming zio-config can read toString'd booleans
val effect = singleOpt("--effect", settings.effect)
val scalarMappings = pairList("--scalarMappings", settings.scalarMappings)
val imports = list("--imports", settings.imports)

scalafmtPath ++ headers ++ packageName ++ genView ++ effect ++ scalarMappings ++ imports
}

def generateSources: List[File] = {
def generateFileSource(graphql: File, settings: CalibanSettings): IO[Option[Throwable], File] = for {
generatedSource <- ZIO.succeed(transformFile(sourceRoot, sourceManaged, settings)(graphql))
_ <- Task(sbt.IO.createDirectory(generatedSource.toPath.getParent.toFile)).asSomeError
opts <- ZIO.fromOption(Options.fromArgs(graphql.toString :: generatedSource.toString :: renderArgs(settings)))
res <- Codegen.generate(opts, GenType.Client).asSomeError
} yield new File(opts.toPath)

def generateUrlSource(graphql: URL, settings: CalibanSettings): IO[Option[Throwable], File] = for {
generatedSource <-
ZIO.succeed(
transformFile(sourceRoot, sourceManaged, settings)(new java.io.File(graphql.getPath().stripPrefix("/")))
)
_ <- Task(sbt.IO.createDirectory(generatedSource.toPath.getParent.toFile)).asSomeError
opts <- ZIO.fromOption(Options.fromArgs(graphql.toString :: generatedSource.toString :: renderArgs(settings)))
res <- Codegen.generate(opts, GenType.Client).asSomeError
} yield new File(opts.toPath)

Runtime.default
.unsafeRun(
for {
fromFiles <- ZIO.foreach(sources.toList)(source =>
generateFileSource(source, collectSettingsFor(source)).asSome.catchAll {
case Some(reason) =>
putStrLn(reason.toString) *> putStrLn(reason.getStackTrace.mkString("\n")).as(None)
case None => ZIO.none
}
)
fromUrls <- ZIO.foreach(urlSettings)(setting =>
generateUrlSource(setting.url, setting).asSome.catchAll {
case Some(reason) =>
putStrLn(reason.toString) *> putStrLn(reason.getStackTrace.mkString("\n")).as(None)
case None => ZIO.none
}
)
} yield fromFiles ++ fromUrls
)
.flatten
}

// NB: This is heavily inspired by the caching technique from eed3si9n's sbt-scalaxb plugin
def cachedGenerateSources =
Tracked.inputChanged(cacheDirectory / "caliban-inputs") {
(inChanged, _: (List[File], FilesInfo[ModifiedFileInfo], String)) =>
Tracked.outputChanged(cacheDirectory / "caliban-output") { (outChanged, outputs: FilesInfo[PlainFileInfo]) =>
if (inChanged || outChanged) generateSources
else outputs.files.toList.map(_.file)
}
}

def inputs: (List[File], FilesInfo[ModifiedFileInfo], String) =
(
sources.toList,
FilesInfo.lastModified(sources.toSet).asInstanceOf[FilesInfo[ModifiedFileInfo]],
BuildInfo.version
)

cachedGenerateSources(inputs)(() =>
FilesInfo.exists((sourceManaged ** "*.scala").get.toSet).asInstanceOf[FilesInfo[PlainFileInfo]]
)
}
}
6 changes: 6 additions & 0 deletions codegen-sbt/src/main/scala/caliban/codegen/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package caliban

package object codegen {
@deprecated("CodegenPlugin has been renamed to CalibanPlugin", "1.1.0")
val CodegenPlugin: CalibanPlugin.type = CalibanPlugin
}

This file was deleted.

This file was deleted.

Loading