Skip to content

Commit 5887936

Browse files
update sttp, serialize query params, fix enums serialization, fix not required files params
1 parent 8edb63d commit 5887936

5 files changed

Lines changed: 134 additions & 68 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem
2828
private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion",
2929
"The version of " +
3030
"sttp client",
31-
"4.0.0-M19");
31+
"4.0.0-RC1");
3232
private static final BooleanProperty USE_SEPARATE_ERROR_CHANNEL = new BooleanProperty("separateErrorChannel",
3333
"Whether to return response as " +
3434
"F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " +
@@ -85,8 +85,7 @@ public ScalaSttp4JsoniterClientCodegen() {
8585
.excludeGlobalFeatures(
8686
GlobalFeature.XMLStructureDefinitions,
8787
GlobalFeature.Callbacks,
88-
GlobalFeature.LinkObjects,
89-
GlobalFeature.ParameterStyling)
88+
GlobalFeature.LinkObjects)
9089
.excludeSchemaSupportFeatures(
9190
SchemaSupportFeature.Polymorphism)
9291
.excludeParameterFeatures(

modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package {{package}}
55
import {{import}}
66
{{/imports}}
77
import {{invokerPackage}}.JsonSupport.{*, given}
8+
import {{invokerPackage}}.FormSerializable
9+
import {{invokerPackage}}.FormStyleFormat
810
import {{invokerPackage}}.Helpers.*
911
import sttp.client4.jsoniter.*
1012
import sttp.client4.*
@@ -19,11 +21,11 @@ class {{classname}}(baseUrl: String):
1921
{{#javadocRenderer}}
2022
{{>javadoc}}
2123
{{/javadocRenderer}}
22-
def {{operationId}}{{>methodParameters}}: Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] =
24+
def {{operationId}}{{>methodParameters}}: sttp.client4.Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] =
2325
val requestURL =
2426
uri"$baseUrl{{{path}}}"{{#queryParams}}
25-
.addParamNamed("{{baseName}}", {{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}}
26-
.addParam("{{keyParamName}}", apiKey.value){{/isKeyInQuery}}{{/isApiKey}}
27+
.addParams(FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}})){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}}
28+
.addParams("{{keyParamName}}" -> apiKey.value){{/isKeyInQuery}}{{/isApiKey}}
2729
2830
basicRequest
2931
.method(Method.{{httpMethod.toUpperCase}}, requestURL)
@@ -42,7 +44,7 @@ class {{classname}}(baseUrl: String):
4244
.multipartBody(Seq({{#formParams}}
4345
{{>paramMultipartCreation}}{{/formParams}}
4446
).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}}
45-
.body({{paramName}}){{/bodyParam}}
47+
{{^isFile}}.body({{paramName}}){{/isFile}}{{#isFile}}.fileBody({{paramName}}){{/isFile}}{{/bodyParam}}
4648
.response({{#separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))){{/returnType}}{{#fnHandleDownload}}{{#returnType}}asJson[{{>operationReturnType}}]{{/returnType}}{{/fnHandleDownload}}{{/separateErrorChannel}}{{^separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))).getRight{{/returnType}}{{#returnType}}asJson[{{>operationReturnType}}].getRight{{/returnType}}{{/separateErrorChannel}})
4749
4850
{{/operation}}
Lines changed: 122 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,130 @@
11
{{>licenseInfo}}
22
package {{invokerPackage}}
33

4-
import scala.language.implicitConversions
4+
import scala.deriving.*
5+
import scala.compiletime.*
6+
import java.io.File
57

6-
object Helpers:
7-
implicit def productToMapStringString[A <: Product](a: A): Map[String, String] =
8-
val fields = a.productElementNames.zipWithIndex.map { case (name, index) =>
9-
name -> a.productElement(index)
10-
}.map {
11-
case (name, value: Option[_]) => value.map(v => name -> v.toString)
12-
case (name, value) => Some(name -> value.toString)
13-
}.collect { case Some(x) => x }
14-
15-
fields.toMap
8+
enum FormStyleFormat:
9+
case FORM
10+
case SPACEDELIMITED
11+
case PIPEDELIMITED
12+
case DEEPOBJECT
1613

14+
object FormSerializable:
1715
type Primitive = String | Short | Int | Long | Float | Double | BigDecimal | Boolean
1816

19-
extension (uri: sttp.model.Uri)
20-
def addParamNamed(name: String, value: Primitive | Option[Primitive] | Map[String, String]): sttp.model.Uri =
21-
value match
22-
case opt: Option[_] => opt.fold(uri) { prim => uri.addParam(name, prim.toString) }
23-
case map: Map[String, String] => map.foldLeft(uri) { case (uri, (k, v)) => uri.addParam(k, v) }
24-
case prim => uri.addParam(name, prim.toString)
25-
26-
def addParamNamed(name: String, value: Iterable[String], format: CollectionFormat): sttp.model.Uri =
27-
format match
28-
case CollectionFormats.MULTI => value.foldLeft(uri) { case (uri, v) => uri.addParam(name, v) }
29-
case maFormat: MergedArrayFormat => uri.addParam(name, value.mkString(maFormat.separator))
30-
31-
/**
32-
* Used for params being arrays
33-
*/
34-
final case class ArrayValues(values: Seq[Any], format: MergedArrayFormat = CollectionFormats.CSV):
35-
override def toString: String = values.mkString(format.separator)
36-
37-
object ArrayValues:
38-
def apply(values: Option[Seq[Any]], format: MergedArrayFormat): ArrayValues =
39-
ArrayValues(values.getOrElse(Seq.empty), format)
40-
41-
def apply(values: Option[Seq[Any]]): ArrayValues = ArrayValues(values, CollectionFormats.CSV)
42-
43-
/**
44-
* Defines how arrays should be rendered in query strings.
45-
*/
46-
sealed trait CollectionFormat
17+
inline def serialize[T](
18+
name: String,
19+
obj: T,
20+
inline format: FormStyleFormat = FormStyleFormat.FORM,
21+
inline explode: Boolean = true
22+
): Map[String, String] =
23+
inline obj match
24+
case primitive: Primitive => serializePrimitive(name, primitive, format, explode)
25+
case array: Seq[Primitive] => serializeArray(name, array, format, explode)
26+
case optPrimitive: Option[Primitive] => optPrimitive.map(value => serializePrimitive(name, value, format, explode)).getOrElse(Map.empty[String, String])
27+
case optArray: Option[Seq[Primitive]] => optArray.map(serializeArray(name, _, format, explode)).getOrElse(Map.empty[String, String])
28+
case freeObj: Map[String, Primitive] => freeObj.map((key, value) => (key, value.toString))
29+
case optObj: Option[t] =>
30+
inline summonInline[Mirror.Of[t]] match
31+
case mirror: Mirror.ProductOf[t] =>
32+
checkFields[mirror.MirroredElemTypes]
33+
val labels = allLabels[mirror.MirroredElemLabels]
34+
optObj.map{ obj =>
35+
val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq
36+
serializeModel(name, keyVals, format, explode)
37+
}.getOrElse(Map.empty[String, String])
38+
case _ => error("ERROR") // TODO - support for enums
39+
case obj =>
40+
inline summonInline[Mirror.Of[T]] match
41+
case _: Mirror.SumOf[T] =>
42+
error("ERROR") // TODO - support for enums
43+
case mirror: Mirror.ProductOf[T] =>
44+
checkFields[mirror.MirroredElemTypes] // Stripe ma IDGAF bo używają deepObject np. tak lines[0][tax_amounts][0][amount] - mimo tego że spec na to nie pozwala
45+
val labels = allLabels[mirror.MirroredElemLabels]
46+
val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq
47+
serializeModel(name, keyVals, format, explode)
48+
49+
50+
51+
private inline def allLabels[T <: Tuple]: List[String] =
52+
constValueTuple[T].toList.asInstanceOf[List[String]]
53+
54+
private inline def checkFields[T <: Tuple]: Unit =
55+
inline erasedValue[T] match {
56+
case _: EmptyTuple => ()
57+
case _: (t *: ts) =>
58+
inline erasedValue[t] match
59+
case _: Primitive => checkFields[ts]
60+
case _ =>
61+
error(
62+
"Cannot derive structure, structure must consist only of primitive fields"
63+
)
64+
}
65+
66+
private inline def serializePrimitive(
67+
paramName: String,
68+
value: Primitive,
69+
inline format: FormStyleFormat,
70+
inline explode: Boolean
71+
): Map[String, String] = {
72+
inline format match
73+
case FormStyleFormat.FORM =>
74+
Map(paramName -> value.toString) // for primitve values explode does not change anything
75+
case FormStyleFormat.SPACEDELIMITED =>
76+
error(
77+
"FormStyleFormat.SpaceDelimited does not support primitive values"
78+
)
79+
case FormStyleFormat.PIPEDELIMITED =>
80+
error("FormStyleFormat.PipeDelimited does not support primitive values")
81+
case FormStyleFormat.DEEPOBJECT =>
82+
error("FormStyleFormat.DeepObject does not support primitive values")
83+
84+
}
85+
private inline def serializeArray(
86+
paramName: String,
87+
values: Seq[Primitive],
88+
inline format: FormStyleFormat,
89+
inline explode: Boolean
90+
): Map[String, String] = {
91+
inline format match
92+
case FormStyleFormat.FORM =>
93+
inline if explode then values.map(s => (paramName, s.toString)).toMap
94+
else Map(paramName -> values.mkString(","))
95+
case FormStyleFormat.SPACEDELIMITED =>
96+
inline if explode then values.map(s => (paramName, s.toString)).toMap
97+
else Map(paramName -> values.mkString(" ")) // Sttp will encode space as +, from https://swagger.io/docs/specification/v3_0/serialization/#query-parameters it is not clear if it should be + or %20
98+
case FormStyleFormat.PIPEDELIMITED =>
99+
inline if explode then values.map(s => (paramName, s.toString)).toMap
100+
else Map(paramName -> values.mkString("|"))
101+
case FormStyleFormat.DEEPOBJECT =>
102+
error("FormStyleFormat.DeepObject does not support arrays")
103+
}
104+
inline def serializeModel(
105+
paramName: String,
106+
keyValPairs: Seq[(String, Primitive)],
107+
inline format: FormStyleFormat,
108+
inline explode: Boolean
109+
): Map[String, String] = {
110+
inline format match
111+
case FormStyleFormat.FORM =>
112+
inline if explode then keyValPairs.map((key, value) => (key, value.toString)).toMap
113+
else Map(paramName -> keyValPairs.flatMap((key,value) => Seq(key, value.toString)).mkString(","))
114+
case FormStyleFormat.SPACEDELIMITED =>
115+
error("FormStyleFormat.SpaceDelimited does not support objects")
116+
case FormStyleFormat.PIPEDELIMITED =>
117+
error("FormStyleFormat.PipeDelimited does not support objects")
118+
case FormStyleFormat.DEEPOBJECT =>
119+
inline if explode then
120+
keyValPairs.map((key, value) => (s"$paramName[$key]", value.toString)).toMap
121+
else error("FormStyleFormat.DeepObject does not support explode=false")
122+
}
123+
end FormSerializable
47124

48-
trait MergedArrayFormat extends CollectionFormat:
49-
def separator: String
50-
51-
object CollectionFormats:
52-
53-
case object CSV extends MergedArrayFormat:
54-
override val separator = ","
55-
56-
case object TSV extends MergedArrayFormat:
57-
override val separator = "\t"
58-
59-
case object SSV extends MergedArrayFormat:
60-
override val separator = " "
61-
62-
case object PIPES extends MergedArrayFormat:
63-
override val separator = "|"
64-
65-
case object MULTI extends CollectionFormat
125+
object Helpers:
126+
extension (request: sttp.client4.Request[?])
127+
def fileBody(file: Option[File] | File): sttp.client4.Request[?] =
128+
file match
129+
case f: File => request.body(f)
130+
case f: Option[File] => f.map(request.body(_)).getOrElse(request)

modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ object {{classname}}:
3939
import com.github.plokhotnyuk.jsoniter_scala.macros.*
4040
import com.github.plokhotnyuk.jsoniter_scala.core.*
4141
{{#isString}}
42-
given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make {
43-
CodecMakerConfig
42+
given {{#fnCodecName}}{{classname}}{{/fnCodecName}}: JsonValueCodec[{{classname}}] = JsonCodecMaker.make {
43+
CodecMakerConfig{{#allowableValues}}
4444
.withAdtLeafClassNameMapper { x =>
4545
JsonCodecMaker.simpleClassName(x) match
4646
{{#values}}
4747
case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}"
4848
{{/values}}
49-
}
49+
}{{/allowableValues}}
5050
.withDiscriminatorFieldName(scala.None)
5151
}
5252
{{/isString}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"{{baseName}}" -> {{#isContainer}}Some(ArrayValues({{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString()){{/isContainer}}{{^isContainer}}{{^required}}{{{paramName}}}{{/required}}{{#required}}Some({{{paramName}}}){{/required}}{{/isContainer}}
1+
"{{baseName}}" -> {{#isContainer}}FormStyleFormat.serialize({{{paramName}}}{{#collectionFormat}}, FormStyleFormat.{{style.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{^required}}{{{paramName}}}{{/required}}{{#required}}Some({{{paramName}}}){{/required}}{{/isContainer}}

0 commit comments

Comments
 (0)