Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,13 @@ public void processOpts() {
super.processOpts();
if (additionalProperties.containsKey("mainPackage")) {
setMainPackage((String) additionalProperties.get("mainPackage"));
additionalProperties.replace("configKeyPath", this.configKeyPath);
apiPackage = mainPackage + ".api";
modelPackage = mainPackage + ".model";
invokerPackage = mainPackage + ".core";
additionalProperties.put("apiPackage", apiPackage);
additionalProperties.put("modelPackage", apiPackage);
additionalProperties.put("invokerPackage", apiPackage);
additionalProperties.put("modelPackage", modelPackage);
additionalProperties.put("invokerPackage", invokerPackage);
}

supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
Expand Down Expand Up @@ -366,6 +367,6 @@ public String escapeQuotationMark(String input) {
}

public void setMainPackage(String mainPackage) {
this.mainPackage = mainPackage;
this.configKeyPath = this.mainPackage = mainPackage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class {{classname}}(baseUrl: String) {
{{/javadocRenderer}}
def {{operationId}}({{>methodParameters}}): ApiRequest[{{>operationReturnType}}] =
ApiRequest[{{>operationReturnType}}](ApiMethods.{{httpMethod.toUpperCase}}, baseUrl, "{{{path}}}", {{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}})
{{#authMethods}}{{#isApiKey}}.withApiKey(apiKey, "{{keyParamName}}", {{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}})
{{#authMethods}}{{#isApiKey}}.withApiKey(apiKey, "{{keyParamName}}", {{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}})
{{/isApiKey}}{{#isBasic}}.withCredentials(basicAuth)
{{/isBasic}}{{/authMethods}}{{#bodyParam}}.withBody({{paramName}})
{{/bodyParam}}{{#formParams}}.withFormParam({{>paramCreation}})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ object ApiInvoker {
*
* @param request the apiRequest to be executed
*/
implicit class ApiRequestImprovements[T](request: ApiRequest[T]) {
implicit class ApiRequestImprovements[T: Manifest](request: ApiRequest[T]) {

def response(invoker: ApiInvoker)(implicit ec: ExecutionContext, system: ActorSystem): Future[ApiResponse[T]] =
response(ec, system, invoker)
Expand All @@ -67,7 +67,7 @@ object ApiInvoker {
def toAkkaHttpMethod: HttpMethod = HttpMethods.getForKey(method.value).getOrElse(HttpMethods.GET)
}

case object DateTimeSerializer extends CustomSerializer[DateTime](format => ( {
case object DateTimeSerializer extends CustomSerializer[DateTime](_ => ( {
case JString(s) =>
ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(s)
}, {
Expand Down Expand Up @@ -215,7 +215,7 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
Uri(r.basePath + opPathWithParams).withQuery(query)
}

def execute[T](r: ApiRequest[T]): Future[ApiResponse[T]] = {
def execute[T: Manifest](r: ApiRequest[T]): Future[ApiResponse[T]] = {
implicit val timeout: Timeout = settings.connectionTimeout

val request = createRequest(makeUri(r), r)
Expand All @@ -239,8 +239,8 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
.flatMap(unmarshallApiResponse(r))
}

def unmarshallApiResponse[T](request: ApiRequest[T])(response: HttpResponse): Future[ApiResponse[T]] = {
def responseForState[V](state: ResponseState, value: V) = {
def unmarshallApiResponse[T: Manifest](request: ApiRequest[T])(response: HttpResponse): Future[ApiResponse[T]] = {
def responseForState[V](state: ResponseState, value: V): ApiResponse[V] = {
state match {
case ResponseState.Success =>
ApiResponse(response.status.intValue, value, response.headers.map(header => (header.name, header.value)).toMap)
Expand All @@ -253,31 +253,29 @@ class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends CustomC
)
}
}

val mf = implicitly(manifest[T])
request
.responseForCode(response.status.intValue)
.map {
case (Manifest.Unit, state: ResponseState) =>
// FIXME Casting is ugly, how to do better?
Future(responseForState(state, Unit)).asInstanceOf[Future[ApiResponse[T]]]
case (manifest: Manifest[T], state: ResponseState) =>
implicit val m: Unmarshaller[HttpEntity, T] = unmarshaller[T](manifest, serialization, formats)

Unmarshal(response.entity)
.to[T]
.recoverWith {
case e ⇒ throw ApiError(response.status.intValue, s"Unable to unmarshall content to [$manifest]", Some(response.entity.toString), e)
}
.map(value => responseForState(state, value))
}
.getOrElse(Future.failed(ApiError(response.status.intValue, "Unexpected response code", Some(response.entity.toString))))
.responseForCode(response.status.intValue) match {
case Some((Manifest.Unit, state: ResponseState)) =>
Future(responseForState(state, Unit).asInstanceOf[ApiResponse[T]])
case Some((manifest, state: ResponseState)) if manifest == mf =>
implicit val m: Unmarshaller[HttpEntity, T] = unmarshaller[T](mf, serialization, formats)
Unmarshal(response.entity)
.to[T]
.recoverWith {
case e ⇒ throw ApiError(response.status.intValue, s"Unable to unmarshall content to [$manifest]", Some(response.entity.toString), e)
}
.map(value => responseForState(state, value))
case None | Some(_) =>
Future.failed(ApiError(response.status.intValue, "Unexpected response code", Some(response.entity.toString)))
}
}
}

sealed trait CustomContentTypes {

protected def normalizedContentType(original: String): ContentType =
ContentType(MediaTypes.forExtension(original), () => HttpCharsets.`UTF-8`)
ContentType(parseContentType(original).mediaType, () => HttpCharsets.`UTF-8`)

protected def parseContentType(contentType: String): ContentType = {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{{>licenseInfo}}
package {{package}}

{{#imports}}
import {{import}}
{{/imports}}
import {{mainPackage}}.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
Copy link
Member

Choose a reason for hiding this comment

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

This will be a potential breaking change unless this is added to the constructor:

importMapping.put("UUID", "java.util.UUID");

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 is already there in ScalaAkkaClientCodegen extends AbstractScalaCodegen extends DefaultCodegen base class.
I will add unit test to check there is no breaking changes for the further versions


{{#models}}
{{#model}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ object ApiKeyLocations {

case object HEADER extends ApiKeyLocation

case object COOKIE extends ApiKeyLocation

}


Expand Down
18 changes: 18 additions & 0 deletions modules/openapi-generator/src/test/resources/3_0/petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ paths:
description: ''
operationId: addPet
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'405':
description: Invalid input
security:
Expand All @@ -41,6 +50,15 @@ paths:
description: ''
operationId: updatePet
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
Copy link
Member

Choose a reason for hiding this comment

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

@chameleon82 are these a must? In other words, would the change still work without documenting the HTTP 200 response in the spec?

We prefer not to update petstore.yaml to keep it as original as possible.

Copy link
Member

Choose a reason for hiding this comment

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

@wing328 Thanks for calling this out. When you say original, what are we keeping it original compared to? This spec isn't the "official" petstore.yaml spec from OpenAPI Specification 3.0 examples.

I'd have some concerns about not including these changes. It would mean that the PUT and POST methods are only documented to ever return a client error. From https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responsesObject:

A container for the expected responses of an operation. The container maps a HTTP response code to the expected response.

The documentation is not necessarily expected to cover all possible HTTP response codes because they may not be known in advance. However, documentation is expected to cover a successful operation response and any known errors.

The default MAY be used as a default response object for all HTTP codes that are not covered individually by the specification.

The Responses Object MUST contain at least one response code, and it SHOULD be the response for a successful operation call.

Although the line "documentation is expected to cover a successful operation response" doesn't use the specification terminology MUST, MAY or SHOULD, I think our test yaml breaking 'expectations' could be confusing especially since there is no 'default' response documented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@wing328 @jimschubert I've checked than one https://petstore3.swagger.io/api/v3/openapi.yaml which currently has those specified, openApi specs also have specified successful response here https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore.yaml and here https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml
However https://petstore.swagger.io/v2/swagger.yaml has not specified even default one.
Seems that case must be clear in OpenApi Specification.
I suppose SHOULD for successful operation means that client has another way to check result. For example, by calling GET /pet/123 after calling POST /pet {id = 123, ... }
For the current case i have added this to validate successful scenarios in the unit tests

Copy link
Member

Choose a reason for hiding this comment

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

You're right that's probably why they've used SHOULD. However, the common way to do that without a returned body is to use links in the header of a successful response. I think your changes to include 2xx type responses makes sense. If we don't include positive responses for these endpoints, I think we should remove the endpoints entirely, because no realistic API returns only client errors for a path.

@wing328 are you cool with keeping these changes? We should probably also review all our go-to specs and make sure they represent real-world designs.

Copy link
Member

Choose a reason for hiding this comment

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

Totally agreed with reviewing the spec to ensure it's correct. Let's go with this PR for the time being.

'400':
description: Invalid ID supplied
'404':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
*/
package hello.world.model

import hello.world.core.ApiModel
import org.joda.time.DateTime
import java.util.UUID
import hello.world.core.ApiModel

case class SomeObj (
`type`: Option[SomeObjEnums.`Type`] = None,
Expand Down
8 changes: 8 additions & 0 deletions samples/client/petstore/scala-akka/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ Class | Method | HTTP request | Description

- [ApiResponse](ApiResponse.md)
- [Category](Category.md)
- [InlineObject](InlineObject.md)
- [InlineObject1](InlineObject1.md)
- [Order](Order.md)
- [Pet](Pet.md)
- [Tag](Tag.md)
Expand All @@ -106,6 +108,12 @@ Authentication schemes defined for the API:
- **API key parameter name**: api_key
- **Location**: HTTP header

### auth_cookie

- **Type**: API key
- **API key parameter name**: AUTH_KEY
- **Location**:


## Author

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.2.8
sbt.version=1.3.6
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ class PetApi(baseUrl: String) {

/**
* Expected answers:
* code 200 : Pet (successful operation)
* code 405 : (Invalid input)
*
* @param body Pet object that needs to be added to the store
* @param pet Pet object that needs to be added to the store
*/
def addPet(body: Pet): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/pet", "application/json")
.withBody(body)
def addPet(pet: Pet): ApiRequest[Pet] =
ApiRequest[Pet](ApiMethods.POST, baseUrl, "/pet", "application/json")
.withBody(pet)
.withSuccessResponse[Pet](200)
.withErrorResponse[Unit](405)


Expand Down Expand Up @@ -107,15 +109,17 @@ class PetApi(baseUrl: String) {

/**
* Expected answers:
* code 200 : Pet (successful operation)
* code 400 : (Invalid ID supplied)
* code 404 : (Pet not found)
* code 405 : (Validation exception)
*
* @param body Pet object that needs to be added to the store
* @param pet Pet object that needs to be added to the store
*/
def updatePet(body: Pet): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.PUT, baseUrl, "/pet", "application/json")
.withBody(body)
def updatePet(pet: Pet): ApiRequest[Pet] =
ApiRequest[Pet](ApiMethods.PUT, baseUrl, "/pet", "application/json")
.withBody(pet)
.withSuccessResponse[Pet](200)
.withErrorResponse[Unit](400)
.withErrorResponse[Unit](404)
.withErrorResponse[Unit](405)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ class StoreApi(baseUrl: String) {
* code 200 : Order (successful operation)
* code 400 : (Invalid Order)
*
* @param body order placed for purchasing the pet
* @param order order placed for purchasing the pet
*/
def placeOrder(body: Order): ApiRequest[Order] =
def placeOrder(order: Order): ApiRequest[Order] =
ApiRequest[Order](ApiMethods.POST, baseUrl, "/store/order", "application/json")
.withBody(body)
.withBody(order)
.withSuccessResponse[Order](200)
.withErrorResponse[Unit](400)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,47 @@ class UserApi(baseUrl: String) {
* Expected answers:
* code 0 : (successful operation)
*
* @param body Created user object
* Available security schemes:
* auth_cookie (apiKey)
*
* @param user Created user object
*/
def createUser(body: User): ApiRequest[Unit] =
def createUser(user: User)(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/user", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withDefaultSuccessResponse[Unit]


/**
* Expected answers:
* code 0 : (successful operation)
*
* @param body List of user object
* Available security schemes:
* auth_cookie (apiKey)
*
* @param user List of user object
*/
def createUsersWithArrayInput(body: Seq[User]): ApiRequest[Unit] =
def createUsersWithArrayInput(user: Seq[User])(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/user/createWithArray", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withDefaultSuccessResponse[Unit]


/**
* Expected answers:
* code 0 : (successful operation)
*
* @param body List of user object
* Available security schemes:
* auth_cookie (apiKey)
*
* @param user List of user object
*/
def createUsersWithListInput(body: Seq[User]): ApiRequest[Unit] =
def createUsersWithListInput(user: Seq[User])(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.POST, baseUrl, "/user/createWithList", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withDefaultSuccessResponse[Unit]


Expand All @@ -68,10 +80,14 @@ class UserApi(baseUrl: String) {
* code 400 : (Invalid username supplied)
* code 404 : (User not found)
*
* Available security schemes:
* auth_cookie (apiKey)
*
* @param username The name that needs to be deleted
*/
def deleteUser(username: String): ApiRequest[Unit] =
def deleteUser(username: String)(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.DELETE, baseUrl, "/user/{username}", "application/json")
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withPathParam("username", username)
.withErrorResponse[Unit](400)
.withErrorResponse[Unit](404)
Expand All @@ -97,6 +113,7 @@ class UserApi(baseUrl: String) {
* Expected answers:
* code 200 : String (successful operation)
* Headers :
* Set-Cookie - Cookie authentication key for use with the `auth_cookie` apiKey authentication.
* X-Rate-Limit - calls per hour allowed by the user
* X-Expires-After - date in UTC when toekn expires
* code 400 : (Invalid username/password supplied)
Expand All @@ -112,16 +129,21 @@ class UserApi(baseUrl: String) {
.withErrorResponse[Unit](400)

object LoginUserHeaders {
def setCookie(r: ApiReturnWithHeaders) = r.getStringHeader("Set-Cookie")
def xRateLimit(r: ApiReturnWithHeaders) = r.getIntHeader("X-Rate-Limit")
def xExpiresAfter(r: ApiReturnWithHeaders) = r.getDateTimeHeader("X-Expires-After")
}

/**
* Expected answers:
* code 0 : (successful operation)
*
* Available security schemes:
* auth_cookie (apiKey)
*/
def logoutUser(): ApiRequest[Unit] =
def logoutUser()(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.GET, baseUrl, "/user/logout", "application/json")
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withDefaultSuccessResponse[Unit]


Expand All @@ -132,12 +154,16 @@ class UserApi(baseUrl: String) {
* code 400 : (Invalid user supplied)
* code 404 : (User not found)
*
* Available security schemes:
* auth_cookie (apiKey)
*
* @param username name that need to be deleted
* @param body Updated user object
* @param user Updated user object
*/
def updateUser(username: String, body: User): ApiRequest[Unit] =
def updateUser(username: String, user: User)(implicit apiKey: ApiKeyValue): ApiRequest[Unit] =
ApiRequest[Unit](ApiMethods.PUT, baseUrl, "/user/{username}", "application/json")
.withBody(body)
.withApiKey(apiKey, "AUTH_KEY", COOKIE)
.withBody(user)
.withPathParam("username", username)
.withErrorResponse[Unit](400)
.withErrorResponse[Unit](404)
Expand Down
Loading