Skip to content
This repository has been archived by the owner on Apr 13, 2022. It is now read-only.

Validation framework extention #349

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
158 changes: 16 additions & 142 deletions src/main/scala/scorex/core/validation/ModifierValidator.scala
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 =
Expand All @@ -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(())
Expand All @@ -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)

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
129 changes: 129 additions & 0 deletions src/main/scala/scorex/core/validation/TaggedValidationState.scala
Original file line number Diff line number Diff line change
@@ -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)

}
Original file line number Diff line number Diff line change
Expand Up @@ -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

}

Loading