Skip to content

Commit 455add6

Browse files
mikkkam.tkachev
andauthored
Implement scala http4s server generator (#17430)
* Implement scala http4s server generator * Fix types and auth * Add proper handling of various responses * Fix configs * Drop null values in json encoder * Add sample files --------- Co-authored-by: m.tkachev <m.tkachev@tinkoff.ru>
1 parent 11caad9 commit 455add6

File tree

27 files changed

+2713
-0
lines changed

27 files changed

+2713
-0
lines changed

.github/workflows/samples-scala.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
- samples/server/petstore/scala-pekko-http-server
3232
- samples/server/petstore/scalatra
3333
- samples/server/petstore/scala-finch # cannot be tested with jdk11
34+
- samples/server/petstore/scala-http4s-server
3435
steps:
3536
- uses: actions/checkout@v4
3637
- uses: actions/setup-java@v4

bin/configs/scala-http4s-server.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
generatorName: scala-http4s-server
2+
outputDir: samples/server/petstore/scala-http4s-server
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/scala-http4s-server
5+
additionalProperties:
6+
artifactId: openapi-scala-http4s-server

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

Lines changed: 852 additions & 0 deletions
Large diffs are not rendered by default.

modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ org.openapitools.codegen.languages.ScalaPekkoClientCodegen
118118
org.openapitools.codegen.languages.ScalaAkkaHttpServerCodegen
119119
org.openapitools.codegen.languages.ScalaFinchServerCodegen
120120
org.openapitools.codegen.languages.ScalaGatlingCodegen
121+
org.openapitools.codegen.languages.ScalaHttp4sServerCodegen
121122
org.openapitools.codegen.languages.ScalaLagomServerCodegen
122123
org.openapitools.codegen.languages.ScalaPlayFrameworkServerCodegen
123124
org.openapitools.codegen.languages.ScalaSttpClientCodegen
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package {{apiPackage}}
2+
3+
import {{apiPackage}}.path._
4+
import {{apiPackage}}.query._
5+
6+
{{#imports}}import {{import}}
7+
{{/imports}}
8+
9+
{{#extraImports}}import {{.}}
10+
{{/extraImports}}
11+
12+
import cats.Monad
13+
import cats.syntax.all._
14+
15+
import org.http4s._
16+
import org.http4s.circe._
17+
import org.http4s.server._
18+
import org.http4s.headers._
19+
import org.http4s.dsl.Http4sDsl
20+
import org.http4s.circe.CirceEntityEncoder._
21+
22+
final case class {{classname}}Routes[
23+
F[_]: JsonDecoder: Monad{{#allAuth}}, {{.}}{{/allAuth}}
24+
](delegate: {{classname}}Delegate[F{{#allAuth}}, {{.}}{{/allAuth}}]) extends Http4sDsl[F] {
25+
{{#operations}}
26+
{{#operation}}
27+
object {{operationId}} {
28+
import {{classname}}Delegate.{{operationId}}Responses
29+
30+
{{#pathParams}}
31+
{{#vendorExtensions.x-refined}}
32+
object {{baseName}}Varr extends RefinedVarr[{{vendorExtensions.x-refined-lft}}, {{{vendorExtensions.x-refined-rgt}}}]
33+
{{/vendorExtensions.x-refined}}
34+
{{/pathParams}}
35+
{{#queryParams}}
36+
{{#isArray}}
37+
{{#required}}
38+
object {{baseName}}QueryParam extends QuerySeqParamDecoderMatcher[{{{items.vendorExtensions.x-type}}}]("{{baseName}}")
39+
{{/required}}
40+
{{^required}}
41+
object {{baseName}}QueryParam extends OptionalQuerySeqParamDecoderMatcher[{{{items.vendorExtensions.x-type}}}]("{{baseName}}")
42+
{{/required}}
43+
{{/isArray}}
44+
{{^isArray}}
45+
{{#required}}
46+
object {{baseName}}QueryParam extends QueryParamDecoderMatcher[{{{vendorExtensions.x-type}}}]("{{baseName}}")
47+
{{/required}}
48+
{{^required}}
49+
object {{baseName}}QueryParam extends OptionalQueryParamDecoderMatcher[{{{vendorExtensions.x-type}}}]("{{baseName}}")
50+
{{/required}}
51+
{{/isArray}}
52+
{{/queryParams}}
53+
54+
{{^vendorExtensions.x-authed}}
55+
val route = HttpRoutes.of[F] {
56+
case req @ {{{httpMethod}}} -> Root{{{vendorExtensions.x-codegen-path}}}{{{vendorExtensions.x-codegen-query}}} =>
57+
{{#vendorExtensions.x-json-body}}
58+
{{#vendorExtensions.x-generic-body}}
59+
req.contentType match {
60+
case Some(`Content-Type`(MediaType.application.json, _)) =>
61+
{{>delegateCallJson}}
62+
case _ =>
63+
{{>delegateCallGeneric}}
64+
}
65+
{{/vendorExtensions.x-generic-body}}
66+
{{^vendorExtensions.x-generic-body}}
67+
{{>delegateCallJson}}
68+
{{/vendorExtensions.x-generic-body}}
69+
{{/vendorExtensions.x-json-body}}
70+
{{^vendorExtensions.x-json-body}}
71+
{{>delegateCallGeneric}}
72+
{{/vendorExtensions.x-json-body}}
73+
}
74+
75+
{{/vendorExtensions.x-authed}}
76+
{{#vendorExtensions.x-authed}}
77+
val route{{authName}} = AuthedRoutes.of[{{authName}}, F] {
78+
case (req @ {{{httpMethod}}} -> Root{{{vendorExtensions.x-codegen-path}}}{{{vendorExtensions.x-codegen-query}}}) as auth =>
79+
{{#vendorExtensions.x-json-body}}
80+
{{#vendorExtensions.x-generic-body}}
81+
req.contentType match {
82+
case Some(`Content-Type`(MediaType.application.json, _)) =>
83+
{{>delegateCallJson}}
84+
case _ =>
85+
{{>delegateCallGeneric}}
86+
}
87+
{{/vendorExtensions.x-generic-body}}
88+
{{^vendorExtensions.x-generic-body}}
89+
{{>delegateCallJson}}
90+
{{/vendorExtensions.x-generic-body}}
91+
{{/vendorExtensions.x-json-body}}
92+
{{^vendorExtensions.x-json-body}}
93+
{{>delegateCallGeneric}}
94+
{{/vendorExtensions.x-json-body}}
95+
}
96+
{{/vendorExtensions.x-authed}}
97+
98+
val responses: {{operationId}}Responses[F] = new {{operationId}}Responses[F] {
99+
{{#responses}}
100+
{{#vendorExtensions.x-response-location}}
101+
{{#vendorExtensions.x-json-response}}
102+
def resp{{code}}(location: Location, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(location, value)
103+
{{/vendorExtensions.x-json-response}}
104+
{{#vendorExtensions.x-generic-response}}
105+
def resp{{code}}(location: Location): F[Response[F]] = {{vendorExtensions.x-response}}(location)
106+
{{/vendorExtensions.x-generic-response}}
107+
{{/vendorExtensions.x-response-location}}
108+
{{#vendorExtensions.x-response-www-auth}}
109+
{{#vendorExtensions.x-json-response}}
110+
def resp{{code}}(authenticate: `WWW-Authenticate`, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(authenticate, value)
111+
{{/vendorExtensions.x-json-response}}
112+
{{#vendorExtensions.x-generic-response}}
113+
def resp{{code}}(authenticate: `WWW-Authenticate`): F[Response[F]] = {{vendorExtensions.x-response}}(authenticate)
114+
{{/vendorExtensions.x-generic-response}}
115+
{{/vendorExtensions.x-response-www-auth}}
116+
{{#vendorExtensions.x-response-allow}}
117+
{{#vendorExtensions.x-json-response}}
118+
def resp{{code}}(allow: Allow, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(allow, value)
119+
{{/vendorExtensions.x-json-response}}
120+
{{#vendorExtensions.x-generic-response}}
121+
def resp{{code}}(allow: Allow): F[Response[F]] = {{vendorExtensions.x-response}}(allow)
122+
{{/vendorExtensions.x-generic-response}}
123+
{{/vendorExtensions.x-response-allow}}
124+
{{#vendorExtensions.x-response-proxy-auth}}
125+
{{#vendorExtensions.x-json-response}}
126+
def resp{{code}}(authenticate: `Proxy-Authenticate`, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(value, authenticate)
127+
{{/vendorExtensions.x-json-response}}
128+
{{#vendorExtensions.x-generic-response}}
129+
def resp{{code}}(authenticate: `Proxy-Authenticate`): F[Response[F]] = {{vendorExtensions.x-response}}(authenticate)
130+
{{/vendorExtensions.x-generic-response}}
131+
{{/vendorExtensions.x-response-proxy-auth}}
132+
{{#vendorExtensions.x-response-standard}}
133+
{{#vendorExtensions.x-json-response}}
134+
def resp{{code}}(value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(value)
135+
{{/vendorExtensions.x-json-response}}
136+
{{#vendorExtensions.x-generic-response}}
137+
def resp{{code}}(): F[Response[F]] = {{vendorExtensions.x-response}}()
138+
{{/vendorExtensions.x-generic-response}}
139+
{{/vendorExtensions.x-response-standard}}
140+
{{/responses}}
141+
}
142+
}
143+
{{/operation}}
144+
{{/operations}}
145+
146+
{{#operationsByAuth}}
147+
val routes{{auth}} =
148+
{{#ops}}
149+
{{.}}.route{{auth}}{{^-last}} <+>{{/-last}}
150+
{{/ops}}
151+
{{/operationsByAuth}}
152+
}
153+
154+
object {{classname}}Delegate {
155+
{{#operations}}
156+
{{#operation}}
157+
trait {{operationId}}Responses[F[_]] {
158+
{{#responses}}
159+
{{#vendorExtensions.x-response-location}}
160+
{{#vendorExtensions.x-json-response}}
161+
def resp{{code}}(location: Location, value: {{{dataType}}}): F[Response[F]]
162+
{{/vendorExtensions.x-json-response}}
163+
{{#vendorExtensions.x-generic-response}}
164+
def resp{{code}}(location: Location): F[Response[F]]
165+
{{/vendorExtensions.x-generic-response}}
166+
{{/vendorExtensions.x-response-location}}
167+
{{#vendorExtensions.x-response-www-auth}}
168+
{{#vendorExtensions.x-json-response}}
169+
def resp{{code}}(authenticate: `WWW-Authenticate`, value: {{{dataType}}}): F[Response[F]]
170+
{{/vendorExtensions.x-json-response}}
171+
{{#vendorExtensions.x-generic-response}}
172+
def resp{{code}}(authenticate: `WWW-Authenticate`): F[Response[F]]
173+
{{/vendorExtensions.x-generic-response}}
174+
{{/vendorExtensions.x-response-www-auth}}
175+
{{#vendorExtensions.x-response-allow}}
176+
{{#vendorExtensions.x-json-response}}
177+
def resp{{code}}(allow: Allow, value: {{{dataType}}}): F[Response[F]]
178+
{{/vendorExtensions.x-json-response}}
179+
{{#vendorExtensions.x-generic-response}}
180+
def resp{{code}}(allow: Allow): F[Response[F]]
181+
{{/vendorExtensions.x-generic-response}}
182+
{{/vendorExtensions.x-response-allow}}
183+
{{#vendorExtensions.x-response-proxy-auth}}
184+
{{#vendorExtensions.x-json-response}}
185+
def resp{{code}}(authenticate: `Proxy-Authenticate`, value: {{{dataType}}}): F[Response[F]]
186+
{{/vendorExtensions.x-json-response}}
187+
{{#vendorExtensions.x-generic-response}}
188+
def resp{{code}}(authenticate: `Proxy-Authenticate`): F[Response[F]]
189+
{{/vendorExtensions.x-generic-response}}
190+
{{/vendorExtensions.x-response-proxy-auth}}
191+
{{#vendorExtensions.x-response-standard}}
192+
{{#vendorExtensions.x-json-response}}
193+
def resp{{code}}(value: {{{dataType}}}): F[Response[F]]
194+
{{/vendorExtensions.x-json-response}}
195+
{{#vendorExtensions.x-generic-response}}
196+
def resp{{code}}(): F[Response[F]]
197+
{{/vendorExtensions.x-generic-response}}
198+
{{/vendorExtensions.x-response-standard}}
199+
{{/responses}}
200+
}
201+
202+
{{/operation}}
203+
{{/operations}}
204+
}
205+
206+
trait {{classname}}Delegate[F[_]{{#allAuth}}, {{.}}{{/allAuth}}] {
207+
{{#operations}}
208+
{{#operation}}
209+
210+
trait {{operationId}} {
211+
import {{classname}}Delegate.{{operationId}}Responses
212+
{{#vendorExtensions.x-json-body}}
213+
214+
{{^vendorExtensions.x-authed}}
215+
def handle(
216+
req: Request[F],
217+
{{operationId}}: F[{{{bodyParam.dataType}}}],
218+
{{> delegateArgs}} responses: {{operationId}}Responses[F]
219+
): F[Response[F]]
220+
{{/vendorExtensions.x-authed}}
221+
222+
{{#vendorExtensions.x-authed}}
223+
def handle_{{authName}}(
224+
auth: {{authName}},
225+
req: Request[F],
226+
{{operationId}}: F[{{{bodyParam.dataType}}}],
227+
{{> delegateArgs}} responses: {{operationId}}Responses[F]
228+
): F[Response[F]]
229+
230+
{{/vendorExtensions.x-authed}}
231+
{{/vendorExtensions.x-json-body}}
232+
233+
{{#vendorExtensions.x-generic-body}}
234+
{{^vendorExtensions.x-authed}}
235+
def handle(
236+
req: Request[F],
237+
{{> delegateArgs}} responses: {{operationId}}Responses[F]
238+
): F[Response[F]]
239+
{{/vendorExtensions.x-authed}}
240+
241+
{{#vendorExtensions.x-authed}}
242+
def handle_{{authName}}(
243+
auth: {{authName}},
244+
req: Request[F],
245+
{{> delegateArgs}} responses: {{operationId}}Responses[F]
246+
): F[Response[F]]
247+
248+
{{/vendorExtensions.x-authed}}
249+
{{/vendorExtensions.x-generic-body}}
250+
}
251+
def {{operationId}}: {{operationId}}
252+
253+
{{/operation}}
254+
{{/operations}}
255+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package {{packageName}}
2+
3+
import org.http4s.circe._
4+
import cats.Monad
5+
import cats.syntax.all._
6+
import cats.data.OptionT
7+
import cats.data.Kleisli
8+
import org.http4s._
9+
import org.http4s.server._
10+
11+
import {{apiPackage}}._
12+
13+
final case class API [
14+
F[_]: JsonDecoder: Monad{{#authMethods}}, {{name}}{{/authMethods}}
15+
](
16+
{{#authMethods}}
17+
{{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}: Kleisli[OptionT[F, *], Request[F], {{name}}],
18+
{{/authMethods}}
19+
)(
20+
{{#apiInfo}}
21+
{{#apis}}
22+
{{#operations}}
23+
delegate{{classname}}: {{classname}}Delegate[F{{#allAuth}}, {{.}}{{/allAuth}}],
24+
{{/operations}}
25+
{{/apis}}
26+
{{/apiInfo}}
27+
){
28+
{{#authToOperationMap}}
29+
{{#addMiddleware}}
30+
val {{#lambda.camelcase}}{{auth}}{{/lambda.camelcase}}Middleware = AuthMiddleware{{^-last}}.withFallThrough{{/-last}}({{#lambda.camelcase}}{{auth}}{{/lambda.camelcase}})
31+
{{/addMiddleware}}
32+
{{/authToOperationMap}}
33+
34+
{{#apiInfo}}
35+
{{#apis}}
36+
{{#operations}}
37+
val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Routes = new {{classname}}Routes(delegate{{classname}})
38+
{{/operations}}
39+
{{/apis}}
40+
{{/apiInfo}}
41+
42+
{{#authToOperationMap}}
43+
val routes{{auth}} = {{#addMiddleware}}{{#lambda.camelcase}}{{auth}}{{/lambda.camelcase}}Middleware({{/addMiddleware}}
44+
{{#ops}}
45+
{{#lambda.camelcase}}{{.}}{{/lambda.camelcase}}Routes.routes{{auth}}{{^-last}} <+>{{/-last}}
46+
{{/ops}}{{#addMiddleware}}){{/addMiddleware}}
47+
{{/authToOperationMap}}
48+
49+
val routesAll =
50+
{{#authToOperationMap}}
51+
routes{{auth}}{{^-last}} <+>{{/-last}}
52+
{{/authToOperationMap}}
53+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version=1.8.2
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
scalaVersion := "2.13.11"
2+
scalacOptions += "-Ymacro-annotations"
3+
4+
val circeVersion = "0.14.5"
5+
def circe(artifact: String): ModuleID = "io.circe" %% s"circe-$artifact" % circeVersion
6+
7+
val http4sVersion = "0.23.23"
8+
def http4s(artifact: String): ModuleID = "org.http4s" %% s"http4s-$artifact" % http4sVersion
9+
10+
val refinedVersion = "0.9.29"
11+
val refined = Seq(
12+
"eu.timepit" %% "refined" % refinedVersion,
13+
"eu.timepit" %% "refined-cats" % refinedVersion
14+
)
15+
16+
val catsVersion = "2.10.0"
17+
val cats = Seq("org.typelevel" %% "cats-core" % catsVersion)
18+
19+
lazy val compilerPlugins = Seq(
20+
compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"),
21+
compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full)
22+
)
23+
24+
libraryDependencies ++= (Seq(
25+
http4s("core"), http4s("ember-server"), http4s("circe"), http4s("dsl"),
26+
circe("core"), circe("generic"), circe("parser"), circe("refined")
27+
) ++ refined ++ cats ++ compilerPlugins)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{{#pathParams}}
2+
{{baseName}}: {{{vendorExtensions.x-type}}},
3+
{{/pathParams}}
4+
{{#queryParams}}
5+
{{#isArray}}
6+
{{#required}}
7+
{{baseName}}: List[{{{items.vendorExtensions.x-type}}}],
8+
{{/required}}
9+
{{^required}}
10+
{{baseName}}: Option[List[{{{items.vendorExtensions.x-type}}}]],
11+
{{/required}}
12+
{{/isArray}}
13+
{{^isArray}}
14+
{{#required}}
15+
{{baseName}}: {{{vendorExtensions.x-type}}},
16+
{{/required}}
17+
{{^required}}
18+
{{baseName}}: Option[{{{vendorExtensions.x-type}}}],
19+
{{/required}}
20+
{{/isArray}}
21+
{{/queryParams}}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{^authName}}
2+
delegate.{{operationId}}.handle(req, {{#pathParams}}{{baseName}}, {{/pathParams}}{{#queryParams}}{{baseName}}, {{/queryParams}}responses)
3+
{{/authName}}
4+
{{#authName}}
5+
delegate.{{operationId}}.handle_{{authName}}(auth, req, {{#pathParams}}{{baseName}}, {{/pathParams}}{{#queryParams}}{{baseName}}, {{/queryParams}}responses)
6+
{{/authName}}

0 commit comments

Comments
 (0)