Skip to content

Commit

Permalink
Merge pull request #768 from johnspade/javatime-codecs-formats
Browse files Browse the repository at this point in the history
Use DateTimeFormatter for javatime codecs
  • Loading branch information
adamw authored Sep 30, 2020
2 parents f847c60 + c6cae5d commit 4822fc8
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 19 deletions.
16 changes: 9 additions & 7 deletions core/src/main/scala/sttp/tapir/Codec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.nio.ByteBuffer
import java.nio.charset.{Charset, StandardCharsets}
import java.nio.file.Path
import java.time._
import java.time.format.DateTimeParseException
import java.time.format.{DateTimeFormatter, DateTimeParseException}
import java.util.{Base64, Date, UUID}

import sttp.model._
Expand Down Expand Up @@ -124,15 +124,17 @@ object Codec extends FormCodecDerivation {
implicit val uuid: Codec[String, UUID, TextPlain] = stringCodec[UUID](UUID.fromString)
implicit val bigDecimal: Codec[String, BigDecimal, TextPlain] = stringCodec[BigDecimal](BigDecimal(_))
implicit val javaBigDecimal: Codec[String, JBigDecimal, TextPlain] = stringCodec[JBigDecimal](new JBigDecimal(_))
implicit val localTime: Codec[String, LocalTime, TextPlain] = stringCodec[LocalTime](LocalTime.parse)
implicit val localDate: Codec[String, LocalDate, TextPlain] = stringCodec[LocalDate](LocalDate.parse)
implicit val offsetDateTime: Codec[String, OffsetDateTime, TextPlain] = stringCodec[OffsetDateTime](OffsetDateTime.parse)
implicit val zonedDateTime: Codec[String, ZonedDateTime, TextPlain] = offsetDateTime.map(_.toZonedDateTime)(_.toOffsetDateTime)
implicit val instant: Codec[String, Instant, TextPlain] = zonedDateTime.map(_.toInstant)(_.atZone(ZoneOffset.UTC))
implicit val localTime: Codec[String, LocalTime, TextPlain] = string.map(LocalTime.parse(_))(DateTimeFormatter.ISO_LOCAL_TIME.format)
implicit val localDate: Codec[String, LocalDate, TextPlain] = string.map(LocalDate.parse(_))(DateTimeFormatter.ISO_LOCAL_DATE.format)
implicit val offsetDateTime: Codec[String, OffsetDateTime, TextPlain] =
string.map(OffsetDateTime.parse(_))(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format)
implicit val zonedDateTime: Codec[String, ZonedDateTime, TextPlain] =
string.map(ZonedDateTime.parse(_))(DateTimeFormatter.ISO_ZONED_DATE_TIME.format)
implicit val instant: Codec[String, Instant, TextPlain] = string.map(Instant.parse(_))(DateTimeFormatter.ISO_INSTANT.format)
implicit val date: Codec[String, Date, TextPlain] = instant.map(Date.from(_))(_.toInstant)
implicit val zoneOffset: Codec[String, ZoneOffset, TextPlain] = stringCodec[ZoneOffset](ZoneOffset.of)
implicit val duration: Codec[String, Duration, TextPlain] = stringCodec[Duration](Duration.parse)
implicit val offsetTime: Codec[String, OffsetTime, TextPlain] = stringCodec[OffsetTime](OffsetTime.parse)
implicit val offsetTime: Codec[String, OffsetTime, TextPlain] = string.map(OffsetTime.parse(_))(DateTimeFormatter.ISO_OFFSET_TIME.format)
implicit val scalaDuration: Codec[String, SDuration, TextPlain] = stringCodec[SDuration](SDuration.apply)
implicit val localDateTime: Codec[String, LocalDateTime, TextPlain] = string.mapDecode { l =>
try {
Expand Down
64 changes: 52 additions & 12 deletions core/src/test/scala/sttp/tapir/CodecTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sttp.tapir

import java.math.{BigDecimal => JBigDecimal}
import java.time._
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.{Date, UUID}

Expand Down Expand Up @@ -73,13 +74,7 @@ class CodecTest extends AnyFlatSpec with Matchers with Checkers {
checkEncodeDecodeToString[Uri]
checkEncodeDecodeToString[BigDecimal]
checkEncodeDecodeToString[JBigDecimal]
checkEncodeDecodeToString[LocalTime]
checkEncodeDecodeToString[LocalDate]
checkEncodeDecodeToString[OffsetDateTime]
checkEncodeDecodeToString[Instant]
checkEncodeDecodeToString[ZoneOffset]
checkEncodeDecodeToString[Duration]
checkEncodeDecodeToString[OffsetTime]
checkEncodeDecodeToString[SDuration]
}

Expand All @@ -90,34 +85,51 @@ class CodecTest extends AnyFlatSpec with Matchers with Checkers {
}

it should "decode LocalDateTime from string with timezone" in {
check((zdt: ZonedDateTime) => localDateTimeCodec.decode(zdt.toOffsetDateTime.toString) == Value(zdt.toLocalDateTime))
check((zdt: ZonedDateTime) =>
localDateTimeCodec.decode(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zdt)) == Value(zdt.toLocalDateTime)
)
}

it should "correctly encode and decode ZonedDateTime" in {
val codec = implicitly[Codec[String, ZonedDateTime, TextPlain]]
codec.encode(ZonedDateTime.of(LocalDateTime.of(2010, 9, 22, 14, 32, 1), ZoneOffset.ofHours(5))) shouldBe "2010-09-22T14:32:01+05:00"
codec.encode(ZonedDateTime.of(LocalDateTime.of(2010, 9, 22, 14, 32, 1), ZoneOffset.UTC)) shouldBe "2010-09-22T14:32:01Z"
check((zdt: ZonedDateTime) => codec.decode(codec.encode(zdt)) == Value(zdt))
check { zdt: ZonedDateTime =>
val encoded = codec.encode(zdt)
codec.decode(encoded) == Value(zdt) && ZonedDateTime.parse(encoded) == zdt
}
}

it should "correctly encode and decode OffsetDateTime" in {
val codec = implicitly[Codec[String, OffsetDateTime, TextPlain]]
codec.encode(OffsetDateTime.of(LocalDateTime.of(2019, 12, 31, 23, 59, 14), ZoneOffset.ofHours(5))) shouldBe "2019-12-31T23:59:14+05:00"
codec.encode(OffsetDateTime.of(LocalDateTime.of(2020, 9, 22, 14, 32, 1), ZoneOffset.ofHours(0))) shouldBe "2020-09-22T14:32:01Z"
check((odt: OffsetDateTime) => codec.decode(codec.encode(odt)) == Value(odt))
check { odt: OffsetDateTime =>
val encoded = codec.encode(odt)
codec.decode(encoded) == Value(odt) && OffsetDateTime.parse(encoded) == odt
}
}

it should "correctly encode and decode example Instants" in {
it should "correctly encode and decode Instant" in {
val codec = implicitly[Codec[String, Instant, TextPlain]]
codec.encode(Instant.ofEpochMilli(1583760958000L)) shouldBe "2020-03-09T13:35:58Z"
codec.decode("2020-02-19T12:35:58Z") shouldBe (Value(Instant.ofEpochMilli(1582115758000L)))
codec.encode(Instant.EPOCH) shouldBe "1970-01-01T00:00:00Z"
codec.decode("2020-02-19T12:35:58Z") shouldBe Value(Instant.ofEpochMilli(1582115758000L))
check { i: Instant =>
val encoded = codec.encode(i)
codec.decode(encoded) == Value(i) && Instant.parse(encoded) == i
}
}

it should "correctly encode and decode example Durations" in {
val codec = implicitly[Codec[String, Duration, TextPlain]]
val start = OffsetDateTime.parse("2020-02-19T12:35:58Z")
codec.encode(Duration.between(start, start.plusDays(791).plusDays(12).plusSeconds(3))) shouldBe "PT19272H3S"
codec.decode("PT3H15S") shouldBe Value(Duration.of(10815000, ChronoUnit.MILLIS))
check { d: Duration =>
val encoded = codec.encode(d)
codec.decode(encoded) == Value(d) && Duration.parse(encoded) == d
}
}

it should "correctly encode and decode example ZoneOffsets" in {
Expand All @@ -132,17 +144,45 @@ class CodecTest extends AnyFlatSpec with Matchers with Checkers {
codec.encode(OffsetTime.parse("13:45:30.123456789+02:00")) shouldBe "13:45:30.123456789+02:00"
codec.decode("12:00-11:30") shouldBe Value(OffsetTime.of(12, 0, 0, 0, ZoneOffset.ofHoursMinutes(-11, -30)))
codec.decode("06:15Z") shouldBe Value(OffsetTime.of(6, 15, 0, 0, ZoneOffset.UTC))
check { ot: OffsetTime =>
val encoded = codec.encode(ot)
codec.decode(encoded) == Value(ot) && OffsetTime.parse(encoded) == ot
}
}

it should "correctly encode and decode Date" in {
val codec = implicitly[Codec[String, Date, TextPlain]]
check((d: Date) => codec.decode(codec.encode(d)) == Value(d))
check { d: Date =>
val encoded = codec.encode(d)
codec.decode(encoded) == Value(d) && Date.from(Instant.parse(encoded)) == d
}
}

it should "decode LocalDateTime from string without timezone" in {
check((ldt: LocalDateTime) => localDateTimeCodec.decode(ldt.toString) == Value(ldt))
}

it should "correctly encode and decode LocalTime" in {
val codec = implicitly[Codec[String, LocalTime, TextPlain]]
codec.encode(LocalTime.of(22, 59, 31, 3)) shouldBe "22:59:31.000000003"
codec.encode(LocalTime.of(13, 30)) shouldBe "13:30:00"
codec.decode("22:59:31.000000003") shouldBe Value(LocalTime.of(22, 59, 31, 3))
check { lt: LocalTime =>
val encoded = codec.encode(lt)
codec.decode(encoded) == Value(lt) && LocalTime.parse(encoded) == lt
}
}

it should "correctly encode and decode LocalDate" in {
val codec = implicitly[Codec[String, LocalDate, TextPlain]]
codec.encode(LocalDate.of(2019, 12, 31)) shouldBe "2019-12-31"
codec.encode(LocalDate.of(2020, 9, 22)) shouldBe "2020-09-22"
check { ld: LocalDate =>
val encoded = codec.encode(ld)
codec.decode(encoded) == Value(ld) && LocalDate.parse(encoded) == ld
}
}

def checkEncodeDecodeToString[T: Arbitrary](implicit c: Codec[String, T, TextPlain], ct: ClassTag[T]): Assertion =
withClue(s"Test for ${ct.runtimeClass.getName}") {
check((a: T) => {
Expand Down

0 comments on commit 4822fc8

Please sign in to comment.