使用Arrow-kt验证对象

时间:2019-05-20 09:20:43

标签: kotlin arrow-kt

我有一个对象(书),应该通过事件(作者已更改)来更新哪些字段。可以说,只有当作者结婚并改名后,本书的“作者”字段才会更改,但是如果作者只是搬到新城市,则本书不会更改。

在这种简单情况下,我可以检查是否book.authorName == event.author.name并返回一个Either<NothingChangedFailure, Book>。但是,我该如何检查多个领域?如果我继续使用Either,则该过程将在遇到的第一个NothingChangedFailure处停止,但是我想汇总所有更新,并且如果书中的任何字段均未更改,则仅返回NothingChangedFailure。 / p>

我尝试使用OptionEither并继续阅读Validated,但如果出现单个故障,它们似乎都无法使整个结果都通过。那有我看不到的选择吗?

1 个答案:

答案 0 :(得分:1)

Validated上有一个示例,展示了我们可以编写验证失败的情况。

对于您的情况(我将在这里进行假设,就像书中的可用字段一样),我想它看起来像是:

data class Book(val title: String, val authorName: String, val pageCount: Int)

在这里,我们使用Semigroup的定义来创建错误:

sealed class BookValidationError {
  data class PropertyNotChanged(val propertyName: String) : BookValidationError()
  data class Multiple(val errors: Nel<BookValidationError>) : BookValidationError()
}

object BookValidationErrorSemigroup : Semigroup<BookValidationError> {
  override fun BookValidationError.combine(b: BookValidationError): BookValidationError = when {
      this is Multiple && b is Multiple -> Multiple(errors + b.errors)
      this is Multiple && b !is Multiple -> Multiple(errors + b)
      this !is Multiple && b is Multiple -> Multiple(this.nel() + b.errors)
      else -> BookValidationError.Multiple(NonEmptyList(this, b))
  } 
}

然后我们可以为错误类型定义相关的ApplicativeError

private val bookApplicativeError : ApplicativeError<ValidatedPartialOf<BookValidationError>, BookValidationError> = 
  Validated.applicativeError(BookValidationErrorSemigroup)

我们将其与帮助器类结合在一起

class BookValidation(
  private val book: Book
) : ApplicativeError<ValidatedPartialOf<BookValidationError>, BookValidationError> by bookApplicativeError {

    fun <T> fieldIsNot(name: String, actualValue: T, incorrectValue: T): Kind<ValidatedPartialOf<BookValidationError>, Book> =
        if(actualValue == incorrectValue) raiseError(BookValidationError.PropertyNotChanged(name))
        else just(book)

}

和便捷访问扩展功能:

fun Book.validateThat(titleIsNot : String, authorNameIsNot: String, pageCountIsNot: Int) = 
    with(BookValidation(this)) {
        map(
            fieldIsNot("title", title, titleIsNot), 
            fieldIsNot("authorName", authorName, authorNameIsNot),
            fieldIsNot("pageCount", pageCount, pageCountIsNot)
        ) { this@validateThat }.handleErrorWith { 
            raiseError(it) 
        }
    }

然后,如果您像这样执行它:

fun main() {
    Book("a", "b", 123).validateThat(
        titleIsNot = "c",
        authorNameIsNot = "d",
        pageCountIsNot = 124
    ).let(::println)
    Book("a", "b", 123).validateThat(
        titleIsNot = "a",
        authorNameIsNot = "b",
        pageCountIsNot = 123
    ).let(::println)
    Book("a", "b", 123).validateThat(
        titleIsNot = "c",
        authorNameIsNot = "b",
        pageCountIsNot = 124
    ).let(::println)
}

第一个有效,输出如下:

Valid(a=Book(title=a, authorName=b, pageCount=123))

但是第二个会输出:

Invalid(e=Multiple(errors=NonEmptyList(all=[PropertyNotChanged(propertyName=pageCount), PropertyNotChanged(propertyName=title), PropertyNotChanged(propertyName=authorName)])))

在此Invalid实例内部,我们有一个NonEmptyList,其中包含所有未通过验证的字段。如果我们重新格式化输出,就可以看到它们:

Invalid(e=Multiple(
  errors=NonEmptyList(all=[
    PropertyNotChanged(propertyName=pageCount), 
    PropertyNotChanged(propertyName=title), 
    PropertyNotChanged(propertyName=authorName)
  ])
))

现在,对于第三种情况,由于只有其中一个保持不变,我们得到以下输出:

Invalid(e=PropertyNotChanged(propertyName=authorName))