ArticleController.kt

package com.example.realworldkotlinspringbootjdbc.presentation

import arrow.core.Some
import arrow.core.getOrHandle
import arrow.core.handleError
import arrow.core.none
import arrow.core.toOption
import com.example.realworldkotlinspringbootjdbc.openapi.generated.controller.ArticlesApi
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.Article
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.GenericErrorModel
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.GenericErrorModelErrors
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.MultipleArticlesResponse
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.NewArticleRequest
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.Profile
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.SingleArticleResponse
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.UpdateArticleRequest
import com.example.realworldkotlinspringbootjdbc.presentation.shared.RealworldAuthenticationUseCaseUnauthorizedException
import com.example.realworldkotlinspringbootjdbc.usecase.article.CreateArticleUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.article.DeleteCreatedArticleUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.article.FeedUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.article.FilterCreatedArticleUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.article.ShowArticleUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.article.UpdateCreatedArticleUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.shared.RealworldAuthenticationUseCase
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestController
import java.time.ZoneOffset

@RestController
class ArticleController(
    val realworldAuthenticationUseCase: RealworldAuthenticationUseCase,
    val showArticle: ShowArticleUseCase,
    val filterCreatedArticle: FilterCreatedArticleUseCase,
    val createArticle: CreateArticleUseCase,
    val deleteArticle: DeleteCreatedArticleUseCase,
    val updateArticle: UpdateCreatedArticleUseCase,
    val feed: FeedUseCase,
) : ArticlesApi {

    override fun getArticles(
        authorization: String?,
        tag: String?,
        author: String?,
        favorited: String?,
        limit: Int,
        offset: Int
    ): ResponseEntity<MultipleArticlesResponse> {
        /**
         * authorizationが
         *   - 無い(null) -> None
         *   - 有る -> 認証 -> 成功 -> Some<RegisteredUser>
         *   - 有る -> 認証 -> 失敗 -> 例外スロー
         */
        val currentUser = authorization.toOption().fold(
            { none() },
            {
                realworldAuthenticationUseCase.execute(it)
                    .getOrHandle { e -> throw RealworldAuthenticationUseCaseUnauthorizedException(e) }
                    .toOption()
            }
        )

        val filteredCreatedArticles = filterCreatedArticle.execute(
            tag = tag,
            author = author,
            favoritedByUsername = favorited,
            limit = limit,
            offset = offset,
            currentUser = currentUser
        ).getOrHandle { throw FilterCreatedArticleUseCaseErrorException(it) }

        return ResponseEntity(
            MultipleArticlesResponse(
                articlesCount = filteredCreatedArticles.articlesCount,
                articles = filteredCreatedArticles.articles.map {
                    Article(
                        slug = it.article.slug.value,
                        title = it.article.title.value,
                        description = it.article.description.value,
                        body = it.article.body.value,
                        tagList = it.article.tagList.map { tag -> tag.value },
                        createdAt = it.article.createdAt.toInstant().atOffset(ZoneOffset.UTC),
                        updatedAt = it.article.updatedAt.toInstant().atOffset(ZoneOffset.UTC),
                        favorited = it.article.favorited,
                        favoritesCount = it.article.favoritesCount,
                        author = Profile(
                            username = it.author.username.value,
                            bio = it.author.bio.value,
                            image = it.author.image.value,
                            following = it.author.following,
                        )
                    )
                }
            ),
            HttpStatus.OK
        )
    }

    data class FilterCreatedArticleUseCaseErrorException(val error: FilterCreatedArticleUseCase.Error) : Exception()

    @ExceptionHandler(value = [FilterCreatedArticleUseCaseErrorException::class])
    fun onFilterCreatedArticleUseCaseErrorException(e: FilterCreatedArticleUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (val errorContents = e.error) {
            is FilterCreatedArticleUseCase.Error.FilterParametersValidationErrors -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = errorContents.errors.map { it.message })),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            is FilterCreatedArticleUseCase.Error.NotFoundUser -> throw UnsupportedOperationException("ありえない")
            is FilterCreatedArticleUseCase.Error.OffsetOverCreatedArticlesCountError -> ResponseEntity(
                GenericErrorModel(
                    errors = GenericErrorModelErrors(
                        body = listOf(
                            "offset値がフィルタした結果の作成済み記事の数を超えています(offset=${errorContents.filterParameters.offset}, articlesCount=${errorContents.articlesCount})"
                        )
                    )
                ),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
        }

    override fun createArticle(
        authorization: String,
        article: NewArticleRequest
    ): ResponseEntity<SingleArticleResponse> {
        val currentUser = realworldAuthenticationUseCase.execute(authorization)
            .getOrHandle { throw RealworldAuthenticationUseCaseUnauthorizedException(it) }

        val createdArticleWithAuthor = createArticle.execute(
            currentUser = currentUser,
            title = article.article.title,
            description = article.article.description,
            body = article.article.body,
            tagList = article.article.tagList,
        ).getOrHandle { throw CreateArticleUseCaseErrorException(it) }

        return ResponseEntity(
            SingleArticleResponse(
                article = Article(
                    slug = createdArticleWithAuthor.article.slug.value,
                    title = createdArticleWithAuthor.article.title.value,
                    description = createdArticleWithAuthor.article.description.value,
                    body = createdArticleWithAuthor.article.body.value,
                    tagList = createdArticleWithAuthor.article.tagList.map { it.value }.toList(),
                    createdAt = createdArticleWithAuthor.article.createdAt.toInstant().atOffset(ZoneOffset.UTC),
                    updatedAt = createdArticleWithAuthor.article.updatedAt.toInstant().atOffset(ZoneOffset.UTC),
                    favorited = createdArticleWithAuthor.article.favorited,
                    favoritesCount = createdArticleWithAuthor.article.favoritesCount,
                    author = Profile(
                        username = createdArticleWithAuthor.author.username.value,
                        bio = createdArticleWithAuthor.author.bio.value,
                        image = createdArticleWithAuthor.author.image.value,
                        following = createdArticleWithAuthor.author.following,
                    )
                )
            ),
            HttpStatus.CREATED
        )
    }

    data class CreateArticleUseCaseErrorException(val error: CreateArticleUseCase.Error) : Exception()

    @ExceptionHandler(value = [CreateArticleUseCaseErrorException::class])
    fun onCreateArticleUseCaseErrorException(e: CreateArticleUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (val errorContents = e.error) {
            is CreateArticleUseCase.Error.InvalidArticle -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = errorContents.errors.map { it.message }.toList())),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
        }

    override fun getArticlesFeed(
        authorization: String,
        limit: Int,
        offset: Int
    ): ResponseEntity<MultipleArticlesResponse> {
        val currentUser = realworldAuthenticationUseCase.execute(authorization)
            .getOrHandle { throw RealworldAuthenticationUseCaseUnauthorizedException(it) }

        val feedCreatedArticles = feed.execute(
            currentUser = currentUser,
            limit = limit.toString(),
            offset = offset.toString(),
        ).getOrHandle { throw FeedUseCaseErrorException(it) }

        return ResponseEntity(
            MultipleArticlesResponse(
                articles = feedCreatedArticles.articles.map {
                    Article(
                        slug = it.article.slug.value,
                        title = it.article.title.value,
                        description = it.article.description.value,
                        body = it.article.body.value,
                        tagList = it.article.tagList.map { tag -> tag.value }.toList(),
                        createdAt = it.article.createdAt.toInstant().atOffset(ZoneOffset.UTC),
                        updatedAt = it.article.updatedAt.toInstant().atOffset(ZoneOffset.UTC),
                        favorited = it.article.favorited,
                        favoritesCount = it.article.favoritesCount,
                        author = Profile(
                            username = it.author.username.value,
                            bio = it.author.bio.value,
                            image = it.author.image.value,
                            following = it.author.following,
                        )
                    )
                }.toList(),
                articlesCount = feedCreatedArticles.articlesCount,
            ),
            HttpStatus.OK
        )
    }

    data class FeedUseCaseErrorException(val error: FeedUseCase.Error) : Exception()

    @ExceptionHandler(value = [FeedUseCaseErrorException::class])
    fun onFeedUseCaseErrorException(e: FeedUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (val errorContents = e.error) {
            is FeedUseCase.Error.FeedParameterValidationErrors -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = errorContents.errors.map { it.message }.toList())),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            is FeedUseCase.Error.OffsetOverCreatedArticlesCountError -> ResponseEntity(
                GenericErrorModel(
                    errors = GenericErrorModelErrors(
                        body = listOf(
                            """
                        offset値が作成済み記事の数を超えています(offset=${errorContents.feedParameters.offset}, articlesCount=${errorContents.articlesCount})
                            """.trimIndent()
                        )
                    )
                ),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
        }

    override fun getArticle(slug: String, authorization: String?): ResponseEntity<SingleArticleResponse> {
        /**
         * authorizationが
         *   - 無い(null) -> None
         *   - 有る -> 認証 -> 成功 -> Some<RegisteredUser>
         *   - 有る -> 認証 -> 失敗 -> None
         */
        val currentUser = authorization.toOption().fold(
            { none() },
            {
                realworldAuthenticationUseCase.execute(it).fold(
                    { none() },
                    { user -> Some(user) }
                )
            }
        )

        val createdArticleWithAuthor = showArticle.execute(slug, currentUser)
            .getOrHandle { throw ShowArticleUseCaseErrorException(it) }

        return ResponseEntity(
            SingleArticleResponse(
                article = Article(
                    slug = createdArticleWithAuthor.article.slug.value,
                    title = createdArticleWithAuthor.article.title.value,
                    description = createdArticleWithAuthor.article.description.value,
                    body = createdArticleWithAuthor.article.body.value,
                    tagList = createdArticleWithAuthor.article.tagList.map { tag -> tag.value },
                    createdAt = createdArticleWithAuthor.article.createdAt.toInstant().atOffset(ZoneOffset.UTC),
                    updatedAt = createdArticleWithAuthor.article.updatedAt.toInstant().atOffset(ZoneOffset.UTC),
                    favorited = createdArticleWithAuthor.article.favorited,
                    favoritesCount = createdArticleWithAuthor.article.favoritesCount,
                    author = Profile(
                        username = createdArticleWithAuthor.author.username.value,
                        bio = createdArticleWithAuthor.author.bio.value,
                        image = createdArticleWithAuthor.author.image.value,
                        following = createdArticleWithAuthor.author.following,
                    )
                )
            ),
            HttpStatus.OK
        )
    }

    data class ShowArticleUseCaseErrorException(val error: ShowArticleUseCase.Error) : Exception()

    @ExceptionHandler(value = [ShowArticleUseCaseErrorException::class])
    fun onShowArticleUseCaseErrorException(e: ShowArticleUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (e.error) {
            is ShowArticleUseCase.Error.NotFoundArticleBySlug -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = listOf("記事が見つかりません"))),
                HttpStatus.NOT_FOUND
            )
            is ShowArticleUseCase.Error.NotFoundUser -> throw UnsupportedOperationException("起こることは想定していないエラーです")
            is ShowArticleUseCase.Error.ValidationErrors -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = listOf("記事が見つかりません"))),
                HttpStatus.NOT_FOUND
            )
        }

    override fun updateArticle(
        authorization: String,
        slug: String,
        article: UpdateArticleRequest
    ): ResponseEntity<SingleArticleResponse> {
        val currentUser = realworldAuthenticationUseCase.execute(authorization)
            .getOrHandle { throw RealworldAuthenticationUseCaseUnauthorizedException(it) }

        val updatedArticleWithAuthor = updateArticle.execute(
            requestedUser = currentUser,
            slug = slug,
            title = article.article.title,
            description = article.article.description,
            body = article.article.body
        ).getOrHandle { throw UpdateCreatedArticleUseCaseErrorException(it) }

        return ResponseEntity(
            SingleArticleResponse(
                article = Article(
                    slug = updatedArticleWithAuthor.article.slug.value,
                    title = updatedArticleWithAuthor.article.title.value,
                    description = updatedArticleWithAuthor.article.description.value,
                    body = updatedArticleWithAuthor.article.body.value,
                    tagList = updatedArticleWithAuthor.article.tagList.map { it.value }.toList(),
                    createdAt = updatedArticleWithAuthor.article.createdAt.toInstant().atOffset(ZoneOffset.UTC),
                    updatedAt = updatedArticleWithAuthor.article.updatedAt.toInstant().atOffset(ZoneOffset.UTC),
                    favorited = updatedArticleWithAuthor.article.favorited,
                    favoritesCount = updatedArticleWithAuthor.article.favoritesCount,
                    author = Profile(
                        username = updatedArticleWithAuthor.author.username.value,
                        bio = updatedArticleWithAuthor.author.bio.value,
                        image = updatedArticleWithAuthor.author.image.value,
                        following = updatedArticleWithAuthor.author.following,
                    )
                )
            ),
            HttpStatus.OK
        )
    }

    data class UpdateCreatedArticleUseCaseErrorException(val error: UpdateCreatedArticleUseCase.Error) : Exception()

    @ExceptionHandler(value = [UpdateCreatedArticleUseCaseErrorException::class])
    fun onUpdateCreatedArticleUseCaseErrorException(e: UpdateCreatedArticleUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (val errorContents = e.error) {
            is UpdateCreatedArticleUseCase.Error.InvalidArticle -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = errorContents.errors.map { it.message }.toList())),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            is UpdateCreatedArticleUseCase.Error.InvalidSlug -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = errorContents.errors.map { it.message }.toList())),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            is UpdateCreatedArticleUseCase.Error.NotAuthor -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = listOf("削除する権限がありません"))),
                HttpStatus.FORBIDDEN
            )
            is UpdateCreatedArticleUseCase.Error.NotFoundArticle -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = listOf("記事が見つかりません"))),
                HttpStatus.NOT_FOUND
            )
        }

    override fun deleteArticle(authorization: String, slug: String): ResponseEntity<Unit> {
        val currentUser = realworldAuthenticationUseCase.execute(authorization)
            .getOrHandle { throw RealworldAuthenticationUseCaseUnauthorizedException(it) }

        deleteArticle.execute(
            author = currentUser,
            slug = slug,
        ).handleError { throw DeleteCreatedArticleUseCaseErrorException(it) }

        return ResponseEntity(Unit, HttpStatus.OK)
    }

    data class DeleteCreatedArticleUseCaseErrorException(val error: DeleteCreatedArticleUseCase.Error) : Exception()

    @ExceptionHandler(value = [DeleteCreatedArticleUseCaseErrorException::class])
    fun onDeleteCreatedArticleUseCaseErrorException(e: DeleteCreatedArticleUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (val errorContents = e.error) {
            is DeleteCreatedArticleUseCase.Error.NotAuthor -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = listOf("削除する権限がありません"))),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            is DeleteCreatedArticleUseCase.Error.NotFoundArticle -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = listOf("記事が見つかりませんでした"))),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            is DeleteCreatedArticleUseCase.Error.ValidationError -> ResponseEntity(
                GenericErrorModel(errors = GenericErrorModelErrors(body = errorContents.errors.map { it.message }.toList())),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
        }
}