From 2a57739631421d8ec35a73ad8a6b389b30c249a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Jourdan-Weil?= Date: Sun, 17 May 2020 19:35:40 +0200 Subject: [PATCH] Add Implicits object to help datatable transformations --- CHANGELOG.md | 2 + docs/datatables.md | 90 ++++++++-- .../scala/io/cucumber/scala/Implicits.scala | 155 ++++++++++++++++++ .../tests/datatables/DatatableAsScala.feature | 95 +++++++++++ .../datatables/DatatableAsScalaSteps.scala | 149 +++++++++++++++++ 5 files changed, 478 insertions(+), 13 deletions(-) create mode 100644 scala/sources/src/main/scala/io/cucumber/scala/Implicits.scala create mode 100644 scala/sources/src/test/resources/tests/datatables/DatatableAsScala.feature create mode 100644 scala/sources/src/test/scala/tests/datatables/DatatableAsScalaSteps.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b47fe3..043a8e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ See also the [CHANGELOG](https://github.com/cucumber/cucumber-jvm/blob/master/CH ### Added +- [Scala] Conversion methods from `DataTable` to scala types ([#56](https://github.com/cucumber/cucumber-jvm-scala/issues/56) Gaƫl Jourdan-Weil) + ### Changed ### Deprecated diff --git a/docs/datatables.md b/docs/datatables.md index 12d679a9..0ef1ee90 100644 --- a/docs/datatables.md +++ b/docs/datatables.md @@ -1,6 +1,10 @@ # DataTables -Cucumber Scala support DataTables with Java types. +Cucumber Scala support DataTables with either: +- Scala types using a `DataTable` as step definition argument and implicit conversions by importing `import io.cucumber.scala.Implicits._` +- Java types in the step definitions arguments + +**The benefit of using Scala types** if that you will be handling `Option`s instead of potentially `null` values in the Java collections. See below the exhaustive list of possible mappings. @@ -10,15 +14,25 @@ See below the exhaustive list of possible mappings. Given the following table as Map of Map | | key1 | key2 | key3 | | row1 | val11 | val12 | val13 | -| row2 | val21 | val22 | val23 | +| row2 | val21 | | val23 | | row3 | val31 | val32 | val33 | ``` ```scala +Given("the following table as Map of Map") { (table: DataTable) => + val scalaTable: Map[String, Map[String, Option[String]]] = table.asScalaRowColumnMap + // Map( + // "row1" -> Map("key1" -> Some("val11"), "key2" -> Some("val12"), "key3" -> Some("val13")), + // "row2" -> Map("key1" -> Some("val21"), "key2" -> None, "key3" -> Some("val23")), + // "row3" -> Map("key1" -> Some("val31"), "key2" -> Some("val32"), "key3" -> Some("val33")) + // ) +} + +// Or: Given("the following table as Map of Map") { (table: JavaMap[String, JavaMap[String, String]]) => // Map( // "row1" -> Map("key1" -> "val11", "key2" -> "val12", "key3" -> "val13"), - // "row2" -> Map("key1" -> "val21", "key2" -> "val22", "key3" -> "val23"), + // "row2" -> Map("key1" -> "val21", "key2" -> null, "key3" -> "val23"), // "row3" -> Map("key1" -> "val31", "key2" -> "val32", "key3" -> "val33") // ) } @@ -30,15 +44,25 @@ Given("the following table as Map of Map") { (table: JavaMap[String, JavaMap[Str Given the following table as List of Map | key1 | key2 | key3 | | val11 | val12 | val13 | -| val21 | val22 | val23 | +| val21 | | val23 | | val31 | val32 | val33 | ``` ```scala +Given("the following table as List of Map") { (table: DataTable) => + val scalaTable: Seq[Map[String, Option[String]]] = table.asScalaMaps + // Seq( + // Map("key1" -> Some("val11"), "key2" -> Some("val12"), "key3" -> Some("val13")), + // Map("key1" -> Some("val21"), "key2" -> None, "key3" -> Some("val23")), + // Map("key1" -> Some("val31"), "key2" -> Some("val32"), "key3" -> Some("val33")) + // ) +} + +// Or: Given("the following table as List of Map") { (table: JavaList[JavaMap[String, String]]) => // Seq( // Map("key1" -> "val11", "key2" -> "val12", "key3" -> "val13"), - // Map("key1" -> "val21", "key2" -> "val22", "key3" -> "val23"), + // Map("key1" -> "val21", "key2" -> null, "key3" -> "val23"), // Map("key1" -> "val31", "key2" -> "val32", "key3" -> "val33") // ) } @@ -49,15 +73,25 @@ Given("the following table as List of Map") { (table: JavaList[JavaMap[String, S ```gherkin Given the following table as Map of List | row1 | val11 | val12 | val13 | -| row2 | val21 | val22 | val23 | +| row2 | val21 | | val23 | | row3 | val31 | val32 | val33 | ``` ```scala +Given("the following table as Map of List") { (table: DataTable) => + val scalaTable: Map[Seq[Option[String]]] = table.asScalaRowMap + // Map( + // "row1" -> Seq(Some("val11"), Some("val12"), Some("val13")), + // "row2" -> Seq(Some("val21"), None, Some("val23")), + // "row3" -> Seq(Some("val31"), Some("val32"), Some("val33")) + // ) +} + +// Or: Given("the following table as Map of List") { (table: JavaMap[String, JavaList[String]]) => // Map( // "row1" -> Seq("val11", "val12", "val13"), - // "row2" -> Seq("val21", "val22", "val23"), + // "row2" -> Seq("val21", null, "val23"), // "row3" -> Seq("val31", "val32", "val33") // ) } @@ -69,15 +103,25 @@ Given("the following table as Map of List") { (table: JavaMap[String, JavaList[S ```gherkin Given the following table as List of List | val11 | val12 | val13 | -| val21 | val22 | val23 | +| val21 | | val23 | | val31 | val32 | val33 | ``` ```scala +Given("the following table as List of List") { (table: DataTable) => + val scalaTable: Seq[Seq[Option[String]]] = table.asScalaLists + // Seq( + // Seq(Some("val11"), Some("val12"), Some("val13")), + // Seq(Some("val21"), None, Some("val23")), + // Seq(Some("val31"), Some("val32"), Some("val33")) + // ) +} + +// Or: Given("the following table as List of List") { (table: JavaList[JavaList[String]]) => // Seq( // Seq("val11", "val12", "val13"), - // Seq("val21", "val22", "val23"), + // Seq("val21", null, "val23"), // Seq("val31", "val32", "val33") // ) } @@ -88,15 +132,25 @@ Given("the following table as List of List") { (table: JavaList[JavaList[String] ```gherkin Given the following table as Map | row1 | val11 | -| row2 | val21 | +| row2 | | | row3 | val31 | ``` ```scala +Given("the following table as Map") { (table: DataTable) => + val scalaTable: Map[String, Option[String]] = table.asScalaMap[String, String] + // Map( + // "row1" -> Some("val11"), + // "row2" -> None, + // "row3" -> Some("val31") + // ) +} + +// Or: Given("the following table as Map") { (table: JavaMap[String, String]) => // Map( // "row1" -> "val11", - // "row2" -> "val21", + // "row2" -> null, // "row3" -> "val31" // ) } @@ -107,15 +161,25 @@ Given("the following table as Map") { (table: JavaMap[String, String]) => ```gherkin Given the following table as List | val11 | -| val21 | +| | | val31 | ``` ```scala +Given("the following table as List") { (table: DataTable) => + val scalaTable: Seq[Option[String]] = table.asScalaList + // Seq( + // Some("val11"), + // None, + // Some("val31") + // ) +} + +// Or: Given("the following table as List") { (table: JavaList[String]) => // Seq( // "val11", - // "val21", + // null, // "val31" // ) } diff --git a/scala/sources/src/main/scala/io/cucumber/scala/Implicits.scala b/scala/sources/src/main/scala/io/cucumber/scala/Implicits.scala new file mode 100644 index 00000000..01c3f7a5 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/Implicits.scala @@ -0,0 +1,155 @@ +package io.cucumber.scala + +import io.cucumber.datatable.DataTable + +import scala.jdk.CollectionConverters._ +import scala.reflect.ClassTag + +/** + * Contains implicit helpers for Cucumber Scala users. + */ +object Implicits { + + /** + * DataTable extension class providing methods to read a DataTable as Scala types. + *

+ * Note: we do not filter out null values because users might rely on the keyset in their implementation. + */ + implicit class ScalaDataTable(table: DataTable) { + + def asScalaDataTable: ScalaDataTable = this + + /** + * Provides a view of the DataTable as a sequence of rows, each row being a key-value map where key is the column name. + * Equivalent of `.asMaps[K,V](classOf[K], classOf[V])` but returned as Scala collection types without `null` values. + * + * @tparam K key type + * @tparam V value type + * @return sequence of rows + */ + def asScalaMaps[K, V](implicit evK: ClassTag[K], evV: ClassTag[V]): Seq[Map[K, Option[V]]] = { + table.asMaps[K, V](evK.runtimeClass, evV.runtimeClass) + .asScala + .map(_.asScala.map(nullToNone).toMap) + .toSeq + } + + /** + * Provides a view of the DataTable as a sequence of rows, each row being a key-value map where key is the column name. + * Equivalent of `.asMaps()` but returned as Scala collection types without `null` values. + * + * @return sequence of rows + */ + def asScalaMaps: Seq[Map[String, Option[String]]] = asScalaMaps[String, String] + + /** + * Provides a view of the DataTable as a key-value map where key are the first column values. + * Equivalent of `.asMap[K,V](classOf[K],classOf[V])` but returned as Scala collection types without `null` values. + * + * @tparam K key type + * @tparam V value type + * @return key-value map + */ + def asScalaMap[K, V](implicit evK: ClassTag[K], evV: ClassTag[V]): Map[K, Option[V]] = { + table.asMap[K, V](evK.runtimeClass, evV.runtimeClass) + .asScala + .map(nullToNone) + .toMap + } + + /** + * Provides a view of the DataTable as a matrix. + * Equivalent of `.asLists[T](classOf[T])` but returned as Scala collection types without `null` values. + * + * @tparam T cell type + * @return matrix + */ + def asScalaLists[T](implicit ev: ClassTag[T]): Seq[Seq[Option[T]]] = { + table.asLists[T](ev.runtimeClass) + .asScala + .map(_.asScala.map(Option.apply).toSeq) + .toSeq + } + + /** + * Provides a view of the DataTable as a matrix. + * Equivalent of `.asLists()` but returned as Scala collection types without `null` values. + * + * @return matrix + */ + def asScalaLists: Seq[Seq[Option[String]]] = asScalaLists[String] + + /** + * Provides a view of the DataTable as a simple list of values. + * Equivalent of `.asList[T](classOf[T])` but returned as Scala collection types without `null` values. + * + * @tparam T cell type + * @return list of values + */ + def asScalaList[T](implicit ev: ClassTag[T]): Seq[Option[T]] = { + table.asList[T](ev.runtimeClass) + .asScala + .map(Option.apply) + .toSeq + } + + /** + * Provides a view of the DataTable as a simple list of values. + * Equivalent of `.asList()` but returned as Scala collection types without `null` values. + * + * @return list of values + */ + def asScalaList: Seq[Option[String]] = asScalaList[String] + + /** + * Provides a view of the DataTable as a full table: a key-value map of row where keys are the first column values + * and each row being itself a key-value map where key is the column name. + * + * @tparam K key type + * @return map of rows + */ + def asScalaRowColumnMap[K](implicit evK: ClassTag[K]): Map[K, Map[String, Option[String]]] = { + table.asMap[K, java.util.Map[String, String]](evK.runtimeClass, classOf[java.util.Map[String, String]]) + .asScala + .map { case (k, v) => (k, v.asScala.map(nullToNone).toMap) } + .toMap + } + + /** + * Provides a view of the DataTable as a full table: a key-value map of row where keys are the first column values + * and each row being itself a key-value map where key is the column name. + * + * @return map of rows + */ + def asScalaRowColumnMap: Map[String, Map[String, Option[String]]] = asScalaRowColumnMap[String] + + /** + * Provides a view of the DataTable as a key-value map of row where keys are the first column values + * and each row being a list of values. + * + * @tparam K key type + * @return map of rows + */ + def asScalaRowMap[K](implicit evK: ClassTag[K]): Map[K, Seq[Option[String]]] = { + table.asMap[K, java.util.List[String]](evK.runtimeClass, classOf[java.util.List[String]]) + .asScala + .map { case (k, v) => (k, v.asScala.map(Option.apply).toSeq) } + .toMap + } + + /** + * Provides a view of the DataTable as a key-value map of row where keys are the first column values + * and each row being a list of values. + * + * @return map of rows + */ + def asScalaRowMap: Map[String, Seq[Option[String]]] = asScalaRowMap[String] + + private def nullToNone[K, V](tuple: (K, V)): (K, Option[V]) = { + val (k, v) = tuple + (k, Option(v)) + } + + } + +} diff --git a/scala/sources/src/test/resources/tests/datatables/DatatableAsScala.feature b/scala/sources/src/test/resources/tests/datatables/DatatableAsScala.feature new file mode 100644 index 00000000..267dd32c --- /dev/null +++ b/scala/sources/src/test/resources/tests/datatables/DatatableAsScala.feature @@ -0,0 +1,95 @@ +Feature: As Cucumber Scala, I want to parse DataTables to Scala types properly + + # Scenarios with Strings + + Scenario: As datatable + Given the following table as Scala DataTable + | key1 | key2 | key3 | + | val11 | val12 | val13 | + | val21 | | val23 | + | val31 | val32 | val33 | + + Scenario: As List of Map + Given the following table as Scala List of Map + | key1 | key2 | key3 | + | val11 | val12 | val13 | + | val21 | | val23 | + | val31 | val32 | val33 | + + Scenario: As List of List + Given the following table as Scala List of List + | val11 | val12 | val13 | + | val21 | | val23 | + | val31 | val32 | val33 | + + Scenario: As Map of Map + Given the following table as Scala Map of Map + | | key1 | key2 | key3 | + | row1 | val11 | val12 | val13 | + | row2 | val21 | | val23 | + | row3 | val31 | val32 | val33 | + + Scenario: As Map of List + Given the following table as Scala Map of List + | row1 | val11 | val12 | val13 | + | row2 | val21 | | val23 | + | row3 | val31 | val32 | val33 | + + Scenario: As Map + Given the following table as Scala Map + | row1 | val11 | + | row2 | | + | row3 | val31 | + + Scenario: As List + Given the following table as Scala List + | val11 | + | | + | val31 | + + # Scenarios with other basic types (Int) + + Scenario: As datatable of integers + Given the following table as Scala DataTable of integers + | 1 | 2 | 3 | + | 11 | 12 | 13 | + | 21 | | 23 | + | 31 | 32 | 33 | + + Scenario: As List of Map of integers + Given the following table as Scala List of Map of integers + | 1 | 2 | 3 | + | 11 | 12 | 13 | + | 21 | | 23 | + | 31 | 32 | 33 | + + Scenario: As List of List of integers + Given the following table as Scala List of List of integers + | 11 | 12 | 13 | + | 21 | | 23 | + | 31 | 32 | 33 | + + Scenario: As Map of Map of integers (partial) + Given the following table as Scala Map of Map of integers + | | key1 | key2 | key3 | + | 10 | val11 | val12 | val13 | + | 20 | val21 | | val23 | + | 30 | val31 | val32 | val33 | + + Scenario: As Map of List of integers (partial) + Given the following table as Scala Map of List of integers + | 10 | val11 | val12 | val13 | + | 20 | val21 | | val23 | + | 30 | val31 | val32 | val33 | + + Scenario: As Map of integers + Given the following table as Scala Map of integers + | 10 | 11 | + | 20 | | + | 30 | 31 | + + Scenario: As List of integers + Given the following table as Scala List of integers + | 11 | + | | + | 31 | \ No newline at end of file diff --git a/scala/sources/src/test/scala/tests/datatables/DatatableAsScalaSteps.scala b/scala/sources/src/test/scala/tests/datatables/DatatableAsScalaSteps.scala new file mode 100644 index 00000000..034b6d19 --- /dev/null +++ b/scala/sources/src/test/scala/tests/datatables/DatatableAsScalaSteps.scala @@ -0,0 +1,149 @@ +package tests.datatables + +import io.cucumber.datatable.DataTable +import io.cucumber.scala.Implicits.ScalaDataTable +import io.cucumber.scala.{EN, ScalaDsl} + +class DatatableAsScalaSteps extends ScalaDsl with EN { + + Given("the following table as Scala DataTable") { (table: DataTable) => + val data: Seq[Map[String, Option[String]]] = table.asScalaDataTable.asScalaMaps + val expected = Seq( + Map("key1" -> Some("val11"), "key2" -> Some("val12"), "key3" -> Some("val13")), + Map("key1" -> Some("val21"), "key2" -> None, "key3" -> Some("val23")), + Map("key1" -> Some("val31"), "key2" -> Some("val32"), "key3" -> Some("val33")) + ) + assert(data == expected) + } + + Given("the following table as Scala List of Map") { (table: DataTable) => + val data: Seq[Map[String, Option[String]]] = table.asScalaMaps + val expected = Seq( + Map("key1" -> Some("val11"), "key2" -> Some("val12"), "key3" -> Some("val13")), + Map("key1" -> Some("val21"), "key2" -> None, "key3" -> Some("val23")), + Map("key1" -> Some("val31"), "key2" -> Some("val32"), "key3" -> Some("val33")) + ) + assert(data == expected) + } + + Given("the following table as Scala List of List") { (table: DataTable) => + val data: Seq[Seq[Option[String]]] = table.asScalaLists + val expected = Seq( + Seq(Some("val11"), Some("val12"), Some("val13")), + Seq(Some("val21"), None, Some("val23")), + Seq(Some("val31"), Some("val32"), Some("val33")) + ) + assert(data == expected) + } + + Given("the following table as Scala Map of Map") { (table: DataTable) => + val data: Map[String, Map[String, Option[String]]] = table.asScalaRowColumnMap + val expected = Map( + "row1" -> Map("key1" -> Some("val11"), "key2" -> Some("val12"), "key3" -> Some("val13")), + "row2" -> Map("key1" -> Some("val21"), "key2" -> None, "key3" -> Some("val23")), + "row3" -> Map("key1" -> Some("val31"), "key2" -> Some("val32"), "key3" -> Some("val33")) + ) + assert(data == expected) + } + + Given("the following table as Scala Map of List") { (table: DataTable) => + val data: Map[String, Seq[Option[String]]] = table.asScalaRowMap + val expected = Map( + "row1" -> Seq(Some("val11"), Some("val12"), Some("val13")), + "row2" -> Seq(Some("val21"), None, Some("val23")), + "row3" -> Seq(Some("val31"), Some("val32"), Some("val33")) + ) + assert(data == expected) + } + + Given("the following table as Scala Map") { (table: DataTable) => + val data: Map[String, Option[String]] = table.asScalaMap[String, String] + val expected = Map( + "row1" -> Some("val11"), + "row2" -> None, + "row3" -> Some("val31") + ) + assert(data == expected) + } + + Given("the following table as Scala List") { (table: DataTable) => + val data: Seq[Option[String]] = table.asScalaList + val expected = Seq( + Some("val11"), + None, + Some("val31") + ) + assert(data == expected) + } + + Given("the following table as Scala DataTable of integers") { (table: DataTable) => + val data: Seq[Map[Int, Option[Int]]] = table.asScalaDataTable.asScalaMaps[Int, Int] + val expected = Seq( + Map(1 -> Some(11), 2 -> Some(12), 3 -> Some(13)), + Map(1 -> Some(21), 2 -> None, 3 -> Some(23)), + Map(1 -> Some(31), 2 -> Some(32), 3 -> Some(33)) + ) + assert(data == expected) + } + + Given("the following table as Scala List of Map of integers") { (table: DataTable) => + val data: Seq[Map[Int, Option[Int]]] = table.asScalaMaps[Int, Int] + val expected = Seq( + Map(1 -> Some(11), 2 -> Some(12), 3 -> Some(13)), + Map(1 -> Some(21), 2 -> None, 3 -> Some(23)), + Map(1 -> Some(31), 2 -> Some(32), 3 -> Some(33)) + ) + assert(data == expected) + } + + Given("the following table as Scala List of List of integers") { (table: DataTable) => + val data: Seq[Seq[Option[Int]]] = table.asScalaLists[Int] + val expected = Seq( + Seq(Some(11), Some(12), Some(13)), + Seq(Some(21), None, Some(23)), + Seq(Some(31), Some(32), Some(33)) + ) + assert(data == expected) + } + + Given("the following table as Scala Map of Map of integers") { (table: DataTable) => + val data: Map[Int, Map[String, Option[String]]] = table.asScalaRowColumnMap[Int] + val expected = Map( + 10 -> Map("key1" -> Some("val11"), "key2" -> Some("val12"), "key3" -> Some("val13")), + 20 -> Map("key1" -> Some("val21"), "key2" -> None, "key3" -> Some("val23")), + 30 -> Map("key1" -> Some("val31"), "key2" -> Some("val32"), "key3" -> Some("val33")) + ) + assert(data == expected) + } + + Given("the following table as Scala Map of List of integers") { (table: DataTable) => + val data: Map[Int, Seq[Option[String]]] = table.asScalaRowMap[Int] + val expected = Map( + 10 -> Seq(Some("val11"), Some("val12"), Some("val13")), + 20 -> Seq(Some("val21"), None, Some("val23")), + 30 -> Seq(Some("val31"), Some("val32"), Some("val33")) + ) + assert(data == expected) + } + + Given("the following table as Scala Map of integers") { (table: DataTable) => + val data: Map[Int, Option[Int]] = table.asScalaMap[Int, Int] + val expected = Map( + 10 -> Some(11), + 20 -> None, + 30 -> Some(31) + ) + assert(data == expected) + } + + Given("the following table as Scala List of integers") { (table: DataTable) => + val data: Seq[Option[Int]] = table.asScalaList[Int] + val expected = Seq( + Some(11), + None, + Some(31) + ) + assert(data == expected) + } + +}