Skip to content

Commit

Permalink
Directive validation
Browse files Browse the repository at this point in the history
  • Loading branch information
ermadmi78 committed Aug 30, 2021
1 parent e0467aa commit 1b9fe91
Show file tree
Hide file tree
Showing 22 changed files with 453 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.github.ermadmi78.kobby.model.parseSchema
import org.junit.jupiter.api.Test
import java.io.InputStreamReader
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail

/**
Expand All @@ -19,7 +20,7 @@ import kotlin.test.fail
class SchemaValidationTest {
private val layout = KotlinLayout(
KotlinTypes.PREDEFINED_SCALARS + mapOf(
"DateTime" to KotlinType("java.time", "OffsetDateTime"),
"Date" to KotlinType("java.time", "LocalDate"),
"JSON" to MAP.parameterize(STRING, ANY.nullable())
),
KotlinContextLayout(
Expand Down Expand Up @@ -100,6 +101,11 @@ class SchemaValidationTest {
emptyMap(),
InputStreamReader(this.javaClass.getResourceAsStream("kobby.graphqls")!!)
)

schema.validate().forEach {
println(it)
}

val files = generateKotlin(schema, layout)

files.forEach {
Expand All @@ -116,6 +122,8 @@ class SchemaValidationTest {
InputStreamReader(this.javaClass.getResourceAsStream("unknown_scalar.graphqls.txt")!!)
)

assertTrue(schema.validate().isEmpty())

val expected = "Kotlin data type for scalar 'DummyScalar' not found. " +
"Please, configure it by means of 'kobby' extension. https://github.com/ermadmi78/kobby"
try {
Expand All @@ -133,6 +141,8 @@ class SchemaValidationTest {
InputStreamReader(this.javaClass.getResourceAsStream("unknown_type.graphqls.txt")!!)
)

assertTrue(schema.validate().isEmpty())

val expected = "Unknown type \"DummyType\""
try {
generateKotlin(schema, layout)
Expand All @@ -149,6 +159,8 @@ class SchemaValidationTest {
InputStreamReader(this.javaClass.getResourceAsStream("unknown_arg_type.graphqls.txt")!!)
)

assertTrue(schema.validate().isEmpty())

val expected = "Unknown type \"DummyArg\""
try {
generateKotlin(schema, layout)
Expand All @@ -165,6 +177,8 @@ class SchemaValidationTest {
InputStreamReader(this.javaClass.getResourceAsStream("unknown_parent.graphqls.txt")!!)
)

assertTrue(schema.validate().isEmpty())

val expected = "Unknown type \"DummyParent\""
try {
generateKotlin(schema, layout)
Expand All @@ -173,4 +187,20 @@ class SchemaValidationTest {
assertEquals(expected, e.message)
}
}

@Test
fun testUnknownParentWithDefault() {
val schema = parseSchema(
emptyMap(),
InputStreamReader(this.javaClass.getResourceAsStream("unknown_parent_with_default.graphqls.txt")!!)
)

val expected = "Unknown type \"DummyParentWithDefault\""
try {
schema.validate()
fail("Must throw: $expected")
} catch (e: KobbyInvalidSchemaException) {
assertEquals(expected, e.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
directive @default on FIELD_DEFINITION

type Query {
temp: Film
}

type Film implements DummyParentWithDefault {
title: String! @default
}
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,14 @@ open class KobbyKotlin : DefaultTask() {
"Schema parsing failed.".throwIt(e)
}

try {
schema.validate().forEach { warning ->
logger.warn(warning)
}
} catch (e: Exception) {
"Schema validation failed.".throwIt(e)
}

val output = try {
generateKotlin(schema, layout)
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@ class GenerateKotlinMojo : AbstractMojo() {
"Schema parsing failed.".throwIt(e)
}

try {
schema.validate().forEach { warning ->
log.warn(warning)
}
} catch (e: Exception) {
"Schema validation failed.".throwIt(e)
}

val output = try {
generateKotlin(schema, layout)
} catch (e: Exception) {
Expand Down
4 changes: 4 additions & 0 deletions kobby-model/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ tasks {
minimize()
configurations = listOf(shadowImplementation)
}

test {
dependsOn(":resolveIntegrationTestDependencies")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,85 @@ class KobbyField internal constructor(
node.isOperation || (overriddenField?.isResolve ?: (resolve || arguments.isNotEmpty()))
}

internal fun validate(warnings: MutableList<String>) {
if (!primaryKey && !required && !default && !selection && !resolve) {
return
}
val topOverriddenField = findTopOverriddenField(node)

var counter = 0
if (primaryKey) {
counter++
warnings.addPropertyWarning(KobbyDirective.PRIMARY_KEY)
topOverriddenField?.takeIf { !it.primaryKey }?.also {
warnings.addOverriddenWarning(KobbyDirective.PRIMARY_KEY, it)
}
}

if (required) {
counter++
warnings.addPropertyWarning(KobbyDirective.REQUIRED)
topOverriddenField?.takeIf { !it.required }?.also {
warnings.addOverriddenWarning(KobbyDirective.REQUIRED, it)
}
}

if (default) {
counter++
warnings.addPropertyWarning(KobbyDirective.DEFAULT)
topOverriddenField?.takeIf { !it.default }?.also {
warnings.addOverriddenWarning(KobbyDirective.DEFAULT, it)
}
}

if (counter > 1) {
warnings += "Restriction violated [${node.name}.$name]: " +
"The field is marked with several directives at once - " +
"@${KobbyDirective.DEFAULT}, @${KobbyDirective.REQUIRED}, @${KobbyDirective.PRIMARY_KEY}, " +
"the behavior of the Kobby Plugin is undefined!"
}

if (selection) {
if (!arguments.values.any { it.isInitialized }) {
warnings += "Restriction violated [${node.name}.$name]: " +
"The @${KobbyDirective.SELECTION} directive can only be applied to a field that " +
"contains optional arguments - nullable arguments or arguments with default value."
}
topOverriddenField?.takeIf { !it.selection }?.also {
warnings.addOverriddenWarning(KobbyDirective.SELECTION, it)
}
}

if (resolve && !node.isOperation) {
topOverriddenField?.takeIf { !it.resolve }?.also {
warnings.addOverriddenWarning(KobbyDirective.RESOLVE, it)
}
}
}

private fun findTopOverriddenField(startNode: KobbyNode): KobbyField? =
overriddenField?.takeIf { it.node != startNode }?.let {
it.findTopOverriddenField(startNode) ?: it
}

private fun MutableList<String>.addPropertyWarning(directive: String) {
if (arguments.isNotEmpty()) {
this += "Restriction violated [${node.name}.$name]: " +
"The [@$directive] directive can only be applied to a field with no arguments."
}

if (type.node.kind != SCALAR && type.node.kind != ENUM) {
this += "Restriction violated [${node.name}.$name]: " +
"The [@$directive] directive can only be applied to a field that returns a scalar or enum type."
}
}

private fun MutableList<String>.addOverriddenWarning(directive: String, topField: KobbyField) {
this += "Restriction violated [${node.name}.$name]: " +
"The [@$directive] directive cannot be applied to overridden fields. " +
"Please, apply [@$directive] directive to [${topField.node.name}.${topField.name}] field."
}

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ class KobbySchema internal constructor(
fun unions(action: (KobbyNode) -> Unit) = unions.values.forEach(action)
fun enums(action: (KobbyNode) -> Unit) = enums.values.forEach(action)
fun inputs(action: (KobbyNode) -> Unit) = inputs.values.forEach(action)

fun validate(): List<String> {
val warnings = mutableListOf<String>()

interfaces { node ->
node.fields { field ->
field.validate(warnings)
}
}

objects { node ->
node.fields { field ->
field.validate(warnings)
}
}

return warnings
}
}

fun KobbySchema(block: KobbySchemaScope.() -> Unit): KobbySchema =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.github.ermadmi78.kobby.model

import org.junit.jupiter.api.Test
import java.io.InputStreamReader
import kotlin.test.assertEquals

/**
* Created on 30.08.2021
*
* @author Dmitry Ermakov ([email protected])
*/
class DirectiveValidationTest {
@Test
fun testDirectiveRestrictions() {
"default_arguments_and_return_type".shouldViolate(
"Restriction violated [Query.first]: The [@primaryKey] directive can only be applied to a field with no arguments.",
"Restriction violated [Query.first]: The [@primaryKey] directive can only be applied to a field that returns a scalar or enum type.",
"Restriction violated [Query.second]: The [@required] directive can only be applied to a field with no arguments.",
"Restriction violated [Query.second]: The [@required] directive can only be applied to a field that returns a scalar or enum type.",
"Restriction violated [Query.third]: The [@default] directive can only be applied to a field with no arguments.",
"Restriction violated [Query.third]: The [@default] directive can only be applied to a field that returns a scalar or enum type."
)

"default_cannot_override".shouldViolate(
"Restriction violated [Country.id]: The [@primaryKey] directive cannot be applied to overridden fields. Please, apply [@primaryKey] directive to [IBase.id] field.",
"Restriction violated [Country.name]: The [@required] directive cannot be applied to overridden fields. Please, apply [@required] directive to [IBase.name] field.",
"Restriction violated [Country.description]: The [@default] directive cannot be applied to overridden fields. Please, apply [@default] directive to [IBase.description] field."
)

"default_can_override".shouldViolate()
"default_override".shouldViolate()
"default_enum".shouldViolate()

"default_mix".shouldViolate(
"Restriction violated [Query.first]: The field is marked with several directives at once - @default, @required, @primaryKey, the behavior of the Kobby Plugin is undefined!",
"Restriction violated [Query.second]: The field is marked with several directives at once - @default, @required, @primaryKey, the behavior of the Kobby Plugin is undefined!",
"Restriction violated [Query.third]: The field is marked with several directives at once - @default, @required, @primaryKey, the behavior of the Kobby Plugin is undefined!",
"Restriction violated [Query.fourth]: The field is marked with several directives at once - @default, @required, @primaryKey, the behavior of the Kobby Plugin is undefined!"
)

"selection_no_optional".shouldViolate(
"Restriction violated [Query.first]: The @selection directive can only be applied to a field that contains optional arguments - nullable arguments or arguments with default value.",
"Restriction violated [Query.second]: The @selection directive can only be applied to a field that contains optional arguments - nullable arguments or arguments with default value.",
"Restriction violated [Query.fourth]: The @selection directive can only be applied to a field that contains optional arguments - nullable arguments or arguments with default value."
)

"selection_optional".shouldViolate()

"selection_cannot_override".shouldViolate(
"Restriction violated [Country.base]: The [@selection] directive cannot be applied to overridden fields. Please, apply [@selection] directive to [Base.base] field."
)
"selection_can_override".shouldViolate()
"selection_override".shouldViolate()

"resolve_cannot_override".shouldViolate(
"Restriction violated [Country.base]: The [@resolve] directive cannot be applied to overridden fields. Please, apply [@resolve] directive to [Base.base] field."
)
"resolve_can_override".shouldViolate()
"resolve_override".shouldViolate()
}

private fun String.shouldViolate(vararg warnings: String) {
val schema = parseSchema(
emptyMap(),
InputStreamReader(this@DirectiveValidationTest.javaClass.getResourceAsStream("$this.graphqls.txt")!!)
)

assertEquals(listOf(*warnings), schema.validate())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
directive @primaryKey on FIELD_DEFINITION
directive @required on FIELD_DEFINITION
directive @default on FIELD_DEFINITION

type Query {
first(arg: Int): [Country!]! @primaryKey
second(arg: Boolean! = false): Country! @required
third(arg: String!): Country @default
}

type Country {
id: ID!
name: String!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
directive @primaryKey on FIELD_DEFINITION
directive @required on FIELD_DEFINITION
directive @default on FIELD_DEFINITION

type Query {
countries: [Country!]!
}

interface IBase {
id: ID! @primaryKey
name: String! @required
description: String @default
}

interface ICountry implements IBase {
id: ID!
name: String!
description: String
}

type Country implements ICountry & IBase {
id: ID! @primaryKey
name: String! @required
description: String @default
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
directive @primaryKey on FIELD_DEFINITION
directive @required on FIELD_DEFINITION
directive @default on FIELD_DEFINITION

type Query {
countries: [Country!]!
}

interface IBase {
id: ID!
name: String!
description: String
}

interface ICountry implements IBase {
id: ID!
name: String!
description: String
}

type Country implements ICountry & IBase {
id: ID! @primaryKey
name: String! @required
description: String @default
}
Loading

0 comments on commit 1b9fe91

Please sign in to comment.