Skip to content
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

Merged
merged 26 commits into from
May 19, 2020
Merged

Extension typeclasses #85

merged 26 commits into from
May 19, 2020

Conversation

jisantuc
Copy link
Contributor

@jisantuc jisantuc commented May 14, 2020

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 with instance constructors for the typeclasses. 🤷‍♂️ It's also failing because I did something wrong with collections. nevermind!

Checklist

  • New tests have been added or existing tests have been modified
  • Changelog updated

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
  • check this out as an example of how this works:
@ import org.scalacheck.Arbitrary.arbitrary
@ import com.azavea.stac4s._
@ import com.azavea.stac4s.Generators._
@ import io.circe.syntax._
@ val collection = arbitrary[StacCollection].sample.get
@ collection.extensionFields // if you got an empty json object, sample and reassign
@ colleciton.asJson.spaces2 // check out your assets as a normal top level field

Closes #82

Closes #81

Closes geotrellis/geotrellis-server#152

@jisantuc jisantuc changed the title [wip] Extension typeclasses Extension typeclasses May 15, 2020
Copy link
Contributor Author

@jisantuc jisantuc left a 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)
Copy link
Contributor Author

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]
Copy link
Contributor Author

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
Copy link
Contributor Author

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 StacItems 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 {
Copy link
Contributor Author

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(
Copy link
Contributor Author

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
Copy link
Contributor Author

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

Comment on lines 41 to 58
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)
)
Copy link
Contributor Author

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

Copy link

@notthatbreezy notthatbreezy left a 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(

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?

Copy link
Contributor Author

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.

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.

Copy link
Contributor Author

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

Copy link
Collaborator

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?

Copy link
Contributor Author

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

Copy link
Collaborator

@pomadchin pomadchin left a 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 👍

  1. 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)
  2. What do you think about adding the com.azavea.stac4s.syntax package? Without syntax 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]

itemCollection.extensionFields.asJson.hcursor
)

def extend(itemCollection: ItemCollection, extensionProperties: T) =
Copy link
Collaborator

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.

Copy link
Contributor Author

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

@jisantuc
Copy link
Contributor Author

syntax package added in 36721b3. I believe that's all the comments so I'm re-requesting reviews

Comment on lines +22 to +25
private val generic = LabelledGeneric[ItemCollection]
private val keys = Keys[generic.Repr].apply
val itemCollectionFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet

Copy link
Collaborator

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #89

Copy link

@notthatbreezy notthatbreezy left a 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.

@jisantuc jisantuc merged commit 7adf740 into master May 19, 2020
@jisantuc jisantuc deleted the feature/js/extension-typeclasses branch May 19, 2020 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants