UpdateCreatedArticleUseCase.kt

package com.example.realworldkotlinspringbootjdbc.usecase.article

import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.left
import arrow.core.right
import com.example.realworldkotlinspringbootjdbc.domain.ArticleRepository
import com.example.realworldkotlinspringbootjdbc.domain.CreatedArticle
import com.example.realworldkotlinspringbootjdbc.domain.CreatedArticleAuthorVerification
import com.example.realworldkotlinspringbootjdbc.domain.OtherUser
import com.example.realworldkotlinspringbootjdbc.domain.RegisteredUser
import com.example.realworldkotlinspringbootjdbc.domain.UpdatableCreatedArticle
import com.example.realworldkotlinspringbootjdbc.domain.article.Slug
import com.example.realworldkotlinspringbootjdbc.usecase.shared_model.CreatedArticleWithAuthor
import com.example.realworldkotlinspringbootjdbc.util.MyError
import org.springframework.stereotype.Service

/**
 * 作成済み記事の更新
 *
 * - nullの場合はもともとのやつで更新する
 * - 変更する箇所がなくてもUpdateする(updated_atを更新)
 * - 著者じゃないと更新できない
 * - タグリストは更新できない(作成時そのまま)
 */
interface UpdateCreatedArticleUseCase {
    /**
     * 実行
     *
     * @param requestedUser リクエストしたユーザー(著者である必要がある)
     * @param slug Slug
     * @param title 更新したいタイトル(nullの場合はもとのやつが採用される)
     * @param description 更新したいdescription(同上)
     * @param body 更新したいbody(同上)
     * @return エラー or 著者情報付きの作成済み記事
     */
    fun execute(
        requestedUser: RegisteredUser,
        slug: String?,
        title: String?,
        description: String?,
        body: String?,
    ): Either<Error, CreatedArticleWithAuthor> = throw NotImplementedError()

    sealed interface Error : MyError {
        data class InvalidSlug(override val errors: List<MyError.ValidationError>) : Error, MyError.ValidationErrors
        data class InvalidArticle(override val errors: List<MyError.ValidationError>) : Error, MyError.ValidationErrors
        data class NotFoundArticle(val slug: Slug) : Error, MyError.Basic
        data class NotAuthor(
            override val cause: MyError,
            val targetArticle: CreatedArticle,
            val notAuthorizedUser: RegisteredUser,
        ) : Error, MyError.MyErrorWithMyError
    }
}

@Service
class UpdateCreatedArticleUseCaseImpl(
    val articleRepository: ArticleRepository,
) : UpdateCreatedArticleUseCase {
    override fun execute(
        requestedUser: RegisteredUser,
        slug: String?,
        title: String?,
        description: String?,
        body: String?,
    ): Either<UpdateCreatedArticleUseCase.Error, CreatedArticleWithAuthor> {
        /**
         * String -> Slug
         * 失敗 -> 早期return
         */
        val validatedSlug = Slug.new(slug).fold(
            { return UpdateCreatedArticleUseCase.Error.InvalidSlug(errors = it).left() },
            { it }
        )

        /**
         * Slug -> CreatedArticle
         * 失敗 -> 早期return
         */
        val foundArticle = when (val result = articleRepository.findBySlug(validatedSlug)) {
            is Left -> when (result.value) {
                is ArticleRepository.FindBySlugError.NotFound -> return UpdateCreatedArticleUseCase.Error.NotFoundArticle(
                    slug = validatedSlug
                ).left()
            }
            is Right -> result.value
        }

        /**
         * 著者かどうかの確認
         * 著者ではない -> 早期return
         * 著者である -> 何もしない
         */
        when (val verifyResult = CreatedArticleAuthorVerification.verify(article = foundArticle, user = requestedUser)) {
            /**
             * 著者ではない -> 早期return
             */
            is Left -> return UpdateCreatedArticleUseCase.Error.NotAuthor(
                cause = verifyResult.value,
                targetArticle = foundArticle,
                notAuthorizedUser = requestedUser,
            ).left()
            /**
             * 著者である -> 更新可能な記事
             */
            is Right -> { /* 何もしない */ }
        }

        /**
         * 更新可能な作成済み記事
         * バリデーション: エラー -> 早期return
         */
        val updatableCreatedArticle = UpdatableCreatedArticle.new(
            originalCreatedArticle = foundArticle,
            title = title,
            description = description,
            body = body,
        ).fold(
            { return UpdateCreatedArticleUseCase.Error.InvalidArticle(errors = it).left() },
            { it }
        )

        /**
         * 作成済み記事の更新
         */
        return when (val result = articleRepository.update(updatableCreatedArticle)) {
            /**
             * 失敗
             */
            is Left -> when (result.value) {
                /**
                 * 原因: 記事が見つからなかった(DeleteとUpdateがほぼ同時にリクエストされた等)
                 */
                is ArticleRepository.UpdateError.NotFoundArticle -> UpdateCreatedArticleUseCase.Error.NotFoundArticle(validatedSlug).left()
            }
            /**
             * 成功
             */
            is Right -> {
                CreatedArticleWithAuthor(
                    article = CreatedArticle.newWithoutValidation(
                        id = updatableCreatedArticle.articleId,
                        title = updatableCreatedArticle.title,
                        slug = foundArticle.slug,
                        body = updatableCreatedArticle.body,
                        createdAt = foundArticle.createdAt,
                        updatedAt = updatableCreatedArticle.updatedAt,
                        description = updatableCreatedArticle.description,
                        tagList = foundArticle.tagList,
                        authorId = foundArticle.authorId,
                        favorited = foundArticle.favorited,
                        favoritesCount = foundArticle.favoritesCount,
                    ),
                    author = OtherUser.newWithoutValidation(
                        userId = requestedUser.userId,
                        username = requestedUser.username,
                        bio = requestedUser.bio,
                        image = requestedUser.image,
                        following = false
                    ),
                ).right()
            }
        }
    }
}