diff --git a/build.sbt b/build.sbt index d710d7256..b4823c178 100644 --- a/build.sbt +++ b/build.sbt @@ -15,7 +15,7 @@ lazy val commonSettings = Seq( Wart.JavaSerializable, Wart.Serializable, Wart.OptionPartial), - scalaVersion := "2.12.7", + scalaVersion := "2.12.9", organization := "org.scorexfoundation", licenses := Seq("CC0" -> url("https://creativecommons.org/publicdomain/zero/1.0/legalcode")), homepage := Some(url("https://github.com/ScorexFoundation/Scorex")), diff --git a/src/main/scala/scorex/core/validation/ModifierValidator.scala b/src/main/scala/scorex/core/validation/ModifierValidator.scala index 2f82be556..d9b027274 100644 --- a/src/main/scala/scorex/core/validation/ModifierValidator.scala +++ b/src/main/scala/scorex/core/validation/ModifierValidator.scala @@ -1,12 +1,7 @@ package scorex.core.validation - -import scorex.core.consensus.ModifierSemanticValidity import scorex.core.utils.ScorexEncoder import scorex.core.validation.ValidationResult._ -import scorex.util.ModifierId - -import scala.util.{Failure, Success, Try} /** * Object with helpers for the modifier validation process. @@ -23,9 +18,21 @@ import scala.util.{Failure, Success, Try} */ object ModifierValidator { - def apply(settings: ValidationSettings)(implicit e: ScorexEncoder): ValidationState[Unit] = { - ValidationState(ModifierValidator.success, settings)(e) - } + /** Start validation in Fail-Fast mode */ + def failFast(implicit e: ScorexEncoder): ValidationState[Unit] = + ValidationState(ModifierValidator.success, ValidationStrategy.FailFast)(e) + + /** Start validation accumulating all the errors */ + def accumulateErrors(implicit e: ScorexEncoder): ValidationState[Unit] = + ValidationState(ModifierValidator.success, ValidationStrategy.AccumulateErrors)(e) + + /** Start tagged validation in Fail-Fast mode */ + def failFastTagged(settings: TaggedValidationRules)(implicit e: ScorexEncoder): TaggedValidationState[Unit] = + TaggedValidationState(ModifierValidator.success, ValidationStrategy.FailFast, settings)(e) + + /** Start tagged validation accumulating all the errors */ + def accumulateErrorsTagged(settings: TaggedValidationRules)(implicit e: ScorexEncoder): TaggedValidationState[Unit] = + TaggedValidationState(ModifierValidator.success, ValidationStrategy.AccumulateErrors, settings)(e) /** report recoverable modifier error that could be fixed by later retries */ def error(errorMessage: String): Invalid = @@ -51,9 +58,8 @@ object ModifierValidator { def fatal(description: String, detail: String): Invalid = fatal(msg(description, detail)) /** unsuccessful validation with a given error; also logs the error as an exception */ - def invalid(error: ModifierError): Invalid = { + def invalid(error: ModifierError): Invalid = Invalid(Seq(error)) - } /** successful validation without payload */ val success: Valid[Unit] = Valid(()) @@ -62,135 +68,3 @@ object ModifierValidator { private def msg(description: String, detail: String): String = s"$description: $detail" } - -/** This is the place where all the validation DSL lives */ -case class ValidationState[T](result: ValidationResult[T], settings: ValidationSettings)(implicit e: ScorexEncoder) { - - /** Create the next validation state as the result of given `operation` */ - def pass[R](operation: => ValidationResult[R]): ValidationState[R] = { - result match { - case Valid(_) => copy(result = operation) - case Invalid(_) if settings.isFailFast || result == operation => asInstanceOf[ValidationState[R]] - case invalid@Invalid(_) => copy(result = invalid.accumulateErrors(operation)) - } - } - - /** Replace payload with the new one, discarding current payload value. This method catches throwables - */ - def payload[R](payload: => R): ValidationState[R] = { - pass(result(payload)) - } - - /** Map payload if validation is successful - */ - def payloadMap[R](f: T => R): ValidationState[R] = { - copy(result = result.map(f)) - } - - /** Validate the condition is `true` or else return the `error` given - */ - def validate(id: Short, condition: => Boolean, details: String = ""): ValidationState[T] = { - pass(if (!settings.isActive(id) || condition) result else settings.getError(id, details)) - } - - /** Reverse condition: Validate the condition is `false` or else return the `error` given */ - def validateNot(id: Short, condition: => Boolean, details: String = ""): ValidationState[T] = { - validate(id, !condition, details) - } - - /** Validate the first argument equals the second. This should not be used with `ModifierId` of type `Array[Byte]`. - * The `error` callback will be provided with detail on argument values for better reporting - */ - def validateEquals[A](id: Short, given: => A, expected: => A): ValidationState[T] = { - pass((given, expected) match { - case _ if !settings.isActive(id) => result - case (a: Array[_], b: Array[_]) if a sameElements b => result - case (_: Array[_], _) => settings.getError(id, s"Given: $given, expected: $expected. Use validateEqualIds when comparing Arrays") - case _ if given == expected => result - case _ => settings.getError(id, s"Given: $given, expected $expected") - }) - } - - /** Validate the `id`s are equal. The `error` callback will be provided with detail on argument values - */ - def validateEqualIds(id: Short, given: => ModifierId, expected: => ModifierId): ValidationState[T] = { - pass { - if (!settings.isActive(id) || given == expected) result - else settings.getError(id, s"Given: ${e.encodeId(given)}, expected ${e.encodeId(expected)}") - } - } - - /** Wrap semantic validity to the validation state: if semantic validity was not Valid, then return the `error` given - */ - def validateSemantics(id: Short, validity: => ModifierSemanticValidity, details: String = ""): ValidationState[T] = { - validateNot(id, validity == ModifierSemanticValidity.Invalid, details) - } - - /** Validate the `condition` is `Success`. Otherwise the `error` callback will be provided with detail - * on a failure exception - */ - def validateNoFailure(id: Short, condition: => Try[_]): ValidationState[T] = { - pass(if (!settings.isActive(id)) result else condition.fold(e => settings.getError(id, e), _ => result)) - } - - /** Validate the `block` doesn't throw an Exception. Otherwise the `error` callback will be provided with detail - * on the exception - */ - def validateNoThrow(id: Short, block: => Any): ValidationState[T] = { - validateNoFailure(id, Try(block)) - } - - /** Validate `operation` against payload is `Valid` or else return the `error` - */ - def validateTry[A](tryValue: => Try[A], error: Throwable => Invalid) - (operation: (ValidationState[T], A) => ValidationResult[T]): ValidationState[T] = { - pass(tryValue.fold(error, v => operation(this, v))) - } - - /** Validate `condition` against payload is `true` or else return the `error` - */ - def validateTryFlatten(id: Short, operation: T => Try[T], condition: T => Boolean): ValidationState[T] = { - pass(result.toTry.flatMap(r => operation(r)) match { - case Failure(ex) => settings.getError(id, ex) - case Success(v) if settings.isActive(id) && !condition(v) => settings.getError(id) - case Success(v) => result(v) - }) - } - - - /** Validate `operation` against option value if it's not `None`. - * If given option is `None` then pass the previous result as success. - * Return `error` if option is `Some` amd condition is `Invalid` - */ - def validateOrSkip[A](option: => Option[A]) - (operation: (ValidationState[T], A) => ValidationResult[T]): ValidationState[T] = { - option - .map(value => pass(operation(this, value))) - .getOrElse(this) - } - - /** Validate condition against option value if it's not `None`. - * If given option is `None` then pass the previous result as success. - * Return `error` if option is `Some` amd condition is `false` - */ - def validateOrSkipFlatten[A](id: Short, option: => Option[A], condition: A => Boolean): ValidationState[T] = { - pass(option match { - case Some(v) if settings.isActive(id) && !condition(v) => settings.getError(id) - case _ => result - }) - } - - /** This could add some sugar when validating elements of a given collection - */ - def validateSeq[A](seq: Iterable[A]) - (operation: (ValidationState[T], A) => ValidationResult[T]): ValidationState[T] = { - seq.foldLeft(this) { (state, elem) => - state.pass(operation(state, elem)) - } - } - - /** This is for nested validations that allow mixing fail-fast and accumulate-errors validation strategies - */ - def validate(operation: => ValidationResult[T]): ValidationState[T] = pass(operation) - -} diff --git a/src/main/scala/scorex/core/validation/ValidationSettings.scala b/src/main/scala/scorex/core/validation/TaggedValidationRules.scala similarity index 60% rename from src/main/scala/scorex/core/validation/ValidationSettings.scala rename to src/main/scala/scorex/core/validation/TaggedValidationRules.scala index 78cb6d61c..799444df2 100644 --- a/src/main/scala/scorex/core/validation/ValidationSettings.scala +++ b/src/main/scala/scorex/core/validation/TaggedValidationRules.scala @@ -3,11 +3,9 @@ package scorex.core.validation import scorex.core.validation.ValidationResult.Invalid /** - * Specifies the strategy to by used (fail-fast or error-accumulative), a set of - * activated validation rules with corresponding error messages + * Validation rules defining how to treat particular tagged rule. */ -abstract class ValidationSettings { - val isFailFast: Boolean +abstract class TaggedValidationRules { def getError(id: Short, e: Throwable): Invalid = getError(id, e.getMessage) diff --git a/src/main/scala/scorex/core/validation/TaggedValidationState.scala b/src/main/scala/scorex/core/validation/TaggedValidationState.scala new file mode 100644 index 000000000..a315ecd7d --- /dev/null +++ b/src/main/scala/scorex/core/validation/TaggedValidationState.scala @@ -0,0 +1,129 @@ +package scorex.core.validation + +import scorex.core.consensus.ModifierSemanticValidity +import scorex.core.utils.ScorexEncoder +import scorex.core.validation.ValidationResult.{Invalid, Valid} +import scorex.util.ModifierId + +import scala.util.{Failure, Success, Try} + +/** Allows to disable validation for particular rule (set of rules) by id. + * This is the place where all the validation DSL lives */ +final case class TaggedValidationState[T](result: ValidationResult[T], + strategy: ValidationStrategy, + settings: TaggedValidationRules) + (implicit e: ScorexEncoder) { + + /** Create the next validation state as the result of given `operation` */ + def pass[R](operation: => ValidationResult[R]): TaggedValidationState[R] = + result match { + case Valid(_) => copy(result = operation) + case Invalid(_) if strategy.isFailFast || result == operation => asInstanceOf[TaggedValidationState[R]] + case invalid@Invalid(_) => copy(result = invalid.accumulateErrors(operation)) + } + + /** Replace payload with the new one, discarding current payload value. This method catches throwables + */ + def payload[R](payload: => R): TaggedValidationState[R] = + pass(result(payload)) + + /** Map payload if validation is successful + */ + def payloadMap[R](f: T => R): TaggedValidationState[R] = + copy(result = result.map(f)) + + /** Validate the condition is `true` or else return the `error` given + */ + def validate(id: Short, condition: => Boolean, details: String = ""): TaggedValidationState[T] = + pass(if (!settings.isActive(id) || condition) result else settings.getError(id, details)) + + /** Reverse condition: Validate the condition is `false` or else return the `error` given */ + def validateNot(id: Short, condition: => Boolean, details: String = ""): TaggedValidationState[T] = + validate(id, !condition, details) + + /** Validate the first argument equals the second. This should not be used with `ModifierId` of type `Array[Byte]`. + * The `error` callback will be provided with detail on argument values for better reporting + */ + def validateEquals[A](id: Short, given: => A, expected: => A): TaggedValidationState[T] = + pass((given, expected) match { + case _ if !settings.isActive(id) => result + case (a: Array[_], b: Array[_]) if a sameElements b => result + case (_: Array[_], _) => settings.getError(id, s"Given: $given, expected: $expected. Use validateEqualIds when comparing Arrays") + case _ if given == expected => result + case _ => settings.getError(id, s"Given: $given, expected $expected") + }) + + /** Validate the `id`s are equal. The `error` callback will be provided with detail on argument values + */ + def validateEqualIds(id: Short, given: => ModifierId, expected: => ModifierId): TaggedValidationState[T] = + pass { + if (!settings.isActive(id) || given == expected) result + else settings.getError(id, s"Given: ${e.encodeId(given)}, expected ${e.encodeId(expected)}") + } + + /** Wrap semantic validity to the validation state: if semantic validity was not Valid, then return the `error` given + */ + def validateSemantics(id: Short, validity: => ModifierSemanticValidity, details: String = "" + ): TaggedValidationState[T] = + validateNot(id, validity == ModifierSemanticValidity.Invalid, details) + + /** Validate the `condition` is `Success`. Otherwise the `error` callback will be provided with detail + * on a failure exception + */ + def validateNoFailure(id: Short, condition: => Try[_]): TaggedValidationState[T] = + pass(if (!settings.isActive(id)) result else condition.fold(e => settings.getError(id, e), _ => result)) + + /** Validate the `block` doesn't throw an Exception. Otherwise the `error` callback will be provided with detail + * on the exception + */ + def validateNoThrow(id: Short, block: => Any): TaggedValidationState[T] = + validateNoFailure(id, Try(block)) + + /** Validate `operation` against payload is `Valid` or else return the `error` + */ + def validateTry[A](tryValue: => Try[A], error: Throwable => Invalid) + (operation: (TaggedValidationState[T], A) => ValidationResult[T]): TaggedValidationState[T] = + pass(tryValue.fold(error, v => operation(this, v))) + + /** Validate `condition` against payload is `true` or else return the `error` + */ + def validateTryFlatten(id: Short, operation: T => Try[T], condition: T => Boolean): TaggedValidationState[T] = + pass(result.toTry.flatMap(r => operation(r)) match { + case Failure(ex) => settings.getError(id, ex) + case Success(v) if settings.isActive(id) && !condition(v) => settings.getError(id) + case Success(v) => result(v) + }) + + /** Validate `operation` against option value if it's not `None`. + * If given option is `None` then pass the previous result as success. + * Return `error` if option is `Some` amd condition is `Invalid` + */ + def validateOrSkip[A](option: => Option[A]) + (operation: (TaggedValidationState[T], A) => ValidationResult[T]): TaggedValidationState[T] = + option + .map(value => pass(operation(this, value))) + .getOrElse(this) + + /** Validate condition against option value if it's not `None`. + * If given option is `None` then pass the previous result as success. + * Return `error` if option is `Some` amd condition is `false` + */ + def validateOrSkipFlatten[A](id: Short, option: => Option[A], condition: A => Boolean): TaggedValidationState[T] = + pass(option match { + case Some(v) if settings.isActive(id) && !condition(v) => settings.getError(id) + case _ => result + }) + + /** This could add some sugar when validating elements of a given collection + */ + def validateSeq[A](seq: Iterable[A]) + (operation: (TaggedValidationState[T], A) => ValidationResult[T]): TaggedValidationState[T] = + seq.foldLeft(this) { (state, elem) => + state.pass(operation(state, elem)) + } + + /** This is for nested validations that allow mixing fail-fast and accumulate-errors validation strategies + */ + def validate(operation: => ValidationResult[T]): TaggedValidationState[T] = pass(operation) + +} diff --git a/src/main/scala/scorex/core/validation/ValidationResult.scala b/src/main/scala/scorex/core/validation/ValidationResult.scala index 028e08b39..5109019c8 100644 --- a/src/main/scala/scorex/core/validation/ValidationResult.scala +++ b/src/main/scala/scorex/core/validation/ValidationResult.scala @@ -100,7 +100,7 @@ object ValidationResult { } /** Shorthand to get the result of validation */ - implicit def fromValidationState[R](state: ValidationState[R]): ValidationResult[R] = state.result + implicit def fromValidationState[R](state: TaggedValidationState[R]): ValidationResult[R] = state.result } diff --git a/src/main/scala/scorex/core/validation/ValidationState.scala b/src/main/scala/scorex/core/validation/ValidationState.scala new file mode 100644 index 000000000..3f6b29441 --- /dev/null +++ b/src/main/scala/scorex/core/validation/ValidationState.scala @@ -0,0 +1,150 @@ +package scorex.core.validation + +import scorex.core.consensus.ModifierSemanticValidity +import scorex.core.utils.ScorexEncoder +import scorex.core.validation.ValidationResult.{Invalid, Valid} +import scorex.util.ModifierId + +import scala.util.Try + +/** This is the place where all the validation DSL lives */ +final case class ValidationState[T](result: ValidationResult[T], strategy: ValidationStrategy) + (implicit e: ScorexEncoder) { + + /** Create the next validation state as the result of given `operation` */ + def pass[R](operation: => ValidationResult[R]): ValidationState[R] = + result match { + case Valid(_) => copy(result = operation) + case Invalid(_) if strategy.isFailFast || result == operation => asInstanceOf[ValidationState[R]] + case invalid@Invalid(_) => copy(result = invalid.accumulateErrors(operation)) + } + + /** Reverse condition: Validate the condition is `false` or else return the `error` given */ + def validateNot(condition: => Boolean)(error: => Invalid): ValidationState[T] = + validate(!condition)(error) + + /** Validate the first argument equals the second. This should not be used with `ModifierId` of type `Array[Byte]`. + * The `error` callback will be provided with detail on argument values for better reporting + */ + def validateEquals[A](given: => A, expected: => A)(error: String => Invalid): ValidationState[T] = + pass((given, expected) match { + case (a: Array[_], b: Array[_]) if a sameElements b => result + case (_: Array[_], _) => error(s"Given: $given, expected: $expected. Use validateEqualIds when comparing Arrays") + case _ if given == expected => result + case _ => error(s"Given: $given, expected $expected") + }) + + /** Validate the `id`s are equal. The `error` callback will be provided with detail on argument values + */ + def validateEqualIds(given: => ModifierId, expected: => ModifierId) + (error: String => Invalid): ValidationState[T] = + validate(given == expected) { + error(s"Given: ${e.encodeId(given)}, expected ${e.encodeId(expected)}") + } + + /** Wrap semantic validity to the validation state: if semantic validity was not Valid, then return the `error` given + */ + def validateSemantics(validity: => ModifierSemanticValidity)(error: => Invalid): ValidationState[T] = + validateNot(validity == ModifierSemanticValidity.Invalid)(error) + + /** Validate the condition is `true` or else return the `error` given + */ + def validate(condition: => Boolean)(error: => Invalid): ValidationState[T] = + pass(if (condition) result else error) + + /** Replace payload with the new one, discarding current payload value. This method catches throwables + */ + def payload[R](payload: => R): ValidationState[R] = + pass(result(payload)) + + /** Map payload if validation is successful + */ + def payloadMap[R](f: T => R): ValidationState[R] = + copy(result = result.map(f)) + + /** Validate the `condition` is `Success`. Otherwise the `error` callback will be provided with detail + * on a failure exception + */ + def validateNoFailure(condition: => Try[_])(error: Throwable => Invalid): ValidationState[T] = + pass(condition.fold(error, _ => result)) + + /** Validate the `block` doesn't throw an Exception. Otherwise the `error` callback will be provided with detail + * on the exception + */ + def validateNoThrow(block: => Any)(error: Throwable => Invalid): ValidationState[T] = + validateNoFailure(Try(block))(error) + + /** Validate `condition` against payload is `true` or else return the `error` + */ + def validateTry[A](tryValue: => Try[A], error: Throwable => Invalid) + (operation: (ValidationState[T], A) => ValidationResult[T]): ValidationState[T] = + pass(tryValue.fold(error, v => operation(this, v))) + + /** Validate condition against option value if it's not `None`. + * If given option is `None` then pass the previous result as success. + * Return `error` if option is `Some` amd condition is `false` + */ + def validateOrSkip[A](option: => Option[A]) + (operation: (ValidationState[T], A) => ValidationResult[T]): ValidationState[T] = + option + .map(value => pass(operation(this, value))) + .getOrElse(this) + + /** This could add some sugar when validating elements of a given collection + */ + def validateSeq[A](seq: Iterable[A]) + (operation: (ValidationState[T], A) => ValidationResult[T]): ValidationState[T] = + seq.foldLeft(this) { (state, elem) => + state.pass(operation(state, elem)) + } + + /** This is for nested validations that allow mixing fail-fast and accumulate-errors validation strategies + */ + def validate(operation: => ValidationResult[T]): ValidationState[T] = pass(operation) + + /** Shortcut `require`-like method for the simple validation with fatal error. + * If you need more convenient checks, use `validate` methods. + */ + def demand(condition: => Boolean, fatalError: => String): ValidationState[T] = + validate(condition)(ModifierValidator.fatal(fatalError)) + + /** Shortcut `require`-like method to validate the `id`s are equal. Otherwise returns fatal error + */ + def demandEqualIds(given: => ModifierId, expected: => ModifierId, fatalError: String): ValidationState[T] = + validateEqualIds(given, expected)(d => ModifierValidator.fatal(fatalError, d)) + + /** Shortcut `require`-like method to validate the arrays are equal. Otherwise returns fatal error + */ + def demandEqualArrays(given: => Array[Byte], expected: => Array[Byte], fatalError: String): ValidationState[T] = + validate(java.util.Arrays.equals(given, expected)) { + ModifierValidator.fatal(s"$fatalError. Given ${e.encode(given)} while expected ${e.encode(expected)}") + } + + /** Shortcut `require`-like method for the `Try` validation with fatal error + */ + def demandSuccess(condition: => Try[_], fatalError: => String): ValidationState[T] = + validateNoFailure(condition)(e => ModifierValidator.fatal(fatalError, e)) + + /** Shortcut `require`-like method to validate that `block` doesn't throw an Exception. + * Otherwise returns fatal error + */ + def demandNoThrow(block: => Any, fatalError: => String): ValidationState[T] = + validateNoThrow(block)(e => ModifierValidator.fatal(fatalError, e)) + + def demandTry[A](tryValue: => Try[A], fatalError: => String) + (operation: (ValidationState[T], A) => ValidationResult[T]): ValidationState[T] = + validateTry(tryValue, e => ModifierValidator.fatal(fatalError, e))(operation) + + /** Shortcut `require`-like method for the simple validation with recoverable error. + * If you need more convenient checks, use `validate` methods. + */ + def recoverable(condition: => Boolean, recoverableError: => String): ValidationState[T] = + validate(condition)(ModifierValidator.error(recoverableError)) + + /** Shortcut `require`-like method to validate the `id`s are equal. Otherwise returns recoverable error + */ + def recoverableEqualIds(given: => ModifierId, expected: => ModifierId, + recoverableError: String): ValidationState[T] = + validateEqualIds(given, expected)(d => ModifierValidator.error(recoverableError, d)) + +} diff --git a/src/main/scala/scorex/core/validation/ValidationStrategy.scala b/src/main/scala/scorex/core/validation/ValidationStrategy.scala new file mode 100644 index 000000000..252ce0cb5 --- /dev/null +++ b/src/main/scala/scorex/core/validation/ValidationStrategy.scala @@ -0,0 +1,14 @@ +package scorex.core.validation + +/** The strategy indicates are we going to perform fail-fast or error-accumulative validation. + * These two could be also mixed by nested validations. + */ +sealed abstract class ValidationStrategy(val isFailFast: Boolean) + +object ValidationStrategy { + + object AccumulateErrors extends ValidationStrategy(false) + + object FailFast extends ValidationStrategy(true) + +} diff --git a/src/test/scala/scorex/core/validation/ValidationSpec.scala b/src/test/scala/scorex/core/validation/ValidationSpec.scala index 8e989a9aa..21993ddb7 100644 --- a/src/test/scala/scorex/core/validation/ValidationSpec.scala +++ b/src/test/scala/scorex/core/validation/ValidationSpec.scala @@ -3,7 +3,7 @@ package scorex.core.validation import org.scalatest.{FlatSpec, Matchers} import scorex.core.bytesToId import scorex.core.consensus.ModifierSemanticValidity -import scorex.core.utils.ScorexEncoding +import scorex.core.utils.{ScorexEncoder, ScorexEncoding} import scorex.core.validation.ValidationResult._ import scala.util.{Failure, Try} @@ -21,14 +21,20 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { 10.toShort -> (_ => error("Deactivated check"), false) ) - val ffSettings: ValidationSettings = constructValidationSettings(true, map) - val aeSettings: ValidationSettings = constructValidationSettings(false, map) + val ffSettings: TaggedValidationRules = constructValidationSettings(failFast = true, map) + val aeSettings: TaggedValidationRules = constructValidationSettings(failFast = false, map) /** Start validation in Fail-Fast mode */ - def failFast: ValidationState[Unit] = ModifierValidator.apply(ffSettings) + def failFast: ValidationState[Unit] = ModifierValidator.failFast /** Start validation accumulating all the errors */ - def accumulateErrors: ValidationState[Unit] = ModifierValidator.apply(aeSettings) + def accumulateErrors: ValidationState[Unit] = ModifierValidator.accumulateErrors + + /** Start tagged validation in Fail-Fast mode */ + def failFastTagged: TaggedValidationState[Unit] = ModifierValidator.failFastTagged(ffSettings) + + /** Start tagged validation accumulating all the errors */ + def accumulateErrorsTagged: TaggedValidationState[Unit] = ModifierValidator.accumulateErrorsTagged(aeSettings) /** report recoverable modifier error that could be fixed by later retries */ def error(errorMessage: String): Invalid = ModifierValidator.error(errorMessage) @@ -47,39 +53,315 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { "ModifierValidation" should "be able to succeed when failing fast" in { val result = failFast - .validate(1, condition = true) + .validate(condition = true)(fatal("")) .result result.isValid shouldBe true result shouldBe a[Valid[_]] } - it should "skip deactivated checks" in { + it should "fail for missed checks" in { val result = failFast - .validate(10, condition = false) + .validate(condition = false)(fatal("")) + .result + result shouldBe an[Invalid] + result.errors should have size 1 + } + + it should "be able to succeed when accumulating errors" in { + val result = accumulateErrors + .validate(condition = true)(fatal("")) .result + result.isValid shouldBe true result shouldBe a[Valid[_]] } - it should "fail for missed checks" in { + it should "support fail fast approach" in { val result = failFast - .validate(2, condition = false) + .validate(condition = false)(fatal("")) + .validate(condition = false)(fatal("")) .result + + result.isValid shouldBe false result shouldBe an[Invalid] result.errors should have size 1 } - it should "be able to succeed when accumulating errors" in { + it should "support error accumulation" in { + val result = accumulateErrors + .validate(condition = false)(error("")) + .validate(condition = false)(error("")) + .validate(condition = true)(fatal("")) + .result + + result.isValid shouldBe false + result shouldBe an[Invalid] + result.errors should have size 2 + } + + it should "be lazy" in { + var i = 0 + val result = failFast + .validate(condition = false)(error("")) + .validate(s"${i = 1}" == s"${()}")(error("")) + .result + result.isValid shouldBe false + result shouldBe an[Invalid] + i shouldBe 0 + } + + it should "support recoverable errors" in { + val result = accumulateErrors + .validate(condition = false)(error("")) + .result + + result shouldBe an[Invalid] + result.errors should have size 1 + all(result.errors) shouldBe a[RecoverableModifierError] + result.asInstanceOf[Invalid].isFatal shouldBe false + } + + it should "support fatal errors" in { + val result = accumulateErrors + .validate(condition = false)(fatal("")) + .validate(condition = false)(error("")) + .result + + result.isValid shouldBe false + result shouldBe an[Invalid] + result.errors should have size 2 + exactly(1, result.errors) shouldBe a[MalformedModifierError] + result.asInstanceOf[Invalid].isFatal shouldBe true + } + + it should "support accumulating nesting" in { + val result = accumulateErrors + .validate(condition = false)(fatal("")) + .validate { + failFast + .validate(condition = true)(fatal("")) + .validate(condition = false)(fatal("")) + .validate(condition = false)(fatal("")) + .result + } + .result + + result.isValid shouldBe false + result.errors should have size 2 + } + + it should "support fail fast nesting" in { + val result = failFast + .validate(condition = true)(fatal("")) + .validate { + accumulateErrors + .validate(condition = true)(fatal("")) + .validate(condition = false)(fatal("")) + .validate(condition = false)(fatal("")) + .result + } + .result + + result.isValid shouldBe false + result.errors should have size 2 + } + + it should "fail fast while nesting" in { + val errMsg = "Special for test" + val result = failFast + .validate(condition = false)(fatal(errMsg)) + .validate { + accumulateErrors + .validate(condition = false)(fatal("")) + .validate(condition = false)(fatal("")) + .result + } + .result + + result.isValid shouldBe false + result.errors.map(_.message) shouldBe Seq(errMsg) + } + + it should "correctly check byte array equality" in { + val len = 32 + val byte1 = 1.toByte + val byte2 = 2.toByte + val id = bytesToId(Array.fill(len)(byte1)) + val result = accumulateErrors + .validateEqualIds(id, bytesToId(Array.fill(len)(byte2)))(_ => fatal("1")) + .validateEqualIds(id, bytesToId(Array.fill(len)(byte1)))(_ => fatal("")) + .validateEqualIds(id, bytesToId(Array.fill(len + 1)(byte1)))(_ => fatal("3")) + .result + + result.isValid shouldBe false + result shouldBe an[Invalid] + result.errors should have size 2 + result.errors.exists(_.message.startsWith("1")) shouldBe true + result.errors.exists(_.message.startsWith("3")) shouldBe true + } + + it should "correctly check equality" in { + val result = accumulateErrors + .validateEquals("123", "12" + "3")(_ => fatal("")) + .validateEquals("123", "122")(_ => fatal("2")) + .result + + result.isValid shouldBe false + result shouldBe an[Invalid] + result.errors should have size 1 + result.errors.exists(_.message.startsWith("2")) shouldBe true + } + + it should "correctly check semantic validity" in { + val result = accumulateErrors + .validateSemantics(ModifierSemanticValidity.Invalid)(fatal("1")) + .validateSemantics(ModifierSemanticValidity.Absent)(fatal("")) + .validateSemantics(ModifierSemanticValidity.Unknown)(fatal("")) + .validateSemantics(ModifierSemanticValidity.Valid)(fatal("")) + .result + + result.isValid shouldBe false + result shouldBe an[Invalid] + result.errors should have size 1 + result.errors.map(_.message) should contain only "1" + } + + it should "support `not` condition" in { + val result = accumulateErrors + .validateNot(condition = false)(fatal("")) + .validateNot(condition = true)(fatal("3")) + .result + + result.isValid shouldBe false + result shouldBe an[Invalid] + result.errors should have size 1 + result.errors.map(_.message) should contain only "3" + } + + it should "carry payload" in { + val data = 1L + val result = accumulateErrors + .payload(data) + .validate(condition = true)(fatal("")) + .result + + result.isValid shouldBe true + result shouldBe Valid(data) + result.payload shouldBe Some(data) + } + + it should "replace payload" in { + val initialData = "Hi there" + val data = 1L val result = accumulateErrors + .payload(initialData) + .validate(condition = true)(fatal("")) + .result(data) + + result.isValid shouldBe true + result shouldBe Valid(data) + result.payload shouldBe Some(data) + } + + it should "fill payload from try" in { + val data = "Hi there" + val result = accumulateErrors + .payload[String]("Random string") + .validateTry(Try(data), _ => fatal(""))((_, d) => Valid(d)) + .result + + result.isValid shouldBe true + result shouldBe Valid(data) + result.payload shouldBe Some(data) + } + + it should "return error when filling payload from failure" in { + val errMsg = "error msg" + val result = accumulateErrors + .payload("Random string") + .validateTry(Failure(new Error("Failed")), _ => fatal(errMsg))((_, d) => Valid(d)) + .result + + result.isValid shouldBe false + result shouldBe an[Invalid] + result.errors.exists(_.message.startsWith(errMsg)) shouldBe true + } + + it should "switch failure payload type" in { + val errMsg = "Failure 1" + val stringFailure: ValidationState[String] = failFast + .payload("Hi there") + .pass(fatal(errMsg)) + + val unitFailure: ValidationState[Unit] = stringFailure + .pass(success) + .pass(fatal(errMsg + "23")) + + val result = unitFailure.result + result.isValid shouldBe false + result shouldBe an[Invalid] + result.payload shouldBe empty + result.errors should have size 1 + result.errors.map(_.message) should contain only errMsg + } + + it should "validate optional for some" in { + val expression = "123" + val errMsg = "Error msg" + val result = accumulateErrors + .validateOrSkip[String](Some(expression))((_, _) => fatal(errMsg)) + .result + + result.isValid shouldBe false + result.errors.map(_.message) should contain only errMsg + } + + it should "skip optional validation for none" in { + val result = accumulateErrors + .validateOrSkip[String](None)((_, _) => fatal("")) + .result + + result.isValid shouldBe true + result shouldBe a[Valid[_]] + } + + // Tagged mode + + "ModifierValidation" should "be able to succeed when failing fast (tagged mode)" in { + val result = failFastTagged .validate(1, condition = true) .result + result.isValid shouldBe true + result shouldBe a[Valid[_]] + } + it should "skip deactivated checks (tagged mode)" in { + val result = failFastTagged + .validate(10, condition = false) + .result result.isValid shouldBe true result shouldBe a[Valid[_]] } - it should "support fail fast approach" in { - val result = failFast + it should "fail for missed checks (tagged mode)" in { + val result = failFastTagged + .validate(2, condition = false) + .result + result shouldBe an[Invalid] + result.errors should have size 1 + } + + it should "be able to succeed when accumulating errors (tagged mode)" in { + val result = accumulateErrorsTagged + .validate(1, condition = true) + .result + + result.isValid shouldBe true + result shouldBe a[Valid[_]] + } + + it should "support fail fast approach (tagged mode)" in { + val result = failFastTagged .validate(1, condition = false) .validate(3, condition = false) .result @@ -89,8 +371,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors should have size 1 } - it should "support error accumulation" in { - val result = accumulateErrors + it should "support error accumulation (tagged mode)" in { + val result = accumulateErrorsTagged .validate(1, condition = false) .validate(3, condition = false) .validate(4, condition = true) @@ -101,9 +383,9 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors should have size 2 } - it should "be lazy" in { + it should "be lazy (tagged mode)" in { var i = 0 - val result = failFast + val result = failFastTagged .validate(1, condition = false) .validate(3, s"${i = 1}" == s"${()}") .result @@ -112,8 +394,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { i shouldBe 0 } - it should "support recoverable errors" in { - val result = accumulateErrors + it should "support recoverable errors (tagged mode)" in { + val result = accumulateErrorsTagged .validate(5, condition = false) .result @@ -123,8 +405,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.asInstanceOf[Invalid].isFatal shouldBe false } - it should "support fatal errors" in { - val result = accumulateErrors + it should "support fatal errors (tagged mode)" in { + val result = accumulateErrorsTagged .validate(5, condition = false) .validate(1, condition = false) .result @@ -136,11 +418,11 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.asInstanceOf[Invalid].isFatal shouldBe true } - it should "support accumulating nesting" in { - val result = accumulateErrors + it should "support accumulating nesting (tagged mode)" in { + val result = accumulateErrorsTagged .validate(1, condition = false) .validate { - failFast + failFastTagged .validate(3, condition = true) .validate(4, condition = false) .validate(5, condition = false) @@ -152,11 +434,11 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors should have size 2 } - it should "support fail fast nesting" in { - val result = failFast + it should "support fail fast nesting (tagged mode)" in { + val result = failFastTagged .validate(1, condition = true) .validate { - accumulateErrors + accumulateErrorsTagged .validate(2, condition = true) .validate(3, condition = false) .validate(4, condition = false) @@ -168,11 +450,11 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors should have size 2 } - it should "fail fast while nesting" in { - val result = failFast + it should "fail fast while nesting (tagged mode)" in { + val result = failFastTagged .validate(3, condition = false) .validate { - accumulateErrors + accumulateErrorsTagged .validate(4, condition = false) .validate(5, condition = false) .result @@ -183,12 +465,12 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.map(_.message) shouldBe Seq(errMsg3) } - it should "correctly check byte array equality" in { + it should "correctly check byte array equality (tagged mode)" in { val len = 32 val byte1 = 1.toByte val byte2 = 2.toByte val id = bytesToId(Array.fill(len)(byte1)) - val result = accumulateErrors + val result = accumulateErrorsTagged .validateEqualIds(1, id, bytesToId(Array.fill(len)(byte2))) .validateEqualIds(4, id, bytesToId(Array.fill(len)(byte1))) .validateEqualIds(3, id, bytesToId(Array.fill(len + 1)(byte1))) @@ -201,8 +483,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.exists(_.message.startsWith(errMsg3)) shouldBe true } - it should "correctly check equality" in { - val result = accumulateErrors + it should "correctly check equality (tagged mode)" in { + val result = accumulateErrorsTagged .validateEquals(1, "123", "12" + "3") .validateEquals(3, "123", "122") .result @@ -213,8 +495,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.exists(_.message.startsWith(errMsg3)) shouldBe true } - it should "correctly check semantic validity" in { - val result = accumulateErrors + it should "correctly check semantic validity (tagged mode)" in { + val result = accumulateErrorsTagged .validateSemantics(1, ModifierSemanticValidity.Invalid) .validateSemantics(2, ModifierSemanticValidity.Absent) .validateSemantics(3, ModifierSemanticValidity.Unknown) @@ -227,8 +509,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.map(_.message) should contain only errMsg1 } - it should "support `not` condition" in { - val result = accumulateErrors + it should "support `not` condition (tagged mode)" in { + val result = accumulateErrorsTagged .validateNot(1, condition = false) .validateNot(3, condition = true) .result @@ -239,9 +521,9 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.map(_.message) should contain only errMsg3 } - it should "carry payload" in { + it should "carry payload (tagged mode)" in { val data = 1L - val result = accumulateErrors + val result = accumulateErrorsTagged .payload(data) .validate(1, condition = true) .result @@ -251,10 +533,10 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.payload shouldBe Some(data) } - it should "replace payload" in { + it should "replace payload (tagged mode)" in { val initialData = "Hi there" val data = 1L - val result = accumulateErrors + val result = accumulateErrorsTagged .payload(initialData) .validate(1, condition = true) .result(data) @@ -264,9 +546,9 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.payload shouldBe Some(data) } - it should "fill payload from try" in { + it should "fill payload from try (tagged mode)" in { val data = "Hi there" - val result = accumulateErrors + val result = accumulateErrorsTagged .payload[String]("Random string") .validateTryFlatten(1, _ => Try(data), trueCondition) .result @@ -276,8 +558,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.payload shouldBe Some(data) } - it should "return error when filling payload from failure" in { - val result = accumulateErrors + it should "return error when filling payload from failure (tagged mode)" in { + val result = accumulateErrorsTagged .payload("Random string") .validateTryFlatten(1, _ => Failure(new Error("Failed")), trueCondition) .result @@ -287,11 +569,11 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.exists(_.message.startsWith(errMsg1)) shouldBe true } - it should "aggregate payload from try" in { + it should "aggregate payload from try (tagged mode)" in { val data1 = 100L val data2 = 50L val expected = data1 / data2 - val result = accumulateErrors + val result = accumulateErrorsTagged .payload[Long](data1) .validateTryFlatten(1, v => Try(v / data2), _ => true) .result @@ -301,8 +583,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.payload shouldBe Some(expected) } - it should "return error when aggregating payload from failure" in { - val result = accumulateErrors + it should "return error when aggregating payload from failure (tagged mode)" in { + val result = accumulateErrorsTagged .payload(1) .validateTryFlatten(1, _ => Failure(new Error("Failed")): Try[Int], _ => true) .result @@ -312,13 +594,13 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.exists(_.message.startsWith(errMsg1)) shouldBe true } - it should "switch failure payload type" in { + it should "switch failure payload type (tagged mode)" in { val errMsg = "Failure 1" - val stringFailure: ValidationState[String] = failFast + val stringFailure: TaggedValidationState[String] = failFastTagged .payload("Hi there") .pass(fatal(errMsg)) - val unitFailure: ValidationState[Unit] = stringFailure + val unitFailure: TaggedValidationState[Unit] = stringFailure .pass(success) .pass(fatal(errMsg + "23")) @@ -330,9 +612,9 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.map(_.message) should contain only errMsg } - it should "validate optional for some" in { + it should "validate optional for some (tagged mode)" in { val expression = "123" - val result = accumulateErrors + val result = accumulateErrorsTagged .validateOrSkipFlatten[String](1, Some(expression), falseCondition) .result @@ -340,8 +622,8 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result.errors.map(_.message) should contain only errMsg1 } - it should "skip optional validation for none" in { - val result = accumulateErrors + it should "skip optional validation for none (tagged mode)" in { + val result = accumulateErrorsTagged .validateOrSkipFlatten[String](1, None, falseCondition) .result @@ -349,13 +631,10 @@ class ValidationSpec extends FlatSpec with Matchers with ScorexEncoding { result shouldBe a[Valid[_]] } + def constructValidationSettings(failFast: Boolean, map: Map[Short, (String => Invalid, Boolean)] + ): TaggedValidationRules = { - - def constructValidationSettings(failFast: Boolean, map: Map[Short, (String => Invalid, Boolean)]): ValidationSettings = { - - new ValidationSettings { - - override val isFailFast: Boolean = failFast + new TaggedValidationRules { override def getError(id: Short, details: String): Invalid = { map.get(id).map(_._1(details)).getOrElse(ModifierValidator.fatal("Unknown message"))