From 3b05fac87cf3059eab89d7b1f284befb73bcd008 Mon Sep 17 00:00:00 2001 From: Christof Nolle Date: Mon, 30 Sep 2024 15:49:48 +0200 Subject: [PATCH] feat: lenient local date parser in java.time parsing a LocalDate is stricter than in joda we want to guaranteebackwards compatibility BEC-254 --- .../main/scala/io/sphere/json/FromJSON.scala | 19 +++++++- .../json/JodaJavaLocalDateCompatSpec.scala | 48 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 json/json-core/src/test/scala/io/sphere/json/JodaJavaLocalDateCompatSpec.scala diff --git a/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala index e109157b..187aa5bb 100644 --- a/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala @@ -403,6 +403,21 @@ object FromJSON extends FromJSONInstances { .parseDefaulting(time.temporal.ChronoField.OFFSET_SECONDS, 0L) .toFormatter() + private val lenientLocalDateParser = + new time.format.DateTimeFormatterBuilder() + .appendValue(time.temporal.ChronoField.YEAR, 1, 9, java.time.format.SignStyle.NORMAL) + .optionalStart() + .appendLiteral('-') + .appendValue(time.temporal.ChronoField.MONTH_OF_YEAR, 1, 2, java.time.format.SignStyle.NORMAL) + .optionalStart() + .appendLiteral('-') + .appendValue(time.temporal.ChronoField.DAY_OF_MONTH, 1, 2, java.time.format.SignStyle.NORMAL) + .optionalEnd() + .optionalEnd() + .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) + .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) + .toFormatter() + implicit val javaInstantReader: FromJSON[time.Instant] = jsonStringReader("Failed to parse date/time: %s")(s => time.Instant.from(lenientInstantParser.parse(s))) @@ -412,8 +427,8 @@ object FromJSON extends FromJSONInstances { time.LocalTime.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_TIME)) implicit val javaLocalDateReader: FromJSON[time.LocalDate] = - jsonStringReader("Failed to parse date: %s")( - time.LocalDate.parse(_, time.format.DateTimeFormatter.ISO_LOCAL_DATE)) + jsonStringReader("Failed to parse date: %s")(s => + time.LocalDate.from(lenientLocalDateParser.parse(s))) implicit val javaYearMonthReader: FromJSON[time.YearMonth] = jsonStringReader("Failed to parse year/month: %s")( diff --git a/json/json-core/src/test/scala/io/sphere/json/JodaJavaLocalDateCompatSpec.scala b/json/json-core/src/test/scala/io/sphere/json/JodaJavaLocalDateCompatSpec.scala new file mode 100644 index 00000000..81e3c108 --- /dev/null +++ b/json/json-core/src/test/scala/io/sphere/json/JodaJavaLocalDateCompatSpec.scala @@ -0,0 +1,48 @@ +package io.sphere.json + +import org.json4s.JString +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import java.time.Instant +import cats.data.Validated.Valid + +class JodaJavaLocalDateCompatSpec extends AnyWordSpec with Matchers { + + val jodaReader = FromJSON.dateReader + val javaReader = FromJSON.javaLocalDateReader + def jsonDateStringWith( + year: String = "2035", + dayOfTheMonth: String = "23", + monthOfTheYear: String = "11"): JString = JString(s"$year-$monthOfTheYear-${dayOfTheMonth}") + + private def test(value: JString) = + (jodaReader.read(value), javaReader.read(value)) match { + case (Valid(jodaDate), Valid(javaDate)) => + jodaDate.getYear shouldBe javaDate.getYear + jodaDate.getMonthOfYear shouldBe javaDate.getMonthValue + jodaDate.getDayOfMonth shouldBe javaDate.getDayOfMonth + case (jodaDate, javaDate) => + fail(s"invalid date. joda: $jodaDate, java: $javaDate") + } + + "parsing a LocalDate" should { + "accept two digit years" in { + test(jsonDateStringWith(year = "50")) + } + "accept year zero" in { + test(JString("0-10-31")) + } + "accept no day set" in { + test(JString("2024-09")) + } + "accept up to nine digit years" in { + (1 to 9).foreach { l => + val year = List.fill(l)("1").mkString("") + test(jsonDateStringWith(year = year)) + } + } + "accept a year with leading zero" in { + test(jsonDateStringWith(year = "02020")) + } + } +}