diff --git a/README.md b/README.md index 0697e464c..e5e3383f3 100644 --- a/README.md +++ b/README.md @@ -39,17 +39,17 @@ _ZIO Schema_ is used by a growing number of ZIO libraries, including [ZIO Flow]( In order to use this library, we need to add the following lines in our `build.sbt` file: ```scala -libraryDependencies += "dev.zio" %% "zio-schema" % "1.1.0" -libraryDependencies += "dev.zio" %% "zio-schema-avro" % "1.1.0" -libraryDependencies += "dev.zio" %% "zio-schema-bson" % "1.1.0" -libraryDependencies += "dev.zio" %% "zio-schema-json" % "1.1.0" -libraryDependencies += "dev.zio" %% "zio-schema-msg-pack" % "1.1.0" -libraryDependencies += "dev.zio" %% "zio-schema-protobuf" % "1.1.0" -libraryDependencies += "dev.zio" %% "zio-schema-thrift" % "1.1.0" -libraryDependencies += "dev.zio" %% "zio-schema-zio-test" % "1.1.0" +libraryDependencies += "dev.zio" %% "zio-schema" % "1.1.1" +libraryDependencies += "dev.zio" %% "zio-schema-avro" % "1.1.1" +libraryDependencies += "dev.zio" %% "zio-schema-bson" % "1.1.1" +libraryDependencies += "dev.zio" %% "zio-schema-json" % "1.1.1" +libraryDependencies += "dev.zio" %% "zio-schema-msg-pack" % "1.1.1" +libraryDependencies += "dev.zio" %% "zio-schema-protobuf" % "1.1.1" +libraryDependencies += "dev.zio" %% "zio-schema-thrift" % "1.1.1" +libraryDependencies += "dev.zio" %% "zio-schema-zio-test" % "1.1.1" // Required for the automatic generic derivation of schemas -libraryDependencies += "dev.zio" %% "zio-schema-derivation" % "1.1.0" +libraryDependencies += "dev.zio" %% "zio-schema-derivation" % "1.1.1" libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided" ``` diff --git a/build.sbt b/build.sbt index 815ad2703..8d7831f37 100644 --- a/build.sbt +++ b/build.sbt @@ -442,6 +442,10 @@ lazy val docs = project mainModuleName := (zioSchemaJVM / moduleName).value, projectStage := ProjectStage.Development, ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(), + mdoc := { + (Compile / run).evaluated + mdoc.evaluated + }, readmeContribution += """| |#### TL;DR diff --git a/docs/basic-building-blocks.md b/docs/basic-building-blocks.md index c75099a92..bcb5ea7f5 100644 --- a/docs/basic-building-blocks.md +++ b/docs/basic-building-blocks.md @@ -26,19 +26,10 @@ To describe scalar data type `A`, we use the `Primitive[A]` data type which basi case class Primitive[A](standardType: StandardType[A]) extends Schema[A] ``` -Primitive values are represented using the `Primitive[A]` type class and represent the elements, that we cannot further define through other means. If we visualize our data structure as a tree, primitives are the leaves. +Primitive values are represented using the `Primitive[A]` type class and represent the elements that we cannot further define through other means. If we visualize our data structure as a tree, primitives are the leaves. -ZIO Schema provides a number of built-in primitive types, that we can use to represent our data. These can be found in the [`StandardType`](https://github.com/zio/zio-schema/blob/main/zio-schema/shared/src/main/scala/zio/schema/StandardType.scala) companion-object: +For a list of all standard types (and therefore primitive types) with built-in support, please see the [standard type reference](standard-type-reference.md) -```scala -sealed trait StandardType[A] -object StandardType { - implicit object UnitType extends StandardType[Unit] - implicit object StringType extends StandardType[String] - implicit object BoolType extends StandardType[Boolean] - // ... -} -``` Inside `Schema`'s companion object, we have an implicit conversion from `StandardType[A]` to `Schema[A]`: diff --git a/docs/sidebars.js b/docs/sidebars.js index 6c0129e4c..6aaf0364a 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -9,6 +9,7 @@ const sidebars = { "motivation", "use-cases", "basic-building-blocks", + "standard-type-reference", { type: "category", label: "Writing Schema", diff --git a/docs/standard-type-reference.md b/docs/standard-type-reference.md new file mode 100644 index 000000000..7fff7e774 --- /dev/null +++ b/docs/standard-type-reference.md @@ -0,0 +1,41 @@ +--- +id: standard-type-reference +title: "Standard Type Reference" +--- +# Standard Type Reference + +ZIO Schema provides a number of built-in primitive types, that we can use to represent our data. These can be seen in the following table: + +|Standard Type|JVM Support|ScalaJS Support|Scala Native Support| +|--------------|:--------------:|:--------------:|:--------------:| +|`Boolean`|✅|✅|✅| +|`Byte`|✅|✅|✅| +|`Char`|✅|✅|✅| +|`Chunk[Byte]`|✅|✅|✅| +|`Double`|✅|✅|✅| +|`Float`|✅|✅|✅| +|`Int`|✅|✅|✅| +|`Long`|✅|✅|✅| +|`Short`|✅|✅|✅| +|`String`|✅|✅|✅| +|`Unit`|✅|✅|✅| +|`java.math.BigDecimal`|✅|✅|✅| +|`java.math.BigInteger`|✅|✅|✅| +|`java.time.DayOfWeek`|✅|✅|✅| +|`java.time.Duration`|✅|✅|✅| +|`java.time.Instant`|✅|✅|✅| +|`java.time.LocalDate`|✅|✅|✅| +|`java.time.LocalDateTime`|✅|✅|✅| +|`java.time.LocalTime`|✅|✅|✅| +|`java.time.Month`|✅|✅|✅| +|`java.time.MonthDay`|✅|✅|✅| +|`java.time.OffsetDateTime`|✅|✅|✅| +|`java.time.OffsetTime`|✅|✅|✅| +|`java.time.Period`|✅|✅|✅| +|`java.time.Year`|✅|✅|✅| +|`java.time.YearMonth`|✅|✅|✅| +|`java.time.ZoneId`|✅|✅|✅| +|`java.time.ZoneOffset`|✅|✅|✅| +|`java.time.ZonedDateTime`|✅|✅|✅| +|`java.util.Currency`|✅|❌|❌| +|`java.util.UUID`|✅|❌|✅| \ No newline at end of file diff --git a/tests/js/src/test/scala-2/zio/schema/PlatformSpecificGen.scala b/tests/js/src/test/scala-2/zio/schema/PlatformSpecificGen.scala new file mode 100644 index 000000000..941b41d24 --- /dev/null +++ b/tests/js/src/test/scala-2/zio/schema/PlatformSpecificGen.scala @@ -0,0 +1,19 @@ +package zio.schema + +import zio.schema.SchemaGen.SchemaTest +import zio.schema.StandardTypeGen.StandardTypeAndGen +import zio.test.Gen + +object PlatformSpecificGen { + + val platformSpecificStandardTypes: Gen[Any, StandardType[_]] = Gen.fromIterable( + List.empty + ) + + def platformSpecificStandardTypeAndGen(standardTypeGen: StandardType[_]): StandardTypeAndGen[_] = + standardTypeGen match { + case _ => StandardType.UnitType -> Gen.unit: StandardTypeAndGen[_] + } + + val platformSpecificSchemasAndGens: List[SchemaTest[_]] = List.empty +} diff --git a/tests/jvm/src/test/scala-2/zio/schema/PlatformSpecificGen.scala b/tests/jvm/src/test/scala-2/zio/schema/PlatformSpecificGen.scala new file mode 100644 index 000000000..ceeb2381c --- /dev/null +++ b/tests/jvm/src/test/scala-2/zio/schema/PlatformSpecificGen.scala @@ -0,0 +1,24 @@ +package zio.schema + +import zio.schema.SchemaGen.SchemaTest +import zio.schema.StandardTypeGen.StandardTypeAndGen +import zio.test.Gen + +object PlatformSpecificGen { + + val platformSpecificStandardTypes: Gen[Any, StandardType[_]] = Gen.fromIterable( + List( + StandardType.CurrencyType + ) + ) + + def platformSpecificStandardTypeAndGen(standardTypeGen: StandardType[_]): StandardTypeAndGen[_] = + standardTypeGen match { + case typ: StandardType.CurrencyType.type => typ -> Gen.currency + case _ => StandardType.UnitType -> Gen.unit: StandardTypeAndGen[_] + } + + val platformSpecificSchemasAndGens: List[SchemaTest[_]] = List( + SchemaTest("Currency", StandardType.CurrencyType, Gen.currency) + ) +} diff --git a/tests/native/src/test/scala-2/zio/schema/PlatformSpecificGen.scala b/tests/native/src/test/scala-2/zio/schema/PlatformSpecificGen.scala new file mode 100644 index 000000000..941b41d24 --- /dev/null +++ b/tests/native/src/test/scala-2/zio/schema/PlatformSpecificGen.scala @@ -0,0 +1,19 @@ +package zio.schema + +import zio.schema.SchemaGen.SchemaTest +import zio.schema.StandardTypeGen.StandardTypeAndGen +import zio.test.Gen + +object PlatformSpecificGen { + + val platformSpecificStandardTypes: Gen[Any, StandardType[_]] = Gen.fromIterable( + List.empty + ) + + def platformSpecificStandardTypeAndGen(standardTypeGen: StandardType[_]): StandardTypeAndGen[_] = + standardTypeGen match { + case _ => StandardType.UnitType -> Gen.unit: StandardTypeAndGen[_] + } + + val platformSpecificSchemasAndGens: List[SchemaTest[_]] = List.empty +} diff --git a/tests/shared/src/test/scala-2/zio/schema/DynamicValueGen.scala b/tests/shared/src/test/scala-2/zio/schema/DynamicValueGen.scala index 1b84b6e5c..d5a5e73d4 100644 --- a/tests/shared/src/test/scala-2/zio/schema/DynamicValueGen.scala +++ b/tests/shared/src/test/scala-2/zio/schema/DynamicValueGen.scala @@ -41,6 +41,7 @@ object DynamicValueGen { case typ: StandardType.ZoneOffsetType.type => gen(typ, JavaTimeGen.anyZoneOffset) case typ: StandardType.UnitType.type => Gen.const(DynamicValue.Primitive((), typ)) case typ: StandardType.UUIDType.type => gen(typ, Gen.uuid) + case typ: StandardType.CurrencyType.type => gen(typ, Gen.currency) } } diff --git a/tests/shared/src/test/scala-2/zio/schema/PatchSpec.scala b/tests/shared/src/test/scala-2/zio/schema/PatchSpec.scala index f20285052..07701496e 100644 --- a/tests/shared/src/test/scala-2/zio/schema/PatchSpec.scala +++ b/tests/shared/src/test/scala-2/zio/schema/PatchSpec.scala @@ -1,29 +1,25 @@ package zio.schema -import zio.schema.StandardType._ +import zio.schema.DeriveSchema.gen +import zio.schema.SchemaGen.{ SchemaTest, schemasAndGens } import zio.schema.types.Arities._ import zio.schema.types.{ Arities, Recursive } import zio.test.Assertion._ import zio.test._ -import zio.{ Chunk, Scope, URIO } +import zio.{ Scope, URIO } object PatchSpec extends ZIOSpecDefault { def spec: Spec[TestEnvironment with Scope, Any] = suite("PatchSpec")( suite("identity law")( suite("standard types")( - test("Int")(patchIdentityLaw[Int]), - test("Long")(patchIdentityLaw[Long]), - test("Float")(patchIdentityLaw[Float]), - test("Double")(patchIdentityLaw[Double]), - test("Boolean")(patchIdentityLaw[Boolean]), - test("Bytes")(patchIdentityLaw[Chunk[Byte]]), - suite("Either") { - test("primitive")(patchIdentityLaw[Either[String, String]]) - }, - suite("Option") { - test("primitive")(patchIdentityLaw[Option[String]]) - } + schemaPatchIdentityLawTests() + ), + suite("option")( + schemaPatchIdentityLawTests(Some(schema => Schema.option(schema))) + ), + suite("either")( + schemaPatchIdentityLawTests(Some(schema => Schema.either(schema, schema)), Some(name => s"Either[$name,$name]")) ), suite("records")( test("singleton")(patchIdentityLaw[Singleton.type]), @@ -39,56 +35,17 @@ object PatchSpec extends ZIOSpecDefault { ), suite("patch law")( suite("standard types")( - test("Int")(patchLaw[Int]), - test("Long")(patchLaw[Long]), - test("Float")(patchLaw[Float]), - test("Double")(patchLaw[Double]), - test("Boolean")(patchLaw[Boolean]), - test("String")(patchLaw[String]), - test("ZonedDateTime")(patchLaw[java.time.ZonedDateTime]), - test("OffsetDateTime")(patchLaw[java.time.OffsetDateTime]), - test("OffsetTime")(patchLaw[java.time.OffsetTime]), - test("LocalTime")(patchLaw[java.time.LocalTime]), - test("LocalDate")(patchLaw[java.time.LocalDate]), - test("Instant")(patchLaw[java.time.Instant]), - test("Duration")(patchLaw[java.time.Duration]), - test("ZoneOffset")(patchLaw[java.time.ZoneOffset]), - test("ZoneId")(patchLaw[java.time.ZoneId]), - test("YearMonth")(patchLaw[java.time.YearMonth]), - test("Year")(patchLaw[java.time.Year]), - test("Period")(patchLaw[java.time.Period]), - test("MonthDay")(patchLaw[java.time.MonthDay]) @@ TestAspect.ignore, // TODO Leap years! - test("Month")(patchLaw[java.time.Month]), - test("DayOfWeek")(patchLaw[java.time.DayOfWeek]), - test("BigInteger")(patchLaw[java.math.BigInteger]), - test("BigDecimal")(patchLaw[java.math.BigDecimal]), - test("Bytes")(patchLaw[Chunk[Byte]]) + schemaPatchLawTests() + ), + suite("option")( + schemaPatchLawTests(Some(schema => Schema.option(schema))) ), - suite("sequences")( + suite("either")( + schemaPatchLawTests(Some(schema => Schema.either(schema, schema)), Some(name => s"Either[$name,$name]")) + ), + suite("lists")( suite("of standard types")( - test("Int")(patchLaw[List[Int]]), - test("Long")(patchLaw[List[Long]]), - test("Float")(patchLaw[List[Float]]), - test("Double")(patchLaw[List[Double]]), - test("Boolean")(patchLaw[List[Boolean]]), - test("String")(patchLaw[List[String]]), - test("ZonedDateTime")(patchLaw[List[java.time.ZonedDateTime]]), - test("OffsetDateTime")(patchLaw[List[java.time.OffsetDateTime]]), - test("OffsetTime")(patchLaw[List[java.time.OffsetTime]]), - test("LocalTime")(patchLaw[List[java.time.LocalTime]]), - test("LocalDate")(patchLaw[List[java.time.LocalDate]]), - test("Instant")(patchLaw[List[java.time.Instant]]), - test("Duration")(patchLaw[List[java.time.Duration]]), - test("ZoneOffset")(patchLaw[List[java.time.ZoneOffset]]), - test("ZoneId")(patchLaw[List[java.time.ZoneId]]), - test("YearMonth")(patchLaw[List[java.time.YearMonth]]), - test("Year")(patchLaw[List[java.time.Year]]), - test("Period")(patchLaw[List[java.time.Period]]), - test("MonthDay")(patchLaw[List[java.time.MonthDay]]) @@ TestAspect.ignore, // TODO Leap years! - test("Month")(patchLaw[List[java.time.Month]]), - test("DayOfWeek")(patchLaw[List[java.time.DayOfWeek]]), - test("BigInteger")(patchLaw[List[java.math.BigInteger]]), - test("BigDecimal")(patchLaw[List[java.math.BigDecimal]]) + schemaPatchLawTests(Some((primitiveSchema => Schema.list(primitiveSchema)))) ), suite("of records")( test("Dog")(patchLaw[List[Pet.Dog]]) @@ -98,31 +55,21 @@ object PatchSpec extends ZIOSpecDefault { test("recursive")(patchLaw[List[Recursive]]) ) ), + suite("vectors")( + suite("of standard types")( + schemaPatchLawTests(Some((primitiveSchema => Schema.vector(primitiveSchema)))) + ), + suite("of records")( + test("Dog")(patchLaw[Vector[Pet.Dog]]) + ), + suite("of enumerations")( + test("Pet")(patchLaw[Vector[Pet]]), + test("recursive")(patchLaw[Vector[Recursive]]) + ) + ), suite("sets")( suite("of standard types")( - test("Int")(patchLaw[Set[Int]]), - test("Long")(patchLaw[Set[Long]]), - test("Float")(patchLaw[Set[Float]]), - test("Double")(patchLaw[Set[Double]]), - test("Boolean")(patchLaw[Set[Boolean]]), - test("String")(patchLaw[Set[String]]), - test("ZonedDateTime")(patchLaw[Set[java.time.ZonedDateTime]]), - test("OffsetDateTime")(patchLaw[Set[java.time.OffsetDateTime]]), - test("OffsetTime")(patchLaw[Set[java.time.OffsetTime]]), - test("LocalTime")(patchLaw[Set[java.time.LocalTime]]), - test("LocalDate")(patchLaw[Set[java.time.LocalDate]]), - test("Instant")(patchLaw[Set[java.time.Instant]]), - test("Duration")(patchLaw[Set[java.time.Duration]]), - test("ZoneOffset")(patchLaw[Set[java.time.ZoneOffset]]), - test("ZoneId")(patchLaw[Set[java.time.ZoneId]]), - test("YearMonth")(patchLaw[Set[java.time.YearMonth]]), - test("Year")(patchLaw[Set[java.time.Year]]), - test("Period")(patchLaw[Set[java.time.Period]]), - test("MonthDay")(patchLaw[Set[java.time.MonthDay]]) @@ TestAspect.ignore, // TODO Leap years! - test("Month")(patchLaw[Set[java.time.Month]]), - test("DayOfWeek")(patchLaw[Set[java.time.DayOfWeek]]), - test("BigInteger")(patchLaw[Set[java.math.BigInteger]]), - test("BigDecimal")(patchLaw[Set[java.math.BigDecimal]]) + schemaPatchLawTests(Some((primitiveSchema => Schema.set(primitiveSchema)))) ), suite("of records")( test("Dog")(patchLaw[Set[Pet.Dog]]) @@ -134,7 +81,10 @@ object PatchSpec extends ZIOSpecDefault { ), suite("maps")( suite("of standard types")( - test("Int -> Int")(patchLaw[Map[Int, Int]]) + schemaPatchLawTests( + Some((primitiveSchema => Schema.map(primitiveSchema, primitiveSchema))), + Some(name => (s"$name -> $name")) + ) ), suite("of records")( test("Int -> Dog")(patchLaw[Map[Int, Pet.Dog]]), @@ -173,6 +123,42 @@ object PatchSpec extends ZIOSpecDefault { assertTrue(schema.diff(a, a).isIdentical) } + private def schemaPatchIdentityLawTests( + schemaConversionFuncOption: Option[Schema[_] => Schema[_]] = None, + renamingFuncOption: Option[String => String] = None + ): List[Spec[Sized with TestConfig, Nothing]] = + schemaPatchTests(schema => patchIdentityLaw(schema), schemaConversionFuncOption, renamingFuncOption) + + private def schemaPatchLawTests( + schemaConversionFuncOption: Option[Schema[_] => Schema[_]] = None, + renamingFuncOption: Option[String => String] = None + ): List[Spec[Sized with TestConfig, Nothing]] = + schemaPatchTests(schema => patchLaw(schema), schemaConversionFuncOption, renamingFuncOption) + + private def schemaPatchTests( + patchingFunc: Schema[_] => URIO[Sized with TestConfig, TestResult], + schemaConversionFuncOption: Option[Schema[_] => Schema[_]], + renamingFuncOption: Option[String => String] + ): List[Spec[Sized with TestConfig, Nothing]] = + schemasAndGens.map { + case SchemaTest(name, standardType, _) => + val primitiveSchema = Schema.primitive(standardType) + val finalSchema = schemaConversionFuncOption.fold[Schema[_]](primitiveSchema) { schemaConversionFunc => + schemaConversionFunc(primitiveSchema) + } + val finalTestName = renamingFuncOption.fold(name)(renamingFunc => renamingFunc(name)) + standardType match { + case _ @StandardType.MonthDayType => + test(finalTestName) { + patchingFunc(finalSchema) + } @@ TestAspect.ignore // TODO Leap years! + case _ => + test(finalTestName) { + patchingFunc(finalSchema) + } + } + } + private def patchLaw[A](implicit schema: Schema[A]): URIO[Sized with TestConfig, TestResult] = { val gen = DeriveGen.gen[A] check(gen <*> gen) { diff --git a/tests/shared/src/test/scala-2/zio/schema/SchemaGen.scala b/tests/shared/src/test/scala-2/zio/schema/SchemaGen.scala index 5fba0879e..46405ce2c 100644 --- a/tests/shared/src/test/scala-2/zio/schema/SchemaGen.scala +++ b/tests/shared/src/test/scala-2/zio/schema/SchemaGen.scala @@ -3,6 +3,7 @@ package zio.schema import scala.collection.immutable.ListMap import zio.Chunk +import zio.schema.PlatformSpecificGen.platformSpecificSchemasAndGens import zio.test.{ Gen, Sized } object SchemaGen { @@ -692,64 +693,64 @@ object SchemaGen { case class SchemaTest[A](name: String, schema: StandardType[A], gen: Gen[Sized, A]) - def schemasAndGens: List[SchemaTest[_]] = List( - SchemaTest("String", StandardType.StringType, Gen.string), - SchemaTest("Bool", StandardType.BoolType, Gen.boolean), - SchemaTest("Short", StandardType.ShortType, Gen.short), - SchemaTest("Int", StandardType.IntType, Gen.int), - SchemaTest("Long", StandardType.LongType, Gen.long), - SchemaTest("Float", StandardType.FloatType, Gen.float), - SchemaTest("Double", StandardType.DoubleType, Gen.double), - SchemaTest("Binary", StandardType.BinaryType, Gen.chunkOf(Gen.byte)), - SchemaTest("Char", StandardType.CharType, Gen.asciiChar), - SchemaTest("UUID", StandardType.UUIDType, Gen.uuid), - SchemaTest( - "BigDecimal", - StandardType.BigDecimalType, - Gen.double.map(d => java.math.BigDecimal.valueOf(d)) - ), - SchemaTest( - "BigInteger", - StandardType.BigIntegerType, - Gen.long.map(n => java.math.BigInteger.valueOf(n)) - ), - SchemaTest("DayOfWeek", StandardType.DayOfWeekType, JavaTimeGen.anyDayOfWeek), - SchemaTest("Duration", StandardType.DurationType, JavaTimeGen.anyDuration), - SchemaTest("Instant", StandardType.InstantType, JavaTimeGen.anyInstant), - SchemaTest("LocalDate", StandardType.LocalDateType, JavaTimeGen.anyLocalDate), - SchemaTest( - "LocalDateTime", - StandardType.LocalDateTimeType, - JavaTimeGen.anyLocalDateTime - ), - SchemaTest( - "LocalTime", - StandardType.LocalTimeType, - JavaTimeGen.anyLocalTime - ), - SchemaTest("Month", StandardType.MonthType, JavaTimeGen.anyMonth), - SchemaTest("MonthDay", StandardType.MonthDayType, JavaTimeGen.anyMonthDay), - SchemaTest( - "OffsetDateTime", - StandardType.OffsetDateTimeType, - JavaTimeGen.anyOffsetDateTime - ), - SchemaTest( - "OffsetTime", - StandardType.OffsetTimeType, - JavaTimeGen.anyOffsetTime - ), - SchemaTest("Period", StandardType.PeriodType, JavaTimeGen.anyPeriod), - SchemaTest("Year", StandardType.YearType, JavaTimeGen.anyYear), - SchemaTest("YearMonth", StandardType.YearMonthType, JavaTimeGen.anyYearMonth), - SchemaTest( - "ZonedDateTime", - StandardType.ZonedDateTimeType, - JavaTimeGen.anyZonedDateTime - ), - SchemaTest("ZoneId", StandardType.ZoneIdType, JavaTimeGen.anyZoneId), - SchemaTest("ZoneOffset", StandardType.ZoneOffsetType, JavaTimeGen.anyZoneOffset), - SchemaTest("UnitType", StandardType.UnitType, Gen.unit) - ) - + def schemasAndGens: List[SchemaTest[_]] = + List( + SchemaTest("String", StandardType.StringType, Gen.string), + SchemaTest("Bool", StandardType.BoolType, Gen.boolean), + SchemaTest("Short", StandardType.ShortType, Gen.short), + SchemaTest("Int", StandardType.IntType, Gen.int), + SchemaTest("Long", StandardType.LongType, Gen.long), + SchemaTest("Float", StandardType.FloatType, Gen.float), + SchemaTest("Double", StandardType.DoubleType, Gen.double), + SchemaTest("Binary", StandardType.BinaryType, Gen.chunkOf(Gen.byte)), + SchemaTest("Char", StandardType.CharType, Gen.asciiChar), + SchemaTest("UUID", StandardType.UUIDType, Gen.uuid), + SchemaTest( + "BigDecimal", + StandardType.BigDecimalType, + Gen.double.map(d => java.math.BigDecimal.valueOf(d)) + ), + SchemaTest( + "BigInteger", + StandardType.BigIntegerType, + Gen.long.map(n => java.math.BigInteger.valueOf(n)) + ), + SchemaTest("DayOfWeek", StandardType.DayOfWeekType, JavaTimeGen.anyDayOfWeek), + SchemaTest("Duration", StandardType.DurationType, JavaTimeGen.anyDuration), + SchemaTest("Instant", StandardType.InstantType, JavaTimeGen.anyInstant), + SchemaTest("LocalDate", StandardType.LocalDateType, JavaTimeGen.anyLocalDate), + SchemaTest( + "LocalDateTime", + StandardType.LocalDateTimeType, + JavaTimeGen.anyLocalDateTime + ), + SchemaTest( + "LocalTime", + StandardType.LocalTimeType, + JavaTimeGen.anyLocalTime + ), + SchemaTest("Month", StandardType.MonthType, JavaTimeGen.anyMonth), + SchemaTest("MonthDay", StandardType.MonthDayType, JavaTimeGen.anyMonthDay), + SchemaTest( + "OffsetDateTime", + StandardType.OffsetDateTimeType, + JavaTimeGen.anyOffsetDateTime + ), + SchemaTest( + "OffsetTime", + StandardType.OffsetTimeType, + JavaTimeGen.anyOffsetTime + ), + SchemaTest("Period", StandardType.PeriodType, JavaTimeGen.anyPeriod), + SchemaTest("Year", StandardType.YearType, JavaTimeGen.anyYear), + SchemaTest("YearMonth", StandardType.YearMonthType, JavaTimeGen.anyYearMonth), + SchemaTest( + "ZonedDateTime", + StandardType.ZonedDateTimeType, + JavaTimeGen.anyZonedDateTime + ), + SchemaTest("ZoneId", StandardType.ZoneIdType, JavaTimeGen.anyZoneId), + SchemaTest("ZoneOffset", StandardType.ZoneOffsetType, JavaTimeGen.anyZoneOffset), + SchemaTest("UnitType", StandardType.UnitType, Gen.unit) + ) ++ platformSpecificSchemasAndGens } diff --git a/tests/shared/src/test/scala-2/zio/schema/StandardTypeGen.scala b/tests/shared/src/test/scala-2/zio/schema/StandardTypeGen.scala index 11752ff76..98a5570be 100644 --- a/tests/shared/src/test/scala-2/zio/schema/StandardTypeGen.scala +++ b/tests/shared/src/test/scala-2/zio/schema/StandardTypeGen.scala @@ -2,43 +2,44 @@ package zio.schema import java.math.{ BigDecimal => JBigDecimal, BigInteger => JBigInt } +import zio.schema.PlatformSpecificGen.{ platformSpecificStandardTypeAndGen, platformSpecificStandardTypes } import zio.test.{ Gen, Sized } object StandardTypeGen { + //IMPORTANT! - Updating the following list without updating the schema primitive case set in zio.schema.DynamicValue.schema will trigger a set of very obscure test failures val anyStandardType: Gen[Any, StandardType[_]] = Gen.fromIterable( List( - (StandardType.StringType), - (StandardType.BoolType), - (StandardType.ShortType), - (StandardType.IntType), - (StandardType.LongType), - (StandardType.FloatType), - (StandardType.DoubleType), - (StandardType.BinaryType), - (StandardType.BigDecimalType), - (StandardType.BigIntegerType), - (StandardType.CharType), - (StandardType.UUIDType), - (StandardType.DayOfWeekType), - (StandardType.DurationType), - (StandardType.InstantType), - (StandardType.LocalDateType), - (StandardType.LocalDateTimeType), - (StandardType.LocalTimeType), - (StandardType.MonthType), - (StandardType.MonthDayType), - (StandardType.OffsetDateTimeType), - (StandardType.OffsetTimeType), - (StandardType.PeriodType), - (StandardType.YearType), - (StandardType.YearMonthType), - (StandardType.ZonedDateTimeType), - (StandardType.ZoneIdType) + StandardType.StringType, + StandardType.BoolType, + StandardType.ShortType, + StandardType.IntType, + StandardType.LongType, + StandardType.FloatType, + StandardType.DoubleType, + StandardType.BinaryType, + StandardType.BigDecimalType, + StandardType.BigIntegerType, + StandardType.CharType, + StandardType.UUIDType, + StandardType.DayOfWeekType, + StandardType.DurationType, + StandardType.InstantType, + StandardType.LocalDateType, + StandardType.LocalDateTimeType, + StandardType.LocalTimeType, + StandardType.MonthType, + StandardType.MonthDayType, + StandardType.OffsetDateTimeType, + StandardType.OffsetTimeType, + StandardType.PeriodType, + StandardType.YearType, + StandardType.YearMonthType, + StandardType.ZonedDateTimeType, + StandardType.ZoneIdType, + StandardType.ZoneOffsetType ) - //FIXME For some reason adding this causes other unrelated tests to break. -// Gen.const(StandardType.ZoneOffset) - ) + ) ++ platformSpecificStandardTypes val javaBigInt: Gen[Any, JBigInt] = Gen.bigInt(JBigInt.valueOf(Long.MinValue), JBigInt.valueOf(Long.MaxValue)).map { sBigInt => @@ -80,7 +81,7 @@ object StandardTypeGen { case typ: StandardType.ZonedDateTimeType.type => typ -> JavaTimeGen.anyZonedDateTime case typ: StandardType.ZoneIdType.type => typ -> JavaTimeGen.anyZoneId case typ: StandardType.ZoneOffsetType.type => typ -> JavaTimeGen.anyZoneOffset - case _ => StandardType.UnitType -> Gen.unit: StandardTypeAndGen[_] + case typ => platformSpecificStandardTypeAndGen(typ) } } } diff --git a/tests/shared/src/test/scala/zio/schema/DefaultValueSpec.scala b/tests/shared/src/test/scala/zio/schema/DefaultValueSpec.scala index 7f917c6b6..8e3cf542b 100644 --- a/tests/shared/src/test/scala/zio/schema/DefaultValueSpec.scala +++ b/tests/shared/src/test/scala/zio/schema/DefaultValueSpec.scala @@ -4,7 +4,7 @@ import zio.Chunk import zio.schema.CaseSet.caseOf import zio.schema.Schema.{ Lazy, Primitive } import zio.test.Assertion._ -import zio.test.{ Spec, ZIOSpecDefault, assert } +import zio.test.{ Spec, TestAspect, ZIOSpecDefault, assert } object DefaultValueSpec extends ZIOSpecDefault { // Record Tests @@ -141,7 +141,14 @@ object DefaultValueSpec extends ZIOSpecDefault { assert(Primitive(StandardType.ZonedDateTimeType).defaultValue)( isRight(isSubtype[java.time.ZonedDateTime](anything)) ) - } + }, + test("Currency default value") { + java.util.Locale + .setDefault(java.util.Locale.US) //This is a workaround for the default locale not being set in the JVM in Github actions as of May, 2024. + assert(Primitive(StandardType.CurrencyType).defaultValue)( + isRight(isSubtype[java.util.Currency](anything)) + ) + } @@ TestAspect.jvmOnly ), suite("Record")( test("basic") { diff --git a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala index 59d8fe424..e1bca4ff5 100644 --- a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala +++ b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroCodec.scala @@ -424,6 +424,12 @@ object AvroCodec { .toEither .left .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.CurrencyType => + Try(value.asInstanceOf[Utf8]) + .flatMap(c => Try(java.util.Currency.getInstance(c.toString))) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) } private def decodeMap(value: Any, schema: Schema.Map[Any, Any]) = { @@ -930,6 +936,8 @@ object AvroCodec { case StandardType.ZonedDateTimeType => val zonedDateTime = a.asInstanceOf[java.time.ZonedDateTime] zonedDateTime.toString + case StandardType.CurrencyType => + a.asInstanceOf[java.util.Currency].getCurrencyCode } private def encodeSequence[A](schema: Schema[A], v: Chunk[A]): Any = { diff --git a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroPropMarker.scala b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroPropMarker.scala index 9217fcced..df0aaab24 100644 --- a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroPropMarker.scala +++ b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroPropMarker.scala @@ -25,6 +25,10 @@ object AvroPropMarker { override def propName: String = "zio.schema.codec.avro.either" } + case object CurrencyWrapper extends AvroPropMarker { + override def propName: String = "zio.schema.codec.avro.currency" + } + final case class DurationChronoUnit(chronoUnit: ChronoUnit) extends AvroPropMarker { override def propName: String = DurationChronoUnit.propName override def value: Any = chronoUnit.name() diff --git a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala index cad31b7d1..95cf24b4c 100644 --- a/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala +++ b/zio-schema-avro/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala @@ -378,6 +378,12 @@ object AvroSchemaCodec extends AvroSchemaCodec { .create(SchemaAvro.Type.STRING) .addMarkerProp(StringDiscriminator(StringType.ZoneDateTime)) ) + case StandardType.CurrencyType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(CurrencyWrapper) + ) } case Optional(codec, _) => for { diff --git a/zio-schema-avro/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala b/zio-schema-avro/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala index 3e91df872..b08832d7e 100644 --- a/zio-schema-avro/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala +++ b/zio-schema-avro/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala @@ -194,6 +194,11 @@ object AvroCodecSpec extends ZIOSpecDefault { val bytes = codec.encode(UUID.randomUUID()) assertTrue(bytes.length == 37) }, + test("Encode Currency") { + val codec = AvroCodec.schemaBasedBinaryCodec[java.util.Currency] + val bytes = codec.encode(java.util.Currency.getInstance("USD")) + assertTrue(bytes.length == 4) + }, test("Encode BigDecimal") { val codec = AvroCodec.schemaBasedBinaryCodec[BigDecimal] val bytes = codec.encode(BigDecimal.valueOf(42.0)) @@ -468,6 +473,13 @@ object AvroCodecSpec extends ZIOSpecDefault { val result = codec.decode(bytes) assertTrue(result == Right(uuid)) }, + test("Decode Currency") { + val codec = AvroCodec.schemaBasedBinaryCodec[java.util.Currency] + val currency = java.util.Currency.getInstance("USD") + val bytes = codec.encode(currency) + val result = codec.decode(bytes) + assertTrue(result == Right(currency)) + }, test("Decode BigDecimal") { val codec = AvroCodec.schemaBasedBinaryCodec[BigDecimal] val bigDecimal = BigDecimal(42) diff --git a/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala b/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala index cc2557f98..67972c64c 100644 --- a/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala +++ b/zio-schema-bson/src/main/scala/zio/schema/codec/BsonSchemaCodec.scala @@ -440,6 +440,7 @@ object BsonSchemaCodec { case StandardType.ZoneIdType => BsonCodec.zoneId.asInstanceOf[BsonCodec[A]] //BsonCodec[java.time.ZoneId] case StandardType.ZoneOffsetType => BsonCodec.zoneOffset.asInstanceOf[BsonCodec[A]] //BsonCodec[java.time.ZoneOffset] + case StandardType.CurrencyType => BsonCodec.currency.asInstanceOf[BsonCodec[A]] //BsonCodec[java.util.Currency] } } diff --git a/zio-schema-docs/src/main/scala/zio/schema/doc/generator/Main.scala b/zio-schema-docs/src/main/scala/zio/schema/doc/generator/Main.scala new file mode 100644 index 000000000..febab3102 --- /dev/null +++ b/zio-schema-docs/src/main/scala/zio/schema/doc/generator/Main.scala @@ -0,0 +1,85 @@ +package zio.schema.doc.generator + +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.{ Files, Paths } + +import scala.io.Source + +object Main { + + private val standardTypeFile = + Paths.get(".", "zio-schema", "jvm", "src", "main", "scala", "zio", "schema", "StandardType.scala").toFile + + final private case class StandardTypeForDoc( + name: String, + isJVMSupported: Boolean = true, + isJSSupported: Boolean = true, + isNativeSupported: Boolean = true + ) + + private def convertBooleanToText(bool: Boolean): String = + if (bool) { + "✅" + } else { + "❌" + } + + private def generateStandardTypeFileText(standardTypeFile: File): String = { + val standardTypeRegex = "StandardType\\[.*".r + val suffixRegex = "((])(?!.*])).*".r + var unsortedStandardTypes: Vector[StandardTypeForDoc] = Vector.empty + var markdownFile = + """--- + |id: standard-type-reference + |title: "Standard Type Reference" + |--- + |# Standard Type Reference + | + |ZIO Schema provides a number of built-in primitive types, that we can use to represent our data. These can be seen in the following table: + | + ||Standard Type|JVM Support|ScalaJS Support|Scala Native Support| + ||--------------|:--------------:|:--------------:|:--------------:|""".stripMargin + + val source = Source.fromFile(standardTypeFile) + + try { + for (line <- source.getLines()) { + if (line.contains("implicit object")) { + val unparsedLine = standardTypeRegex + .findFirstIn(line) + .getOrElse("Expected StandardType to be present in line while parsing standard type doc") + val trimmedLine = unparsedLine.trim() + val lineWithoutPrefix = trimmedLine.replace("StandardType[", "") + val standardTypeStr = suffixRegex.replaceFirstIn(lineWithoutPrefix, "") + val standardTypeForDoc = standardTypeStr match { + case typ @ "java.util.UUID" => StandardTypeForDoc(typ, isJSSupported = false) + case typ @ "java.util.Currency" => StandardTypeForDoc(typ, isJSSupported = false, isNativeSupported = false) + case typ => StandardTypeForDoc(typ) + } + unsortedStandardTypes = unsortedStandardTypes :+ standardTypeForDoc + } + } + + val sortedStandardTypes = unsortedStandardTypes.sortBy(_.name) + sortedStandardTypes.foreach { standardType => + val jsSupport = convertBooleanToText(standardType.isJSSupported) + val jvmSupport = convertBooleanToText(standardType.isJVMSupported) + val nativeSupport = convertBooleanToText(standardType.isNativeSupported) + markdownFile += s"\n|`${standardType.name}`|$jvmSupport|$jsSupport|$nativeSupport|" + } + markdownFile + } finally { + source.close() + } + } + + def main(args: Array[String]): Unit = { + val table = generateStandardTypeFileText(standardTypeFile) + Files.write( + Paths.get(".", "docs", "standard-type-reference.md"), + table.getBytes(StandardCharsets.UTF_8) + ) + () + } +} diff --git a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala index 758b5adbd..5c28e7a2c 100644 --- a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala @@ -161,6 +161,7 @@ object JsonCodec { case StandardType.ZonedDateTimeType => ZJsonCodec.zonedDateTime //ZJsonCodec[java.time.ZonedDateTime] case StandardType.ZoneIdType => ZJsonCodec.zoneId //ZJsonCodec[java.time.ZoneId] case StandardType.ZoneOffsetType => ZJsonCodec.zoneOffset //ZJsonCodec[java.time.ZoneOffset] + case StandardType.CurrencyType => ZJsonCodec.currency //ZJsonCodec[java.util.Currency] } } @@ -697,7 +698,7 @@ object JsonCodec { cases.map(case_ => case_.id -> case_.schema.asInstanceOf[Schema.CaseClass0[Z]].defaultConstruct()).toMap ZJsonDecoder.string.mapOrFail( s => - caseMap.get(caseNameAliases.get(s).getOrElse(s)) match { + caseMap.get(caseNameAliases.getOrElse(s, s)) match { case Some(z) => Right(z) case None => Left("unrecognized string") } diff --git a/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala b/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala index 3a72e3031..a658fc806 100644 --- a/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala @@ -58,6 +58,7 @@ package object json { case StandardType.OffsetTimeType => Json.Str(value.asInstanceOf[java.time.OffsetTime].toString) case StandardType.OffsetDateTimeType => Json.Str(value.asInstanceOf[java.time.OffsetDateTime].toString) case StandardType.ZonedDateTimeType => Json.Str(value.asInstanceOf[java.time.ZonedDateTime].toString) + case StandardType.CurrencyType => Json.Str(value.asInstanceOf[java.util.Currency].toString) } case DynamicValue.Singleton(_) => Json.Obj() case DynamicValue.SomeValue(value) => toJson(value) diff --git a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala index 923c19da5..dd33e8ccf 100644 --- a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala +++ b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala @@ -46,7 +46,13 @@ object JsonCodecSpec extends ZIOSpecDefault { }, test("ZoneId") { assertEncodesJson(Schema.Primitive(StandardType.ZoneIdType), ZoneId.systemDefault()) - } + }, + test("Currency") { + assertEncodesJson( + Schema.Primitive(StandardType.CurrencyType), + java.util.Currency.getInstance("USD") + ) + } @@ TestAspect.jvmOnly ), suite("fallback")( test("left") { @@ -415,7 +421,12 @@ object JsonCodecSpec extends ZIOSpecDefault { test("any") { check(Gen.string)(s => assertDecodes(Schema[String], s, stringify(s))) } - ) + ), + test("Currency") { + check(Gen.currency)( + currency => assertDecodes(Schema[java.util.Currency], currency, stringify(currency.getCurrencyCode)) + ) + } @@ TestAspect.jvmOnly ), suite("generic record")( test("with extra fields") { @@ -1410,7 +1421,7 @@ object JsonCodecSpec extends ZIOSpecDefault { assertZIO(stream)(equalTo(json)) } - private def assertEncodesJson[A](schema: Schema[A], value: A)(implicit enc: JsonEncoder[A]) = { + def assertEncodesJson[A](schema: Schema[A], value: A)(implicit enc: JsonEncoder[A]): ZIO[Any, Nothing, TestResult] = { val stream = ZStream .succeed(value) .via(JsonCodec.schemaBasedBinaryCodec[A](schema).streamEncoder) diff --git a/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackDecoder.scala b/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackDecoder.scala index 0e7e3bc05..cad77533f 100644 --- a/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackDecoder.scala +++ b/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackDecoder.scala @@ -279,6 +279,16 @@ private[codec] class MessagePackDecoder(bytes: Chunk[Byte]) { decodeString(path).map(OffsetDateTime.parse(_)) case StandardType.ZonedDateTimeType => decodeString(path).map(ZonedDateTime.parse(_)) + case StandardType.CurrencyType => + decodeString(path).flatMap { rawCurrencyCode => + try { + Right { + java.util.Currency.getInstance(rawCurrencyCode) + } + } catch { + case NonFatal(err) => fail(path, s"Invalid currency code: ${err.getMessage}") + } + } case _ => fail(path, s"Unsupported primitive type $standardType") } diff --git a/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackEncoder.scala b/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackEncoder.scala index e7e6097ae..7c5e5ea64 100644 --- a/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackEncoder.scala +++ b/zio-schema-msg-pack/src/main/scala/zio/schema/codec/MessagePackEncoder.scala @@ -260,6 +260,9 @@ private[codec] class MessagePackEncoder { case (StandardType.ZonedDateTimeType, v: ZonedDateTime) => packer.packString(v.toString) () + case (StandardType.CurrencyType, v: java.util.Currency) => + packer.packString(v.getCurrencyCode) + () case (_, _) => throw new NotImplementedError(s"No encoder for $standardType") } diff --git a/zio-schema-msg-pack/src/test/scala-2/zio/schema/codec/MessagePackCodecSpec.scala b/zio-schema-msg-pack/src/test/scala-2/zio/schema/codec/MessagePackCodecSpec.scala index fd99401cc..2de452bdd 100644 --- a/zio-schema-msg-pack/src/test/scala-2/zio/schema/codec/MessagePackCodecSpec.scala +++ b/zio-schema-msg-pack/src/test/scala-2/zio/schema/codec/MessagePackCodecSpec.scala @@ -221,6 +221,13 @@ object MessagePackCodecSpec extends ZIOSpecDefault { ed2 <- encodeAndDecodeNS(Schema[UUID], value) } yield assert(ed)(equalTo(Chunk(value))) && assert(ed2)(equalTo(value)) }, + test("currencies") { + val value = java.util.Currency.getInstance("USD") + for { + ed <- encodeAndDecode(Schema[java.util.Currency], value) + ed2 <- encodeAndDecodeNS(Schema[java.util.Currency], value) + } yield assert(ed)(equalTo(Chunk(value))) && assert(ed2)(equalTo(value)) + }, test("day of weeks") { val value = DayOfWeek.of(3) for { diff --git a/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala b/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala index e319d2b8d..77a87e90f 100644 --- a/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala +++ b/zio-schema-protobuf/shared/src/main/scala/zio/schema/codec/ProtobufCodec.scala @@ -97,6 +97,7 @@ object ProtobufCodec { case StandardType.OffsetTimeType => false case StandardType.OffsetDateTimeType => false case StandardType.ZonedDateTimeType => false + case StandardType.CurrencyType => false } } @@ -424,6 +425,8 @@ object ProtobufCodec { encodePrimitive(fieldNumber, StandardType.StringType, v.toString) case (StandardType.ZonedDateTimeType, v: ZonedDateTime) => encodePrimitive(fieldNumber, StandardType.StringType, v.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)) + case (StandardType.CurrencyType, v: java.util.Currency) => + encodePrimitive(fieldNumber, StandardType.StringType, v.getCurrencyCode) case (_, _) => throw new NotImplementedError(s"No encoder for $standardType") } @@ -610,7 +613,8 @@ object ProtobufCodec { OffsetDateTime.parse(stringDecoder(context)) case StandardType.ZonedDateTimeType => ZonedDateTime.parse(stringDecoder(context)) - case st => fail(context, s"Unsupported primitive type $st") + case StandardType.CurrencyType => java.util.Currency.getInstance(stringDecoder(context)) + case st => fail(context, s"Unsupported primitive type $st") } override protected def startCreatingRecord(context: DecoderContext, record: Schema.Record[_]): DecoderContext = diff --git a/zio-schema-protobuf/shared/src/test/scala-2/zio/schema/codec/ProtobufCodecSpec.scala b/zio-schema-protobuf/shared/src/test/scala-2/zio/schema/codec/ProtobufCodecSpec.scala index dd4a94db7..1d4d5b9fe 100644 --- a/zio-schema-protobuf/shared/src/test/scala-2/zio/schema/codec/ProtobufCodecSpec.scala +++ b/zio-schema-protobuf/shared/src/test/scala-2/zio/schema/codec/ProtobufCodecSpec.scala @@ -400,6 +400,14 @@ object ProtobufCodecSpec extends ZIOSpecDefault { } yield assert(ed)(equalTo(Chunk(value))) && assert(ed2)(equalTo(value)) } }, + test("currencies") { + check(Gen.currency) { value => + for { + ed <- encodeAndDecode(Schema[java.util.Currency], value) + ed2 <- encodeAndDecodeNS(Schema[java.util.Currency], value) + } yield assert(ed)(equalTo(Chunk(value))) && assert(ed2)(equalTo(value)) + } + } @@ TestAspect.jvmOnly, test("packed sequences") { check(Gen.listOf(Gen.int)) { ints => val list = PackedList(ints) diff --git a/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala b/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala index 506c76337..bfc928d58 100644 --- a/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala +++ b/zio-schema-thrift/src/main/scala/zio/schema/codec/ThriftCodec.scala @@ -371,6 +371,8 @@ object ThriftCodec { p.writeString(v.toString) case (StandardType.ZonedDateTimeType, v: ZonedDateTime) => p.writeString(v.toString) + case (StandardType.CurrencyType, v: java.util.Currency) => + p.writeString(v.getCurrencyCode) case (_, _) => fail(s"No encoder for $standardType") } @@ -424,6 +426,7 @@ object ThriftCodec { case StandardType.OffsetTimeType => TType.STRING case StandardType.OffsetDateTimeType => TType.STRING case StandardType.ZonedDateTimeType => TType.STRING + case StandardType.CurrencyType => TType.STRING case _ => TType.VOID } @@ -588,6 +591,8 @@ object ThriftCodec { OffsetDateTime.parse(decodeString(context.path)) case StandardType.ZonedDateTimeType => ZonedDateTime.parse(decodeString(context.path)) + case StandardType.CurrencyType => + java.util.Currency.getInstance(decodeString(context.path)) case _ => fail(context, s"Unsupported primitive type $typ") } diff --git a/zio-schema-thrift/src/test/scala-2/zio/schema/codec/ThriftCodecSpec.scala b/zio-schema-thrift/src/test/scala-2/zio/schema/codec/ThriftCodecSpec.scala index a45fb452b..5f21af2ca 100644 --- a/zio-schema-thrift/src/test/scala-2/zio/schema/codec/ThriftCodecSpec.scala +++ b/zio-schema-thrift/src/test/scala-2/zio/schema/codec/ThriftCodecSpec.scala @@ -273,6 +273,13 @@ object ThriftCodecSpec extends ZIOSpecDefault { ed2 <- encodeAndDecodeNS(Schema[UUID], value) } yield assert(ed)(equalTo(Chunk(value))) && assert(ed2)(equalTo(value)) }, + test("currencies") { + val value = java.util.Currency.getInstance("USD") + for { + ed <- encodeAndDecode(Schema[java.util.Currency], value) + ed2 <- encodeAndDecodeNS(Schema[java.util.Currency], value) + } yield assert(ed)(equalTo(Chunk(value))) && assert(ed2)(equalTo(value)) + }, test("day of weeks") { val value = DayOfWeek.of(3) for { diff --git a/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala b/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala index c2cf96731..5decbb32e 100644 --- a/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala +++ b/zio-schema-zio-test/shared/src/main/scala/zio/schema/DeriveGen.scala @@ -513,6 +513,7 @@ object DeriveGen { case StandardType.OffsetTimeType => Gen.offsetTime case StandardType.OffsetDateTimeType => Gen.offsetDateTime case StandardType.ZonedDateTimeType => Gen.zonedDateTime + case StandardType.CurrencyType => Gen.currency } gen.map(_.asInstanceOf[A]) diff --git a/zio-schema/js/src/main/scala/zio/schema/StandardType.scala b/zio-schema/js/src/main/scala/zio/schema/StandardType.scala index e561bef75..152609bdf 100644 --- a/zio-schema/js/src/main/scala/zio/schema/StandardType.scala +++ b/zio-schema/js/src/main/scala/zio/schema/StandardType.scala @@ -51,6 +51,7 @@ object StandardType { final val OFFSET_DATE_TIME = "offsetDateTime" final val ZONED_DATE_TIME = "zonedDateTime" final val UUID = "uuid" + final val CURRENCY = "currency" } def fromString(tag: String): Option[StandardType[_]] = @@ -85,6 +86,7 @@ object StandardType { case Tags.OFFSET_DATE_TIME => Some(OffsetDateTimeType) case Tags.ZONED_DATE_TIME => Some(ZonedDateTimeType) case Tags.UUID => Some(UUIDType) + case Tags.CURRENCY => Some(CurrencyType) } def apply[A](implicit standardType: StandardType[A]): StandardType[A] = standardType @@ -163,6 +165,13 @@ object StandardType { override def defaultValue: Either[String, java.util.UUID] = Left("UUID generation not available in ScalaJS") } + implicit object CurrencyType extends StandardType[java.util.Currency] { + override def tag: String = Tags.CURRENCY + override def defaultValue: Either[String, java.util.Currency] = Left("Currency generation not available in ScalaJS") + override def compare(x: java.util.Currency, y: java.util.Currency): Int = + x.getCurrencyCode.compareTo(y.getCurrencyCode) + } + implicit object BigDecimalType extends StandardType[java.math.BigDecimal] { override def tag: String = Tags.BIG_DECIMAL override def compare(x: java.math.BigDecimal, y: java.math.BigDecimal): Int = x.compareTo(y) diff --git a/zio-schema/jvm/src/main/scala/zio/schema/StandardType.scala b/zio-schema/jvm/src/main/scala/zio/schema/StandardType.scala index b3f39fc43..19721881f 100644 --- a/zio-schema/jvm/src/main/scala/zio/schema/StandardType.scala +++ b/zio-schema/jvm/src/main/scala/zio/schema/StandardType.scala @@ -51,6 +51,7 @@ object StandardType { final val OFFSET_DATE_TIME = "offsetDateTime" final val ZONED_DATE_TIME = "zonedDateTime" final val UUID = "uuid" + final val CURRENCY = "currency" } def fromString(tag: String): Option[StandardType[_]] = @@ -85,6 +86,7 @@ object StandardType { case Tags.OFFSET_DATE_TIME => Some(OffsetDateTimeType) case Tags.ZONED_DATE_TIME => Some(ZonedDateTimeType) case Tags.UUID => Some(UUIDType) + case Tags.CURRENCY => Some(CurrencyType) } def apply[A](implicit standardType: StandardType[A]): StandardType[A] = standardType @@ -163,6 +165,30 @@ object StandardType { override def defaultValue: Either[String, java.util.UUID] = Right(java.util.UUID.randomUUID()) } + implicit object CurrencyType extends StandardType[java.util.Currency] { + override def tag: String = Tags.CURRENCY + override def defaultValue: Either[String, java.util.Currency] = + try { + val currency = java.util.Currency.getInstance(java.util.Locale.getDefault()) + if (currency == null) { + Left( + "Could not get default currency. In most cases, this is because a currency could not be determined from the provided locale (e.g. when the locale is set to Antarctica). Please inspect the default locale to ensure that it is properly set and try again." + ) + } else { + Right(currency) + } + + } catch { + case ex: Throwable => + Left( + s"Could not get default currency. In most cases, this is because the default locale was not set on the JVM. If this is the case, running java.util.Locale.setDefault() before getting the default currency should fix this. Error message: ${ex.getMessage}." + ) + } + + override def compare(x: java.util.Currency, y: java.util.Currency): Int = + x.getCurrencyCode.compareTo(y.getCurrencyCode) + } + implicit object BigDecimalType extends StandardType[java.math.BigDecimal] { override def tag: String = Tags.BIG_DECIMAL override def compare(x: java.math.BigDecimal, y: java.math.BigDecimal): Int = x.compareTo(y) @@ -176,7 +202,7 @@ object StandardType { } //java.time specific types - implicit object DayOfWeekType extends StandardType[DayOfWeek] { + implicit object DayOfWeekType extends StandardType[java.time.DayOfWeek] { override def tag: String = Tags.DAY_OF_WEEK override def compare(x: DayOfWeek, y: DayOfWeek): Int = x.getValue.compareTo(y.getValue) override def defaultValue: Either[String, DayOfWeek] = diff --git a/zio-schema/native/src/main/scala/zio/schema/StandardType.scala b/zio-schema/native/src/main/scala/zio/schema/StandardType.scala index b65a46ee5..15dfe639f 100644 --- a/zio-schema/native/src/main/scala/zio/schema/StandardType.scala +++ b/zio-schema/native/src/main/scala/zio/schema/StandardType.scala @@ -51,6 +51,7 @@ object StandardType { final val OFFSET_DATE_TIME = "offsetDateTime" final val ZONED_DATE_TIME = "zonedDateTime" final val UUID = "uuid" + final val CURRENCY = "currency" } def fromString(tag: String): Option[StandardType[_]] = @@ -85,6 +86,7 @@ object StandardType { case Tags.OFFSET_DATE_TIME => Some(OffsetDateTimeType) case Tags.ZONED_DATE_TIME => Some(ZonedDateTimeType) case Tags.UUID => Some(UUIDType) + case Tags.CURRENCY => Some(CurrencyType) } def apply[A](implicit standardType: StandardType[A]): StandardType[A] = standardType @@ -163,6 +165,17 @@ object StandardType { override def defaultValue: Either[String, java.util.UUID] = Right(java.util.UUID.randomUUID()) } + implicit object CurrencyType extends StandardType[java.util.Currency] { + + override def tag: String = Tags.CURRENCY + + override def defaultValue: Either[String, java.util.Currency] = + Left("Currency generation not available in Scala Native") + + override def compare(x: java.util.Currency, y: java.util.Currency): Int = + x.getCurrencyCode.compareTo(y.getCurrencyCode) + } + implicit object BigDecimalType extends StandardType[java.math.BigDecimal] { override def tag: String = Tags.BIG_DECIMAL override def compare(x: java.math.BigDecimal, y: java.math.BigDecimal): Int = x.compareTo(y) diff --git a/zio-schema/shared/src/main/scala/zio/schema/Differ.scala b/zio-schema/shared/src/main/scala/zio/schema/Differ.scala index c7bc8b27e..5796e53aa 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/Differ.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/Differ.scala @@ -20,7 +20,7 @@ import java.time.{ ZoneOffset, ZonedDateTime => JZonedDateTime } -import java.util.UUID +import java.util.{ Currency, UUID } import scala.annotation.nowarn import scala.collection.immutable.ListMap @@ -255,8 +255,16 @@ object Differ { case Schema.Primitive(StandardType.OffsetDateTimeType, _) => offsetDateTime case Schema.Primitive(StandardType.ZonedDateTimeType, _) => zonedDateTime case Schema.Primitive(StandardType.ZoneOffsetType, _) => zoneOffset - case Schema.Tuple2(leftSchema, rightSchema, _) => fromSchema(leftSchema) <*> fromSchema(rightSchema) - case Schema.Optional(schema, _) => fromSchema(schema).optional + case Schema.Primitive(StandardType.CurrencyType, _) => + string.transformOrFail[Currency]( + (currency: Currency) => Right(currency.toString), + (s: String) => + try { + Right(Currency.getInstance(s)) + } catch { case e: Throwable => Left(s"$s is not a valid Currency: ${e.getMessage}") } + ) + case Schema.Tuple2(leftSchema, rightSchema, _) => fromSchema(leftSchema) <*> fromSchema(rightSchema) + case Schema.Optional(schema, _) => fromSchema(schema).optional case Schema.Sequence(schema, g, f, _, _) => fromSchema(schema).chunk.transform(f, g) case Schema.Set(s, _) => set(s) @@ -523,6 +531,13 @@ object Differ { } } + val currency: Differ[Currency] = + (thisValue: Currency, thatValue: Currency) => + if (thisValue == thatValue) + Patch.identical + else + Patch.Currency(thatValue) + def tuple[A, B](left: Differ[A], right: Differ[B]): Differ[(A, B)] = (thisValue: (A, B), thatValue: (A, B)) => (thisValue, thatValue) match { diff --git a/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala b/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala index f06358f0f..75289f3e0 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala @@ -239,6 +239,7 @@ object DynamicValue { lazy val typeId: TypeId = TypeId.parse("zio.schema.DynamicValue") + //TODO: Refactor case set so that adding a new type to StandardType without updating the below list will trigger a compile error lazy val schema: Schema[DynamicValue] = Schema.EnumN( typeId, @@ -284,6 +285,7 @@ object DynamicValue { .:+:(primitiveOffsetDateTimeCase) .:+:(primitiveZonedDateTimeCase) .:+:(primitiveUUIDCase) + .:+:(primitiveCurrencyCase) .:+:(singletonCase) ) @@ -922,4 +924,16 @@ object DynamicValue { } ) + private val primitiveCurrencyCase: Schema.Case[DynamicValue, DynamicValue.Primitive[java.util.Currency]] = + Schema.Case( + "Currency", + Schema.primitive[java.util.Currency].transform(currency => DynamicValue.Primitive(currency, StandardType[java.util.Currency]), _.value), { + case dv @ DynamicValue.Primitive(_: java.util.Currency, _) => dv.asInstanceOf[DynamicValue.Primitive[java.util.Currency]] + case _ => throw new IllegalArgumentException + }, + (dv: DynamicValue.Primitive[java.util.Currency]) => dv.asInstanceOf[DynamicValue], { + case DynamicValue.Primitive(_: java.util.Currency, _) => true + case _ => false + } + ) } diff --git a/zio-schema/shared/src/main/scala/zio/schema/Patch.scala b/zio-schema/shared/src/main/scala/zio/schema/Patch.scala index 5dcee2e3c..42c261ff8 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/Patch.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/Patch.scala @@ -206,6 +206,13 @@ object Patch { override def invert: Patch[JZonedDateTime] = ZonedDateTime(localDateTimeDiff.invert, zoneIdDiff.invert) } + final case class Currency(currencyCodeDiff: java.util.Currency) extends Patch[java.util.Currency] { + override def patch(input: java.util.Currency): scala.Either[String, java.util.Currency] = + Right(input) + + override def invert: Patch[java.util.Currency] = Currency(currencyCodeDiff) + } + final case class Tuple[A, B](leftDifference: Patch[A], rightDifference: Patch[B]) extends Patch[(A, B)] { override def isIdentical: Boolean = leftDifference.isIdentical && rightDifference.isIdentical