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

[Draft] oneOf support for query strings #1018

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

GrafBlutwurst
Copy link
Contributor

As mentioned in gitter this tackles the oneOf support for query string. But I'll need some help with a couple bits.

  1. OpenApi Generation: I am unsure what the correct way to flatten out nested options is.
  2. Testing. I haven't quite found in which endpoints definition to hook this in (besides openapi) to make sure it works for all the implementations
  3. Sealed Trait encodings? Is there a sensible way to have those in headers or is it strictly enums? Currently the encoding doesn't stop you from doing it since it's a simple "find first success" decoding strategy. So it has no tagging support or anything like that. I am unsure what exactly we need here.

Formatting and cleaning is not done yet as it's still a draft.

I also added a possible encoding of Alternative and anolog to Tupler that could help people have less issues with heavily nested eithers. If we decide to retain this I'll probably use it for a bit more sizable test for query string enums.

@GrafBlutwurst GrafBlutwurst marked this pull request as draft February 28, 2022 12:21
@GrafBlutwurst
Copy link
Contributor Author

In particular what is going to need some design-elbow-grease is the question if we want to do this fallback strategy.

Right now there's nothing stopping you from doing something like this:

sealed trait Foo
case class Bar(value:String) extends Foo
case class Baz(value:String, value2:Int) extends Foo

Note that in terms of structural subtyping (depending on the particular encoding you choose for the case classes) we have Baz <: Bar so with a simple fallback decoding, without type tagging, assuming we try Bar first, any valid value of Foo will be decoded to Bar. This is quite the pitfall (though arguable you probably shouldn't encode something like this in a query string anyway). But the question remains how we want to deal with. If we want this to be as safe as JsonSchema we'll need an algebra of similar expressive power I think.

@codecov
Copy link

codecov bot commented Feb 28, 2022

Codecov Report

Merging #1018 (99d6859) into master (02b9c48) will decrease coverage by 0.79%.
The diff coverage is 18.84%.

❗ Current head 99d6859 differs from pull request most recent head 1c18d83. Consider uploading reports for the commit 1c18d83 to get more accurate results

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1018      +/-   ##
==========================================
- Coverage   75.71%   74.92%   -0.80%     
==========================================
  Files         144      145       +1     
  Lines        4756     4825      +69     
  Branches       56       50       -6     
==========================================
+ Hits         3601     3615      +14     
- Misses       1155     1210      +55     
Impacted Files Coverage Δ
.../main/scala/endpoints4s/akkahttp/client/Urls.scala 94.73% <0.00%> (-5.27%) ⬇️
.../main/scala/endpoints4s/akkahttp/server/Urls.scala 84.09% <0.00%> (-7.27%) ⬇️
...rc/main/scala/endpoints4s/http4s/client/Urls.scala 93.10% <0.00%> (-6.90%) ⬇️
...rc/main/scala/endpoints4s/http4s/server/Urls.scala 90.47% <0.00%> (-9.53%) ⬇️
...chema/src/main/scala/endpoints4s/Alternative.scala 0.00% <0.00%> (ø)
.../src/main/scala/endpoints4s/sttp/client/Urls.scala 97.29% <0.00%> (-2.71%) ⬇️
...napi/src/main/scala/endpoints4s/openapi/Urls.scala 87.87% <85.71%> (-0.59%) ⬇️
...ebra/src/main/scala/endpoints4s/algebra/Urls.scala 93.75% <100.00%> (+0.41%) ⬆️
...src/main/scala/endpoints4s/openapi/Endpoints.scala 100.00% <0.00%> (+1.92%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 02b9c48...1c18d83. Read the comment docs.

Copy link
Member

@julienrf julienrf left a comment

Choose a reason for hiding this comment

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

Thank you @GrafBlutwurst.

May I ask you a bit more details about your use cases? It might be easier to support a special case of oneOf: the case of enumerations, where the type of all the alternatives is the same, but the set of valid values is a fixed subset of the possible values for that type.

Regarding the tests, the current approach consists in adding them here for server interpreters and here for client interpreters.

@@ -83,6 +83,32 @@ trait Urls extends algebra.Urls {
)
type QueryStringParam[A] = DocumentedQueryStringParam[A]

//TODO: This is kinda ugly. But discriminated schemas make little sense on query strings?
//Stil maybe this should be done better, but I haven't yet fully understood the openapi schema to figure out how to deal with nesting here
private def unpackSchema[A](qsp:QueryStringParam[A]):List[Schema] = qsp.schema match {
Copy link
Member

Choose a reason for hiding this comment

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

Indeed, this is a signal that tells us that something is wrong with our approach. Most likely we need an intermediate structure somewhere to avoid this.

example = None, //TODO: What to put here?
title = None,//TODO: What to put here?
),
isRequired = qspa.isRequired || qspb.isRequired, //TODO: This feels wrong?
Copy link
Member

Choose a reason for hiding this comment

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

Here too, this is a signal that the signature of oneOfQueryStringParam is wrong, or that the type of QueryStringParam is wrong in the openapi interpreter (maybe it should be something else than the current DocumentedQueryStringParam).

@GrafBlutwurst
Copy link
Contributor Author

In most cases the usecase is really just straight up enums. So in particular we have a search endpoint where we'd like to have the search parameters in the URL so that this search can easily be bookmarked or shared by users.

Most of the parameters are e.g. TYPE=x where x oneOf (A,B,C) so fixed values enumerations. That should be easily doable in a way that also lends itself to generating openapi spec (which is what we're very interested in).

The other much harder case is e.g. DATE=IS_BETWEEN(lower, upper) where lower and upper are ISO date strings. And here is the scenario where you'd have a sealed trait that is decoded from a query string because DATE=IS_NULL is also fine for us. I'm not even sure if it's possible to have this automatically derived in openapi.

Right now we just work with a stringly types query string that we xmapPartial but that suppresses any structural info on the openapi

@julienrf
Copy link
Member

Most of the parameters are e.g. TYPE=x where x oneOf (A,B,C) so fixed values enumerations. That should be easily doable in a way that also lends itself to generating openapi spec (which is what we're very interested in).

For that use case, an approach similar to enumeration in JsonSchemas should work. What do you think?

The other much harder case is e.g. DATE=IS_BETWEEN(lower, upper) where lower and upper are ISO date strings. And here is the scenario where you'd have a sealed trait that is decoded from a query string because DATE=IS_NULL is also fine for us. I'm not even sure if it's possible to have this automatically derived in openapi.

Here, I am not even sure there is a way to express that in JSON Schema. It seems that “ranges” are supported for numeric values only. I think the only solution is to indicate in the description of the query parameter what are the validation constraints.

@GrafBlutwurst
Copy link
Contributor Author

I think you're right. I'll probably start from scratch again and restrict it to "true" enums. Maybe it would be interesting to explore to "derive" documentation for query params that actually carry data (so like the DATE) In our case we have in the backend a filter ADT that we decode from the query strings in an xmap. My current idea is to use the structure of that ADT to dervive the documentation text block explaining what goes into each possibility.

So I think I'll restrict this MR purely to enums and then internally develop the documentation block derivation, if it pans out I'll upstream it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants