Skip to content

Commit

Permalink
refactor & reuse existing validations after merge of #244
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasPfeifer committed Feb 28, 2020
1 parent 8b7ef14 commit be11dc4
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 87 deletions.
178 changes: 92 additions & 86 deletions core/src/main/scala/caliban/validation/Validator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -521,56 +521,6 @@ object Validator {
)
}

private def validateInterface(t: __Type): IO[ValidationError, Unit] = {
def validateName(name: String) = {
if (name.isEmpty) {
failValidation("msg", "explanatory")
} else if (name.startsWith("__")) {
failValidation("msg", "explanatory")
} else {
IO.unit
}
}
def validateFieldName(field: __Field): IO[ValidationError, Unit] = {
validateName(field.name)
}
def validateFieldArgumentsName(args: List[__InputValue]): IO[ValidationError, Unit] = {
IO.foreach(args){ arg => validateName(arg.name) }.unit
}

def validateFieldReturnOutputType(`type`: () => __Type): IO[ValidationError, Unit] = IO.unit
def validateFieldArgumentsInputType(args: List[__InputValue]): IO[ValidationError, Unit] = IO.unit

def validateInterfaceField(field: __Field): IO[ValidationError, Unit] =
for {
_ <- validateFieldName(field)
_ <- validateFieldReturnOutputType(field.`type`)
_ <- validateFieldArgumentsName(field.args)
_ <- validateFieldArgumentsInputType(field.args)
} yield ()

def duplicateFieldName(fields: List[__Field]): Option[__Field] =
fields.groupBy(_.name).collectFirst { case (_, f :: _ :: _) => f }

t.fields(__DeprecatedArgs(Some(true))) match {
case None | Some(Nil) =>
failValidation(
s"message",
"explanatory"
)
case Some(fields) =>
duplicateFieldName(fields) match {
case Some(value) =>
failValidation(
s"message with $value",
"explanatory"
)
case None =>
IO.foreach(fields){ validateInterfaceField }.unit
}
}
}

private def validateUnion(t: __Type): IO[ValidationError, Unit] = {

def isObject(t: __Type): Boolean = t.kind match {
Expand All @@ -596,61 +546,117 @@ object Validator {
}

private def validateInputObject(t: __Type): IO[ValidationError, Unit] = {
// https://spec.graphql.org/June2018/#IsInputType()
def isInputType(t: __Type): Either[__Type, Unit] = t.kind match {
case __TypeKind.LIST | __TypeKind.NON_NULL => t.ofType.fold[Either[__Type, Unit]](Left(t))(isInputType)
case __TypeKind.SCALAR | __TypeKind.ENUM | __TypeKind.INPUT_OBJECT => Right(())
case _ => Left(t)
def validateFields(fields: List[__InputValue]): IO[ValidationError, Unit] =
noDuplicateInputValueName(fields) <*
IO.foreach(fields)(field =>
for {
_ <- doesNotStartWithUnderscore(field.name, "input value", "InputObject")
_ <- onlyInputType(field.`type`())
} yield ()
)

t.inputFields match {
case None | Some(Nil) =>
failValidation(
s"InputObject ${t.name.getOrElse("")} does not have fields",
"An Input Object type must define one or more input fields"
)
case Some(fields) => validateFields(fields)
}
}

def validateFields(fields: List[__InputValue]): IO[ValidationError, Unit] =
duplicateFieldName(fields) <*
private def validateInterface(t: __Type): IO[ValidationError, Unit] = {
def validateInterfaceArgument(arg: __InputValue): IO[ValidationError, Unit] =
for {
_ <- doesNotStartWithUnderscore(arg.name, "argument input value", "Interface")
_ <- onlyOutputType(arg.`type`())
} yield ()

def validateFields(fields: List[__Field]): IO[ValidationError, Unit] =
noDuplicateFieldName(fields) <*
IO.foreach(fields)(field =>
for {
_ <- doesNotStartWithUnderscore(field)
_ <- onlyInputFieldType(field)
_ <- doesNotStartWithUnderscore(field.name, "field", "Interface")
_ <- onlyOutputType(field.`type`())
_ <- IO.foreach(field.args)(validateInterfaceArgument)
} yield ()
)

def duplicateFieldName(fields: List[__InputValue]): IO[ValidationError, Unit] =
fields
.groupBy(_.name)
.collectFirst { case (_, f :: _ :: _) => f }
.fold[IO[ValidationError, Unit]](IO.unit)(duplicateField =>
failValidation(
s"InputObject has repeated fields: ${duplicateField.name}",
"The input field must have a unique name within that Input Object type; no two input fields may share the same name"
)
t.fields(__DeprecatedArgs(Some(true))) match {
case None | Some(Nil) =>
failValidation(
s"message",
"explanatory"
)
case Some(fields) => validateFields(fields)
}
}

def doesNotStartWithUnderscore(field: __InputValue): IO[ValidationError, Unit] =
IO.when(field.name.startsWith("__"))(
private def onlyInputType(`type`: __Type): IO[ValidationError, Unit] = {
// https://spec.graphql.org/June2018/#IsInputType()
def isInputType(t: __Type): Either[__Type, Unit] = {
import __TypeKind._
t.kind match {
case LIST | NON_NULL => t.ofType.fold[Either[__Type, Unit]](Left(t))(isInputType)
case SCALAR | ENUM | INPUT_OBJECT => Right(())
case _ => Left(t)
}
}

IO.whenCase(isInputType(`type`)) {
case Left(errorType) =>
failValidation(
s"InputObject can't start with '__': ${field.name}",
"""The input field must not have a name which begins with the
characters {"__"} (two underscores)"""
s"${errorType.name.getOrElse("")} is of kind ${errorType.kind}, must be an InputType",
"""The input field must accept a type where IsInputType(type) returns true, https://spec.graphql.org/June2018/#IsInputType()"""
)
)
}
}

def onlyInputFieldType(field: __InputValue): IO[ValidationError, Unit] =
IO.whenCase(isInputType(field.`type`())) {
case Left(errorType) =>
failValidation(
s"${errorType.name.getOrElse("")} is of kind ${errorType.kind}, must be an InputType",
"""The input field must accept a type where IsInputType(inputFieldType) returns true, https://spec.graphql.org/June2018/#IsInputType()"""
)
private def onlyOutputType(`type`: __Type): IO[ValidationError, Unit] = {
// https://spec.graphql.org/June2018/#IsOutputType()
def isOutputType(t: __Type): Either[__Type, Unit] = {
import __TypeKind._
t.kind match {
case LIST | NON_NULL => t.ofType.fold[Either[__Type, Unit]](Left(t))(isOutputType)
case SCALAR | OBJECT | INTERFACE | UNION | ENUM => Right(())
case _ => Left(t)
}
}

t.inputFields match {
case None | Some(Nil) =>
IO.whenCase(isOutputType(`type`)) {
case Left(errorType) =>
failValidation(
s"InputObject ${t.name.getOrElse("")} does not have fields",
"An Input Object type must define one or more input fields"
s"${errorType.name.getOrElse("")} is of kind ${errorType.kind}, must be an OutputType",
"""The input field must accept a type where IsOutputType(type) returns true, https://spec.graphql.org/June2018/#IsInputType()"""
)
case Some(fields) => validateFields(fields)
}
}

private def noDuplicateFieldName(fields: List[__Field]) =
noDuplicateName[__Field](fields, _.name)

private def noDuplicateInputValueName(inputValues: List[__InputValue]) =
noDuplicateName[__InputValue](inputValues, _.name)

private def noDuplicateName[T](listOfNamed: List[T], nameExtractor: T => String): IO[ValidationError, Unit] =
listOfNamed
.groupBy(nameExtractor(_))
.collectFirst { case (_, f :: _ :: _) => f }
.fold[IO[ValidationError, Unit]](IO.unit)(duplicate =>
failValidation(
s"InputObject has repeated fields: ${nameExtractor(duplicate)}",
"The input field must have a unique name within that Input Object type; no two input fields may share the same name"
)
)

private def doesNotStartWithUnderscore(name: String, typeName: String, inType: String): IO[ValidationError, Unit] =
IO.when(name.startsWith("__"))(
failValidation(
s"A $typeName in $inType can't start with '__': $name",
s"""The $typeName must not have a name which begins with the characters {"__"} (two underscores)"""
)
)

case class Context(
document: Document,
rootType: RootType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object ValidationSchemaSpec
testM("name can't start with '__'") {
check(
graphQL(resolverWrongMutationUnderscore),
"InputObject can't start with '__': __name"
"A input value in InputObject can't start with '__': __name"
)
},
testM("should only contain types for which IsInputType(type) is true") {
Expand Down

0 comments on commit be11dc4

Please sign in to comment.