From e4f4b988c120cd7011ac18c4715a6a883d7df47a Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Sun, 13 Jan 2019 16:42:06 +0800 Subject: [PATCH 1/8] Added support for Foldable Alternative (specifically Chain) and NonEmptyChain. --- CONTRIBUTORS | 1 + modules/cats/README.md | 4 ++-- modules/cats/build.sbt | 3 ++- .../pureconfig/module/cats/package.scala | 21 ++++++++++++++++++- .../pureconfig/module/cats/CatsSuite.scala | 11 +++++++++- 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 813b9e5f5..71adaea65 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -19,3 +19,4 @@ Yuriy Badalyantc @lmnet Anshul Bajpai @anshulbajpai Nicolae Namolovan @NicolaeNMV Dmitry Polienko @nigredo-tori +Hao Yang ANG @yangzai diff --git a/modules/cats/README.md b/modules/cats/README.md index 9821fb795..421acb8d8 100644 --- a/modules/cats/README.md +++ b/modules/cats/README.md @@ -17,9 +17,9 @@ libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats" % "0.10.1" ### Reading cats data structures from a config The following cats data structures are supported: - -* `NonEmptyList`, `NonEmptyVector`, `NonEmptySet` +* `NonEmptyList`, `NonEmptyVector`, `NonEmptySet`, `NonEmptyChain` * `NonEmptyMap[K, V]` implicits of `ConfigReader[Map[K, V]]` and `Order[K]` should be in the scope. +* Cats data types with `Foldable` and `Alternative` (i.e. non-reducible) typeclass instances, e.g. `Chain`. For example, if your key is a `String` then `Order[String]` can be imported from `cats.instances.string._` All of these data structures rely on the instances of their unrestricted (i.e. possibly empty) variants. diff --git a/modules/cats/build.sbt b/modules/cats/build.sbt index 9b6b6030b..f9f891a54 100644 --- a/modules/cats/build.sbt +++ b/modules/cats/build.sbt @@ -2,7 +2,8 @@ name := "pureconfig-cats" libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % "1.4.0", - "org.typelevel" %% "cats-laws" % "1.4.0" % "test") + "org.typelevel" %% "cats-laws" % "1.4.0" % "test", + Dependencies.shapeless % Provided) developers := List( Developer("derekmorr", "Derek Morr", "morr.derek@gmail.com", url("https://github.com/derekmorr")), diff --git a/modules/cats/src/main/scala/pureconfig/module/cats/package.scala b/modules/cats/src/main/scala/pureconfig/module/cats/package.scala index 195a2ee3c..b821076b4 100644 --- a/modules/cats/src/main/scala/pureconfig/module/cats/package.scala +++ b/modules/cats/src/main/scala/pureconfig/module/cats/package.scala @@ -1,10 +1,14 @@ package pureconfig.module -import _root_.cats.data.{ NonEmptyList, NonEmptyMap, NonEmptySet, NonEmptyVector } +import _root_.cats.data._ import _root_.cats.kernel.Order +import _root_.cats.{ Alternative, Foldable } +import _root_.cats.implicits._ import pureconfig.{ ConfigReader, ConfigWriter } +import shapeless._ import scala.collection.immutable.{ SortedMap, SortedSet } +import scala.language.higherKinds import scala.reflect.ClassTag /** @@ -34,4 +38,19 @@ package object cats { fromNonEmpty(reader)(x => NonEmptyMap.fromMap(SortedMap(x.toSeq: _*)(ord.toOrdering))) implicit def nonEmptyMapWriter[A, B](implicit writer: ConfigWriter[Map[A, B]]): ConfigWriter[NonEmptyMap[A, B]] = writer.contramap(_.toSortedMap) + + // For emptiable foldables not covered by TraversableOnce reader/writer, e.g. Chain. + implicit def nonReducibleReader[T, F[_]: Foldable: Alternative](implicit + reader: ConfigReader[TraversableOnce[T]], + ev: ¬¬[F[T]] <:!< (TraversableOnce[T] ∨ Option[T])): ConfigReader[F[T]] = + reader.map(to => (to :\ Alternative[F].empty[T])(_.pure[F] <+> _)) + implicit def nonReducibleWriter[T, F[_]: Foldable: Alternative](implicit + writer: ConfigWriter[TraversableOnce[T]], + ev: ¬¬[F[T]] <:!< (TraversableOnce[T] ∨ Option[T])): ConfigWriter[F[T]] = + writer.contramap(_.toList) + + implicit def nonEmptyChainReader[T](implicit reader: ConfigReader[Chain[T]]): ConfigReader[NonEmptyChain[T]] = + fromNonEmpty(reader)(NonEmptyChain.fromChain) + implicit def nonEmptyChainWriter[T](implicit writer: ConfigWriter[Chain[T]]): ConfigWriter[NonEmptyChain[T]] = + writer.contramap(_.toChain) } diff --git a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala index b71eec534..d25a5f7a2 100644 --- a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala +++ b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala @@ -1,6 +1,6 @@ package pureconfig.module.cats -import cats.data.{ NonEmptyList, NonEmptyMap, NonEmptySet, NonEmptyVector } +import cats.data._ import cats.instances.int._ import cats.instances.string._ import com.typesafe.config.ConfigFactory.parseString @@ -16,6 +16,7 @@ class CatsSuite extends BaseSuite with ConfigConvertChecks { case class NumVec(numbers: NonEmptyVector[Int]) case class NumSet(numbers: NonEmptySet[Int]) case class NumMap(numbers: NonEmptyMap[String, Int]) + case class NumChain(numbers: NonEmptyChain[Int]) checkReadWrite[Numbers](parseString(s"""{ numbers: [1,2,3] }""").root() → Numbers(NonEmptyList(1, List(2, 3)))) checkReadWrite[NumVec](parseString(s"""{ numbers: [1,2,3] }""").root() → NumVec(NonEmptyVector(1, Vector(2, 3)))) @@ -23,6 +24,9 @@ class CatsSuite extends BaseSuite with ConfigConvertChecks { checkReadWrite[NumMap](parseString(s"""{ numbers {"1": 1, "2": 2, "3": 3 } }""").root() → NumMap(NonEmptyMap(("1", 1), SortedMap("2" → 2, "3" → 3)))) + // Chain seems to break referential transparency unless prepend/append order is the same + checkReadWrite[NumChain](parseString(s"""{ numbers: [1,2,3] }""").root() → + NumChain(NonEmptyChain.fromChainPrepend(1, 2 +: 3 +: Chain.empty))) it should "return an EmptyTraversableFound when reading empty lists into NonEmptyList" in { val config = parseString("{ numbers: [] }") @@ -43,4 +47,9 @@ class CatsSuite extends BaseSuite with ConfigConvertChecks { val config = parseString("{ numbers{} }") config.to[NumMap] should failWith(EmptyTraversableFound("scala.collection.immutable.Map"), "numbers") } + + it should "return an EmptyTraversableFound when reading empty map into NonEmptyChain" in { + val config = parseString("{ numbers: [] }") + config.to[NumChain] should failWith(EmptyTraversableFound("cats.data.Chain"), "numbers") + } } From 61765021beaa7f7805b26f403eb662f9c2bbfb15 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Sun, 13 Jan 2019 17:15:56 +0800 Subject: [PATCH 2/8] Updated readme examples with NonEmptyChain. --- modules/cats/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/cats/README.md b/modules/cats/README.md index 421acb8d8..84b272150 100644 --- a/modules/cats/README.md +++ b/modules/cats/README.md @@ -28,7 +28,7 @@ Custom collection readers, if any, may affect the behavior of these too. Here is an example of usage: ```scala -import cats.data.{NonEmptyList, NonEmptySet, NonEmptyVector, NonEmptyMap} +import cats.data.{NonEmptyList, NonEmptySet, NonEmptyVector, NonEmptyMap, NonEmptyChain} import cats.instances.string._ import com.typesafe.config.ConfigFactory.parseString import pureconfig._ @@ -39,7 +39,8 @@ case class MyConfig( numberList: NonEmptyList[Int], numberSet: NonEmptySet[Int], numberVector: NonEmptyVector[Int], - numberMap: NonEmptyMap[String, Int] + numberMap: NonEmptyMap[String, Int], + numberChain: NonEmptyChain[Int] ) ``` @@ -49,7 +50,8 @@ val conf = parseString("""{ number-list: [1,2,3], number-set: [1,2,3], number-vector: [1,2,3], - number-map { "one": 1, "two": 2, "three": 3 } + number-map { "one": 1, "two": 2, "three": 3 }, + number-chain: [1,2,3] }""") // conf: com.typesafe.config.Config = Config(SimpleConfigObject({"number-list":[1,2,3],"number-map":{"one":1,"three":3,"two":2},"number-set":[1,2,3],"number-vector":[1,2,3]})) From 6d1c38f08f7332e268497e9f9451cea9a8286b91 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Sun, 13 Jan 2019 18:11:15 +0800 Subject: [PATCH 3/8] Updated tut. --- modules/cats/README.md | 5 +++-- modules/cats/src/main/tut/README.md | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/cats/README.md b/modules/cats/README.md index 84b272150..003cf2d84 100644 --- a/modules/cats/README.md +++ b/modules/cats/README.md @@ -17,6 +17,7 @@ libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats" % "0.10.1" ### Reading cats data structures from a config The following cats data structures are supported: + * `NonEmptyList`, `NonEmptyVector`, `NonEmptySet`, `NonEmptyChain` * `NonEmptyMap[K, V]` implicits of `ConfigReader[Map[K, V]]` and `Order[K]` should be in the scope. * Cats data types with `Foldable` and `Alternative` (i.e. non-reducible) typeclass instances, e.g. `Chain`. @@ -53,10 +54,10 @@ val conf = parseString("""{ number-map { "one": 1, "two": 2, "three": 3 }, number-chain: [1,2,3] }""") -// conf: com.typesafe.config.Config = Config(SimpleConfigObject({"number-list":[1,2,3],"number-map":{"one":1,"three":3,"two":2},"number-set":[1,2,3],"number-vector":[1,2,3]})) +// conf: com.typesafe.config.Config = Config(SimpleConfigObject({"number-chain":[1,2,3],"number-list":[1,2,3],"number-map":{"one":1,"three":3,"two":2},"number-set":[1,2,3],"number-vector":[1,2,3]})) loadConfig[MyConfig](conf) -// res0: pureconfig.ConfigReader.Result[MyConfig] = Right(MyConfig(NonEmptyList(1, 2, 3),TreeSet(1, 2, 3),NonEmptyVector(1, 2, 3),Map(one -> 1, three -> 3, two -> 2))) +// res0: pureconfig.ConfigReader.Result[MyConfig] = Right(MyConfig(NonEmptyList(1, 2, 3),TreeSet(1, 2, 3),NonEmptyVector(1, 2, 3),Map(one -> 1, three -> 3, two -> 2),Chain(1, 2, 3))) ``` ### Using cats type class instances for readers and writers diff --git a/modules/cats/src/main/tut/README.md b/modules/cats/src/main/tut/README.md index 3750de45d..b02e28935 100644 --- a/modules/cats/src/main/tut/README.md +++ b/modules/cats/src/main/tut/README.md @@ -18,8 +18,9 @@ libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats" % "0.10.1" The following cats data structures are supported: -* `NonEmptyList`, `NonEmptyVector`, `NonEmptySet` +* `NonEmptyList`, `NonEmptyVector`, `NonEmptySet`, `NonEmptyChain` * `NonEmptyMap[K, V]` implicits of `ConfigReader[Map[K, V]]` and `Order[K]` should be in the scope. +* Cats data types with `Foldable` and `Alternative` (i.e. non-reducible) typeclass instances, e.g. `Chain`. For example, if your key is a `String` then `Order[String]` can be imported from `cats.instances.string._` All of these data structures rely on the instances of their unrestricted (i.e. possibly empty) variants. @@ -28,7 +29,7 @@ Custom collection readers, if any, may affect the behavior of these too. Here is an example of usage: ```tut:silent -import cats.data.{NonEmptyList, NonEmptySet, NonEmptyVector, NonEmptyMap} +import cats.data.{NonEmptyList, NonEmptySet, NonEmptyVector, NonEmptyMap, NonEmptyChain} import cats.instances.string._ import com.typesafe.config.ConfigFactory.parseString import pureconfig._ @@ -39,7 +40,8 @@ case class MyConfig( numberList: NonEmptyList[Int], numberSet: NonEmptySet[Int], numberVector: NonEmptyVector[Int], - numberMap: NonEmptyMap[String, Int] + numberMap: NonEmptyMap[String, Int], + numberChain: NonEmptyChain[Int] ) ``` @@ -49,7 +51,8 @@ val conf = parseString("""{ number-list: [1,2,3], number-set: [1,2,3], number-vector: [1,2,3], - number-map { "one": 1, "two": 2, "three": 3 } + number-map { "one": 1, "two": 2, "three": 3 }, + number-chain: [1,2,3] }""") loadConfig[MyConfig](conf) From 26a819e2c33895b989b23cc837331d42efb5413a Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Mon, 14 Jan 2019 11:31:01 +0800 Subject: [PATCH 4/8] Fixed typo in test case. --- .../cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala index d25a5f7a2..2bfd8b7d0 100644 --- a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala +++ b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala @@ -48,7 +48,7 @@ class CatsSuite extends BaseSuite with ConfigConvertChecks { config.to[NumMap] should failWith(EmptyTraversableFound("scala.collection.immutable.Map"), "numbers") } - it should "return an EmptyTraversableFound when reading empty map into NonEmptyChain" in { + it should "return an EmptyTraversableFound when reading empty chain into NonEmptyChain" in { val config = parseString("{ numbers: [] }") config.to[NumChain] should failWith(EmptyTraversableFound("cats.data.Chain"), "numbers") } From 63fa92bcf2a31be84fdaefab92d5a237400e9267 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Fri, 18 Jan 2019 00:19:42 +0800 Subject: [PATCH 5/8] Compare NumChain with defined Equality instead. --- .../scala/pureconfig/module/cats/CatsSuite.scala | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala index 2bfd8b7d0..8be71c4c6 100644 --- a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala +++ b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala @@ -3,7 +3,9 @@ package pureconfig.module.cats import cats.data._ import cats.instances.int._ import cats.instances.string._ +import cats.syntax.foldable._ import com.typesafe.config.ConfigFactory.parseString +import org.scalactic.Equality import pureconfig.generic.auto._ import pureconfig.syntax._ import pureconfig.{ BaseSuite, ConfigConvertChecks } @@ -12,6 +14,14 @@ import scala.collection.immutable.{ SortedMap, SortedSet } class CatsSuite extends BaseSuite with ConfigConvertChecks { + //TODO: Should be safe to drop after Cats version >= 1.6 (https://github.com/typelevel/cats/pull/2690) + implicit val numChainEq: Equality[NumChain] = new Equality[NumChain] { + override def areEqual(a: NumChain, b: Any): Boolean = b match { + case NumChain(nec) => implicitly[Equality[TraversableOnce[Int]]].areEqual(a.numbers.toList, nec.toList) + case _ => false + } + } + case class Numbers(numbers: NonEmptyList[Int]) case class NumVec(numbers: NonEmptyVector[Int]) case class NumSet(numbers: NonEmptySet[Int]) @@ -24,9 +34,7 @@ class CatsSuite extends BaseSuite with ConfigConvertChecks { checkReadWrite[NumMap](parseString(s"""{ numbers {"1": 1, "2": 2, "3": 3 } }""").root() → NumMap(NonEmptyMap(("1", 1), SortedMap("2" → 2, "3" → 3)))) - // Chain seems to break referential transparency unless prepend/append order is the same - checkReadWrite[NumChain](parseString(s"""{ numbers: [1,2,3] }""").root() → - NumChain(NonEmptyChain.fromChainPrepend(1, 2 +: 3 +: Chain.empty))) + checkReadWrite[NumChain](parseString(s"""{ numbers: [1,2,3] }""").root() → NumChain(NonEmptyChain(1, 2, 3))) it should "return an EmptyTraversableFound when reading empty lists into NonEmptyList" in { val config = parseString("{ numbers: [] }") From e3d08927132c25a9b374c0cd4bc2bbbfbba09a69 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Tue, 22 Jan 2019 22:10:41 +0800 Subject: [PATCH 6/8] Replaced Shapeless with Exported. --- modules/cats/build.sbt | 3 +-- .../scala/pureconfig/module/cats/package.scala | 15 +++++---------- .../scala/pureconfig/module/cats/CatsSuite.scala | 4 +--- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/modules/cats/build.sbt b/modules/cats/build.sbt index f9f891a54..9b6b6030b 100644 --- a/modules/cats/build.sbt +++ b/modules/cats/build.sbt @@ -2,8 +2,7 @@ name := "pureconfig-cats" libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % "1.4.0", - "org.typelevel" %% "cats-laws" % "1.4.0" % "test", - Dependencies.shapeless % Provided) + "org.typelevel" %% "cats-laws" % "1.4.0" % "test") developers := List( Developer("derekmorr", "Derek Morr", "morr.derek@gmail.com", url("https://github.com/derekmorr")), diff --git a/modules/cats/src/main/scala/pureconfig/module/cats/package.scala b/modules/cats/src/main/scala/pureconfig/module/cats/package.scala index b821076b4..ef3dbd804 100644 --- a/modules/cats/src/main/scala/pureconfig/module/cats/package.scala +++ b/modules/cats/src/main/scala/pureconfig/module/cats/package.scala @@ -4,8 +4,7 @@ import _root_.cats.data._ import _root_.cats.kernel.Order import _root_.cats.{ Alternative, Foldable } import _root_.cats.implicits._ -import pureconfig.{ ConfigReader, ConfigWriter } -import shapeless._ +import pureconfig.{ ConfigReader, ConfigWriter, Exported } import scala.collection.immutable.{ SortedMap, SortedSet } import scala.language.higherKinds @@ -40,14 +39,10 @@ package object cats { writer.contramap(_.toSortedMap) // For emptiable foldables not covered by TraversableOnce reader/writer, e.g. Chain. - implicit def nonReducibleReader[T, F[_]: Foldable: Alternative](implicit - reader: ConfigReader[TraversableOnce[T]], - ev: ¬¬[F[T]] <:!< (TraversableOnce[T] ∨ Option[T])): ConfigReader[F[T]] = - reader.map(to => (to :\ Alternative[F].empty[T])(_.pure[F] <+> _)) - implicit def nonReducibleWriter[T, F[_]: Foldable: Alternative](implicit - writer: ConfigWriter[TraversableOnce[T]], - ev: ¬¬[F[T]] <:!< (TraversableOnce[T] ∨ Option[T])): ConfigWriter[F[T]] = - writer.contramap(_.toList) + implicit def lowPriorityNonReducibleReader[T, F[_]: Foldable: Alternative](implicit reader: ConfigReader[TraversableOnce[T]]): Exported[ConfigReader[F[T]]] = + Exported(reader.map(to => (to :\ Alternative[F].empty[T])(_.pure[F] <+> _))) + implicit def lowPriorityNonReducibleWriter[T, F[_]: Foldable: Alternative](implicit writer: ConfigWriter[TraversableOnce[T]]): Exported[ConfigWriter[F[T]]] = + Exported(writer.contramap(_.toList)) implicit def nonEmptyChainReader[T](implicit reader: ConfigReader[Chain[T]]): ConfigReader[NonEmptyChain[T]] = fromNonEmpty(reader)(NonEmptyChain.fromChain) diff --git a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala index 8be71c4c6..f3a642d7f 100644 --- a/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala +++ b/modules/cats/src/test/scala/pureconfig/module/cats/CatsSuite.scala @@ -1,9 +1,7 @@ package pureconfig.module.cats import cats.data._ -import cats.instances.int._ -import cats.instances.string._ -import cats.syntax.foldable._ +import cats.implicits._ import com.typesafe.config.ConfigFactory.parseString import org.scalactic.Equality import pureconfig.generic.auto._ From 56355ac07dcd200d50bf5613977904cef36c076c Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Wed, 23 Jan 2019 02:57:58 +0800 Subject: [PATCH 7/8] Reordered lines to avoid confusion. --- modules/cats/README.md | 2 +- modules/cats/src/main/tut/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/cats/README.md b/modules/cats/README.md index 003cf2d84..dde026027 100644 --- a/modules/cats/README.md +++ b/modules/cats/README.md @@ -18,9 +18,9 @@ libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats" % "0.10.1" The following cats data structures are supported: +* Cats data types with `Foldable` and `Alternative` (i.e. non-reducible) typeclass instances, e.g. `Chain`. * `NonEmptyList`, `NonEmptyVector`, `NonEmptySet`, `NonEmptyChain` * `NonEmptyMap[K, V]` implicits of `ConfigReader[Map[K, V]]` and `Order[K]` should be in the scope. -* Cats data types with `Foldable` and `Alternative` (i.e. non-reducible) typeclass instances, e.g. `Chain`. For example, if your key is a `String` then `Order[String]` can be imported from `cats.instances.string._` All of these data structures rely on the instances of their unrestricted (i.e. possibly empty) variants. diff --git a/modules/cats/src/main/tut/README.md b/modules/cats/src/main/tut/README.md index b02e28935..e6c2657a8 100644 --- a/modules/cats/src/main/tut/README.md +++ b/modules/cats/src/main/tut/README.md @@ -18,9 +18,9 @@ libraryDependencies += "com.github.pureconfig" %% "pureconfig-cats" % "0.10.1" The following cats data structures are supported: +* Cats data types with `Foldable` and `Alternative` (i.e. non-reducible) typeclass instances, e.g. `Chain`. * `NonEmptyList`, `NonEmptyVector`, `NonEmptySet`, `NonEmptyChain` * `NonEmptyMap[K, V]` implicits of `ConfigReader[Map[K, V]]` and `Order[K]` should be in the scope. -* Cats data types with `Foldable` and `Alternative` (i.e. non-reducible) typeclass instances, e.g. `Chain`. For example, if your key is a `String` then `Order[String]` can be imported from `cats.instances.string._` All of these data structures rely on the instances of their unrestricted (i.e. possibly empty) variants. From 506b3226c2028be682ad0fb2c5d70ab3aa2edf61 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Thu, 24 Jan 2019 04:11:43 +0800 Subject: [PATCH 8/8] Trigger CI.