diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ea9fe6..89e8fd05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Allowed items to have both point in time and time range datetimes [#405](https://github.com/azavea/stac4s/pull/405) ## [0.6.2] - 2021-07-29 ### Fixed diff --git a/modules/core-test/js/src/test/scala/com/azavea/stac4s/JsSerDeSpec.scala b/modules/core-test/js/src/test/scala/com/azavea/stac4s/JsSerDeSpec.scala index b489b261..7332a518 100644 --- a/modules/core-test/js/src/test/scala/com/azavea/stac4s/JsSerDeSpec.scala +++ b/modules/core-test/js/src/test/scala/com/azavea/stac4s/JsSerDeSpec.scala @@ -16,8 +16,6 @@ class JsSerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with checkAll("Codec.ItemCollection", CodecTests[ItemCollection].unserializableCodec) checkAll("Codec.StacItem", CodecTests[StacItem].unserializableCodec) checkAll("Codec.Geometry", CodecTests[Geometry].unserializableCodec) - checkAll("Codec.ItemDatetime", CodecTests[ItemDatetime].unserializableCodec) - checkAll("Codec.ItemProperties", CodecTests[ItemProperties].unserializableCodec) /** Ensure that the datetime field is present but null for time ranges * @@ -25,7 +23,7 @@ class JsSerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with * https://github.com/radiantearth/stac-spec/blob/v1.0.0-rc.4/item-spec/common-metadata.md#date-and-time-range */ test("Encoded time ranges print null datetime") { - val tr = ItemDatetime.TimeRange( + val tr = TimeRange( Instant.parse("2021-01-01T00:00:00Z"), Instant.parse("2022-01-01T00:00:00Z") ) diff --git a/modules/core-test/jvm/src/test/scala/com/azavea/stac4s/JvmSerDeSpec.scala b/modules/core-test/jvm/src/test/scala/com/azavea/stac4s/JvmSerDeSpec.scala index 511a83b4..4a962e4a 100644 --- a/modules/core-test/jvm/src/test/scala/com/azavea/stac4s/JvmSerDeSpec.scala +++ b/modules/core-test/jvm/src/test/scala/com/azavea/stac4s/JvmSerDeSpec.scala @@ -29,8 +29,6 @@ class JvmSerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers wit checkAll("Codec.StacLayer", CodecTests[StacLayer].unserializableCodec) checkAll("Codec.PeriodDuration", CodecTests[PeriodDuration].unserializableCodec) checkAll("Codec.PeriodicExtent", CodecTests[PeriodicExtent].unserializableCodec) - checkAll("Codec.ItemDatetime", CodecTests[ItemDatetime].unserializableCodec) - checkAll("Codec.ItemProperties", CodecTests[ItemProperties].unserializableCodec) /** Ensure that the datetime field is present but null for time ranges * @@ -38,7 +36,7 @@ class JvmSerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers wit * https://github.com/radiantearth/stac-spec/blob/v1.0.0-rc.4/item-spec/common-metadata.md#date-and-time-range */ test("Encoded time ranges print null datetime") { - val tr = ItemDatetime.TimeRange( + val tr = TimeRange( Instant.parse("2021-01-01T00:00:00Z"), Instant.parse("2022-01-01T00:00:00Z") ) diff --git a/modules/core-test/shared/src/test/scala/com/azavea/stac4s/SerDeSpec.scala b/modules/core-test/shared/src/test/scala/com/azavea/stac4s/SerDeSpec.scala index 3af5e9c3..a4b98fe0 100644 --- a/modules/core-test/shared/src/test/scala/com/azavea/stac4s/SerDeSpec.scala +++ b/modules/core-test/shared/src/test/scala/com/azavea/stac4s/SerDeSpec.scala @@ -6,6 +6,7 @@ import com.azavea.stac4s.extensions.label._ import com.azavea.stac4s.extensions.layer._ import com.azavea.stac4s.meta.ForeignImplicits._ import com.azavea.stac4s.testing.TestInstances._ +import com.azavea.stac4s.types._ import io.circe.Decoder import io.circe.parser._ @@ -33,6 +34,8 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M checkAll("Codec.StacProviderRole", CodecTests[StacProviderRole].unserializableCodec) checkAll("Codec.ThreeDimBbox", CodecTests[ThreeDimBbox].unserializableCodec) checkAll("Codec.TwoDimBbox", CodecTests[TwoDimBbox].unserializableCodec) + checkAll("Codec.ItemDatetime", CodecTests[ItemDatetime].unserializableCodec) + checkAll("Codec.ItemProperties", CodecTests[ItemProperties].unserializableCodec) // extensions @@ -72,7 +75,6 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M private def accumulatingDecodeTest[T: Decoder]: Assertion = decodeAccumulating[T]("{}").fold( errs => { - println(s"Errs: $errs") errs.size should be > 1 }, _ => fail("Decoding succeeded but should not have") diff --git a/modules/core/js/src/main/scala/com/azavea/stac4s/StacItem.scala b/modules/core/js/src/main/scala/com/azavea/stac4s/StacItem.scala index 8d41e2e7..025d0f27 100644 --- a/modules/core/js/src/main/scala/com/azavea/stac4s/StacItem.scala +++ b/modules/core/js/src/main/scala/com/azavea/stac4s/StacItem.scala @@ -4,8 +4,8 @@ import com.azavea.stac4s.geometry.Geometry import cats.Eq import io.circe._ -import monocle.macros.{GenLens, GenPrism} -import monocle.{Lens, Optional} +import monocle.Lens +import monocle.macros.GenLens final case class StacItem( id: String, @@ -28,20 +28,8 @@ final case class StacItem( object StacItem { - private val datetimeField: Lens[StacItem, ItemDatetime] = GenLens[StacItem](_.properties.datetime) - - private val toDatetime = GenPrism[ItemDatetime, ItemDatetime.PointInTime] - - private val toTimeRange = GenPrism[ItemDatetime, ItemDatetime.TimeRange] - val propertiesExtension: Lens[StacItem, JsonObject] = GenLens[StacItem](_.properties.extensionFields) - val datetimePrism: Optional[StacItem, ItemDatetime.PointInTime] = - datetimeField.composePrism(toDatetime) - - val timeRangePrism: Optional[StacItem, ItemDatetime.TimeRange] = - datetimeField.composePrism(toTimeRange) - implicit val eqStacItem: Eq[StacItem] = Eq.fromUniversalEquals implicit val encStacItem: Encoder[StacItem] = Encoder diff --git a/modules/core/jvm/src/main/scala/com/azavea/stac4s/StacItem.scala b/modules/core/jvm/src/main/scala/com/azavea/stac4s/StacItem.scala index f6387c04..a77f0982 100644 --- a/modules/core/jvm/src/main/scala/com/azavea/stac4s/StacItem.scala +++ b/modules/core/jvm/src/main/scala/com/azavea/stac4s/StacItem.scala @@ -5,8 +5,8 @@ import com.azavea.stac4s.meta._ import cats.Eq import geotrellis.vector.Geometry import io.circe._ -import monocle.macros.{GenLens, GenPrism} -import monocle.{Lens, Optional} +import monocle.Lens +import monocle.macros.GenLens final case class StacItem( id: String, @@ -29,20 +29,8 @@ final case class StacItem( object StacItem { - private val datetimeField: Lens[StacItem, ItemDatetime] = GenLens[StacItem](_.properties.datetime) - - private val toDatetime = GenPrism[ItemDatetime, ItemDatetime.PointInTime] - - private val toTimeRange = GenPrism[ItemDatetime, ItemDatetime.TimeRange] - val propertiesExtension: Lens[StacItem, JsonObject] = GenLens[StacItem](_.properties.extensionFields) - val datetimePrism: Optional[StacItem, ItemDatetime.PointInTime] = - datetimeField.composePrism(toDatetime) - - val timeRangePrism: Optional[StacItem, ItemDatetime.TimeRange] = - datetimeField.composePrism(toTimeRange) - implicit val eqStacItem: Eq[StacItem] = Eq.fromUniversalEquals implicit val encStacItem: Encoder[StacItem] = Encoder diff --git a/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemDatetime.scala b/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemDatetime.scala index 0eef6172..9f25b8c5 100644 --- a/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemDatetime.scala +++ b/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemDatetime.scala @@ -1,42 +1,30 @@ package com.azavea.stac4s -import cats.kernel.Eq import cats.syntax.apply._ -import cats.syntax.functor._ -import io.circe.syntax._ import io.circe.{Decoder, Encoder, HCursor} import java.time.Instant -sealed abstract class ItemDatetime +case class PointInTime(when: Instant) -object ItemDatetime { +case class TimeRange(start: Instant, end: Instant) - case class PointInTime(when: Instant) extends ItemDatetime - - case class TimeRange(start: Instant, end: Instant) extends ItemDatetime - - implicit val eqItemDatetime: Eq[ItemDatetime] = Eq.fromUniversalEquals - - implicit val decPointInTime: Decoder[PointInTime] = { cursor: HCursor => - cursor.get[Instant]("datetime") map { PointInTime } - } +object TimeRange { implicit val decTimeRange: Decoder[TimeRange] = { cursor: HCursor => - (cursor.get[Instant]("start_datetime"), cursor.get[Instant]("end_datetime")) mapN { TimeRange } + (cursor.get[Instant]("start_datetime"), cursor.get[Instant]("end_datetime")) mapN { TimeRange.apply } } - implicit val decItemDatetime: Decoder[ItemDatetime] = decPointInTime.widen or decTimeRange.widen - - implicit val encPointInTime: Encoder[PointInTime] = Encoder.forProduct1("datetime")(_.when) - implicit val encTimeRange: Encoder[TimeRange] = Encoder.forProduct3("datetime", "start_datetime", "end_datetime")(range => (Option.empty[Instant], range.start, range.end) ) +} - implicit val encItemDateTime: Encoder[ItemDatetime] = { - case pit @ PointInTime(_) => pit.asJson - case tr @ TimeRange(_, _) => tr.asJson +object PointInTime { + + implicit val decPointInTime: Decoder[PointInTime] = { cursor: HCursor => + cursor.get[Instant]("datetime") map { PointInTime.apply } } + implicit val encPointInTime: Encoder[PointInTime] = Encoder.forProduct1("datetime")(_.when) } diff --git a/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemProperties.scala b/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemProperties.scala index dcf2b670..dc11fe26 100644 --- a/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemProperties.scala +++ b/modules/core/shared/src/main/scala/com/azavea/stac4s/ItemProperties.scala @@ -1,5 +1,7 @@ package com.azavea.stac4s +import com.azavea.stac4s.types._ + import cats.data.NonEmptyList import cats.kernel.Eq import cats.syntax.apply._ diff --git a/modules/core/shared/src/main/scala/com/azavea/stac4s/types.scala b/modules/core/shared/src/main/scala/com/azavea/stac4s/types.scala index c717aa64..f677b478 100644 --- a/modules/core/shared/src/main/scala/com/azavea/stac4s/types.scala +++ b/modules/core/shared/src/main/scala/com/azavea/stac4s/types.scala @@ -1,11 +1,46 @@ package com.azavea.stac4s +import cats.data.Ior +import cats.kernel.Eq import eu.timepit.refined.W import eu.timepit.refined.api.Refined import eu.timepit.refined.generic._ +import io.circe.syntax._ +import io.circe.{Decoder, DecodingFailure, Encoder, HCursor} package object types { type CatalogType = String Refined Equal[W.`"Catalog"`.T] type CollectionType = String Refined Equal[W.`"Collection"`.T] + + type ItemDatetime = Ior[PointInTime, TimeRange] + + implicit val encItemDateTime: Encoder[ItemDatetime] = { + case Ior.Left(pit @ PointInTime(_)) => pit.asJson + case Ior.Right(tr @ TimeRange(_, _)) => tr.asJson + case Ior.Both(pit @ PointInTime(_), tr @ TimeRange(_, _)) => + // order is important here! the time range encoder also writes a `null` value to the + // datetime field, which overwrites what the point-in-time encoder wants to write. + val out = tr.asJson.deepMerge(pit.asJson) + out + } + + implicit val decItemDateTime: Decoder[ItemDatetime] = { c: HCursor => + (c.as[PointInTime], c.as[TimeRange]) match { + case (Right(pit), Right(tr)) => Right(Ior.Both(pit, tr)) + case (_, Right(tr)) => Right(Ior.Right(tr)) + case (Right(pit), _) => Right(Ior.Left(pit)) + case (Left(err1), Left(err2)) => + (err1, err2) match { + case (DecodingFailure(decFailure1, h1), DecodingFailure(decFailure2, h2)) => + Left(DecodingFailure(s"${decFailure1}. ${decFailure2}", h1 ++ h2)) + // since they're decoding the same cursor, if one of the errors is a ParsingFailure instead + // of a decoding failure, they _both_ should be, so we just need the first one + case _ => + Left(err1) + } + } + } + + implicit val eqItemDatetime: Eq[ItemDatetime] = Eq.fromUniversalEquals } diff --git a/modules/testing/js/src/main/scala/JsInstances.scala b/modules/testing/js/src/main/scala/JsInstances.scala index 0e41360f..474b96b5 100644 --- a/modules/testing/js/src/main/scala/JsInstances.scala +++ b/modules/testing/js/src/main/scala/JsInstances.scala @@ -8,8 +8,6 @@ import com.azavea.stac4s.{ Bbox, Interval, ItemCollection, - ItemDatetime, - ItemProperties, Proprietary, SpatialExtent, StacAsset, @@ -62,7 +60,7 @@ trait JsInstances extends GenericInstances { private[testing] def stacItemGen: Gen[StacItem] = ( nonEmptyStringGen, - Gen.const("1.0.0-rc2"), + Gen.const("1.0.0"), Gen.const(List.empty[String]), Gen.const("Feature"), geometryGen, @@ -70,13 +68,13 @@ trait JsInstances extends GenericInstances { nonEmptyListGen(TestInstances.stacLinkGen) map { _.toList }, TestInstances.assetMapGen, Gen.option(nonEmptyStringGen), - itemPropertiesGen + TestInstances.itemPropertiesGen ).mapN(StacItem.apply) private[testing] def stacItemShortGen: Gen[StacItem] = ( nonEmptyStringGen, - Gen.const("1.0.0-rc2"), + Gen.const("1.0.0"), Gen.const(List.empty[String]), Gen.const("Feature"), geometryGen, @@ -84,7 +82,7 @@ trait JsInstances extends GenericInstances { Gen.const(Nil), Gen.const(Map.empty[String, StacAsset]), Gen.option(nonEmptyStringGen), - itemPropertiesGen + TestInstances.itemPropertiesGen ).mapN(StacItem.apply) private[testing] def itemCollectionGen: Gen[ItemCollection] = @@ -110,7 +108,7 @@ trait JsInstances extends GenericInstances { private[testing] def stacCollectionShortGen: Gen[StacCollection] = ( Arbitrary.arbitrary[CollectionType], - Gen.const("1.0.0-rc2"), + Gen.const("1.0.0"), Gen.const(Nil), nonEmptyStringGen, nonEmptyStringGen.map(_.some), @@ -137,30 +135,6 @@ trait JsInstances extends GenericInstances { StacLayer.apply ) - private def itemDateTimeGen: Gen[ItemDatetime] = Gen.oneOf[ItemDatetime]( - instantGen map { ItemDatetime.PointInTime }, - (instantGen, instantGen) mapN { - case (i1, i2) if i2.isAfter(i1) => ItemDatetime.TimeRange(i1, i2) - case (i1, i2) => ItemDatetime.TimeRange(i2, i1) - } - ) - - private def itemPropertiesGen: Gen[ItemProperties] = ( - itemDateTimeGen, - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(instantGen), - Gen.option(instantGen), - Gen.option(TestInstances.stacLicenseGen), - Gen.option(nonEmptyListGen(TestInstances.stacProviderGen)), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(nonEmptyListGen(nonEmptyAlphaRefinedStringGen)), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(TestInstances.finiteDoubleGen), - TestInstances.itemExtensionFieldsGen - ) mapN { ItemProperties.apply } - implicit val arbItem: Arbitrary[StacItem] = Arbitrary { stacItemGen } val arbItemShort: Arbitrary[StacItem] = Arbitrary { stacItemShortGen } @@ -181,13 +155,6 @@ trait JsInstances extends GenericInstances { stacLayerGen } - implicit val arbItemDatetime: Arbitrary[ItemDatetime] = Arbitrary { - itemDateTimeGen - } - - implicit val arbItemProperties: Arbitrary[ItemProperties] = Arbitrary { - itemPropertiesGen - } } object JsInstances extends JsInstances {} diff --git a/modules/testing/jvm/src/main/scala/JvmInstances.scala b/modules/testing/jvm/src/main/scala/JvmInstances.scala index b46170f0..f0c0d6cc 100644 --- a/modules/testing/jvm/src/main/scala/JvmInstances.scala +++ b/modules/testing/jvm/src/main/scala/JvmInstances.scala @@ -3,13 +3,11 @@ package com.azavea.stac4s.testing import com.azavea.stac4s.extensions.layer.StacLayer import com.azavea.stac4s.extensions.periodic.PeriodicExtent import com.azavea.stac4s.syntax._ -import com.azavea.stac4s.types.CollectionType +import com.azavea.stac4s.types._ import com.azavea.stac4s.{ Bbox, Interval, ItemCollection, - ItemDatetime, - ItemProperties, NumericRangeSummary, SchemaSummary, SpatialExtent, @@ -184,7 +182,7 @@ trait JvmInstances extends GenericInstances { private[testing] def stacItemShortGen: Gen[StacItem] = ( nonEmptyStringGen, - Gen.const("1.0.0-rc2"), + Gen.const("1.0.0"), Gen.const(List.empty[String]), Gen.const("Feature"), rectangleGen, @@ -192,7 +190,7 @@ trait JvmInstances extends GenericInstances { Gen.const(Nil), Gen.const(Map.empty[String, StacAsset]), Gen.option(nonEmptyStringGen), - itemPropertiesGen + TestInstances.itemPropertiesGen ).mapN(StacItem.apply) private[testing] def itemCollectionGen: Gen[ItemCollection] = @@ -289,34 +287,10 @@ trait JvmInstances extends GenericInstances { PeriodicExtent.apply } - private def itemDateTimeGen: Gen[ItemDatetime] = Gen.oneOf[ItemDatetime]( - instantGen map { ItemDatetime.PointInTime }, - (instantGen, instantGen) mapN { - case (i1, i2) if i2.isAfter(i1) => ItemDatetime.TimeRange(i1, i2) - case (i1, i2) => ItemDatetime.TimeRange(i2, i1) - } - ) - - private def itemPropertiesGen: Gen[ItemProperties] = ( - itemDateTimeGen, - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(instantGen), - Gen.option(instantGen), - Gen.option(TestInstances.stacLicenseGen), - Gen.option(nonEmptyListGen(TestInstances.stacProviderGen)), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(nonEmptyListGen(nonEmptyAlphaRefinedStringGen)), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(nonEmptyAlphaRefinedStringGen), - Gen.option(TestInstances.finiteDoubleGen), - TestInstances.itemExtensionFieldsGen - ) mapN { ItemProperties.apply } - private[testing] def stacItemGen: Gen[StacItem] = ( nonEmptyStringGen, - Gen.const("1.0.0-rc2"), + Gen.const("1.0.0"), Gen.const(List.empty[String]), Gen.const("Feature"), rectangleGen, @@ -324,7 +298,7 @@ trait JvmInstances extends GenericInstances { nonEmptyListGen(TestInstances.stacLinkGen) map { _.toList }, TestInstances.assetMapGen, Gen.option(nonEmptyStringGen), - itemPropertiesGen + TestInstances.itemPropertiesGen ).mapN(StacItem.apply) implicit val arbItem: Arbitrary[StacItem] = Arbitrary { stacItemGen } @@ -373,14 +347,6 @@ trait JvmInstances extends GenericInstances { intervalGen } - implicit val arbItemDatetime: Arbitrary[ItemDatetime] = Arbitrary { - itemDateTimeGen - } - - implicit val arbItemProperties: Arbitrary[ItemProperties] = Arbitrary { - itemPropertiesGen - } - implicit val arbSummaryValue: Arbitrary[SummaryValue] = Arbitrary { summaryValueGen } diff --git a/modules/testing/shared/src/main/scala/TestInstances.scala b/modules/testing/shared/src/main/scala/TestInstances.scala index 11d60a79..f312df89 100644 --- a/modules/testing/shared/src/main/scala/TestInstances.scala +++ b/modules/testing/shared/src/main/scala/TestInstances.scala @@ -6,8 +6,9 @@ import com.azavea.stac4s.extensions.eo._ import com.azavea.stac4s.extensions.label.LabelClassClasses._ import com.azavea.stac4s.extensions.label._ import com.azavea.stac4s.extensions.layer.{LayerItemExtension, StacLayerProperties} -import com.azavea.stac4s.types.CatalogType +import com.azavea.stac4s.types._ +import cats.data.Ior import cats.syntax.apply._ import cats.syntax.functor._ import enumeratum.scalacheck._ @@ -329,6 +330,40 @@ trait TestInstances extends NumericInstances with GenericInstances { Gen.listOfN(5, (nonEmptyStringGen, cogAssetGen).tupled) map { Map(_: _*) } ) + // we know that the list of instances will have a min and max because we're constructing + // it within this method and it always has three elements. + @SuppressWarnings(Array("UnsafeTraversableMethods")) + private[testing] def itemDateTimeGen: Gen[ItemDatetime] = Gen.oneOf[ItemDatetime]( + instantGen map { inst => Ior.Left(PointInTime(inst)) }, + (instantGen, instantGen) mapN { + case (i1, i2) if i2.isAfter(i1) => Ior.Right(TimeRange(i1, i2)) + case (i1, i2) => Ior.Right(TimeRange(i2, i1)) + }, + (instantGen, instantGen, instantGen) mapN { case (i1, i2, i3) => + val instants = List(i1, i2, i3) + val start = instants.min + val end = instants.max + val middle = instants.filter(inst => (inst != start && inst != end)).headOption.getOrElse(start) + Ior.Both(PointInTime(middle), TimeRange(start, end)) + } + ) + + private[testing] def itemPropertiesGen: Gen[ItemProperties] = ( + itemDateTimeGen, + Gen.option(nonEmptyAlphaRefinedStringGen), + Gen.option(nonEmptyAlphaRefinedStringGen), + Gen.option(instantGen), + Gen.option(instantGen), + Gen.option(stacLicenseGen), + Gen.option(nonEmptyListGen(stacProviderGen)), + Gen.option(nonEmptyAlphaRefinedStringGen), + Gen.option(nonEmptyListGen(nonEmptyAlphaRefinedStringGen)), + Gen.option(nonEmptyAlphaRefinedStringGen), + Gen.option(nonEmptyAlphaRefinedStringGen), + Gen.option(finiteDoubleGen), + itemExtensionFieldsGen + ) mapN { ItemProperties.apply } + implicit val arbMediaType: Arbitrary[StacMediaType] = Arbitrary { mediaTypeGen } @@ -418,6 +453,14 @@ trait TestInstances extends NumericInstances with GenericInstances { implicit val arbStacLayerProperties: Arbitrary[StacLayerProperties] = Arbitrary { stacLayerPropertiesGen } + + implicit val arbItemDatetime: Arbitrary[ItemDatetime] = Arbitrary { + itemDateTimeGen + } + + implicit val arbItemProperties: Arbitrary[ItemProperties] = Arbitrary { + itemPropertiesGen + } } object TestInstances extends TestInstances {}