CommentController.kt

package com.example.realworldkotlinspringbootjdbc.presentation

import arrow.core.Some
import arrow.core.none
import arrow.core.toOption
import com.example.realworldkotlinspringbootjdbc.openapi.generated.controller.CommentsApi
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.Comment
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.GenericErrorModel
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.GenericErrorModelErrors
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.MultipleCommentsResponse
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.NewCommentRequest
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.Profile
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.SingleCommentResponse
import com.example.realworldkotlinspringbootjdbc.presentation.shared.RealworldAuthenticationUseCaseUnauthorizedException
import com.example.realworldkotlinspringbootjdbc.usecase.comment.CreateCommentUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.comment.DeleteCommentUseCase
import com.example.realworldkotlinspringbootjdbc.usecase.comment.ListCommentUseCase
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 CommentController(
    val realworldAuthenticationUseCase: RealworldAuthenticationUseCase,
    val listCommentUseCase: ListCommentUseCase,
    val createCommentUseCase: CreateCommentUseCase,
    val deleteCommentUseCase: DeleteCommentUseCase
) : CommentsApi {
    override fun getArticleComments(slug: String, authorization: String?): ResponseEntity<MultipleCommentsResponse> {
        val currentUser = authorization.toOption().fold(
            { none() },
            {
                realworldAuthenticationUseCase.execute(it).fold(
                    { none() },
                    { result -> Some(result) }
                )
            }
        )

        val commentWithAuthors = listCommentUseCase.execute(slug, currentUser).fold(
            { throw ListCommentUseCaseErrorException(it) },
            { it }
        )

        return ResponseEntity(
            MultipleCommentsResponse(
                commentWithAuthors.map {
                    Comment(
                        id = it.comment.id.value,
                        createdAt = it.comment.createdAt.toInstant().atOffset(ZoneOffset.UTC),
                        updatedAt = it.comment.updatedAt.toInstant().atOffset(ZoneOffset.UTC),
                        body = it.comment.body.value,
                        Profile(
                            username = it.author.username.value,
                            bio = it.author.bio.value,
                            image = it.author.image.value,
                            following = it.author.following
                        )
                    )
                }
            ),
            HttpStatus.OK
        )
    }

    data class ListCommentUseCaseErrorException(val error: ListCommentUseCase.Error) : Exception()

    @ExceptionHandler(value = [ListCommentUseCaseErrorException::class])
    fun onListCommentUseCaseErrorException(e: ListCommentUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (e.error) {
            /**
             * slug が不正だった場合
             */
            is ListCommentUseCase.Error.InvalidSlug -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("slug が不正です"))),
                HttpStatus.UNPROCESSABLE_ENTITY
            )

            /**
             * slug に該当する記事が見つからなかった場合
             */
            is ListCommentUseCase.Error.NotFound -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("記事が見つかりませんでした"))),
                HttpStatus.NOT_FOUND
            )
        }

    override fun createArticleComment(
        authorization: String,
        slug: String,
        comment: NewCommentRequest
    ): ResponseEntity<SingleCommentResponse> {
        val currenUser = realworldAuthenticationUseCase.execute(authorization).fold(
            { throw RealworldAuthenticationUseCaseUnauthorizedException(it) },
            { it }
        )

        val commentWithOtherUser = createCommentUseCase.execute(slug, comment.comment.body, currenUser).fold(
            { throw CreateCommentUseCaseErrorException(it) },
            { it }
        )

        return ResponseEntity(
            SingleCommentResponse(
                Comment(
                    id = commentWithOtherUser.comment.id.value,
                    createdAt = commentWithOtherUser.comment.createdAt.toInstant().atOffset(ZoneOffset.UTC),
                    updatedAt = commentWithOtherUser.comment.updatedAt.toInstant().atOffset(ZoneOffset.UTC),
                    body = commentWithOtherUser.comment.body.value,
                    Profile(
                        username = commentWithOtherUser.author.username.value,
                        bio = commentWithOtherUser.author.bio.value,
                        image = commentWithOtherUser.author.image.value,
                        following = commentWithOtherUser.author.following
                    )
                )
            ),
            HttpStatus.OK
        )
    }

    data class CreateCommentUseCaseErrorException(val error: CreateCommentUseCase.Error) : Exception()

    @ExceptionHandler(value = [CreateCommentUseCaseErrorException::class])
    fun onCreateCommentUseCaseErrorException(e: CreateCommentUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (val error = e.error) {
            /**
             * 原因: CommentBody がバリデーションエラー
             */
            is CreateCommentUseCase.Error.InvalidCommentBody -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = error.errors.map { it.message })),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            /**
             * 原因: Slug がバリデーションエラー
             */
            is CreateCommentUseCase.Error.InvalidSlug -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("slug が不正です"))),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            /**
             * 原因: 記事が見つかりませんでした
             */
            is CreateCommentUseCase.Error.NotFound -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("記事が見つかりませんでした"))),
                HttpStatus.NOT_FOUND
            )
        }

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

        deleteCommentUseCase.execute(slug, id, currentUser).fold(
            { throw DeleteCommentUseCaseErrorException(it) },
            {}
        )

        return ResponseEntity(
            HttpStatus.OK
        )
    }

    data class DeleteCommentUseCaseErrorException(val error: DeleteCommentUseCase.Error) : Exception()

    @ExceptionHandler(value = [DeleteCommentUseCaseErrorException::class])
    fun onDeleteCommentUseCaseErrorException(e: DeleteCommentUseCaseErrorException): ResponseEntity<GenericErrorModel> =
        when (e.error) {
            /**
             * 原因: CommentId がバリデーションエラー
             */
            is DeleteCommentUseCase.Error.InvalidCommentId -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("コメント ID が不正です"))),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            /**
             * 原因: Slug がバリデーションエラー
             */
            is DeleteCommentUseCase.Error.InvalidSlug -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("Slug が不正です"))),
                HttpStatus.UNPROCESSABLE_ENTITY
            )
            /**
             * 原因: 認可されていない(削除しようとしたが実行ユーザー(CurrentUserId)のものじゃなかった)
             */
            is DeleteCommentUseCase.Error.NotAuthorizedDeleteComment -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("コメントの削除が許可されていません"))),
                HttpStatus.UNAUTHORIZED
            )
            /**
             * 原因: 記事が見つからなかった
             */
            is DeleteCommentUseCase.Error.NotFoundArticleBySlug -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("記事が見つかりませんでした"))),
                HttpStatus.NOT_FOUND
            )
            /**
             * 原因: CommentId に該当するコメントがなかった
             */
            is DeleteCommentUseCase.Error.NotFoundCommentByCommentId -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("コメントが見つかりませんでした"))),
                HttpStatus.NOT_FOUND
            )
        }
}