diff --git a/modules/core/shared/src/test/scala/neotype/test/definitions/Newtypes.scala b/modules/core/shared/src/test/scala/neotype/test/definitions/Newtypes.scala index b1c76d3..b2917dc 100644 --- a/modules/core/shared/src/test/scala/neotype/test/definitions/Newtypes.scala +++ b/modules/core/shared/src/test/scala/neotype/test/definitions/Newtypes.scala @@ -40,3 +40,12 @@ case class CompositeUnderlying( subtype: String, simpleSubtype: Int ) + +// Test types for stacked types +type NewtypeValidatedSubtype = NewtypeValidatedSubtype.Type +object NewtypeValidatedSubtype extends Newtype[ValidatedSubtype] + +type TwiceValidatedSubtype = TwiceValidatedSubtype.Type +object TwiceValidatedSubtype extends Subtype[ValidatedSubtype]: + override inline def validate(input: ValidatedSubtype): Boolean | String = + if input.length > 15 then true else "String must be longer than 15 characters" diff --git a/modules/neotype-zio-config/src/main/scala/neotype/interop/zioconfig/ZioConfigInstances.scala b/modules/neotype-zio-config/src/main/scala/neotype/interop/zioconfig/ZioConfigInstances.scala index ef25bba..8977ae3 100644 --- a/modules/neotype-zio-config/src/main/scala/neotype/interop/zioconfig/ZioConfigInstances.scala +++ b/modules/neotype-zio-config/src/main/scala/neotype/interop/zioconfig/ZioConfigInstances.scala @@ -10,3 +10,24 @@ given validated[A, B](using nt: ValidatedWrappedType[A, B], config: DeriveConfig given simple[A, B](using nt: SimpleWrappedType[A, B], config: DeriveConfig[A]): DeriveConfig[B] = nt.unsafeMakeF(config) + +given validatedMap[A, B, V](using + nt: ValidatedWrappedType[A, B], + mapConfig: DeriveConfig[Map[A, V]] +): DeriveConfig[Map[B, V]] = + mapConfig.mapOrFail(avMap => + avMap.foldLeft[Either[Config.Error, Map[B, V]]](Right(Map.empty)) { case (acc, (keyA, value)) => + for + map <- acc + b <- nt.make(keyA) + .left + .map(e => Config.Error.InvalidData(Chunk.empty, message = e)) + yield map + (b -> value) + } + ) + +given simpleMap[A, B, V](using + nt: SimpleWrappedType[A, B], + mapConfig: DeriveConfig[Map[A, V]] +): DeriveConfig[Map[B, V]] = + mapConfig.map(avMap => avMap.map((a, v) => nt.unsafeMake(a) -> v)) diff --git a/modules/neotype-zio-config/src/test/scala/neotype/interop/zioconfig/ZioConfigSpec.scala b/modules/neotype-zio-config/src/test/scala/neotype/interop/zioconfig/ZioConfigSpec.scala index e3b02e5..3a60307 100644 --- a/modules/neotype-zio-config/src/test/scala/neotype/interop/zioconfig/ZioConfigSpec.scala +++ b/modules/neotype-zio-config/src/test/scala/neotype/interop/zioconfig/ZioConfigSpec.scala @@ -8,6 +8,16 @@ import zio.config.magnolia.* import zio.test.* object ZioConfigSpec extends ZIOSpecDefault: + case class ValidatedMapConfig(entries: Map[ValidatedNewtype, Int]) + case class ValidatedSubtypeMapConfig(entries: Map[ValidatedSubtype, Int]) + case class NewtypeValidatedSubtypeMapConfig(entries: Map[NewtypeValidatedSubtype, Int]) + case class TwiceValidatedSubtypeMapConfig(entries: Map[TwiceValidatedSubtype, Int]) + + type SimpleStringNewtype = SimpleStringNewtype.Type + object SimpleStringNewtype extends Newtype[String] + + case class SimpleMapConfig(entries: Map[SimpleStringNewtype, Int]) + def spec = suite("zio-config")( test("successfully read config") { val expectedConfig = Composite( @@ -44,5 +54,83 @@ object ZioConfigSpec extends ZIOSpecDefault: config.is(_.left).getMessage.contains("String must not be empty"), config.is(_.left).getMessage.contains("String must be longer than 10 characters") ) - } + }, + suite("Map")( + test("validated map keys") { + val expectedConfig = ValidatedMapConfig( + Map(ValidatedNewtype("hello") -> 1, ValidatedNewtype("world") -> 2) + ) + + val source = ConfigProvider.fromMap( + Map( + "entries.hello" -> "1", + "entries.world" -> "2" + ) + ) + + for config <- read(deriveConfig[ValidatedMapConfig] from source) + yield assertTrue(config == expectedConfig) + }, + test("simple map keys") { + val expectedConfig = SimpleMapConfig( + Map(SimpleStringNewtype("foo") -> 1, SimpleStringNewtype("bar") -> 2) + ) + + val source = ConfigProvider.fromMap( + Map( + "entries.foo" -> "1", + "entries.bar" -> "2" + ) + ) + + for config <- read(deriveConfig[SimpleMapConfig] from source) + yield assertTrue(config == expectedConfig) + }, + test("fails with invalid validated map keys") { + val source = ConfigProvider.fromMap( + Map( + "entries.short" -> "1" + ) + ) + + for config <- read(deriveConfig[ValidatedSubtypeMapConfig] from source).either + yield assertTrue( + config.is(_.left).getMessage.contains("String must be longer than 10 characters") + ) + }, + test("NewtypeValidatedSubtype map keys") { + val expectedConfig = NewtypeValidatedSubtypeMapConfig( + Map(NewtypeValidatedSubtype(ValidatedSubtype("long enough key")) -> 1) + ) + + val source = ConfigProvider.fromMap( + Map("entries.long enough key" -> "1") + ) + + for config <- read(deriveConfig[NewtypeValidatedSubtypeMapConfig] from source) + yield assertTrue(config == expectedConfig) + }, + test("TwiceValidatedSubtype map keys") { + val expectedConfig = TwiceValidatedSubtypeMapConfig( + Map(TwiceValidatedSubtype.makeOrThrow(ValidatedSubtype("this key is long enough")) -> 1) + ) + + val source = ConfigProvider.fromMap( + Map("entries.this key is long enough" -> "1") + ) + + for config <- read(deriveConfig[TwiceValidatedSubtypeMapConfig] from source) + yield assertTrue(config == expectedConfig) + }, + test("fails with TwiceValidatedSubtype map keys that are too short") { + val source = ConfigProvider.fromMap( + Map("entries.twelve chars" -> "1") + ) + + for config <- read(deriveConfig[TwiceValidatedSubtypeMapConfig] from source).either + yield assertTrue( + config.is(_.left).getMessage.contains("String must be longer than 15 characters") + ) + } + ) )