-
-
Notifications
You must be signed in to change notification settings - Fork 6.7k
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
Improve sttpOpenApiClient generator #6684
Conversation
- remove apiModel - do not preinline response type - define sbt version - replace ApiRequestT with Request - update sttp version
72247e9
to
4588362
Compare
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.
@ghostbuster91 thanks for the PR
Had no chance to test compiled changes yet, but generally there is a lot of good improvements. Thanks
I leaved some comments and will try to check compiled client soon.
modules/openapi-generator/src/main/resources/scala-sttp/build.sbt.mustache
Show resolved
Hide resolved
|
||
@Override | ||
public void updateAdditionalProperties(Map<String, Object> additionalProperties) { | ||
if (additionalProperties.containsKey(name)) { |
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.
seems like an error as mainPackage
is missed here.
also when you fix this next scenario will probably failed:
"--mainPackage=my.custom.package&--apiPackage=my.custom.api.different.package"
because Map can have those properties in unwanted order
Could you please cover this method with unit tests?
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.
Hmm, this is definitively a good idea to cover it with some tests
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 isn't an error since the mainPackage
property isn't used anywhere. All packages are specified by apiPackage
, modelPackage
and invokerPackage
properties
importMapping.remove("Set"); | ||
importMapping.remove("Map"); | ||
|
||
typeMapping = new HashMap<>(); |
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.
some of those imports and mappings already exists in AbstractScalaCodegen
. Is it possible to reuse them instead of overwrite with the same values?
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.
Actually there are not the same. In AbstractScalaCodegen
only importMappings
are defined, while here we remove some of them and redefine them as typeMappings
or instantiationTypes
.
samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/PetApi.scala
Outdated
Show resolved
Hide resolved
*/ | ||
implicit class ApiRequestImprovements[R[_], T](request: RequestT[Identity, Either[ResponseError[Exception], T], Nothing]) { | ||
|
||
def result(implicit backend: SttpBackend[R, Nothing, Nothing]): R[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.
Could we have kinda helper to get final results? Otherwise it could lead code duplication in applications or be implemented there
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.
The way sttp-client is normally used (at least, what we've seen on gitter, issue reports etc.) is that people simply call .send()
with an implicit backend in scope. What kind of functionality would the helper add?
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 that helper can be on the app side as error transformations can be different from app to app.
The main idea of that helper is to get successful result directly. For example for Try
framework it could be something like
val pet: Pet = new PetApi("http://swagger").getPetById(1).result()
instead of code like next for every method
new PetApi("http://swagger").getPetById(1).send()
.flatMap {
case Right(pet) => pet
doSomething(pet)
case ...
}
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.
For that purpose we've introduced the separateErrorChannel
option: using sttp's mechanism (response specifications), requests can be modeled to return full error information (so the response will be roughly an Either[Error, Success]
), or the errors can be represented as failed effects (as the errors are Throwables
).
Of course people might want to generate code using full error information and then flatten the result anyway. Though here, as you write, it's quite an application-specific thing. I think if I wanted to flatten the results, I'd write an extension method, which would convert F[Either[Error, Success]]
into a F[Success]
, e.g.:
new PetApi("http://swagger").getPetById(1).send().flattenErrors
where
implicit class TryOps[E <: Throwable, S](r: Try[Either[E, S]]) {
def flattenErrors: Try[S] = r.flatMap {
case Left(e) => Failed(e)
case Right(s) => Success(s)
}
}
but I think this would belong to the specific app - not in the generated code. Otherwise we should have this in sttp directly :)
.body({{paramName}}){{/bodyParam}} | ||
.response(asJson[{{>operationReturnType}}]) | ||
.response({{#separateErrorChannel}}asJson{{/separateErrorChannel}}{{^separateErrorChannel}}asJsonAlwaysUnsafe{{/separateErrorChannel}}[{{>operationReturnType}}]) |
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.
Looks good. Could you help to separate api errors as exceptions as described here softwaremill/sttp-model#5 ? The main idea is to split application exceptions (ideally with models) from transport level exceptions by using expected status codes.
Thanks
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.
Transport exceptions are already modeled using SttpClientException
. These exceptions are thrown when read or connection exceptions occur, and result in a failed effect (failed Future
or IO
).
Though you are right that there's a piece missing here, if the OpenAPI description contains an e.g. JSON representation of an error? Then yes, we will need a custom hierarchy (as opposed to the standard ResponseError
), covering three possibilities:
DeserializationError[T](body: String, error: T)
- if deserializing the success or error json failsUnknownHttpError(body: String)
- for unmapped status codesHttpError[T](body: T)
- for mapped status codes
Then, the overall request type would be: Request[Either[ResponseError[T, U], V]]
, where:
T
is the type of deserialization errors, e.g.io.circe.Error
U
is the type of the http errors - but as there might be a couple of these, probably this needs to be a sealed trait, which is a implemented by all possible errors for this endpoint? This could be tricky, as each endpoint might have different error cases (e.g.NotFound
).V
is the type of the deserialized http success value (e.g.User
)
Of course we'd need better naming for these :)
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.
how about exposing other http metadata inside errors, like status / status code, headers (some APIs expose debug/tracing information), hostname, etc?
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.
another question (and I'm not sure whether OpenAPI Spec supports it) to return same info for correct (V
) responses
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 always returned in the Response
object which wraps the body. So the type of the result of a call is e.g. Response[Either[ResponseError, V]]
, where V
is the type of success
@ghostbuster91 I've tested compiled code, looks very good. |
@chameleon82 Thanks for the review and your suggestions, I'm going to start working on it. |
@@ -0,0 +1 @@ | |||
sbt.version=1.2.4 |
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 file/version might be the cause of CI build failed with next error:
[warn] ::::::::::::::::::::::::::::::::::::::::::::::
[warn] :: UNRESOLVED DEPENDENCIES ::
[warn] ::::::::::::::::::::::::::::::::::::::::::::::
[warn] :: org.scala-sbt#compiler-bridge_2.13;1.2.3: not found
[warn] ::::::::::::::::::::::::::::::::::::::::::::::
[error] ## Exception when compiling 14 sources to /home/circleci/OpenAPITools/openapi-generator/samples/client/petstore/scala-sttp/target/scala-2.13/classes
[error] The compiler bridge sources org.scala-sbt:compiler-bridge_2.13:1.2.3:compile could not be retrieved.
[error]
[error] Note: Unresolved dependencies path:
[error] org.scala-sbt:compiler-bridge_2.13:1.2.3
[error] +- org.scala-sbt.temp:temp-module-52f238aede5a1d8d7a484c2bec27a144810e2ac0:1.2.3
Could you try sbt.version=1.2.8? Should we even specify this version explicitly?
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.
Yes, the sbt version should be specified in project/build.properties
so that the version is fixed and not platform-dependent. However, we should probably use 1.3.12
, which is the newest one.
"joda-time library", "2.10.6"); | ||
private static final StringProperty JSON4S_VERSION = new StringProperty("json4sVersion", "The version of json4s " + | ||
"library", "3.6.8"); | ||
private static final StringProperty CIRCE_VERSION = new StringProperty("circeVersion", "The version of circe " + |
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.
oh that's nice
0.12+
stops supporting scala 2.11
.body({{paramName}}){{/bodyParam}} | ||
.response(asJson[{{>operationReturnType}}]) | ||
.response({{#separateErrorChannel}}asJson{{/separateErrorChannel}}{{^separateErrorChannel}}asJsonAlwaysUnsafe{{/separateErrorChannel}}[{{>operationReturnType}}]) |
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.
how about exposing other http metadata inside errors, like status / status code, headers (some APIs expose debug/tracing information), hostname, etc?
.body({{paramName}}){{/bodyParam}} | ||
.response(asJson[{{>operationReturnType}}]) | ||
.response({{#separateErrorChannel}}asJson{{/separateErrorChannel}}{{^separateErrorChannel}}asJsonAlwaysUnsafe{{/separateErrorChannel}}[{{>operationReturnType}}]) |
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.
another question (and I'm not sure whether OpenAPI Spec supports it) to return same info for correct (V
) responses
Co-authored-by: eugeniyk <[email protected]>
@chameleon82 I think that I fixed/reviewed all your suggestions apart from translating application's errors from openapi to sttp. Still we think that it would be a great thing to have, so we would like to implement it, but to not extend this PR too much, we would like to do it in a separate one. Also to make this task a little bit easier we will first introduce some modification to the core of sttp (see softwaremill/sttp#618). Having said that, let me know if there is still anything which should be done before we merge this PR? |
Is there any other backend which has such simple example app in the README so I can look at it? |
Yeah, that definitely better to create separate PR for this improvement
Not sure we have good example. @wing328 how do you think about having some "basic example" in generated README file or to have specific client documentation somewhere? It might be not easy for newbies to start using generated client properly
Imo looks pretty good. Approved. @jimschubert could you help with review and merge? |
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.
Looks good, just a few minor comments.
} | ||
} | ||
|
||
private static abstract class CustomLambda implements Mustache.Lambda { |
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.
Note that at we have well-tested lambdas in the org.openapitools.codegen.templating.mustache
package which handle camel case, title case, uppercase, lowercase, etc. We should probably move these to use the tested lambdas at some point.
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 agree. I just copy-pasted it from scala-akka generator AFAIR. We should change it there as well.
@@ -1,48 +0,0 @@ | |||
{{>licenseInfo}} |
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.
removal of these built-in templates is a breaking change, so this needs to get into 5.0
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.
Is compatibility maintained between generators which aren't stable? (sttp generator is marked as beta)
) | ||
|
||
scalacOptions := Seq( | ||
"-unchecked", | ||
"-deprecation", | ||
"-feature" | ||
) | ||
|
||
publishArtifact in (Compile, packageDoc) := false |
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.
We should consider adding this back. Default behavior should be to avoid accidentally publishing content. Users can then configure publishing and ignore the build file, or enable published at invocation.
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.
Right, well our goal here was to create a minimal working project, keeping things as simple as possible, usable for an application developer (who doesn't normally publish artifacts, or has a custom publishing pipeline anyway). But maybe our approach here is incorrect - are there some guidelines as to what should be the functionality of the generated build?
Thanks!
I left a few minor comments. We can address them at any point in the future, like next time we modify this generator. |
Thanks for merging! As a next step, as @ghostbuster91 mentioned, we'd like to work on improving the typed-errors in sttp-client itself, and then building on that, improving the generator. Another direction is creating an sbt plugin which would generate the client as part of an existing project's build (so not a self-contained build, but only the sources, much like e.g. the gRPC sbt plugin works). This is what we described in #6685. Is this something that we could work on as part of OpenAPITools, and if so, should this be scoped only to this generator, or maybe it would be usable by other generators as well? |
* master: (142 commits) update python samples clarify direction of py client side validation flag (#6850) fix erronous cmd arg example for docker in readme (#6846) [BUG] [JAVA] Fix multiple files upload (#4803) (#6808) [kotlin][client] fix retrofit dependencies (#6836) [PowerShell] add more fields to be customized (#6835) [Java][WebClient]remove the dead code from java ApiClient.mustache (#6556) [PHP] Better handling of invalid data (array) (#6760) Make ApiClient in retrofit2 be able to use own OkHttpClient (#6699) mark python2 support in flask as deprecated (#6653) update samples [Java][jersey2] Add a getter for the User-Agent header value (#6831) Provides a default nil value for optional init parameters (#6827) [Java] Deprecate feignVersion option (#6824) [R] Enum R6Class Support, closes #3367 (#5728) [Rust][Client] Unify sync/async client structure (#6753) [php-ze-ph] Set required PHP version to ^7.2 (#6763) [Java][client][native][Gradle] Add missing jackson-databind-nullable (#6802) Improve sttpOpenApiClient generator (#6684) Update docker-tag-latest-release.yml ...
We'd like to propose the following changes to the scala-sttp generator. While the existing version works quite well (thanks to all of the contributors!), in a couple of places it diverges from the way sttp is most often used in the wild, and from the original design goals.
List of changes together with reasons behind them:
Despite breaking changes I opened this PR against master branch since the sttp generator is marked as beta. But let me know if that needs to be changed.
PR checklist
./bin/generate-samples.sh
to update all Petstore samples related to your fix. This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master. These must match the expectations made by your contribution. You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example./bin/generate-samples.sh bin/config/java*
. For Windows users, please run the script in Git BASH.master
Summoning technical committee:
@clasnake @jimschubert , @shijinkui , @ramzimaalej , @chameleon82 , @Bouillie