Skip to content

Commit

Permalink
#21 Added endpoint to POST /homework (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
vityaman authored Mar 28, 2024
1 parent 9f34821 commit 731351c
Show file tree
Hide file tree
Showing 20 changed files with 554 additions and 67 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled
ij_continuation_indent_size = 4
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,22 @@ A simple learning management system.

## Build & Run

### Build the Botalka Service

```bash
gradle :botalka:bootJar
```

### Start infrastructure

```bash
gradle :botalka:build
docker compose down
docker compose up --build --force-recreate
```

### Connect to LMS Database

```bash
docker exec -it lms-database bash
psql -h localhost -p 5432 -d $POSTGRES_DB -U $POSTGRES_USER
```
30 changes: 21 additions & 9 deletions botalka/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {
id("org.springframework.boot") version "3.2.3"
id("io.spring.dependency-management") version "1.1.4"

id("org.openapi.generator") version "5.3.0"
id("org.openapi.generator") version "7.4.0"

id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
id("io.gitlab.arturbosch.detekt") version "1.23.5"
Expand All @@ -28,6 +28,7 @@ val basePackage = "$group.lms.botalka"

val jooqVersion = "3.19.6"
val testcontainersVersion = "1.19.7"
val kotestVersion = "5.8.1"

java {
sourceCompatibility = JavaVersion.VERSION_21
Expand Down Expand Up @@ -55,10 +56,13 @@ dependencies {

implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.4.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("jakarta.validation:jakarta.validation-api:3.0.2")

implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
runtimeOnly("org.postgresql:r2dbc-postgresql")

implementation("org.apache.commons:commons-lang3:3.14.0")

implementation("org.jooq:jooq:$jooqVersion")
implementation("org.jooq:jooq-kotlin:$jooqVersion")
jooqCodegen("jakarta.xml.bind:jakarta.xml.bind-api:4.0.2")
Expand All @@ -69,8 +73,13 @@ dependencies {
jooqCodegen("org.testcontainers:testcontainers:$testcontainersVersion")
testImplementation("org.testcontainers:r2dbc:$testcontainersVersion")

testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
testImplementation("io.kotest:kotest-property:$kotestVersion")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("io.projectreactor:reactor-test")
Expand Down Expand Up @@ -106,17 +115,14 @@ tasks.register<OpenAPIGenerateTask>(generateControllers) {
packageName = pkg
modelPackage = pkg
modelNameSuffix = "Message"
generateModelTests = false
generateModelTests = true
generateApiTests = false
configOptions =
mapOf(
"useSpringBoot3" to "true",
"serializationLibrary" to "jackson",
"dateLibrary" to "kotlinx-datetime",
"enumPropertyNaming" to "UPPERCASE",
"dateLibrary" to "java8",
"bigDecimalAsString" to "true",
"hideGenerationTimestamp" to "true",
"useBeanValidation" to "false",
"performBeanValidation" to "false",
"openApiNullable" to "false",
"reactive" to "true",
"interfaceOnly" to "true",
Expand Down Expand Up @@ -194,15 +200,21 @@ koverReport {
}

jooq {
val schemaSql = "$projectDir/src/main/resources/database/schema.sql"
val jdbcUrl = {
val schemaSql = "$projectDir/src/main/resources/database/schema.sql"
val protocol = "jdbc:tc:postgresql:16"
val tmpfs = "TC_TMPFS=/testtmpfs:rw&amp"
val script = "TC_INITSCRIPT=file:$schemaSql"
"$protocol:///test?$tmpfs;$script"
}

executions {
create("main") {
configuration {
logging = org.jooq.meta.jaxb.Logging.DEBUG
jdbc {
driver = "org.testcontainers.jdbc.ContainerDatabaseDriver"
url = "jdbc:tc:postgresql:16:///test?TC_TMPFS=/testtmpfs:rw&amp;TC_INITSCRIPT=file:$schemaSql"
url = jdbcUrl()
username = "postgres"
password = "postgres"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ru.itmo.lms.botalka.api.http.endpoint

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
import ru.itmo.lms.botalka.api.http.HomeworkDraftMessage
import ru.itmo.lms.botalka.api.http.HomeworkMessage
import ru.itmo.lms.botalka.api.http.apis.HomeworkApi
import ru.itmo.lms.botalka.api.http.message.toMessage
import ru.itmo.lms.botalka.api.http.message.toModel
import ru.itmo.lms.botalka.logic.HomeworkService

@RestController
class HomeworkHttpApi(
@Autowired private val service: HomeworkService,
) : HomeworkApi {
override suspend fun homeworkPost(
homeworkDraftMessage: HomeworkDraftMessage,
): ResponseEntity<HomeworkMessage> {
val draft = homeworkDraftMessage.toModel()
val homework = service.create(draft)
return ResponseEntity.ok(homework.toMessage())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ru.itmo.lms.botalka.api.http.message

import ru.itmo.lms.botalka.api.http.HomeworkDraftMessage
import ru.itmo.lms.botalka.api.http.HomeworkMessage
import ru.itmo.lms.botalka.domain.model.Homework

fun Homework.toMessage(): HomeworkMessage =
HomeworkMessage(
id = this.id.number,
title = this.title.text,
description = this.description.text,
maxScore = this.maxScore.value.toInt(),
publicationMoment = this.publicationMoment,
creationMoment = this.creationMoment,
)

fun Homework.Draft.toMessage(): HomeworkDraftMessage =
HomeworkDraftMessage(
title = this.title.text,
description = this.description.text,
maxScore = this.maxScore.value.toInt(),
publicationMoment = this.publicationMoment,
)

fun HomeworkDraftMessage.toModel(): Homework.Draft =
Homework.Draft(
title = Homework.Title(this.title),
description = Homework.Description(this.description),
maxScore = Homework.Score(this.maxScore.toShort()),
publicationMoment = this.publicationMoment,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ru.itmo.lms.botalka.commons

import org.apache.commons.lang3.StringUtils

fun String.abbreviated(maxLength: Int = 8): String =
StringUtils.abbreviate(this, maxLength)
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package ru.itmo.lms.botalka.domain.model

import ru.itmo.lms.botalka.commons.abbreviated
import java.time.OffsetDateTime

data class Homework(
val id: Id,
val title: Title,
val description: Description,
val maxScore: Score,
val publicationMoment: OffsetDateTime,
val creationMoment: OffsetDateTime,
) {
@JvmInline
value class Id(val number: Int) {
init {
require(0 < number) {
"""
Unique id must be a positive, got $number
""".trimIndent()
}
}
}

@JvmInline
value class Title(val text: String) {
init {
require(text.length in lengthRange) {
"""
Title must be in range $lengthRange,
got ${text.abbreviated()} with length ${text.length}
""".trimIndent()
}
}

companion object {
private val lengthRange = 8..64
}
}

@JvmInline
value class Description(val text: String) {
init {
require(text.length in lengthRange) {
"""
Description must be in range $lengthRange, "
got ${text.abbreviated()} with length ${text.length}
""".trimIndent()
}
}

companion object {
private val lengthRange = 8..16_384
}
}

@JvmInline
value class Score(val value: Short) {
init {
require(value in range) {
"""
Score must be in range $range, got $value
""".trimIndent()
}
}

companion object {
private val range = 0..2000
}
}

data class Draft(
val title: Title,
val description: Description,
val maxScore: Score,
val publicationMoment: OffsetDateTime,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.itmo.lms.botalka.logic

import ru.itmo.lms.botalka.domain.model.Homework

interface HomeworkService {
suspend fun create(homework: Homework.Draft): Homework
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.itmo.lms.botalka.logic.basic

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import ru.itmo.lms.botalka.domain.model.Homework
import ru.itmo.lms.botalka.logic.HomeworkService
import ru.itmo.lms.botalka.storage.HomeworkStorage

@Service
class BasicHomeworkService(
@Autowired private val storage: HomeworkStorage,
) : HomeworkService {
override suspend fun create(homework: Homework.Draft): Homework =
storage.create(homework)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.itmo.lms.botalka.storage

import ru.itmo.lms.botalka.domain.model.Homework

interface HomeworkStorage {
suspend fun create(homework: Homework.Draft): Homework
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ru.itmo.lms.botalka.storage.jooq

import org.jooq.DSLContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

@Component
data class JooqDatabase(@Autowired val execute: DSLContext)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.itmo.lms.botalka.storage.jooq

import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import ru.itmo.lms.botalka.domain.model.Homework
import ru.itmo.lms.botalka.storage.HomeworkStorage
import ru.itmo.lms.botalka.storage.jooq.entity.toModel
import ru.itmo.lms.botalka.storage.jooq.tables.references.HOMEWORK

@Repository
class JooqHomeworkStorage(
@Autowired private val database: JooqDatabase,
) : HomeworkStorage {
override suspend fun create(homework: Homework.Draft): Homework =
database.execute
.insertInto(
HOMEWORK,
HOMEWORK.TITLE,
HOMEWORK.DESCRIPTION,
HOMEWORK.MAX_SCORE,
HOMEWORK.PUBLICATION_MOMENT,
)
.values(
homework.title.text,
homework.description.text,
homework.maxScore.value,
homework.publicationMoment,
)
.returningResult(HOMEWORK.fields().asList())
.coerce(HOMEWORK)
.toMono()
.map { it.toModel() }
.awaitSingle()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package ru.itmo.lms.botalka.storage.jooq
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.reactive.awaitSingle
import org.jooq.DSLContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import ru.itmo.lms.botalka.domain.model.Note
Expand All @@ -14,16 +13,18 @@ import ru.itmo.lms.botalka.storage.jooq.tables.references.NOTE

@Repository
class JooqNoteStorage(
@Autowired private val dsl: DSLContext,
@Autowired private val database: JooqDatabase,
) : NoteStorage {
override fun getAll(): Flow<Note> =
dsl.selectFrom(NOTE)
database.execute
.selectFrom(NOTE)
.toFlux()
.map { it.toModel() }
.asFlow()

override suspend fun create(note: Note.Draft): Note =
dsl.insertInto(NOTE, NOTE.CONTENT)
database.execute
.insertInto(NOTE, NOTE.CONTENT)
.values(note.content)
.returningResult(NOTE.ID, NOTE.CONTENT)
.toMono()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ru.itmo.lms.botalka.storage.jooq.entity

import ru.itmo.lms.botalka.domain.model.Homework
import ru.itmo.lms.botalka.storage.jooq.tables.records.HomeworkRecord

fun HomeworkRecord.toModel(): Homework =
Homework(
id = Homework.Id(this.id!!),
title = Homework.Title(this.title),
description = Homework.Description(this.description),
maxScore = Homework.Score(this.maxScore),
publicationMoment = this.publicationMoment,
creationMoment = this.creationMoment!!,
)
Loading

0 comments on commit 731351c

Please sign in to comment.