add integration with posts creating
This commit is contained in:
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user