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

Issue/7 implicit null #23

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ Change Log

Badges: `[UPDATED]`, `[FIXED]`, `[ADDED]`, `[DEPRECATED]`, `[REMOVED]`, `[BREAKING]`

Version 0.4.0 *(WIP)*
---
=======
* `[ADDED]`: Support implicit-nullness of top level fields of variables. To provide optional variables to `Variables` class, you now need to set it via its member methods.
* `[BREAKING]`: kgql now requires schema.json.
* TBD


Version 0.3.1 *(2019-07-12)*
---

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ kgql requires Gradle __5.3.1 or later__

Supported GraphQL file extension: `.gql` or `.graphql`

You need `schema.json` file which you can get via [introspection query](https://graphql.org/learn/introspection/).
You can use tools like [graphql-cli/graphql-cli](https://github.com/graphql-cli/graphql-cli).

#### For Android Project

```gradle
Expand Down Expand Up @@ -50,6 +53,7 @@ kgql {
// mapper for non-scalar type
"UserProfile": "com.sample.data.UserProfile"
]
schemaJson = file("src/main/kgql/schema.json) // defaults to "src/main/kgql/schema.json"
}
```

Expand All @@ -75,11 +79,12 @@ repositories {

kgql {
packageName = "com.sample"
sourceSet = files("src/main/kgql")
sourceSet = files("src/main/kgql") // defaults to "src/main/kgql"
typeMapper = [
// mapper for non-scalar type
"UserProfile": "com.sample.data.UserProfile"
]
schemaJson = file("src/main/kgql/schema.json) // defaults to "src/main/kgql/schema.json"
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import graphql.language.NonNullType
import graphql.language.Type

class KgqlCustomTypeMapper(
val typeMap: Map<GraphQLCustomTypeName, GraphQLCustomTypeFQName>
val typeMap: Map<GraphQLCustomTypeName, GraphQLCustomTypeFQName>,
enumNameSet: Set<String>
) {
val enumMap = enumNameSet.associate { it to typeMap[it] }

fun get(type: Type<*>): TypeName {
return when (type) {
is NonNullType -> get(type.type).copy(nullable = false)
Expand Down Expand Up @@ -43,4 +46,20 @@ class KgqlCustomTypeMapper(
fun hasCustomType(gqlTypeName: String): Boolean {
return typeMap.containsKey(gqlTypeName)
}

/**
* Check if [type] is defined as Enum in GraphQL schema
*/
fun isEnum(type: TypeName): Boolean {
return enumMap.containsValue(type.copy(nullable = false, annotations = emptyList()).toString())
}

fun isPrimitive(type: TypeName): Boolean {
return listOf(
String::class.asTypeName(),
Int::class.asTypeName(),
Float::class.asTypeName(),
Boolean::class.asTypeName()
).contains(type.copy(nullable = false, annotations = emptyList()))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ class KgqlEnvironment(
* An output directory to place the generated class files
*/
private val outputDirectory: File? = null,
private val typeMap: Map<GraphQLCustomTypeName, GraphQLCustomTypeFQName>
typeMap: Map<GraphQLCustomTypeName, GraphQLCustomTypeFQName>,
enumNameSet: Set<String>
) {

private val typeMapper = KgqlCustomTypeMapper(typeMap, enumNameSet)

sealed class CompilationStatus {
class Success : CompilationStatus()
object Success : CompilationStatus()
class Failure(val errors: List<String>) : CompilationStatus()
}

Expand All @@ -38,14 +41,15 @@ class KgqlEnvironment(

forEachSourceFile { file ->
try {
KgqlCompiler.compile(file, typeMap, writer, logger)
KgqlCompiler.compile(file, typeMapper, writer, logger)
} catch (e: Throwable) {
println("error: $e")
e.message?.let { errors.add(it) }
}
}

return if (errors.isEmpty()) {
CompilationStatus.Success()
CompilationStatus.Success
} else {
CompilationStatus.Failure(errors)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.codingfeline.kgql.compiler.generator

import com.codingfeline.kgql.compiler.GraphQLCustomTypeFQName
import com.codingfeline.kgql.compiler.GraphQLCustomTypeName
import com.codingfeline.kgql.compiler.KgqlCustomTypeMapper
import com.codingfeline.kgql.compiler.KgqlFile
import com.codingfeline.kgql.compiler.Logger
Expand All @@ -14,12 +12,11 @@ import graphql.language.TypeName as GqlTypeName

class DocumentWrapperGenerator(
val sourceFile: KgqlFile,
typeMap: Map<GraphQLCustomTypeName, GraphQLCustomTypeFQName>
val typeMapper: KgqlCustomTypeMapper
) {

val rawDocument = sourceFile.source.readText()
val document = Parser().parseDocument(rawDocument)
val typeMapper = KgqlCustomTypeMapper(typeMap)

val className = "${sourceFile.source.nameWithoutExtension.capitalize()}Document"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.codingfeline.kgql.compiler.generator

import com.codingfeline.kgql.compiler.GraphQLCustomTypeFQName
import com.codingfeline.kgql.compiler.GraphQLCustomTypeName
import com.codingfeline.kgql.compiler.KgqlCustomTypeMapper
import com.codingfeline.kgql.compiler.KgqlFile
import com.codingfeline.kgql.compiler.Logger
import com.squareup.kotlinpoet.AnnotationSpec
Expand All @@ -15,22 +14,22 @@ object KgqlCompiler {

fun compile(
file: KgqlFile,
typeMap: Map<GraphQLCustomTypeName, GraphQLCustomTypeFQName>,
typeMapper: KgqlCustomTypeMapper,
output: FileAppender,
logger: Logger
) {
writeDocumentWrapperFile(file, typeMap, output, logger)
writeDocumentWrapperFile(file, typeMapper, output, logger)
}

fun writeDocumentWrapperFile(
sourceFile: KgqlFile,
typeMap: Map<GraphQLCustomTypeName, GraphQLCustomTypeFQName>,
typeMapper: KgqlCustomTypeMapper,
output: FileAppender,
logger: Logger
) {
val packageName = sourceFile.packageName
val outputDirectory = "${sourceFile.outputDirectory.absolutePath}/${packageName.replace(".", "/")}"
val documentWrapperType = DocumentWrapperGenerator(sourceFile, typeMap).generateType(logger)
val documentWrapperType = DocumentWrapperGenerator(sourceFile, typeMapper).generateType(logger)
FileSpec.builder(sourceFile.packageName, sourceFile.source.nameWithoutExtension)
.apply {
addType(documentWrapperType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ class OperationWrapperGenerator(
.addAnnotation(ClassName("kotlinx.serialization", "UnstableDefault"))

if (hasVariables) {
variableSpec = VariableWrapperGenerator(operation.variableDefinitions, typeMapper).generateType()
variableSpec = VariablesWrapperGenerator(
operation.variableDefinitions,
ClassName.bestGuess(parentFQName).nestedClass(name),
typeMapper
).generateType()
objectSpec.addType(variableSpec)
}

Expand Down Expand Up @@ -90,7 +94,7 @@ class OperationWrapperGenerator(

if (variablesSpec != null) {
spec.addStatement(
"return %N.stringify(serializer(), %N(variables = variables))",
"return %N.stringify(serializer(), %N(variables = variables.asJsonObject()))",
jsonSpec,
requestBodySpec
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import graphql.language.OperationDefinition
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject

class RequestBodyGenerator(
val operation: OperationDefinition,
Expand All @@ -21,13 +21,11 @@ class RequestBodyGenerator(
) {

fun generateType(): TypeSpec {
val variablesType = variablesSpec.typeNameOrUnit()
val variablesType = JsonObject::class.asTypeName()
val spec = TypeSpec.classBuilder("Request")
.addModifiers(KModifier.DATA)
.addAnnotation(Serializable::class)
.addSuperinterface(
KgqlRequestBody::class.asTypeName().plusParameter(variablesType)
)
.addSuperinterface(KgqlRequestBody::class.asTypeName())

val constructorSpec = FunSpec.constructorBuilder()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.codingfeline.kgql.compiler.generator

import com.codingfeline.kgql.compiler.KgqlCustomTypeMapper
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.buildCodeBlock
import kotlinx.serialization.internal.EnumSerializer
import kotlinx.serialization.internal.NullableSerializer

class SerializerGenerator(
private val typeMapper: KgqlCustomTypeMapper
) {

fun generateCodeBlock(type: TypeName): CodeBlock {
return visitType(type).foldRight(CodeBlock.of(""), { value, acc -> value.generateCodeBlock(acc) })
}

private fun visitType(type: TypeName): List<SerializerPart> {
return when (type) {
is ParameterizedTypeName -> {
// should be List type
listOf(SerializerPart.List(type.isNullable)) + visitType(type.typeArguments.first())
}
else -> {
if (typeMapper.isCustomType(type)) {
if (typeMapper.isEnum(type)) {
listOf(
SerializerPart.Enum(
type.copy(
nullable = false,
annotations = emptyList()
), type.isNullable
)
)
} else {
listOf(
SerializerPart.Custom(
type.copy(
nullable = false,
annotations = emptyList()
), type.isNullable
)
)
}
} else {
listOf(
SerializerPart.Primitive(
type.copy(
nullable = false,
annotations = emptyList()
), type.isNullable
)
)
}
}
}
}


sealed class SerializerPart {
abstract val isNullable: Boolean
abstract fun generateCodeBlockInternal(block: CodeBlock): CodeBlock

private fun wrapWithNullableSerializerIfNeeded(block: CodeBlock): CodeBlock {
return if (isNullable) {
buildCodeBlock {
add("%T(%L)", NullableSerializer::class.asTypeName(), block)
}
} else {
block
}
}

fun generateCodeBlock(block: CodeBlock): CodeBlock {
return wrapWithNullableSerializerIfNeeded(generateCodeBlockInternal(block))
}

data class List(override val isNullable: Boolean = true) : SerializerPart() {
override fun generateCodeBlockInternal(block: CodeBlock): CodeBlock {
return buildCodeBlock {
add("%L.%M", block, listSerializer)
}
}
}

data class Primitive(val typeName: TypeName, override val isNullable: Boolean = true) : SerializerPart() {
override fun generateCodeBlockInternal(block: CodeBlock): CodeBlock {
return buildCodeBlock {
add("%T.%M()", typeName, primitiveSerializer)
}
}
}

data class Custom(val typeName: TypeName, override val isNullable: Boolean = true) : SerializerPart() {
override fun generateCodeBlockInternal(block: CodeBlock): CodeBlock {
return buildCodeBlock {
add("%T.serializer()", typeName)
}
}
}

data class Enum(val type: TypeName, override val isNullable: Boolean = true) : SerializerPart() {
override fun generateCodeBlockInternal(block: CodeBlock): CodeBlock {
return buildCodeBlock {
add("%T(%T::class)", EnumSerializer::class.asTypeName(), type)
}
}
}

companion object {
val primitiveSerializer = MemberName("kotlinx.serialization", "serializer")
val listSerializer = MemberName("kotlinx.serialization", "list")
}
}
}
Loading