Skip to content

Commit

Permalink
Add h2 db activemodel tests, where, find by id (#11)
Browse files Browse the repository at this point in the history
* Add h2 db activemodel tesdts

* add find by id, DRY

* code cleanup

* moar tests

* Add show view

* Update the readme
  • Loading branch information
felipecsl authored Feb 21, 2019
1 parent 8c90d64 commit 03f4ddc
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 23 deletions.
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# Kales

Let's see how hard it would be to create web framework
like Ruby on Rails but in Kotlin.
Kales run on top of [Ktor](https://ktor.io/).
A modern web framework built for developer productivity and safety.
Kales run on top of [Ktor](https://ktor.io/) and uses a Model-View-Controller architecture.

It uses a Model-View-Controller architecture. Database access is
done via [JDBI](http://jdbi.org/) and configured from a `database.yml` resource file (similar to Rails).
Database access is done via [JDBI](http://jdbi.org/) and configured from a `database.yml` resource
file (similar to Rails).

## Running
More documentation coming soon!

## Usage

Kales comes with a command line application `kales-cli` that can generate most of the boilerplate
needed to bootstrap a new web app using Kales. More details about this coming soon!

## Running the example app

```
./gradlew sampleapp:run
Expand Down Expand Up @@ -36,8 +42,11 @@ data class Video(
val title: String
) : ApplicationRecord() {
companion object {
// Returns all records in the table
fun all() = allRecords<Video>()

fun where(vararg clause: Pair<String, Any>) = whereRecords<Video>(clause.toMap())

fun find(id: Int) = findRecord<Video>(id)
}
}
```
Expand All @@ -51,8 +60,11 @@ class ExampleController(call: ApplicationCall) : ApplicationController(call) {
return null
}

override fun show() =
IndexView(IndexViewModel(call.parameters["id"] ?: "?", listOf()))
override fun show(): Any? {
bindings = ShowViewModel(Video.find(call.parameters["id"]?.toInt()
?: throw IllegalArgumentException("Missing parameter id")))
return null
}
}
```

Expand Down
1 change: 1 addition & 0 deletions activemodel/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ dependencies {
api "org.jdbi:jdbi3-postgres:$jdbiVersion"
implementation 'org.yaml:snakeyaml:1.23'
testImplementation "com.google.truth:truth:$truthVersion"
testRuntime 'com.h2database:h2:1.4.197'
}
75 changes: 64 additions & 11 deletions activemodel/src/main/kotlin/kales/ApplicationRecord.kt
Original file line number Diff line number Diff line change
@@ -1,32 +1,85 @@
package kales

import org.jdbi.v3.core.Handle
import org.jdbi.v3.core.Jdbi
import org.jdbi.v3.core.h2.H2DatabasePlugin
import org.jdbi.v3.core.kotlin.KotlinPlugin
import org.jdbi.v3.core.kotlin.mapTo
import org.jdbi.v3.postgres.PostgresPlugin
import org.yaml.snakeyaml.Yaml

abstract class ApplicationRecord {
companion object {
val JDBI: Jdbi = Jdbi.create(dbConnectionString()).installPlugins()
val JDBI: Jdbi = Jdbi.create(dbConnectionString())
.installPlugin(PostgresPlugin())
.installPlugin(H2DatabasePlugin())
.installPlugin(KotlinPlugin())

@Suppress("UNCHECKED_CAST")
private fun dbConnectionString(): String {
val yaml = Yaml()
val stream = ApplicationRecord::class.java.classLoader.getResourceAsStream("database.yml")
val data = yaml.load<Map<String, Any>>(stream)
val devData = data["development"] as Map<String, String>
val host = devData["host"]
val adapter = devData["adapter"]
val database = devData["database"]
val username = devData["username"]
val password = devData["password"]
return "jdbc:$adapter://$host/$database?user=$username&password=$password"
// TODO handle muliple environments
val devData = data["development"] as? Map<String, String> ?: throwMissingField("development")
val adapter = devData["adapter"] ?: throwMissingField("adapter")
val host = devData["host"] ?: throwMissingField("host")
val database = devData["database"] ?: throwMissingField("database")
return if (adapter == "h2") {
"jdbc:$adapter:$host:$database"
} else {
val username = devData["username"] ?: ""
val password = devData["password"] ?: ""
"jdbc:$adapter://$host/$database?user=$username&password=$password"
}
}

private fun throwMissingField(name: String): Nothing =
throw IllegalArgumentException(
"Please set a value for the field '$name' in the file database.yml")

inline fun <reified T : ApplicationRecord> allRecords(): List<T> {
JDBI.open().use {
val tableName = T::class.simpleName!!.toLowerCase()
return it.createQuery("select * from ${tableName}s").mapTo<T>().list()
useJdbi {
val tableName = toTableName<T>()
return it.createQuery("select * from $tableName").mapTo<T>().list()
}
}

inline fun <reified T : ApplicationRecord> whereRecords(clause: Map<String, Any>): List<T> {
useJdbi {
val tableName = toTableName<T>()
val whereClause = clause.keys.joinToString(" and ") { k -> "$k = :$k" }
val query = it.createQuery("select * from $tableName where $whereClause")
clause.forEach { k, v -> query.bind(k, v) }
return query.mapTo<T>().list()
}
}

inline fun <reified T : ApplicationRecord> createRecord(values: Map<String, Any>): Int {
useJdbi {
val tableName = toTableName<T>()
val cols = values.keys.joinToString(prefix = "(", postfix = ")")
val refs = values.keys.joinToString(prefix = "(", postfix = ")") { k -> ":$k" }
val update = it.createUpdate("insert into $tableName $cols values $refs")
values.forEach { k, v -> update.bind(k, v) }
return update.execute()
}
}

/** TODO I think Rails raises RecordNotFound in this case instead of returning null. Should we do the same? */
inline fun <reified T : ApplicationRecord> findRecord(id: Int): T? {
useJdbi {
val tableName = toTableName<T>()
return it.createQuery("select * from $tableName where id = :id")
.bind("id", id)
.mapTo<T>()
.findFirst()
.orElse(null)
}
}

inline fun <T> useJdbi(block: (Handle) -> T) = JDBI.open().use { block(it) }

inline fun <reified T> toTableName() = "${T::class.simpleName!!.toLowerCase()}s"
}
}
56 changes: 56 additions & 0 deletions activemodel/src/test/kotlin/kales/ApplicationRecordTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package kales

import com.google.common.truth.Truth.assertThat
import org.junit.Test

class ApplicationRecordTest {
@Test fun `test no records`() {
withTestDb {
assertThat(TestModel.all()).isEmpty()
assertThat(TestModel.where("id" to 1)).isEmpty()
assertThat(TestModel.find(1)).isNull()
assertThat(TestModel.find(2)).isNull()
}
}

@Test fun `test all`() {
withTestDb {
TestModel.create("name" to "Hello World")
TestModel.create("name" to "Ping Pong")
val expectedModel1 = TestModel(1, "Hello World")
val expectedModel2 = TestModel(2, "Ping Pong")
assertThat(TestModel.all()).containsExactly(expectedModel1, expectedModel2)
}
}

@Test fun `test where`() {
withTestDb {
TestModel.create("name" to "Hello World")
TestModel.create("name" to "Ping Pong")
val expectedModel1 = TestModel(1, "Hello World")
val expectedModel2 = TestModel(2, "Ping Pong")
assertThat(TestModel.where("name" to "Hello World")).containsExactly(expectedModel1)
assertThat(TestModel.where("id" to 1, "name" to "Hello World")).containsExactly(expectedModel1)
assertThat(TestModel.where("id" to 1)).containsExactly(expectedModel1)
assertThat(TestModel.where("id" to 2)).containsExactly(expectedModel2)
assertThat(TestModel.where("id" to 3)).isEmpty()
}
}

@Test fun `test find`() {
withTestDb {
TestModel.create("name" to "Hello World")
TestModel.create("name" to "Ping Pong")
assertThat(TestModel.find(1)).isEqualTo(TestModel(1, "Hello World"))
assertThat(TestModel.find(2)).isEqualTo(TestModel(2, "Ping Pong"))
assertThat(TestModel.find(3)).isNull()
}
}

private fun withTestDb(block: () -> Unit) {
ApplicationRecord.JDBI.withHandle<Any, RuntimeException> {
it.execute("CREATE TABLE testmodels (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR)")
block()
}
}
}
16 changes: 16 additions & 0 deletions activemodel/src/test/kotlin/kales/TestModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package kales

data class TestModel(
val id: Int,
val name: String
) : ApplicationRecord() {
companion object {
fun all() = allRecords<TestModel>()

fun where(vararg clause: Pair<String, Any>) = whereRecords<TestModel>(clause.toMap())

fun create(vararg values: Pair<String, Any>) = createRecord<TestModel>(values.toMap())

fun find(id: Int) = findRecord<TestModel>(id)
}
}
6 changes: 6 additions & 0 deletions activemodel/src/test/resources/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
development:
adapter: h2
host: mem
database: test
username:
password:
10 changes: 10 additions & 0 deletions kales/src/main/kotlin/kales/KalesApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.html.respondHtmlTemplate
import io.ktor.http.content.files
import io.ktor.http.content.static
import io.ktor.http.content.staticRootFolder
import io.ktor.routing.Route
import io.ktor.routing.Routing
import io.ktor.routing.get
import kales.actionpack.ApplicationController
import kales.actionview.ActionView
import kales.actionview.ApplicationLayout
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.primaryConstructor
Expand All @@ -29,6 +33,12 @@ class KalesApplication<T : ApplicationLayout>(
application.install(Routing) {
routing = this
routes()
static("assets") {
staticRootFolder = File("assets")
files("javascripts")
files("stylesheets")
files("images")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package kales.sample.app.controllers
import io.ktor.application.ApplicationCall
import kales.actionpack.ApplicationController
import kales.sample.app.models.Video
import kales.sample.app.views.example.IndexView
import kales.sample.app.views.example.IndexViewModel
import kales.sample.app.views.example.ShowViewModel

class ExampleController(call: ApplicationCall) : ApplicationController(call) {
override fun index(): Any? {
bindings = IndexViewModel("Felipe", Video.all())
return null
}

override fun show() =
IndexView(IndexViewModel(call.parameters["id"] ?: "?", listOf()))
override fun show(): Any? {
bindings = ShowViewModel(Video.find(call.parameters["id"]?.toInt()
?: throw IllegalArgumentException("Missing parameter id")))
return null
}
}
4 changes: 4 additions & 0 deletions sampleapp/src/main/kotlin/kales/sample/app/models/Video.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ data class Video(
) : ApplicationRecord() {
companion object {
fun all() = allRecords<Video>()

fun where(vararg clause: Pair<String, Any>) = whereRecords<Video>(clause.toMap())

fun find(id: Int) = findRecord<Video>(id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kales.sample.app.views.example

import kales.actionview.ActionView
import kotlinx.html.FlowContent
import kotlinx.html.h2
import kotlinx.html.h3

class ShowView(
bindings: ShowViewModel? = ShowViewModel()
) : ActionView<ShowViewModel>(bindings) {
override fun render(content: FlowContent) {
content.apply {
h2 { +"Details" }
h3 {
+"Video ${bindings?.video}"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kales.sample.app.views.example

import kales.actionview.ViewModel
import kales.sample.app.models.Video

data class ShowViewModel(
val video: Video? = null
) : ViewModel

0 comments on commit 03f4ddc

Please sign in to comment.