-
-
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
Changes from 31 commits
a09822e
a412444
e8c3890
e3a0654
4ac8894
88908e2
5c255e4
6c5d792
3b27bc5
cac19fc
86ec499
97fd17f
a7a15a2
551bb35
2a91b11
a380531
5cab35d
714f930
a5f0a05
dfca277
5efad87
3c836f9
a377921
d176eeb
f7c384c
4a5ff27
e2df636
b3017bb
86150cb
4588362
b572673
91dd9b0
9242bc4
de82caf
d868eda
1f9cd3e
a6647d2
8f5a98f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package {{invokerPackage}} | ||
|
||
{{#java8}} | ||
import java.time.{LocalDate, LocalDateTime, OffsetDateTime, ZoneId} | ||
import java.time.format.DateTimeFormatter | ||
import scala.util.Try | ||
{{/java8}} | ||
{{#joda}} | ||
import org.joda.time.DateTime | ||
import org.joda.time.format.ISODateTimeFormat | ||
{{/joda}} | ||
|
||
{{#json4s}} | ||
object DateSerializers { | ||
import org.json4s.{Serializer, CustomSerializer, JNull} | ||
import org.json4s.JsonAST.JString | ||
{{#java8}} | ||
case object DateTimeSerializer extends CustomSerializer[OffsetDateTime](_ => ( { | ||
case JString(s) => | ||
Try(OffsetDateTime.parse(s, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) orElse | ||
Try(LocalDateTime.parse(s).atZone(ZoneId.systemDefault()).toOffsetDateTime) getOrElse (null) | ||
case JNull => null | ||
}, { | ||
case d: OffsetDateTime => | ||
JString(d.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) | ||
})) | ||
|
||
case object LocalDateSerializer extends CustomSerializer[LocalDate]( _ => ( { | ||
case JString(s) => LocalDate.parse(s) | ||
case JNull => null | ||
}, { | ||
case d: LocalDate => | ||
JString(d.format(DateTimeFormatter.ISO_LOCAL_DATE)) | ||
})) | ||
{{/java8}} | ||
{{#joda}} | ||
case object DateTimeSerializer extends CustomSerializer[DateTime](_ => ( { | ||
case JString(s) => | ||
ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(s) | ||
case JNull => null | ||
}, { | ||
case d: org.joda.time.DateTime => | ||
JString(ISODateTimeFormat.dateTime().print(d)) | ||
}) | ||
) | ||
|
||
case object LocalDateSerializer extends CustomSerializer[org.joda.time.LocalDate](_ => ( { | ||
case JString(s) => org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd").parseLocalDate(s) | ||
case JNull => null | ||
}, { | ||
case d: org.joda.time.LocalDate => JString(d.toString("yyyy-MM-dd")) | ||
})) | ||
{{/joda}} | ||
|
||
def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ LocalDateSerializer :+ DateTimeSerializer | ||
} | ||
{{/json4s}} | ||
{{#circe}} | ||
trait DateSerializers { | ||
import io.circe.{Decoder, Encoder} | ||
{{#java8}} | ||
implicit val isoOffsetDateTimeDecoder: Decoder[OffsetDateTime] = Decoder.decodeOffsetDateTimeWithFormatter(DateTimeFormatter.ISO_OFFSET_DATE_TIME) | ||
implicit val isoOffsetDateTimeEncoder: Encoder[OffsetDateTime] = Encoder.encodeOffsetDateTimeWithFormatter(DateTimeFormatter.ISO_OFFSET_DATE_TIME) | ||
|
||
implicit val localDateDecoder: Decoder[LocalDate] = Decoder.decodeLocalDateWithFormatter(DateTimeFormatter.ISO_LOCAL_DATE) | ||
implicit val localDateEncoder: Encoder[LocalDate] = Encoder.encodeLocalDateWithFormatter(DateTimeFormatter.ISO_LOCAL_DATE) | ||
{{/java8}} | ||
{{#joda}} | ||
implicit val dateTimeDecoder: Decoder[DateTime] = Decoder.decodeString.map(ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(_)) | ||
implicit val dateTimeEncoder: Encoder[DateTime] = Encoder.encodeString.contramap(ISODateTimeFormat.dateTime().print(_)) | ||
|
||
implicit val localDateDecoder: Decoder[org.joda.time.LocalDate] = Decoder.decodeString.map(org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd").parseLocalDate(_)) | ||
implicit val localDateEncoder: Encoder[org.joda.time.LocalDate] = Encoder.encodeString.contramap(_.toString("yyyy-MM-dd")) | ||
{{/joda}} | ||
} | ||
{{/circe}} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{{>licenseInfo}} | ||
package {{invokerPackage}} | ||
|
||
{{#models.0}} | ||
import {{modelPackage}}._ | ||
{{/models.0}} | ||
{{#json4s}} | ||
import org.json4s._ | ||
import sttp.client.json4s.SttpJson4sApi | ||
import scala.reflect.ClassTag | ||
|
||
object JsonSupport extends SttpJson4sApi { | ||
def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]](){{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} :+ | ||
new EnumNameSerializer({{classname}}Enums.{{datatypeWithEnum}}){{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} | ||
|
||
private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) extends Serializer[E#Value] { | ||
import JsonDSL._ | ||
val EnumerationClass: Class[E#Value] = classOf[E#Value] | ||
|
||
def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = { | ||
case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => | ||
json match { | ||
case JString(value) => enum.withName(value) | ||
case value => throw new MappingException(s"Can't convert $value to $EnumerationClass") | ||
} | ||
} | ||
|
||
private[this] def isValid(json: JValue) = json match { | ||
case JString(value) if enum.values.exists(_.toString == value) => true | ||
case _ => false | ||
} | ||
|
||
def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { | ||
case i: E#Value => i.toString | ||
} | ||
} | ||
|
||
implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all | ||
implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization | ||
} | ||
{{/json4s}} | ||
{{#circe}} | ||
import io.circe.{Decoder, Encoder} | ||
import io.circe.generic.AutoDerivation | ||
import sttp.client.circe.SttpCirceApi | ||
|
||
object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers { | ||
{{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} | ||
implicit val {{classname}}{{datatypeWithEnum}}Decoder: Decoder[{{classname}}Enums.{{datatypeWithEnum}}] = Decoder.decodeEnumeration({{classname}}Enums.{{datatypeWithEnum}}) | ||
implicit val {{classname}}{{datatypeWithEnum}}Encoder: Encoder[{{classname}}Enums.{{datatypeWithEnum}}] = Encoder.encodeEnumeration({{classname}}Enums.{{datatypeWithEnum}}) | ||
{{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} | ||
} | ||
{{/circe}} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{#hasMore}}, {{/hasMore}}{{/allParams}}{{#authMethods.0}})(implicit {{#authMethods}}{{#isApiKey}}apiKey: ApiKeyValue{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}basicAuth: BasicCredentials{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: BearerToken{{/isBasicBearer}}{{/isBasic}}{{#hasMore}}, {{/hasMore}}{{/authMethods}}{{/authMethods.0}} | ||
{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{#hasMore}}, {{/hasMore}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{#hasMore}}, {{/hasMore}}{{/allParams}} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{{#required}} | ||
{{#isFile}} | ||
multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}}) | ||
{{/isFile}} | ||
{{^isFile}} | ||
multipart("{{baseName}}", {{paramName}}) | ||
{{/isFile}} | ||
{{/required}} | ||
{{^required}} | ||
{{#isFile}} | ||
{{paramName}}.map(multipartFile("{{baseName}}", _)) | ||
{{/isFile}} | ||
{{^isFile}} | ||
{{paramName}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}_{{/isContainer}})) | ||
{{/isFile}} | ||
{{/required}} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sbt.version=1.2.4 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the sbt version should be specified in |
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 (failedFuture
orIO
).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 codesThen, 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
) responsesThere 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]]
, whereV
is the type of success