diff --git a/core/shared/src/main/scala-3/org/virtuslab/yaml/YamlDecoderCrossCompat.scala b/core/shared/src/main/scala-3/org/virtuslab/yaml/YamlDecoderCrossCompat.scala index c9579b4e8..48300bef3 100644 --- a/core/shared/src/main/scala-3/org/virtuslab/yaml/YamlDecoderCrossCompat.scala +++ b/core/shared/src/main/scala-3/org/virtuslab/yaml/YamlDecoderCrossCompat.scala @@ -31,13 +31,16 @@ private[yaml] trait DecoderMacros { protected def constructValues[T]( elemLabels: List[String], instances: List[YamlDecoder[_]], + optionalTypes: List[Boolean], valuesMap: Map[String, Node], p: Mirror.ProductOf[T] ) = { - val values = elemLabels.zip(instances).map { case (label, c) => + val values = elemLabels.zip(instances).zip(optionalTypes).map { case ((label, c), isOptional) => valuesMap.get(label) match case Some(value) => c.construct(value) - case None => Left(ConstructError(s"Key $label doesn't exist in parsed document")) + case None => + if (isOptional) Right(None) + else Left(ConstructError(s"Key $label doesn't exist in parsed document")) } val (left, right) = values.partitionMap(identity) if left.nonEmpty then Left(left.head) @@ -45,8 +48,9 @@ private[yaml] trait DecoderMacros { } protected inline def deriveProduct[T](p: Mirror.ProductOf[T]) = - val instances = summonAll[p.MirroredElemTypes] - val elemLabels = getElemLabels[p.MirroredElemLabels] + val instances = summonAll[p.MirroredElemTypes] + val elemLabels = getElemLabels[p.MirroredElemLabels] + val optionalTypes = getOptionalTypes[p.MirroredElemTypes] new YamlDecoder[T] { override def construct(node: Node)(using constructor: LoadSettings = LoadSettings.empty @@ -54,8 +58,14 @@ private[yaml] trait DecoderMacros { node match case Node.MappingNode(mappings, _) => for { - valuesMap <- extractKeyValues(mappings) - constructedValues <- constructValues(elemLabels, instances, valuesMap, p) + valuesMap <- extractKeyValues(mappings) + constructedValues <- constructValues( + elemLabels, + instances, + optionalTypes, + valuesMap, + p + ) } yield (constructedValues) case _ => Left(ConstructError(s"Expected MappingNode, got ${node.getClass.getSimpleName}")) @@ -87,4 +97,9 @@ private[yaml] trait DecoderMacros { case _: EmptyTuple => Nil case _: (head *: tail) => constValue[head].toString :: getElemLabels[tail] + protected inline def getOptionalTypes[T <: Tuple]: List[Boolean] = inline erasedValue[T] match + case _: EmptyTuple => Nil + case _: (Option[_] *: tail) => true :: getOptionalTypes[tail] + case _: (_ *: tail) => false :: getOptionalTypes[tail] + } diff --git a/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala b/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala index 508a1500b..f57c2d8bf 100644 --- a/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala +++ b/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala @@ -42,8 +42,12 @@ class DecoderSuite extends munit.FunSuite: test("option") { // todo Option YamlEncoder - case class OptionTypes(double: Option[Double], float: Option[Float], int: Option[Int]) - derives YamlDecoder + case class OptionTypes( + double: Option[Double], + float: Option[Float], + int: Option[Int], + short: Option[Short] + ) derives YamlDecoder val numberYaml = s"""double: ${Double.MaxValue} @@ -54,7 +58,8 @@ class DecoderSuite extends munit.FunSuite: val expectedNumber = OptionTypes( double = Some(Double.MaxValue), float = None, - int = None + int = None, + short = None ) assertEquals(numberYaml.as[OptionTypes], Right(expectedNumber))