sangria-graphql/sangria

Question/Bug Around Custom `FromInput`s and `ResultMarshaller`s

Closed this issue · 4 comments

It seems to me that it should be impossible to define a custom FromInput and ResultMarshaller on an Argument unless
the custom ResultMarshaller has it's Node type defined as Any. This would mean that
sangria.marshalling.circe.circeFromInput from sangria-circe can't be used for Arguments.

Looking at sangria.execution.ValueCollector#getArgumentValues we can see that we call
sangria.execution.ValueCoercionHelper#resolveMapValue with marshaller defined as CoercedScalaResultMarshaller.default.
We also pass in our custom FromInput Marshaller under firstKindMarshaller. The problem is that valueMap is defined as
fromInput.fromResult which means if we have a custom marshaller we expect the type passed into fromInput.fromResult
to be our custom marshaller Node type which causes a ClassCastException.

Example

If we define a FromInput as

object OurMarshaller extends ResultMarshaller {
  type Node = OurType
}

object OurFromInput extends FromInput[Any] {
  val marshaller: ResultMarshaller = OurMarshaller

  def fromResult(node: marshaller.Node): Any
    ???
  }
}

When we call resolveMapValue with types annotated

// sangria.execution.ValueCollector#getArgumentValue

resolveMapValue(
  ...
  marshaller, // ResultMarshaller { type Node = Any }
  fromInput.marshaller, // ResultMarshaller { type Node = OurType }
  valueMap = fromInput.fromResult, // OurType => Any
  ...
)(
  acc,
  
  astValue.map(
    coerceInputValue(
      argDef.argumentType,
      argPath,
      _,
      forAstNode,
      Some(variables),
      marshaller,
      fromInput.marshaller,
      fromScalarMiddleware = fromScalarMiddleware,
      isArgument = true))
)

With types annotated

// sangria.execution.ValueCoercionHelper.resolveMapValue

def resolveMapValue(
     ...
     marshaller: ResultMarshaller, // ResultMarshaller { type Node = Any }
     firstKindMarshaller: ResultMarshaller, // ResultMarshaller { type Node = OurType }
     valueMap: Nothing => Any = defaultValueMapFn, // OurType => Any
     ...
   )(
     value: Option[Either[Vector[Violation], Trinary[marshaller.Node]]] // Option[Either[Vector[Violation], Trinary[Any]]]
   ): marshaller.MapBuilder = {
  // right here we cast OurType => Any to Any => Any
  val valueMapTyped = valueMap.asInstanceOf[Any => marshaller.Node]
  
  // when trying to use valueMapTyped, we are calling it with value, extract into v which has the type Any
  marshaller.addMapNodeElem(acc, fieldName, valueMapTyped(v), ofType.isOptional)
}

This means we're effectively calling

OurFromInput.fromResult(v: Any)

when v must have the type OurType to actually succeed. This causes a ClassCastException

To me it seems that it isn't possible to use our own custom ResultMarshaller that doesn't take in Any for coercing
Arguments. This would mean that sangria.marshalling.circe.circeFromInput wouldn't work for Arguments either.

Am I going about defining a custom ResultMarshaller and using it for Arguments the correct way? Is there some way that
circeFromInput could be used for parsing Arguments?

yanns commented

Are you maybe looking at federation? If yes you can have a look at https://github.com/sangria-graphql/sangria-federated

Hi @yanns I've been looking at the Argument class and more specifically how we resolve its value at execution time ValueCoercionHelper.resolveMapValue I'm trying to write my own custom FromInput, but in the process of trying to figure this out, I started wondering if it's even possible to use sangria.marshalling.circe.circeFromInput as the FromInput for an Argument?

yanns commented

Maybe @xsoufiane can help here?

Hi @dylanowen hope that I am understanding you correctly, did you check this part of the doc. From what I saw, the FromInput in Circe is only for Json, and If you wanna define a FromInput for a different type, then you define another instance FromInput instance for it. First, the input is unmarshalled to the correct Node type (in our case Json), then your FromInput[T] instance takes care of deserializing T from Node.