From 28e1f9f6d963e16eb86d2093fd2d1edf8074992d Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:38:48 +0200 Subject: [PATCH] Add Schema for `zio.json.ast.Json` --- .../main/scala/zio/schema/codec/package.scala | 82 +++++++++++ .../zio/schema/codec/JsonCodecSpec.scala | 130 +++++++++++++++++- 2 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala 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 new file mode 100644 index 000000000..4eb7c92ac --- /dev/null +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala @@ -0,0 +1,82 @@ +package zio.schema.codec + +import zio.Chunk +import zio.json.ast.Json +import zio.schema.annotation.directDynamicMapping +import zio.schema.{DynamicValue, Schema, StandardType, TypeId} + +import java.util.Base64 +import scala.collection.immutable.ListMap + +package object json { + implicit val schemaJson: Schema[Json] = + Schema.dynamicValue.annotate(directDynamicMapping()).transform(toJson, fromJson) + + private def toJson(dv: DynamicValue): Json = + dv match { + case DynamicValue.Record(id, values) => + values.foldLeft(Json.Obj()){case (obj, (name, value)) => (name, toJson(value)) +: obj } + case DynamicValue.Enumeration(_, _) => + throw new Exception("DynamicValue.Enumeration is unsupported") + case DynamicValue.Sequence(values) => + Json.Arr(values.map(toJson)) + case DynamicValue.Dictionary(_) => + throw new Exception("DynamicValue.Dictionary is unsupported") + case DynamicValue.SetValue(values) => + Json.Arr(Chunk.fromIterable(values.map(toJson))) + case DynamicValue.Primitive(value, standardType) => + standardType.asInstanceOf[StandardType[_]] match { + case StandardType.UnitType => Json.Obj() + case StandardType.StringType => Json.Str(value.asInstanceOf[String]) + case StandardType.BoolType => Json.Bool(value.asInstanceOf[Boolean]) + case StandardType.ByteType => Json.Num(value.asInstanceOf[Byte]) + case StandardType.ShortType => Json.Num(value.asInstanceOf[Short]) + case StandardType.IntType => Json.Num(value.asInstanceOf[Int]) + case StandardType.LongType => Json.Num(value.asInstanceOf[Long]) + case StandardType.FloatType => Json.Num(value.asInstanceOf[Float]) + case StandardType.DoubleType => Json.Num(value.asInstanceOf[Double]) + case StandardType.BinaryType => Json.Str(Base64.getEncoder.encodeToString(value.asInstanceOf[Chunk[Byte]].toArray)) + case StandardType.CharType => Json.Str(value.asInstanceOf[Char].toString) + case StandardType.UUIDType => Json.Str(value.asInstanceOf[java.util.UUID].toString) + case StandardType.BigDecimalType => Json.Num(value.asInstanceOf[java.math.BigDecimal]) + case StandardType.BigIntegerType => Json.Num(BigDecimal(value.asInstanceOf[java.math.BigInteger])) + case StandardType.DayOfWeekType => Json.Str(value.asInstanceOf[java.time.DayOfWeek].toString) + case StandardType.MonthType => Json.Str(value.asInstanceOf[java.time.Month].toString) + case StandardType.MonthDayType => Json.Str(value.asInstanceOf[java.time.MonthDay].toString) + case StandardType.PeriodType => Json.Str(value.asInstanceOf[java.time.Period].toString) + case StandardType.YearType => Json.Num(value.asInstanceOf[java.time.Year].getValue) + case StandardType.YearMonthType => Json.Str(value.asInstanceOf[java.time.YearMonth].toString) + case StandardType.ZoneIdType => Json.Str(value.asInstanceOf[java.time.ZoneId].toString) + case StandardType.ZoneOffsetType => Json.Str(value.asInstanceOf[java.time.ZoneOffset].toString) + case StandardType.DurationType => Json.Str(value.asInstanceOf[java.time.Duration].toString) + case StandardType.InstantType => Json.Str(value.asInstanceOf[java.time.Instant].toString) + case StandardType.LocalDateType => Json.Str(value.asInstanceOf[java.time.LocalDate].toString) + case StandardType.LocalTimeType => Json.Str(value.asInstanceOf[java.time.LocalTime].toString) + case StandardType.LocalDateTimeType => Json.Str(value.asInstanceOf[java.time.LocalDateTime].toString) + 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 DynamicValue.Singleton(instance) => Json.Obj() + case DynamicValue.SomeValue(value) => toJson(value) + case DynamicValue.NoneValue => Json.Null + case DynamicValue.Tuple(left, right) => Json.Arr(Chunk(toJson(left), toJson(right))) + case DynamicValue.LeftValue(value) => Json.Obj("Left" -> toJson(value)) + case DynamicValue.RightValue(value) => Json.Obj("Right" -> toJson(value)) + case DynamicValue.DynamicAst(_) => throw new Exception("DynamicValue.DynamicAst is unsupported") + case DynamicValue.Error(_) => throw new Exception("DynamicValue.Error is unsupported") + } + private def fromJson(json: Json): DynamicValue = + json match { + case Json.Null => DynamicValue.NoneValue + case Json.Bool(value) => DynamicValue.Primitive(value, StandardType.BoolType) + case Json.Num(value) => DynamicValue.Primitive(value, StandardType.BigDecimalType) + case Json.Str(value) => DynamicValue.Primitive(value, StandardType.StringType) + case Json.Arr(values) => DynamicValue.Sequence(values.map(fromJson)) + case Json.Obj(values) => + DynamicValue.Record( + TypeId.parse("Json.Obj"), + ListMap.from(values.map { case (name, value) => (name, fromJson(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 933c6ccb3..7bba2013d 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 @@ -1,26 +1,26 @@ package zio.schema.codec -import java.time.{ ZoneId, ZoneOffset } - -import scala.collection.immutable.ListMap - import zio.Console._ import zio._ import zio.json.JsonDecoder.JsonError -import zio.json.{ DeriveJsonEncoder, JsonEncoder } +import zio.json.ast.Json +import zio.json.{DeriveJsonEncoder, JsonEncoder} import zio.schema.CaseSet._ import zio.schema._ import zio.schema.annotation._ import zio.schema.codec.DecodeError.ReadError import zio.schema.codec.JsonCodec.JsonEncoder.charSequenceToByteChunk -import zio.schema.codec.JsonCodecSpec.PaymentMethod.{ CreditCard, PayPal, WireTransfer } -import zio.schema.codec.JsonCodecSpec.Subscription.{ OneTime, Recurring } +import zio.schema.codec.JsonCodecSpec.PaymentMethod.{CreditCard, PayPal, WireTransfer} +import zio.schema.codec.JsonCodecSpec.Subscription.{OneTime, Recurring} import zio.schema.meta.MetaSchema import zio.stream.ZStream import zio.test.Assertion._ import zio.test.TestAspect._ import zio.test._ +import java.time.{ZoneId, ZoneOffset} +import scala.collection.immutable.ListMap + object JsonCodecSpec extends ZIOSpecDefault { def spec: Spec[TestEnvironment, Any] = @@ -251,6 +251,64 @@ object JsonCodecSpec extends ZIOSpecDefault { charSequenceToByteChunk("""{"foo":"s","bar":1}""") ) } + ), + suite("zio.json.ast.Json encoding")( + test("Json.Obj") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Obj("foo" -> Json.Str("bar"), "null" -> Json.Null), + charSequenceToByteChunk("""{"foo":"bar","null":null}""") + ) + }, + test("Json.Arr") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Arr(Json.Str("foo"), Json.Num(1)), + charSequenceToByteChunk("""["foo",1]""") + ) + }, + test("Json.Num Int") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Num(1), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Long") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Num(1L), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Double") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Num(1.1), + charSequenceToByteChunk("""1.1"""), + ) + }, + test("Json.Str") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Str("foo"), + charSequenceToByteChunk(""""foo"""") + ) + }, + test("Json.Bool") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Bool(true), + charSequenceToByteChunk("""true""") + ) + }, + test("Json.Null") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Null, + charSequenceToByteChunk("""null""") + ) + } ) ) @@ -488,6 +546,64 @@ object JsonCodecSpec extends ZIOSpecDefault { ) ) } + ), + suite("zio.json.ast.Json decoding")( + test("Json.Obj") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Obj("foo" -> Json.Str("bar"), "null" -> Json.Null), + charSequenceToByteChunk("""{"foo":"bar","null":null}""") + ) + }, + test("Json.Arr") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Arr(Json.Str("foo"), Json.Num(1)), + charSequenceToByteChunk("""["foo",1]""") + ) + }, + test("Json.Num Int") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Num(1), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Long") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Num(1L), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Double") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Num(1.1), + charSequenceToByteChunk("""1.1"""), + ) + }, + test("Json.Str") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Str("foo"), + charSequenceToByteChunk(""""foo"""") + ) + }, + test("Json.Bool") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Bool(true), + charSequenceToByteChunk("""true""") + ) + }, + test("Json.Null") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Null, + charSequenceToByteChunk("""null""") + ) + } ) )