Skip to content

Commit

Permalink
Initial commit of wip from https://github.com/zio/zio-web/pull/90. zio#7
Browse files Browse the repository at this point in the history


>
Co-authored-by: Brandon Brown <brandon@bbrownsound.com>
Co-authored-by: Jason Pickens <jasonpickensnz@gmail.com>
  • Loading branch information
brbrown25 committed Nov 24, 2020
1 parent cbc83ed commit e86607e
Show file tree
Hide file tree
Showing 13 changed files with 826 additions and 5 deletions.
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ lazy val core = project
.settings(stdSettings("zio-schema-core"))
.settings(
libraryDependencies ++= Seq(
"dev.zio" %% "zio" % zioVersion,
"dev.zio" %% "zio" % zioVersion,
"dev.zio" %% "zio-json" % zioJsonVersion,
"com.propensive" %% "magnolia" % magnoliaVersion,
"org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided
)
)
)
9 changes: 9 additions & 0 deletions core/src/main/scala/zio/schema/codec/Codec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package zio.schema.codec

import zio.json.{ JsonDecoder, JsonEncoder }
import zio.schema.Schema

trait Codec {
def encoder[A](schema: Schema[A]): JsonEncoder[A]
def decoder[A](schema: Schema[A]): JsonDecoder[A]
}
43 changes: 43 additions & 0 deletions core/src/main/scala/zio/schema/codec/json/CodecDecoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package zio.schema.codec.json

import java.time.DayOfWeek
import zio.json.JsonDecoder
import zio.json.internal.RetractReader
import zio.schema.StandardType

object CodecDecoder {

final def primitiveDecoder[A](standardType: StandardType[A]): JsonDecoder[A] =
standardType match {
case StandardType.UnitType => unitDecoder
case StandardType.StringType => JsonDecoder[String]
case StandardType.BoolType => JsonDecoder[Boolean]
case StandardType.ShortType => JsonDecoder[Short]
case StandardType.IntType => JsonDecoder[Int]
case StandardType.LongType => JsonDecoder[Long]
case StandardType.FloatType => JsonDecoder[Float]
case StandardType.DoubleType => JsonDecoder[Double]
case StandardType.ByteType => JsonDecoder[Byte]
case StandardType.CharType => JsonDecoder[Char]
case StandardType.DayOfWeekType => JsonDecoder[DayOfWeek]
case StandardType.Duration(_) => JsonDecoder[java.time.Duration]
case StandardType.Instant(_) => JsonDecoder[java.time.Instant]
case StandardType.LocalDate(_) => JsonDecoder[java.time.LocalDate]
case StandardType.LocalDateTime(_) => JsonDecoder[java.time.LocalDateTime]
case StandardType.LocalTime(_) => JsonDecoder[java.time.LocalTime]
case StandardType.Month => JsonDecoder[java.time.Month]
case StandardType.MonthDay => JsonDecoder[java.time.MonthDay]
case StandardType.OffsetDateTime(_) => JsonDecoder[java.time.OffsetDateTime]
case StandardType.OffsetTime(_) => JsonDecoder[java.time.OffsetTime]
case StandardType.Period => JsonDecoder[java.time.Period]
case StandardType.Year => JsonDecoder[java.time.Year]
case StandardType.YearMonth => JsonDecoder[java.time.YearMonth]
case StandardType.ZonedDateTime(_) => JsonDecoder[java.time.ZonedDateTime]
case StandardType.ZoneId => JsonDecoder[java.time.ZoneId]
case StandardType.ZoneOffset => JsonDecoder[java.time.ZoneOffset]
}

private val unitDecoder: JsonDecoder[Unit] =
(_: List[JsonDecoder.JsonError], _: RetractReader) => ()

}
88 changes: 88 additions & 0 deletions core/src/main/scala/zio/schema/codec/json/CodecEncoder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package zio.schema.codec.json

import zio.json.JsonEncoder.string
import zio.json.internal.Write
import zio.json.{ JsonEncoder, JsonFieldEncoder }
import zio.schema.StandardType

object CodecEncoder {

final def primitiveEncoder[A](standardType: StandardType[A]): JsonEncoder[A] =
standardType match {
case StandardType.UnitType => unitEncoder
case StandardType.StringType => JsonEncoder[String]
case StandardType.BoolType => JsonEncoder[Boolean]
case StandardType.ShortType => JsonEncoder[Short]
case StandardType.IntType => JsonEncoder[Int]
case StandardType.LongType => JsonEncoder[Long]
case StandardType.FloatType => JsonEncoder[Float]
case StandardType.DoubleType => JsonEncoder[Double]
case StandardType.ByteType => JsonEncoder[Byte]
case StandardType.CharType => JsonEncoder[Char]
case StandardType.DayOfWeekType => JsonEncoder[java.time.DayOfWeek]
case StandardType.Duration(_) => JsonEncoder[java.time.Duration]
case StandardType.Instant(_) => JsonEncoder[java.time.Instant]
case StandardType.LocalDate(_) => JsonEncoder[java.time.LocalDate]
case StandardType.LocalDateTime(_) => JsonEncoder[java.time.LocalDateTime]
case StandardType.LocalTime(_) => JsonEncoder[java.time.LocalTime]
case StandardType.Month => JsonEncoder[java.time.Month]
case StandardType.MonthDay => JsonEncoder[java.time.MonthDay]
case StandardType.OffsetDateTime(_) => JsonEncoder[java.time.OffsetDateTime]
case StandardType.OffsetTime(_) => JsonEncoder[java.time.OffsetTime]
case StandardType.Period => JsonEncoder[java.time.Period]
case StandardType.Year => JsonEncoder[java.time.Year]
case StandardType.YearMonth => JsonEncoder[java.time.YearMonth]
case StandardType.ZonedDateTime(_) => JsonEncoder[java.time.ZonedDateTime]
case StandardType.ZoneId => JsonEncoder[java.time.ZoneId]
case StandardType.ZoneOffset => JsonEncoder[java.time.ZoneOffset]
}

private val unitEncoder: JsonEncoder[Unit] =
(_: Unit, _: Option[Int], _: Write) => ()

// A modified version of zio.json.EncoderLowPriority2.keyValueIterable.
final def recordEncoder[A](keyValueEncoders: Map[String, JsonEncoder[_]]): JsonEncoder[A] = new JsonEncoder[A] {

def unsafeEncode(record: A, indent: Option[Int], out: Write): Unit = {
if (keyValueEncoders.isEmpty) return out.write("{}")

val K = JsonFieldEncoder.string

// FIXME: Can we do this safely?
val product = record.asInstanceOf[Product]
val elements = for (i <- 0 until product.productArity) yield {
val name = product.productElementName(i)
val value = product.productElement(i)
name -> value
}

out.write('{')
val indent_ = JsonEncoder.bump(indent)
JsonEncoder.pad(indent_, out)
var first = true
elements.foreach {
case (k, a) =>
// FIXME: Can we do this safely?
val A = keyValueEncoders.getOrElse(k, throw new Exception(s"No Schema found for field '$k'."))
// FIXME: Can we do this safely?
if (!A.isNothing(a.asInstanceOf)) {
if (first)
first = false
else {
out.write(',')
if (!indent.isEmpty)
JsonEncoder.pad(indent_, out)
}

string.unsafeEncode(K.unsafeEncodeField(k), indent_, out)
if (indent.isEmpty) out.write(':')
else out.write(" : ")
// FIXME: Can we do this safely?
A.unsafeEncode(a.asInstanceOf, indent_, out)
}
}
JsonEncoder.pad(indent, out)
out.write('}')
}
}
}
31 changes: 31 additions & 0 deletions core/src/main/scala/zio/schema/codec/json/JsonCodec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package zio.schema.codec.json

import zio.json.{ JsonDecoder, JsonEncoder }
import zio.schema._
import zio.schema.codec.Codec

// TODO: Should this be a class that takes a character encoding parameter?
object JsonCodec extends Codec {

override def encoder[A](schema: Schema[A]): JsonEncoder[A] = schema match {
case Schema.Record(structure) =>
val keyValueEncoders = structure.view.mapValues(encoder(_)).toMap
CodecEncoder.recordEncoder(keyValueEncoders)
case Schema.Sequence(element) => JsonEncoder.chunk(encoder(element))
case Schema.Enumeration(enumeration) => CodecEncoder.recordEncoder(enumeration.view.mapValues(encoder(_)).toMap)
case Schema.Transform(_, _, _) => ???
case Schema.Primitive(standardType) => CodecEncoder.primitiveEncoder(standardType)
case Schema.Tuple(left, right) => JsonEncoder.tuple2(encoder(left), encoder(right))
case Schema.Optional(schema) => JsonEncoder.option(encoder(schema))
}

override def decoder[A](schema: Schema[A]): JsonDecoder[A] = schema match {
case Schema.Record(_) => ???
case Schema.Sequence(element) => JsonDecoder.chunk(decoder(element))
case Schema.Enumeration(_) => ???
case Schema.Transform(_, _, _) => ???
case Schema.Primitive(standardType) => CodecDecoder.primitiveDecoder(standardType)
case Schema.Tuple(left, right) => JsonDecoder.tuple2(decoder(left), decoder(right))
case Schema.Optional(schema) => JsonDecoder.option(decoder(schema))
}
}
2 changes: 1 addition & 1 deletion core/src/main/scala/zio/schema/macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,4 @@ object DeriveSchema {
)

implicit def gen[T]: Schema[T] = macro Magnolia.gen[T]
}
}
2 changes: 1 addition & 1 deletion core/src/test/scala/zio/schema/DeriveSchemaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@ object DeriveSchemaSpec extends DefaultRunnableSpec {
assert(statusSchema)(hasSameSchema(expectedSchema))
}
)
}
}
133 changes: 133 additions & 0 deletions core/src/test/scala/zio/schema/JavaTimeGen.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package zio.schema

import java.time._
import java.time.temporal.ChronoField
import scala.jdk.CollectionConverters._
import zio.ZIO
import zio.random.Random
import zio.stream.ZStream
import zio.test.{ Gen, Sample }

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)

val anyIntYear: Gen[Random, Int] = chronoFieldValue(ChronoField.YEAR).map(_.toInt)

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)
}

val anyDuration: Gen[Random, Duration] = Gen.anyLong.zipWith(Gen.long(0, 999_999_999L)) { (seconds, nanos) =>
Duration.ofSeconds(seconds, nanos)
}

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

val anyZoneId: Gen[Random, ZoneId] =
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)
}
}
2 changes: 1 addition & 1 deletion core/src/test/scala/zio/schema/SchemaAssertions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ object SchemaAssertions {

private def hasSameKeys[K, V](map1: Map[K, V], map2: Map[K, V]): Boolean =
map1.keySet.diff(map2.keySet).isEmpty
}
}
Loading

0 comments on commit e86607e

Please sign in to comment.