From e58a4011c3aeea7a6bd9c0ce611ae8d7e0d086cc Mon Sep 17 00:00:00 2001 From: Dan Harris <1327726+thinkharderdev@users.noreply.github.com> Date: Thu, 18 Mar 2021 13:45:47 -0400 Subject: [PATCH] Fixes #7: Implement JsonCodec (#28) * Initial commit of wip from https://github.com/zio/zio-web/pull/90. #7 > Co-authored-by: Brandon Brown Co-authored-by: Jason Pickens * WIP trying to get build working * wip * Fixes #7: Implement JSON Codec * Formatting * More unit tests * unit tests * Add non-streaming methods to JsonCodec * More test fixes * Transform failure tests * Update to published version of zio-json * Scala 2.12 support * Work around jdk 8 duration parsing bug Co-authored-by: Brandon Brown Co-authored-by: thinkharder --- build.sbt | 1 + .../scala/zio/schema/codec/JsonCodec.scala | 235 ++++++++++ .../test/scala/zio/schema/JavaTimeGen.scala | 137 ++++++ .../src/test/scala/zio/schema/SchemaGen.scala | 272 +++++++++++ .../scala/zio/schema/StandardTypeGen.scala | 72 +++ .../zio/schema/codec/JsonCodecSpec.scala | 431 ++++++++++++++++++ project/BuildHelper.scala | 1 + 7 files changed, 1149 insertions(+) create mode 100644 core/src/main/scala/zio/schema/codec/JsonCodec.scala create mode 100644 core/src/test/scala/zio/schema/JavaTimeGen.scala create mode 100644 core/src/test/scala/zio/schema/SchemaGen.scala create mode 100644 core/src/test/scala/zio/schema/StandardTypeGen.scala create mode 100644 core/src/test/scala/zio/schema/codec/JsonCodecSpec.scala diff --git a/build.sbt b/build.sbt index 6b72bb6e1..a5d8876ba 100644 --- a/build.sbt +++ b/build.sbt @@ -58,6 +58,7 @@ lazy val core = project libraryDependencies ++= Seq( "dev.zio" %% "zio" % zioVersion, "dev.zio" %% "zio-streams" % zioVersion, + "dev.zio" %% "zio-json" % zioJsonVersion, "com.propensive" %% "magnolia" % magnoliaVersion, "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided ) diff --git a/core/src/main/scala/zio/schema/codec/JsonCodec.scala b/core/src/main/scala/zio/schema/codec/JsonCodec.scala new file mode 100644 index 000000000..1d156d19c --- /dev/null +++ b/core/src/main/scala/zio/schema/codec/JsonCodec.scala @@ -0,0 +1,235 @@ +package zio.schema.codec + +import java.nio.CharBuffer +import java.nio.charset.StandardCharsets + +import zio.json.JsonCodec._ +import zio.json.JsonDecoder.{ JsonError, UnsafeJson } +import zio.json.internal.{ Lexer, RetractReader, Write } +import zio.json.{ JsonCodec => ZJsonCodec, JsonDecoder, JsonEncoder, JsonFieldDecoder, JsonFieldEncoder } +import zio.schema.{ StandardType, _ } +import zio.stream.ZTransducer +import zio.{ Chunk, ChunkBuilder, ZIO } + +object JsonCodec extends Codec { + + override def encoder[A](schema: Schema[A]): ZTransducer[Any, Nothing, A, Byte] = + ZTransducer.fromPush( + (opt: Option[Chunk[A]]) => + ZIO.succeed(opt.map(values => values.flatMap(Encoder.encode(schema, _))).getOrElse(Chunk.empty)) + ) + + override def decoder[A](schema: Schema[A]): ZTransducer[Any, String, Byte, A] = schema match { + case Schema.Primitive(StandardType.UnitType) => + ZTransducer.fromPush(_ => ZIO.succeed(Chunk.unit)) + case _ => + ZTransducer.utfDecode >>> ZTransducer.fromFunctionM( + (s: String) => ZIO.fromEither(Decoder.decode(schema, s)) + ) + } + + override def encode[A](schema: Schema[A]): A => Chunk[Byte] = Encoder.encode(schema, _) + + override def decode[A](schema: Schema[A]): Chunk[Byte] => Either[String, A] = + (chunk: Chunk[Byte]) => Decoder.decode(schema, new String(chunk.toArray, Encoder.CHARSET)) + + object Codecs { + protected[codec] val unitEncoder: JsonEncoder[Unit] = new JsonEncoder[Unit] { + override def unsafeEncode(a: Unit, indent: Option[Int], out: Write): Unit = () + override def isNothing(a: Unit): Boolean = true + } + protected[codec] val unitDecoder: JsonDecoder[Unit] = + (_: List[JsonDecoder.JsonError], _: RetractReader) => () + + protected[codec] val unitCodec: ZJsonCodec[Unit] = ZJsonCodec(unitEncoder, unitDecoder) + + protected[codec] def failDecoder[A](message: String): JsonDecoder[A] = + (trace: List[JsonDecoder.JsonError], _: RetractReader) => throw UnsafeJson(JsonError.Message(message) :: trace) + + private[codec] def primitiveCodec[A](standardType: StandardType[A]): ZJsonCodec[A] = + standardType match { + case StandardType.UnitType => unitCodec + case StandardType.StringType => ZJsonCodec.string + case StandardType.BoolType => ZJsonCodec.boolean + case StandardType.ShortType => ZJsonCodec.short + case StandardType.IntType => ZJsonCodec.int + case StandardType.LongType => ZJsonCodec.long + case StandardType.FloatType => ZJsonCodec.float + case StandardType.DoubleType => ZJsonCodec.double + case StandardType.BinaryType => ZJsonCodec.chunk(ZJsonCodec.byte) + case StandardType.CharType => ZJsonCodec.char + case StandardType.DayOfWeekType => ZJsonCodec.dayOfWeek // ZJsonCodec[java.time.DayOfWeek] + case StandardType.Duration(_) => ZJsonCodec.duration //ZJsonCodec[java.time.Duration] + case StandardType.Instant(_) => ZJsonCodec.instant //ZJsonCodec[java.time.Instant] + case StandardType.LocalDate(_) => ZJsonCodec.localDate //ZJsonCodec[java.time.LocalDate] + case StandardType.LocalDateTime(_) => ZJsonCodec.localDateTime //ZJsonCodec[java.time.LocalDateTime] + case StandardType.LocalTime(_) => ZJsonCodec.localTime //ZJsonCodec[java.time.LocalTime] + case StandardType.Month => ZJsonCodec.month //ZJsonCodec[java.time.Month] + case StandardType.MonthDay => ZJsonCodec.monthDay //ZJsonCodec[java.time.MonthDay] + case StandardType.OffsetDateTime(_) => ZJsonCodec.offsetDateTime //ZJsonCodec[java.time.OffsetDateTime] + case StandardType.OffsetTime(_) => ZJsonCodec.offsetTime //ZJsonCodec[java.time.OffsetTime] + case StandardType.Period => ZJsonCodec.period //ZJsonCodec[java.time.Period] + case StandardType.Year => ZJsonCodec.year //ZJsonCodec[java.time.Year] + case StandardType.YearMonth => ZJsonCodec.yearMonth //ZJsonCodec[java.time.YearMonth] + case StandardType.ZonedDateTime(_) => ZJsonCodec.zonedDateTime //ZJsonCodec[java.time.ZonedDateTime] + case StandardType.ZoneId => ZJsonCodec.zoneId //ZJsonCodec[java.time.ZoneId] + case StandardType.ZoneOffset => ZJsonCodec.zoneOffset //ZJsonCodec[java.time.ZoneOffset] + } + } + + object Encoder { + import Codecs._ + import JsonEncoder.{ bump, pad } + + private[codec] val CHARSET = StandardCharsets.UTF_8 + + final def encode[A](schema: Schema[A], value: A): Chunk[Byte] = + charSequenceToByteChunk(schemaEncoder(schema, value).encodeJson(value, None)) + + private[codec] def charSequenceToByteChunk(chars: CharSequence): Chunk[Byte] = { + val bytes = CHARSET.newEncoder().encode(CharBuffer.wrap(chars)) + Chunk.fromByteBuffer(bytes) + } + + private def schemaEncoder[A](schema: Schema[A], value: A): JsonEncoder[A] = schema match { + case Schema.Primitive(standardType) => primitiveCodec(standardType) + case Schema.Sequence(schema) => JsonEncoder.chunk(schemaEncoder(schema, value)) + case Schema.Transform(c, _, g) => transformEncoder(c, value, g) + case s @ Schema.Tuple(_, _) => tupleEncoder(s, value) + case s @ Schema.Optional(_) => optionEncoder(s, value) + case Schema.Fail(_) => unitEncoder.contramap(_ => ()) + case Schema.Record(structure) => recordEncoder(structure, value) + case Schema.Enumeration(structure) => enumEncoder(structure, value) + } + + private def tupleEncoder[A, B](schema: Schema.Tuple[A, B], value: (A, B)): JsonEncoder[(A, B)] = + schemaEncoder(schema.left, value._1).both(schemaEncoder(schema.right, value._2)) + + private def optionEncoder[A](schema: Schema.Optional[A], value: Option[A]): JsonEncoder[Option[A]] = value match { + case Some(v) => JsonEncoder.option(schemaEncoder(schema.codec, v)) + case None => + (_: Option[A], _: Option[Int], out: Write) => out.write("null") + } + + private def transformEncoder[A, B](schema: Schema[A], value: B, g: B => Either[String, A]): JsonEncoder[B] = { + (_: B, indent: Option[Int], out: Write) => + g(value) match { + case Left(_) => () + case Right(a) => schemaEncoder(schema, a).unsafeEncode(a, indent, out) + } + } + + private def recordEncoder(structure: Map[String, Schema[_]], value: Map[String, _]): JsonEncoder[Map[String, _]] = { + (_: Map[String, _], indent: Option[Int], out: Write) => + { + if (structure.isEmpty) { + out.write("{}") + } else { + out.write('{') + val indent_ = bump(indent) + pad(indent_, out) + var first = true + structure.foreach { + case (k, a) => + val enc = schemaEncoder(a.asInstanceOf[Schema[Any]], value(k)) + if (first) + first = false + else { + out.write(',') + if (indent.isDefined) + JsonEncoder.pad(indent_, out) + } + + string.unsafeEncode(JsonFieldEncoder.string.unsafeEncodeField(k), indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + enc.unsafeEncode(value(k), indent_, out) + } + pad(indent, out) + out.write('}') + } + + } + } + + private def enumEncoder(structure: Map[String, Schema[_]], value: Map[String, _]): JsonEncoder[Map[String, _]] = { + (a: Map[String, _], indent: Option[Int], out: Write) => + { + if (structure.isEmpty) { + out.write("{}") + } else { + out.write('{') + val indent_ = bump(indent) + pad(indent_, out) + var first = true + val (k, v) = a.toSeq.head + val enc = schemaEncoder(structure(k).asInstanceOf[Schema[Any]], value(k)) + if (first) + first = false + else { + out.write(',') + if (indent.isDefined) + pad(indent_, out) + } + + string.unsafeEncode(JsonFieldEncoder.string.unsafeEncodeField(k), indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + enc.unsafeEncode(v, indent_, out) + pad(indent, out) + out.write('}') + } + } + } + } + + object Decoder { + import Codecs._ + final def decode[A](schema: Schema[A], json: String): Either[String, A] = + schemaDecoder(schema).decodeJson(json) + + private def schemaDecoder[A](schema: Schema[A]): JsonDecoder[A] = schema match { + case Schema.Primitive(standardType) => primitiveCodec(standardType) + case Schema.Optional(codec) => JsonDecoder.option(schemaDecoder(codec)) + case Schema.Tuple(left, right) => JsonDecoder.tuple2(schemaDecoder(left), schemaDecoder(right)) + case Schema.Transform(codec, f, _) => schemaDecoder(codec).mapOrFail(f) + case Schema.Sequence(codec) => JsonDecoder.chunk(schemaDecoder(codec)) + case Schema.Fail(message) => failDecoder(message) + case Schema.Record(structure) => recordDecoder(structure) + case Schema.Enumeration(structure) => enumDecoder(structure) + } + + private def recordDecoder(structure: Map[String, Schema[_]]): JsonDecoder[Map[String, Any]] = { + (trace: List[JsonError], in: RetractReader) => + { + val builder: ChunkBuilder[(String, Any)] = zio.ChunkBuilder.make[(String, Any)](structure.size) + Lexer.char(trace, in, '{') + if (Lexer.firstField(trace, in)) + do { + val field = Lexer.string(trace, in).toString + val trace_ = JsonError.ObjectAccess(field) :: trace + Lexer.char(trace_, in, ':') + val value = schemaDecoder(structure(field)).unsafeDecode(trace_, in) + builder += ((JsonFieldDecoder.string.unsafeDecodeField(trace_, field), value)) + } while (Lexer.nextField(trace, in)) + builder.result().toMap + } + } + + private def enumDecoder(structure: Map[String, Schema[_]]): JsonDecoder[Map[String, Any]] = { + (trace: List[JsonError], in: RetractReader) => + { + val builder: ChunkBuilder[(String, Any)] = zio.ChunkBuilder.make[(String, Any)](structure.size) + Lexer.char(trace, in, '{') + if (Lexer.firstField(trace, in)) { + val field = Lexer.string(trace, in).toString + val trace_ = JsonError.ObjectAccess(field) :: trace + Lexer.char(trace_, in, ':') + val value = schemaDecoder(structure(field)).unsafeDecode(trace_, in) + builder += ((JsonFieldDecoder.string.unsafeDecodeField(trace_, field), value)) + } + builder.result().toMap + } + } + } +} diff --git a/core/src/test/scala/zio/schema/JavaTimeGen.scala b/core/src/test/scala/zio/schema/JavaTimeGen.scala new file mode 100644 index 000000000..47590ca4a --- /dev/null +++ b/core/src/test/scala/zio/schema/JavaTimeGen.scala @@ -0,0 +1,137 @@ +package zio.schema + +import java.time._ +import java.time.temporal.ChronoField + +import zio.random.Random +import zio.test.Gen + +object JavaTimeGen { + + val anyDayOfWeek: Gen[Random, DayOfWeek] = Gen.oneOf( + Gen.const(DayOfWeek.MONDAY), + Gen.const(DayOfWeek.TUESDAY), + Gen.const(DayOfWeek.WEDNESDAY), + Gen.const(DayOfWeek.THURSDAY), + Gen.const(DayOfWeek.FRIDAY), + Gen.const(DayOfWeek.SATURDAY), + Gen.const(DayOfWeek.SUNDAY) + ) + + val anyMonth: Gen[Random, Month] = Gen.oneOf( + Gen.const(Month.JANUARY), + Gen.const(Month.FEBRUARY), + Gen.const(Month.MARCH), + Gen.const(Month.APRIL), + Gen.const(Month.MAY), + Gen.const(Month.JUNE), + Gen.const(Month.JULY), + Gen.const(Month.AUGUST), + Gen.const(Month.SEPTEMBER), + Gen.const(Month.OCTOBER), + Gen.const(Month.NOVEMBER), + Gen.const(Month.DECEMBER) + ) + + val anyNanoOfDay: Gen[Random, Long] = chronoFieldValue(ChronoField.NANO_OF_DAY) + + val anyEpochDay: Gen[Random, Long] = chronoFieldValue(ChronoField.EPOCH_DAY) + + val anyMonthOfYear: Gen[Random, Int] = chronoFieldValue(ChronoField.MONTH_OF_YEAR).map(_.toInt) + + val anyMonthDay: Gen[Random, MonthDay] = + for { + month <- anyMonth + dayOfMonth <- Gen.int(1, month.maxLength) + } yield MonthDay.of(month, dayOfMonth) + + //Needs to be an ISO-8601 year between 0000 and 9999 + val anyIntYear: Gen[Random, Int] = Gen.int(0, 9999) + + val anyYear: Gen[Random, Year] = anyIntYear.map(Year.of) + + val anyYearMonth: Gen[Random, YearMonth] = + anyIntYear.zipWith(anyMonthOfYear) { (year, month) => + YearMonth.of(year, month) + } + + private def chronoFieldValue(chronoField: ChronoField) = { + val range = chronoField.range + Gen.long(range.getMinimum, range.getMaximum) + } + + //FIXME There is a bug in JDK Duration parsing that caused issues in zio-json (https://github.com/zio/zio-json/issues/214). + // Do not generate Durations with - seconds.Once that is addressed can remove filter condition + val anyDuration: Gen[Random, Duration] = Gen.anyLong + .zipWith(Gen.long(0, 999999999L)) { (seconds, nanos) => + Duration.ofSeconds(seconds, nanos) + } + .filter(_.getSeconds > 0) + + val anyPeriod: Gen[Random, Period] = + for { + years <- Gen.anyInt + months <- Gen.anyInt + days <- Gen.anyInt + } yield Period.of(years, months, days) + + val anyInstant: Gen[Random, Instant] = Gen + .long(Instant.MIN.getEpochSecond, Instant.MAX.getEpochSecond) + .zipWith(Gen.int(Instant.MIN.getNano, Instant.MAX.getNano)) { (seconds, nanos) => + Instant.ofEpochSecond(seconds, nanos.toLong) + } + + val anyLocalDate: Gen[Random, LocalDate] = anyEpochDay.map(LocalDate.ofEpochDay) + + val anyLocalTime: Gen[Random, LocalTime] = anyNanoOfDay.map(LocalTime.ofNanoOfDay) + + val anyLocalDateTime: Gen[Random, LocalDateTime] = anyLocalDate.zipWith(anyLocalTime) { (date, time) => + LocalDateTime.of(date, time) + } + + val anyZoneOffset: Gen[Random, ZoneOffset] = + Gen.int(ZoneOffset.MIN.getTotalSeconds, ZoneOffset.MAX.getTotalSeconds).map(ZoneOffset.ofTotalSeconds) + + // This uses ZoneRulesProvider which has an effectful static initializer. +// private val regionZoneIds = +// ZIO.succeed(ZoneId.getAvailableZoneIds.asScala.toSet.map(ZoneId.of)) +// +// private val zoneOffsets = +// (ZoneOffset.MIN.getTotalSeconds to ZoneOffset.MAX.getTotalSeconds).map(ZoneOffset.ofTotalSeconds) + +// private val zoneIds = regionZoneIds.map(_.toList ++ zoneOffsets) + + // FIXME: Shuffle is really slow. + //private val zoneIds = + // for { + // ids <- regionZoneIds + // all = ids ++ zoneOffsets + // random <- ZIO.service[Random.Service] + // shuffled <- random.shuffle(all.toList) + // } yield shuffled + + //FIXME Sampling causes some sort of pathological performance issue. + val anyZoneId: Gen[Random, ZoneId] = Gen.const(ZoneId.systemDefault()) +// Gen(ZStream.fromIterableM(zoneIds).map { +// case offset: ZoneOffset => Sample.noShrink(offset) +// // FIXME: This is really slow even when it isn't shrinking. +// //Sample.shrinkIntegral(ZoneOffset.UTC.getTotalSeconds)(offset.getTotalSeconds).map { seconds => +// // ZoneOffset.ofTotalSeconds(seconds) +// //} +// case zone => Sample.noShrink(zone) +// }) + + // TODO: This needs to be double checked. I have encountered problems generating these in the past. + // See https://github.com/BotTech/scala-hedgehog-spines/blob/master/core/src/main/scala/com/lightbend/hedgehog/generators/time/TimeGenerators.scala + val anyZonedDateTime: Gen[Random, ZonedDateTime] = anyLocalDateTime.zipWith(anyZoneId) { (dateTime, zone) => + ZonedDateTime.of(dateTime, zone) + } + + val anyOffsetTime: Gen[Random, OffsetTime] = anyLocalTime.zipWith(anyZoneOffset) { (time, offset) => + OffsetTime.of(time, offset) + } + + val anyOffsetDateTime: Gen[Random, OffsetDateTime] = anyLocalDateTime.zipWith(anyZoneOffset) { (dateTime, offset) => + OffsetDateTime.of(dateTime, offset) + } +} diff --git a/core/src/test/scala/zio/schema/SchemaGen.scala b/core/src/test/scala/zio/schema/SchemaGen.scala new file mode 100644 index 000000000..8cea41dd7 --- /dev/null +++ b/core/src/test/scala/zio/schema/SchemaGen.scala @@ -0,0 +1,272 @@ +package zio.schema + +import zio.Chunk +import zio.random.Random +import zio.test.{ Gen, Sized } + +object SchemaGen { + + val anyPrimitive: Gen[Random, Schema.Primitive[_]] = + StandardTypeGen.anyStandardType.map(Schema.Primitive(_)) + + type PrimitiveAndGen[A] = (Schema.Primitive[A], Gen[Random with Sized, A]) + + val anyPrimitiveAndGen: Gen[Random, PrimitiveAndGen[_]] = + StandardTypeGen.anyStandardTypeAndGen.map { + case (standardType, gen) => Schema.Primitive(standardType) -> gen + } + + type PrimitiveAndValue[A] = (Schema.Primitive[A], A) + + val anyPrimitiveAndValue: Gen[Random with Sized, PrimitiveAndValue[_]] = + for { + (schema, gen) <- anyPrimitiveAndGen + value <- gen + } yield schema -> value + + val anyOptional: Gen[Random with Sized, Schema.Optional[_]] = + anySchema.map(Schema.Optional(_)) + + type OptionalAndGen[A] = (Schema.Optional[A], Gen[Random with Sized, Option[A]]) + + val anyOptionalAndGen: Gen[Random with Sized, OptionalAndGen[_]] = + anyPrimitiveAndGen.map { + case (schema, gen) => Schema.Optional(schema) -> Gen.option(gen) + } + + type OptionalAndValue[A] = (Schema.Optional[A], Option[A]) + + val anyOptionalAndValue: Gen[Random with Sized, OptionalAndValue[_]] = + for { + (schema, gen) <- anyOptionalAndGen + value <- gen + } yield schema -> value + + val anyTuple: Gen[Random with Sized, Schema.Tuple[_, _]] = + anySchema.zipWith(anySchema) { (a, b) => + Schema.Tuple(a, b) + } + + type TupleAndGen[A, B] = (Schema.Tuple[A, B], Gen[Random with Sized, (A, B)]) + + val anyTupleAndGen: Gen[Random with Sized, TupleAndGen[_, _]] = + for { + (schemaA, genA) <- anyPrimitiveAndGen + (schemaB, genB) <- anyPrimitiveAndGen + } yield Schema.Tuple(schemaA, schemaB) -> genA.zip(genB) + + type TupleAndValue[A, B] = (Schema.Tuple[A, B], (A, B)) + + val anyTupleAndValue: Gen[Random with Sized, TupleAndValue[_, _]] = + for { + (schema, gen) <- anyTupleAndGen + (valueA, valueB) <- gen + } yield schema -> ((valueA, valueB)) + + val anySequence: Gen[Random with Sized, Schema.Sequence[_]] = + anySchema.map(Schema.Sequence(_)) + + type SequenceAndGen[A] = (Schema.Sequence[A], Gen[Random with Sized, Chunk[A]]) + + val anySequenceAndGen: Gen[Random with Sized, SequenceAndGen[_]] = + anyPrimitiveAndGen.map { + case (schema, gen) => + Schema.Sequence(schema) -> Gen.chunkOf(gen) + } + + type SequenceAndValue[A] = (Schema.Sequence[A], Chunk[A]) + + val anySequenceAndValue: Gen[Random with Sized, SequenceAndValue[_]] = + for { + (schema, gen) <- anySequenceAndGen + value <- gen + } yield schema -> value + + val anyEnumeration: Gen[Random with Sized, Schema.Enumeration] = + Gen.mapOf(Gen.anyString, anySchema).map(Schema.Enumeration) + + type EnumerationAndGen = (Schema.Enumeration, Gen[Random with Sized, Map[String, _]]) + + val anyEnumerationAndGen: Gen[Random with Sized, EnumerationAndGen] = + for { + keyToSchemaAndGen <- Gen.mapOf(Gen.anyString, anyPrimitiveAndGen) + } yield { + val structure = keyToSchemaAndGen.toSeq.map { + case (k, (schema, _)) => k -> schema + }.toMap + val keyValueGenerators = keyToSchemaAndGen.map { + case (key, (_, gen)) => Gen.const(key).zip(gen) + }.toSeq + val gen = Gen.oneOf(keyValueGenerators: _*).map(Map(_)) + Schema.Enumeration(structure) -> gen + } + + type EnumerationAndValue = (Schema.Enumeration, Map[String, _]) + + val anyEnumerationAndValue: Gen[Random with Sized, EnumerationAndValue] = + for { + (schema, gen) <- anyEnumerationAndGen + value <- gen + } yield schema -> value + + val anyRecord: Gen[Random with Sized, Schema.Record] = + Gen.mapOf(Gen.anyString, anySchema).map(Schema.Record) + + type RecordAndGen = (Schema.Record, Gen[Random with Sized, Map[String, _]]) + + val anyRecordAndGen: Gen[Random with Sized, RecordAndGen] = + for { + keyToSchemaAndGen <- Gen.mapOf(Gen.anyString, anyPrimitiveAndGen) + } yield { + val structure = keyToSchemaAndGen.toSeq.map { + case (s, (schema, _)) => s -> schema + }.toMap + val keyValueGenerators = keyToSchemaAndGen.map { + case (key, (_, gen)) => Gen.const(key).zip(gen) + }.toSeq + val gen = keyValueGenerators.foldLeft[Gen[Random with Sized, Map[String, _]]](Gen.const(Map.empty)) { + (acc, gen) => + for { + map <- acc + keyValue <- gen + } yield map + keyValue + } + Schema.Record(structure) -> gen + } + + type RecordAndValue = (Schema.Record, Map[String, _]) + + val anyRecordAndValue: Gen[Random with Sized, RecordAndValue] = + for { + (schema, gen) <- anyRecordAndGen + value <- gen + } yield schema -> value + + val anyRecordOfRecordsAndValue: Gen[Random with Sized, RecordAndValue] = + for { + (schema1, gen1) <- anyRecordAndGen + (schema2, gen2) <- anyRecordAndGen + (schema3, gen3) <- anyRecordAndGen + (key1, value1) <- Gen.anyString.zip(gen1) + (key2, value2) <- Gen.anyString.zip(gen2) + (key3, value3) <- Gen.anyString.zip(gen3) + } yield Schema.Record(Map(key1 -> schema1, key2 -> schema2, key3 -> schema3)) -> Map( + (key1, value1), + (key2, value2), + (key3, value3) + ) + + type SequenceTransform[A] = Schema.Transform[Chunk[A], List[A]] + + val anySequenceTransform: Gen[Random with Sized, SequenceTransform[_]] = { + anySequence.map(schema => transformSequence(schema)) + } + + type SequenceTransformAndGen[A] = (SequenceTransform[A], Gen[Random with Sized, List[A]]) + + val anySequenceTransformAndGen: Gen[Random with Sized, SequenceTransformAndGen[_]] = + anyPrimitiveAndGen.map { + case (schema, gen) => + transformSequence(Schema.Sequence(schema)) -> Gen.listOf(gen) + } + + // TODO: Add some random Left values. + private def transformSequence[A](schema: Schema.Sequence[A]): SequenceTransform[A] = + Schema.Transform[Chunk[A], List[A]](schema, chunk => Right(chunk.toList), list => Right(Chunk.fromIterable(list))) + + type SequenceTransformAndValue[A] = (SequenceTransform[A], List[A]) + + val anySequenceTransformAndValue: Gen[Random with Sized, SequenceTransformAndValue[_]] = + for { + (schema, gen) <- anySequenceTransformAndGen + value <- gen + } yield schema -> value + + type RecordTransform[A] = Schema.Transform[Map[String, _], A] + + val anyRecordTransform: Gen[Random with Sized, RecordTransform[_]] = { + anyRecord.map(schema => transformRecord(schema)) + } + + type RecordTransformAndGen[A] = (RecordTransform[A], Gen[Random with Sized, A]) + + // TODO: How do we generate a value of a type that we know nothing about? + val anyRecordTransformAndGen: Gen[Random with Sized, RecordTransformAndGen[_]] = + Gen.empty +// anyRecordAndGen.map { +// case (schema, gen) => transformRecord(schema) -> gen +// } + + // TODO: Dynamically generate a case class. + def transformRecord[A](schema: Schema.Record): RecordTransform[A] = + Schema.Transform[Map[String, _], A](schema, _ => Left("Not implemented."), _ => Left("Not implemented.")) + + type RecordTransformAndValue[A] = (RecordTransform[A], A) + + val anyRecordTransformAndValue: Gen[Random with Sized, RecordTransformAndValue[_]] = + for { + (schema, gen) <- anyRecordTransformAndGen + value <- gen + } yield schema -> value + + type EnumerationTransform[A] = Schema.Transform[Map[String, _], A] + + val anyEnumerationTransform: Gen[Random with Sized, EnumerationTransform[_]] = { + anyEnumeration.map(schema => transformEnumeration(schema)) + } + + type EnumerationTransformAndGen[A] = (EnumerationTransform[A], Gen[Random with Sized, A]) + + // TODO: How do we generate a value of a type that we know nothing about? + val anyEnumerationTransformAndGen: Gen[Random with Sized, EnumerationTransformAndGen[_]] = + Gen.empty +// anyEnumerationAndGen.map { +// case (schema, gen) => transformEnumeration(schema) -> gen +// } + + // TODO: Dynamically generate a sealed trait and case/value classes. + def transformEnumeration[A](schema: Schema.Enumeration): EnumerationTransform[_] = + Schema.Transform[Map[String, _], A](schema, _ => Left("Not implemented."), _ => Left("Not implemented.")) + + type EnumerationTransformAndValue[A] = (EnumerationTransform[A], A) + + val anyEnumerationTransformAndValue: Gen[Random with Sized, EnumerationTransformAndValue[_]] = + for { + (schema, gen) <- anyEnumerationTransformAndGen + value <- gen + } yield schema -> value + + val anyTransform: Gen[Random with Sized, Schema.Transform[_, _]] = Gen.oneOf( + anySequenceTransform, + anyRecordTransform, + anyEnumerationTransform + ) + + type TransformAndValue[A] = (Schema.Transform[_, A], A) + + val anyTransformAndValue: Gen[Random with Sized, TransformAndValue[_]] = + Gen.oneOf[Random with Sized, TransformAndValue[_]]( + anySequenceTransformAndValue, + anyRecordTransformAndValue, + anyEnumerationTransformAndValue + ) + + type TransformAndGen[A] = (Schema.Transform[_, A], Gen[Random with Sized, A]) + + val anyTransformAndGen: Gen[Random with Sized, TransformAndGen[_]] = + Gen.oneOf[Random with Sized, TransformAndGen[_]]( + anySequenceTransformAndGen, + anyRecordTransformAndGen, + anyEnumerationTransformAndGen + ) + + lazy val anySchema: Gen[Random with Sized, Schema[_]] = Gen.oneOf( + anyPrimitive, + anyOptional, + anyTuple, + anySequence, + anyEnumeration, + anyRecord, + anyTransform + ) +} diff --git a/core/src/test/scala/zio/schema/StandardTypeGen.scala b/core/src/test/scala/zio/schema/StandardTypeGen.scala new file mode 100644 index 000000000..038bee4e6 --- /dev/null +++ b/core/src/test/scala/zio/schema/StandardTypeGen.scala @@ -0,0 +1,72 @@ +package zio.schema + +import java.time.format.DateTimeFormatter +import java.time.temporal.{ ChronoUnit } + +import zio.random.Random +import zio.test.{ Gen, Sized } + +object StandardTypeGen { + + val anyStandardType: Gen[Random, StandardType[_]] = Gen.oneOf( + Gen.const(StandardType.StringType), + Gen.const(StandardType.BoolType), + Gen.const(StandardType.ShortType), + Gen.const(StandardType.IntType), + Gen.const(StandardType.LongType), + Gen.const(StandardType.FloatType), + Gen.const(StandardType.DoubleType), + Gen.const(StandardType.BinaryType), + Gen.const(StandardType.CharType), + Gen.const(StandardType.DayOfWeekType), + Gen.const(StandardType.Duration(ChronoUnit.SECONDS)), + Gen.const(StandardType.Instant(DateTimeFormatter.ISO_DATE_TIME)), + Gen.const(StandardType.LocalDate(DateTimeFormatter.ISO_DATE)), + Gen.const(StandardType.LocalDateTime(DateTimeFormatter.ISO_LOCAL_DATE_TIME)), + Gen.const(StandardType.LocalTime(DateTimeFormatter.ISO_LOCAL_TIME)), + Gen.const(StandardType.Month), + Gen.const(StandardType.MonthDay), + Gen.const(StandardType.OffsetDateTime(DateTimeFormatter.ISO_OFFSET_DATE_TIME)), + Gen.const(StandardType.OffsetTime(DateTimeFormatter.ISO_OFFSET_TIME)), + Gen.const(StandardType.Period), + Gen.const(StandardType.Year), + Gen.const(StandardType.YearMonth), + Gen.const(StandardType.ZonedDateTime(DateTimeFormatter.ISO_ZONED_DATE_TIME)), + Gen.const(StandardType.ZoneId) + //FIXME For some reason adding this causes other unrelated tests to break. +// Gen.const(StandardType.ZoneOffset) + ) + + type StandardTypeAndGen[A] = (StandardType[A], Gen[Random with Sized, A]) + + val anyStandardTypeAndGen: Gen[Random, StandardTypeAndGen[_]] = { + anyStandardType.map { + case typ: StandardType.StringType.type => typ -> Gen.anyString + case typ: StandardType.BoolType.type => typ -> Gen.boolean + case typ: StandardType.ShortType.type => typ -> Gen.anyShort + case typ: StandardType.IntType.type => typ -> Gen.anyInt + case typ: StandardType.LongType.type => typ -> Gen.anyLong + case typ: StandardType.FloatType.type => typ -> Gen.anyFloat + case typ: StandardType.DoubleType.type => typ -> Gen.anyDouble + case typ: StandardType.BinaryType.type => typ -> Gen.chunkOf(Gen.anyByte) + case typ: StandardType.CharType.type => typ -> Gen.anyASCIIChar + case typ: StandardType.DayOfWeekType.type => typ -> JavaTimeGen.anyDayOfWeek + case typ: StandardType.Duration => typ -> JavaTimeGen.anyDuration + case typ: StandardType.Instant => typ -> JavaTimeGen.anyInstant + case typ: StandardType.LocalDate => typ -> JavaTimeGen.anyLocalDate + case typ: StandardType.LocalDateTime => typ -> JavaTimeGen.anyLocalDateTime + case typ: StandardType.LocalTime => typ -> JavaTimeGen.anyLocalTime + case typ: StandardType.Month.type => typ -> JavaTimeGen.anyMonth + case typ: StandardType.MonthDay.type => typ -> JavaTimeGen.anyMonthDay + case typ: StandardType.OffsetDateTime => typ -> JavaTimeGen.anyOffsetDateTime + case typ: StandardType.OffsetTime => typ -> JavaTimeGen.anyOffsetTime + case typ: StandardType.Period.type => typ -> JavaTimeGen.anyPeriod + case typ: StandardType.Year.type => typ -> JavaTimeGen.anyYear + case typ: StandardType.YearMonth.type => typ -> JavaTimeGen.anyYearMonth + case typ: StandardType.ZonedDateTime => typ -> JavaTimeGen.anyZonedDateTime + case typ: StandardType.ZoneId.type => typ -> JavaTimeGen.anyZoneId + case typ: StandardType.ZoneOffset.type => typ -> JavaTimeGen.anyZoneOffset + case _ => StandardType.UnitType -> Gen.unit: StandardTypeAndGen[_] + } + } +} diff --git a/core/src/test/scala/zio/schema/codec/JsonCodecSpec.scala b/core/src/test/scala/zio/schema/codec/JsonCodecSpec.scala new file mode 100644 index 000000000..1653caff0 --- /dev/null +++ b/core/src/test/scala/zio/schema/codec/JsonCodecSpec.scala @@ -0,0 +1,431 @@ +package zio.schema.codec + +// import java.time.Year +import java.time.{ ZoneId, ZoneOffset } + +import zio.duration._ +import zio.json.JsonDecoder.JsonError +import zio.json.{ DeriveJsonEncoder, JsonEncoder } +import zio.random.Random +import zio.schema.{ JavaTimeGen, Schema, SchemaGen, StandardType } +import zio.stream.ZStream +import zio.test.Assertion._ +import zio.test.TestAspect._ +import zio.test.environment.TestEnvironment +import zio.test.{ testM, _ } +import zio.{ Chunk, ZIO } + +//TODO encode and decode specs +object JsonCodecSpec extends DefaultRunnableSpec { + + def spec: ZSpec[TestEnvironment, Any] = + suite("JsonCodec Spec")( + encoderSuite, + decoderSuite, + encoderDecoderSuite + ) @@ timeout(90.seconds) + + // TODO: Add tests for the transducer contract. + + private val encoderSuite = suite("Should correctly encode")( + suite("primitive")( + testM("unit") { + assertEncodesUnit + }, + testM("string")( + checkM(Gen.anyString)(assertEncodesString) + ), + testM("ZoneOffset") { + assertEncodesJson(Schema.Primitive(StandardType.ZoneOffset), ZoneOffset.UTC) + }, + testM("ZoneId") { + assertEncodesJson(Schema.Primitive(StandardType.ZoneId), ZoneId.systemDefault()) + } + ), + suite("optional")( + testM("of primitives") { + assertEncodesJson( + Schema.Optional(Schema.Primitive(StandardType.StringType)), + Some("value") + ) &> + assertEncodesJson( + Schema.Optional(Schema.Primitive(StandardType.StringType)), + None + ) + } + ), + suite("tuple")( + testM("of primitives") { + assertEncodesJson( + Schema.Tuple( + Schema.Primitive(StandardType.StringType), + Schema.Primitive(StandardType.IntType) + ), + ("L", 1) + ) + } + ), + suite("sequence")( + testM("of primitives") { + assertEncodesJson( + Schema.Sequence(Schema.Primitive(StandardType.StringType)), + Chunk("a", "b", "c") + ) + } + ), + suite("record")( + testM("of primitives") { + assertEncodes( + recordSchema, + Map[String, Any]("foo" -> "s", "bar" -> 1), + JsonCodec.Encoder.charSequenceToByteChunk("""{"foo":"s","bar":1}""") + ) + }, + testM("of records") { + assertEncodes( + nestedRecordSchema, + Map[String, Any]("l1" -> "s", "l2" -> Map("foo" -> "s", "bar" -> 1)), + JsonCodec.Encoder.charSequenceToByteChunk("""{"l1":"s","l2":{"foo":"s","bar":1}}""") + ) + }, + testM("case class") { + assertEncodesJson( + searchRequestSchema, + SearchRequest("query", 1, 2) + ) + } + ), + suite("enumeration")( + testM("of primitives") { + assertEncodes( + enumSchema, + Map[String, Any]("string" -> "foo"), + JsonCodec.Encoder.charSequenceToByteChunk("""{"string":"foo"}""") + ) + }, + testM("ADT") { + assertEncodes( + adtSchema, + Enumeration(StringValue("foo")), + JsonCodec.Encoder.charSequenceToByteChunk("""{"value":{"string":"foo"}}""") + ) + } + ) + ) + + private val decoderSuite = suite("Should correctly decode")( + suite("primitive")( + testM("unit") { + assertDecodesUnit + }, + suite("string")( + testM("example") { + assertDecodesString("hello") + }, + testM("any") { + checkM(Gen.anyString)(assertDecodesString) + } + ) + ), + suite("transform")( + testM("string") { + val stringSchema = Schema.Primitive(StandardType.StringType) + val transformSchema = stringSchema.transform[Int](_ => 1, _.toString) + assertDecodes(transformSchema, 1, stringify("string")) + }, + testM("failed") { + val errorMessage = "I'm sorry Dave, I can't do that" + val schema: Schema[Int] = Schema + .Primitive(StandardType.StringType) + .transformOrFail[Int](_ => Left(errorMessage), i => Right(i.toString)) + checkM(Gen.int(Int.MinValue, Int.MaxValue)) { int => + assertDecodesToError( + schema, + JsonEncoder.string.encodeJson(int.toString, None), + JsonError.Message(errorMessage) :: Nil + ) + } + } + ) + ) + + private val encoderDecoderSuite = suite("Encoding then decoding")( + testM("unit") { + assertEncodesThenDecodes(Schema.Primitive(StandardType.UnitType), ()) + }, + testM("primitive") { + checkM(SchemaGen.anyPrimitiveAndValue) { + case (schema, value) => assertEncodesThenDecodes(schema, value) + } + }, + suite("optional")( + testM("of primitive") { + checkM(SchemaGen.anyOptionalAndValue) { + case (schema, value) => assertEncodesThenDecodes(schema, value) + } + }, + testM("of tuple") { + checkM(SchemaGen.anyTupleAndValue) { + case (schema, value) => + assertEncodesThenDecodes(Schema.Optional(schema), Some(value)) &> + assertEncodesThenDecodes(Schema.Optional(schema), None) + } + }, + testM("of record") { + checkM(SchemaGen.anyRecordAndValue) { + case (schema, value) => + assertEncodesThenDecodes(Schema.Optional(schema), Some(value)) &> + assertEncodesThenDecodes(Schema.Optional(schema), None) + } + }, + testM("of enumeration") { + checkM(SchemaGen.anyEnumerationAndValue) { + case (schema, value) => + assertEncodesThenDecodes(Schema.Optional(schema), Some(value)) &> + assertEncodesThenDecodes(Schema.Optional(schema), None) + } + }, + testM("of sequence") { + checkM(SchemaGen.anySequenceAndValue) { + case (schema, value) => + assertEncodesThenDecodes(Schema.Optional(schema), Some(value)) &> + assertEncodesThenDecodes(Schema.Optional(schema), None) + } + } + ), + testM("tuple") { + checkM(SchemaGen.anyTupleAndValue) { + case (schema, value) => assertEncodesThenDecodes(schema, value) + } + }, + testM("sequence") { + checkM(SchemaGen.anySequenceAndValue) { + case (schema, value) => assertEncodesThenDecodes(schema, value) + } + }, + testM("sequence of ZoneOffset") { + //FIXME test independently because including ZoneOffset in StandardTypeGen.anyStandardType wreaks havoc. + checkM(Gen.chunkOf(JavaTimeGen.anyZoneOffset)) { chunk => + assertEncodesThenDecodes( + Schema.Sequence(Schema.Primitive(StandardType.ZoneOffset)), + chunk + ) + } + }, + suite("record")( + testM("any") { + checkM(SchemaGen.anyRecordAndValue) { + case (schema, value) => assertEncodesThenDecodes(schema, value) + } + }, + testM("minimal test case") { + SchemaGen.anyRecordAndValue.runHead.flatMap { + case Some((schema, value)) => + val key = new String(Array('\u0007', '\n')) + val embedded = Schema.Record(Map(key -> schema)) + assertEncodesThenDecodes(embedded, Map(key -> value)) + case None => ZIO.fail("Should never happen!") + } + }, + testM("record of records") { + checkM(SchemaGen.anyRecordOfRecordsAndValue) { + case (schema, value) => assertEncodesThenDecodes(schema, value) + } + }, + testM("of primitives") { + assertEncodesThenDecodes( + recordSchema, + Map[String, Any]("foo" -> "s", "bar" -> 1) + ) + }, + testM("of ZoneOffsets") { + checkM(JavaTimeGen.anyZoneOffset) { zoneOffset => + assertEncodesThenDecodes( + Schema.Record(Map("zoneOffset" -> Schema.Primitive(StandardType.ZoneOffset))), + Map[String, Any]("zoneOffset" -> zoneOffset) + ) + } + }, + testM("of record") { + assertEncodesThenDecodes( + nestedRecordSchema, + Map[String, Any]("l1" -> "s", "l2" -> Map[String, Any]("foo" -> "s", "bar" -> 1)) + ) + }, + testM("case class") { + checkM(searchRequestGen)(assertEncodesThenDecodes(searchRequestSchema, _)) + } + ), + suite("enumeration")( + testM("of primitives") { + assertEncodesThenDecodes( + enumSchema, + Map("string" -> "foo") + ) + }, + testM("ADT") { + assertEncodesThenDecodes( + adtSchema, + Enumeration(StringValue("foo")) + ) &> assertEncodesThenDecodes(adtSchema, Enumeration(IntValue(-1))) &> assertEncodesThenDecodes( + adtSchema, + Enumeration(BooleanValue(false)) + ) + } + ), + suite("transform")( + testM("any") { + checkM(SchemaGen.anyTransformAndValue) { + case (schema, value) => + assertEncodesThenDecodes(schema, value) + } + } + ) + ) + + private def assertEncodesUnit = { + val schema = Schema.Primitive(StandardType.UnitType) + assertEncodes(schema, (), Chunk.empty) + } + + private def assertEncodesString(value: String) = { + val schema = Schema.Primitive(StandardType.StringType) + assertEncodes(schema, value, stringify(value)) + } + + private def assertEncodes[A](schema: Schema[A], value: A, chunk: Chunk[Byte]) = { + val stream = ZStream + .succeed(value) + .transduce(JsonCodec.encoder(schema)) + .runCollect + assertM(stream)(equalTo(chunk)) + } + + private def assertEncodesJson[A](schema: Schema[A], value: A)(implicit enc: JsonEncoder[A]) = { + val stream = ZStream + .succeed(value) + .transduce(JsonCodec.encoder(schema)) + .runCollect + assertM(stream)(equalTo(jsonEncoded(value))) + } + + private def assertDecodesUnit = { + val schema = Schema.Primitive(StandardType.UnitType) + assertDecodes(schema, (), Chunk.empty) + } + + private def assertDecodesToError[A](schema: Schema[A], json: CharSequence, errors: List[JsonError]) = { + val stream = ZStream + .fromChunk(JsonCodec.Encoder.charSequenceToByteChunk(json)) + .transduce(JsonCodec.decoder(schema)) + .catchAll(ZStream.succeed[String](_)) + .runHead + assertM(stream)(isSome(equalTo(JsonError.render(errors)))) + } + + private def assertDecodesString(value: String) = { + val schema = Schema.Primitive(StandardType.StringType) + assertDecodes(schema, value, stringify(value)) + } + + private def assertDecodes[A](schema: Schema[A], value: A, chunk: Chunk[Byte]) = { + val result = ZStream.fromChunk(chunk).transduce(JsonCodec.decoder(schema)).runCollect + assertM(result)(equalTo(Chunk(value))) + } + + private def assertEncodesThenDecodes[A](schema: Schema[A], value: A) = { + val result = ZStream + .succeed(value) + .transduce(JsonCodec.encoder(schema)) + .runCollect + .flatMap { encoded => + ZStream + .fromChunk(encoded) + .transduce(JsonCodec.decoder(schema)) + .runCollect + } + assertM(result)(equalTo(Chunk(value))) + } + + private def jsonEncoded[A](value: A)(implicit enc: JsonEncoder[A]): Chunk[Byte] = + JsonCodec.Encoder.charSequenceToByteChunk(enc.encodeJson(value, None)) + + private def stringify(s: String): Chunk[Byte] = { + val encoded = JsonEncoder.string.encodeJson(s, None) + JsonCodec.Encoder.charSequenceToByteChunk(encoded) + } + + case class SearchRequest(query: String, pageNumber: Int, resultPerPage: Int) + + object SearchRequest { + implicit val encoder: JsonEncoder[SearchRequest] = DeriveJsonEncoder.gen[SearchRequest] + } + + private val searchRequestGen: Gen[Random with Sized, SearchRequest] = + for { + query <- Gen.anyString + pageNumber <- Gen.int(Int.MinValue, Int.MaxValue) + results <- Gen.int(Int.MinValue, Int.MaxValue) + } yield SearchRequest(query, pageNumber, results) + + val searchRequestSchema: Schema[SearchRequest] = Schema.caseClassN( + "query" -> Schema[String], + "pageNumber" -> Schema[Int], + "resultPerPage" -> Schema[Int] + )(SearchRequest.apply, SearchRequest.unapply) + + val recordSchema: Schema.Record = Schema.Record( + Map( + "foo" -> Schema.Primitive(StandardType.StringType), + "bar" -> Schema.Primitive(StandardType.IntType) + ) + ) + + val nestedRecordSchema: Schema.Record = Schema.Record( + Map( + "l1" -> Schema.Primitive(StandardType.StringType), + "l2" -> recordSchema + ) + ) + + val enumSchema: Schema.Enumeration = Schema.Enumeration( + Map( + "string" -> Schema.Primitive(StandardType.StringType), + "int" -> Schema.Primitive(StandardType.IntType), + "boolean" -> Schema.Primitive(StandardType.BoolType) + ) + ) + + sealed trait OneOf + case class StringValue(value: String) extends OneOf + case class IntValue(value: Int) extends OneOf + case class BooleanValue(value: Boolean) extends OneOf + + val schemaOneOf: Schema[OneOf] = Schema.Transform( + Schema.enumeration( + Map( + "string" -> Schema[String], + "int" -> Schema[Int], + "boolean" -> Schema[Boolean] + ) + ), + (value: Map[String, _]) => { + value + .get("string") + .map(v => Right(StringValue(v.asInstanceOf[String]))) + .orElse(value.get("int").map(v => Right(IntValue(v.asInstanceOf[Int])))) + .orElse(value.get("boolean").map(v => Right(BooleanValue(v.asInstanceOf[Boolean])))) + .getOrElse(Left("No value found")) + }, { + case StringValue(v) => Right(Map("string" -> v)) + case IntValue(v) => Right(Map("int" -> v)) + case BooleanValue(v) => Right(Map("boolean" -> v)) + } + ) + + case class Enumeration(oneOf: OneOf) + + val adtSchema: Schema[Enumeration] = + Schema.caseClassN("value" -> schemaOneOf)(Enumeration, Enumeration.unapply) + +} diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 47d1407b2..c6e7221af 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -11,6 +11,7 @@ object BuildHelper { val zioVersion = "1.0.3" val zioNioVersion = "1.0.0-RC9" + val zioJsonVersion = "0.1.2" val silencerVersion = "1.7.1" val magnoliaVersion = "0.16.0"