use jackson for serializing responses
This commit is contained in:
parent
470f882b29
commit
63b8aaaafe
11
pom.xml
11
pom.xml
@ -15,6 +15,7 @@
|
||||
<kotlin.version>2.4.0</kotlin.version>
|
||||
<application.mainClass>MainKt</application.mainClass>
|
||||
<jib.version>3.5.2</jib.version>
|
||||
<jackson.version>2.21.3</jackson.version>
|
||||
<jib.from.image>eclipse-temurin:21-jre</jib.from.image>
|
||||
<jib.to.image>${project.artifactId}:${project.version}</jib.to.image>
|
||||
</properties>
|
||||
@ -94,6 +95,16 @@
|
||||
<artifactId>javalin</artifactId>
|
||||
<version>7.2.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-kotlin</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
|
||||
@ -9,6 +9,7 @@ import dev.mduchene.bolts.web.EntityDefinitionController
|
||||
import dev.mduchene.bolts.web.HomeController
|
||||
import dev.mduchene.bolts.web.UserController
|
||||
import io.javalin.Javalin
|
||||
import io.javalin.json.JavalinJackson
|
||||
|
||||
fun main() {
|
||||
val database = Database(DatabaseConfig.fromEnvironment())
|
||||
@ -26,6 +27,7 @@ fun main() {
|
||||
)
|
||||
|
||||
Javalin.create { config ->
|
||||
config.jsonMapper(JavalinJackson())
|
||||
controllers.forEach { it.register(config.routes) }
|
||||
}.start(System.getenv("SERVER_PORT")?.toIntOrNull() ?: 7070)
|
||||
}
|
||||
|
||||
@ -12,12 +12,16 @@ class AuthController(private val loginService: LoginService) : Controller {
|
||||
val session = loginService.authenticate(username, password)
|
||||
|
||||
if (session != null) {
|
||||
ctx.jsonResult(
|
||||
"""{"username":"${session.user.username.toJsonString()}","role":"${session.user.role.toJsonString()}","token":"${session.token}"}""",
|
||||
ctx.json(
|
||||
LoginResponse(
|
||||
username = session.user.username,
|
||||
role = session.user.role,
|
||||
token = session.token,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ctx.status(HttpStatus.UNAUTHORIZED)
|
||||
.jsonResult("""{"message":"Invalid username or password"}""")
|
||||
.json(ErrorResponse("Invalid username or password"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package dev.mduchene.bolts.web
|
||||
|
||||
import dev.mduchene.bolts.entity.EntityDefinition
|
||||
import dev.mduchene.bolts.entity.EntityDefinitionRepository
|
||||
import dev.mduchene.bolts.entity.EntityField
|
||||
import dev.mduchene.bolts.entity.FieldType
|
||||
import dev.mduchene.bolts.entity.RelationshipType
|
||||
import dev.mduchene.bolts.user.LoginService
|
||||
@ -17,13 +15,13 @@ class EntityDefinitionController(
|
||||
override fun register(routes: JavalinDefaultRoutingApi) {
|
||||
routes.get("/api/entity-definitions") { ctx ->
|
||||
if (!ctx.requireAdmin(loginService)) return@get
|
||||
ctx.jsonResult(entities.findAll().toJson())
|
||||
ctx.json(entities.findAll())
|
||||
}
|
||||
routes.post("/api/entity-definitions") { ctx ->
|
||||
if (!ctx.requireAdmin(loginService)) return@post
|
||||
val name = ctx.requiredFormParam("name") ?: return@post
|
||||
val identifier = ctx.requiredFormParam("identifier") ?: return@post
|
||||
ctx.status(HttpStatus.CREATED).jsonResult(entities.create(name, identifier).toJson())
|
||||
ctx.status(HttpStatus.CREATED).json(entities.create(name, identifier))
|
||||
}
|
||||
routes.patch("/api/entity-definitions/{entityId}") { ctx ->
|
||||
if (!ctx.requireAdmin(loginService)) return@patch
|
||||
@ -32,7 +30,7 @@ class EntityDefinitionController(
|
||||
val identifier = ctx.requiredFormParam("identifier")
|
||||
if (id == null || name == null || identifier == null) return@patch
|
||||
val entity = entities.update(id, name, identifier)
|
||||
if (entity == null) ctx.notFound() else ctx.jsonResult(entity.toJson())
|
||||
if (entity == null) ctx.notFound() else ctx.json(entity)
|
||||
}
|
||||
routes.delete("/api/entity-definitions/{entityId}") { ctx ->
|
||||
if (!ctx.requireAdmin(loginService)) return@delete
|
||||
@ -59,7 +57,7 @@ class EntityDefinitionController(
|
||||
relationship.type,
|
||||
)
|
||||
if (field == null) ctx.notFound() else {
|
||||
ctx.status(HttpStatus.CREATED).jsonResult(field.toJson())
|
||||
ctx.status(HttpStatus.CREATED).json(field)
|
||||
}
|
||||
}
|
||||
routes.patch("/api/entity-definitions/{entityId}/fields/{fieldId}") { ctx ->
|
||||
@ -86,7 +84,7 @@ class EntityDefinitionController(
|
||||
relationship.targetEntityId,
|
||||
relationship.type,
|
||||
)
|
||||
if (field == null) ctx.notFound() else ctx.jsonResult(field.toJson())
|
||||
if (field == null) ctx.notFound() else ctx.json(field)
|
||||
}
|
||||
routes.delete("/api/entity-definitions/{entityId}/fields/{fieldId}") { ctx ->
|
||||
if (!ctx.requireAdmin(loginService)) return@delete
|
||||
@ -121,11 +119,3 @@ class EntityDefinitionController(
|
||||
val type: RelationshipType?,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<EntityDefinition>.toJson() = joinToString(prefix = "[", postfix = "]") { it.toJson() }
|
||||
|
||||
private fun EntityDefinition.toJson() =
|
||||
"""{"id":$id,"name":"${name.toJsonString()}","identifier":"${identifier.toJsonString()}","fields":${fields.joinToString(prefix = "[", postfix = "]") { it.toJson() }}}"""
|
||||
|
||||
private fun EntityField.toJson() =
|
||||
"""{"id":$id,"name":"${name.toJsonString()}","identifier":"${identifier.toJsonString()}","type":"$type","targetEntityId":${targetEntityId ?: "null"},"targetEntityName":${targetEntityName?.let { "\"${it.toJsonString()}\"" } ?: "null"},"relationshipType":${relationshipType?.let { "\"$it\"" } ?: "null"}}"""
|
||||
|
||||
@ -7,7 +7,7 @@ import io.javalin.http.HttpStatus
|
||||
internal fun Context.requireAdmin(loginService: LoginService): Boolean {
|
||||
val token = header("Authorization")?.removePrefix("Bearer ")
|
||||
if (loginService.userForToken(token)?.role == "admin") return true
|
||||
status(HttpStatus.FORBIDDEN).jsonResult("""{"message":"Admin access required"}""")
|
||||
status(HttpStatus.FORBIDDEN).json(ErrorResponse("Admin access required"))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -18,31 +18,9 @@ internal fun Context.requiredFormParam(name: String): String? {
|
||||
}
|
||||
|
||||
internal fun Context.badRequest(message: String) {
|
||||
status(HttpStatus.BAD_REQUEST).jsonResult("""{"message":"${message.toJsonString()}"}""")
|
||||
status(HttpStatus.BAD_REQUEST).json(ErrorResponse(message))
|
||||
}
|
||||
|
||||
internal fun Context.notFound() {
|
||||
status(HttpStatus.NOT_FOUND).jsonResult("""{"message":"Not found"}""")
|
||||
}
|
||||
|
||||
internal fun Context.jsonResult(value: String): Context =
|
||||
contentType("application/json").result(value)
|
||||
|
||||
internal fun String.toJsonString() = buildString {
|
||||
for (character in this@toJsonString) {
|
||||
when (character) {
|
||||
'\\' -> append("\\\\")
|
||||
'"' -> append("\\\"")
|
||||
'\b' -> append("\\b")
|
||||
'\u000C' -> append("\\f")
|
||||
'\n' -> append("\\n")
|
||||
'\r' -> append("\\r")
|
||||
'\t' -> append("\\t")
|
||||
else -> if (character.code < 0x20) {
|
||||
append("\\u%04x".format(character.code))
|
||||
} else {
|
||||
append(character)
|
||||
}
|
||||
}
|
||||
}
|
||||
status(HttpStatus.NOT_FOUND).json(ErrorResponse("Not found"))
|
||||
}
|
||||
|
||||
17
src/main/kotlin/dev/mduchene/bolts/web/Responses.kt
Normal file
17
src/main/kotlin/dev/mduchene/bolts/web/Responses.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package dev.mduchene.bolts.web
|
||||
|
||||
data class ErrorResponse(
|
||||
val message: String,
|
||||
)
|
||||
|
||||
data class LoginResponse(
|
||||
val username: String,
|
||||
val role: String,
|
||||
val token: String,
|
||||
)
|
||||
|
||||
data class UserResponse(
|
||||
val id: Long,
|
||||
val username: String,
|
||||
val role: String,
|
||||
)
|
||||
@ -12,10 +12,13 @@ class UserController(
|
||||
routes.get("/api/users") { ctx ->
|
||||
if (!ctx.requireAdmin(loginService)) return@get
|
||||
|
||||
val result = users.findAll().joinToString(prefix = "[", postfix = "]") { user ->
|
||||
"""{"id":${user.id},"username":"${user.username.toJsonString()}","role":"${user.role.toJsonString()}"}"""
|
||||
}
|
||||
ctx.jsonResult(result)
|
||||
ctx.json(users.findAll().map { user ->
|
||||
UserResponse(
|
||||
id = user.id,
|
||||
username = user.username,
|
||||
role = user.role,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
package dev.mduchene.bolts.web
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import dev.mduchene.bolts.entity.EntityDefinition
|
||||
import dev.mduchene.bolts.entity.EntityField
|
||||
import dev.mduchene.bolts.entity.FieldType
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ResponseSerializationTest {
|
||||
private val mapper = ObjectMapper().registerKotlinModule()
|
||||
|
||||
@Test
|
||||
fun `serializes response data classes`() {
|
||||
val response = LoginResponse(
|
||||
username = "admin\"user",
|
||||
role = "admin",
|
||||
token = "token",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"""{"username":"admin\"user","role":"admin","token":"token"}""",
|
||||
mapper.writeValueAsString(response),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serializes entity definitions without handwritten json`() {
|
||||
val entity = EntityDefinition(
|
||||
id = 1,
|
||||
name = "Company",
|
||||
identifier = "company",
|
||||
fields = listOf(
|
||||
EntityField(
|
||||
id = 2,
|
||||
name = "Annual revenue",
|
||||
identifier = "annualRevenue",
|
||||
type = FieldType.NUMBER,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"""{"id":1,"name":"Company","identifier":"company","fields":[{"id":2,"name":"Annual revenue","identifier":"annualRevenue","type":"NUMBER","targetEntityId":null,"targetEntityName":null,"relationshipType":null}]}""",
|
||||
mapper.writeValueAsString(entity),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user