Skip to content

Commit b51b18e

Browse files
authored
Scala cask api effects (#19936)
* Scala-cask improvements: * fixe for grouped methods which have routes containing dashes. Previously our OperationGroup work-around would potentially Create methods like ‘foo-bar’, which isn’t a valid function name * Fix to not import some.package.Array[Byte] when binary format is specified * Fix for grouped operations which contain duplicate query parameters * Fix for binary response fields. This can come up with the following example "responses" : { "200" : { "content" : { "application/json" : { "schema" : { "format" : "binary", "type" : "string" } } }, "description" : "data" }, * Fix for enum model classes Extracted complex logic for ‘asData’ and ‘asModel’ transformations for properties * Introduced a generic effect F[_] for services This was done to support composable services (Service A calls Service B) by using monadic Effect types (ones which can flatMap) * Fixed unique union types for responses, asModel and asData fixes for non-model types * scala-cask: regenerated samples * Fix for reserved-word properties in the API * Fix for null imports and reserved-word enum types * Fixes for api methods with backticked params * Fix for duplicate (by name) grouped params * small syntax fix * logging response type * Regenerated samples * String.format fix
1 parent cded99c commit b51b18e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1725
-821
lines changed

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

Lines changed: 307 additions & 40 deletions
Large diffs are not rendered by default.

modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import scala.reflect.ClassTag
1313
import scala.util.*
1414
import upickle.default.*
1515

16+
17+
extension (f: java.io.File) {
18+
def bytes: Array[Byte] = java.nio.file.Files.readAllBytes(f.toPath)
19+
def toBase64: String = java.util.Base64.getEncoder.encodeToString(bytes)
20+
}
21+
22+
given Writer[java.io.File] = new Writer[java.io.File] {
23+
def write0[V](out: upickle.core.Visitor[?, V], v: java.io.File) = out.visitString(v.toBase64, -1)
24+
}
25+
1626
// needed for BigDecimal params
1727
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
1828

modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import {{modelPackage}}.*
1010

1111
import upickle.default.{ReadWriter => RW, macroRW}
1212
import upickle.default.*
13+
import scala.util.Try
1314

1415
{{#imports}}import {{import}}
1516
{{/imports}}
1617

17-
class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
18+
class {{classname}}Routes(service : {{classname}}Service[Try]) extends cask.Routes {
1819
1920
{{#route-groups}}
2021
// route group for {{methodName}}
@@ -35,7 +36,7 @@ class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
3536
* {{description}}
3637
*/
3738
{{vendorExtensions.x-annotation}}("{{vendorExtensions.x-cask-path}}")
38-
def {{operationId}}({{vendorExtensions.x-cask-path-typed}}) = {
39+
def {{operationId}}({{{vendorExtensions.x-cask-path-typed}}}) = {
3940
{{#authMethods}}
4041
// auth method {{name}} : {{type}}, keyParamName: {{keyParamName}}
4142
{{/authMethods}}

modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,123 @@ package {{apiPackage}}
88

99
{{#imports}}import _root_.{{import}}
1010
{{/imports}}
11-
11+
import scala.util.Failure
12+
import scala.util.Try
1213
import _root_.{{modelPackage}}.*
1314

15+
/**
16+
* The {{classname}}Service companion object.
17+
*
18+
* Use the {{classname}}Service() companion object to create an instance which returns a 'not implemented' error
19+
* for each operation.
20+
*
21+
*/
1422
object {{classname}}Service {
15-
def apply() : {{classname}}Service = new {{classname}}Service {
23+
24+
/**
25+
* The 'Handler' is an implementation of {{classname}}Service convenient for delegating or overriding individual functions
26+
*/
27+
case class Handler[F[_]](
1628
{{#operations}}
1729
{{#operation}}
18-
override def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}} = ???
30+
{{operationId}}Handler : ({{{vendorExtensions.x-param-list-typed}}}) => F[{{{vendorExtensions.x-response-type}}}]{{^-last}}, {{/-last}}
31+
{{/operation}}
32+
{{/operations}}
33+
) extends {{classname}}Service[F] {
34+
{{#operations}}
35+
{{#operation}}
36+
37+
override def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : F[{{{vendorExtensions.x-response-type}}}] = {
38+
{{operationId}}Handler({{{vendorExtensions.x-param-list}}})
39+
}
1940
{{/operation}}
2041
{{/operations}}
2142
}
43+
44+
def apply() : {{classname}}Service[Try] = {{classname}}Service.Handler[Try](
45+
{{#operations}}
46+
{{#operation}}
47+
({{#allParams}}_{{^-last}}, {{/-last}}{{/allParams}}) => notImplemented("{{operationId}}"){{^-last}}, {{/-last}}
48+
{{/operation}}
49+
{{/operations}}
50+
)
51+
52+
private def notImplemented(name : String) = Failure(new Exception(s"TODO: $name not implemented"))
2253
}
2354

2455
/**
2556
* The {{classname}} business-logic
57+
*
58+
*
59+
* The 'asHandler' will return an implementation which allows for easily overriding individual operations.
60+
*
61+
* equally there are "on<Function>" helper methods for easily overriding individual functions
62+
*
63+
* @tparam F the effect type (Future, Try, IO, ID, etc) of the operations
2664
*/
27-
trait {{classname}}Service {
65+
trait {{classname}}Service[F[_]] {
2866
{{#operations}}
2967
{{#operation}}
3068
/** {{{summary}}}
3169
* {{{description}}}
3270
* @return {{returnType}}
3371
*/
34-
def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}}
72+
def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : F[{{{vendorExtensions.x-response-type}}}]
73+
74+
/**
75+
* override {{operationId}} with the given handler
76+
* @return a new implementation of {{classname}}Service[F] with {{operationId}} overridden using the given handler
77+
*/
78+
final def {{vendorExtensions.x-handlerName}}(handler : ({{{vendorExtensions.x-param-list-typed}}}) => F[{{{vendorExtensions.x-response-type}}}]) : {{classname}}Service[F] = {
79+
asHandler.copy({{operationId}}Handler = handler)
80+
}
3581
{{/operation}}
3682
{{/operations}}
83+
84+
/**
85+
* @return a Handler implementation of this service
86+
*/
87+
final def asHandler : {{classname}}Service.Handler[F] = this match {
88+
case h : {{classname}}Service.Handler[F] => h
89+
case _ =>
90+
{{classname}}Service.Handler[F](
91+
{{#operations}}
92+
{{#operation}}
93+
({{{vendorExtensions.x-param-list}}}) => {{operationId}}({{{vendorExtensions.x-param-list}}}){{^-last}}, {{/-last}}
94+
{{/operation}}
95+
{{/operations}}
96+
)
97+
}
98+
99+
/**
100+
* This function will change the effect type of this service.
101+
*
102+
* It's not unlike a typical map operation from A => B, except we're not mapping
103+
* a type from A to B, but rather from F[A] => G[A] using the 'changeEffect' function.
104+
*
105+
* For, this could turn an asynchronous service (one which returns Future[_] types) into
106+
* a synchronous one (one which returns Try[_] types) by awaiting on the Future.
107+
*
108+
* It could change an IO type (like cats effect or ZIO) into an ID[A] which is just:
109+
* ```
110+
* type ID[A] => A
111+
* ```
112+
*
113+
* @tparam G the new "polymorphic" effect type
114+
* @param changeEffect the "natural transformation" which can change one effect type into another
115+
* @return a new {{classname}}Service service implementation with effect type [G]
116+
*/
117+
final def mapEffect[G[_]](changeEffect : [A] => F[A] => G[A]) : {{classname}}Service[G] = {
118+
val self = this
119+
120+
new {{classname}}Service[G] {
121+
{{#operations}}
122+
{{#operation}}
123+
override def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : G[{{{vendorExtensions.x-response-type}}}] = changeEffect {
124+
self.{{operationId}}({{{vendorExtensions.x-param-list}}})
125+
}
126+
{{/operation}}
127+
{{/operations}}
128+
}
129+
}
37130
}

modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
//> using lib "com.lihaoyi::cask:0.9.2"
33
//> using lib "com.lihaoyi::scalatags:0.8.2"
44
{{>licenseInfo}}
5-
65
// this file was generated from app.mustache
76
package {{packageName}}
87

8+
import scala.util.Try
99
{{#imports}}import {{import}}
1010
{{/imports}}
1111
import _root_.{{modelPackage}}.*
@@ -20,16 +20,17 @@ import _root_.{{apiPackage}}.*
2020
* If you wanted fine-grained control over the routes and services, you could
2121
* extend the cask.MainRoutes and mix in this trait by using this:
2222
*
23-
* \{\{\{
23+
* ```
2424
* override def allRoutes = appRoutes
25-
* \}\}\}
25+
* ```
2626
*
2727
* More typically, however, you would extend the 'BaseApp' class
2828
*/
2929
trait AppRoutes {
3030
{{#operations}}
31-
def app{{classname}}Service : {{classname}}Service = {{classname}}Service()
31+
def app{{classname}}Service : {{classname}}Service[Try] = {{classname}}Service()
3232
def routeFor{{classname}} : {{classname}}Routes = {{classname}}Routes(app{{classname}}Service)
33+
3334
{{/operations}}
3435

3536
def appRoutes = Seq(

modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// this file was generated from app.mustache
77
package {{packageName}}
88

9+
import scala.util.Try
910
{{#imports}}import {{import}}
1011
{{/imports}}
1112
import _root_.{{modelPackage}}.*
@@ -16,7 +17,7 @@ import _root_.{{apiPackage}}.*
1617
* passing in the custom business logic services
1718
*/
1819
class BaseApp({{#operations}}
19-
override val app{{classname}}Service : {{classname}}Service = {{classname}}Service(),
20+
override val app{{classname}}Service : {{classname}}Service[Try] = {{classname}}Service(),
2021
{{/operations}}
2122
override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {
2223

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

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package {{modelPackage}}
44
{{#imports}}import {{import}}
55
{{/imports}}
6+
67
import scala.util.control.NonFatal
78

89
// see https://com-lihaoyi.github.io/upickle/
@@ -11,52 +12,13 @@ import upickle.default.*
1112

1213
{{#models}}
1314
{{#model}}
14-
case class {{classname}}(
15-
{{#vars}}
16-
{{#description}}
17-
/* {{{description}}} */
18-
{{/description}}
19-
{{name}}: {{#isEnum}}{{^required}}Option[{{/required}}{{classname}}.{{datatypeWithEnum}}{{^required}}]{{/required}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-model}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-model}}} {{/required}}{{^-last}},{{/-last}}
20-
{{/vars}}
21-
22-
{{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}}
23-
) {
24-
25-
def asJsonString: String = asData.asJsonString
26-
def asJson: ujson.Value = asData.asJson
27-
28-
def asData : {{classname}}Data = {
29-
{{classname}}Data(
30-
{{#vars}}
31-
{{name}} = {{name}}{{#vendorExtensions.x-map-asModel}}.map(_.asData){{/vendorExtensions.x-map-asModel}}{{#vendorExtensions.x-wrap-in-optional}}.getOrElse({{{defaultValue}}}){{/vendorExtensions.x-wrap-in-optional}}{{^-last}},{{/-last}}
32-
{{/vars}}
33-
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
34-
)
35-
}
36-
}
37-
38-
object {{classname}} {
39-
given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel)
40-
41-
enum Fields(val fieldName : String) extends Field(fieldName) {
42-
{{#vars}}
43-
case {{name}} extends Fields("{{name}}")
44-
{{/vars}}
45-
}
46-
47-
{{#vars}}
48-
{{#isEnum}}
49-
// baseName={{{baseName}}}
50-
// nameInCamelCase = {{{nameInCamelCase}}}
51-
enum {{datatypeWithEnum}} derives ReadWriter {
52-
{{#_enum}}
53-
case {{.}}
54-
{{/_enum}}
55-
}
56-
{{/isEnum}}
57-
{{/vars}}
5815

59-
}
16+
{{#isEnum}}
17+
{{>modelEnum}}
18+
{{/isEnum}}
19+
{{^isEnum}}
20+
{{>modelClass}}
21+
{{/isEnum}}
6022

6123
{{/model}}
6224
{{/models}}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
case class {{classname}}(
3+
{{#vars}}
4+
{{#description}}
5+
/* {{{description}}} */
6+
{{/description}}
7+
{{name}}: {{#isEnum}}{{^required}}Option[{{/required}}{{classname}}.{{datatypeWithEnum}}{{^required}}]{{/required}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-model}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-model}}} {{/required}}{{^-last}},{{/-last}}
8+
{{/vars}}
9+
10+
{{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}}
11+
) {
12+
13+
def asJsonString: String = asData.asJsonString
14+
def asJson: ujson.Value = asData.asJson
15+
16+
def asData : {{classname}}Data = {
17+
{{classname}}Data(
18+
{{#vars}}
19+
{{name}} = {{{vendorExtensions.x-asData}}}{{^-last}},{{/-last}}
20+
{{/vars}}
21+
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
22+
)
23+
}
24+
}
25+
26+
object {{classname}} {
27+
given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel)
28+
29+
enum Fields(val fieldName : String) extends Field(fieldName) {
30+
{{#vars}}
31+
case {{name}} extends Fields("{{name}}")
32+
{{/vars}}
33+
}
34+
35+
{{#vars}}
36+
{{#isEnum}}
37+
// baseName={{{baseName}}}
38+
// nameInCamelCase = {{{nameInCamelCase}}}
39+
enum {{datatypeWithEnum}} derives ReadWriter {
40+
{{#_enum}}
41+
case {{.}}
42+
{{/_enum}}
43+
}
44+
{{/isEnum}}
45+
{{/vars}}
46+
47+
}

0 commit comments

Comments
 (0)