Skip to content

Commit 68daf0b

Browse files
committed
fix(openapi): make API its own gradle subproject
If the openAPI will be out basis for provided endpoints, we will respectively need the YAMLs and generated artifacts in several places. Compiling everything in the model-server is quite a lot of code which is now isolated in its own subproject.
1 parent bbdb5c6 commit 68daf0b

File tree

15 files changed

+272
-95
lines changed

15 files changed

+272
-95
lines changed

api/build.gradle.kts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
2+
3+
plugins {
4+
kotlin("jvm")
5+
kotlin("plugin.serialization")
6+
alias(libs.plugins.openapi.generator)
7+
}
8+
9+
description = "OpenAPI specifications for modelix components"
10+
11+
defaultTasks.add("build")
12+
13+
java {
14+
toolchain {
15+
languageVersion.set(JavaLanguageVersion.of(11))
16+
}
17+
}
18+
19+
dependencies {
20+
implementation(kotlin("stdlib"))
21+
implementation(project(":model-api"))
22+
implementation(project(":model-server-api"))
23+
24+
implementation(libs.google.gson)
25+
26+
implementation(libs.ktor.server.core)
27+
implementation(libs.ktor.server.resources)
28+
}
29+
30+
// OpenAPI integration
31+
val basePackage = project.group.toString()
32+
val openAPIgenerationPath = "${project.layout.buildDirectory.get()}/generated/openapi"
33+
34+
// Pairs of the different OpenAPI files we use. Each pair must have its own 'category' as first argument as these
35+
// are used to generate corresponding packages
36+
val openApiFiles = listOf(
37+
"public" to "model-server",
38+
"operative" to "model-server-operative",
39+
"light" to "model-server-light",
40+
"html" to "model-server-html",
41+
"deprecated" to "model-server-deprecated",
42+
)
43+
44+
// generate tasks for each OpenAPI file
45+
openApiFiles.forEach {
46+
val targetTaskName = "openApiGenerate-${it.second}"
47+
val targetPackageName = "$basePackage.api.${it.first}"
48+
val outputPath = "$openAPIgenerationPath/${it.first}"
49+
tasks.register<GenerateTask>(targetTaskName) {
50+
// we let the Gradle OpenAPI generator plugin build data classes and API interfaces based on the provided
51+
// OpenAPI specification. That way, the code is forced to stay in sync with the API specification.
52+
generatorName.set("kotlin-server")
53+
inputSpec.set(layout.projectDirectory.dir("openapi/specifications").file("${it.second}.yaml").toString())
54+
outputDir.set(outputPath)
55+
packageName.set(targetPackageName)
56+
apiPackage.set(targetPackageName)
57+
modelPackage.set(targetPackageName)
58+
// We use patched mustache so that only the necessary parts (i.e. resources and models)
59+
// are generated. additionally we patch the used serialization framework as the `ktor` plugin
60+
// uses a different one than we do in the model-server. The templates are based on
61+
// https://github.com/OpenAPITools/openapi-generator/tree/809b3331a95b3c3b7bcf025d16ae09dc0682cd69/modules/openapi-generator/src/main/resources/kotlin-server
62+
templateDir.set("$projectDir/openapi/templates")
63+
configOptions.set(
64+
mapOf(
65+
// we use the ktor generator to generate server side resources and model (i.e. data classes)
66+
"library" to "ktor",
67+
// the generated artifacts are not built independently, thus no dedicated build files have to be generated
68+
"omitGradleWrapper" to "true",
69+
// the path to resource generation we need
70+
"featureResources" to "true",
71+
// disable features we do not use
72+
"featureAutoHead" to "false",
73+
"featureCompression" to "false",
74+
"featureHSTS" to "false",
75+
"featureMetrics" to "false",
76+
),
77+
)
78+
// generate only Paths and Models - only this set will produce the intended Paths.kt as well as the models
79+
// the openapi generator is generally very picky and configuring it is rather complex
80+
globalProperties.putAll(
81+
mapOf(
82+
"models" to "",
83+
"apis" to "",
84+
"supportingFiles" to "Paths.kt",
85+
),
86+
)
87+
}
88+
89+
// Ensure that the OpenAPI generator runs before starting to compile
90+
tasks.named("processResources") {
91+
dependsOn(targetTaskName)
92+
}
93+
tasks.named("compileKotlin") {
94+
dependsOn(targetTaskName)
95+
}
96+
tasks.named("runKtlintCheckOverMainSourceSet") {
97+
dependsOn(targetTaskName)
98+
}
99+
100+
// do not apply ktlint on the generated files
101+
ktlint {
102+
filter {
103+
exclude {
104+
it.file.toPath().toAbsolutePath().startsWith(outputPath)
105+
}
106+
}
107+
}
108+
109+
// add openAPI generated artifacts to the sourceSets
110+
sourceSets["main"].kotlin.srcDir("$outputPath/src/main/kotlin")
111+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# WARNING - EXPERIMENTAL
2+
# This file was auto generated from the existing API using an IntelliJ plugin,
3+
# see https://www.jetbrains.com/help/idea/openapi.html#generate_openapi
4+
#
5+
# Manual changes were done for this 'spec' to work in-place with the
6+
# model-server for now. A lot of changes were done and are still necessary
7+
# to make this OpenAPI a viable artifact. It will most likely be split
8+
# into multiple OpenAPI files.
9+
10+
openapi: "3.0.3"
11+
12+
info:
13+
title: "model-server operative API"
14+
description: "modelix operative API"
15+
version: "1.0.0"
16+
17+
servers:
18+
- url: '/'
19+
description: model-server
20+
21+
paths:
22+
/metrics:
23+
get:
24+
operationId: getMetrics
25+
responses:
26+
"200":
27+
$ref: '#/components/responses/200'
28+
29+
components:
30+
responses:
31+
"200":
32+
description: OK
33+
content:
34+
text/plain:
35+
schema:
36+
type: string

api/model-server.yaml renamed to api/openapi/specifications/model-server.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -553,12 +553,6 @@ paths:
553553
$ref: '#/components/responses/200'
554554
"404":
555555
$ref: '#/components/responses/404'
556-
/metrics:
557-
get:
558-
operationId: getMetrics
559-
responses:
560-
"200":
561-
$ref: '#/components/responses/200'
562556
components:
563557
responses:
564558
"200":
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package {{packageName}}
2+
3+
import io.ktor.server.application.*
4+
import io.ktor.http.*
5+
{{#featureResources}}
6+
import io.ktor.server.resources.*
7+
{{/featureResources}}
8+
9+
import io.ktor.server.routing.*
10+
11+
fun Application.main() {
12+
{{#generateApis}}
13+
install(Routing) {
14+
{{#apiInfo}}
15+
{{#apis}}
16+
{{#operations}}
17+
{{classname}}()
18+
{{/operations}}
19+
{{/apis}}
20+
{{/apiInfo}}
21+
}
22+
23+
{{/generateApis}}
24+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{{>licenseInfo}}
2+
package {{packageName}}
3+
4+
import io.ktor.resources.*
5+
import kotlinx.serialization.*
6+
{{#imports}}import {{import}}
7+
{{/imports}}
8+
9+
{{#apiInfo}}
10+
object Paths {
11+
{{#apis}}
12+
{{#operations}}
13+
{{#operation}}
14+
/**{{#summary}}
15+
* {{.}}{{/summary}}
16+
* {{unescapedNotes}}
17+
{{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
18+
{{/allParams}}*/
19+
{{#hasParams}}
20+
@Serializable @Resource("{{path}}") class {{operationId}}({{#allParams}}val {{paramName}}: {{{dataType}}}{{^required}}? = null{{/required}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}})
21+
{{/hasParams}}
22+
{{^hasParams}}
23+
@Serializable @Resource("{{path}}") class {{operationId}}
24+
{{/hasParams}}
25+
26+
{{/operation}}
27+
{{/operations}}
28+
{{/apis}}
29+
}
30+
{{/apiInfo}}

api/openapi/templates/api.mustache

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{{>licenseInfo}}
2+
package {{apiPackage}}
3+
4+
import com.google.gson.Gson
5+
import io.ktor.http.*
6+
import io.ktor.server.application.*
7+
import io.ktor.server.response.*
8+
{{#featureResources}}
9+
import {{packageName}}.Paths
10+
import io.ktor.server.resources.options
11+
import io.ktor.server.resources.get
12+
import io.ktor.server.resources.post
13+
import io.ktor.server.resources.put
14+
import io.ktor.server.resources.delete
15+
import io.ktor.server.resources.head
16+
import io.ktor.server.resources.patch
17+
{{/featureResources}}
18+
import io.ktor.server.routing.*
19+
{{#imports}}import {{import}}
20+
{{/imports}}
21+
22+
{{#operations}}
23+
fun Route.{{classname}}() {
24+
val gson = Gson()
25+
val empty = mutableMapOf<String, Any?>()
26+
27+
{{#operation}}
28+
{{^featureResources}}
29+
route("{{path}}") {
30+
{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} {
31+
{{#lambda.indented_12}}{{>libraries/ktor/_api_body}}{{/lambda.indented_12}}
32+
}
33+
}
34+
{{/featureResources}}
35+
{{#featureResources}}
36+
{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}<Paths.{{operationId}}> {
37+
{{#lambda.indented_8}}{{>libraries/ktor/_api_body}}{{/lambda.indented_8}}
38+
}
39+
{{/featureResources}}
40+
41+
{{/operation}}
42+
}
43+
{{/operations}}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import kotlinx.serialization.Serializable
2+
3+
/**
4+
* {{{description}}}
5+
{{#vars}}
6+
* @param {{{name}}} {{{description}}}
7+
{{/vars}}
8+
*/
9+
@Serializable
10+
data class {{classname}}(
11+
{{#requiredVars}}
12+
{{>data_class_req_var}}{{^-last}},
13+
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
14+
{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}},
15+
{{/-last}}{{/optionalVars}}
16+
)

0 commit comments

Comments
 (0)