Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement path via PathValue instead of Either[String, Int] for paths #2048

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 3 additions & 11 deletions core/src/main/scala/caliban/CalibanError.scala
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -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
Expand All @@ -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 }
)
Expand Down
88 changes: 73 additions & 15 deletions core/src/main/scala/caliban/Value.scala
Original file line number Diff line number Diff line change
@@ -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 =>
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
3 changes: 2 additions & 1 deletion core/src/main/scala/caliban/execution/Deferred.scala
Original file line number Diff line number Diff line change
@@ -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]
)
17 changes: 7 additions & 10 deletions core/src/main/scala/caliban/execution/Executor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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] =
Expand All @@ -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] =
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/scala/caliban/execution/FieldInfo.scala
Original file line number Diff line number Diff line change
@@ -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]
)
8 changes: 4 additions & 4 deletions core/src/main/scala/caliban/interop/circe/circe.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand All @@ -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(
Expand Down
20 changes: 10 additions & 10 deletions core/src/main/scala/caliban/interop/jsoniter/jsoniter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/caliban/interop/play/play.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 4 additions & 3 deletions core/src/main/scala/caliban/interop/zio/zio.scala
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -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]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Loading