Skip to content

Commit

Permalink
Fix #4: Add code generator (#15)
Browse files Browse the repository at this point in the history
* Fix #4: Add code generator

Ports the Scala code generator from sangria-codegen.

* Fix formatting

* Language resilient BuilderSpec

the 'file not exist' output is localized, which causes
this test to fail on non-english machines.

* Upgrade to sbt 1.1.0

* Use sbt.IO and SchemaLoader

* Refactor GraphQLSchemaPlugin into two separate plugins

* Use newest feature in the test-project

* fix autocompletion in graphqlSchema parser

* Fix test-project

* Scalafmt

* Fix test by replacing tabs

* Add newline in generated module and add better assertion output

* Remove tabs from rebase

* Add new scala source generator

* Add GraphQLQuery trait generation

* Generate additional types

* Add test for input types

* Add more documentation

* Add apollo codegen style test and codegen style setting

* Fix Sangria code generator and scripted tests

* Scalafmt formatting
  • Loading branch information
jonas authored and muuki88 committed Mar 15, 2018
1 parent 7d5ca8e commit 9f55fb9
Show file tree
Hide file tree
Showing 98 changed files with 5,206 additions and 285 deletions.
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ Your build can contain multiple schemas. They are stored in the `graphqlSchemas`
This allows to compare arbitrary schemas, write schema.json files for each of them and validate
your queries against them.

There are already two schemas predefined. The `build` schema and the `prod` schema.
There is already one schemas predefined. The `build` schema is defined by the `graphqlSchemaGen` task.
You can configure the `graphqlSchemas` label with

* The `build` schema is defined by the `graphqlSchemaGen` task.
* The `prod` schema is defined by the `graphqlProductionSchema`.
```sbt
name in graphqlSchemaGen := "local-build"
```

### Add a schema

Expand All @@ -86,16 +88,6 @@ Schemas are defined via a `GraphQLSchema` case class. You need to define
* a `description`. Explain where this schema comes from and what it represents
* a `schemaTask`. A sbt task that generates the schema

This is how the `prod` schema is defined.

```scala
graphqlSchemas += GraphQLSchema(
GraphQLSchemaLabels.PROD,
"schema generated by the graphqlProductionSchema task",
graphqlProductionSchema.taskValue
)
```

You can also define a schema from a `SchemaLoader`. This requires defining an anonymous sbt task.

```scala
Expand Down
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ libraryDependencies ++= Seq(
"org.sangria-graphql" %% "sangria-circe" % "1.1.0",
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"org.scalaj" %% "scalaj-http" % "2.3.0"
"org.scalaj" %% "scalaj-http" % "2.3.0",
"org.scalameta" %% "scalameta" % "2.1.2",
"org.scalatest" %% "scalatest" % "3.0.4" % Test
)

// scripted test settings
scriptedLaunchOpts += "-Dproject.version=" + version.value
scriptedLaunchOpts += "-Dcodegen.samples.dir=" + ((baseDirectory in ThisBuild).value / "src/test/resources")

// project meta data
licenses := Seq(
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.0.4
sbt.version=1.1.0
70 changes: 70 additions & 0 deletions src/main/scala/rocks/muki/graphql/GraphQLCodegenPlugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package rocks.muki.graphql

import rocks.muki.graphql.codegen._
import rocks.muki.graphql.schema.SchemaLoader
import sbt.Keys._
import sbt.{Result => _, _}

import scala.meta._

object GraphQLCodegenPlugin extends AutoPlugin {

override def requires: Plugins = GraphQLPlugin

object autoImport {
val graphqlCodegenSchema = taskKey[File]("GraphQL schema file")
val graphqlCodegenQueries = taskKey[Seq[File]]("GraphQL query documents")

val graphqlCodegenStyle =
settingKey[CodeGenStyles.Style]("The resulting code generation style")

val graphqlCodegenPackage =
settingKey[String]("Package for the generated code")
val graphqlCodegen = taskKey[Seq[File]]("Generate GraphQL API code")

val Apollo = CodeGenStyles.Apollo
val Sangria = CodeGenStyles.Sangria

}
import autoImport._

override def projectSettings: Seq[Setting[_]] = Seq(
graphqlCodegenStyle := Apollo,
graphqlCodegenSchema := (resourceDirectory in Compile).value / "schema.graphql",
resourceDirectories in graphqlCodegen := (resourceDirectories in Compile).value,
includeFilter in graphqlCodegen := "*.graphql",
excludeFilter in graphqlCodegen := HiddenFileFilter,
graphqlCodegenQueries := Defaults
.collectFiles(resourceDirectories in graphqlCodegen,
includeFilter in graphqlCodegen,
excludeFilter in graphqlCodegen)
.value,
sourceGenerators in Compile += graphqlCodegen.taskValue,
graphqlCodegenPackage := "graphql.codegen",
name in graphqlCodegen := "GraphQLCodegen",
graphqlCodegen := {
val log = streams.value.log
val targetDir = sourceManaged.value / "sbt-graphql"
//val generator = ScalametaGenerator((name in graphqlCodegen).value)
val queries = graphqlCodegenQueries.value
log.info(s"Generate code for ${queries.length} queries")
log.info(
s"Use schema ${graphqlCodegenSchema.value} for query validation")

val packageName = graphqlCodegenPackage.value
val schema =
SchemaLoader.fromFile(graphqlCodegenSchema.value).loadSchema()

val moduleName = (name in graphqlCodegen).value
val context = CodeGenContext(schema,
targetDir,
queries,
packageName,
moduleName,
log)

graphqlCodegenStyle.value(context)
}
)

}
75 changes: 75 additions & 0 deletions src/main/scala/rocks/muki/graphql/GraphQLPlugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package rocks.muki.graphql

import sbt.Keys._
import sbt._
import rocks.muki.graphql.schema.{GraphQLSchemas, SchemaLoader}

/**
* == GraphQL Plugin ==
*
* Root plugin for all other graphql plugins. Provides a schema registry that can be used for
*
* - validating queries against a specific schema
* - comparing schemas
* - code generation based on a specific schema
*
*/
object GraphQLPlugin extends AutoPlugin {

object autoImport {

/**
* Helper to load schemas from different places
*/
val GraphQLSchemaLoader: SchemaLoader.type =
rocks.muki.graphql.schema.SchemaLoader

val GraphQLSchema: rocks.muki.graphql.schema.GraphQLSchema.type =
rocks.muki.graphql.schema.GraphQLSchema

/**
* Contains all schemas available in this build.
*
* @example Adding a new schema
* {{{
* graphqlSchemas += GraphQLSchema(
* "temporary",
* "schema loaded from schema.json in the base directory",
* SchemaLoader.fromFile(baseDirectory.value / "schema.json")),
* }}}
*
*/
val graphqlSchemas: SettingKey[GraphQLSchemas] =
settingKey[GraphQLSchemas]("all schemas available in this build")

/**
* Renders the given schema into a graphql file.
* The input is the label in the graphqlSchemas setting.
*/
val graphqlRenderSchema: InputKey[File] =
inputKey[File]("renders the given schema to a graphql file")

}
import autoImport._

override def projectSettings: Seq[Setting[_]] = Seq(
graphqlSchemas := GraphQLSchemas(),
// schema rendering
target in graphqlRenderSchema := (target in Compile).value / "graphql",
graphqlRenderSchema := graphqlRenderSchemaTask.evaluated
)

private val graphqlRenderSchemaTask = Def.inputTaskDyn[File] {
val log = streams.value.log
val schemaDefinition = singleGraphQLSchemaParser.parsed
val file = (target in graphqlRenderSchema).value / s"${schemaDefinition.label}.graphql"
log.info(s"Rendering schema to: ${file.getPath}")

Def.task {
val schema = schemaDefinition.schemaTask.value
IO.write(file, schema.renderPretty)
file
}
}

}
93 changes: 51 additions & 42 deletions src/main/scala/rocks/muki/graphql/GraphQLQueryPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,67 @@ object GraphQLQueryPlugin extends AutoPlugin {
*/
val graphqlValidateQueries: TaskKey[Unit] =
taskKey[Unit]("validate all queries in the graphql source directory")

val graphqlQueryDirectory: SettingKey[File] =
settingKey[File]("directory that contains all graphql queries")
}

import autoImport._
import GraphQLSchemaPlugin.autoImport._

override def projectSettings: Seq[Setting[_]] = Seq(
sourceDirectory in (Compile, graphqlValidateQueries) := (sourceDirectory in Compile).value / "graphql",
graphqlValidateQueries := {
val log = streams.value.log
val schemaFile = IO.read(graphqlSchemaGen.value)
val schemaDocument = QueryParser
.parse(schemaFile)
.getOrElse(
sys.error(
"Invalid graphql schema generated by `graphqlSchemaGen` task")
)
val schema = Schema.buildFromAst(schemaDocument)
// TODO separate these into two auto plugins
override def projectSettings: Seq[Setting[_]] =
pluginSettings(Compile) ++ pluginSettings(IntegrationTest)

val src = (sourceDirectory in (Compile, graphqlValidateQueries)).value
val graphqlFiles = (src ** "*.graphql").get
val violations = graphqlFiles.flatMap {
file =>
log.info(s"Validate ${file.getPath}")
val query = IO.read(file)
val violations = QueryParser
.parse(query)
.fold(
error => Vector(InvalidQueryValidation(error)),
query => QueryValidator.default.validateQuery(schema, query)
private def pluginSettings(config: Configuration): Seq[Setting[_]] =
inConfig(config)(
Seq(
graphqlQueryDirectory := (sourceDirectory in Compile).value / "graphql",
graphqlValidateQueries := {
val log = streams.value.log
val schemaFile = IO.read(graphqlSchemaGen.value)
val schemaDocument = QueryParser
.parse(schemaFile)
.getOrElse(
sys.error(
"Invalid graphql schema generated by `graphqlSchemaGen` task")
)
if (violations.nonEmpty) {
log.error(s"File: ${file.getAbsolutePath}")
log.error("## Query ##")
log.error(query)
log.error("## Violations ##")
violations.foreach(v => log.error(v.errorMessage))
List(QueryViolations(file, query, violations))
} else {
Nil
val schema = Schema.buildFromAst(schemaDocument)

log.info(s"Checking graphql files in ${graphqlQueryDirectory.value}")
val graphqlFiles = (graphqlQueryDirectory.value ** "*.graphql").get
val violations = graphqlFiles.flatMap {
file =>
log.info(s"Validate ${file.getPath}")
val query = IO.read(file)
val violations = QueryParser
.parse(query)
.fold(
error => Vector(InvalidQueryValidation(error)),
query => QueryValidator.default.validateQuery(schema, query)
)
if (violations.nonEmpty) {
log.error(s"File: ${file.getAbsolutePath}")
log.error("## Query ##")
log.error(query)
log.error("## Violations ##")
violations.foreach(v => log.error(v.errorMessage))
List(QueryViolations(file, query, violations))
} else {
Nil
}
}
}

if (violations.nonEmpty) {
log.error("Validation errors in")
violations.foreach { queryViolation =>
log.error(s"File: ${queryViolation.file.getAbsolutePath}")
if (violations.nonEmpty) {
log.error("Validation errors in")
violations.foreach { queryViolation =>
log.error(s"File: ${queryViolation.file.getAbsolutePath}")
}
quietError("Some queries contain validation violations")
}
log.success(s"All ${graphqlFiles.size} graphql files are valid")
}
quietError("Some queries contain validation violations")
}
log.success(s"All ${graphqlFiles.size} graphql files are valid")
}
)
))

/**
* Aggregates violations for a single file
Expand Down
Loading

0 comments on commit 9f55fb9

Please sign in to comment.