diff --git a/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala b/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala index 6fe921555c..78df1c68f6 100644 --- a/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala +++ b/adapters/quick/src/main/scala/caliban/QuickRequestHandler.scala @@ -14,7 +14,6 @@ import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.stream.ZStream import java.nio.charset.StandardCharsets.UTF_8 -import scala.util.Try import scala.util.control.NonFatal final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R, E]) { @@ -103,11 +102,7 @@ final private class QuickRequestHandler[-R, E](interpreter: GraphQLInterpreter[R .flatMap(v => ZIO.attempt(readFromArray[A](v.toArray))) .orElseFail(Response.badRequest) - def parsePath(path: String): List[Either[String, Int]] = - path.split('.').toList.map { segment => - try Right(segment.toInt) - catch { case _: NumberFormatException => Left(segment) } - } + def parsePath(path: String): List[PathValue] = path.split('.').toList.map(PathValue.parse) for { partsMap <- request.body.asMultipartForm.mapBoth(_ => Response.internalServerError, _.map) diff --git a/core/src/main/scala/caliban/CalibanError.scala b/core/src/main/scala/caliban/CalibanError.scala index 8ee56ac64a..f8791fc317 100644 --- a/core/src/main/scala/caliban/CalibanError.scala +++ b/core/src/main/scala/caliban/CalibanError.scala @@ -1,7 +1,7 @@ package caliban import caliban.ResponseValue.{ ListValue, ObjectValue } -import caliban.Value.{ IntValue, StringValue } +import caliban.Value.StringValue import caliban.interop.circe.{ IsCirceDecoder, IsCirceEncoder } import caliban.interop.jsoniter.IsJsoniterCodec import caliban.interop.play.{ IsPlayJsonReads, IsPlayJsonWrites } @@ -68,7 +68,7 @@ object CalibanError { */ case class ExecutionError( msg: String, - path: List[Either[String, Int]] = Nil, + path: List[PathValue] = Nil, locationInfo: Option[LocationInfo] = None, innerThrowable: Option[Throwable] = None, extensions: Option[ObjectValue] = None @@ -80,15 +80,7 @@ object CalibanError { List( "message" -> Some(StringValue(msg)), "locations" -> locationInfo.map(li => ListValue(List(li.toResponseValue))), - "path" -> Some(path).collect { - case p if p.nonEmpty => - ListValue( - p.map { - case Left(value) => StringValue(value) - case Right(value) => IntValue(value) - } - ) - }, + "path" -> Some(path).collect { case p if p.nonEmpty => ListValue(p) }, "extensions" -> extensions ).collect { case (name, Some(v)) => name -> v } ) diff --git a/core/src/main/scala/caliban/Value.scala b/core/src/main/scala/caliban/Value.scala index 5b1551d947..83fbaa1e2a 100644 --- a/core/src/main/scala/caliban/Value.scala +++ b/core/src/main/scala/caliban/Value.scala @@ -1,14 +1,15 @@ package caliban -import scala.util.Try +import caliban.Value.StringValue import caliban.interop.circe._ -import caliban.interop.tapir.IsTapirSchema import caliban.interop.jsoniter.IsJsoniterCodec import caliban.interop.play.{ IsPlayJsonReads, IsPlayJsonWrites } +import caliban.interop.tapir.IsTapirSchema import caliban.interop.zio.{ IsZIOJsonDecoder, IsZIOJsonEncoder } import caliban.rendering.ValueRenderer import zio.stream.Stream +import scala.util.control.NonFatal import scala.util.hashing.MurmurHash3 sealed trait InputValue { self => @@ -104,26 +105,26 @@ object ResponseValue { sealed trait Value extends InputValue with ResponseValue object Value { - case object NullValue extends Value { + case object NullValue extends Value { override def toString: String = "null" } - sealed trait IntValue extends Value { + sealed trait IntValue extends Value { def toInt: Int def toLong: Long def toBigInt: BigInt } - sealed trait FloatValue extends Value { + sealed trait FloatValue extends Value { def toFloat: Float def toDouble: Double def toBigDecimal: BigDecimal } - case class StringValue(value: String) extends Value { + case class StringValue(value: String) extends Value with PathValue { override def toString: String = s""""${value.replace("\"", "\\\"").replace("\n", "\\n")}"""" } - case class BooleanValue(value: Boolean) extends Value { + case class BooleanValue(value: Boolean) extends Value { override def toString: String = if (value) "true" else "false" } - case class EnumValue(value: String) extends Value { + case class EnumValue(value: String) extends Value { override def toString: String = s""""${value.replace("\"", "\\\"")}"""" override def toInputString: String = ValueRenderer.enumInputValueRenderer.render(this) } @@ -132,24 +133,35 @@ object Value { def apply(v: Int): IntValue = IntNumber(v) def apply(v: Long): IntValue = LongNumber(v) def apply(v: BigInt): IntValue = BigIntNumber(v) - def apply(s: String): IntValue = - Try(IntNumber(s.toInt)) orElse - Try(LongNumber(s.toLong)) getOrElse - BigIntNumber(BigInt(s)) - final case class IntNumber(value: Int) extends IntValue { + @deprecated("Use `fromStringUnsafe` instead", "2.5.0") + def apply(s: String): IntValue = fromStringUnsafe(s) + + @throws[NumberFormatException]("if the string is not a valid representation of an integer") + def fromStringUnsafe(s: String): IntValue = + try { + val mod = if (s.charAt(0) == '-') 1 else 0 + val size = s.length - mod + if (size < 10) IntNumber(s.toInt) + else if (size < 19) LongNumber(s.toLong) + else BigIntNumber(BigInt(s)) + } catch { + case NonFatal(_) => BigIntNumber(BigInt(s)) // Should never happen, but we leave it as a fallback + } + + final case class IntNumber(value: Int) extends IntValue with PathValue { override def toInt: Int = value override def toLong: Long = value.toLong override def toBigInt: BigInt = BigInt(value) override def toString: String = value.toString } - final case class LongNumber(value: Long) extends IntValue { + final case class LongNumber(value: Long) extends IntValue { override def toInt: Int = value.toInt override def toLong: Long = value override def toBigInt: BigInt = BigInt(value) override def toString: String = value.toString } - final case class BigIntNumber(value: BigInt) extends IntValue { + final case class BigIntNumber(value: BigInt) extends IntValue { override def toInt: Int = value.toInt override def toLong: Long = value.toLong override def toBigInt: BigInt = value @@ -183,3 +195,49 @@ object Value { } } } + +sealed trait PathValue extends Value { + def isKey: Boolean = this match { + case StringValue(_) => true + case _ => false + } +} + +object PathValue { + def fromEither(either: Either[String, Int]): PathValue = either.fold(Key.apply, Index.apply) + + object Key { + def apply(value: String): PathValue = Value.StringValue(value) + def unapply(value: PathValue): Option[String] = value match { + case Value.StringValue(s) => Some(s) + case _ => None + } + } + object Index { + def apply(value: Int): PathValue = Value.IntValue.IntNumber(value) + def unapply(value: PathValue): Option[Int] = value match { + case Value.IntValue.IntNumber(i) => Some(i) + case _ => None + } + } + + /** + * This function parses a string and returns a PathValue. + * If the string contains only digits, it returns a `PathValue.Index`. + * Otherwise, it returns a `PathValue.Key`. + * + * @param value the string to parse + * @return a PathValue which is either an `Index` if the string is numeric, or a `Key` otherwise + */ + def parse(value: String): PathValue = { + var i = 0 + val size = value.length + while (i < size) { + val c = value.charAt(i) + if (c >= '0' && c <= '9') i += 1 + else return PathValue.Key(value) + } + try PathValue.Index(value.toInt) + catch { case NonFatal(_) => PathValue.Key(value) } // Should never happen, just a fallback + } +} diff --git a/core/src/main/scala/caliban/execution/Deferred.scala b/core/src/main/scala/caliban/execution/Deferred.scala index b0f84f5e00..729544e615 100644 --- a/core/src/main/scala/caliban/execution/Deferred.scala +++ b/core/src/main/scala/caliban/execution/Deferred.scala @@ -1,9 +1,10 @@ package caliban.execution +import caliban.PathValue import caliban.schema.ReducedStep case class Deferred[-R]( - path: List[Either[String, Int]], + path: List[PathValue], step: ReducedStep[R], label: Option[String] ) diff --git a/core/src/main/scala/caliban/execution/Executor.scala b/core/src/main/scala/caliban/execution/Executor.scala index 1311fde84a..f545a7ca81 100644 --- a/core/src/main/scala/caliban/execution/Executor.scala +++ b/core/src/main/scala/caliban/execution/Executor.scala @@ -61,14 +61,14 @@ object Executor { step: Step[R], currentField: Field, arguments: Map[String, InputValue], - path: List[Either[String, Int]] + path: List[PathValue] ): ReducedStep[R] = { def reduceObjectStep(objectName: String, getFieldStep: String => Step[R]): ReducedStep[R] = { def reduceField(f: Field): (String, ReducedStep[R], FieldInfo) = { val field = if (f.name == "__typename") PureStep(StringValue(objectName)) - else reduceStep(getFieldStep(f.name), f, f.arguments, Left(f.aliasedName) :: path) + else reduceStep(getFieldStep(f.name), f, f.arguments, PathValue.Key(f.aliasedName) :: path) (f.aliasedName, field, fieldInfo(f, path, f.directives)) } @@ -107,7 +107,7 @@ object Executor { val lb = List.newBuilder[ReducedStep[R]] var remaining = steps while (remaining ne Nil) { - lb += reduceStep(remaining.head, currentField, arguments, Right(i) :: path) + lb += reduceStep(remaining.head, currentField, arguments, PathValue.Index(i) :: path) i += 1 remaining = remaining.tail } @@ -298,7 +298,7 @@ object Executor { def runIncrementalQuery( step: ReducedStep[R], cache: Cache, - path: List[Either[String, Int]], + path: List[PathValue], label: Option[String] ) = for { @@ -311,10 +311,7 @@ object Executor { } yield (Incremental.Defer( result, errors = resultErrors.reverse, - path = ListValue(path.map { - case Left(s) => StringValue(s) - case Right(i) => IntValue(i) - }.reverse), + path = ListValue(path.reverse), label = label ) -> defers) @@ -369,7 +366,7 @@ object Executor { } } - private def fieldInfo(field: Field, path: List[Either[String, Int]], fieldDirectives: List[Directive]): FieldInfo = + private def fieldInfo(field: Field, path: List[PathValue], fieldDirectives: List[Directive]): FieldInfo = FieldInfo(field.aliasedName, field, path, fieldDirectives, field.parentType) private def reduceList[R](list: List[ReducedStep[R]], areItemsNullable: Boolean): ReducedStep[R] = @@ -388,7 +385,7 @@ object Executor { else ReducedStep.ObjectStep(items) private def effectfulExecutionError( - path: List[Either[String, Int]], + path: List[PathValue], locationInfo: Option[LocationInfo], cause: Cause[Throwable] ): Cause[ExecutionError] = diff --git a/core/src/main/scala/caliban/execution/FieldInfo.scala b/core/src/main/scala/caliban/execution/FieldInfo.scala index 9e7b007821..10bd60ac4e 100644 --- a/core/src/main/scala/caliban/execution/FieldInfo.scala +++ b/core/src/main/scala/caliban/execution/FieldInfo.scala @@ -1,12 +1,13 @@ package caliban.execution +import caliban.PathValue import caliban.introspection.adt.__Type import caliban.parsing.adt.Directive case class FieldInfo( name: String, details: Field, - path: List[Either[String, Int]], + path: List[PathValue], directives: List[Directive] = Nil, parent: Option[__Type] ) diff --git a/core/src/main/scala/caliban/interop/circe/circe.scala b/core/src/main/scala/caliban/interop/circe/circe.scala index bcb785c6a9..ab4e9080db 100644 --- a/core/src/main/scala/caliban/interop/circe/circe.scala +++ b/core/src/main/scala/caliban/interop/circe/circe.scala @@ -120,10 +120,10 @@ object json { } yield LocationInfo(column, line) ) - private implicit val pathEitherDecoder: Decoder[Either[String, Int]] = Decoder.instance { cursor => + private implicit val pathEitherDecoder: Decoder[PathValue] = Decoder.instance { cursor => (cursor.as[String].toOption, cursor.as[Int].toOption) match { - case (Some(s), _) => Right(Left(s)) - case (_, Some(n)) => Right(Right(n)) + case (Some(s), _) => Right(PathValue.Key(s)) + case (_, Some(n)) => Right(PathValue.Index(n)) case _ => Left(DecodingFailure("failed to decode as string or int", cursor.history)) } } @@ -133,7 +133,7 @@ object json { implicit val errorValueDecoder: Decoder[CalibanError] = Decoder.instance(cursor => for { message <- cursor.downField("message").as[String] - path <- cursor.downField("path").as[Option[List[Either[String, Int]]]] + path <- cursor.downField("path").as[Option[List[PathValue]]] locations <- cursor.downField("locations").downArray.as[Option[LocationInfo]] extensions <- cursor.downField("extensions").as[Option[ResponseValue.ObjectValue]] } yield CalibanError.ExecutionError( diff --git a/core/src/main/scala/caliban/interop/jsoniter/jsoniter.scala b/core/src/main/scala/caliban/interop/jsoniter/jsoniter.scala index 4bfaa7ebf9..c43f3a9598 100644 --- a/core/src/main/scala/caliban/interop/jsoniter/jsoniter.scala +++ b/core/src/main/scala/caliban/interop/jsoniter/jsoniter.scala @@ -237,28 +237,28 @@ private[caliban] object ErrorJsoniter { private case class ErrorDTO( message: String, - path: Option[List[Either[String, Int]]], + path: Option[List[PathValue]], locations: Option[List[LocationInfo]], extensions: Option[ResponseValue.ObjectValue] ) - private implicit val eitherCodec: JsonValueCodec[Either[String, Int]] = new JsonValueCodec[Either[String, Int]] { - override def decodeValue(in: JsonReader, default: Either[String, Int]): Either[String, Int] = { + private implicit val pathCodec: JsonValueCodec[PathValue] = new JsonValueCodec[PathValue] { + override def decodeValue(in: JsonReader, default: PathValue): PathValue = { val b = in.nextToken() in.rollbackToken() b match { - case '"' => Left(in.readString(null)) - case x if (x >= '0' && x <= '9') || x == '-' => Right(in.readInt()) + case '"' => PathValue.Key(in.readString(null)) + case x if (x >= '0' && x <= '9') || x == '-' => PathValue.Index(in.readInt()) case _ => in.decodeError("expected int or string") } } - override def encodeValue(x: Either[String, Int], out: JsonWriter): Unit = + override def encodeValue(x: PathValue, out: JsonWriter): Unit = x match { - case Left(s) => out.writeVal(s) - case Right(i) => out.writeVal(i) + case StringValue(s) => out.writeVal(s) + case IntValue.IntNumber(i) => out.writeVal(i) } - override def nullValue: Either[String, Int] = - null.asInstanceOf[Either[String, Int]] + override def nullValue: PathValue = + null.asInstanceOf[PathValue] } private implicit val objectValueCodec: JsonValueCodec[ResponseValue.ObjectValue] = diff --git a/core/src/main/scala/caliban/interop/play/play.scala b/core/src/main/scala/caliban/interop/play/play.scala index b14bd64e9f..235f39582f 100644 --- a/core/src/main/scala/caliban/interop/play/play.scala +++ b/core/src/main/scala/caliban/interop/play/play.scala @@ -161,8 +161,8 @@ object json { .value .toList .map { - case JsString(s) => Left(s) - case JsNumber(bd) => Right(bd.toInt) + case JsString(s) => PathValue.Key(s) + case JsNumber(bd) => PathValue.Index(bd.toInt) case _ => throw new Exception("invalid json") }, locationInfo = e.locations.flatMap(_.headOption), diff --git a/core/src/main/scala/caliban/interop/zio/zio.scala b/core/src/main/scala/caliban/interop/zio/zio.scala index 8b1561ca6c..60cea5c442 100644 --- a/core/src/main/scala/caliban/interop/zio/zio.scala +++ b/core/src/main/scala/caliban/interop/zio/zio.scala @@ -1,7 +1,7 @@ package caliban.interop.zio +import caliban.Value._ import caliban._ -import caliban.Value.{ BooleanValue, EnumValue, FloatValue, IntValue, NullValue, StringValue } import caliban.parsing.adt.LocationInfo import zio.Chunk import zio.json.{ JsonDecoder, JsonEncoder } @@ -377,11 +377,12 @@ private[caliban] object ErrorZioJson { message: String, extensions: Option[ResponseValue.ObjectValue], locations: Option[List[LocationInfo]], - path: Option[List[Either[String, Int]]] + path: Option[List[PathValue]] ) private object ErrorDTO { implicit val locationInfoDecoder: JsonDecoder[LocationInfo] = DeriveJsonDecoder.gen[LocationInfo] - implicit val pathDecoder: JsonDecoder[Either[String, Int]] = JsonDecoder[String].orElseEither(JsonDecoder[Int]) + implicit val pathDecoder: JsonDecoder[PathValue] = + JsonDecoder[String].map(PathValue.Key(_)) orElse JsonDecoder[Int].map(PathValue.Index(_)) implicit val responseObjectValueDecoder: JsonDecoder[ResponseValue.ObjectValue] = ValueZIOJson.Obj.responseDecoder implicit val decoder: JsonDecoder[ErrorDTO] = DeriveJsonDecoder.gen[ErrorDTO] } diff --git a/core/src/main/scala/caliban/parsing/parsers/NumberParsers.scala b/core/src/main/scala/caliban/parsing/parsers/NumberParsers.scala index 3e4d308b31..0c30011a5c 100644 --- a/core/src/main/scala/caliban/parsing/parsers/NumberParsers.scala +++ b/core/src/main/scala/caliban/parsing/parsers/NumberParsers.scala @@ -10,7 +10,7 @@ private[caliban] trait NumberParsers extends StringParsers { def integerPart(implicit ev: P[Any]): P[Unit] = P( (negativeSign.? ~~ "0") | (negativeSign.? ~~ nonZeroDigit ~~ digit.repX) ) - def intValue(implicit ev: P[Any]): P[IntValue] = integerPart.!.map(IntValue(_)) + def intValue(implicit ev: P[Any]): P[IntValue] = integerPart.!.map(IntValue.fromStringUnsafe) def sign(implicit ev: P[Any]): P[Unit] = P("-" | "+") def exponentIndicator(implicit ev: P[Any]): P[Unit] = P(CharIn("eE")) diff --git a/core/src/main/scala/caliban/schema/Step.scala b/core/src/main/scala/caliban/schema/Step.scala index 85d1478ce2..427c512577 100644 --- a/core/src/main/scala/caliban/schema/Step.scala +++ b/core/src/main/scala/caliban/schema/Step.scala @@ -3,7 +3,7 @@ package caliban.schema import caliban.CalibanError.ExecutionError import caliban.Value.NullValue import caliban.execution.{ Field, FieldInfo } -import caliban.{ InputValue, ResponseValue } +import caliban.{ InputValue, PathValue, ResponseValue } import zio.query.ZQuery import zio.stream.ZStream @@ -93,7 +93,7 @@ object ReducedStep { case class DeferStep[-R]( obj: ReducedStep[R], deferred: List[(ReducedStep[R], Option[String])], - path: List[Either[String, Int]] + path: List[PathValue] ) extends ReducedStep[R] // PureStep is both a Step and a ReducedStep so it is defined outside this object diff --git a/core/src/main/scala/caliban/uploads/Upload.scala b/core/src/main/scala/caliban/uploads/Upload.scala index 4715af1604..2ad2d3d3a3 100644 --- a/core/src/main/scala/caliban/uploads/Upload.scala +++ b/core/src/main/scala/caliban/uploads/Upload.scala @@ -1,7 +1,7 @@ package caliban.uploads -import caliban.Value.{ NullValue, StringValue } -import caliban.{ GraphQLRequest, InputValue } +import caliban.Value.{ IntValue, NullValue, StringValue } +import caliban.{ GraphQLRequest, InputValue, PathValue } import zio.stream.ZSink import zio.{ Chunk, RIO, UIO, URIO } @@ -30,7 +30,7 @@ case class FileMeta( */ case class GraphQLUploadRequest( request: GraphQLRequest, - fileMap: List[(String, List[Either[String, Int]])], // This needs to be used to remap the input values + fileMap: List[(String, List[PathValue])], // This needs to be used to remap the input values fileHandle: UIO[Uploads] ) { @@ -43,8 +43,9 @@ case class GraphQLUploadRequest( variables = request.variables.map { vars => fileMap.foldLeft(vars) { case (acc, (name, rest)) => val value = rest match { - case Left("variables") :: Left(key) :: path => acc.get(key).map(loop(_, path, name)).map(key -> _) - case _ => None + case PathValue.Key("variables") :: PathValue.Key(key) :: path => + acc.get(key).map(loop(_, path, name)).map(key -> _) + case _ => None } value.fold(acc)(v => acc + v) } @@ -55,22 +56,22 @@ case class GraphQLUploadRequest( * We need to continue stepping through the path until we find the point where we are supposed * to inject the string. */ - private def loop(value: InputValue, path: List[Either[String, Int]], name: String): InputValue = + private def loop(value: InputValue, path: List[PathValue], name: String): InputValue = path.headOption match { - case Some(Left(key)) => + case Some(StringValue(key)) => value match { case InputValue.ObjectValue(fields) => val v = fields.get(key).fold[InputValue](NullValue)(loop(_, path.drop(1), name)) InputValue.ObjectValue(fields + (key -> v)) case _ => NullValue } - case Some(Right(idx)) => + case Some(IntValue.IntNumber(idx)) => value match { case InputValue.ListValue(values) => InputValue.ListValue(replaceAt(values, idx)(loop(_, path.drop(1), name))) case _ => NullValue } - case None => + case None => // If we are out of values then we are at the end of the path, so we need to replace this current node // with a string node containing the file name StringValue(name) diff --git a/core/src/main/scala/caliban/wrappers/ApolloCaching.scala b/core/src/main/scala/caliban/wrappers/ApolloCaching.scala index 9155d84611..3310d89958 100644 --- a/core/src/main/scala/caliban/wrappers/ApolloCaching.scala +++ b/core/src/main/scala/caliban/wrappers/ApolloCaching.scala @@ -7,7 +7,7 @@ import caliban.execution.FieldInfo import caliban.parsing.adt.Directive import caliban.schema.Annotations.GQLDirective import caliban.wrappers.Wrapper.{ EffectfulWrapper, FieldWrapper, OverallWrapper } -import caliban.{ CalibanError, GraphQLRequest, GraphQLResponse, ResponseValue } +import caliban._ import zio._ import zio.query.ZQuery @@ -67,7 +67,7 @@ object ApolloCaching { case class CacheHint( fieldName: String = "", - path: List[Either[String, Int]] = Nil, + path: List[PathValue] = Nil, maxAge: Duration, scope: CacheScope ) { @@ -75,7 +75,7 @@ object ApolloCaching { def toResponseValue: ResponseValue = ObjectValue( List( - "path" -> ListValue((Left(fieldName) :: path).reverse.map(_.fold(StringValue.apply, IntValue(_)))), + "path" -> ListValue((PathValue.Key(fieldName) :: path).reverse), "maxAge" -> IntValue(maxAge.toMillis / 1000), "scope" -> StringValue(scope match { case CacheScope.Private => "PRIVATE" diff --git a/core/src/main/scala/caliban/wrappers/ApolloTracing.scala b/core/src/main/scala/caliban/wrappers/ApolloTracing.scala index 1a7605717a..f86211b964 100644 --- a/core/src/main/scala/caliban/wrappers/ApolloTracing.scala +++ b/core/src/main/scala/caliban/wrappers/ApolloTracing.scala @@ -9,7 +9,7 @@ import caliban.execution.{ ExecutionRequest, FieldInfo } import caliban.parsing.adt.Document import caliban.rendering.DocumentRenderer import caliban.wrappers.Wrapper.{ EffectfulWrapper, FieldWrapper, OverallWrapper, ParsingWrapper, ValidationWrapper } -import caliban.{ CalibanError, GraphQLRequest, GraphQLResponse, ResponseValue } +import caliban.{ CalibanError, GraphQLRequest, GraphQLResponse, PathValue, ResponseValue } import zio._ import zio.query.ZQuery @@ -69,7 +69,7 @@ object ApolloTracing { } case class Resolver( - path: List[Either[String, Int]] = Nil, + path: List[PathValue] = Nil, parentType: String = "", fieldName: String = "", returnType: String = "", @@ -79,10 +79,7 @@ object ApolloTracing { def toResponseValue: ResponseValue = ObjectValue( List( - "path" -> ListValue((Left(fieldName) :: path).reverse.map { - case Left(s) => StringValue(s) - case Right(i) => IntValue(i) - }), + "path" -> ListValue((PathValue.Key(fieldName) :: path).reverse), "parentType" -> StringValue(parentType), "fieldName" -> StringValue(fieldName), "returnType" -> StringValue(returnType), diff --git a/core/src/main/scala/caliban/wrappers/FieldMetrics.scala b/core/src/main/scala/caliban/wrappers/FieldMetrics.scala index cac8ed7ed8..a44c4f8904 100644 --- a/core/src/main/scala/caliban/wrappers/FieldMetrics.scala +++ b/core/src/main/scala/caliban/wrappers/FieldMetrics.scala @@ -1,8 +1,9 @@ package caliban.wrappers +import caliban.Value.StringValue +import caliban._ import caliban.execution.FieldInfo import caliban.wrappers.Wrapper.OverallWrapper -import caliban.{ CalibanError, GraphQLRequest, GraphQLResponse, ResponseValue } import zio._ import zio.metrics.MetricKeyType.Histogram import zio.metrics.{ Metric, MetricKey, MetricLabel } @@ -27,9 +28,9 @@ object FieldMetrics { def recordFailures(fieldNames: List[String]): UIO[Unit] = ZIO.foreachDiscard(fieldNames)(fn => failed.tagged("field", fn).increment) - def recordSuccesses(nodeOffsets: Map[Vector[Either[String, Int]], Long], timings: List[Timing]): UIO[Unit] = + def recordSuccesses(nodeOffsets: Map[Vector[PathValue], Long], timings: List[Timing]): UIO[Unit] = ZIO.foreachDiscard(timings) { timing => - val d = timing.duration - nodeOffsets.getOrElse(timing.path :+ Left(timing.name), 0L) + val d = timing.duration - nodeOffsets.getOrElse(timing.path :+ StringValue(timing.name), 0L) succeeded.tagged(Set(MetricLabel("field", timing.fullName))).increment *> duration.tagged(Set(MetricLabel("field", timing.fullName))).update(d / 1e9) } @@ -44,7 +45,7 @@ object FieldMetrics { private case class Timing( name: String, - path: Vector[Either[String, Int]], + path: Vector[PathValue], fullName: String, duration: Long ) @@ -84,8 +85,9 @@ object FieldMetrics { .forkDaemon } - private def resolveNodeOffsets(timings: List[Timing]): Map[Vector[Either[String, Int]], Long] = { - val map = new java.util.HashMap[Vector[Either[String, Int]], Long]() + private def resolveNodeOffsets(timings: List[Timing]): Map[Vector[PathValue], Long] = { + + val map = new java.util.HashMap[Vector[PathValue], Long]() var remaining = timings while (!remaining.isEmpty) { val t = remaining.head @@ -96,7 +98,7 @@ object FieldMetrics { val segment = iter.next() if (!iter.hasNext) { continue = false // Last element of `.inits` is an empty list - } else if (segment.last.isLeft) { // List indices are not fields so we don't care about recording their offset + } else if (segment.last.isKey) { // List indices are not fields so we don't care about recording their offset map.compute( segment, (_, v) => diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index 11ee7c7679..b6cbf07765 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -468,7 +468,7 @@ object ExecutionSpec extends ZIOSpecDefault { response.errors == List( ExecutionError( "Effect failure", - List(Left("a"), Left("b"), Left("c")), + List(StringValue("a"), StringValue("b"), StringValue("c")), Some(LocationInfo(21, 5)), Some(e) ) @@ -784,7 +784,7 @@ object ExecutionSpec extends ZIOSpecDefault { assertTrue(result.data.toString == """{"user1":{"name":"user","friends":["friend"]},"user2":null}""") && assertTrue( result.errors.collectFirst { case e: ExecutionError => e }.map(_.path).get == - List(Left("user2"), Left("friends")) + List(StringValue("user2"), StringValue("friends")) ) ) }, diff --git a/core/src/test/scala/caliban/interop/circe/GraphQLResponseCirceSpec.scala b/core/src/test/scala/caliban/interop/circe/GraphQLResponseCirceSpec.scala index 9233cafebe..3be4ab0776 100644 --- a/core/src/test/scala/caliban/interop/circe/GraphQLResponseCirceSpec.scala +++ b/core/src/test/scala/caliban/interop/circe/GraphQLResponseCirceSpec.scala @@ -4,7 +4,7 @@ import caliban.CalibanError.ExecutionError import caliban.ResponseValue.{ ListValue, ObjectValue } import caliban.Value.{ IntValue, StringValue } import caliban.parsing.adt.LocationInfo -import caliban.{ CalibanError, GraphQLResponse } +import caliban.{ CalibanError, GraphQLResponse, PathValue } import io.circe._ import io.circe.parser.decode import io.circe.syntax._ @@ -89,7 +89,7 @@ object GraphQLResponseCirceSpec extends ZIOSpecDefault { errors = List( ExecutionError( "boom", - path = List(Left("step"), Right(0)), + path = List(PathValue.Key("step"), PathValue.Index(0)), locationInfo = Some(LocationInfo(1, 2)), extensions = Some( ObjectValue( diff --git a/core/src/test/scala/caliban/interop/jsoniter/GraphQLResponseJsoniterSpec.scala b/core/src/test/scala/caliban/interop/jsoniter/GraphQLResponseJsoniterSpec.scala index 0c25515e43..0305972247 100644 --- a/core/src/test/scala/caliban/interop/jsoniter/GraphQLResponseJsoniterSpec.scala +++ b/core/src/test/scala/caliban/interop/jsoniter/GraphQLResponseJsoniterSpec.scala @@ -4,7 +4,7 @@ import caliban.CalibanError.ExecutionError import caliban.ResponseValue.{ ListValue, ObjectValue } import caliban.Value.{ IntValue, StringValue } import caliban.parsing.adt.LocationInfo -import caliban.{ CalibanError, GraphQLResponse, TestUtils } +import caliban.{ CalibanError, GraphQLResponse, PathValue, TestUtils } import com.github.plokhotnyuk.jsoniter_scala.core._ import zio.test.Assertion.equalTo import zio.test.{ assert, assertTrue, ZIOSpecDefault } @@ -72,11 +72,11 @@ object GraphQLResponseJsoniterSpec extends ZIOSpecDefault { assert(readFromString[GraphQLResponse[CalibanError]](req))( equalTo( GraphQLResponse( - data = ObjectValue(List("value" -> IntValue("42"))), + data = ObjectValue(List("value" -> IntValue(42))), errors = List( ExecutionError( "boom", - path = List(Left("step"), Right(0)), + path = List(PathValue.Key("step"), PathValue.Index(0)), locationInfo = Some(LocationInfo(1, 2)), extensions = Some( ObjectValue( diff --git a/core/src/test/scala/caliban/interop/play/GraphQLResponsePlaySpec.scala b/core/src/test/scala/caliban/interop/play/GraphQLResponsePlaySpec.scala index c043dda4d4..9951736fad 100644 --- a/core/src/test/scala/caliban/interop/play/GraphQLResponsePlaySpec.scala +++ b/core/src/test/scala/caliban/interop/play/GraphQLResponsePlaySpec.scala @@ -2,8 +2,8 @@ package caliban.interop.play import caliban.CalibanError.ExecutionError import caliban.ResponseValue.{ ListValue, ObjectValue } -import caliban.{ CalibanError, GraphQLResponse, Value } -import caliban.Value.StringValue +import caliban.{ CalibanError, GraphQLResponse, PathValue, Value } +import caliban.Value.{ IntValue, StringValue } import caliban.parsing.adt.LocationInfo import play.api.libs.json._ import zio.test.Assertion._ @@ -87,11 +87,11 @@ object GraphQLResponsePlaySpec extends ZIOSpecDefault { isRight( equalTo( GraphQLResponse( - data = ObjectValue(List("value" -> Value.IntValue("42"))), + data = ObjectValue(List("value" -> Value.IntValue(42))), errors = List( ExecutionError( "boom", - path = List(Left("step"), Right(0)), + path = List(PathValue.Key("step"), PathValue.Index(0)), locationInfo = Some(LocationInfo(1, 2)), extensions = Some( ObjectValue( diff --git a/core/src/test/scala/caliban/interop/zio/GraphQLResponseZIOSpec.scala b/core/src/test/scala/caliban/interop/zio/GraphQLResponseZIOSpec.scala index 52dd2fa2ae..c164829ad7 100644 --- a/core/src/test/scala/caliban/interop/zio/GraphQLResponseZIOSpec.scala +++ b/core/src/test/scala/caliban/interop/zio/GraphQLResponseZIOSpec.scala @@ -4,7 +4,7 @@ import caliban.CalibanError.ExecutionError import caliban.ResponseValue.{ ListValue, ObjectValue } import caliban.Value.{ IntValue, StringValue } import caliban.parsing.adt.LocationInfo -import caliban.{ CalibanError, GraphQLResponse } +import caliban.{ CalibanError, GraphQLResponse, PathValue } import zio.json._ import zio.test.Assertion.{ equalTo, isRight } import zio.test.{ assert, assertTrue, ZIOSpecDefault } @@ -73,11 +73,11 @@ object GraphQLResponseZIOSpec extends ZIOSpecDefault { isRight( equalTo( GraphQLResponse( - data = ObjectValue(List("value" -> IntValue("42"))), + data = ObjectValue(List("value" -> IntValue(42))), errors = List( ExecutionError( "boom", - path = List(Left("step"), Right(0)), + path = List(PathValue.Key("step"), PathValue.Index(0)), locationInfo = Some(LocationInfo(1, 2)), extensions = Some( ObjectValue( diff --git a/federation/src/main/scala/caliban/federation/tracing/ApolloFederatedTracing.scala b/federation/src/main/scala/caliban/federation/tracing/ApolloFederatedTracing.scala index 80aa6456d7..1a2b44d255 100644 --- a/federation/src/main/scala/caliban/federation/tracing/ApolloFederatedTracing.scala +++ b/federation/src/main/scala/caliban/federation/tracing/ApolloFederatedTracing.scala @@ -1,17 +1,16 @@ package caliban.federation.tracing -import caliban.CalibanError import caliban.CalibanError.ExecutionError -import caliban.execution.FieldInfo import caliban.ResponseValue.ObjectValue -import caliban.Value.StringValue +import caliban.Value.{ IntValue, StringValue } +import caliban._ +import caliban.execution.FieldInfo import caliban.wrappers.Wrapper.{ EffectfulWrapper, FieldWrapper, OverallWrapper } -import caliban.{ GraphQLRequest, GraphQLResponse, ResponseValue } import com.google.protobuf.timestamp.Timestamp import mdg.engine.proto.reports.Trace import mdg.engine.proto.reports.Trace.{ Error, Location, Node } -import zio.query.ZQuery import zio._ +import zio.query.ZQuery import java.util.Base64 import java.util.concurrent.TimeUnit @@ -101,7 +100,7 @@ object ApolloFederatedTracing { ref.update(state => state.copy( root = state.root.insert( - (Left(fieldInfo.name) :: fieldInfo.path).toVector, + (PathValue.Key(fieldInfo.name) :: fieldInfo.path).toVector, Node( id = id, startTime = startTime - state.startTime, @@ -126,19 +125,22 @@ object ApolloFederatedTracing { ) } - private type VPath = Vector[Either[String, Int]] + private type VPath = Vector[PathValue] private final case class Tracing(root: NodeTrie, startTime: Long = 0) - private final case class NodeTrie(node: Option[Node], children: Map[Either[String, Int], NodeTrie]) { + private final case class NodeTrie(node: Option[Node], children: Map[PathValue, NodeTrie]) { def insert(path: VPath, node: Node): NodeTrie = NodeTrie.loopInsert(this, path, node, path.length - 1) def reduce(initial: Node = Node()): Node = node.getOrElse(initial).copy(child = children.values.map(_.reduce(Node())).toList) } private object NodeTrie { - val empty: NodeTrie = NodeTrie(None, Map.empty[Either[String, Int], NodeTrie]) + val empty: NodeTrie = NodeTrie(None, Map.empty[PathValue, NodeTrie]) - private def newEmptyNode(id: Either[String, Int]) = - Node(id = id.fold(Node.Id.ResponseName.apply, Node.Id.Index.apply)) + private def newEmptyNode(id: PathValue) = + Node(id = id match { + case StringValue(s) => Node.Id.ResponseName(s) + case IntValue.IntNumber(value) => Node.Id.Index(value) + }) private def loopInsert(trie: NodeTrie, path: VPath, value: Node, step: Int): NodeTrie = if (step == -1) { diff --git a/interop/tapir/src/main/scala/caliban/interop/tapir/HttpUploadInterpreter.scala b/interop/tapir/src/main/scala/caliban/interop/tapir/HttpUploadInterpreter.scala index 61bda5c138..a8b11c30ce 100644 --- a/interop/tapir/src/main/scala/caliban/interop/tapir/HttpUploadInterpreter.scala +++ b/interop/tapir/src/main/scala/caliban/interop/tapir/HttpUploadInterpreter.scala @@ -1,5 +1,6 @@ package caliban.interop.tapir +import caliban.Value.{ IntValue, StringValue } import caliban._ import caliban.interop.tapir.TapirAdapter._ import caliban.uploads.{ FileMeta, GraphQLUploadRequest, Uploads } @@ -23,8 +24,7 @@ sealed trait HttpUploadInterpreter[-R, E] { self => serverRequest: ServerRequest )(implicit streamConstructor: StreamConstructor[BS]): ZIO[R, TapirResponse, CalibanResponse[BS]] - private def parsePath(path: String): List[Either[String, Int]] = - path.split('.').map(c => Try(c.toInt).toEither.left.map(_ => c)).toList + private def parsePath(path: String): List[PathValue] = path.split('.').map(PathValue.parse).toList def serverEndpoint[R1 <: R, S](streams: Streams[S])(implicit streamConstructor: StreamConstructor[streams.BinaryStream],