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
|
package com.insanusmokrassar.postssystem.core.content
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.internal.*
|
||||||
|
|
||||||
typealias ContentId = String
|
typealias ContentId = String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content which is planned to be registered in database
|
* Content which is planned to be registered in database
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable(ContentSerializer::class)
|
||||||
sealed class Content
|
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
|
@Serializable
|
||||||
data class SimpleSpecialContent(
|
data class SimpleSpecialContent(
|
||||||
val internalId: ContentId
|
val internalId: ContentId
|
||||||
|
@ -6,5 +6,5 @@ import com.insanusmokrassar.postssystem.core.post.PostId
|
|||||||
|
|
||||||
private fun generateId() = uuid4().toString()
|
private fun generateId() = uuid4().toString()
|
||||||
|
|
||||||
internal fun generatePostId(): PostId = generateId()
|
fun generatePostId(): PostId = generateId()
|
||||||
internal fun generateContentId(): ContentId = 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 ':core'
|
||||||
|
include ':exposed'
|
||||||
|
Loading…
Reference in New Issue
Block a user