Skip to content

Commit

Permalink
add a "real" database implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
BBB committed Nov 9, 2023
1 parent 870e2be commit 9182daa
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 35 deletions.
7 changes: 7 additions & 0 deletions build.gradle.kts
Expand Up @@ -20,11 +20,18 @@ dependencies {
implementation(group = "org.http4k", name = "http4k-client-okhttp")
implementation(group = "org.http4k", name = "http4k-format-jackson")

implementation(platform("org.jetbrains.exposed:exposed-bom:0.44.1"))
implementation(group = "org.jetbrains.exposed", name = "exposed-core")
implementation(group = "org.jetbrains.exposed", name = "exposed-jdbc")
implementation(group = "org.jetbrains.exposed", name = "exposed-java-time")
implementation(group = "org.postgresql", name = "postgresql", version = "42.6.0")

testImplementation(kotlin("test"))
testImplementation("io.strikt:strikt-core:0.34.1")
testImplementation("io.kotest:kotest-assertions-core:5.8.0")
testImplementation(group = "org.http4k", name = "http4k-testing-kotest")
testImplementation(group = "dev.mrbergin", name = "result4k-kotest-matchers", version = "2022.10.2")
testImplementation(group = "com.h2database",name = "h2", version = "2.2.224")
}

tasks.test {
Expand Down
62 changes: 62 additions & 0 deletions src/main/kotlin/me/relph/domain/adapter/DatabaseUserStorage.kt
@@ -0,0 +1,62 @@
package me.relph.domain.adapter

import dev.forkhandles.result4k.Failure
import dev.forkhandles.result4k.Result4k
import dev.forkhandles.result4k.Success
import me.relph.domain.port.User
import me.relph.domain.port.UserId
import me.relph.domain.port.UserStorage
import me.relph.domain.port.UserStorageFailure
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction

object Users : Table() {
val id: Column<String> = varchar("id", 10)
val name: Column<String> = varchar("name", length = 50)
override val primaryKey = PrimaryKey(id, name = "PK_User_ID") // name is optional here
}

private fun ResultRow.toUser() = User(UserId.of(this[Users.id]), this[Users.name])
class DatabaseUserStorage(private val database: Database) : UserStorage {

override fun insert(vararg users: User): Result4k<Unit, UserStorageFailure> {
return try {
Users.batchInsert(users.toList()) { user ->
this[Users.name] = user.name
this[Users.id] = user.id.value
}
return Success(Unit)
}catch (e: Exception) {
Failure(UserStorageFailure.Unclassified(e))
}
}

override fun byId(id: UserId): Result4k<User, UserStorageFailure> = try {
Success(Users.select {
Users.id.eq(id.value)
}.limit(1).single().toUser())
} catch (e: Exception) {
when (e) {
is NoSuchElementException -> Failure(UserStorageFailure.NotFound(id))
else -> Failure(UserStorageFailure.Unclassified(e))
}
}

override fun all(): Result4k<List<User>, UserStorageFailure> = try {
Success(Users.selectAll().map { it.toUser() })
} catch (e: Exception) {
Failure(UserStorageFailure.Unclassified(e))
}

override fun <T> transaction(block: () -> T): T = transaction(database) {
block()
}

}
Expand Up @@ -9,7 +9,12 @@ import me.relph.domain.port.UserStorage
import me.relph.domain.port.UserStorageFailure
import me.relph.domain.port.UserStorageFailure.NotFound

class InMemoryUserStorage(private val users: Map<UserId, User> = mapOf()) : UserStorage {
class InMemoryUserStorage(private val users: MutableMap<UserId, User> = mutableMapOf()) : UserStorage {
override fun insert(vararg toAdd: User): Result4k<Unit, UserStorageFailure> {
toAdd.forEach { users[it.id] = it }
return Success(Unit)
}
override fun <T> transaction(block: () -> T): T = block()
override fun byId(id: UserId): Result4k<User, NotFound> = users[id].asResultOr { NotFound(id) }
override fun all(): Result4k<List<User>, UserStorageFailure> = Success(users.values.toList())
}
5 changes: 5 additions & 0 deletions src/main/kotlin/me/relph/domain/port/AppStorage.kt
@@ -0,0 +1,5 @@
package me.relph.domain.port

interface AppStorage {
fun <T>transaction(block: () -> T): T
}
10 changes: 8 additions & 2 deletions src/main/kotlin/me/relph/domain/port/UserStorage.kt
Expand Up @@ -9,13 +9,19 @@ class UserId private constructor(override val value: String) : StringValue(value
}


data class User(val name: String)
data class User(val id: UserId, val name: String)
sealed class UserStorageFailure(val message: String) {
data class NotFound(val id: UserId, val cause: Throwable? = null) : UserStorageFailure("Unable to find it: $id")
/**
* The aim here is to remove all instances of this from our monitoring
* Implementation specific errors should all be mapped to Domain specific types
*/
data class Unclassified(val cause: Throwable? = null) : UserStorageFailure("Unclassified")
}


interface UserStorage {
interface UserStorage: AppStorage {
fun insert(vararg users: User): Result4k<Unit, UserStorageFailure>
fun byId(id: UserId): Result4k<User, UserStorageFailure>
fun all(): Result4k<List<User>, UserStorageFailure>
}
4 changes: 2 additions & 2 deletions src/test/kotlin/me/relph/adapters/http/GreetTest.kt
Expand Up @@ -19,12 +19,12 @@ internal class GreetTest {
val app = HexHttpApp(hub)
val response = app(Request(Method.GET, "/greet/roger"))
response shouldHaveStatus Status.OK
response shouldHaveBody "hello anon x"
response shouldHaveBody "hello anon"
}

@Test
fun `greets the user when they exist`() {
val hub = Hub(InMemoryUserStorage(mapOf(UserId.of("1") to User("Roger"))))
val hub = Hub(InMemoryUserStorage(mutableMapOf(UserId.of("1") to User(UserId.of("1"), "Roger"))))
val app = HexHttpApp(hub)
val response = app(Request(Method.GET, "/greet/1"))
response shouldHaveStatus Status.OK
Expand Down
4 changes: 2 additions & 2 deletions src/test/kotlin/me/relph/adapters/http/ListUsersTest.kt
Expand Up @@ -22,9 +22,9 @@ internal class ListUsersTest {

@Test
fun `can list all users (some)`() {
val hub = Hub(InMemoryUserStorage(mapOf(UserId.of("1") to User("roger"))))
val hub = Hub(InMemoryUserStorage(mutableMapOf(UserId.of("1") to User(UserId.of("1"), "roger"))))
val app = HexHttpApp(hub)
val response = app(Request(Method.GET, "/users"))
response.shouldHaveBody(usersLens, be(listOf(User("roger"))))
response.shouldHaveBody(usersLens, be(listOf(User(UserId.of("1"), "roger"))))
}
}
23 changes: 23 additions & 0 deletions src/test/kotlin/me/relph/domain/adapter/DatabaseUserStorageTest.kt
@@ -0,0 +1,23 @@
package me.relph.domain.adapter

import me.relph.domain.port.UserStorage
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach

class DatabaseUserStorageTest : UserStorageContract {

override val storage: UserStorage = DatabaseUserStorage(
Database.connect(
"jdbc:h2:file:/tmp/boom-test;AUTO_SERVER=TRUE",
driver = "org.h2.Driver",
user = "root",
password = ""
)
)
override fun createSchema() {
SchemaUtils.create(Users)
}

}
@@ -1,32 +1,5 @@
package me.relph.domain.adapter

import dev.mrbergin.kotest.result4k.shouldBeFailure
import dev.mrbergin.kotest.result4k.shouldBeSuccess
import me.relph.domain.port.User
import me.relph.domain.port.UserId
import me.relph.domain.port.UserStorage
import me.relph.domain.port.UserStorageFailure.NotFound
import org.junit.jupiter.api.Test

private val roger = User("Roger")

interface UserStorageContract {
val storage: UserStorage
@Test
fun `can get a user by id`() {
storage.byId(UserId.of("1")) shouldBeSuccess roger
}

@Test
fun `can not get a user by id`() {
storage.byId(UserId.of("2")) shouldBeFailure NotFound(UserId.of("2"))
}
@Test
fun `can get all users`() {
storage.all() shouldBeSuccess listOf(roger)
}
}

internal class InMemoryUserStorageTest : UserStorageContract {
override val storage = InMemoryUserStorage(mapOf(UserId.of("1") to roger))
override val storage = InMemoryUserStorage()
}
51 changes: 51 additions & 0 deletions src/test/kotlin/me/relph/domain/adapter/UserStorageContract.kt
@@ -0,0 +1,51 @@
package me.relph.domain.adapter

import dev.mrbergin.kotest.result4k.shouldBeFailure
import dev.mrbergin.kotest.result4k.shouldBeSuccess
import me.relph.domain.port.User
import me.relph.domain.port.UserId
import me.relph.domain.port.UserStorage
import me.relph.domain.port.UserStorageFailure
import org.jetbrains.exposed.sql.SchemaUtils
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

val roger = User(UserId.of("1"), "Roger")

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
interface UserStorageContract {
val storage: UserStorage

fun createSchema() {}


@BeforeAll
fun beforeAll() {
storage.transaction {
createSchema()
storage.insert(roger)
}
}

@Test
fun `can get a user by id`() {
storage.transaction {
storage.byId(UserId.of("1")) shouldBeSuccess roger
}
}

@Test
fun `can not get a user by id`() {
storage.transaction {
storage.byId(UserId.of("2")) shouldBeFailure UserStorageFailure.NotFound(UserId.of("2"))
}
}

@Test
fun `can get all users`() {
storage.transaction {
storage.all() shouldBeSuccess listOf(roger)
}
}
}

0 comments on commit 9182daa

Please sign in to comment.