FilterParameters.kt

package com.example.realworldkotlinspringbootjdbc.domain.article

import arrow.core.Option
import arrow.core.ValidatedNel
import arrow.core.invalidNel
import arrow.core.validNel
import arrow.core.zip
import arrow.typeclasses.Semigroup
import com.example.realworldkotlinspringbootjdbc.util.MyError

/**
 * フィルタ用パラメーター
 *
 * 作成済み記事一覧のフィルタに使うパラメータ郡
 *
 * tag: タグでフィルタ
 * author: 書いた人でフィルタ
 * favoritesByUsername: 特定のユーザーがお気に入り済みかどうかでフィルタ
 * limit: 1度に表示する記事の最大値
 * offset: 何個目から記事を表示するか(100個あるうち、offset=10で11個目から等)
 *
 * 注
 * - Option<T>の時、Noneはフィルタなしを表現する
 * - Usernameなどを利用して、バリデーションは通さない
 *   - 例: InvalidなUsernameをNoneにした時、フィルタ無しになってしまうため
 */
interface FilterParameters {
    val tag: Option<String>
    val author: Option<String>
    val favoritedByUsername: Option<String>
    val limit: Int
    val offset: Int

    /**
     * 実装
     */
    private data class ValidatedFilterParameters(
        override val tag: Option<String>,
        override val author: Option<String>,
        override val favoritedByUsername: Option<String>,
        override val limit: Int,
        override val offset: Int
    ) : FilterParameters

    /**
     * Factory メソッド
     */
    companion object {
        /**
         * new
         *
         * @param tag
         * @param author
         * @param favoritedByUsername
         * @param limit
         * @param offset
         * @return バリデーションエラー or フィルタ用パラメーター
         */
        fun new(
            tag: String? = null,
            author: String? = null,
            favoritedByUsername: String? = null,
            limit: Int = ValidationError.LimitError.DEFAULT,
            offset: Int = ValidationError.OffsetError.DEFAULT,
        ): ValidatedNel<ValidationError, FilterParameters> {
            val convertToValidatedLimit: (Int) -> ValidatedNel<ValidationError, Int> = {
                when {
                    it < ValidationError.LimitError.MINIMUM -> ValidationError.LimitError.RequireMinimumOrOver(it).invalidNel()
                    it > ValidationError.LimitError.MAXIMUM -> ValidationError.LimitError.RequireMaximumOrUnder(it).invalidNel()
                    else -> it.validNel()
                }
            }
            val convertToValidatedOffset: (Int) -> ValidatedNel<ValidationError, Int> = {
                when {
                    it < ValidationError.OffsetError.MINIMUM -> ValidationError.OffsetError.RequireMinimumOrOver(it).invalidNel()
                    else -> it.validNel()
                }
            }

            /**
             * 引数 -> ValidatedNel<ValidationError, Int>
             */
            return convertToValidatedLimit(limit).zip(
                Semigroup.nonEmptyList(),
                convertToValidatedOffset(offset)
            ) { a, b ->
                ValidatedFilterParameters(
                    tag = Option.fromNullable(tag),
                    author = Option.fromNullable(author),
                    favoritedByUsername = Option.fromNullable(favoritedByUsername),
                    limit = a,
                    offset = b
                )
            }
        }
    }

    /**
     * ドメインルール
     */
    sealed interface ValidationError : MyError.ValidationError {
        sealed interface LimitError : ValidationError {
            companion object {
                const val DEFAULT = 20
                const val MINIMUM = 1
                const val MAXIMUM = 100
            }
            override val key: String get() = LimitError::class.simpleName.toString()
            data class RequireMinimumOrOver(val value: Int) : LimitError {
                override val message: String get() = "${MINIMUM}以上である必要があります"
            }
            data class RequireMaximumOrUnder(val value: Int) : LimitError {
                override val message: String get() = "${MAXIMUM}以下である必要があります"
            }
        }

        sealed interface OffsetError : ValidationError {
            companion object {
                const val DEFAULT = 0
                const val MINIMUM = 0
                // const val MAXIMUM = Int.MAX_VALUE
            }
            override val key: String get() = LimitError::class.simpleName.toString()
            data class RequireMinimumOrOver(val value: Int) : LimitError {
                override val message: String get() = "${MINIMUM}以上である必要があります"
            }
            /**
             * MAXIMUM = Int.MAX_VALUEなので実装しない
             */
        }
    }
}