Skip to content

Commit 92ce337

Browse files
authored
Merge pull request #862 from modelix/feature/media-type-versioning
MODELIX-949: media type versioning + branch meta data access
2 parents 43922d7 + 2abc6b0 commit 92ce337

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)