fully rewrite content

This commit is contained in:
InsanusMokrassar 2020-11-26 16:54:57 +06:00
parent 20e3fd6934
commit 5c1f5c0bba
25 changed files with 441 additions and 602 deletions

View File

@ -7,25 +7,25 @@ import dev.inmo.postssystem.core.post.*
import dev.inmo.postssystem.core.post.repo.PostsRepo import dev.inmo.postssystem.core.post.repo.PostsRepo
import dev.inmo.postssystem.core.publishing.* import dev.inmo.postssystem.core.publishing.*
class BusinessPostCreatingCase( //class BusinessPostCreatingCase(
private val postsRepo: PostsRepo, // private val postsRepo: PostsRepo,
private val contentRepo: ContentRepo, // private val contentRepo: ContentRepo,
private val publishingRegistrar: PublishingRegistrar, // private val publishingRegistrar: PublishingRegistrar,
private val postKeyGenerator: PostKeyGenerator = { _, _ -> uuid4().toString() }, // private val postKeyGenerator: PostKeyGenerator = { _, _ -> uuid4().toString() },
private val publishingKeyReceiverGetter: PublishingKeyReceiverGetter // private val publishingKeyReceiverGetter: PublishingKeyReceiverGetter
) : PostCreatingCase { //) : PostCreatingCase {
override suspend fun createPost(postContent: List<Content>, triggerId: TriggerId?): RegisteredPost? { // override suspend fun createPost(postContent: List<Content>, triggerId: TriggerId?): RegisteredPost? {
val content = postContent.mapNotNull { contentRepo.registerContent(it) } // val content = postContent.mapNotNull { contentRepo.registerContent(it) }
val post = postsRepo.createPost(SimplePost(content.map { it.id })) ?: return null // val post = postsRepo.createPost(SimplePost(content.map { it.id })) ?: return null
//
triggerId ?.let { // triggerId ?.let {
val key = postKeyGenerator(post.id, triggerId) // val key = postKeyGenerator(post.id, triggerId)
//
if (publishingRegistrar.registerTriggerForPost(key, post.id)) { // if (publishingRegistrar.registerTriggerForPost(key, post.id)) {
publishingKeyReceiverGetter(it) ?.acceptKey(post.id, key) // publishingKeyReceiverGetter(it) ?.acceptKey(post.id, key)
} // }
} // }
//
return post // return post
} // }
} //}

View File

@ -14,6 +14,7 @@ kotlin {
api "com.soywiz.korlibs.klock:klock:$klockVersion" api "com.soywiz.korlibs.klock:klock:$klockVersion"
api "dev.inmo:micro_utils.common:$microutils_version" api "dev.inmo:micro_utils.common:$microutils_version"
api "dev.inmo:micro_utils.coroutines:$microutils_version"
api "dev.inmo:micro_utils.repos.common:$microutils_version" api "dev.inmo:micro_utils.repos.common:$microutils_version"
api "dev.inmo:micro_utils.mime_types:$microutils_version" api "dev.inmo:micro_utils.mime_types:$microutils_version"
} }

View File

@ -22,21 +22,7 @@ data class SpecialContent(
val internalId: ContentId val internalId: ContentId
) : Content ) : Content
@Serializable
data class TextContent(
val text: String
) : Content
@Serializable
data class BinaryContent(
val mimeType: MimeType,
val originalFileName: String,
@Serializable(ByteArrayAllocatorSerializer::class)
val dataAllocator: ByteArrayAllocator
) : Content
val BinaryContent.isImage: Boolean
get() = mimeType is KnownMimeTypes.Image
/** /**
* Content which is already registered in database. Using its [id] you can retrieve all known * Content which is already registered in database. Using its [id] you can retrieve all known

View File

@ -1,26 +1,12 @@
package dev.inmo.postssystem.core.content.api package dev.inmo.postssystem.core.content.api.business
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.Repo
import dev.inmo.micro_utils.repos.UpdatedValuePair import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.postssystem.core.content.* import dev.inmo.postssystem.core.content.*
import dev.inmo.postssystem.core.content.api.*
import dev.inmo.postssystem.core.generateContentId import dev.inmo.postssystem.core.generateContentId
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
interface BusinessContentRepoReadHelpInterface : Repo {
suspend fun getKeysByPagination(pagination: Pagination): PaginationResult<ContentId>
suspend fun contains(contentId: ContentId): Boolean
suspend fun getType(contentId: ContentId): AdapterType?
suspend fun count(): Long
}
interface BusinessContentRepoWriteHelpInterface : Repo {
suspend fun deleteContentId(contentId: ContentId)
suspend fun saveType(contentId: ContentId, type: AdapterType): Boolean
}
interface BusinessContentRepoHelpInterface : BusinessContentRepoReadHelpInterface, BusinessContentRepoWriteHelpInterface
typealias AdapterType = String
interface BusinessContentRepoContentAdapter { interface BusinessContentRepoContentAdapter {
val type: AdapterType val type: AdapterType
suspend fun storeContent(contentId: ContentId, content: Content): Boolean suspend fun storeContent(contentId: ContentId, content: Content): Boolean
@ -30,7 +16,7 @@ interface BusinessContentRepoContentAdapter {
class BusinessReadContentRepo( class BusinessReadContentRepo(
adapters: List<BusinessContentRepoContentAdapter>, adapters: List<BusinessContentRepoContentAdapter>,
private val helperRepo: BusinessContentRepoReadHelpInterface, private val helperRepo: BusinessContentRepoReadHelper,
) : ReadContentRepo { ) : ReadContentRepo {
private val adaptersMap: Map<String, BusinessContentRepoContentAdapter> = adapters.map { private val adaptersMap: Map<String, BusinessContentRepoContentAdapter> = adapters.map {
it.type to it it.type to it
@ -61,7 +47,7 @@ class BusinessReadContentRepo(
class BusinessWriteContentRepo( class BusinessWriteContentRepo(
private val adapters: List<BusinessContentRepoContentAdapter>, private val adapters: List<BusinessContentRepoContentAdapter>,
private val helperRepo: BusinessContentRepoHelpInterface private val helperRepo: BusinessContentRepoHelper
) : WriteContentRepo { ) : WriteContentRepo {
private val adaptersMap = adapters.map { it.type to it }.toMap() private val adaptersMap = adapters.map { it.type to it }.toMap()
private val _deletedObjectsIdsFlow = MutableSharedFlow<ContentId>() private val _deletedObjectsIdsFlow = MutableSharedFlow<ContentId>()
@ -110,7 +96,7 @@ class BusinessWriteContentRepo(
class BusinessContentRepo( class BusinessContentRepo(
adapters: List<BusinessContentRepoContentAdapter>, adapters: List<BusinessContentRepoContentAdapter>,
helperRepo: BusinessContentRepoHelpInterface helperRepo: BusinessContentRepoHelper
) : ContentRepo, ReadContentRepo by BusinessReadContentRepo( ) : ContentRepo, ReadContentRepo by BusinessReadContentRepo(
adapters, adapters,
helperRepo helperRepo

View File

@ -0,0 +1,59 @@
package dev.inmo.postssystem.core.content.api.business
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.PaginationResult
import dev.inmo.micro_utils.repos.*
import dev.inmo.postssystem.core.content.ContentId
typealias AdapterType = String
interface BusinessContentRepoReadHelper : Repo {
suspend fun getKeysByPagination(pagination: Pagination): PaginationResult<ContentId>
suspend fun contains(contentId: ContentId): Boolean
suspend fun getType(contentId: ContentId): AdapterType?
suspend fun count(): Long
}
interface BusinessContentRepoWriteHelper : Repo {
suspend fun deleteContentId(contentId: ContentId)
suspend fun saveType(contentId: ContentId, type: AdapterType): Boolean
}
interface BusinessContentRepoHelper : BusinessContentRepoReadHelper, BusinessContentRepoWriteHelper
class KeyValueBusinessContentRepoReadHelper(
private val keyValueRepo: ReadStandardKeyValueRepo<ContentId, AdapterType>
) : BusinessContentRepoReadHelper {
override suspend fun getKeysByPagination(pagination: Pagination): PaginationResult<ContentId> = keyValueRepo.keys(pagination)
override suspend fun contains(contentId: ContentId): Boolean = keyValueRepo.contains(contentId)
override suspend fun getType(contentId: ContentId): AdapterType? = keyValueRepo.get(contentId)
override suspend fun count(): Long = keyValueRepo.count()
}
class KeyValueBusinessContentRepoWriteHelper(
private val keyValueRepo: WriteStandardKeyValueRepo<ContentId, AdapterType>
) : BusinessContentRepoWriteHelper {
override suspend fun deleteContentId(contentId: ContentId) { keyValueRepo.unset(contentId) }
override suspend fun saveType(contentId: ContentId, type: AdapterType): Boolean {
keyValueRepo.set(contentId, type)
return true
}
}
class KeyValueBusinessContentRepoHelper(
private val keyValueRepo: StandardKeyValueRepo<ContentId, AdapterType>
) : BusinessContentRepoHelper, BusinessContentRepoReadHelper by KeyValueBusinessContentRepoReadHelper(
keyValueRepo
), BusinessContentRepoWriteHelper by KeyValueBusinessContentRepoWriteHelper(
keyValueRepo
)
fun StandardKeyValueRepo<ContentId, AdapterType>.asBusinessContentRepo(
adapters: List<BusinessContentRepoContentAdapter>
) = BusinessContentRepo(
adapters,
KeyValueBusinessContentRepoHelper(this)
)
fun StandardKeyValueRepo<ContentId, AdapterType>.asBusinessContentRepo(
vararg adapters: BusinessContentRepoContentAdapter
) = asBusinessContentRepo(adapters.toList())

View File

@ -0,0 +1,29 @@
package dev.inmo.postssystem.core.content.api.business.content_adapters
import dev.inmo.micro_utils.repos.*
import dev.inmo.postssystem.core.content.Content
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.postssystem.core.content.api.business.AdapterType
import dev.inmo.postssystem.core.content.api.business.BusinessContentRepoContentAdapter
class KeyValueBusinessContentRepoAdapter<T>(
override val type: AdapterType,
private val keyValueRepo: StandardKeyValueRepo<ContentId, T>,
private val contentToData: suspend (Content) -> T?,
private val dataToContent: suspend (T) -> Content
) : BusinessContentRepoContentAdapter {
override suspend fun storeContent(contentId: ContentId, content: Content): Boolean {
keyValueRepo.set(contentId, contentToData(content) ?: return false)
return true
}
override suspend fun getContent(contentId: ContentId): Content? {
return dataToContent(
keyValueRepo.get(contentId) ?: return null
)
}
override suspend fun removeContent(contentId: ContentId) {
keyValueRepo.unset(contentId)
}
}

View File

@ -0,0 +1,54 @@
package dev.inmo.postssystem.core.content.api.business.content_adapters.binary
import dev.inmo.micro_utils.repos.*
import dev.inmo.postssystem.core.content.Content
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.postssystem.core.content.api.business.AdapterType
import dev.inmo.postssystem.core.content.api.business.BusinessContentRepoContentAdapter
import dev.inmo.postssystem.core.content.api.business.content_adapters.KeyValueBusinessContentRepoAdapter
import kotlinx.serialization.json.Json
private val format = Json { ignoreUnknownKeys = true }
class BinaryBusinessContentRepoContentAdapter(
private val dataStore: KeyValueRepo<String, String>,
private val filesStore: KeyValueRepo<String, ByteArray>,
private val removeOnAbsentInOneOfStores: Boolean = false
) : BusinessContentRepoContentAdapter {
override val type: AdapterType
get() = "binary"
override suspend fun storeContent(contentId: ContentId, content: Content): Boolean {
(content as? BinaryContent) ?.also {
filesStore.set(it.originalFileName, it.dataAllocator())
dataStore.set(
contentId,
format.encodeToString(BinaryContent.serializer(), it.copy { ByteArray(0) })
)
} ?: return false
return true
}
override suspend fun getContent(contentId: ContentId): Content? {
return filesStore.get(contentId) ?.let {
val serializedData = dataStore.get(contentId)
if (serializedData != null) {
format.decodeFromString(BinaryContent.serializer(), serializedData).copy {
it
}
} else {
null
}
} ?: null.also {
if (removeOnAbsentInOneOfStores) {
filesStore.unset(contentId)
dataStore.unset(contentId)
}
}
}
override suspend fun removeContent(contentId: ContentId) {
filesStore.unset(contentId)
dataStore.unset(contentId)
}
}

View File

@ -0,0 +1,19 @@
package dev.inmo.postssystem.core.content.api.business.content_adapters.binary
import dev.inmo.micro_utils.common.ByteArrayAllocator
import dev.inmo.micro_utils.common.ByteArrayAllocatorSerializer
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
import dev.inmo.micro_utils.mime_types.MimeType
import dev.inmo.postssystem.core.content.Content
import kotlinx.serialization.Serializable
@Serializable
data class BinaryContent(
val mimeType: MimeType,
val originalFileName: String,
@Serializable(ByteArrayAllocatorSerializer::class)
val dataAllocator: ByteArrayAllocator
) : Content
val BinaryContent.isImage: Boolean
get() = mimeType is KnownMimeTypes.Image

View File

@ -0,0 +1,15 @@
package dev.inmo.postssystem.core.content.api.business.content_adapters.text
import dev.inmo.micro_utils.repos.*
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.postssystem.core.content.api.business.BusinessContentRepoContentAdapter
import dev.inmo.postssystem.core.content.api.business.content_adapters.KeyValueBusinessContentRepoAdapter
class TextBusinessContentRepoContentAdapter(
private val keyValueRepo: StandardKeyValueRepo<ContentId, String>
) : BusinessContentRepoContentAdapter by KeyValueBusinessContentRepoAdapter(
"regularText",
keyValueRepo,
{ (it as? TextContent) ?.text },
{ TextContent(it) }
)

View File

@ -0,0 +1,9 @@
package dev.inmo.postssystem.core.content.api.business.content_adapters.text
import dev.inmo.postssystem.core.content.Content
import kotlinx.serialization.Serializable
@Serializable
data class TextContent(
val text: String
) : Content

View File

@ -1,11 +1,26 @@
package dev.inmo.postssystem.core.api package dev.inmo.postssystem.core.api
import dev.inmo.micro_utils.common.ByteArrayAllocator
import dev.inmo.micro_utils.mime_types.KnownMimeTypes
import dev.inmo.postssystem.core.content.* import dev.inmo.postssystem.core.content.*
import dev.inmo.postssystem.core.content.api.business.content_adapters.binary.BinaryContent
import dev.inmo.postssystem.core.content.api.business.content_adapters.text.TextContent
import dev.inmo.postssystem.core.generateContentId import dev.inmo.postssystem.core.generateContentId
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
private val jsonFormat = Json {
serializersModule = SerializersModule {
polymorphic(Content::class) {
subclass(TextContent::class, TextContent.serializer())
subclass(BinaryContent::class, BinaryContent.serializer())
}
}
}
class ContentSerialization { class ContentSerialization {
private val simpleTextTestEntries = 10 private val simpleTextTestEntries = 10
private val simpleSpecialTestEntries = 10 private val simpleSpecialTestEntries = 10
@ -15,7 +30,9 @@ class ContentSerialization {
val contents = (0 until simpleTextTestEntries).map { val contents = (0 until simpleTextTestEntries).map {
TextContent("Example$it") TextContent("Example$it")
} + (0 until simpleSpecialTestEntries).map { } + (0 until simpleSpecialTestEntries).map {
SpecialContent("$it") BinaryContent(KnownMimeTypes.Any, "$it.example") {
byteArrayOf(it.toByte())
}
} }
val registeredContentFakes = contents.map { content -> val registeredContentFakes = contents.map { content ->
@ -26,13 +43,33 @@ class ContentSerialization {
} }
val stringified = registeredContentFakes.map { val stringified = registeredContentFakes.map {
Json.encodeToString(RegisteredContent.serializer(), it) jsonFormat.encodeToString(RegisteredContent.serializer(), it)
} }
val parsed = stringified.map { val parsed = stringified.map {
Json.decodeFromString(RegisteredContent.serializer(), it) jsonFormat.decodeFromString(RegisteredContent.serializer(), it)
} }
parsed.forEachIndexed { i, registeredContent -> assertEquals(registeredContentFakes[i], registeredContent) } parsed.forEachIndexed { i, registeredContent ->
val content = registeredContent.content
assertEquals(registeredContentFakes[i].id, registeredContent.id)
when (content) {
is TextContent -> assertEquals(registeredContentFakes[i].content, content)
is BinaryContent -> {
val expectedContent = registeredContentFakes[i].content as BinaryContent
val fakeByteArrayAllocator: ByteArrayAllocator = { byteArrayOf() }
assertEquals(
expectedContent.copy(dataAllocator = fakeByteArrayAllocator),
content.copy(dataAllocator = fakeByteArrayAllocator)
)
val expectedData = expectedContent.dataAllocator()
val parsedData = content.dataAllocator()
assertEquals(expectedData.size, parsedData.size)
expectedData.withIndex().forEach { (i, byte) ->
assertEquals(byte, parsedData[i])
}
}
}
}
} }
} }

View File

@ -0,0 +1,79 @@
package dev.inmo.postssystem.core.content.api.business.content_adapters.binary
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions
import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.set
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.io.File
class FilesStoreRepoAdapter(
private val filesRepo: KeyValueRepo<String, File>,
private val temporalFilesFolder: File
) : KeyValueRepo<String, ByteArray> {
private val File.asByteArray
get() = readBytes()
override val onNewValue: Flow<Pair<String, ByteArray>> = filesRepo.onNewValue.map { (filename, file) ->
filename to file.asByteArray
}
override val onValueRemoved: Flow<String> = filesRepo.onValueRemoved
init {
temporalFilesFolder.mkdirs()
}
override suspend fun contains(key: String): Boolean = filesRepo.contains(key)
override suspend fun count(): Long = filesRepo.count()
override suspend fun get(k: String): ByteArray? = filesRepo.get(k) ?.asByteArray
override suspend fun keys(
v: ByteArray,
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = emptyPaginationResult()
override suspend fun keys(
pagination: Pagination,
reversed: Boolean
): PaginationResult<String> = filesRepo.keys(pagination, reversed)
override suspend fun set(toSet: Map<String, ByteArray>) {
supervisorScope {
toSet.map { (filename, bytes) ->
launch {
safelyWithoutExceptions {
val file = File(temporalFilesFolder, filename).also {
it.delete()
doOutsideOfCoroutine {
it.createNewFile()
it.writeBytes(bytes)
}
}
filesRepo.set(filename, file)
doOutsideOfCoroutine { file.delete() }
}
}
}
}.joinAll()
}
override suspend fun unset(toUnset: List<String>) = filesRepo.unset(toUnset)
override suspend fun values(
pagination: Pagination,
reversed: Boolean
): PaginationResult<ByteArray> = filesRepo.values(pagination, reversed).let {
PaginationResult(
it.page,
it.pagesNumber,
it.results.map { it.readBytes() },
it.size
)
}
}

View File

@ -1,154 +0,0 @@
package dev.inmo.postssystem.core.exposed
import dev.inmo.postssystem.core.content.*
import dev.inmo.postssystem.core.content.api.ContentRepo
import dev.inmo.postssystem.core.exposed.content.*
import dev.inmo.postssystem.core.generateContentId
import dev.inmo.micro_utils.pagination.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private val Content.type
get() = when (this) {
is TextContent -> "text"
is BinaryContent -> "binary"
is SpecialContent -> "special"
}
private class ContentRepoDatabaseTable(
private val database: Database,
tableName: String = "",
private val textHolder: ContentHolderRepo<TextContent>,
private val binaryHolder: ContentHolderRepo<BinaryContent>,
private val specialHolder: ContentHolderRepo<SpecialContent>
) : Table(tableName), ContentRepo, ContentHolderRepo<Content> {
internal val idColumn = text("_id")
internal val typeColumn = text("type")
init {
transaction(
db = database
) {
SchemaUtils.createMissingTablesAndColumns(this@ContentRepoDatabaseTable)
}
}
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()
private val String.holder
get() = when (this) {
"text" -> textHolder
"binary" -> binaryHolder
"special" -> specialHolder
else -> null
}
override suspend fun putContent(id: ContentId, content: Content) {
when (content) {
is TextContent -> textHolder.putContent(id, content)
is BinaryContent -> binaryHolder.putContent(id, content)
is SpecialContent -> specialHolder.putContent(id, content)
}
}
override suspend fun getContent(id: ContentId): Content? = transaction(
db = database
) {
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.get(typeColumn)
} ?.holder ?.getContent(id)
override suspend fun removeContent(id: ContentId) {
transaction(
db = database
) {
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.get(typeColumn)
} ?.holder ?.removeContent(id)
}
override suspend fun registerContent(content: Content): RegisteredContent? {
val id = generateContentId()
val type = content.type
return transaction(
db = database
) {
insert {
it[idColumn] = id
it[typeColumn] = type
}.getOrNull(idColumn)
} ?.let { id ->
putContent(id, content)
RegisteredContent(
id,
content
)
} ?.also {
contentCreatedBroadcastChannel.send(it)
} ?: null.also {
removeContent(id)
}
}
override suspend fun deleteContent(id: ContentId): Boolean {
val content = getContentById(id) ?: return false
return transaction(
db = database
) {
deleteWhere {
idColumn.eq(id)
} > 0
}.also {
if (it) {
removeContent(id)
contentDeletedBroadcastChannel.send(content)
}
}
}
private fun ResultRow.asRegisteredContent(content: Content): RegisteredContent? = get(idColumn).let {
RegisteredContent(
it,
content
)
}
override suspend fun getContentsIds(): Set<ContentId> {
return transaction(
db = database
) {
selectAll().map { it[idColumn] }
}.toSet()
}
override suspend fun getContentById(id: ContentId): RegisteredContent? {
val content = getContent(id) ?: return null
return transaction(
db = database
) {
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.asRegisteredContent(content)
}
}
override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<RegisteredContent> {
return transaction(
db = database
) {
selectAll().count() to selectAll().paginate(pagination).map { it[idColumn] }
}.let { (count, results) ->
results.mapNotNull { RegisteredContent(it, getContent(it) ?: return@mapNotNull null) }.createPaginationResult(
pagination,
count
)
}
}
}
class ExposedContentRepo (
database: Database,
tableName: String = "",
textHolder: ContentHolderRepo<TextContent> = TextContentHolderRepo(database),
binaryHolder: ContentHolderRepo<BinaryContent> = BinaryContentHolderRepo(database),
specialHolder: ContentHolderRepo<SpecialContent> = SpecialContentHolderRepo(database)
) : ContentRepo by ContentRepoDatabaseTable(database, tableName, textHolder, binaryHolder, specialHolder)

View File

@ -1,67 +0,0 @@
package dev.inmo.postssystem.core.exposed.content
import dev.inmo.postssystem.core.content.BinaryContent
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.micro_utils.mime_types.mimeType
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
private class BinaryContentHolderRepoTable(
private val database: Database
) : ContentHolderRepo<BinaryContent>, Table() {
private val idColumn = text("id")
private val dataColumn = blob("data")
private val mimeColumn = text("mimeType")
private val originalFileNameColumn = text("filename")
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
init {
transaction (
db = database
) {
SchemaUtils.createMissingTablesAndColumns(this@BinaryContentHolderRepoTable)
}
}
override suspend fun getContent(id: ContentId): BinaryContent? = transaction (
db = database
) {
select {
idColumn.eq(id)
}.limit(1).firstOrNull() ?.let {
val bytes = it[dataColumn].bytes
BinaryContent(
mimeType(it[mimeColumn]),
it[originalFileNameColumn]
) {
bytes
}
}
}
override suspend fun removeContent(id: ContentId) {
transaction(
db = database
) {
deleteWhere { idColumn.eq(id) }
}
}
override suspend fun putContent(id: ContentId, content: BinaryContent) {
transaction(
db = database
) {
insert {
it[idColumn] = id
it[originalFileNameColumn] = content.originalFileName
it[mimeColumn] = content.mimeType.raw
it[dataColumn] = ExposedBlob(content.dataAllocator())
}
}
}
}
class BinaryContentHolderRepo(
database: Database
) : ContentHolderRepo<BinaryContent> by BinaryContentHolderRepoTable(database)

View File

@ -1,10 +0,0 @@
package dev.inmo.postssystem.core.exposed.content
import dev.inmo.postssystem.core.content.Content
import dev.inmo.postssystem.core.content.ContentId
interface ContentHolderRepo<T : Content> {
suspend fun getContent(id: ContentId) : T?
suspend fun removeContent(id: ContentId)
suspend fun putContent(id: ContentId, content: T)
}

View File

@ -1,55 +0,0 @@
package dev.inmo.postssystem.core.exposed.content
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.postssystem.core.content.SpecialContent
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private class SpecialContentHolderRepoTable(
private val database: Database
) : ContentHolderRepo<SpecialContent>, Table() {
private val idColumn = text("id")
private val internalIdColumn = text("internalId")
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
init {
transaction(
db = database
) {
SchemaUtils.createMissingTablesAndColumns(this@SpecialContentHolderRepoTable)
}
}
override suspend fun getContent(id: ContentId): SpecialContent? = transaction(
db = database
) {
select {
idColumn.eq(id)
}.limit(1).firstOrNull() ?.get(internalIdColumn) ?.let {
SpecialContent(it)
}
}
override suspend fun removeContent(id: ContentId) {
transaction(
db = database
) {
deleteWhere { idColumn.eq(id) }
}
}
override suspend fun putContent(id: ContentId, content: SpecialContent) {
transaction(
db = database
) {
insert {
it[idColumn] = id
it[internalIdColumn] = content.internalId
}
}
}
}
class SpecialContentHolderRepo(
database: Database
) : ContentHolderRepo<SpecialContent> by SpecialContentHolderRepoTable(database)

View File

@ -1,55 +0,0 @@
package dev.inmo.postssystem.core.exposed.content
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.postssystem.core.content.TextContent
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private class TextContentHolderRepoTable(
private val database: Database
) : ContentHolderRepo<TextContent>, Table() {
private val idColumn = text("id")
private val textColumn = text("text")
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
init {
transaction(
db = database
) {
SchemaUtils.createMissingTablesAndColumns(this@TextContentHolderRepoTable)
}
}
override suspend fun getContent(id: ContentId): TextContent? = transaction(
db = database
) {
select {
idColumn.eq(id)
}.limit(1).firstOrNull() ?.get(textColumn) ?.let {
TextContent(it)
}
}
override suspend fun removeContent(id: ContentId) {
transaction(
db = database
) {
deleteWhere { idColumn.eq(id) }
}
}
override suspend fun putContent(id: ContentId, content: TextContent) {
transaction(
db = database
) {
insert {
it[idColumn] = id
it[textColumn] = content.text
}
}
}
}
class TextContentHolderRepo(
database: Database
) : ContentHolderRepo<TextContent> by TextContentHolderRepoTable(database)

View File

@ -1,7 +1,15 @@
package dev.inmo.postssystem.core.exposed package dev.inmo.postssystem.core.exposed
import dev.inmo.postssystem.core.content.TextContent import dev.inmo.micro_utils.repos.create
import kotlinx.coroutines.runBlocking import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.postssystem.core.content.api.business.AdapterType
import dev.inmo.postssystem.core.content.api.business.asBusinessContentRepo
import dev.inmo.postssystem.core.content.api.business.content_adapters.text.TextBusinessContentRepoContentAdapter
import dev.inmo.postssystem.core.content.api.business.content_adapters.text.TextContent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transactionManager import org.jetbrains.exposed.sql.transactions.transactionManager
import java.io.File import java.io.File
@ -25,33 +33,49 @@ class ExposedContentRepoCommonTests {
it.delete() it.delete()
it.deleteOnExit() it.deleteOnExit()
} }
ExposedContentRepo( val database = Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also {
Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also {
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
} }
ExposedKeyValueRepo<ContentId, AdapterType>(
database,
{ text("contentId") },
{ text("adapterType") },
"ContentRepo"
).asBusinessContentRepo(
TextBusinessContentRepoContentAdapter(
ExposedKeyValueRepo<ContentId, String>(
database,
{ text("contentId") },
{ text("text") },
"TextContentRepo"
)
)
) )
} }
val results = apis.mapIndexed { i, api -> val results = apis.mapIndexed { i, api ->
val content = runBlocking { api.registerContent(TextContent(i.toString())) } val expectedContent = TextContent(i.toString())
assert(content != null) val contents = runBlocking { api.create(TextContent(i.toString())) }
val ids = runBlocking { api.getContentsIds() } val idsCount = runBlocking { api.count() }
assertEquals(ids.size, 1) assertEquals(idsCount, 1)
content!! assert(contents.isNotEmpty())
assertEquals(
expectedContent,
contents.first().content
)
contents.first()
} }
results.forEachIndexed { i, content -> results.forEachIndexed { i, content ->
apis.forEachIndexed { j, api -> apis.forEachIndexed { j, api ->
assert( assert(
runBlocking { runBlocking {
api.getContentById(content.id) == (if (i != j) null else content) api.getById(content.id) == (if (i != j) null else content)
} }
) )
assert(
runBlocking { runBlocking {
api.deleteContent(content.id) == (i == j) api.deleteById(content.id)
} }
)
} }
} }

View File

@ -5,23 +5,23 @@ import dev.inmo.postssystem.core.ktor.contentRootRoute
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.WebSockets import io.ktor.client.features.websocket.WebSockets
class ContentRepoKtorClient private constructor( //class ContentRepoKtorClient private constructor(
readContentRepo: ReadContentRepo, // readContentRepo: ReadContentRepo,
writeContentRepo: WriteContentRepo // writeContentRepo: WriteContentRepo
) : ContentRepo, ReadContentRepo by readContentRepo, WriteContentRepo by writeContentRepo { //) : ContentRepo, ReadContentRepo by readContentRepo, WriteContentRepo by writeContentRepo {
constructor( // constructor(
baseUrl: String, // baseUrl: String,
client: HttpClient = HttpClient { // client: HttpClient = HttpClient {
install(WebSockets) // install(WebSockets)
} // }
) : this( // ) : this(
ReadContentRepoKtorClient( // ReadContentRepoKtorClient(
"$baseUrl/$contentRootRoute", // "$baseUrl/$contentRootRoute",
client // client
), // ),
WriteContentRepoKtorClient( // WriteContentRepoKtorClient(
"$baseUrl/$contentRootRoute", // "$baseUrl/$contentRootRoute",
client // client
) // )
) // )
} //}

View File

@ -12,23 +12,23 @@ import io.ktor.client.HttpClient
import io.ktor.client.request.get import io.ktor.client.request.get
import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.nullable
class ReadContentRepoKtorClient( //class ReadContentRepoKtorClient(
private val baseUrl: String, // private val baseUrl: String,
private val client: HttpClient = HttpClient() // private val client: HttpClient = HttpClient()
) : ReadContentRepo { //) : ReadContentRepo {
override suspend fun getContentsIds(): Set<ContentId> = client.get<ByteArray>( // override suspend fun getContentsIds(): Set<ContentId> = client.get<ByteArray>(
"$baseUrl/$getContentsIdsRoute" // "$baseUrl/$getContentsIdsRoute"
).let { // ).let {
standardKtorSerialFormat.decodeFromByteArray(contentIdsSerializer, it) // standardKtorSerialFormat.decodeFromByteArray(contentIdsSerializer, it)
} // }
//
override suspend fun getContentById(id: ContentId): RegisteredContent? = client.uniget( // override suspend fun getContentById(id: ContentId): RegisteredContent? = client.uniget(
"$baseUrl/$getContentByIdRoute/$id", // "$baseUrl/$getContentByIdRoute/$id",
RegisteredContent.serializer().nullable // RegisteredContent.serializer().nullable
) // )
//
override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<RegisteredContent> = client.uniget( // override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<RegisteredContent> = client.uniget(
"$baseUrl/$getContentByPaginationRoute".includeQueryParams(pagination.asUrlQueryParts), // "$baseUrl/$getContentByPaginationRoute".includeQueryParams(pagination.asUrlQueryParts),
registeredContentPaginationResultSerializer // registeredContentPaginationResultSerializer
) // )
} //}

View File

@ -9,29 +9,29 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
class WriteContentRepoKtorClient( //class WriteContentRepoKtorClient(
private val baseUrl: String, // private val baseUrl: String,
private val client: HttpClient = HttpClient() // private val client: HttpClient = HttpClient()
) : WriteContentRepo { //) : WriteContentRepo {
override val contentCreatedFlow: Flow<RegisteredContent> = client.createStandardWebsocketFlow( // override val contentCreatedFlow: Flow<RegisteredContent> = client.createStandardWebsocketFlow(
"$baseUrl/$contentCreatedFlowRoute", // "$baseUrl/$contentCreatedFlowRoute",
deserializer = RegisteredContent.serializer() // deserializer = RegisteredContent.serializer()
) // )
//
override val contentDeletedFlow: Flow<RegisteredContent> = client.createStandardWebsocketFlow( // override val contentDeletedFlow: Flow<RegisteredContent> = client.createStandardWebsocketFlow(
"$baseUrl/$contentDeletedFlowRoute", // "$baseUrl/$contentDeletedFlowRoute",
deserializer = RegisteredContent.serializer() // deserializer = RegisteredContent.serializer()
) // )
//
override suspend fun registerContent(content: Content): RegisteredContent? = client.unipost( // override suspend fun registerContent(content: Content): RegisteredContent? = client.unipost(
"$baseUrl/$registerContentRoute", // "$baseUrl/$registerContentRoute",
BodyPair(Content.serializer(), content), // BodyPair(Content.serializer(), content),
RegisteredContent.serializer().nullable // RegisteredContent.serializer().nullable
) // )
//
override suspend fun deleteContent(id: ContentId): Boolean = client.unipost( // override suspend fun deleteContent(id: ContentId): Boolean = client.unipost(
"$baseUrl/$deleteContentRoute", // "$baseUrl/$deleteContentRoute",
BodyPair(ContentId.serializer(), id), // BodyPair(ContentId.serializer(), id),
Boolean.serializer() // Boolean.serializer()
) // )
} //}

View File

@ -1,26 +0,0 @@
package dev.inmo.postssystem.core.ktor.server.content
import dev.inmo.postssystem.core.content.api.ContentRepo
import dev.inmo.postssystem.core.ktor.contentRootRoute
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.routing.Route
import io.ktor.routing.route
fun Route.configureContentRepoRoutes(
proxyTo: ContentRepo
) {
route(
contentRootRoute
) {
configureReadContentRepoRoutes(proxyTo)
configureWriteContentRepoRoutes(proxyTo)
}
}
class ContentRepoRoutingConfigurator(
private val proxyTo: ContentRepo
) : ApplicationRoutingConfigurator.Element {
override fun Route.invoke() {
configureContentRepoRoutes(proxyTo)
}
}

View File

@ -1,49 +0,0 @@
package dev.inmo.postssystem.core.ktor.server.content
import dev.inmo.postssystem.core.content.ContentId
import dev.inmo.postssystem.core.content.RegisteredContent
import dev.inmo.postssystem.core.content.api.ReadContentRepo
import dev.inmo.postssystem.core.ktor.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import dev.inmo.micro_utils.ktor.server.getParameterOrSendError
import dev.inmo.micro_utils.ktor.server.unianswer
import dev.inmo.micro_utils.pagination.extractPagination
import io.ktor.application.call
import io.ktor.routing.Route
import io.ktor.routing.get
import kotlinx.serialization.builtins.nullable
fun Route.configureReadContentRepoRoutes(
proxyTo: ReadContentRepo
) {
get(getContentsIdsRoute) {
call.unianswer(
contentIdsSerializer,
proxyTo.getContentsIds()
)
}
get("$getContentByIdRoute/{id}") {
val id: ContentId = call.getParameterOrSendError("id") ?: return@get
call.unianswer(
RegisteredContent.serializer().nullable,
proxyTo.getContentById(id)
)
}
get(getContentByPaginationRoute) {
val pagination = call.request.queryParameters.extractPagination
call.unianswer(
registeredContentPaginationResultSerializer,
proxyTo.getContentByPagination(pagination)
)
}
}
class ReadContentRepoRoutingConfigurator(
private val proxyTo: ReadContentRepo
) : ApplicationRoutingConfigurator.Element {
override fun Route.invoke() {
configureReadContentRepoRoutes(proxyTo)
}
}

View File

@ -1,43 +0,0 @@
package dev.inmo.postssystem.core.ktor.server.content
import dev.inmo.postssystem.core.content.*
import dev.inmo.postssystem.core.content.api.WriteContentRepo
import dev.inmo.postssystem.core.ktor.*
import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.application.call
import io.ktor.routing.Route
import io.ktor.routing.post
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
fun Route.configureWriteContentRepoRoutes(
proxyTo: WriteContentRepo
) {
includeWebsocketHandling(contentCreatedFlowRoute, proxyTo.contentCreatedFlow, RegisteredContent.serializer())
includeWebsocketHandling(contentDeletedFlowRoute, proxyTo.contentDeletedFlow, RegisteredContent.serializer())
post(registerContentRoute) {
val content = call.uniload(Content.serializer())
val registered = proxyTo.registerContent(content)
call.unianswer(
RegisteredContent.serializer().nullable,
registered
)
}
post(deleteContentRoute) {
val contentId = call.uniload(ContentId.serializer())
val isDeleted = proxyTo.deleteContent(contentId)
call.unianswer(
Boolean.serializer(),
isDeleted
)
}
}
class WriteContentRepoRoutingConfigurator(
private val proxyTo: WriteContentRepo
) : ApplicationRoutingConfigurator.Element {
override fun Route.invoke() {
configureWriteContentRepoRoutes(proxyTo)
}
}

View File

@ -19,7 +19,7 @@ uuidVersion=0.2.3
exposed_version=0.28.1 exposed_version=0.28.1
test_sqlite_version=3.32.3.2 test_sqlite_version=3.32.3.2
microutils_version=0.4.5 microutils_version=0.4.6
javax_activation_version=1.1.1 javax_activation_version=1.1.1