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

Improve sttpOpenApiClient generator #6684

Merged
merged 38 commits into from
Jul 1, 2020

Conversation

ghostbuster91
Copy link
Contributor

@ghostbuster91 ghostbuster91 commented Jun 16, 2020

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:

  • allow libraries' versions customization - although it is not hard to change them by hand we think that it plays nicely with the rest of the cli options and improves the overal user's experience
  • return erros by default in a separate error channel - being explicit in terms of returned error types is the way to go in scala (there is a switch which allows to restore the old behavior)
  • some of the helper functions were incorporated into sttp directly
  • some of the helper functions were removed as they were redundant/unnecessary
  • auth parameters are passed explicitly rather than implicitly - we belive that the rule of tumb to follow when choosing between imlicit vs explicit arguments is whether they are business or techincal one. Auth parameters belongs to the first category.
  • because credentials were no longer passed implicitly there was no need to keep custom auth releated classes
  • json processing related instances are now imported via single import statement
  • fixed multipart requests
  • removed ApiModel class - it was artifical and redundant
  • added circe as another json library - circe is as popular as json4s if not more
  • removed sttpClientCodegen inheritance from akkaClientCodegen
  • extract classes to deal with cli properties - Dealing with them is quite cumbersome and what is worse it is error-prone. It is easy to make a mistake, especially when dealing with properties which mutually exlude themselfs. Having another layer of abstraction on top of them makes things much easier.
  • build.sbt was reduced to minimum

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

  • Read the contribution guidelines.
  • If contributing template-only or documentation-only changes which will change sample output, build the project beforehand.
  • Run the shell script ./bin/generate-samples.shto 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.
  • File the PR against the correct branch: master
  • Copy the technical committee to review the pull request if your PR is targeting a particular programming language.

Summoning technical committee:
@clasnake @jimschubert , @shijinkui , @ramzimaalej , @chameleon82 , @Bouillie

@ghostbuster91 ghostbuster91 changed the title Better sttp Improve sttpOpenApiClient generator Jun 16, 2020
Copy link
Contributor

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


@Override
public void updateAdditionalProperties(Map<String, Object> additionalProperties) {
if (additionalProperties.containsKey(name)) {
Copy link
Contributor

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?

Copy link
Contributor Author

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

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

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?

Copy link
Contributor Author

@ghostbuster91 ghostbuster91 Jun 23, 2020

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 AbstractScalaCodegenonly importMappings are defined, while here we remove some of them and redefine them as typeMappings or instantiationTypes.

*/
implicit class ApiRequestImprovements[R[_], T](request: RequestT[Identity, Either[ResponseError[Exception], T], Nothing]) {

def result(implicit backend: SttpBackend[R, Nothing, Nothing]): R[T] = {
Copy link
Contributor

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

Copy link

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?

Copy link
Contributor

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 ...
   }

Copy link

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

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

Copy link

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 fails
  • UnknownHttpError(body: String) - for unmapped status codes
  • HttpError[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 :)

Copy link
Contributor

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?

Copy link
Contributor

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

Copy link

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

@chameleon82
Copy link
Contributor

@ghostbuster91 I've tested compiled code, looks very good.
As a suggestion it might be good to have simple example of simple app for beginners in README file.
Please also check comments above and re-trigger CI's

@ghostbuster91
Copy link
Contributor Author

@chameleon82 Thanks for the review and your suggestions, I'm going to start working on it.

@@ -0,0 +1 @@
sbt.version=1.2.4
Copy link
Contributor

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:

https://app.circleci.com/pipelines/github/OpenAPITools/openapi-generator/3736/workflows/1d16cd64-dd72-4f2b-8a68-92adfbdb29b7/jobs/17104/steps

[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?

Copy link

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 " +
Copy link
Contributor

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

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

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

samples/client/petstore/scala-sttp/build.sbt Show resolved Hide resolved
@ghostbuster91
Copy link
Contributor Author

@chameleon82 I think that I fixed/reviewed all your suggestions apart from translating application's errors from openapi to sttp.
As @adamw mentioned this might not be a trivial thing to do as each endpoint can have its own errors' hierarchy.

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?

@ghostbuster91
Copy link
Contributor Author

ghostbuster91 commented Jun 29, 2020

@ghostbuster91 I've tested compiled code, looks very good.
As a suggestion it might be good to have simple example of simple app for beginners in README file.
Please also check comments above and re-trigger CI's

Is there any other backend which has such simple example app in the README so I can look at it?

@chameleon82
Copy link
Contributor

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).

Yeah, that definitely better to create separate PR for this improvement

Is there any other backend which has such simple example app in the README so I can look at it?

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

Having said that, let me know if there is still anything which should be done before we merge this PR?

Imo looks pretty good. Approved.

@jimschubert could you help with review and merge?

Copy link
Member

@jimschubert jimschubert left a 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 {
Copy link
Member

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.

Copy link
Contributor Author

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}}
Copy link
Member

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

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link

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!

@jimschubert
Copy link
Member

I left a few minor comments. We can address them at any point in the future, like next time we modify this generator.

@jimschubert jimschubert merged commit 323cd38 into OpenAPITools:master Jul 1, 2020
@adamw
Copy link

adamw commented Jul 1, 2020

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?

jimschubert added a commit that referenced this pull request Jul 3, 2020
* 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
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants