add ExposedContentAPI and test that it is correctly work with different connections
This commit is contained in:
parent
6a4b904b9a
commit
e920451412
@ -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
|
||||
|
@ -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
38
exposed/build.gradle
Normal 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"
|
||||
}
|
3
exposed/gradle.properties
Normal file
3
exposed/gradle.properties
Normal 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
0
exposed/settings.gradle
Normal 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)
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -16,3 +16,4 @@ include 'services:webservice'
|
||||
*/
|
||||
|
||||
include ':core'
|
||||
include ':exposed'
|
||||
|
Loading…
Reference in New Issue
Block a user