Email.kt

package com.example.realworldkotlinspringbootjdbc.domain.user

import arrow.core.Option
import arrow.core.Validated
import arrow.core.ValidatedNel
import arrow.core.invalid
import arrow.core.invalidNel
import arrow.core.valid
import com.example.realworldkotlinspringbootjdbc.util.MyError

interface Email {
    val value: String

    /**
     * 実装
     */
    private data class EmailImpl(override val value: String) : Email

    /**
     * Factory メソッド
     */
    companion object {
        /**
         * Validation 無し
         */
        fun newWithoutValidation(email: String): Email = EmailImpl(email)

        /**
         * Validation 有り
         */
        fun new(email: String?): ValidatedNel<ValidationError, Email> =
            when (val result = ValidationError.Required.check(email)) {
                is Validated.Invalid -> result.value.invalidNel()
                is Validated.Valid -> {
                    val existedEmail = result.value
                    ValidationError.InvalidFormat.check(existedEmail)
                        .map { EmailImpl(existedEmail) }
                }
            }
    }

    /**
     * ドメインルール
     */
    sealed interface ValidationError : MyError.ValidationError {
        override val key: String get() = Email::class.simpleName.toString()

        /**
         * Nullは駄目
         */
        object Required : ValidationError {
            override val message: String get() = "メールアドレスを入力してください。"
            fun check(password: String?): Validated<Required, String> =
                Option.fromNullable(password).fold(
                    { Required.invalid() },
                    { it.valid() }
                )
        }

        /**
         * メールアドレスの形式であること
         *
         * 参考
         * https://android.googlesource.com/platform/frameworks/base/+/81aa097/core/java/android/util/Patterns.java#146
         */
        data class InvalidFormat(val email: String) : ValidationError {
            override val message: String get() = "メールアドレスが不正な形式です。(正しい形式例:john@example.com)"
            companion object {
                private const val emailPattern = """[a-zA-Z0-9+._%\-+]{1,256}""" +
                    """\@""" +
                    """[a-zA-Z0-9][a-zA-Z0-9\-]{0,64}""" +
                    "(" +
                    """\.""" +
                    """[a-zA-Z0-9][a-zA-Z0-9\-]{0,25}""" +
                    ")+"
                fun check(email: String): ValidatedNel<ValidationError, Unit> =
                    if (email.matches(emailPattern.toRegex())) { Unit.valid() } else { InvalidFormat(email).invalidNel() }
            }
        }
    }
}