Skip to content

Commit 2abc6b0

Browse files
committed
feat(model-server): implement BranchV1 for branches
This adds a new media type for requesting branch metadata. The commit also prepares the openapi-generator templates to support media-type versioning by optionally being able to declare separate handler methods per media type. For registering types for JSON serialization, the model needs to be annotated with the intended media type via the x-modelix-media-type vendor extension.
1 parent 43922d7 commit 2abc6b0

File tree

9 files changed

+211
-12
lines changed

9 files changed

+211
-12
lines changed

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ ktor-serialization = { group = "io.ktor", name = "ktor-serialization", version.r
8282
ktor-serialization-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
8383

8484
keycloak-authz-client = { group = "org.keycloak", name = "keycloak-authz-client", version = "25.0.1" }
85+
86+
kotest-assertions-coreJvm = { group = "io.kotest", name = "kotest-assertions-core-jvm", version = "5.9.1" }
87+
kotest-assertions-ktor = { group = "io.kotest.extensions", name = "kotest-assertions-ktor", version = "2.0.0" }
88+
8589
guava = { group = "com.google.guava", name = "guava", version = "33.2.1-jre" }
8690
org-json = { group = "org.json", name = "json", version = "20240303" }
8791
google-oauth-client = { group = "com.google.oauth-client", name = "google-oauth-client", version = "1.36.0" }

model-server-openapi/specifications/model-server-v2.yaml

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,36 @@ paths:
140140
required: true
141141
schema:
142142
type: string
143+
x-modelix-media-type-handlers:
144+
- v1:
145+
- 'application/x.modelix.branch+json;version=1'
146+
- delta:
147+
- 'application/x-modelix-objects-v2'
148+
- 'application/x-modelix-objects'
149+
- 'application/json'
150+
- 'text/plain'
151+
- '*/*'
143152
responses:
144153
"404":
145154
$ref: '#/components/responses/404'
146155
"200":
147-
$ref: '#/components/responses/versionDelta'
148-
# content:
149-
# '*/*':
150-
# schema:
151-
# $ref: "#/components/schemas/VersionDelta"
156+
description: "Information about a branch for content type `application/x.modelix.branch+json;version=*'. Else all model data of the branch in version delta format."
157+
content:
158+
'application/x.modelix.branch+json;version=1':
159+
schema:
160+
$ref: "#/components/schemas/BranchV1"
161+
'application/x-modelix-objects-v2':
162+
schema:
163+
type: string
164+
'application/x-modelix-objects':
165+
schema:
166+
type: string
167+
'application/json':
168+
schema:
169+
type: object
170+
'text/plain':
171+
schema:
172+
type: string
152173
default:
153174
$ref: '#/components/responses/GeneralError'
154175
post:
@@ -538,3 +559,15 @@ components:
538559
type: string
539560
value2:
540561
type: string
562+
BranchV1:
563+
x-modelix-media-type: 'application/x.modelix.branch+json;version=1'
564+
type: object
565+
properties:
566+
name:
567+
type: string
568+
current_hash:
569+
type: string
570+
example: 7fQeo*xrdfZuHZtaKhbp0OosarV5tVR8N3pW8JPkl7ZE
571+
required:
572+
- name
573+
- current_hash

model-server/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ dependencies {
6666

6767
testImplementation(libs.bundles.apache.cxf)
6868
testImplementation(libs.junit)
69+
testImplementation(libs.kotest.assertions.coreJvm)
70+
testImplementation(libs.kotest.assertions.ktor)
6971
testImplementation(libs.cucumber.java)
7072
testImplementation(libs.ktor.server.test.host)
7173
testImplementation(libs.kotlin.coroutines.test)

model-server/src/main/kotlin/org/modelix/model/server/Main.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import kotlinx.serialization.json.Json
5555
import org.apache.commons.io.FileUtils
5656
import org.apache.ignite.Ignition
5757
import org.modelix.api.v1.Problem
58+
import org.modelix.api.v2.Paths.registerJsonTypes
5859
import org.modelix.authorization.KeycloakUtils
5960
import org.modelix.authorization.NoPermissionException
6061
import org.modelix.authorization.NotLoggedInException
@@ -203,6 +204,7 @@ object Main {
203204
}
204205
install(ContentNegotiation) {
205206
json()
207+
registerJsonTypes()
206208
}
207209
install(CORS) {
208210
anyHost()

model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.flow
3939
import kotlinx.coroutines.flow.onEmpty
4040
import kotlinx.coroutines.flow.withIndex
4141
import kotlinx.coroutines.withContext
42+
import org.modelix.api.v2.BranchV1
4243
import org.modelix.api.v2.DefaultApi
4344
import org.modelix.authorization.getUserName
4445
import org.modelix.model.InMemoryModels
@@ -94,7 +95,9 @@ class ModelReplicationServer(
9495
}
9596
}
9697

97-
private fun repositoryId(paramValue: String?) = RepositoryId(checkNotNull(paramValue) { "Parameter 'repository' not available" })
98+
private fun repositoryId(paramValue: String?) =
99+
RepositoryId(checkNotNull(paramValue) { "Parameter 'repository' not available" })
100+
98101
private suspend fun <R> runWithRepository(repository: String, body: suspend () -> R): R {
99102
return repositoriesManager.runWithRepository(repositoryId(repository), body)
100103
}
@@ -107,7 +110,7 @@ class ModelReplicationServer(
107110
call.respondText(repositoriesManager.getBranchNames(repositoryId(repository)).joinToString("\n"))
108111
}
109112

110-
override suspend fun PipelineContext<Unit, ApplicationCall>.getRepositoryBranch(
113+
override suspend fun PipelineContext<Unit, ApplicationCall>.getRepositoryBranchDelta(
111114
repository: String,
112115
branch: String,
113116
lastKnown: String?,
@@ -119,6 +122,18 @@ class ModelReplicationServer(
119122
}
120123
}
121124

125+
override suspend fun PipelineContext<Unit, ApplicationCall>.getRepositoryBranchV1(
126+
repository: String,
127+
branch: String,
128+
lastKnown: String?,
129+
) {
130+
runWithRepository(repository) {
131+
val branchRef = repositoryId(repository).getBranchReference(branch)
132+
val versionHash = repositoriesManager.getVersionHash(branchRef) ?: throw BranchNotFoundException(branchRef)
133+
call.respond(BranchV1(branch, versionHash))
134+
}
135+
}
136+
122137
override suspend fun PipelineContext<Unit, ApplicationCall>.deleteRepositoryBranch(
123138
repository: String,
124139
branch: String,

model-server/src/main/resources/openapi/templates/Paths.kt.mustache

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package {{packageName}}
33

44
import io.ktor.resources.*
55
import kotlinx.serialization.*
6+
import io.ktor.http.ContentType
7+
import io.ktor.serialization.kotlinx.KotlinxSerializationConverter
8+
import io.ktor.serialization.kotlinx.json.DefaultJson
9+
import io.ktor.server.plugins.contentnegotiation.ContentNegotiationConfig
610
{{#imports}}import {{import}}
711
{{/imports}}
812

@@ -26,5 +30,22 @@ object Paths {
2630
{{/operation}}
2731
{{/operations}}
2832
{{/apis}}
33+
34+
/**
35+
* Registers all models from /components/schemas with an x-modelix-media-type vendor extension to be serializable
36+
* as JSON for that media type.
37+
*/
38+
fun ContentNegotiationConfig.registerJsonTypes() {
39+
{{#models}}
40+
{{#model}}
41+
{{#vendorExtensions}}
42+
{{#x-modelix-media-type}}
43+
register(ContentType.parse("{{{.}}}"), KotlinxSerializationConverter(DefaultJson))
44+
{{/x-modelix-media-type}}
45+
{{/vendorExtensions}}
46+
{{/model}}
47+
{{/models}}
48+
}
49+
2950
}
3051
{{/apiInfo}}

model-server/src/main/resources/openapi/templates/api.mustache

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package {{apiPackage}}
33

44
import io.ktor.http.*
55
import io.ktor.server.application.*
6+
import io.ktor.server.request.*
67
import io.ktor.server.response.*
78
{{#featureResources}}
89
import {{packageName}}.Paths
@@ -22,21 +23,50 @@ import io.ktor.util.pipeline.PipelineContext
2223
abstract class {{classname}} {
2324
{{#operations}}
2425
{{#operation}}
26+
27+
{{#vendorExtensions}}
28+
{{#x-modelix-media-type-handlers}}
29+
{{#entrySet}}
30+
2531
/**{{#summary}}
2632
* {{.}}{{/summary}}
2733
*
2834
* {{unescapedNotes}}
2935
*
3036
* {{httpMethod}} {{path}}
3137
*
32-
{{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
33-
{{/allParams}}
38+
{{#allParams}}
39+
* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
40+
{{/allParams}}
41+
*/
42+
{{#isDeprecated}}
43+
@Deprecated("deprecated flag is set in the OpenAPI specification")
44+
{{/isDeprecated}}
45+
abstract suspend fun PipelineContext<Unit, ApplicationCall>.{{operationId}}{{#lambda.titlecase}}{{key}}{{/lambda.titlecase}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}})
46+
47+
{{/entrySet}}
48+
{{/x-modelix-media-type-handlers}}
49+
{{^x-modelix-media-type-handlers}}
50+
51+
/**{{#summary}}
52+
* {{.}}{{/summary}}
53+
*
54+
* {{unescapedNotes}}
55+
*
56+
* {{httpMethod}} {{path}}
57+
*
58+
{{#allParams}}
59+
* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
60+
{{/allParams}}
3461
*/
3562
{{#isDeprecated}}
3663
@Deprecated("deprecated flag is set in the OpenAPI specification")
3764
{{/isDeprecated}}
3865
abstract suspend fun PipelineContext<Unit, ApplicationCall>.{{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}})
3966

67+
{{/x-modelix-media-type-handlers}}
68+
{{/vendorExtensions}}
69+
4070
{{/operation}}
4171
{{/operations}}
4272

@@ -55,9 +85,30 @@ abstract class {{classname}} {
5585
{{#operations}}
5686
{{#operation}}
5787
protected open fun Route.install_{{operationId}}() {
88+
{{#vendorExtensions}}
89+
{{#x-modelix-media-type-handlers}}
90+
{{#entrySet}}
91+
92+
accept(
93+
{{#value}}
94+
ContentType.parse("{{{.}}}"),
95+
{{/value}}
96+
) {
97+
{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}<Paths.{{operationId}}> { parameters ->
98+
{{operationId}}{{#lambda.titlecase}}{{key}}{{/lambda.titlecase}}({{#allParams}}parameters.{{paramName}}{{^-last}}, {{/-last}}{{/allParams}})
99+
}
100+
}
101+
102+
{{/entrySet}}
103+
{{/x-modelix-media-type-handlers}}
104+
{{^x-modelix-media-type-handlers}}
105+
58106
{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}<Paths.{{operationId}}> { parameters ->
59-
{{#lambda.indented_8}}{{operationId}}({{#allParams}}parameters.{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}){{/lambda.indented_8}}
107+
{{operationId}}({{#allParams}}parameters.{{paramName}}{{^-last}}, {{/-last}}{{/allParams}})
60108
}
109+
110+
{{/x-modelix-media-type-handlers}}
111+
{{/vendorExtensions}}
61112
}
62113
{{/operation}}
63114
{{/operations}}

model-server/src/test/kotlin/org/modelix/model/server/ModelServerTestUtil.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.ktor.server.routing.IgnoreTrailingSlash
2727
import io.ktor.server.testing.ApplicationTestBuilder
2828
import io.ktor.server.websocket.WebSockets
2929
import kotlinx.coroutines.runBlocking
30+
import org.modelix.api.v2.Paths.registerJsonTypes
3031
import org.modelix.authorization.installAuthentication
3132
import org.modelix.model.client2.ModelClientV2
3233
import org.modelix.model.server.Main.installStatusPages
@@ -38,7 +39,10 @@ suspend fun ApplicationTestBuilder.createModelClient(): ModelClientV2 {
3839

3940
fun Application.installDefaultServerPlugins() {
4041
install(WebSockets)
41-
install(ContentNegotiation) { json() }
42+
install(ContentNegotiation) {
43+
json()
44+
registerJsonTypes()
45+
}
4246
install(Resources)
4347
install(IgnoreTrailingSlash)
4448
installStatusPages()

0 commit comments

Comments
 (0)