Skip to content

Commit

Permalink
Merge pull request #301 from sideeffffect/regular-decoders
Browse files Browse the repository at this point in the history
Reuse the existing decoders
  • Loading branch information
tgodzik authored May 31, 2024
2 parents 48c374c + 4c13911 commit 4149333
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 52 deletions.
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

0 comments on commit 4149333

Please sign in to comment.