Skip to content

Commit

Permalink
FIX #48 Implement magic #import like apollo-graphql (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
muuki88 authored Sep 12, 2018
1 parent e63a9d6 commit ec8f1aa
Show file tree
Hide file tree
Showing 16 changed files with 340 additions and 6 deletions.
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,16 @@ You can configure the output in various ways
* `sourceDirectories in graphqlCodegen` - List of directories where graphql files should be looked up.
Default is `sourceDirectory in graphqlCodegen`, which defaults to `sourceDirectory in Compile / "graphql"`
* `includeFilter in graphqlCodegen` - Filter graphql files. Default is `"*.graphql"`
* `excludeFilter in graphqlCodegen` - Filter graphql files. Default is `HiddenFileFilter`
* `excludeFilter in graphqlCodegen` - Filter graphql files. Default is `HiddenFileFilter || "*.fragment.graphql"`
* `graphqlCodegenQueries` - Contains all graphql query files. By default this setting contains all
files that reside in `sourceDirectories in graphqlCodegen` and that match the `includeFilter` / `excludeFilter` settings.
* `graphqlCodegenPackage` - The package where all generated code is placed. Default is `graphql.codegen`
* `name in graphqlCodegen` - Used as a module name in the `Sangria` code generator.
* `graphqlCodegenJson` - Generate JSON encoders/decoders with your graphql query. Default is `JsonCodec.None`.
Note that not all styles support JSON encoder/decoder generation.
* `graphqlCodegenImports: Seq[String]` - A list of additional that are included in every generated file
* `graphqlCodegenPreProcessors: Seq[PreProcessor]` - A list of preprocessors that can alter the original graphql query before it is being parsed.
By default the `magic #imports` for including fragments are enabled. See the `magic #imports` section for more details.


### JSON support
Expand Down Expand Up @@ -283,6 +285,62 @@ which is represented as `java.time.ZoneDateTime`. Add this as an import
graphqlCodegenImports += "java.time.ZoneDateTime"
```

### Magic #imports

This is a feature tries to replicate the [apollographql/graphql-tag loader.js](https://github.com/apollographql/graphql-tag/blob/ae792b67ef16ae23a0a7a8d78af8b698e8acd7d2/loader.js#L29-L37)
feature, which enables including (or actually inlining) partials into a graphql query with magic comments.

#### Explained

The syntax is straightforward

```graphql
#import path/to/included.fragment.graphql
```

The fragment files should be named liked this

```
<name>.fragment.graphql
```

There is a `excludeFilter in graphqlCodegen`, which removes them from code generation so they are just used for inlining
and interface generation.

The resolving of paths works like this

- The path is resolved by checking all `sourceDirectories in graphqlCodegen` for the given path
- No relative paths like `./foo.fragment.graphql` are supported
- Imports are resolved recursively. This means you can `#import` fragments in a fragment.

#### Example

I have a file `CharacterInfo.fragment.graphql` which contains only a single fragment

```graphql
fragment CharacterInfo on Character {
name
}
```

And the actual graphql query file


```graphql
query HeroFragmentQuery {
hero {
...CharacterInfo
}
human(id: "Lea") {
homePlanet
...CharacterInfo
}
}

#import fragments/CharacterInfo.fragment.graphql
```


### Codegen style Apollo

As the name suggests the output is similar to the one in apollo codegen.
Expand Down
11 changes: 9 additions & 2 deletions src/main/scala/rocks/muki/graphql/GraphQLCodegenPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ object GraphQLCodegenPlugin extends AutoPlugin {
settingKey[Seq[String]](
"Additional imports to add to the generated code")

val graphqlCodegenPreProcessors = taskKey[Seq[PreProcessor]](
"Preprocessors that should be applied before the graphql file is parsed")

val graphqlCodegen = taskKey[Seq[File]]("Generate GraphQL API code")

val Apollo = CodeGenStyles.Apollo
Expand All @@ -45,7 +48,7 @@ object GraphQLCodegenPlugin extends AutoPlugin {
sourceDirectories in graphqlCodegen := List(
(sourceDirectory in graphqlCodegen).value),
includeFilter in graphqlCodegen := "*.graphql",
excludeFilter in graphqlCodegen := HiddenFileFilter,
excludeFilter in graphqlCodegen := HiddenFileFilter || "*.fragment.graphql",
graphqlCodegenQueries := Defaults
.collectFiles(sourceDirectories in graphqlCodegen,
includeFilter in graphqlCodegen,
Expand All @@ -54,6 +57,9 @@ object GraphQLCodegenPlugin extends AutoPlugin {
sourceGenerators in Compile += graphqlCodegen.taskValue,
graphqlCodegenPackage := "graphql.codegen",
graphqlCodegenImports := Seq.empty,
graphqlCodegenPreProcessors := List(
PreProcessors.magicImports((sourceDirectories in graphqlCodegen).value)
),
name in graphqlCodegen := "GraphQLCodegen",
graphqlCodegen := {
val log = streams.value.log
Expand All @@ -69,8 +75,8 @@ object GraphQLCodegenPlugin extends AutoPlugin {
SchemaLoader.fromFile(graphqlCodegenSchema.value).loadSchema()

val imports = graphqlCodegenImports.value

val jsonCodeGen = graphqlCodegenJson.value
val preProcessors = graphqlCodegenPreProcessors.value
log.info(
s"Generating json decoding with: ${jsonCodeGen.getClass.getSimpleName}")

Expand All @@ -84,6 +90,7 @@ object GraphQLCodegenPlugin extends AutoPlugin {
moduleName,
jsonCodeGen,
imports,
preProcessors,
log)

graphqlCodegenStyle.value(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import sbt.Logger
* @param moduleName optional module name for single-file based generators
* @param jsonCodeGen
* @param imports list of imports to be added to the generated file
* @param preProcessors pre processors that should be applied before the graphql file is parsed
* @param log output log
*/
case class CodeGenContext(
Expand All @@ -27,5 +28,6 @@ case class CodeGenContext(
moduleName: String,
jsonCodeGen: JsonCodeGen,
imports: Seq[String],
preProcessors: Seq[PreProcessor],
log: Logger
)
10 changes: 8 additions & 2 deletions src/main/scala/rocks/muki/graphql/codegen/CodeGenStyles.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ object CodeGenStyles {
// Process all the graphql files
val files = inputFiles.map { inputFile =>
for {
queryDocument <- DocumentLoader.single(schema, inputFile)
processedFile <- PreProcessors(inputFile,
context.targetDirectory,
context.preProcessors)
queryDocument <- DocumentLoader.single(schema, processedFile)
typedDocument <- TypedDocumentParser(schema, queryDocument)
.parse()
sourceCode <- ApolloSourceGenerator(inputFile.getName,
Expand All @@ -78,7 +81,10 @@ object CodeGenStyles {

val interfaceFile = for {
// use all queries to determine the interfaces & types we need
allQueries <- DocumentLoader.merged(schema, inputFiles.toList)
processedFiles <- PreProcessors(inputFiles,
context.targetDirectory,
context.preProcessors)
allQueries <- DocumentLoader.merged(schema, processedFiles.toList)
typedDocument <- TypedDocumentParser(schema, allQueries)
.parse()
codeGenerator = ApolloSourceGenerator("Interfaces.scala",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ object DocumentLoader {
violations.isEmpty,
document,
Failure(
s"Invalid query: ${violations.map(_.errorMessage).mkString(", ")}"))
s"Invalid query in ${file.getAbsolutePath}:\n${violations.map(_.errorMessage).mkString(", ")}"))
} yield document
}

Expand Down
113 changes: 113 additions & 0 deletions src/main/scala/rocks/muki/graphql/codegen/PreProcessors.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package rocks.muki.graphql.codegen

import sbt._

import cats.syntax.either._

object PreProcessors {

/**
* Applies all preprocessors and returns a new file with the processed content
*
* @param graphqlFile the input graphql file
* @param targetDir target directory where the processed file should be written
* @param preProcessors the list of preprocessors
* @return a new file with the processed content
*/
def apply(graphqlFile: File,
targetDir: File,
preProcessors: Seq[PreProcessor]): Result[File] = {
val processedFile = targetDir / graphqlFile.getName

for {
input <- Either.catchNonFatal(IO.read(graphqlFile)).leftMap { error =>
Failure(s"Failed to read $graphqlFile: ${error.getMessage}")
}
// poor-mans traverse
processedContent <- preProcessors.foldLeft(Right(input): Result[String]) {
case (Right(processedContent), processor) =>
processor(processedContent)
case (error, _) => error
}
} yield {
IO.write(processedFile, processedContent)
processedFile
}
}

/**
* Applies the preprocessors to all given files
*
* @param graphqlFiles graphql input files
* @param targetDir target directory where the processed file should be written
* @param preProcessors the list of preprocessors
* @return a list of new files with the processed content
*/
def apply(graphqlFiles: Seq[File],
targetDir: File,
preProcessors: Seq[PreProcessor]): Result[Seq[File]] = {
graphqlFiles
.map(file => apply(file, targetDir, preProcessors))
.foldLeft[Result[List[File]]](Right(List.empty)) {
case (Left(failure), Left(nextFailure)) =>
Left(Failure(failure.message + "\n" + nextFailure.message))
case (Left(failure), _) => Left(failure)
case (_, Left(failure)) => Left(failure)
case (Right(files), Right(file)) => Right(files :+ file)
}
}

/**
* Allows to import other graphql files (e.g. fragments) into an graphql file.
*
* Allowed characters in the path are
* - everything that matches `\w`, e.g. characters, decimals and _
* - forward slashes `/`
* - dots `.`
*
* @example {{{
* #import fragments/foo.graphql
* }}}
* @param rootDirectories a list of directories that should be used to resolve magic imports
* @return
*/
def magicImports(rootDirectories: Seq[File]): PreProcessor = graphQLFile => {
// match the import file path
val Import = "#import\\s*([\\w\\/\\.]*)".r

val processed = graphQLFile.split(IO.Newline).map {
case Import(filePath) =>
rootDirectories.map(dir => dir / filePath).find(_.exists()) match {
case Some(importedGraphQLFile) =>
val importedGraphQLContent = IO.read(importedGraphQLFile)
for {
recursiveProcessed <- magicImports(rootDirectories)(
importedGraphQLContent)
} yield recursiveProcessed + IO.Newline
case None =>
Left(Failure(s"Could not resolve $filePath in $rootDirectories"))
}
case line => Right(line)
}

// poor mans cats.Validated
val errors = processed.collect {
case Left(error) => error
}

if (errors.isEmpty) {
Right(
processed
.collect {
case Right(line) => line
}
.mkString(IO.Newline))
} else {
Left(errors.reduce[Failure] {
case (reduced, nextFailure) =>
Failure(reduced.message + IO.Newline + reduced.message)
})
}
}

}
12 changes: 12 additions & 0 deletions src/main/scala/rocks/muki/graphql/codegen/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,17 @@
package rocks.muki.graphql

package object codegen {

/**
* Type alias for a processing result during a code generation step
* @tparam T the success type
*/
type Result[T] = Either[Failure, T]

/**
* Type alias for a graphql file pre-processing function.
* _ The input is the raw graphql file content
* _ The output is the transformed graphql file content or an error
*/
type PreProcessor = String => Result[String]
}
16 changes: 16 additions & 0 deletions src/sbt-test/codegen/apollo-magic-imports/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name := "test"
enablePlugins(GraphQLCodegenPlugin)
scalaVersion := "2.12.4"

libraryDependencies ++= Seq(
"org.sangria-graphql" %% "sangria" % "1.3.0"
)

graphqlCodegenStyle := Apollo

TaskKey[Unit]("check") := {
val generatedFiles = (graphqlCodegen in Compile).value
val interfacesFile = generatedFiles.find(_.getName == "Interfaces.scala")

assert(interfacesFile.isDefined, s"Could not find generated scala class. Available files\n ${generatedFiles.mkString("\n ")}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("rocks.muki" % "sbt-graphql" % sys.props("project.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
query HeroFragmentQuery {
hero {
...CharacterInfo
}
human(id: "Lea") {
homePlanet
...CharacterInfo
}
}

#import fragments/CharacterFriends.fragment.graphql
#import fragments/CharacterInfo.fragment.graphql

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
query HeroFragmentWithAbsolutQuery {
hero {
...CharacterInfoWithAbsolutImport
}
human(id: "Lea") {
homePlanet
...CharacterInfoWithAbsolutImport
}
}

#import fragments/CharacterInfoWithAbsolutImport.fragment.graphql

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fragment CharacterFriends on Character {
name
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fragment CharacterInfo on Character {
name
friends {
...CharacterFriends
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import fragments/CharacterFriends.fragment.graphql

fragment CharacterInfoWithAbsolutImport on Character {
name
friends {
...CharacterFriends
}
}
Loading

0 comments on commit ec8f1aa

Please sign in to comment.