add ExposedContentAPI and test that it is correctly work with different connections

This commit is contained in:
InsanusMokrassar 2019-11-11 00:07:54 +06:00
parent 6a4b904b9a
commit e920451412
8 changed files with 240 additions and 4 deletions

View File

@ -1,15 +1,76 @@
package com.insanusmokrassar.postssystem.core.content
import kotlinx.serialization.Serializable
import kotlinx.serialization.*
import kotlinx.serialization.internal.*
typealias ContentId = String
/**
* Content which is planned to be registered in database
*/
@Serializable
@Serializable(ContentSerializer::class)
sealed class Content
@Serializer(Content::class)
object ContentSerializer : KSerializer<Content> {
override val descriptor: SerialDescriptor = object : SerialClassDescImpl("com.insanusmokrassar.postssystem.core.content.Content") {
init {
addElement("type") // req will have index 0
addElement("data") // res will have index 1
}
}
override fun serialize(encoder: Encoder, obj: Content) {
encoder.beginCollection(
descriptor,
2
).also {
when (obj) {
is SimpleSpecialContent -> {
it.encodeStringElement(descriptor, 0, SimpleSpecialContent.serializer().descriptor.name)
it.encodeSerializableElement(descriptor, 1, SimpleSpecialContent.serializer(), obj)
}
is SimpleTextContent -> {
it.encodeStringElement(descriptor, 0, SimpleTextContent.serializer().descriptor.name)
it.encodeSerializableElement(descriptor, 1, SimpleTextContent.serializer(), obj)
}
}
}
}
private fun <T> CompositeDecoder.deserialize(i: Int, deserializationStrategy: DeserializationStrategy<T>): T {
return decodeSerializableElement(
descriptor,
i,
deserializationStrategy
)
}
override fun deserialize(decoder: Decoder): Content {
lateinit var result: Content
decoder.beginStructure(descriptor).let {
var type: String? = null
deserializeLoop@while (true) {
when (val i = it.decodeElementIndex(descriptor)) {
CompositeDecoder.READ_DONE -> break@deserializeLoop
0 -> type = it.decodeStringElement(descriptor, i)
1 -> result = when (type) {
SimpleSpecialContent.serializer().descriptor.name -> it.deserialize(
i, SimpleSpecialContent.serializer()
)
SimpleTextContent.serializer().descriptor.name -> it.deserialize(
i, SimpleTextContent.serializer()
)
else -> throw UnsupportedOperationException("Can't decode object with type $type")
}
else -> throw SerializationException("Unknown index $i")
}
}
}
return result
}
}
@Serializable
data class SimpleSpecialContent(
val internalId: ContentId

View File

@ -6,5 +6,5 @@ import com.insanusmokrassar.postssystem.core.post.PostId
private fun generateId() = uuid4().toString()
internal fun generatePostId(): PostId = generateId()
internal fun generateContentId(): ContentId = generateId()
fun generatePostId(): PostId = generateId()
fun generateContentId(): ContentId = generateId()

38
exposed/build.gradle Normal file
View File

@ -0,0 +1,38 @@
project.version = "$project_public_version"
project.group = "$project_public_group"
buildscript {
repositories {
mavenLocal()
jcenter()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
}
}
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin'
repositories {
mavenLocal()
jcenter()
mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" }
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation project(":core")
api "org.jetbrains.exposed:exposed:$exposed_version"
testImplementation "org.xerial:sqlite-jdbc:$test_sqlite_version"
testImplementation "org.junit.jupiter:junit-jupiter-api:$test_junit_version"
api "com.soywiz.korlibs.klock:klock:$klockVersion"
api "com.benasher44:uuid:$uuidVersion"
}

View File

@ -0,0 +1,3 @@
exposed_version=0.17.7
test_sqlite_version=3.28.0
test_junit_version=5.5.2

0
exposed/settings.gradle Normal file
View File

View File

@ -0,0 +1,83 @@
package com.insanusmokrassar.postssystem.core.exposed
import com.insanusmokrassar.postssystem.core.content.*
import com.insanusmokrassar.postssystem.core.content.api.ContentAPI
import com.insanusmokrassar.postssystem.core.utils.generateContentId
import com.insanusmokrassar.postssystem.core.utils.pagination.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private class ContentAPIDatabaseTable(
private val database: Database
) : Table("ContentAPI"), ContentAPI {
internal val idColumn = text("_id")
internal val dataColumn = text("data")
private inline fun <T> transaction(noinline body: Transaction.() -> T): T = transaction(database, body)
init {
transaction {
SchemaUtils.createMissingTablesAndColumns(this@ContentAPIDatabaseTable)
}
}
private val contentCreatedBroadcastChannel = BroadcastChannel<RegisteredContent>(Channel.BUFFERED)
private val contentDeletedBroadcastChannel = BroadcastChannel<RegisteredContent>(Channel.BUFFERED)
override val contentCreatedFlow: Flow<RegisteredContent> = contentCreatedBroadcastChannel.asFlow()
override val contentDeletedFlow: Flow<RegisteredContent> = contentDeletedBroadcastChannel.asFlow()
override suspend fun createContent(content: Content): RegisteredContent? {
return transaction {
insert {
it[idColumn] = generateContentId()
it[dataColumn] = Json.plain.stringify(Content.serializer(), content)
}.getOrNull(idColumn) ?.let { id ->
RegisteredContent(
id,
content
)
}
} ?.also {
contentCreatedBroadcastChannel.send(it)
}
}
override suspend fun deleteContent(id: ContentId): Boolean {
val content = getContentById(id) ?: return false
return transaction {
deleteWhere {
idColumn.eq(id)
} > 0
}.also {
if (it) {
contentDeletedBroadcastChannel.send(content)
}
}
}
private fun ResultRow.asRegisteredContent(): RegisteredContent = RegisteredContent(
get(idColumn),
Json.plain.parse(Content.serializer(), get(dataColumn))
)
override suspend fun getContentById(id: ContentId): RegisteredContent? {
return transaction {
select { idColumn.eq(id) }.firstOrNull() ?.asRegisteredContent()
}
}
override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<out RegisteredContent> {
return transaction {
selectAll().count() to selectAll().limit(n = pagination.size, offset = pagination.firstIndex).map { it.asRegisteredContent() }
}.let { (count, results) ->
pagination.createResult(count, results)
}
}
}
class ExposedContentAPI (
private val database: Database
) : ContentAPI by ContentAPIDatabaseTable(database)

View File

@ -0,0 +1,50 @@
package com.insanusmokrassar.postssystem.core.exposed
import com.insanusmokrassar.postssystem.core.content.SimpleTextContent
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transactionManager
import org.junit.jupiter.api.Test
import java.io.File
import java.sql.Connection
class ExposedContentAPICommonTests {
private val tempFolder = System.getProperty("java.io.tmpdir")!!
@Test
fun `Test that it is possible to use several different databases at one time`() {
val numberOfDatabases = 2
val databaseFiles = (0 until numberOfDatabases).map {
"$tempFolder/ExposedContentAPICommonTestsDB$it.db"
}
val apis = databaseFiles.map {
ExposedContentAPI(
Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also {
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
}
)
}
val results = apis.mapIndexed { i, api ->
val content = runBlocking { api.createContent(SimpleTextContent(i.toString())) }
assert(content != null)
content!!
}
results.forEachIndexed { i, content ->
apis.forEachIndexed { j, api ->
assert(
runBlocking {
api.getContentById(content.id) == (if (i != j) null else content)
}
)
}
}
databaseFiles.forEach {
File(it).delete()
}
}
}

View File

@ -16,3 +16,4 @@ include 'services:webservice'
*/
include ':core'
include ':exposed'