All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.schema.codec.AvroPropMarker.scala Maven / Gradle / Ivy

The newest version!
package zio.schema.codec

import java.time.format.DateTimeFormatter
import java.time.temporal.{ ChronoUnit, TemporalUnit }

import scala.jdk.CollectionConverters._
import scala.util.Try

import org.apache.avro.{ LogicalType, LogicalTypes, Schema => SchemaAvro }

sealed trait AvroPropMarker {
  def propName: String
  def value: Any = true
}

object AvroPropMarker {
  val wrapperNamePrefix = "wrapper"
  val wrapperNamespace  = "zio.schema.codec.avro"

  case object UnionWrapper extends AvroPropMarker {
    override def propName: String = "zio.schema.codec.avro.wrapper"
  }

  case object EitherWrapper extends 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()
  }

  final case class Formatter(dateTimeFormatter: DateTimeFormatter) extends AvroPropMarker {
    override def propName: String = Formatter.propName
    override def value: Any = dateTimeFormatter match {
      case f if f.equals(DateTimeFormatter.ISO_LOCAL_DATE_TIME)  => "ISO_LOCAL_DATE_TIME"
      case f if f.equals(DateTimeFormatter.ISO_DATE)             => "ISO_DATE"
      case f if f.equals(DateTimeFormatter.ISO_TIME)             => "ISO_TIME"
      case f if f.equals(DateTimeFormatter.ISO_LOCAL_TIME)       => "ISO_LOCAL_TIME"
      case f if f.equals(DateTimeFormatter.ISO_LOCAL_DATE)       => "ISO_LOCAL_DATE"
      case f if f.equals(DateTimeFormatter.ISO_OFFSET_DATE_TIME) => "ISO_OFFSET_DATE_TIME"
      case f if f.equals(DateTimeFormatter.ISO_OFFSET_DATE)      => "ISO_OFFSET_DATE"
      case f if f.equals(DateTimeFormatter.ISO_OFFSET_TIME)      => "ISO_OFFSET_TIME"
      case f if f.equals(DateTimeFormatter.ISO_ZONED_DATE_TIME)  => "ISO_ZONED_DATE_TIME"
      case f if f.equals(DateTimeFormatter.ISO_ORDINAL_DATE)     => "ISO_ORDINAL_DATE"
      case f if f.equals(DateTimeFormatter.ISO_WEEK_DATE)        => "ISO_WEEK_DATE"
      case f if f.equals(DateTimeFormatter.ISO_INSTANT)          => "ISO_INSTANT"
      case f if f.equals(DateTimeFormatter.ISO_DATE_TIME)        => "ISO_DATE_TIME"
      case f if f.equals(DateTimeFormatter.RFC_1123_DATE_TIME)   => "RFC_1123_DATE_TIME"
      case f if f.equals(DateTimeFormatter.BASIC_ISO_DATE)       => "BASIC_ISO_DATE"
      case _                                                     => dateTimeFormatter.toString
    }
  }

  object Formatter {
    // TODO: use only a single property marker and distinguish by value.
    //  A json with "zio.schema.codec.avro.dateTimeFormatter": false is confusing
    //  that is still uses the dateTimeFormatter regardless of the value
    val propName           = "zio.schema.codec.avro.dateTimeFormatter"
    val default: Formatter = Formatter(DateTimeFormatter.ISO_INSTANT)

    def fromAvroStringOrDefault(avroSchema: SchemaAvro, stringType: StringType): Either[String, Formatter] =
      fromAvroString(avroSchema).map {
        case Some(value) => value
        case None        => getDefaultByStringType(stringType)
      }

    def fromAvroStringOrDefault(avroSchema: SchemaAvro, logicalType: LogicalType): Either[String, Formatter] =
      fromAvroString(avroSchema).map {
        case Some(value) => value
        case None        => getDefaultByLogicalType(logicalType)
      }

    private def getDefaultByLogicalType(logicalType: LogicalType): Formatter =
      logicalType match {
        case _: LogicalTypes.Date                 => Formatter(DateTimeFormatter.ISO_DATE)
        case _: LogicalTypes.LocalTimestampMicros => Formatter(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
        case _: LogicalTypes.LocalTimestampMillis => Formatter(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
        case _: LogicalTypes.TimeMicros           => Formatter(DateTimeFormatter.ISO_LOCAL_TIME)
        case _: LogicalTypes.TimeMillis           => Formatter(DateTimeFormatter.ISO_LOCAL_TIME)
        case _: LogicalTypes.TimestampMicros      => Formatter(DateTimeFormatter.ISO_INSTANT)
        case _: LogicalTypes.TimestampMillis      => Formatter(DateTimeFormatter.ISO_INSTANT)
        case _                                    => Formatter(DateTimeFormatter.ISO_INSTANT)
      }

    private def getDefaultByStringType(stringType: StringType): Formatter =
      stringType match {
        case StringType.ZoneId         => Formatter(DateTimeFormatter.ISO_ZONED_DATE_TIME)
        case StringType.Instant        => Formatter(DateTimeFormatter.ISO_INSTANT)
        case StringType.LocalDate      => Formatter(DateTimeFormatter.ISO_LOCAL_DATE)
        case StringType.LocalTime      => Formatter(DateTimeFormatter.ISO_LOCAL_TIME)
        case StringType.LocalDateTime  => Formatter(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
        case StringType.OffsetTime     => Formatter(DateTimeFormatter.ISO_OFFSET_TIME)
        case StringType.OffsetDateTime => Formatter(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
        case StringType.ZoneDateTime   => Formatter(DateTimeFormatter.ISO_ZONED_DATE_TIME)
      }

    private def fromAvroString(avroSchema: SchemaAvro): Either[String, Option[Formatter]] =
      avroSchema.getObjectProps.asScala.get(propName).collect {
        case "ISO_LOCAL_DATE_TIME"  => Right(Formatter(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
        case "ISO_DATE"             => Right(Formatter(DateTimeFormatter.ISO_DATE))
        case "ISO_TIME"             => Right(Formatter(DateTimeFormatter.ISO_TIME))
        case "ISO_LOCAL_TIME"       => Right(Formatter(DateTimeFormatter.ISO_LOCAL_TIME))
        case "ISO_LOCAL_DATE"       => Right(Formatter(DateTimeFormatter.ISO_LOCAL_DATE))
        case "ISO_OFFSET_DATE_TIME" => Right(Formatter(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
        case "ISO_OFFSET_DATE"      => Right(Formatter(DateTimeFormatter.ISO_OFFSET_DATE))
        case "ISO_OFFSET_TIME"      => Right(Formatter(DateTimeFormatter.ISO_OFFSET_TIME))
        case "ISO_ZONED_DATE_TIME"  => Right(Formatter(DateTimeFormatter.ISO_ZONED_DATE_TIME))
        case "ISO_ORDINAL_DATE"     => Right(Formatter(DateTimeFormatter.ISO_ORDINAL_DATE))
        case "ISO_WEEK_DATE"        => Right(Formatter(DateTimeFormatter.ISO_WEEK_DATE))
        case "ISO_INSTANT"          => Right(Formatter(DateTimeFormatter.ISO_INSTANT))
        case "ISO_DATE_TIME"        => Right(Formatter(DateTimeFormatter.ISO_DATE_TIME))
        case "RFC_1123_DATE_TIME"   => Right(Formatter(DateTimeFormatter.RFC_1123_DATE_TIME))
        case "BASIC_ISO_DATE"       => Right(Formatter(DateTimeFormatter.BASIC_ISO_DATE))
        case s: String =>
          Try {
            Formatter(DateTimeFormatter.ofPattern(s))
          }.toEither.left.map(_.getMessage)
      } match {
        case Some(value) => value.map(Some(_))
        case None        => Right(None)
      }

  }

  object DurationChronoUnit {
    val default: DurationChronoUnit = DurationChronoUnit(ChronoUnit.MILLIS)
    val propName                    = "zio.schema.codec.avro.durationChronoUnit"

    def fromAvroDuration(avroSchema: SchemaAvro): Option[DurationChronoUnit] =
      avroSchema.getObjectProps.asScala.get(propName).collect {
        case "NANOS"     => DurationChronoUnit(ChronoUnit.NANOS)
        case "MICROS"    => DurationChronoUnit(ChronoUnit.MICROS)
        case "MILLIS"    => DurationChronoUnit(ChronoUnit.MILLIS)
        case "SECONDS"   => DurationChronoUnit(ChronoUnit.SECONDS)
        case "MINUTES"   => DurationChronoUnit(ChronoUnit.MINUTES)
        case "HOURS"     => DurationChronoUnit(ChronoUnit.HOURS)
        case "HALF_DAYS" => DurationChronoUnit(ChronoUnit.HALF_DAYS)
        case "DAYS"      => DurationChronoUnit(ChronoUnit.DAYS)
        case "WEEKS"     => DurationChronoUnit(ChronoUnit.WEEKS)
        case "MONTHS"    => DurationChronoUnit(ChronoUnit.MONTHS)
        case "YEARS"     => DurationChronoUnit(ChronoUnit.YEARS)
        case "DECADES"   => DurationChronoUnit(ChronoUnit.DECADES)
        case "CENTURIES" => DurationChronoUnit(ChronoUnit.CENTURIES)
        case "MILLENNIA" => DurationChronoUnit(ChronoUnit.MILLENNIA)
        case "ERAS"      => DurationChronoUnit(ChronoUnit.ERAS)
        case "FOREVER"   => DurationChronoUnit(ChronoUnit.FOREVER)
      }

    def fromTemporalUnit(temporalUnit: TemporalUnit): Option[DurationChronoUnit] =
      temporalUnit match {
        case unit: ChronoUnit => Some(DurationChronoUnit(unit))
        case _                => None
      }
  }

  final case class IntDiscriminator(intType: IntType) extends AvroPropMarker {
    override def propName: String = IntType.propName
    override def value: Any       = intType.value
  }

  final case class StringDiscriminator(stringType: StringType) extends AvroPropMarker {
    override def propName: String = StringType.propName
    override def value: Any       = stringType.value
  }

  final case class RecordDiscriminator(recordType: RecordType) extends AvroPropMarker {
    override def propName: String = RecordType.propName
    override def value: Any       = recordType.value
  }
}

sealed trait RecordType { self =>

  def value: String = self match {
    case RecordType.MonthDay  => "monthDay"
    case RecordType.Period    => "period"
    case RecordType.YearMonth => "yearMonth"
    case RecordType.Duration  => "duration"
    case RecordType.Tuple     => "tuple"
  }
}

object RecordType {
  val propName: String = "zio.schema.codec.recordType"

  def fromAvroRecord(avroSchema: SchemaAvro): Option[RecordType] =
    if (avroSchema.getType == SchemaAvro.Type.RECORD) {
      avroSchema.getObjectProps.asScala.get(propName).collect {
        case "monthDay"  => RecordType.MonthDay
        case "period"    => RecordType.Period
        case "yearMonth" => RecordType.YearMonth
        case "duration"  => RecordType.Duration
        case "tuple"     => RecordType.Tuple
      }
    } else None

  case object MonthDay  extends RecordType
  case object Period    extends RecordType
  case object YearMonth extends RecordType
  case object Duration  extends RecordType
  case object Tuple     extends RecordType
}

sealed trait StringType { self =>

  def value: String = self match {
    case StringType.ZoneId         => "zoneId"
    case StringType.Instant        => "instant"
    case StringType.LocalDate      => "localDate"
    case StringType.LocalTime      => "localTime"
    case StringType.LocalDateTime  => "localDateTime"
    case StringType.OffsetTime     => "offsetTime"
    case StringType.OffsetDateTime => "offsetDateTime"
    case StringType.ZoneDateTime   => "zoneDateTime"
  }
}

object StringType {
  val propName = "zio.schema.codec.stringType"

  def fromAvroString(avroSchema: SchemaAvro): Option[StringType] =
    if (avroSchema.getType == SchemaAvro.Type.STRING) {
      avroSchema.getObjectProps.asScala.get(propName).collect {
        case "zoneId"         => StringType.ZoneId
        case "instant"        => StringType.Instant
        case "localDate"      => StringType.LocalDate
        case "localTime"      => StringType.LocalTime
        case "localDateTime"  => StringType.LocalDateTime
        case "offsetTime"     => StringType.OffsetTime
        case "offsetDateTime" => StringType.OffsetDateTime
        case "zoneDateTime"   => StringType.ZoneDateTime
      }
    } else None

  case object ZoneId         extends StringType
  case object Instant        extends StringType
  case object LocalDate      extends StringType
  case object LocalTime      extends StringType
  case object LocalDateTime  extends StringType
  case object OffsetTime     extends StringType
  case object OffsetDateTime extends StringType
  case object ZoneDateTime   extends StringType
}

sealed trait IntType { self =>

  def value: String = self match {
    case IntType.Char       => "char"
    case IntType.Short      => "short"
    case IntType.DayOfWeek  => "dayOfWeek"
    case IntType.Month      => "month"
    case IntType.Year       => "year"
    case IntType.ZoneOffset => "zoneOffset"
  }
}

object IntType {
  val propName = "zio.schema.codec.intType"

  def fromAvroInt(avroSchema: SchemaAvro): Option[IntType] =
    if (avroSchema.getType == SchemaAvro.Type.INT) {
      avroSchema.getObjectProps.asScala.get(propName).collect {
        case "char"       => IntType.Char
        case "short"      => IntType.Short
        case "dayOfWeek"  => IntType.DayOfWeek
        case "month"      => IntType.Month
        case "year"       => IntType.Year
        case "zoneOffset" => IntType.ZoneOffset
      }
    } else None

  case object Char       extends IntType
  case object Short      extends IntType
  case object DayOfWeek  extends IntType
  case object Month      extends IntType
  case object Year       extends IntType
  case object ZoneOffset extends IntType
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy