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

Reuse the existing decoders #301

Merged
merged 11 commits into from
May 31, 2024
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
134 changes: 83 additions & 51 deletions core/shared/src/main/scala/org/virtuslab/yaml/YamlDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,29 @@ import org.virtuslab.yaml.Node._
/**
* A type class that provides a conversion from a [[Node]] into given type [[T]]
*/
trait YamlDecoder[T] {
trait YamlDecoder[T] { self =>
def construct(node: Node)(implicit
settings: LoadSettings = LoadSettings.empty
): Either[ConstructError, T]

final def orElse[T1 >: T](that: => YamlDecoder[T1]): YamlDecoder[T1] = new YamlDecoder[T1] {
override def construct(
node: Node
)(implicit settings: LoadSettings): Either[ConstructError, T1] =
self.construct(node) match {
case result @ Right(_) => result
case Left(_) => that.construct(node)
}
}

final def widen[T1 >: T]: YamlDecoder[T1] = self.asInstanceOf[YamlDecoder[T1]]

final def map[T1](f: T => T1): YamlDecoder[T1] = new YamlDecoder[T1] {
override def construct(node: Node)(implicit
settings: LoadSettings
): Either[ConstructError, T1] =
self.construct(node).map(f)
}
}

object YamlDecoder extends YamlDecoderCompanionCrossCompat {
Expand All @@ -39,79 +58,92 @@ object YamlDecoder extends YamlDecoderCompanionCrossCompat {
)

implicit def forInt: YamlDecoder[Int] = YamlDecoder { case s @ ScalarNode(value, _) =>
value.toIntOption.toRight(cannotParse(value, "Int", s))
Try(java.lang.Integer.decode(value.replaceAll("_", "")).toInt).toEither.left
.map(ConstructError.from(_, "Int", s))
}

implicit def forLong: YamlDecoder[Long] = YamlDecoder { case s @ ScalarNode(value, _) =>
value.toLongOption.toRight(cannotParse(value, "Long", s))
Try(java.lang.Long.decode(value.replaceAll("_", "")).toLong).toEither.left
.map(ConstructError.from(_, "Long", s))
}

implicit def forDouble: YamlDecoder[Double] = YamlDecoder { case s @ ScalarNode(value, _) =>
value.toDoubleOption.toRight(cannotParse(value, "Double", s))
Try(java.lang.Double.parseDouble(value.replaceAll("_", ""))).toEither.left
.map(ConstructError.from(_, "Double", s))
}

implicit def forFloat: YamlDecoder[Float] = YamlDecoder { case s @ ScalarNode(value, _) =>
value.toFloatOption.toRight(cannotParse(value, "Float", s))
Try(java.lang.Float.parseFloat(value.replaceAll("_", ""))).toEither.left
.map(ConstructError.from(_, "Float", s))
}

implicit def forShort: YamlDecoder[Short] = YamlDecoder { case s @ ScalarNode(value, _) =>
value.toShortOption.toRight(cannotParse(value, "Short", s))
Try(java.lang.Short.decode(value.replaceAll("_", "")).toShort).toEither.left
.map(ConstructError.from(_, "Short", s))
}

implicit def forByte: YamlDecoder[Byte] = YamlDecoder { case s @ ScalarNode(value, _) =>
value.toByteOption.toRight(cannotParse(value, "Byte", s))
Try(java.lang.Byte.decode(value.replaceAll("_", "")).toByte).toEither.left
.map(ConstructError.from(_, "Byte", s))
}

implicit def forBoolean: YamlDecoder[Boolean] = YamlDecoder { case s @ ScalarNode(value, _) =>
value.toBooleanOption.toRight(cannotParse(value, "Boolean", s))
}

implicit def forBigInt: YamlDecoder[BigInt] = YamlDecoder { case s @ ScalarNode(value, _) =>
Try(BigInt(value.replaceAll("_", ""))).toEither.left
.map(ConstructError.from(_, "BigInt", s))
}

implicit def forBigDecimal: YamlDecoder[BigDecimal] = YamlDecoder {
case s @ ScalarNode(value, _) =>
Try(BigDecimal(value.replaceAll("_", ""))).toEither.left
.map(ConstructError.from(_, "BigDecimal", s))
}

implicit def forAny: YamlDecoder[Any] = new YamlDecoder[Any] {
def construct(node: Node)(implicit settings: LoadSettings = LoadSettings.empty) = {
node match {
case ScalarNode(value, tag: CoreSchemaTag) if Tag.corePrimitives.contains(tag) =>
tag match {
case Tag.nullTag => Right(None)
case Tag.boolean => value.toBooleanOption.toRight(cannotParse(value, "Boolean", node))
case Tag.int =>
val valueNorm = value.replaceAll("_", "")
Try(java.lang.Integer.decode(valueNorm))
.orElse(Try(java.lang.Long.decode(valueNorm)))
.orElse(Try(BigInt(valueNorm)))
.toEither
.left
.map(t => ConstructError.from(t, "int", node))
case Tag.float =>
val valueNorm = value.replaceAll("_", "")
Try(java.lang.Float.parseFloat(valueNorm))
.orElse(Try(java.lang.Double.parseDouble(valueNorm)))
.orElse(Try(BigDecimal(valueNorm)))
.toEither
.left
.map(t => ConstructError.from(t, "float", node))
case Tag.str => Right(value)
}
case MappingNode(mappings, Tag.map) =>
val decoder = implicitly[YamlDecoder[Map[Any, Any]]]
decoder.construct(node)
case SequenceNode(seq, Tag.seq) =>
val decoder = implicitly[YamlDecoder[Seq[Any]]]
decoder.construct(node)
case _ =>
settings.constructors.get(node.tag) match {
case Some(decoder) => decoder.construct(node)
case None =>
Left(
ConstructError(
s"""|Could't construct runtime instance of ${node.tag}
|${node.pos.map(_.errorMsg).getOrElse("")}
|If you're using custom datatype consider using yaml.as[MyType] instead of Any
|Or define LoadSettings where you'll specify how to construct ${node.tag}
|""".stripMargin
)
def construct(node: Node)(implicit settings: LoadSettings = LoadSettings.empty) = node match {
case ScalarNode(_, Tag.nullTag) =>
Right(None)
case node @ ScalarNode(_, Tag.boolean) =>
forBoolean.construct(node)
case node @ ScalarNode(_, Tag.int) =>
forByte
.widen[Any]
.orElse(forShort.widen[Any])
.orElse(forInt.widen[Any])
.orElse(forLong.widen[Any])
.orElse(forBigInt.widen[Any])
.construct(node)
case node @ ScalarNode(_, Tag.float) =>
forFloat
.widen[Any]
.orElse(forDouble.widen[Any])
.orElse(forBigDecimal.widen[Any])
.construct(node)
case ScalarNode(value, Tag.str) =>
Right(value)
case MappingNode(mappings, Tag.map) =>
val decoder = implicitly[YamlDecoder[Map[Any, Any]]]
decoder.construct(node)
case SequenceNode(seq, Tag.seq) =>
val decoder = implicitly[YamlDecoder[Seq[Any]]]
decoder.construct(node)
case _ =>
settings.constructors.get(node.tag) match {
case Some(decoder) => decoder.construct(node)
case None =>
Left(
ConstructError(
s"""|Could't construct runtime instance of ${node.tag}
|${node.pos.map(_.errorMsg).getOrElse("")}
|If you're using custom datatype consider using yaml.as[MyType] instead of Any
|Or define LoadSettings where you'll specify how to construct ${node.tag}
|""".stripMargin
)
}
}
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class DecoderErrorsSuite extends BaseDecoderErrorSuite:

assertError(
yaml.as[Person],
s"""|Cannot parse xxx as Int
s"""|For input string: "xxx"
|at 1:5, expected Int
|age: xxx
| ^
Expand Down