Skip to content

Commit

Permalink
Custom Scalar Specification URLs (#1171)
Browse files Browse the repository at this point in the history
* Custom Scalar Specification URLs

* Use fold
  • Loading branch information
ghostdogpr authored Nov 25, 2021
1 parent 509ea49 commit 1fe632c
Show file tree
Hide file tree
Showing 12 changed files with 61 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ trait SchemaDerivation[R] extends LowPriorityDerivedSchema {
makeEnum(
Some(getName(ctx)),
getDescription(ctx),
subtypes.collect { case (__Type(_, Some(name), description, _, _, _, _, _, _, _, _), annotations) =>
subtypes.collect { case (__Type(_, Some(name), description, _, _, _, _, _, _, _, _, _), annotations) =>
__EnumValue(
name,
description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ trait SchemaDerivation[A] {
makeEnum(
Some(getName(annotations, info)),
getDescription(annotations),
subTypes.collect { case (name, __Type(_, _, description, _, _, _, _, _, _, _, _), annotations) =>
subTypes.collect { case (name, __Type(_, _, description, _, _, _, _, _, _, _, _, _), annotations) =>
__EnumValue(
name,
description,
Expand Down
8 changes: 7 additions & 1 deletion core/src/main/scala/caliban/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ object Rendering {
case __TypeKind.SCALAR =>
t.name.flatMap(name =>
if (isBuiltinScalar(name)) None
else Some(s"""${renderDescription(t.description)}scalar $name""".stripMargin)
else
Some(
s"""${renderDescription(t.description)}scalar $name${renderSpecifiedBy(t.specifiedBy)}""".stripMargin
)
)
case __TypeKind.NON_NULL => None
case __TypeKind.LIST => None
Expand Down Expand Up @@ -91,6 +94,9 @@ object Rendering {
case Some(value) => if (value.contains("\n")) s"""\"\"\"$value\"\"\" """ else s""""$value" """
}

private def renderSpecifiedBy(specifiedBy: Option[String]): String =
specifiedBy.fold("")(url => s""" @specifiedBy(url: "$url")""")

private def renderDirectiveArgument(value: InputValue): Option[String] = value match {
case InputValue.ListValue(values) =>
Some(values.flatMap(renderDirectiveArgument).mkString("[", ",", "]"))
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/scala/caliban/introspection/Introspector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ object Introspector extends IntrospectionDerivation {
),
Set(__DirectiveLocation.FIELD, __DirectiveLocation.FRAGMENT_SPREAD, __DirectiveLocation.INLINE_FRAGMENT),
List(__InputValue("if", None, () => Types.makeNonNull(Types.boolean), None))
),
__Directive(
"specifiedBy",
Some(
"The @specifiedBy directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar types. The URL should point to a human-readable specification of the data format, serialization, and coercion rules. It must not appear on built-in scalar types."
),
Set(__DirectiveLocation.SCALAR),
List(__InputValue("url", None, () => Types.makeNonNull(Types.string), None))
)
)

Expand Down
15 changes: 14 additions & 1 deletion core/src/main/scala/caliban/introspection/adt/__Type.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package caliban.introspection.adt

import caliban.Value.StringValue
import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition
import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition._
import caliban.parsing.adt.Type.{ ListType, NamedType }
Expand All @@ -15,6 +16,7 @@ case class __Type(
enumValues: __DeprecatedArgs => Option[List[__EnumValue]] = _ => None,
inputFields: Option[List[__InputValue]] = None,
ofType: Option[__Type] = None,
specifiedBy: Option[String] = None,
directives: Option[List[Directive]] = None,
origin: Option[String] = None
) {
Expand All @@ -28,6 +30,7 @@ case class __Type(
args => (enumValues(args) ++ that.enumValues(args)).reduceOption(_ ++ _),
(inputFields ++ that.inputFields).reduceOption(_ ++ _),
(ofType ++ that.ofType).reduceOption(_ |+| _),
(specifiedBy ++ that.specifiedBy).reduceOption((_, b) => b),
(directives ++ that.directives).reduceOption(_ ++ _),
(origin ++ that.origin).reduceOption((_, b) => b)
)
Expand All @@ -46,7 +49,17 @@ case class __Type(
def toTypeDefinition: Option[TypeDefinition] =
kind match {
case __TypeKind.SCALAR =>
Some(ScalarTypeDefinition(description, name.getOrElse(""), directives.getOrElse(Nil)))
Some(
ScalarTypeDefinition(
description,
name.getOrElse(""),
directives
.getOrElse(Nil) ++
specifiedBy
.map(url => Directive("specifiedBy", Map("url" -> StringValue(url)), directives.size))
.toList
)
)
case __TypeKind.OBJECT =>
Some(
ObjectTypeDefinition(
Expand Down
35 changes: 21 additions & 14 deletions core/src/main/scala/caliban/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,18 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
* Creates a scalar schema for a type `A`
* @param name name of the scalar type
* @param description description of the scalar type
* @param specifiedBy URL of the scalar specification
* @param makeResponse function from `A` to [[ResponseValue]] that defines how to resolve `A`
*/
def scalarSchema[A](name: String, description: Option[String], makeResponse: A => ResponseValue): Schema[Any, A] =
def scalarSchema[A](
name: String,
description: Option[String],
specifiedBy: Option[String],
makeResponse: A => ResponseValue
): Schema[Any, A] =
new Schema[Any, A] {
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = makeScalar(name, description)
override def toType(isInput: Boolean, isSubscription: Boolean): __Type =
makeScalar(name, description, specifiedBy)
override def resolve(value: A): Step[Any] = PureStep(makeResponse(value))
}

Expand Down Expand Up @@ -239,18 +246,18 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
directives
)

implicit val unitSchema: Schema[Any, Unit] = scalarSchema("Unit", None, _ => ObjectValue(Nil))
implicit val booleanSchema: Schema[Any, Boolean] = scalarSchema("Boolean", None, BooleanValue.apply)
implicit val stringSchema: Schema[Any, String] = scalarSchema("String", None, StringValue.apply)
implicit val uuidSchema: Schema[Any, UUID] = scalarSchema("ID", None, uuid => StringValue(uuid.toString))
implicit val shortSchema: Schema[Any, Short] = scalarSchema("Short", None, IntValue(_))
implicit val intSchema: Schema[Any, Int] = scalarSchema("Int", None, IntValue(_))
implicit val longSchema: Schema[Any, Long] = scalarSchema("Long", None, IntValue(_))
implicit val bigIntSchema: Schema[Any, BigInt] = scalarSchema("BigInt", None, IntValue(_))
implicit val doubleSchema: Schema[Any, Double] = scalarSchema("Float", None, FloatValue(_))
implicit val floatSchema: Schema[Any, Float] = scalarSchema("Float", None, FloatValue(_))
implicit val bigDecimalSchema: Schema[Any, BigDecimal] = scalarSchema("BigDecimal", None, FloatValue(_))
implicit val uploadSchema: Schema[Any, Upload] = scalarSchema("Upload", None, _ => StringValue("<upload>"))
implicit val unitSchema: Schema[Any, Unit] = scalarSchema("Unit", None, None, _ => ObjectValue(Nil))
implicit val booleanSchema: Schema[Any, Boolean] = scalarSchema("Boolean", None, None, BooleanValue.apply)
implicit val stringSchema: Schema[Any, String] = scalarSchema("String", None, None, StringValue.apply)
implicit val uuidSchema: Schema[Any, UUID] = scalarSchema("ID", None, None, uuid => StringValue(uuid.toString))
implicit val shortSchema: Schema[Any, Short] = scalarSchema("Short", None, None, IntValue(_))
implicit val intSchema: Schema[Any, Int] = scalarSchema("Int", None, None, IntValue(_))
implicit val longSchema: Schema[Any, Long] = scalarSchema("Long", None, None, IntValue(_))
implicit val bigIntSchema: Schema[Any, BigInt] = scalarSchema("BigInt", None, None, IntValue(_))
implicit val doubleSchema: Schema[Any, Double] = scalarSchema("Float", None, None, FloatValue(_))
implicit val floatSchema: Schema[Any, Float] = scalarSchema("Float", None, None, FloatValue(_))
implicit val bigDecimalSchema: Schema[Any, BigDecimal] = scalarSchema("BigDecimal", None, None, FloatValue(_))
implicit val uploadSchema: Schema[Any, Upload] = scalarSchema("Upload", None, None, _ => StringValue("<upload>"))

implicit def optionSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, Option[A]] = new Schema[R0, Option[A]] {
override def optional: Boolean = true
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/caliban/schema/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ object Types {
/**
* Creates a new scalar type with the given name.
*/
def makeScalar(name: String, description: Option[String] = None): __Type =
__Type(__TypeKind.SCALAR, Some(name), description)
def makeScalar(name: String, description: Option[String] = None, specifiedBy: Option[String] = None): __Type =
__Type(__TypeKind.SCALAR, Some(name), description, specifiedBy = specifiedBy)

val boolean: __Type = makeScalar("Boolean")
val string: __Type = makeScalar("String")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ object FragmentValidator {

fields.foreach {
case field @ SelectedField(
__Type(_, Some(name), _, _, _, _, _, _, _, _, _),
__Type(_, Some(name), _, _, _, _, _, _, _, _, _, _),
_,
_
) if isConcrete(field.parentType) =>
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/scala/caliban/RenderingSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ object RenderingSpec extends DefaultRunnableSpec {
|}
|
|"Description of custom scalar emphasizing proper captain ship names"
|scalar CaptainShipName
|scalar CaptainShipName @specifiedBy(url: "http://someUrl")
|
|union Role = Captain | Engineer | Mechanic | Pilot
|
Expand Down
1 change: 1 addition & 0 deletions core/src/test/scala/caliban/TestUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ object TestUtils {
implicit val captainShipNameSchema: Schema[Any, CaptainShipName] = scalarSchema(
"CaptainShipName",
Some("Description of custom scalar emphasizing proper captain ship names"),
Some("http://someUrl"),
name => StringValue(name.value)
)
}
Expand Down
Loading

0 comments on commit 1fe632c

Please sign in to comment.