diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 136ec537..eaeb1de5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,6 +5,7 @@ Brief description of what this PR does, and why it is needed. ### Checklist - [ ] New tests have been added or existing tests have been modified +- [ ] Changelog updated ### Notes diff --git a/CHANGELOG.md b/CHANGELOG.md index cc73fc12..b13e6620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added +- Created typeclasses for linking extensions to the items they extend [#85](https://github.com/azavea/stac4s/pull/85) + ### Changed ### Deprecated diff --git a/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala b/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala index f36d7027..4725b365 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala @@ -1,42 +1,73 @@ package com.azavea.stac4s import cats.Eq +import cats.implicits._ import io.circe._ import io.circe.refined._ +import io.circe.syntax._ +import shapeless.LabelledGeneric +import shapeless.ops.record.Keys final case class ItemCollection( _type: String = "FeatureCollection", stacVersion: StacVersion, stacExtensions: List[String], features: List[StacItem], - links: List[StacLink] + links: List[StacLink], + extensionFields: JsonObject = ().asJsonObject ) object ItemCollection { + private val generic = LabelledGeneric[ItemCollection] + private val keys = Keys[generic.Repr].apply + val itemCollectionFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet + implicit val eqItemCollection: Eq[ItemCollection] = Eq.fromUniversalEquals - implicit val encItemCollection: Encoder[ItemCollection] = Encoder.forProduct5( - "type", - "stac_version", - "stac_extensions", - "features", - "links" - )(itemCollection => + implicit val encItemCollection: Encoder[ItemCollection] = new Encoder[ItemCollection] { + + def apply(collection: ItemCollection): Json = { + val baseEncoder: Encoder[ItemCollection] = Encoder.forProduct5( + "type", + "stac_version", + "stac_extensions", + "features", + "links" + )(itemCollection => + ( + itemCollection._type, + itemCollection.stacVersion, + itemCollection.stacExtensions, + itemCollection.features, + itemCollection.links + ) + ) + + baseEncoder(collection).deepMerge(collection.extensionFields.asJson) + } + } + + implicit val decItemCollection: Decoder[ItemCollection] = { c: HCursor => ( - itemCollection._type, - itemCollection.stacVersion, - itemCollection.stacExtensions, - itemCollection.features, - itemCollection.links + c.get[String]("type"), + c.get[StacVersion]("stac_version"), + c.get[Option[List[String]]]("stac_extensions"), + c.get[List[StacItem]]("features"), + c.get[Option[List[StacLink]]]("links"), + c.value.as[JsonObject] + ).mapN( + ( + _type: String, + stacVersion: StacVersion, + extensions: Option[List[String]], + features: List[StacItem], + links: Option[List[StacLink]], + document: JsonObject + ) => + ItemCollection(_type, stacVersion, extensions getOrElse Nil, features, links getOrElse Nil, document.filter({ + case (k, _) => !itemCollectionFields.contains(k) + })) ) - ) - - implicit val decItemCollection: Decoder[ItemCollection] = Decoder.forProduct5( - "type", - "stac_version", - "stac_extensions", - "features", - "links" - )(ItemCollection.apply) + } } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala index efd037d5..6ef62609 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala @@ -1,7 +1,11 @@ package com.azavea.stac4s import cats.Eq +import cats.implicits._ import io.circe._ +import io.circe.syntax._ +import shapeless.LabelledGeneric +import shapeless.ops.record.Keys final case class StacCatalog( stacVersion: String, @@ -9,25 +13,67 @@ final case class StacCatalog( id: String, title: Option[String], description: String, - links: List[StacLink] + links: List[StacLink], + extensionFields: JsonObject = ().asJsonObject ) object StacCatalog { + private val generic = LabelledGeneric[StacCatalog] + private val keys = Keys[generic.Repr].apply + val catalogFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet + implicit val eqStacCatalog: Eq[StacCatalog] = Eq.fromUniversalEquals - implicit val encCatalog: Encoder[StacCatalog] = - Encoder.forProduct6("stac_version", "stac_extensions", "id", "title", "description", "links")(catalog => + implicit val encCatalog: Encoder[StacCatalog] = new Encoder[StacCatalog] { + + def apply(catalog: StacCatalog): Json = { + val baseEncoder: Encoder[StacCatalog] = + Encoder.forProduct6("stac_version", "stac_extensions", "id", "title", "description", "links")(catalog => + ( + catalog.stacVersion, + catalog.stacExtensions, + catalog.id, + catalog.title, + catalog.description, + catalog.links + ) + ) + + baseEncoder(catalog).deepMerge(catalog.extensionFields.asJson) + } + } + + implicit val decCatalog: Decoder[StacCatalog] = { c: HCursor => + ( + c.get[String]("stac_version"), + c.get[List[String]]("stac_extensions"), + c.get[String]("id"), + c.get[Option[String]]("title"), + c.get[String]("description"), + c.get[List[StacLink]]("links"), + c.value.as[JsonObject] + ).mapN( ( - catalog.stacVersion, - catalog.stacExtensions, - catalog.id, - catalog.title, - catalog.description, - catalog.links - ) + version: String, + extensions: List[String], + id: String, + title: Option[String], + description: String, + links: List[StacLink], + document: JsonObject + ) => + StacCatalog.apply( + version, + extensions, + id, + title, + description, + links, + document.filter({ + case (k, _) => !catalogFields.contains(k) + }) + ) ) - - implicit val decCatalog: Decoder[StacCatalog] = - Decoder.forProduct6("stac_version", "stac_extensions", "id", "title", "description", "links")(StacCatalog.apply) + } } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala index 86286bde..1461dd34 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala @@ -1,8 +1,12 @@ package com.azavea.stac4s import cats.Eq +import cats.implicits._ import geotrellis.vector.{io => _} import io.circe._ +import io.circe.syntax._ +import shapeless.LabelledGeneric +import shapeless.ops.record.Keys final case class StacCollection( stacVersion: String, @@ -14,60 +18,76 @@ final case class StacCollection( license: StacLicense, providers: List[StacProvider], extent: StacExtent, + summaries: JsonObject, properties: JsonObject, - links: List[StacLink] + links: List[StacLink], + extensionFields: JsonObject = ().asJsonObject ) object StacCollection { + private val generic = LabelledGeneric[StacCollection] + private val keys = Keys[generic.Repr].apply + val collectionFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet + implicit val eqStacCollection: Eq[StacCollection] = Eq.fromUniversalEquals - implicit val encoderStacCollection: Encoder[StacCollection] = - Encoder.forProduct11( - "stac_version", - "stac_extensions", - "id", - "title", - "description", - "keywords", - "license", - "providers", - "extent", - "properties", - "links" - )(collection => - ( - collection.stacVersion, - collection.stacExtensions, - collection.id, - collection.title, - collection.description, - collection.keywords, - collection.license, - collection.providers, - collection.extent, - collection.properties, - collection.links + implicit val encoderStacCollection: Encoder[StacCollection] = new Encoder[StacCollection] { + + def apply(collection: StacCollection): Json = { + val baseEncoder: Encoder[StacCollection] = Encoder.forProduct12( + "stac_version", + "stac_extensions", + "id", + "title", + "description", + "keywords", + "license", + "providers", + "extent", + "summaries", + "properties", + "links" + )(collection => + ( + collection.stacVersion, + collection.stacExtensions, + collection.id, + collection.title, + collection.description, + collection.keywords, + collection.license, + collection.providers, + collection.extent, + collection.summaries, + collection.properties, + collection.links + ) ) - ) - implicit val decoderStacCollection: Decoder[StacCollection] = - Decoder.forProduct11( - "stac_version", - "stac_extensions", - "id", - "title", - "description", - "keywords", - "license", - "providers", - "extent", - "properties", - "links" - )( + baseEncoder(collection).deepMerge(collection.extensionFields.asJson) + } + } + + implicit val decoderStacCollection: Decoder[StacCollection] = { c: HCursor => + ( + c.get[String]("stac_version"), + c.get[Option[List[String]]]("stac_extensions"), + c.get[String]("id"), + c.get[Option[String]]("title"), + c.get[String]("description"), + c.get[Option[List[String]]]("keywords"), + c.get[StacLicense]("license"), + c.get[Option[List[StacProvider]]]("providers"), + c.get[StacExtent]("extent"), + c.get[Option[JsonObject]]("summaries"), + c.get[JsonObject]("properties"), + c.get[List[StacLink]]("links"), + c.value.as[JsonObject] + ).mapN( ( stacVersion: String, - stacExtensions: List[String], + stacExtensions: Option[List[String]], id: String, title: Option[String], description: String, @@ -75,12 +95,14 @@ object StacCollection { license: StacLicense, providers: Option[List[StacProvider]], extent: StacExtent, - properties: Option[JsonObject], - links: List[StacLink] + summaries: Option[JsonObject], + properties: JsonObject, + links: List[StacLink], + extensionFields: JsonObject ) => StacCollection( stacVersion, - stacExtensions, + stacExtensions getOrElse Nil, id, title, description, @@ -88,8 +110,13 @@ object StacCollection { license, providers getOrElse List.empty, extent, - properties getOrElse JsonObject.fromMap(Map.empty), - links + summaries getOrElse JsonObject.fromMap(Map.empty), + properties, + links, + extensionFields.filter({ + case (k, _) => !collectionFields.contains(k) + }) ) ) + } } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala index 3bd7141c..400322be 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala @@ -1,27 +1,61 @@ package com.azavea.stac4s import cats.Eq +import cats.implicits._ import io.circe._ +import io.circe.syntax._ +import shapeless.LabelledGeneric +import shapeless.ops.record.Keys final case class StacItemAsset( href: String, title: Option[String], description: Option[String], - roles: List[StacAssetRole], - _type: Option[StacMediaType] + roles: Set[StacAssetRole], + _type: Option[StacMediaType], + extensionFields: JsonObject = ().asJsonObject ) object StacItemAsset { + private val generic = LabelledGeneric[StacItemAsset] + private val keys = Keys[generic.Repr].apply + val assetFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet + implicit val eqStacItemAsset: Eq[StacItemAsset] = Eq.fromUniversalEquals - implicit val encStacItemAsset: Encoder[StacItemAsset] = - Encoder.forProduct5("href", "title", "description", "roles", "type")(asset => - (asset.href, asset.title, asset.description, asset.roles, asset._type) - ) + implicit val encStacItemAsset: Encoder[StacItemAsset] = new Encoder[StacItemAsset] { + + def apply(asset: StacItemAsset): Json = { + + val baseEncoder: Encoder[StacItemAsset] = + Encoder.forProduct5("href", "title", "description", "roles", "type")(asset => + (asset.href, asset.title, asset.description, asset.roles, asset._type) + ) + baseEncoder(asset).deepMerge(asset.extensionFields.asJson) + } + } - implicit val decStacItemAsset: Decoder[StacItemAsset] = - Decoder.forProduct5("href", "title", "description", "roles", "type")( - StacItemAsset.apply + implicit val decStacItemAsset: Decoder[StacItemAsset] = { c: HCursor => + ( + c.get[String]("href"), + c.get[Option[String]]("title"), + c.get[Option[String]]("description"), + c.get[Option[Set[StacAssetRole]]]("roles"), + c.get[Option[StacMediaType]]("type"), + c.value.as[JsonObject] + ).mapN( + ( + href: String, + title: Option[String], + description: Option[String], + roles: Option[Set[StacAssetRole]], + mediaType: Option[StacMediaType], + document: JsonObject + ) => + StacItemAsset(href, title, description, roles getOrElse Set.empty, mediaType, document.filter({ + case (k, _) => !assetFields.contains(k) + })) ) + } } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala index 156bff9d..81bdee97 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala @@ -2,24 +2,38 @@ package com.azavea.stac4s import cats.implicits._ import io.circe._ +import io.circe.syntax._ +import shapeless.LabelledGeneric +import shapeless.ops.record.Keys final case class StacLink( href: String, rel: StacLinkType, _type: Option[StacMediaType], title: Option[String], - labelExtAssets: List[String] + extensionFields: JsonObject = ().asJsonObject ) object StacLink { - implicit val encStacLink: Encoder[StacLink] = Encoder.forProduct5( - "href", - "rel", - "type", - "title", - "label:assets" - )(link => (link.href, link.rel, link._type, link.title, link.labelExtAssets)) + private val generic = LabelledGeneric[StacLink] + private val keys = Keys[generic.Repr].apply + val linkFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet + + implicit val encStacLink: Encoder[StacLink] = new Encoder[StacLink] { + + def apply(link: StacLink): Json = { + val baseEncoder = Encoder + .forProduct4( + "href", + "rel", + "type", + "title" + )((link: StacLink) => (link.href, link.rel, link._type, link.title)) + + baseEncoder(link).deepMerge(link.extensionFields.asJson) + } + } implicit val decStacLink: Decoder[StacLink] = { c: HCursor => ( @@ -27,15 +41,18 @@ object StacLink { c.downField("rel").as[StacLinkType], c.get[Option[StacMediaType]]("type"), c.get[Option[String]]("title"), - c.get[Option[List[String]]]("label:assets") + c.value.as[JsonObject] ).mapN( ( href: String, rel: StacLinkType, _type: Option[StacMediaType], title: Option[String], - assets: Option[List[String]] - ) => StacLink(href, rel, _type, title, assets getOrElse List.empty) + document: JsonObject + ) => + StacLink(href, rel, _type, title, document.filter({ + case (k, _) => !linkFields.contains(k) + })) ) } } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/CatalogExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CatalogExtension.scala new file mode 100644 index 00000000..88047971 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CatalogExtension.scala @@ -0,0 +1,25 @@ +package com.azavea.stac4s.extensions + +import com.azavea.stac4s.StacCatalog + +import io.circe.{Decoder, Encoder} +import io.circe.syntax._ + +trait CatalogExtension[T] { + def getExtensionFields(catalog: StacCatalog): ExtensionResult[T] + def addExtensionFields(catalog: StacCatalog, extensionFields: T): StacCatalog +} + +object CatalogExtension { + def apply[T](implicit ev: CatalogExtension[T]): CatalogExtension[T] = ev + + def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) = + new CatalogExtension[T] { + + def getExtensionFields(catalog: StacCatalog): ExtensionResult[T] = + decoder.decodeAccumulating(catalog.extensionFields.asJson.hcursor) + + def addExtensionFields(catalog: StacCatalog, extensionFields: T): StacCatalog = + catalog.copy(extensionFields = catalog.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields))) + } +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/CollectionExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CollectionExtension.scala new file mode 100644 index 00000000..4306b832 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CollectionExtension.scala @@ -0,0 +1,28 @@ +package com.azavea.stac4s.extensions + +import com.azavea.stac4s.StacCollection + +import io.circe.{Decoder, Encoder} +import io.circe.syntax._ + +trait CollectionExtension[T] { + def getExtensionFields(collection: StacCollection): ExtensionResult[T] + + def addExtensionFields(collection: StacCollection, extensionFields: T): StacCollection +} + +object CollectionExtension { + def apply[T](implicit ev: CollectionExtension[T]): CollectionExtension[T] = ev + + def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) = + new CollectionExtension[T] { + + def getExtensionFields(collection: StacCollection): ExtensionResult[T] = + decoder.decodeAccumulating(collection.extensionFields.asJson.hcursor) + + def addExtensionFields(collection: StacCollection, extensionFields: T): StacCollection = + collection.copy(extensionFields = + collection.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields)) + ) + } +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemAssetExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemAssetExtension.scala new file mode 100644 index 00000000..3b910f39 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemAssetExtension.scala @@ -0,0 +1,25 @@ +package com.azavea.stac4s.extensions + +import com.azavea.stac4s.StacItemAsset + +import io.circe.{Decoder, Encoder} +import io.circe.syntax._ + +trait ItemAssetExtension[T] { + def getExtensionFields(asset: StacItemAsset): ExtensionResult[T] + def addExtensionFields(asset: StacItemAsset, extensionFields: T): StacItemAsset +} + +object ItemAssetExtension { + def apply[T](implicit ev: ItemAssetExtension[T]): ItemAssetExtension[T] = ev + + def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) = + new ItemAssetExtension[T] { + + def getExtensionFields(asset: StacItemAsset): ExtensionResult[T] = + decoder.decodeAccumulating(asset.extensionFields.asJson.hcursor) + + def addExtensionFields(asset: StacItemAsset, extensionFields: T): StacItemAsset = + asset.copy(extensionFields = asset.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields))) + } +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemCollectionExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemCollectionExtension.scala new file mode 100644 index 00000000..12ee2cca --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemCollectionExtension.scala @@ -0,0 +1,29 @@ +package com.azavea.stac4s.extensions + +import com.azavea.stac4s._ + +import io.circe.{Decoder, Encoder} +import io.circe.syntax._ + +trait ItemCollectionExtension[T] { + def getExtensionFields(itemCollection: ItemCollection): ExtensionResult[T] + def addExtensionFields(itemCollection: ItemCollection, properties: T): ItemCollection +} + +object ItemExtensionCollection { + def apply[T](implicit ev: ItemCollectionExtension[T]): ItemCollectionExtension[T] = ev + + def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]): ItemCollectionExtension[T] = + new ItemCollectionExtension[T] { + + def getExtensionFields(itemCollection: ItemCollection): ExtensionResult[T] = + decoder.decodeAccumulating( + itemCollection.extensionFields.asJson.hcursor + ) + + def addExtensionFields(itemCollection: ItemCollection, extensionProperties: T): ItemCollection = + itemCollection.copy(extensionFields = + itemCollection.extensionFields.deepMerge(objectEncoder.encodeObject(extensionProperties)) + ) + } +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemExtension.scala new file mode 100644 index 00000000..968ec3d2 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemExtension.scala @@ -0,0 +1,30 @@ +package com.azavea.stac4s.extensions + +import com.azavea.stac4s.StacItem + +import io.circe._ +import io.circe.syntax._ + +// typeclass trait for anything that is an extension of item properties +trait ItemExtension[T] { + def getExtensionFields(item: StacItem): ExtensionResult[T] + def addExtensionFields(item: StacItem, properties: T): StacItem +} + +object ItemExtension { + // summoner + def apply[T](implicit ev: ItemExtension[T]): ItemExtension[T] = ev + + // constructor for anything with a `Decoder` and an `Encoder.AsObject` + def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]): ItemExtension[T] = + new ItemExtension[T] { + + def getExtensionFields(item: StacItem): ExtensionResult[T] = + decoder.decodeAccumulating( + item.properties.asJson.hcursor + ) + + def addExtensionFields(item: StacItem, extensionProperties: T) = + item.copy(properties = item.properties.deepMerge(objectEncoder.encodeObject(extensionProperties))) + } +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/LinkExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/LinkExtension.scala new file mode 100644 index 00000000..fdfc9e59 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/LinkExtension.scala @@ -0,0 +1,26 @@ +package com.azavea.stac4s.extensions + +import com.azavea.stac4s.StacLink + +import io.circe.{Decoder, Encoder} +import io.circe.syntax._ + +trait LinkExtension[T] { + def getExtensionFields(link: StacLink): ExtensionResult[T] + + def addExtensionFields(link: StacLink, extensionFields: T): StacLink +} + +object LinkExtension { + def apply[T](implicit ev: LinkExtension[T]): LinkExtension[T] = ev + + def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) = + new LinkExtension[T] { + + def getExtensionFields(link: StacLink): ExtensionResult[T] = + decoder.decodeAccumulating(link.extensionFields.asJson.hcursor) + + def addExtensionFields(link: StacLink, extensionFields: T): StacLink = + link.copy(extensionFields = link.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields))) + } +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/AssetCollectionExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/AssetCollectionExtension.scala new file mode 100644 index 00000000..458570e1 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/AssetCollectionExtension.scala @@ -0,0 +1,23 @@ +package com.azavea.stac4s.extensions.asset + +import com.azavea.stac4s.extensions.CollectionExtension + +import io.circe._ +import io.circe.syntax._ + +final case class AssetCollectionExtension( + assets: Map[String, StacCollectionAsset] +) + +object AssetCollectionExtension { + + implicit val encAssetCollectionExtension: Encoder.AsObject[AssetCollectionExtension] = Encoder + .AsObject[Map[String, Json]] + .contramapObject((extensionFields: AssetCollectionExtension) => Map("assets" -> extensionFields.assets.asJson)) + + implicit val decAssetCollectionExtension: Decoder[AssetCollectionExtension] = + Decoder.forProduct1("assets")(AssetCollectionExtension.apply) + + implicit val collectionExtension: CollectionExtension[AssetCollectionExtension] = + CollectionExtension.instance +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacCollectionAsset.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/StacCollectionAsset.scala similarity index 90% rename from modules/core/src/main/scala/com/azavea/stac4s/StacCollectionAsset.scala rename to modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/StacCollectionAsset.scala index 0dfe4256..71a26712 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/StacCollectionAsset.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/StacCollectionAsset.scala @@ -1,4 +1,6 @@ -package com.azavea.stac4s +package com.azavea.stac4s.extensions.asset + +import com.azavea.stac4s._ import cats.Eq import io.circe._ diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelExtensionProperties.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelItemExtension.scala similarity index 53% rename from modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelExtensionProperties.scala rename to modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelItemExtension.scala index 315df1c5..2907b009 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelExtensionProperties.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelItemExtension.scala @@ -1,10 +1,13 @@ package com.azavea.stac4s.extensions.label +import com.azavea.stac4s.extensions.ItemExtension + import cats.Eq import cats.implicits._ -import io.circe.{Decoder, Encoder, HCursor} +import io.circe.{Decoder, Encoder, HCursor, Json} +import io.circe.syntax._ -case class LabelExtensionProperties( +case class LabelItemExtension( properties: LabelProperties, classes: List[LabelClass], description: String, @@ -14,29 +17,23 @@ case class LabelExtensionProperties( overviews: List[LabelOverview] ) -object LabelExtensionProperties { +object LabelItemExtension { - implicit val encLabelExtensionProperties: Encoder[LabelExtensionProperties] = Encoder.forProduct7( - "label:properties", - "label:classes", - "label:description", - "label:type", - "label:tasks", - "label:methods", - "label:overviews" - )(extensionProps => - ( - extensionProps.properties, - extensionProps.classes, - extensionProps.description, - extensionProps._type, - extensionProps.tasks, - extensionProps.methods, - extensionProps.overviews + implicit val encLabelExtensionPropertiesObject: Encoder.AsObject[LabelItemExtension] = Encoder + .AsObject[Map[String, Json]] + .contramapObject((properties: LabelItemExtension) => + Map( + "label:properties" -> properties.properties.asJson, + "label:classes" -> properties.classes.asJson, + "label:description" -> properties.description.asJson, + "label:type" -> properties._type.asJson, + "label:tasks" -> properties.tasks.asJson, + "label:methods" -> properties.methods.asJson, + "label:overviews" -> properties.overviews.asJson + ) ) - ) - implicit val decLabelExtensionProperties: Decoder[LabelExtensionProperties] = new Decoder[LabelExtensionProperties] { + implicit val decLabelExtensionProperties: Decoder[LabelItemExtension] = new Decoder[LabelItemExtension] { def apply(c: HCursor) = ( @@ -57,7 +54,7 @@ object LabelExtensionProperties { methods: Option[List[LabelMethod]], overviews: Option[List[LabelOverview]] ) => - LabelExtensionProperties( + LabelItemExtension( properties, classes, description, @@ -69,5 +66,7 @@ object LabelExtensionProperties { ) } - implicit val eqLabelExtensionProperties: Eq[LabelExtensionProperties] = Eq.fromUniversalEquals + implicit val eqLabelExtensionProperties: Eq[LabelItemExtension] = Eq.fromUniversalEquals + + implicit val itemExtensionLabelProperties: ItemExtension[LabelItemExtension] = ItemExtension.instance } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelLinkExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelLinkExtension.scala new file mode 100644 index 00000000..c94531c5 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelLinkExtension.scala @@ -0,0 +1,21 @@ +package com.azavea.stac4s.extensions.label + +import com.azavea.stac4s.extensions.LinkExtension + +import cats.data.NonEmptyList +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ + +case class LabelLinkExtension(assets: NonEmptyList[String]) + +object LabelLinkExtension { + + implicit val encLabelLinkExtensionObject: Encoder.AsObject[LabelLinkExtension] = Encoder + .AsObject[Map[String, Json]] + .contramapObject((extensionFields: LabelLinkExtension) => Map("label:assets" -> extensionFields.assets.asJson)) + + implicit val decLabelLinkExtension: Decoder[LabelLinkExtension] = + Decoder.forProduct1("label:assets")(LabelLinkExtension.apply) + + implicit val linkExtension: LinkExtension[LabelLinkExtension] = LinkExtension.instance +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala new file mode 100644 index 00000000..53b3891c --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala @@ -0,0 +1,24 @@ +package com.azavea.stac4s.extensions.layer + +import com.azavea.stac4s.extensions.ItemExtension + +import cats.data.NonEmptyList +import cats.Eq +import eu.timepit.refined.types.string.NonEmptyString +import io.circe.{Decoder, Encoder} +import io.circe.refined._ +import io.circe.syntax._ + +case class LayerItemExtension(ids: NonEmptyList[NonEmptyString]) + +object LayerItemExtension { + implicit val eqLayerProperties: Eq[LayerItemExtension] = Eq.fromUniversalEquals + + implicit val encLayerProperties: Encoder.AsObject[LayerItemExtension] = + Encoder.AsObject.instance[LayerItemExtension] { o => Map("layer:ids" -> o.ids.asJson).asJsonObject } + + implicit val decLayerProperties: Decoder[LayerItemExtension] = + Decoder.forProduct1("layer:ids")(LayerItemExtension.apply) + + implicit val itemExtension: ItemExtension[LayerItemExtension] = ItemExtension.instance +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerProperties.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerProperties.scala deleted file mode 100644 index ac87b122..00000000 --- a/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerProperties.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.azavea.stac4s.extensions.layer - -import cats.Eq -import io.circe.{Decoder, Encoder} -import io.circe.syntax._ - -case class LayerProperties(ids: List[String]) - -object LayerProperties { - implicit val eqLayerProperties: Eq[LayerProperties] = Eq.fromUniversalEquals - - implicit val encLayerProperties: Encoder.AsObject[LayerProperties] = Encoder.AsObject.instance[LayerProperties] { o => - Map("layer:ids" -> o.ids.asJson).asJsonObject - } - - implicit val decLayerProperties: Decoder[LayerProperties] = Decoder[Map[String, List[String]]] emap { - _.get("layer:ids") match { - case Some(l) => Right(LayerProperties(l)) - case _ => Left("Could not decode LayerProperties.") - } - } -} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/package.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/package.scala new file mode 100644 index 00000000..18ef87b8 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/package.scala @@ -0,0 +1,13 @@ +package com.azavea.stac4s + +import cats.data.ValidatedNel +import io.circe.Error + +package object extensions { + + // convenience type not to have to write ValidatedNel in a few places / + // to expose a nicer API to users (a la MAML: + // https://github.com/geotrellis/maml/blob/713c6a0c54646d1972855bf5a1f0efddd108f95d/shared/src/main/scala/error/package.scala#L8) + type ExtensionResult[T] = ValidatedNel[Error, T] + +} diff --git a/modules/core/src/main/scala/com/azavea/stac4s/package.scala b/modules/core/src/main/scala/com/azavea/stac4s/package.scala index 71026161..d5548a5b 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/package.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/package.scala @@ -39,4 +39,11 @@ package object stac4s { implicit val eqTemporalExtent: Eq[TemporalExtent] = Eq.fromUniversalEquals + def substituteFieldName(fieldName: String): Option[String] = fieldName match { + case "_type" => Some("type") + case "stacVersion" => Some("stac_version") + case "stacExtensions" => Some("stac_extensions") + case "extensionFields" => None + case s => Some(s) + } } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/syntax/package.scala b/modules/core/src/main/scala/com/azavea/stac4s/syntax/package.scala new file mode 100644 index 00000000..f2b093b4 --- /dev/null +++ b/modules/core/src/main/scala/com/azavea/stac4s/syntax/package.scala @@ -0,0 +1,60 @@ +package com.azavea.stac4s + +import com.azavea.stac4s.extensions._ + +package object syntax { + + implicit class stacItemExtensions(item: StacItem) { + + def getExtensionFields[T](implicit ev: ItemExtension[T]): ExtensionResult[T] = + ev.getExtensionFields(item) + + def addExtensionFields[T](properties: T)(implicit ev: ItemExtension[T]): StacItem = + ev.addExtensionFields(item, properties) + } + + implicit class stacCollectionExtensions(collection: StacCollection) { + + def getExtensionFields[T](implicit ev: CollectionExtension[T]): ExtensionResult[T] = + ev.getExtensionFields(collection) + + def addExtensionFields[T](properties: T)(implicit ev: CollectionExtension[T]): StacCollection = + ev.addExtensionFields(collection, properties) + } + + implicit class stacCatalogExtensions(catalog: StacCatalog) { + + def getExtensionFields[T](implicit ev: CatalogExtension[T]): ExtensionResult[T] = + ev.getExtensionFields(catalog) + + def addExtensionFields[T](properties: T)(implicit ev: CatalogExtension[T]): StacCatalog = + ev.addExtensionFields(catalog, properties) + } + + implicit class stacItemAssetExtensions(itemAsset: StacItemAsset) { + + def getExtensionFields[T](implicit ev: ItemAssetExtension[T]): ExtensionResult[T] = + ev.getExtensionFields(itemAsset) + + def addExtensionFields[T](properties: T)(implicit ev: ItemAssetExtension[T]): StacItemAsset = + ev.addExtensionFields(itemAsset, properties) + } + + implicit class stacLinkExtensions(link: StacLink) { + + def getExtensionFields[T](implicit ev: LinkExtension[T]): ExtensionResult[T] = + ev.getExtensionFields(link) + + def addExtensionFields[T](properties: T)(implicit ev: LinkExtension[T]): StacLink = + ev.addExtensionFields(link, properties) + } + + implicit class stacItemCollectionExtensions(itemCollection: ItemCollection) { + + def getExtensionFields[T](implicit ev: ItemCollectionExtension[T]): ExtensionResult[T] = + ev.getExtensionFields(itemCollection) + + def addExtensionFields[T](properties: T)(implicit ev: ItemCollectionExtension[T]): ItemCollection = + ev.addExtensionFields(itemCollection, properties) + } +} diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json index 93db0fed..d4cc41b2 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json @@ -7,28 +7,23 @@ "links" : [ { "href" : "./catalog.json", - "rel" : "self", - "label:assets" : [] + "rel" : "self" }, { "href" : "./catalog.json", - "rel" : "root", - "label:assets" : [] + "rel" : "root" }, { "href" : "./landsat-8-l1/catalog.json", - "rel" : "child", - "label:assets" : [] + "rel" : "child" }, { "href" : "./layers/ca/catalog.json", - "rel" : "child", - "label:assets" : [] + "rel" : "child" }, { "href" : "./layers/us/catalog.json", - "rel" : "child", - "label:assets" : [] + "rel" : "child" } ] } diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json index 5261bbdd..7162167e 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json @@ -44,28 +44,20 @@ "links" : [ { "href" : "../../catalog.json", - "rel" : "root", - "label:assets" : [ - ] + "rel" : "root" }, { "href" : "../catalog.json", - "rel" : "parent", - "label:assets" : [ - ] + "rel" : "parent" }, { "href" : "./LC81530252014153LGN00.json", - "rel" : "self", - "label:assets" : [ - ] + "rel" : "self" }, { "href" : "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0", "rel" : "alternate", - "type" : "text/html", - "label:assets" : [ - ] + "type" : "text/html" } ], "assets" : { diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json index 5f7c3c3f..7fb70822 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json @@ -135,27 +135,19 @@ "links" : [ { "href" : "../../catalog.json", - "rel" : "root", - "label:assets" : [ - ] + "rel" : "root" }, { "href" : "../../catalog.json", - "rel" : "parent", - "label:assets" : [ - ] + "rel" : "parent" }, { "href" : "./catalog.json", - "rel" : "self", - "label:assets" : [ - ] + "rel" : "self" }, { "href" : "./2014-153/LC81530252014153LGN00.json", - "rel" : "item", - "label:assets" : [ - ] + "rel" : "item" } ] } diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json index 5119a764..4831425d 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json @@ -11,27 +11,19 @@ "links" : [ { "href" : "../catalog.json", - "rel" : "root", - "label:assets" : [ - ] + "rel" : "root" }, { "href" : "../catalog.json", - "rel" : "parent", - "label:assets" : [ - ] + "rel" : "parent" }, { "href" : "./catalog.json", - "rel" : "self", - "label:assets" : [ - ] + "rel" : "self" }, { "href" : "../../landsat-8-l1/2014-153/LC81530252014153LGN00.json", - "rel" : "item", - "label:assets" : [ - ] + "rel" : "item" } ] } diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json index 443e83d7..fb2e08cd 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json @@ -11,27 +11,19 @@ "links" : [ { "href" : "../catalog.json", - "rel" : "root", - "label:assets" : [ - ] + "rel" : "root" }, { "href" : "../catalog.json", - "rel" : "parent", - "label:assets" : [ - ] + "rel" : "parent" }, { "href" : "./catalog.json", - "rel" : "self", - "label:assets" : [ - ] + "rel" : "self" }, { "href" : "../../landsat-8-l1/2014-153/LC81530252014153LGN00.json", - "rel" : "item", - "label:assets" : [ - ] + "rel" : "item" } ] } diff --git a/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json index 23bf7e91..8ea41e9b 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json @@ -7,18 +7,15 @@ "links" : [ { "href" : "./catalog.json", - "rel" : "self", - "label:assets" : [] + "rel" : "self" }, { "href" : "./catalog.json", - "rel" : "root", - "label:assets" : [] + "rel" : "root" }, { "href" : "./landsat-8-l1/catalog.json", - "rel" : "child", - "label:assets" : [] + "rel" : "child" } ] } diff --git a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json index 5fee655a..0135797a 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json @@ -43,28 +43,20 @@ "links" : [ { "href" : "../../catalog.json", - "rel" : "root", - "label:assets" : [ - ] + "rel" : "root" }, { "href" : "../catalog.json", - "rel" : "parent", - "label:assets" : [ - ] + "rel" : "parent" }, { "href" : "./LC81530252014153LGN00.json", - "rel" : "self", - "label:assets" : [ - ] + "rel" : "self" }, { "href" : "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0", "rel" : "alternate", - "type" : "text/html", - "label:assets" : [ - ] + "type" : "text/html" } ], "assets" : { diff --git a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json index 5f7c3c3f..2d80fbaa 100644 --- a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json +++ b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json @@ -43,6 +43,7 @@ ] } }, + "summaries": {}, "properties" : { "collection" : "landsat-8-l1", "eo:instrument" : "OLI_TIRS", @@ -135,27 +136,19 @@ "links" : [ { "href" : "../../catalog.json", - "rel" : "root", - "label:assets" : [ - ] + "rel" : "root" }, { "href" : "../../catalog.json", - "rel" : "parent", - "label:assets" : [ - ] + "rel" : "parent" }, { "href" : "./catalog.json", - "rel" : "self", - "label:assets" : [ - ] + "rel" : "self" }, { "href" : "./2014-153/LC81530252014153LGN00.json", - "rel" : "item", - "label:assets" : [ - ] + "rel" : "item" } ] } diff --git a/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala index fb873bef..61799ac7 100644 --- a/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala +++ b/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala @@ -1,10 +1,12 @@ package com.azavea.stac4s +import com.azavea.stac4s.extensions.ItemExtension import com.azavea.stac4s.extensions.layer._ +import cats.data.NonEmptyList +import cats.implicits._ +import eu.timepit.refined.types.string.NonEmptyString +import geotrellis.vector.{io => _, _} import io.circe.syntax._ -import cats.syntax.either._ -import cats.syntax.option._ -import geotrellis.vector._ import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers @@ -26,41 +28,31 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "./catalog.json", rel = StacLinkType.Self, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ), StacLink( href = "./catalog.json", rel = StacLinkType.StacRoot, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ), StacLink( href = "./landsat-8-l1/catalog.json", rel = StacLinkType.Child, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ), StacLink( href = "./layers/ca/catalog.json", rel = StacLinkType.Child, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ), StacLink( href = "./layers/us/catalog.json", rel = StacLinkType.Child, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ) ) ) @@ -81,29 +73,25 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "../catalog.json", rel = StacLinkType.StacRoot, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "../catalog.json", rel = StacLinkType.Parent, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "./catalog.json", rel = StacLinkType.Self, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "../../landsat-8-l1/2014-153/LC81530252014153LGN00.json", rel = StacLinkType.Item, _type = None, - title = None, - labelExtAssets = Nil + title = None ) ) ) @@ -179,36 +167,34 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { "landsat:geometric_rmse_model_y" -> 4.654.asJson, "landsat:geometric_rmse_verify" -> 5.364.asJson, "landsat:image_quality_oli" -> 9.asJson - ).asJsonObject.deepMerge(LayerProperties(List(layerUS.id, layerCA.id)).asJsonObject), // layer extension + ).asJsonObject.deepMerge( + LayerItemExtension(NonEmptyList.of(layerUS.id, layerCA.id) map { NonEmptyString.unsafeFrom }).asJsonObject + ), // layer extension links = List( StacLink( href = "../../catalog.json", rel = StacLinkType.StacRoot, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "../catalog.json", rel = StacLinkType.Parent, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "./LC81530252014153LGN00.json", rel = StacLinkType.Self, _type = None, - title = None, - labelExtAssets = Nil + title = None ), // { "rel":"alternate", "href": "https://landsatonaws.com/L8/153/025/LC81530252014153LGN00", "type": "text/html"}, StacLink( href = "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0", rel = StacLinkType.Alternate, _type = `text/html`.some, - title = None, - labelExtAssets = Nil + title = None ) ), assets = Map( @@ -217,21 +203,21 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_thumb_large.jpg", title = "Thumbnail".some, description = "A medium sized thumbnail".some, - roles = List(StacAssetRole.Thumbnail), + roles = Set(StacAssetRole.Thumbnail), _type = `image/jpeg`.some ), "metadata" -> StacItemAsset( href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_MTL.txt", title = "Original Metadata".some, description = "The original MTL metadata file provided for each Landsat scene".some, - roles = List(StacAssetRole.Metadata), + roles = Set(StacAssetRole.Metadata), _type = VendorMediaType("mtl").some ), "B1" -> StacItemAsset( href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B1.TIF", title = "Coastal Band (B1)".some, description = "Coastal Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [0], ), @@ -239,7 +225,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B2.TIF", title = "Blue Band (B2)".some, description = "Blue Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [1], ), @@ -247,7 +233,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B3.TIF", title = "Green Band (B3)".some, description = "Green Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [2], ), @@ -255,7 +241,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B4.TIF", title = "Red Band (B4)".some, description = "Red Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [3], ), @@ -263,7 +249,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B5.TIF", title = "NIR Band (B5)".some, description = "NIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [4], ), @@ -271,7 +257,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B6.TIF", title = "SWIR (B6)".some, description = "SWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [5], ), @@ -279,7 +265,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B7.TIF", title = "SWIR Band (B7)".some, description = "SWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [6], ), @@ -287,7 +273,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B8.TIF", title = "Panchromatic Band (B8)".some, description = "Panchromatic Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [7], ), @@ -295,7 +281,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B9.TIF", title = "Cirrus Band (B9)".some, description = "Cirrus Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [8], ), @@ -303,7 +289,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B10.TIF", title = "LWIR Band (B10)".some, description = "LWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [9], ), @@ -311,7 +297,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B11.TIF", title = "LWIR Band (B11)".some, description = "LWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [10], ) @@ -319,6 +305,9 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers { collection = collection.id.some ) + ItemExtension[LayerItemExtension].getExtensionFields(item) shouldBe LayerItemExtension( + NonEmptyList.fromListUnsafe(List("layer-us", "layer-ca") map { NonEmptyString.unsafeFrom }) + ).valid item.asJson.deepDropNullValues shouldBe getJson( "/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json" ) diff --git a/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala index b9f234af..3c37e4c3 100644 --- a/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala +++ b/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala @@ -26,25 +26,19 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "./catalog.json", rel = StacLinkType.Self, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ), StacLink( href = "./catalog.json", rel = StacLinkType.StacRoot, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ), StacLink( href = "./landsat-8-l1/catalog.json", rel = StacLinkType.Child, _type = None, - title = None, - // should it be an optional thing? - labelExtAssets = Nil + title = None ) ) ) @@ -77,6 +71,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { List(TemporalExtent(Instant.parse("2013-06-01T00:56:49.001Z"), Instant.parse("2020-01-01T00:56:49.001Z"))) ) ), + summaries = ().asJsonObject, // properties can be anything // it is a part where extensions can be // at least EO, Label and potentially the layer extension @@ -174,29 +169,25 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "../../catalog.json", rel = StacLinkType.StacRoot, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "../../catalog.json", rel = StacLinkType.Parent, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "./catalog.json", rel = StacLinkType.Self, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "./2014-153/LC81530252014153LGN00.json", rel = StacLinkType.Item, _type = None, - title = None, - labelExtAssets = Nil + title = None ) ) ) @@ -267,30 +258,26 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "../../catalog.json", rel = StacLinkType.StacRoot, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "../catalog.json", rel = StacLinkType.Parent, _type = None, - title = None, - labelExtAssets = Nil + title = None ), StacLink( href = "./LC81530252014153LGN00.json", rel = StacLinkType.Self, _type = None, - title = None, - labelExtAssets = Nil + title = None ), // { "rel":"alternate", "href": "https://landsatonaws.com/L8/153/025/LC81530252014153LGN00", "type": "text/html"}, StacLink( href = "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0", rel = StacLinkType.Alternate, _type = `text/html`.some, - title = None, - labelExtAssets = Nil + title = None ) ), assets = Map( @@ -299,21 +286,21 @@ class CatalogSpec extends AnyFunSpec with Matchers { "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_thumb_large.jpg", title = "Thumbnail".some, description = "A medium sized thumbnail".some, - roles = List(StacAssetRole.Thumbnail), + roles = Set(StacAssetRole.Thumbnail), _type = `image/jpeg`.some ), "metadata" -> StacItemAsset( href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_MTL.txt", title = "Original Metadata".some, description = "The original MTL metadata file provided for each Landsat scene".some, - roles = List(StacAssetRole.Metadata), + roles = Set(StacAssetRole.Metadata), _type = VendorMediaType("mtl").some ), "B1" -> StacItemAsset( href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B1.TIF", title = "Coastal Band (B1)".some, description = "Coastal Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [0], ), @@ -321,7 +308,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B2.TIF", title = "Blue Band (B2)".some, description = "Blue Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [1], ), @@ -329,7 +316,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B3.TIF", title = "Green Band (B3)".some, description = "Green Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [2], ), @@ -337,7 +324,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B4.TIF", title = "Red Band (B4)".some, description = "Red Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [3], ), @@ -345,7 +332,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B5.TIF", title = "NIR Band (B5)".some, description = "NIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [4], ), @@ -353,7 +340,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B6.TIF", title = "SWIR (B6)".some, description = "SWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [5], ), @@ -361,7 +348,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B7.TIF", title = "SWIR Band (B7)".some, description = "SWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [6], ), @@ -369,7 +356,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B8.TIF", title = "Panchromatic Band (B8)".some, description = "Panchromatic Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [7], ), @@ -377,7 +364,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B9.TIF", title = "Cirrus Band (B9)".some, description = "Cirrus Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [8], ), @@ -385,7 +372,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B10.TIF", title = "LWIR Band (B10)".some, description = "LWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [9], ), @@ -393,7 +380,7 @@ class CatalogSpec extends AnyFunSpec with Matchers { href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B11.TIF", title = "LWIR Band (B11)".some, description = "LWIR Band Top Of the Atmosphere".some, - roles = Nil, + roles = Set.empty, _type = `image/tiff`.some // "eo:bands": [10], ) diff --git a/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala b/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala index 9857b9ee..e29fa7a8 100644 --- a/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala +++ b/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala @@ -1,10 +1,12 @@ package com.azavea.stac4s import com.azavea.stac4s.extensions.label._ +import com.azavea.stac4s.extensions.asset._ import cats.data.NonEmptyList import cats.implicits._ import geotrellis.vector.{Geometry, Point, Polygon} import io.circe.JsonObject +import io.circe.syntax._ import org.scalacheck._ import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.cats.implicits._ @@ -13,7 +15,8 @@ import java.time.Instant import com.github.tbouron.SpdxLicense import com.azavea.stac4s.extensions.label.LabelClassClasses.NamedLabelClasses import com.azavea.stac4s.extensions.label.LabelClassClasses.NumberedLabelClasses -import com.azavea.stac4s.extensions.layer.LayerProperties +import com.azavea.stac4s.extensions.layer.LayerItemExtension +import eu.timepit.refined.types.string.NonEmptyString object Generators { @@ -36,6 +39,35 @@ object Generators { private def instantGen: Gen[Instant] = arbitrary[Int] map { x => Instant.now.plusMillis(x.toLong) } + private def assetCollectionExtensionGen: Gen[AssetCollectionExtension] = + Gen + .mapOf( + (nonEmptyStringGen, stacCollectionAssetGen).tupled + ) + .map(AssetCollectionExtension.apply) + + private def collectionExtensionFieldsGen: Gen[JsonObject] = Gen.oneOf( + Gen.const(().asJsonObject), + assetCollectionExtensionGen.map(_.asJsonObject) + ) + + private def itemExtensionFieldsGen: Gen[JsonObject] = Gen.oneOf( + Gen.const(().asJsonObject), + labelExtensionPropertiesGen map { _.asJsonObject }, + layerPropertiesGen map { _.asJsonObject } + ) + + private def labelLinkExtensionGen: Gen[LabelLinkExtension] = + Gen + .nonEmptyListOf(nonEmptyStringGen) + .map(NonEmptyList.fromListUnsafe) + .map(LabelLinkExtension.apply) + + private def linkExtensionFields: Gen[JsonObject] = Gen.oneOf( + Gen.const(().asJsonObject), + labelLinkExtensionGen.map(_.asJsonObject) + ) + private def mediaTypeGen: Gen[StacMediaType] = Gen.oneOf( `image/tiff`, `image/vnd.stac.geotiff`, @@ -124,7 +156,7 @@ object Generators { Gen.const(StacLinkType.Self), // self link type is required by TMS reification Gen.option(mediaTypeGen), Gen.option(nonEmptyStringGen), - Gen.nonEmptyListOf[String](arbitrary[String]) + linkExtensionFields ).mapN(StacLink.apply) private def temporalExtentGen: Gen[TemporalExtent] = { @@ -154,8 +186,9 @@ object Generators { nonEmptyStringGen, Gen.option(nonEmptyStringGen), Gen.option(nonEmptyStringGen), - Gen.containerOf[Set, StacAssetRole](assetRoleGen) map { _.toList }, - Gen.option(mediaTypeGen) + Gen.containerOf[Set, StacAssetRole](assetRoleGen), + Gen.option(mediaTypeGen), + Gen.const(().asJsonObject) ) mapN { StacItemAsset.apply } @@ -186,7 +219,7 @@ object Generators { Gen.nonEmptyListOf(stacLinkGen), Gen.nonEmptyMap((nonEmptyStringGen, cogAssetGen).tupled), Gen.option(nonEmptyStringGen), - Gen.const(JsonObject.fromMap(Map.empty)) + itemExtensionFieldsGen ).mapN(StacItem.apply) private def stacCatalogGen: Gen[StacCatalog] = @@ -196,7 +229,8 @@ object Generators { nonEmptyStringGen, Gen.option(nonEmptyStringGen), nonEmptyStringGen, - Gen.listOf(stacLinkGen) + Gen.listOf(stacLinkGen), + Gen.const(().asJsonObject) ).mapN(StacCatalog.apply) private def stacCollectionGen: Gen[StacCollection] = @@ -210,8 +244,10 @@ object Generators { stacLicenseGen, Gen.listOf(stacProviderGen), stacExtentGen, + Gen.const(().asJsonObject), Gen.const(JsonObject.fromMap(Map.empty)), - Gen.listOf(stacLinkGen) + Gen.listOf(stacLinkGen), + collectionExtensionFieldsGen ).mapN(StacCollection.apply) private def itemCollectionGen: Gen[ItemCollection] = @@ -220,7 +256,8 @@ object Generators { Gen.const(StacVersion.unsafeFrom("0.9.0")), Gen.const(Nil), Gen.listOf[StacItem](stacItemGen), - Gen.listOf[StacLink](stacLinkGen) + Gen.listOf[StacLink](stacLinkGen), + Gen.const(().asJsonObject) ).mapN(ItemCollection.apply) private def labelClassNameGen: Gen[LabelClassName] = @@ -304,10 +341,12 @@ object Generators { private def labelPropertiesGen: Gen[LabelProperties] = Gen.option(Gen.listOf(nonEmptyStringGen)).map(LabelProperties.fromOption) - private def layerPropertiesGen: Gen[LayerProperties] = - Gen.listOf(nonEmptyStringGen).map(LayerProperties.apply) + private def layerPropertiesGen: Gen[LayerItemExtension] = + Gen + .nonEmptyListOf(nonEmptyStringGen) + .map(layerIds => LayerItemExtension(NonEmptyList.fromListUnsafe(layerIds map { NonEmptyString.unsafeFrom }))) - private def labelExtensionPropertiesGen: Gen[LabelExtensionProperties] = + private def labelExtensionPropertiesGen: Gen[LabelItemExtension] = ( labelPropertiesGen, Gen.listOf(labelClassGen), @@ -316,7 +355,7 @@ object Generators { Gen.listOf(labelTaskGen), Gen.listOf(labelMethodGen), Gen.listOf(labelOverviewGen) - ).mapN(LabelExtensionProperties.apply) + ).mapN(LabelItemExtension.apply) implicit val arbMediaType: Arbitrary[StacMediaType] = Arbitrary { mediaTypeGen @@ -374,6 +413,10 @@ object Generators { assetRoleGen } + implicit val arbStacLink: Arbitrary[StacLink] = Arbitrary { + stacLinkGen + } + implicit val arbLabelClassName: Arbitrary[LabelClassName] = Arbitrary { labelClassNameGen } implicit val arbLabelClassClasses: Arbitrary[LabelClassClasses] = Arbitrary { labelClassClassesGen } @@ -394,9 +437,17 @@ object Generators { implicit val arbLabelProperties: Arbitrary[LabelProperties] = Arbitrary { labelPropertiesGen } - implicit val arbLabelExtensionProperties: Arbitrary[LabelExtensionProperties] = Arbitrary { + implicit val arbLabelExtensionProperties: Arbitrary[LabelItemExtension] = Arbitrary { labelExtensionPropertiesGen } - implicit val arbLayerProperties: Arbitrary[LayerProperties] = Arbitrary { layerPropertiesGen } + implicit val arbLabelLinkExtension: Arbitrary[LabelLinkExtension] = Arbitrary { + labelLinkExtensionGen + } + + implicit val arbLayerProperties: Arbitrary[LayerItemExtension] = Arbitrary { layerPropertiesGen } + + implicit val arbAssetExtensionProperties: Arbitrary[AssetCollectionExtension] = Arbitrary { + assetCollectionExtensionGen + } } diff --git a/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala index d386d541..0e956d1b 100644 --- a/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala +++ b/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala @@ -1,9 +1,11 @@ package com.azavea.stac4s +import com.azavea.stac4s.extensions.asset._ import com.azavea.stac4s.extensions.label._ import com.azavea.stac4s.meta._ import Generators._ import geotrellis.vector.Geometry +import io.circe.syntax._ import io.circe.parser._ import io.circe.testing.{ArbitraryInstances, CodecTests} import org.scalatest.funsuite.AnyFunSuite @@ -12,7 +14,7 @@ import org.scalatestplus.scalacheck.Checkers import org.typelevel.discipline.scalatest.FunSuiteDiscipline import java.time.Instant -import com.azavea.stac4s.extensions.layer.LayerProperties +import com.azavea.stac4s.extensions.layer.LayerItemExtension class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with Matchers with ArbitraryInstances { @@ -43,7 +45,7 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M checkAll("Codec.LabelClassClasses", CodecTests[LabelClassClasses].unserializableCodec) checkAll("Codec.LabelClassName", CodecTests[LabelClassName].unserializableCodec) checkAll("Codec.LabelCount", CodecTests[LabelCount].unserializableCodec) - checkAll("Codec.LabelExtensionProperties", CodecTests[LabelExtensionProperties].unserializableCodec) + checkAll("Codec.LabelExtensionProperties", CodecTests[LabelItemExtension].unserializableCodec) checkAll("Codec.LabelMethod", CodecTests[LabelMethod].unserializableCodec) checkAll("Codec.LabelOverview", CodecTests[LabelOverview].unserializableCodec) checkAll("Codec.LabelProperties", CodecTests[LabelProperties].unserializableCodec) @@ -52,12 +54,12 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M checkAll("Codec.LabelType", CodecTests[LabelType].unserializableCodec) // Layer extension - checkAll("Codec.LayerProperties", CodecTests[LayerProperties].unserializableCodec) + checkAll("Codec.LayerProperties", CodecTests[LayerItemExtension].unserializableCodec) // unit tests test("ignore optional fields") { val link = decode[StacLink]("""{"href":"s3://foo/item.json","rel":"item"}""") - link map { _.labelExtAssets } shouldBe Right(List.empty[String]) + link map { _.extensionFields } shouldBe Right(().asJsonObject) } } diff --git a/modules/core/src/test/scala/com/azavea/stac4s/SyntaxSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/SyntaxSpec.scala new file mode 100644 index 00000000..7e5368e5 --- /dev/null +++ b/modules/core/src/test/scala/com/azavea/stac4s/SyntaxSpec.scala @@ -0,0 +1,60 @@ +package com.azavea.stac4s + +import com.azavea.stac4s.syntax._ +import com.azavea.stac4s.extensions._ +import com.azavea.stac4s.extensions.asset._ +import com.azavea.stac4s.extensions.label._ +import Generators._ +import cats.implicits._ +import io.circe.syntax._ +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import org.scalatestplus.scalacheck.Checkers + +class SyntaxSpec extends AnyFunSuite with Checkers with Matchers { + test("item syntax results in the same values as typeclass summoner to extend") { + check { (item: StacItem, labelExtension: LabelItemExtension) => + item.addExtensionFields(labelExtension) == ItemExtension[LabelItemExtension] + .addExtensionFields(item, labelExtension) + } + } + + test("item syntax results in the same values as typeclass summoner to parse") { + check { (item: StacItem, labelExtension: LabelItemExtension) => + item.addExtensionFields(labelExtension).getExtensionFields[LabelItemExtension] == + labelExtension.valid + } + } + + test("collection syntax results in the same values as typeclass summoner to extend") { + check { (collection: StacCollection, assetExtension: AssetCollectionExtension) => + collection.addExtensionFields(assetExtension) == CollectionExtension[AssetCollectionExtension] + .addExtensionFields(collection, assetExtension) + } + } + + test("collection syntax results in the same values as typeclass summoner to parse") { + check { (collection: StacCollection, assetExtension: AssetCollectionExtension) => + // We have to nuke existing extensions, because otherwise the collection can start with + // some assets in its asset extension, in which case the decoded result will have extra + // data + val withoutExtensions = collection.copy(extensionFields = ().asJsonObject) + withoutExtensions + .addExtensionFields(assetExtension) + .getExtensionFields[AssetCollectionExtension] == assetExtension.valid + } + } + + test("link syntax results in the same values as typeclass summoner to extend") { + check { (stacLink: StacLink, labelLinkExtension: LabelLinkExtension) => + stacLink.addExtensionFields(labelLinkExtension) == LinkExtension[LabelLinkExtension] + .addExtensionFields(stacLink, labelLinkExtension) + } + } + + test("link syntax results in the same values as typeclass summoner to parse") { + check { (stacLink: StacLink, labelLinkExtension: LabelLinkExtension) => + stacLink.addExtensionFields(labelLinkExtension).getExtensionFields[LabelLinkExtension] == labelLinkExtension.valid + } + } +}