diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..a9d86fd --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Scala Steward: Reformat with scalafmt 3.8.6 +818bb309d47dc5586750f30ba9bd8999feb02471 diff --git a/.scalafmt.conf b/.scalafmt.conf index 7be7c34..f426865 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.5" +version = "3.8.6" runner.dialect = scala3 preset = IntelliJ maxColumn = 120 diff --git a/build.sbt b/build.sbt index 3de7e19..e785978 100644 --- a/build.sbt +++ b/build.sbt @@ -24,13 +24,7 @@ lazy val vigilo = .withoutSuffixFor(JVMPlatform) .in(file("vigilo")) .settings(sharedSettings *) - .settings( - name := "vigilo", - organization := "io.vigilo", - version := "0.0.1" - ) - .jvmSettings( - libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test" - ) + .settings(name := "vigilo", organization := "io.vigilo", version := "0.0.1") + .jvmSettings(libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test") // configure Scala-Native settings .nativeSettings( /* ... */ ) // defined in sbt-scala-native diff --git a/project/plugins.sbt b/project/plugins.sbt index d68239a..02d772c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.10") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.10") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.2") diff --git a/vigilo/jvm/src/test/scala/io/vigilo/ValidatorTest.scala b/vigilo/jvm/src/test/scala/io/vigilo/ValidatorTest.scala index 309bf22..d235e2f 100644 --- a/vigilo/jvm/src/test/scala/io/vigilo/ValidatorTest.scala +++ b/vigilo/jvm/src/test/scala/io/vigilo/ValidatorTest.scala @@ -5,26 +5,26 @@ import org.scalatest.flatspec.* import org.scalatest.matchers.should.Matchers.* val messages = Map[String, String]( - "validation.number.min" -> "Value for %s must be bigger or equal %d", - "validation.number.max" -> "Value for %s must be less or equal %d", - "validation.number.size" -> "Value for %s must be between %d and %d", - "validation.required" -> "Value for %s is required", + "validation.number.min" -> "Value for %s must be bigger or equal %d", + "validation.number.max" -> "Value for %s must be less or equal %d", + "validation.number.size" -> "Value for %s must be between %d and %d", + "validation.required" -> "Value for %s is required", "validation.string.blank" -> "Value for %s can't be blank", - "validation.string.min" -> "Size of text for %s must be bigger or equal %d", - "validation.string.max" -> "Size of text for %s must be less or equal %d", - "validation.string.size" -> "Size of text for %s must be between %d and %d", - "validation.email" -> "Value of %s must be a valid e-mail", - "validation.regex" -> "Value of %s doest not match with expected pattern", - "validation.rel" -> "Value of relation %s is required", - "validation.list.min" -> "List size of %s must be bigger os equal %s", - "validation.list.max" -> "List size of %s must be less os equal %s", - "validation.list.size" -> "List size of %s must be between %d and %d", - "validation.values" -> "Value of %s must be one of %s", - "validation.choices.min" -> "Value of %s must be %d between the options %s", - "validation.choices.max" -> "Value of %s must be at most %d selections between then options %s", + "validation.string.min" -> "Size of text for %s must be bigger or equal %d", + "validation.string.max" -> "Size of text for %s must be less or equal %d", + "validation.string.size" -> "Size of text for %s must be between %d and %d", + "validation.email" -> "Value of %s must be a valid e-mail", + "validation.regex" -> "Value of %s doest not match with expected pattern", + "validation.rel" -> "Value of relation %s is required", + "validation.list.min" -> "List size of %s must be bigger os equal %s", + "validation.list.max" -> "List size of %s must be less os equal %s", + "validation.list.size" -> "List size of %s must be between %d and %d", + "validation.values" -> "Value of %s must be one of %s", + "validation.choices.min" -> "Value of %s must be %d between the options %s", + "validation.choices.max" -> "Value of %s must be at most %d selections between then options %s", "validation.choices.size" -> "Value of %s must be between %d and %d selections between then options %s", - "validation.date.min" -> "Date of %s must be before of %s", - "validation.date.max" -> "Date of %s must be after of %s", + "validation.date.min" -> "Date of %s must be before of %s", + "validation.date.max" -> "Date of %s must be after of %s" ) given ValidatorMessages: @@ -34,68 +34,58 @@ given ValidatorMessages: case class ValueRelation(id: Int = 0, name: String = "", valid: Boolean = true) extends Relation // Test case classes with validation annotations -case class BasicModel( - @required id: Int = 0, - @required name: String = "", - @email email: String = "" - ) derives Validator +case class BasicModel(@required id: Int = 0, @required name: String = "", @email email: String = "") derives Validator case class StringValidationModel( - @string(blank = false) nonBlankString: String = "", - @string(empty = false) nonEmptyString: String = "", - @string(min = 5) minString: String = "", - @string(max = 10) maxString: String = "", - @string(min = 3, max = 8) rangeString: String = "" - ) derives Validator + @string(blank = false) nonBlankString: String = "", + @string(empty = false) nonEmptyString: String = "", + @string(min = 5) minString: String = "", + @string(max = 10) maxString: String = "", + @string(min = 3, max = 8) rangeString: String = "" +) derives Validator case class NumberValidationModel( - @number(min = 10) minValue: Int = 0, - @number(max = 100) maxValue: Int = 0, - @number(min = 20, max = 50) rangeValue: Int = 0 - ) derives Validator + @number(min = 10) minValue: Int = 0, + @number(max = 100) maxValue: Int = 0, + @number(min = 20, max = 50) rangeValue: Int = 0 +) derives Validator -case class RegexpValidationModel( - @regexp(pattern = "\\d{3}-\\d{3}-\\d{4}") phoneNumber: String = "" - ) derives Validator +case class RegexpValidationModel(@regexp(pattern = "\\d{3}-\\d{3}-\\d{4}") phoneNumber: String = "") derives Validator -case class RelationValidationModel( - @required @rel relation: Option[ValueRelation] = None - ) derives Validator +case class RelationValidationModel(@required @rel relation: Option[ValueRelation] = None) derives Validator case class ChoiceValidationModel( - @choice(group = "group1", min = 2, max = 2) option1: Option[ValueRelation] = None, - @choice(group = "group1", min = 2, max = 2) option2: Option[ValueRelation] = None, - @choice(group = "group1", min = 2, max = 2) option3: Option[ValueRelation] = None, - @choice(group = "group2", max = 2) option4: Option[ValueRelation] = None, - @choice(group = "group2", max = 2) option5: Option[ValueRelation] = None - ) derives Validator + @choice(group = "group1", min = 2, max = 2) option1: Option[ValueRelation] = None, + @choice(group = "group1", min = 2, max = 2) option2: Option[ValueRelation] = None, + @choice(group = "group1", min = 2, max = 2) option3: Option[ValueRelation] = None, + @choice(group = "group2", max = 2) option4: Option[ValueRelation] = None, + @choice(group = "group2", max = 2) option5: Option[ValueRelation] = None +) derives Validator case class OptionsValidationModel( - @options(values = Seq("A", "B", "C")) stringOption: String = "", - @options(values = Seq(1, 2, 3)) intOption: Int = 0 - ) derives Validator + @options(values = Seq("A", "B", "C")) stringOption: String = "", + @options(values = Seq(1, 2, 3)) intOption: Int = 0 +) derives Validator case class ListValidationModel( - @list(min = 2) minList: List[String] = List(), - @list(max = 5) maxList: List[String] = List(), - @list(min = 1, max = 3) rangeList: List[String] = List() - ) derives Validator + @list(min = 2) minList: List[String] = List(), + @list(max = 5) maxList: List[String] = List(), + @list(min = 1, max = 3) rangeList: List[String] = List() +) derives Validator case class CombinedValidationModel( - @required @string(min = 5, max = 20) username: String = "", - @required @email email: String = "", - @required @number(min = 18, max = 120) age: Int = 0, - @required @rel relation: Option[ValueRelation] = None, - @required @list(min = 1) @options(values = Seq("admin", "user", "guest")) roles: List[String] = List() - ) derives Validator - - + @required @string(min = 5, max = 20) username: String = "", + @required @email email: String = "", + @required @number(min = 18, max = 120) age: Int = 0, + @required @rel relation: Option[ValueRelation] = None, + @required @list(min = 1) @options(values = Seq("admin", "user", "guest")) roles: List[String] = List() +) derives Validator // sbt testOnly *RouterTest class ValidationTests extends AnyFlatSpec { "Required validation" should "fail for null values" in { - val model = BasicModel(id = 1, name = null, email = "valid@email.com") + val model = BasicModel(id = 1, name = null, email = "valid@email.com") val result = model.validate result.isValid shouldBe false @@ -103,9 +93,8 @@ class ValidationTests extends AnyFlatSpec { result.messages("name").head should include("required") } - it should "fail for empty strings" in { - val model = BasicModel(id = 1, name = "", email = "valid@email.com") + val model = BasicModel(id = 1, name = "", email = "valid@email.com") val result = model.validate result.isValid shouldBe false @@ -114,7 +103,7 @@ class ValidationTests extends AnyFlatSpec { } it should "fail for zero values in numbers" in { - val model = BasicModel(id = 0, name = "Test", email = "valid@email.com") + val model = BasicModel(id = 0, name = "Test", email = "valid@email.com") val result = model.validate result.isValid shouldBe false @@ -122,18 +111,16 @@ class ValidationTests extends AnyFlatSpec { result.messages("id").head should include("required") } - it should "pass for valid values" in { - val model = BasicModel(id = 1, name = "Test", email = "valid@email.com") + val model = BasicModel(id = 1, name = "Test", email = "valid@email.com") val result = model.validate result.isValid shouldBe true } - // Test cases for string validation "String validation" should "fail for blank strings when blank=false" in { - val model = StringValidationModel( + val model = StringValidationModel( nonBlankString = " ", nonEmptyString = "test", minString = "validlength", @@ -148,7 +135,7 @@ class ValidationTests extends AnyFlatSpec { } it should "fail for empty strings when empty=false" in { - val model = StringValidationModel( + val model = StringValidationModel( nonBlankString = "test", nonEmptyString = "", minString = "validlength", @@ -161,9 +148,8 @@ class ValidationTests extends AnyFlatSpec { result.messages should contain key "nonEmptyString" } - it should "fail for strings shorter than min length" in { - val model = StringValidationModel( + val model = StringValidationModel( nonBlankString = "test", nonEmptyString = "test", minString = "shor", @@ -177,9 +163,8 @@ class ValidationTests extends AnyFlatSpec { result.messages("minString").head should include("must be bigger or equal 5") } - it should "fail for strings longer than max length" in { - val model = StringValidationModel( + val model = StringValidationModel( nonBlankString = "test", nonEmptyString = "test", minString = "validlength", @@ -193,10 +178,9 @@ class ValidationTests extends AnyFlatSpec { result.messages("maxString").head should include("must be less or equal 10") } - it should "fail for strings outside of range length" in { // Too short - val modelShort = StringValidationModel( + val modelShort = StringValidationModel( nonBlankString = "test", nonEmptyString = "test", minString = "validlength", @@ -210,7 +194,7 @@ class ValidationTests extends AnyFlatSpec { resultShort.messages("rangeString").head should include("must be between 3 and 8") // Too long - val modelLong = StringValidationModel( + val modelLong = StringValidationModel( nonBlankString = "test", nonEmptyString = "test", minString = "validlength", @@ -224,9 +208,8 @@ class ValidationTests extends AnyFlatSpec { resultLong.messages("rangeString").head should include("must be between 3 and 8") } - it should "pass for valid strings" in { - val model = StringValidationModel( + val model = StringValidationModel( nonBlankString = "test", nonEmptyString = "test", minString = "validlength", @@ -240,14 +223,9 @@ class ValidationTests extends AnyFlatSpec { result.isValid shouldBe true } - // Test cases for number validation "Number validation" should "fail for values below minimum" in { - val model = NumberValidationModel( - minValue = 5, - maxValue = 50, - rangeValue = 30 - ) + val model = NumberValidationModel(minValue = 5, maxValue = 50, rangeValue = 30) val result = model.validate result.isValid shouldBe false @@ -255,13 +233,8 @@ class ValidationTests extends AnyFlatSpec { result.messages("minValue").head should include("must be bigger or equal 10") } - it should "fail for values above maximum" in { - val model = NumberValidationModel( - minValue = 15, - maxValue = 150, - rangeValue = 30 - ) + val model = NumberValidationModel(minValue = 15, maxValue = 150, rangeValue = 30) val result = model.validate result.isValid shouldBe false @@ -269,14 +242,9 @@ class ValidationTests extends AnyFlatSpec { result.messages("maxValue").head should include("must be less or equal 100") } - it should "fail for values outside range" in { // Below minimum - val modelBelow = NumberValidationModel( - minValue = 15, - maxValue = 50, - rangeValue = 15 - ) + val modelBelow = NumberValidationModel(minValue = 15, maxValue = 50, rangeValue = 15) val resultBelow = modelBelow.validate resultBelow.isValid shouldBe false @@ -284,11 +252,7 @@ class ValidationTests extends AnyFlatSpec { resultBelow.messages("rangeValue").head should include("must be between 20 and 50") // Above maximum - val modelAbove = NumberValidationModel( - minValue = 15, - maxValue = 50, - rangeValue = 60 - ) + val modelAbove = NumberValidationModel(minValue = 15, maxValue = 50, rangeValue = 60) val resultAbove = modelAbove.validate resultAbove.isValid shouldBe false @@ -296,23 +260,17 @@ class ValidationTests extends AnyFlatSpec { resultAbove.messages("rangeValue").head should include("must be between 20 and 50") } - it should "pass for valid numbers" in { - val model = NumberValidationModel( - minValue = 15, - maxValue = 50, - rangeValue = 30 - ) + val model = NumberValidationModel(minValue = 15, maxValue = 50, rangeValue = 30) val result = model.validate result.messages shouldBe Map.empty result.isValid shouldBe true } - // Test cases for regexp validation "Regexp validation" should "fail for strings not matching pattern" in { - val model = RegexpValidationModel(phoneNumber = "invalid") + val model = RegexpValidationModel(phoneNumber = "invalid") val result = model.validate result.isValid shouldBe false @@ -320,18 +278,16 @@ class ValidationTests extends AnyFlatSpec { result.messages("phoneNumber").head should include("match with expected pattern") } - it should "pass for strings matching pattern" in { - val model = RegexpValidationModel(phoneNumber = "123-456-7890") + val model = RegexpValidationModel(phoneNumber = "123-456-7890") val result = model.validate result.isValid shouldBe true } - // Test cases for relation validation "Relation validation" should "fail for None values" in { - val model = RelationValidationModel(relation = None) + val model = RelationValidationModel(relation = None) val result = model.validate result.isValid shouldBe false @@ -339,9 +295,8 @@ class ValidationTests extends AnyFlatSpec { result.messages("relation").head should include("required") } - it should "fail for invalid relations" in { - val model = RelationValidationModel(relation = Some(ValueRelation(1, "test", valid = false))) + val model = RelationValidationModel(relation = Some(ValueRelation(1, "test", valid = false))) val result = model.validate result.isValid shouldBe false @@ -349,43 +304,40 @@ class ValidationTests extends AnyFlatSpec { result.messages("relation").head should include("relation") } - it should "pass for valid relations" in { - val model = RelationValidationModel(relation = Some(ValueRelation(1, "test", valid = true))) + val model = RelationValidationModel(relation = Some(ValueRelation(1, "test", valid = true))) val result = model.validate result.isValid shouldBe true } - // Test cases for choice validation "Choice validation" should "fail when minimum choices not met" in { val relation = Some(ValueRelation(1, "test", valid = true)) - val model = ChoiceValidationModel( + val model = ChoiceValidationModel( option1 = None, option2 = None, option3 = relation, // This needs another selection in the group since min=2 option4 = None, option5 = None ) - val result = model.validate + val result = model.validate result.isValid shouldBe false result.messages should contain key "option3" result.messages("option3").head should include("must be between 2 and 2 selections between then options") } - it should "fail when maximum choices exceeded" in { val relation = Some(ValueRelation(1, "test", valid = true)) - val model = ChoiceValidationModel( + val model = ChoiceValidationModel( option1 = relation, option2 = relation, // Exceeds max=1 by default for group1 option3 = None, option4 = None, option5 = None ) - val result = model.validate + val result = model.validate result.isValid shouldBe false // There would be a message about maximum choices for group1 @@ -393,39 +345,33 @@ class ValidationTests extends AnyFlatSpec { it should "fail when both option4 and option5 are selected exceeding max=2" in { val relation = Some(ValueRelation(1, "test", valid = true)) - val model = ChoiceValidationModel( + val model = ChoiceValidationModel( option1 = relation, option2 = relation, option3 = None, option4 = relation, option5 = relation ) - val result = model.validate + val result = model.validate result.messages shouldBe Map.empty result.isValid shouldBe true // This should pass as max=2 for group2 } - it should "pass for valid choice selections" in { val relation = Some(ValueRelation(1, "test", valid = true)) - val model = ChoiceValidationModel( - option1 = relation, - option2 = relation, - option3 = None, - option4 = relation, - option5 = None - ) - val result = model.validate + val model = + ChoiceValidationModel(option1 = relation, option2 = relation, option3 = None, option4 = relation, option5 = None) + val result = model.validate result.isValid shouldBe true } // Test cases for options validation "Options validation" should "fail for values not in the options list" in { - val model = OptionsValidationModel( + val model = OptionsValidationModel( stringOption = "D", // Not in ["A", "B", "C"] - intOption = 5 // Not in [1, 2, 3] + intOption = 5 // Not in [1, 2, 3] ) val result = model.validate @@ -436,24 +382,19 @@ class ValidationTests extends AnyFlatSpec { result.messages("intOption").head should include("must be one of") } - it should "pass for values in the options list" in { - val model = OptionsValidationModel( - stringOption = "A", - intOption = 2 - ) + val model = OptionsValidationModel(stringOption = "A", intOption = 2) val result = model.validate result.messages shouldBe Map.empty result.isValid shouldBe true } - // Test cases for list validation "List validation" should "fail for lists with fewer items than minimum" in { - val model = ListValidationModel( + val model = ListValidationModel( minList = List("one"), // Minimum is 2 maxList = List("one", "two"), - rangeList = List() // Empty but minimum is 1 + rangeList = List() // Empty but minimum is 1 ) val result = model.validate @@ -464,12 +405,11 @@ class ValidationTests extends AnyFlatSpec { result.messages("rangeList").head should include("must be between 1 and 3") } - it should "fail for lists with more items than maximum" in { - val model = ListValidationModel( + val model = ListValidationModel( minList = List("one", "two"), maxList = List("one", "two", "three", "four", "five", "six"), // Max is 5 - rangeList = List("one", "two", "three", "four") // Max is 3 + rangeList = List("one", "two", "three", "four") // Max is 3 ) val result = model.validate @@ -480,9 +420,8 @@ class ValidationTests extends AnyFlatSpec { result.messages("rangeList").head should include("must be between 1 and 3") } - it should "pass for lists with valid sizes" in { - val model = ListValidationModel( + val model = ListValidationModel( minList = List("one", "two"), maxList = List("one", "two", "three", "four", "five"), rangeList = List("one", "two", "three") @@ -496,13 +435,13 @@ class ValidationTests extends AnyFlatSpec { "Combined validations" should "check all validations" in { // Create a model with multiple validation failures val invalidModel = CombinedValidationModel( - username = "user", // Too short (min=5) - email = "invalid-email", // Invalid email format - age = 15, // Below min age (18) - relation = None, // Required relation is missing + username = "user", // Too short (min=5) + email = "invalid-email", // Invalid email format + age = 15, // Below min age (18) + relation = None, // Required relation is missing roles = List("superadmin") // Invalid role (not in options) ) - val result = invalidModel.validate + val result = invalidModel.validate result.isValid shouldBe false result.messages should contain key "username" @@ -512,7 +451,7 @@ class ValidationTests extends AnyFlatSpec { result.messages should contain key "roles" // Create a valid model - val validModel = CombinedValidationModel( + val validModel = CombinedValidationModel( username = "validuser", email = "valid@email.com", age = 25, @@ -525,7 +464,3 @@ class ValidationTests extends AnyFlatSpec { } } - - - - diff --git a/vigilo/shared/src/main/scala/io/vigilo/core/annotations.scala b/vigilo/shared/src/main/scala/io/vigilo/core/annotations.scala index e5b4197..ffdc956 100644 --- a/vigilo/shared/src/main/scala/io/vigilo/core/annotations.scala +++ b/vigilo/shared/src/main/scala/io/vigilo/core/annotations.scala @@ -11,12 +11,7 @@ sealed trait validation extends StaticAnnotation // validate null, None, empty string, zero number and rel.valid case class required() extends validation -case class string( - blank: Boolean = false, - empty: Boolean = false, - min: Int = 0, - max: Int = 0 -) extends validation +case class string(blank: Boolean = false, empty: Boolean = false, min: Int = 0, max: Int = 0) extends validation // work with Relation or Option[Relation] case class rel() extends validation @@ -31,18 +26,12 @@ case class email() extends validation case class regexp(pattern: String) extends validation // Work with any value -case class choice( - group: String, - min: Int = 1, - max: Int = 1 -) extends validation +case class choice(group: String, min: Int = 1, max: Int = 1) extends validation // Work with any value case class options(values: Seq[Any]) extends validation case class list(min: Int = 0, max: Int = 0) extends validation -case class date(min: Instant | Null = null, - max: Instant | Null = null, - pattern: String, - time: Boolean = false) extends validation +case class date(min: Instant | Null = null, max: Instant | Null = null, pattern: String, time: Boolean = false) + extends validation diff --git a/vigilo/shared/src/main/scala/io/vigilo/core/macros.scala b/vigilo/shared/src/main/scala/io/vigilo/core/macros.scala index 74960f1..682742b 100644 --- a/vigilo/shared/src/main/scala/io/vigilo/core/macros.scala +++ b/vigilo/shared/src/main/scala/io/vigilo/core/macros.scala @@ -4,42 +4,32 @@ import scala.quoted.* object macros: - case class FieldInfo( - name: String, - annotations: Seq[validation] - ) + case class FieldInfo(name: String, annotations: Seq[validation]) inline def extractValidationsFields[T]: Seq[FieldInfo] = ${ extractValidationsFieldsImpl[T] } - def extractValidationsFieldsImpl[T: Type](using - Quotes - ): Expr[Seq[FieldInfo]] = { + def extractValidationsFieldsImpl[T: Type](using Quotes): Expr[Seq[FieldInfo]] = { import quotes.reflect.* - val targetSym = TypeRepr.of[T].typeSymbol + val targetSym = TypeRepr.of[T].typeSymbol val baseAnnoType = TypeRepr.of[validation] - def getAnnotations[A <: validation](using - Type[A] - ): Seq[Expr[(String, Seq[A])]] = { + def getAnnotations[A <: validation](using Type[A]): Seq[Expr[(String, Seq[A])]] = { - val entries = targetSym - .primaryConstructor - .paramSymss - .flatten + val entries = targetSym.primaryConstructor.paramSymss.flatten .map { field => - val fieldName = Expr(field.name) + val fieldName = Expr(field.name) // Filtra apenas anotações que estendem BaseAnnotation val fieldAnnotations = field.annotations.filter { anno => anno.tpe <:< baseAnnoType } // Converte as anotações encontradas para uma expressão de lista - val annotationExprs = fieldAnnotations.map { anno => + val annotationExprs = fieldAnnotations.map { anno => // Para cada anotação, criamos uma expressão para instanciá-la val annoTerm = anno.asExpr.asInstanceOf[Expr[A]] annoTerm } - val annoListExpr = Expr.ofSeq(annotationExprs) + val annoListExpr = Expr.ofSeq(annotationExprs) // Cria a entrada do map (nome do campo -> lista de anotações) '{ ($fieldName, $annoListExpr) } } diff --git a/vigilo/shared/src/main/scala/io/vigilo/core/validators.scala b/vigilo/shared/src/main/scala/io/vigilo/core/validators.scala index 7eec1f9..e9093b7 100644 --- a/vigilo/shared/src/main/scala/io/vigilo/core/validators.scala +++ b/vigilo/shared/src/main/scala/io/vigilo/core/validators.scala @@ -6,55 +6,48 @@ import io.vigilo.core.macros.FieldInfo import java.time.Instant import java.time.format.DateTimeFormatter - object validators: type VType = ValidatorMessages ?=> Either[Seq[String], Unit] trait FieldValidator[T]: - def validate( - fieldName: String, - validations: Seq[validation], - value: T - ): VType + def validate(fieldName: String, validations: Seq[validation], value: T): VType object FieldValidator: def apply[A](using fv: FieldValidator[A]): FieldValidator[A] = fv private type VNumber = Short | Int | Long | Float | Double - extension(n: VNumber) + extension (n: VNumber) private infix def gt(i: VNumber): Boolean = (n, i) match - case (x: Short, y: Short) => x > y - case (x: Int, y: Int) => x > y - case (x: Long, y: Long) => x > y - case (x: Float, y: Float) => x > y + case (x: Short, y: Short) => x > y + case (x: Int, y: Int) => x > y + case (x: Long, y: Long) => x > y + case (x: Float, y: Float) => x > y case (x: Double, y: Double) => x > y - case _ => false + case _ => false private infix def lt(i: VNumber): Boolean = (n, i) match - case (x: Short, y: Short) => x < y - case (x: Int, y: Int) => x < y - case (x: Long, y: Long) => x < y - case (x: Float, y: Float) => x < y + case (x: Short, y: Short) => x < y + case (x: Int, y: Int) => x < y + case (x: Long, y: Long) => x < y + case (x: Float, y: Float) => x < y case (x: Double, y: Double) => x < y - case _ => false + case _ => false private infix def eq(i: VNumber): Boolean = (n, i) match - case (x: Short, y: Short) => x == y - case (x: Int, y: Int) => x == y - case (x: Long, y: Long) => x == y - case (x: Float, y: Float) => x == y + case (x: Short, y: Short) => x == y + case (x: Int, y: Int) => x == y + case (x: Long, y: Long) => x == y + case (x: Float, y: Float) => x == y case (x: Double, y: Double) => x == y - case _ => false + case _ => false - def validateNumber(fieldName: String, value: VNumber, validations: Seq[validation]) - (using msg: ValidatorMessages): Either[Seq[String], Unit] = + def validateNumber(fieldName: String, value: VNumber, validations: Seq[validation])(using + msg: ValidatorMessages + ): Either[Seq[String], Unit] = def createStringMsg(key: String, args: Any*) = - msg.message( - s"validation.number.$key", - msg.message(fieldName) +: args * - ) + msg.message(s"validation.number.$key", msg.message(fieldName) +: args*) val results = validations @@ -62,15 +55,15 @@ object validators: v match case number(min, max) => (min, max) match - case (0, 0) => acc - case (x, 0) if value lt x => acc :+ createStringMsg("min", min) - case (0, x) if value gt x => acc :+ createStringMsg("max", max) + case (0, 0) => acc + case (x, 0) if value lt x => acc :+ createStringMsg("min", min) + case (0, x) if value gt x => acc :+ createStringMsg("max", max) case (x, y) if x > 0 && y > 0 && ((value lt x) || (value gt y)) => acc :+ createStringMsg("size", min, max) - case _ => acc - case required() => + case _ => acc + case required() => if value eq 0 then acc :+ createStringMsg("required") else acc - case _ => acc + case _ => acc } if results.nonEmpty then Left(results) @@ -78,17 +71,10 @@ object validators: given StringValidator: FieldValidator[String] = new FieldValidator[String]: - override def validate( - fieldName: String, - validations: Seq[validation], - value: String - ): VType = - val msg = summon[ValidatorMessages] + override def validate(fieldName: String, validations: Seq[validation], value: String): VType = + val msg = summon[ValidatorMessages] def createStringMsg(key: String, args: Any*) = - msg.message( - s"validation.$key", - msg.message(fieldName) +: args* - ) + msg.message(s"validation.$key", msg.message(fieldName) +: args*) val results = validations @@ -100,37 +86,38 @@ object validators: acc :+ createStringMsg("string.blank") case (_, false) if value.isEmpty => acc :+ createStringMsg("string.blank") - case _ => + case _ => (min, max) match - case (0, 0) => acc - case (x, 0) if value.length < x => + case (0, 0) => acc + case (x, 0) if value.length < x => acc :+ createStringMsg("string.min", x) - case (0, x) if value.length > x => + case (0, x) if value.length > x => acc :+ createStringMsg("string.max", x) case (x, y) if x > 0 && y > 0 && (value.length < x || value.length > y) => acc :+ createStringMsg("string.size", x, y) - case _ => acc - case required() => + case _ => acc + case required() => value match case null => acc :+ createStringMsg("required") case x if x.isEmpty => acc :+ createStringMsg("required") case _ => acc - case email() => + case email() => value match - case null => acc :+ createStringMsg("email") - case s => - val emailRegex = """^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r + case null => acc :+ createStringMsg("email") + case s => + val emailRegex = + """^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r if emailRegex.findFirstMatchIn(s).isDefined then acc else acc :+ createStringMsg("email") - case regexp(pattern) => + case regexp(pattern) => value match - case null => acc :+ createStringMsg("regex") - case s => + case null => acc :+ createStringMsg("regex") + case s => if pattern.r.findFirstMatchIn(s).isDefined then acc else acc :+ createStringMsg("regex") - case _ => acc + case _ => acc } if results.nonEmpty then Left(results) @@ -138,57 +125,33 @@ object validators: given ShortValidator: FieldValidator[Short] = new FieldValidator[Short]: - override def validate( - fieldName: String, - validations: Seq[validation], - value: Short - ): VType = + override def validate(fieldName: String, validations: Seq[validation], value: Short): VType = validateNumber(fieldName, value, validations) given IntValidator: FieldValidator[Int] = new FieldValidator[Int]: - override def validate( - fieldName: String, - validations: Seq[validation], - value: Int - ): VType = + override def validate(fieldName: String, validations: Seq[validation], value: Int): VType = validateNumber(fieldName, value, validations) given LongValidator: FieldValidator[Long] = new FieldValidator[Long]: - def validate( - fieldName: String, - validations: Seq[validation], - value: Long - ): VType = + def validate(fieldName: String, validations: Seq[validation], value: Long): VType = validateNumber(fieldName, value, validations) given FloatValidator: FieldValidator[Float] = new FieldValidator[Float]: - def validate( - fieldName: String, - validations: Seq[validation], - value: Float - ): VType = + def validate(fieldName: String, validations: Seq[validation], value: Float): VType = validateNumber(fieldName, value, validations) given DoubleValidator: FieldValidator[Double] = new FieldValidator[Double]: - def validate( - fieldName: String, - validations: Seq[validation], - value: Double - ): VType = + def validate(fieldName: String, validations: Seq[validation], value: Double): VType = validateNumber(fieldName, value, validations) - given RelationValidator: [A <: Relation] => FieldValidator[A] = + given RelationValidator: [A <: Relation] => FieldValidator[A] = new FieldValidator[A]: - def validate( - fieldName: String, - validations: Seq[validation], - value: A - ): VType = - val msg = summon[ValidatorMessages] + def validate(fieldName: String, validations: Seq[validation], value: A): VType = + val msg = summon[ValidatorMessages] val results = validations .foldRight(Seq.empty[String]) { (v, acc) => @@ -197,7 +160,7 @@ object validators: if value == null || !value.valid then acc :+ msg.message("validation.rel", msg.message(fieldName)) else acc - case _ => acc + case _ => acc } if results.nonEmpty @@ -206,36 +169,48 @@ object validators: given InstantValidator: FieldValidator[Instant] = new FieldValidator[Instant]: - def validate( - fieldName: String, - validations: Seq[validation], - value: Instant - ): VType = - val msg = summon[ValidatorMessages] + def validate(fieldName: String, validations: Seq[validation], value: Instant): VType = + val msg = summon[ValidatorMessages] val results = validations .foldRight(Seq.empty[String]) { (v, acc) => v match - case date(min = null, max = null) => acc - case date(min = min, max = null, pattern = pattern) if min != null => + case date(min = null, max = null) => acc + case date(min = min, max = null, pattern = pattern) if min != null => if value.isBefore(min) - then acc :+ msg.message( - "validation.date.min", msg.message(fieldName), DateTimeFormatter.ofPattern(pattern).format(min)) + then + acc :+ msg.message( + "validation.date.min", + msg.message(fieldName), + DateTimeFormatter.ofPattern(pattern).format(min) + ) else acc - case date(min = null, max = max, pattern = pattern) if max != null => + case date(min = null, max = max, pattern = pattern) if max != null => if value.isAfter(max) - then acc :+ msg.message( - "validation.date.max", msg.message(fieldName), DateTimeFormatter.ofPattern(pattern).format(max)) + then + acc :+ msg.message( + "validation.date.max", + msg.message(fieldName), + DateTimeFormatter.ofPattern(pattern).format(max) + ) else acc case date(min = min, max = max, pattern = pattern) if min != null && max != null => if value.isBefore(min) - then acc :+ msg.message( - "validation.date.min", msg.message(fieldName), DateTimeFormatter.ofPattern(pattern).format(min)) + then + acc :+ msg.message( + "validation.date.min", + msg.message(fieldName), + DateTimeFormatter.ofPattern(pattern).format(min) + ) else if value.isAfter(max) - then acc :+ msg.message( - "validation.date.max", msg.message(fieldName), DateTimeFormatter.ofPattern(pattern).format(max)) + then + acc :+ msg.message( + "validation.date.max", + msg.message(fieldName), + DateTimeFormatter.ofPattern(pattern).format(max) + ) else acc - case _ => acc + case _ => acc } if results.nonEmpty @@ -244,12 +219,8 @@ object validators: given OptionValidator: [A: FieldValidator] => FieldValidator[Option[A]] = new FieldValidator[Option[A]]: - def validate( - fieldName: String, - validations: Seq[validation], - value: Option[A] - ): VType = - val msg = summon[ValidatorMessages] + def validate(fieldName: String, validations: Seq[validation], value: Option[A]): VType = + val msg = summon[ValidatorMessages] val results = validations .foldRight(Seq.empty[String]) { (v, acc) => @@ -257,11 +228,11 @@ object validators: case required() => value match case None | null => acc :+ msg.message("validation.required", msg.message(fieldName)) - case Some(t) => + case Some(t) => FieldValidator[A].validate(fieldName, validations, t) match case Left(seq) => acc ++ seq - case _ => acc - case _ => acc + case _ => acc + case _ => acc } if results.nonEmpty then Left(results) @@ -269,73 +240,67 @@ object validators: given IterableValidator: [A, Col <: Iterable[A]] => FieldValidator[Col] = new FieldValidator[Col]: - def validate( - fieldName: String, - validations: Seq[validation], - value: Col - ): VType = - val msg = summon[ValidatorMessages] + def validate(fieldName: String, validations: Seq[validation], value: Col): VType = + val msg = summon[ValidatorMessages] val results = validations .foldRight(Seq.empty[String]) { (v, acc) => v match - case required() => + case required() => value match case it if it.isEmpty => acc :+ msg.message("validation.required", msg.message(fieldName)) - case _ => acc - case options(opts) => + case _ => acc + case options(opts) => if value.exists(x => !opts.contains(x)) then acc :+ msg.message("validation.options", msg.message(fieldName), opts) else acc - case list(min, 0) if value.size < min => + case list(min, 0) if value.size < min => acc :+ msg.message("validation.list.min", msg.message(fieldName), min) - case list(0, max) if value.size > max => + case list(0, max) if value.size > max => acc :+ msg.message("validation.list.max", msg.message(fieldName), max) case list(min, max) if min > 0 && max > 0 && (value.size < min || value.size > max) => acc :+ msg.message("validation.list.size", msg.message(fieldName), min, max) - case _ => acc + case _ => acc } if results.nonEmpty then Left(results) else Right(()) - - private[vigilo] def optionsValidator( - fields: Seq[FieldInfo], - values: Seq[(String, Any)] - ): ValidatorMessages ?=> Either[Map[String, String], Unit] = - val msg = summon[ValidatorMessages] + fields: Seq[FieldInfo], + values: Seq[(String, Any)] + ): ValidatorMessages ?=> Either[Map[String, String], Unit] = + val msg = summon[ValidatorMessages] val optionsAnno = fields .filterNot { fd => fd.annotations.exists { case _: list => true - case _ => false + case _ => false } } .filter { fd => fd.annotations.exists { case _: options => true - case _ => false + case _ => false } } .collect { it => - val first = it.annotations.collectFirst{ case opt: options => opt }.get + val first = it.annotations.collectFirst { case opt: options => opt }.get (fieldName = it.name, opts = first.values) } val results = optionsAnno.foldRight(Map.empty[String, String]) { (v, acc) => val fieldName = v.fieldName - val opts = v.opts + val opts = v.opts values.find(_._1 == fieldName) match case Some(s) => if opts.contains(s._2) then acc else acc + (fieldName -> msg.message("validation.values", fieldName, opts)) - case None => + case None => acc } @@ -344,9 +309,9 @@ object validators: else Right(()) private[vigilo] def choicesValidator( - fields: Seq[FieldInfo], - values: Seq[(String, Any)] - ): ValidatorMessages ?=> Either[Map[String, String], Unit] = + fields: Seq[FieldInfo], + values: Seq[(String, Any)] + ): ValidatorMessages ?=> Either[Map[String, String], Unit] = val msg = summon[ValidatorMessages] val choicesAnno = @@ -354,7 +319,7 @@ object validators: .filter { fd => fd.annotations.exists { case _: choice => true - case _ => false + case _ => false } } .collect { it => @@ -373,20 +338,20 @@ object validators: .map(_._2) .count { case Some(_) => true - case _ => false + case _ => false } choice match - case (min = 0, max = 0) => acc - case (min = x, max = 0) if count < x => + case (min = 0, max = 0) => acc + case (min = x, max = 0) if count < x => acc ++ names.map(s => s -> msg.message("validation.choices.min", s, x, names)).toMap - case (min = 0, max = x) if count > x => + case (min = 0, max = x) if count > x => acc ++ names.map(s => s -> msg.message("validation.choices.max", s, x, names)).toMap case (min = x, max = y) if x > 0 && y > 0 && (count < x || count > y) => acc ++ names.map(s => s -> msg.message("validation.choices.size", s, x, y, names)).toMap - case _ => acc + case _ => acc } if results.nonEmpty then Left(results) - else Right(()) \ No newline at end of file + else Right(()) diff --git a/vigilo/shared/src/main/scala/io/vigilo/package.scala b/vigilo/shared/src/main/scala/io/vigilo/package.scala index 46192b8..6c5c66c 100644 --- a/vigilo/shared/src/main/scala/io/vigilo/package.scala +++ b/vigilo/shared/src/main/scala/io/vigilo/package.scala @@ -8,24 +8,9 @@ import scala.deriving.Mirror package object vigilo: - export io.vigilo.core.{ - validation, - string, - number, - rel, - choice, - options, - regexp, - email, - list, - required, - Relation - } - - export Validator.{ - validate, - validateWithIgnoreFields - } + export io.vigilo.core.{choice, email, list, number, options, regexp, rel, required, string, validation, Relation} + + export Validator.{validate, validateWithIgnoreFields} export io.vigilo.core.validators.FieldValidator @@ -48,24 +33,20 @@ package object vigilo: .getOrElse(Nil) extension [T](value: T)(using validator: Validator[T]) - def validate: ValidatorMessages ?=> Result = validator.validate(value) - def validateWithIgnoreFields(fields: String*): ValidatorMessages ?=> Result = validator.validateWithIgnoreFields(value, fields*) + def validate: ValidatorMessages ?=> Result = validator.validate(value) + def validateWithIgnoreFields(fields: String*): ValidatorMessages ?=> Result = + validator.validateWithIgnoreFields(value, fields*) inline def apply[A](using validator: Validator[A]): Validator[A] = validator private def validateOthers(value: Any, fields: Seq[FieldInfo]): ValidatorMessages ?=> Map[String, String] = val product = value.asInstanceOf[Product] - val values = - product - .productElementNames - .zipWithIndex - .map { - case (name, i) => (name, product.productElement(i)) - } - .toSeq - - - val vchoices = + val values = + product.productElementNames.zipWithIndex.map { case (name, i) => + (name, product.productElement(i)) + }.toSeq + + val vchoices = choicesValidator(fields, values) match case Right(()) => Map.empty case Left(map) => map @@ -73,39 +54,35 @@ package object vigilo: optionsValidator(fields, values) match case Right(()) => Map.empty case Left(map) => map - + vchoices ++ voptions inline given derived: [A] => (m: Mirror.Of[A], msg: ValidatorMessages) => Validator[A] = val fields = extractValidationsFields[A] - type Mets = m.MirroredElemTypes - type Mels = m.MirroredElemLabels + type Mets = m.MirroredElemTypes + type Mels = m.MirroredElemLabels type Label = m.MirroredLabel - inline m match case p: Mirror.ProductOf[A] => new Validator[A]: override def validate(value: A): ValidatorMessages ?=> Result = val othersResults = - validateOthers(value, fields).map { - (k, v) => k -> Seq(v) + validateOthers(value, fields).map { (k, v) => + k -> Seq(v) } - - val results = runValidations[A, Mets, Mels]( - value.asInstanceOf[Product], - fields - ) + + val results = runValidations[A, Mets, Mels](value.asInstanceOf[Product], fields) Result(results ++ othersResults) override def validateWithIgnoreFields(value: A, ignoreFields: String*): ValidatorMessages ?=> Result = val othersResults = - validateOthers(value, fields).map { - (k, v) => k -> Seq(v) + validateOthers(value, fields).map { (k, v) => + k -> Seq(v) } val results = runValidations[A, Mets, Mels]( @@ -118,11 +95,11 @@ package object vigilo: case _ => throw new Exception(s"can't get product of type") private inline def runValidations[A, Mets, Mels]( - product: Product, - fields: Seq[FieldInfo], - i: Int = 0, - acc: Map[String, Seq[String]] = Map.empty - ): ValidatorMessages ?=> Map[String, Seq[String]] = { + product: Product, + fields: Seq[FieldInfo], + i: Int = 0, + acc: Map[String, Seq[String]] = Map.empty + ): ValidatorMessages ?=> Map[String, Seq[String]] = inline (erasedValue[Mets], erasedValue[Mels]) match // base case @@ -132,18 +109,17 @@ package object vigilo: val fieldName = constValue[mel & String] val validations = findAnnotations(fields, fieldName) - val newAcc = + val newAcc = inline if validations.isEmpty then acc - else summonFrom { - case validator: FieldValidator[met] => - val value = product.productElement(i).asInstanceOf[met] - validator.validate(fieldName, validations, value) match - case Right(()) => acc - case Left(results) => acc + (fieldName -> results) - case _ => acc - } - + else + summonFrom { + case validator: FieldValidator[met] => + val value = product.productElement(i).asInstanceOf[met] + validator.validate(fieldName, validations, value) match + case Right(()) => acc + case Left(results) => acc + (fieldName -> results) + case _ => acc + } runValidations[A, metsTail, melsTail](product, fields, i + 1, newAcc) - }