UpdatableCreatedArticle.kt

package com.example.realworldkotlinspringbootjdbc.domain

import arrow.core.Option
import arrow.core.ValidatedNel
import arrow.core.invalid
import arrow.core.invalidNel
import arrow.core.validNel
import arrow.core.zip
import arrow.typeclasses.Semigroup
import com.example.realworldkotlinspringbootjdbc.domain.article.Body
import com.example.realworldkotlinspringbootjdbc.domain.article.Description
import com.example.realworldkotlinspringbootjdbc.domain.article.Title
import com.example.realworldkotlinspringbootjdbc.util.MyError
import java.util.Date

/**
 * 更新可能な作成済み記事
 *
 * - オリジナルと差分が無い場合、エラーとする
 * - 項目がnullの場合は、オリジナルの方を採用する
 */
interface UpdatableCreatedArticle {
    val articleId: ArticleId
    val title: Title
    val description: Description
    val body: Body
    val updatedAt: Date

    /**
     * 実装
     */
    private data class ValidatedUpdatableCreatedArticle(
        override val articleId: ArticleId,
        override val title: Title,
        override val description: Description,
        override val body: Body,
        override val updatedAt: Date,
    ) : UpdatableCreatedArticle

    companion object {
        fun new(
            originalCreatedArticle: CreatedArticle,
            title: String?,
            description: String?,
            body: String?,
            updatedAt: Date = Date(),
        ): ValidatedNel<MyError.ValidationError, UpdatableCreatedArticle> {
            val newTitleOrOriginal: () -> ValidatedNel<Title.ValidationError, Title> = { ->
                Option.fromNullable(title).fold(
                    { originalCreatedArticle.title.validNel() }, // nullの場合は、オリジナルを採用
                    { Title.new(it) }
                )
            }
            val newDescriptionOrOriginal: () -> ValidatedNel<Description.ValidationError, Description> = { ->
                Option.fromNullable(description).fold(
                    { originalCreatedArticle.description.validNel() }, // nullの場合は、オリジナルを採用
                    { Description.new(it) }
                )
            }
            val newBodyOrOriginal: () -> ValidatedNel<Body.ValidationError, Body> = { ->
                Option.fromNullable(body).fold(
                    { originalCreatedArticle.body.validNel() }, // nullの場合は、オリジナルを採用
                    { Body.new(it) }
                )
            }

            val (validatedTitle, validatedDescription, validatedBody) =
                newTitleOrOriginal().zip(
                    Semigroup.nonEmptyList(),
                    newDescriptionOrOriginal(),
                    newBodyOrOriginal()
                ) { a, b, c -> Triple(a, b, c) }.fold(
                    { return it.invalid() }, // 1つでもバリデーションエラーがある場合: Invalid
                    { it }
                )
            return if (
                validatedTitle.value == originalCreatedArticle.title.value &&
                validatedDescription.value == originalCreatedArticle.description.value &&
                validatedBody.value == originalCreatedArticle.body.value
            ) {
                ValidationError.NothingAttributeToUpdatable.invalidNel()
            } else {
                ValidatedUpdatableCreatedArticle(
                    articleId = originalCreatedArticle.id,
                    title = validatedTitle,
                    description = validatedDescription,
                    body = validatedBody,
                    updatedAt = updatedAt,
                ).validNel()
            }
        }
    }

    /**
     * ドメインルール
     */
    sealed interface ValidationError : MyError.ValidationError {
        /**
         * 変更が1つもないのは駄目
         */
        object NothingAttributeToUpdatable : ValidationError {
            override val key: String get() = UpdatableCreatedArticle::class.simpleName.toString()
            override val message: String get() = "更新する項目が有りません"
        }
    }
}