Skip to content

Commit

Permalink
Merge pull request #612 from gnieh/feature/607-csv-case-class-defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
ybasket authored Jul 15, 2024
2 parents fef8818 + cc8de9b commit 0358494
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ private[generic] trait LowPriorityMapShapedCsvRowDecoder1 {
default: Option[Head] :: DefaultTail,
anno: Anno :: AnnoTail): DecoderResult[FieldType[Key, Head] :: Tail] = {
val head = row(anno.head.fold(witness.value.name)(_.name)) match {
case Some(head) if head.isEmpty && default.head.nonEmpty =>
default.head.toRight(new DecoderError("Should not happen", row.line))
case Some(head) =>
Head(head).leftMap(_.withLine(row.line))
case _ =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 fs2-data Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fs2.data.csv.generic

import cats.data.NonEmptyList
import fs2.data.csv.CsvRow
import fs2.data.csv.generic.semiauto.deriveCsvRowDecoder
import weaver.SimpleIOSuite

object CsvRowDecoderDefaultsTest extends SimpleIOSuite {

val csvRowDefaultI = CsvRow.unsafe(NonEmptyList.of("", "test", "42"), NonEmptyList.of("i", "s", "j"))
val csvRowNoI =
CsvRow.unsafe(NonEmptyList.of("test", "42"), NonEmptyList.of("s", "j"))
val csvRowEmptyJ =
CsvRow.unsafe(NonEmptyList.of("1", "test", ""), NonEmptyList.of("i", "s", "j"))

case class Test(i: Int = 0, s: String, j: Option[Int])
case class TestDefaults(i: Int = 7, j: String = "foo")

val testDecoder = deriveCsvRowDecoder[Test]
val testDefaultsDecoder = deriveCsvRowDecoder[TestDefaults]

pureTest("case classes should be handled properly with default value and empty cell") {
expect(testDecoder(csvRowDefaultI) == Right(Test(0, "test", Some(42))))
}

pureTest("case classes should be handled properly with default value and missing column") {
expect(testDecoder(csvRowNoI) == Right(Test(0, "test", Some(42))))
}

pureTest("case classes should be handled properly with default string value and empty cell") {
expect(testDefaultsDecoder(csvRowEmptyJ) == Right(TestDefaults(1, "foo")))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,31 @@ object CsvRowDecoderTest extends SimpleIOSuite {
case class TestRename(s: String, @CsvName("j") k: Int, i: Int)
case class TestOptionRename(s: String, @CsvName("j") k: Option[Int], i: Int)
case class TestOptionalString(i: Int, s: Option[String], j: Int)
case class TestDefaults(i: Int = 7, j: String = "foo")

val testDecoder = deriveCsvRowDecoder[Test]
val testOrderDecoder = deriveCsvRowDecoder[TestOrder]
val testRenameDecoder = deriveCsvRowDecoder[TestRename]
val testOptionRenameDecoder = deriveCsvRowDecoder[TestOptionRename]
val testOptionalStringDecoder = deriveCsvRowDecoder[TestOptionalString]
val testDefaultsDecoder = deriveCsvRowDecoder[TestDefaults]

pureTest("case classes should be decoded properly by header name and not position") {
expect(testDecoder(csvRow) == Right(Test(1, "test", Some(42)))) and
expect(testOrderDecoder(csvRow) == Right(TestOrder("test", 42, 1)))
}

// TODO: Re-enable once Scala 3 supports defaults, remove CsvRowDecoderDefaultsTest
/*pureTest("case classes should be handled properly with default value and empty cell") {
expect(testDecoder(csvRowDefaultI) == Right(Test(0, "test", Some(42))))
}*/
}
/*pureTest("case classes should be handled properly with default value and missing column") {
pureTest("case classes should be handled properly with default value and missing column") {
expect(testDecoder(csvRowNoI) == Right(Test(0, "test", Some(42))))
}
pureTest("case classes should be handled properly with default string value and empty cell") {
expect(testDefaultsDecoder(csvRowEmptyJ) == Right(TestDefaults(1, "foo")))
}*/

pureTest("case classes should be handled properly with optional value and empty cell") {
Expand Down
4 changes: 3 additions & 1 deletion site/documentation/csv/generic.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Module: [![Maven Central](https://img.shields.io/maven-central/v/org.gnieh/fs2-d

The `fs2-data-csv-generic` module provides automatic (Scala 2-only) and semi-automatic derivation for `RowDecoder` and `CsvRowDecoder`.

It makes it easier to support custom row types but is based on [shapeless][shapeless], which can have a significant impact on compilation time on Scala 2. On Scala 3, it relies on mix of hand-written derivation on top of `scala.deriving.Mirror` and the more light-weight [shapeless-3][shapeless-3], so that compile times shouldn't be problematic as on Scala 2. Note that auto derivation is currently not yet supported on Scala, same goes for using default constructor arguments of `case class`es (for background see [dotty#11667][dotty#11667]).
It makes it easier to support custom row types but is based on [shapeless][shapeless], which can have a significant impact on compilation time on Scala 2. On Scala 3, it relies on mix of hand-written derivation on top of `scala.deriving.Mirror` and the more light-weight [shapeless-3][shapeless-3], so that compile times shouldn't be problematic as on Scala 2. Note that auto derivation is currently not yet supported on Scala 3, same goes for using default constructor arguments of `case class`es (for background see [dotty#11667][dotty#11667]).

To demonstrate how it works, let's work again with the CSV data from the [core][csv-doc] module documentation.

Expand Down Expand Up @@ -123,6 +123,8 @@ val decoded = stream.through(decodeUsingHeaders[MyRowDefault]())
decoded.compile.toList
```

It's important to note that by the limitations of the CSV file format, there's no clear notion of when default values would apply. `fs2-data-csv-generic` treats values as missing if there's no column with the expected name or if the value is empty. This implies that cells with an empty value won't be parsed of there's a default present, even if the corresponding `CellDecoder` instance could handle empty input, like `CellDecoder[String]`. If you need to handle empty inputs explicitly, refrain from defining a (non-empty) default or define the `CsvRowDecoder` instance manually.

[csv-doc]: /documentation/csv/index.md
[shapeless]: https://github.com/milessabin/shapeless
[shapeless-3]: https://github.com/typelevel/shapeless-3
Expand Down

0 comments on commit 0358494

Please sign in to comment.