add integration with posts creating

This commit is contained in:
2022-01-22 13:30:49 +06:00
parent d1fa0242fa
commit 5e61c2a770
22 changed files with 548 additions and 43 deletions
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
client
features/content
binary
common
src
commonMain
kotlin
dev
server
src
jvmMain
kotlin
dev
inmo
postssystem
features
common
src
commonMain
kotlin
dev
inmo
postssystem
features
content
gradle.properties
server
build.gradle
src
main
java
dev
inmo
postssystem
server
services/posts
client
src
commonMain
common
server

@ -0,0 +1,11 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.postssystem.services.posts.common.*
class ClientPostsService(
baseUrl: String,
unifiedRequester: UnifiedRequester
) : PostsService,
ReadPostsService by ClientReadPostsService(baseUrl, unifiedRequester),
WritePostsService by ClientWritePostsService(baseUrl, unifiedRequester)

@ -0,0 +1,22 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
import dev.inmo.postssystem.services.posts.common.ReadPostsService
import dev.inmo.postssystem.services.posts.common.postsRootPath
import kotlinx.serialization.builtins.nullable
class ClientReadPostsService(
private val baseUrl: String,
private val unifiedRequester: UnifiedRequester
) : ReadPostsService, ReadCRUDRepo<RegisteredPost, PostId> by KtorReadStandardCrudRepo(
buildStandardUrl(baseUrl, postsRootPath),
unifiedRequester,
RegisteredPost.serializer(),
RegisteredPost.serializer().nullable,
PostId.serializer()
)

@ -0,0 +1,123 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.ktor.common.encodeHex
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.client.request.forms.InputProvider
import io.ktor.client.request.forms.formData
import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.http.HttpHeaders
import kotlinx.serialization.builtins.*
class ClientWritePostsService(
private val baseUrl: String,
private val unifiedRequester: UnifiedRequester
) : WritePostsService {
private val root = buildStandardUrl(baseUrl, postsRootPath)
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
private val contentsSerializer = ListSerializer(ContentSerializer)
private val createFullPath = buildStandardUrl(
root,
createRouting
)
private val removeFullPath = buildStandardUrl(
root,
removeRoute
)
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
return if (newPost.content.any { it is BinaryContent }) {
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
formData {
newPost.content.forEachIndexed { i, content ->
when (content) {
is BinaryContent -> append(
i.toString(),
InputProvider(block = content.inputProvider),
headers {
append(HttpHeaders.ContentType, content.mimeType.raw)
append(HttpHeaders.ContentDisposition, "filename=\"${content.filename.name}\"")
}.build()
)
else -> append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(ContentSerializer, content)
)
}
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
createFullPath,
contentsSerializer to newPost.content,
RegisteredPost.serializer().nullable
)
}
}
override suspend fun update(
postId: PostId,
content: List<Either<ContentId, Content>>
): RegisteredPost? {
return if (content.any { it.optionalT2.data is BinaryContent }) {
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
formData {
content.forEachIndexed { i, eitherContent ->
eitherContent.onFirst {
append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
)
}.onSecond {
when (it) {
is BinaryContent -> append(
i.toString(),
InputProvider(block = it.inputProvider),
headers {
append(HttpHeaders.ContentType, it.mimeType.raw)
append(HttpHeaders.ContentDisposition, "filename=\"${it.filename.name}\"")
}.build()
)
else -> append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
)
}
}
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
buildStandardUrl(
root,
updateRouting,
postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId)
),
contentsEitherSerializer to content,
RegisteredPost.serializer().nullable
)
}
}
override suspend fun remove(postId: PostId) = unifiedRequester.unipost(
removeFullPath,
PostId.serializer() to postId,
Unit.serializer()
)
}

@ -12,6 +12,8 @@ kotlin {
dependencies {
api project(":postssystem.features.common.common")
api project(":postssystem.features.posts.common")
api "dev.inmo:micro_utils.repos.common:$microutils_version"
api "dev.inmo:micro_utils.repos.ktor.client:$microutils_version"
}
}
}

@ -1,3 +1,5 @@
package dev.inmo.postssystem.services.posts.common
const val postsRootPath = "posts"
const val postsPostIdParameter = "postId"

@ -0,0 +1,9 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.postssystem.features.content.common.Content
import kotlinx.serialization.Serializable
@Serializable
data class FullNewPost(
val content: List<Content>
)

@ -0,0 +1,3 @@
package dev.inmo.postssystem.services.posts.common
interface PostsService : ReadPostsService, WritePostsService

@ -0,0 +1,7 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
interface ReadPostsService : ReadCRUDRepo<RegisteredPost, PostId>

@ -0,0 +1,16 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.micro_utils.common.Either
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentId
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
interface WritePostsService {
suspend fun create(newPost: FullNewPost): RegisteredPost?
suspend fun update(
postId: PostId,
content: List<Either<ContentId, Content>>
): RegisteredPost?
suspend fun remove(postId: PostId)
}

@ -14,6 +14,8 @@ kotlin {
api project(":postssystem.features.content.server")
api project(":postssystem.features.posts.server")
api project(":postssystem.features.users.server")
api "dev.inmo:micro_utils.common:$microutils_version"
api "org.jetbrains.kotlinx:kotlinx-serialization-properties:$kotlin_serialisation_core_version"
}
}
}

@ -0,0 +1,49 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.repos.create
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentId
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
import dev.inmo.postssystem.features.posts.common.*
import dev.inmo.postssystem.features.posts.server.ServerPostsStorage
import dev.inmo.postssystem.services.posts.common.FullNewPost
import dev.inmo.postssystem.services.posts.common.WritePostsService
class DefaultWritePostsService(
private val postsStorage: ServerPostsStorage,
private val contentStorage: ServerContentStorage<Content>
) : WritePostsService {
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
val contentIds = contentStorage.create(newPost.content).map { it.id }
return postsStorage.create(NewPost(contentIds)).firstOrNull()
}
override suspend fun update(postId: PostId, content: List<Either<ContentId, Content>>): RegisteredPost? {
if (!postsStorage.contains(postId)) {
return null
}
val newContent = content.mapNotNull {
when (it) {
is EitherFirst -> {
it.t1
}
is EitherSecond -> {
contentStorage.create(it.t2).firstOrNull() ?.id
}
}
}
return postsStorage.update(
postId,
NewPost(newContent)
)
}
override suspend fun remove(postId: PostId) {
return postsStorage.deleteById(postId)
}
}

@ -0,0 +1,61 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
import dev.inmo.micro_utils.ktor.common.decodeHex
import dev.inmo.postssystem.features.content.common.ContentSerializer
import dev.inmo.postssystem.services.posts.common.FullNewPost
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.content.PartData
import io.ktor.request.receiveMultipart
import io.ktor.util.asStream
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.use
suspend fun PipelineContext<Unit, ApplicationCall>.downloadFullNewPost(
serialFormat: StandardKtorSerialFormat
): FullNewPost {
val multipart = call.receiveMultipart()
val map = mutableMapOf<String, Any>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
map[name] = serialFormat.decodeHex(
ContentSerializer,
capturedPart.value
)
}
capturedPart is PartData.FileItem -> {
val filename = FileName(capturedPart.originalFileName ?: error("File name is unknown for default part"))
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
}
else -> {}
}
part = multipart.readPart()
}
}

@ -0,0 +1,211 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.ktor.common.decodeHex
import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.posts.common.*
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.content.PartData
import io.ktor.request.isMultipart
import io.ktor.request.receiveMultipart
import io.ktor.routing.*
import io.ktor.util.asStream
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.use
import io.ktor.utils.io.streams.asInput
import kotlinx.serialization.builtins.*
class ServerPostsServiceRoutingConfigurator(
private val readPostsService: ReadPostsService,
private val writePostsService: WritePostsService? = readPostsService as? WritePostsService,
private val unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element {
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
private val contentsSerializer = ListSerializer(ContentSerializer)
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContents(): List<Content> {
return unifiedRouter.run {
if (call.request.isMultipart()) {
val multipart = call.receiveMultipart()
val list = mutableListOf<Pair<String, Content>>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
list.add(
name to unifiedRouter.serialFormat.decodeHex(
ContentSerializer,
capturedPart.value
)
)
}
capturedPart is PartData.FileItem -> {
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
val mimeType = capturedPart.contentType ?.let {
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
} ?: error("File type is unknown for default part")
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
list.add(
name to BinaryContent(
filename,
mimeType
) { resultInput.inputStream().asInput() }
)
}
else -> {}
}
part = multipart.readPart()
}
list.sortedBy { it.first }.map { it.second }
} else {
uniload(contentsSerializer)
}
}
}
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
return unifiedRouter.run {
if (call.request.isMultipart()) {
val multipart = call.receiveMultipart()
val list = mutableListOf<Pair<String, Either<ContentId, Content>>>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
list.add(
name to unifiedRouter.serialFormat.decodeHex(
contentEitherSerializer,
capturedPart.value
)
)
}
capturedPart is PartData.FileItem -> {
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
val mimeType = capturedPart.contentType ?.let {
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
} ?: error("File type is unknown for default part")
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
list.add(
name to BinaryContent(
filename,
mimeType
) { resultInput.inputStream().asInput() }.either()
)
}
else -> {}
}
part = multipart.readPart()
}
list.sortedBy { it.first }.map { it.second }
} else {
uniload(contentsEitherSerializer)
}
}
}
override fun Route.invoke() {
authenticate {
route(postsRootPath) {
configureReadStandardCrudRepoRoutes(
readPostsService,
RegisteredPost.serializer(),
RegisteredPost.serializer().nullable,
PostId.serializer(),
unifiedRouter
)
writePostsService ?.let {
unifiedRouter.apply {
post(createRouting) {
val data = receiveContents()
unianswer(
RegisteredPost.serializer().nullable,
writePostsService.create(FullNewPost(data))
)
}
post(updateRouting) {
val postId = call.decodeUrlQueryValueOrSendError(postsPostIdParameter, PostId.serializer()) ?: return@post
val data = receiveContentsEithers()
unianswer(
RegisteredPost.serializer().nullable,
writePostsService.update(
postId,
data
)
)
}
post(removeRoute) {
val postId = uniload(PostId.serializer())
unianswer(
Unit.serializer(),
writePostsService.remove(postId)
)
}
}
}
}
}
}
}