-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extension typeclasses #85
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wound up a lot bigger than it felt like, so here are some explanatory comments. Also I didn't test the label items extension because I'd have to write a lot of generators, but I'm happy to tack that on -- I'm at 6 hours on a 2 point card + a 1 point card, so there's time for sure
) | ||
) | ||
|
||
baseEncoder(collection).deepMerge(collection.extensionFields.asJson) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the first big increase in verbosity-- everything with extensionFields
has a baseEncoder
that gets mashed together with whatever's in the extensionFields
attribute
c.get[Option[List[String]]]("stac_extensions"), | ||
c.get[List[StacItem]]("features"), | ||
c.get[Option[List[StacLink]]]("links"), | ||
c.value.as[JsonObject] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the second part of the big increase in verbosity -- derivation is no longer correct for any of these, since we need "root document without the vanilla fields"
import io.circe._ | ||
import io.circe.syntax._ | ||
|
||
// typeclass trait for anything that is an extension of item properties |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was the first one I wrote, so it's where the comments are, even though it's not first in the file order. It's pretty basic, since StacItem
s don't have extensionFields
, they just toss stuff into properties
, but it explains the general strategy.
def extend(link: StacLink, extensionFields: T): StacLink | ||
} | ||
|
||
object LinkExtension { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the simplest example with extensionFields
import io.circe._ | ||
import io.circe.syntax._ | ||
|
||
final case class AssetCollectionExtension( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out we'd implemented the asset
extension? I don't know why we did that. But I moved it to the appropriate zone. We could consider moving implemented extensions into another module, or a module per extension or something? I don't think the second one is great because there are tons of extensions but the first one might be reasonable
@@ -1,4 +1,6 @@ | |||
package com.azavea.stac4s | |||
package com.azavea.stac4s.extensions.asset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved because it's an extension
modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json
Show resolved
Hide resolved
private def collectionExtensionFieldsGen: Gen[JsonObject] = Gen.oneOf( | ||
Gen.const(().asJsonObject), | ||
Gen | ||
.mapOf( | ||
(nonEmptyStringGen, stacCollectionAssetGen).tupled | ||
) | ||
.map(AssetCollectionExtension.apply) | ||
.map(_.asJsonObject) | ||
) | ||
|
||
private def linkExtensionFields: Gen[JsonObject] = Gen.oneOf( | ||
Gen.const(().asJsonObject), | ||
Gen | ||
.nonEmptyListOf(nonEmptyStringGen) | ||
.map(NonEmptyList.fromListUnsafe) | ||
.map(LabelLinkExtension.apply) | ||
.map(_.asJsonObject) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by adding these generators, we can ensure that with and without the extension fields, we can still roundtrip
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks great - the comments are helpful and it looks like a good strategy. There are a few minor comments but nothing major. The comment about changing asset roles to a set
can be left for a new issue if there are downstream consequences that make it more than a simple switch.
I think if you still have time to write tests for the label extension(s) that would be good since we'll want to encourage writing tests for extensions as we go forward.
) | ||
|
||
object StacCatalog { | ||
|
||
val catalogFields = Set( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definition of "fields" seems like the riskiest part of this strategy. Do you think there is a way to write tests that would catch incorrectly adding or removing fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not hard to ensure that no fields from the case class are also keys in the extensionFields
json object -- we can use LabelledGeneric
from shapeless to extract the field names, and I think we can just map _type
=> type
in the names and end up with something that mostly works. I'm trying to think of whether this is a terrible idea for some reason I haven't thought of yet. I have end-of-day brain though so I'll think about it again tomorrow morning.
I actually ran into this issue while working on this PR -- I'd forgotten the filter
in collection and had about a half hour of trouble figuring out what was wrong with my collection json round trips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of using LabelledGeneric
a bunch - seems like something that will be useful to keep the strings in sync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ade5ae9 -- I tried out the generic strategy in the linked blog post but had trouble with the implicits, so we have a less generic generics strategy here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jisantuc what was decided btw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're used in the linked commit
modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json
Show resolved
Hide resolved
modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala
Outdated
Show resolved
Hide resolved
modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM in general; I left a couple of minor comments. And I like the general idea 👍
- I think that it would be really very useful to add some usage examples into CatalogLayer spec (it is the only "real" spec and also the only simple usage example of stac4s)
- What do you think about adding the
com.azavea.stac4s.syntax
package? Withoutsyntax
it is not very obvious how to use it.
// looks really complicated
ItemExtension[LayerItemExtension].getProperties(item)
How about smth like
item.getProperties[LayerItemExtension]
item.addProperties[T]
// or smth like
item.getPropertyExtension[T]
modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala
Outdated
Show resolved
Hide resolved
modules/core/src/main/scala/com/azavea/stac4s/extensions/CatalogExtension.scala
Outdated
Show resolved
Hide resolved
itemCollection.extensionFields.asJson.hcursor | ||
) | ||
|
||
def extend(itemCollection: ItemCollection, extensionProperties: T) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing return type in a public method here, can be nice to have it for users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding, but since this is evidence for a typeclass defined like 12 lines up... is it really missing? I'm curious about the custom here. extend
has a required return type from the trait
modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala
Outdated
Show resolved
Hide resolved
|
private val generic = LabelledGeneric[ItemCollection] | ||
private val keys = Keys[generic.Repr].apply | ||
val itemCollectionFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we put it into a single function to reduce some boilerplate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the thing I had trouble with implicits with -- it wasn't obvious to me how to get the implicit evidence required to have getFieldNames[T]
cooperate. Specifically, the evidence provided in the example blog post:
implicit def toAttributes[T, Repr <: HList, KeysRepr <: HList](
implicit gen: LabelledGeneric.Aux[T, Repr],
keys: Keys.Aux[Repr, KeysRepr],
traversable: ToTraversable.Aux[KeysRepr, List, Symbol]
):
was insufficient for Keys[generic.Repr]
when I tried making it generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened #89
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me - I like @pomadchin's suggestion to reduce boilerplate, but that change wouldn't require another review IMO.
Overview
This PR adds typeclasses for extending each kind of STAC object. It also updates the case classes for each type of STAC object to include
extensionFields
, since otherwise it was gonna be really hard to write the companion objects withinstance
constructors for the typeclasses. 🤷♂️It's also failing because I did something wrong with collections.nevermind!Checklist
Notes
There's a bit of a tradeoff here between verbosity of encoders / decoders for the base types and the convenience of extending them. I think it's a good trade, since the base types shouldn't change much, but we want to add a lot of extensions, but someone could argue otherwise.
Testing
bloop console core-test
Closes #82
Closes #81
Closes geotrellis/geotrellis-server#152