~jan0sch/smederee

Showing details for patch a735c49a4838632116734ae3b37941b97adddf91.
2022-08-12 (Fri), 11:00 AM - Jens Grassel - a735c49a4838632116734ae3b37941b97adddf91

Refactoring: Switch from Nel to Nec when validating

- also extend NewVcsRepositoryForm and VcsRepository
Summary of changes
11 files modified with 138 lines added and 62 lines removed
  • modules/hub/src/main/scala/de/smederee/hub/Account.scala with 12 added and 12 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala with 1 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala with 7 added and 7 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala with 50 added and 11 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala with 12 added and 12 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala with 6 added and 6 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala with 8 added and 8 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 1 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala with 1 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/forms/types.scala with 14 added and 2 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala with 26 added and 1 removed lines
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala	2025-02-02 17:10:45.172789028 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala	2025-02-02 17:10:45.176789035 +0000
@@ -120,14 +120,14 @@
     * @return
     *   Either a list of errors or the validated Password.
     */
-  def validate(source: String): ValidatedNel[String, Password] =
+  def validate(source: String): ValidatedNec[String, Password] =
     Option(source).map(_.trim.nonEmpty) match {
       case Some(true) =>
         if (source.trim.length < 12)
-          "Password must be at least 12 characters long!".invalidNel
+          "Password must be at least 12 characters long!".invalidNec
         else
-          source.trim.getBytes(StandardCharsets.UTF_8).validNel
-      case _ => "Password must not be empty!".invalidNel
+          source.trim.getBytes(StandardCharsets.UTF_8).validNec
+      case _ => "Password must not be empty!".invalidNec
     }
 }
 
@@ -304,29 +304,29 @@
     * @return
     *   Either a list of errors or the validated username.
     */
-  def validate(s: String): ValidatedNel[String, Username] =
+  def validate(s: String): ValidatedNec[String, Username] =
     Option(s).map(_.trim.nonEmpty) match {
       case Some(true) =>
         val input = s.trim
         val miniumLength =
           if (input.length >= 2)
-            input.validNel
+            input.validNec
           else
-            "Username too short (min. 2 characters)!".invalidNel
+            "Username too short (min. 2 characters)!".invalidNec
         val maximumLength =
           if (input.length < 32)
-            input.validNel
+            input.validNec
           else
-            "Username too long (max. 31 characters)!".invalidNel
+            "Username too long (max. 31 characters)!".invalidNec
         val alphanumeric =
           if (isAlphanumeric.matches(input))
-            input.validNel
+            input.validNec
           else
-            "Username must be all lowercase alphanumeric characters and start with a character.".invalidNel
+            "Username must be all lowercase alphanumeric characters and start with a character.".invalidNec
         (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
           name
         }
-      case _ => "Username must not be empty!".invalidNel
+      case _ => "Username must not be empty!".invalidNec
     }
 }
 
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala	2025-02-02 17:10:45.172789028 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala	2025-02-02 17:10:45.176789035 +0000
@@ -77,7 +77,7 @@
             BadRequest.apply(
               views.html.login()(loginPath, csrf, title = "Smederee - Login to your account".some)(
                 formData,
-                FormErrors.fromNel(es)
+                FormErrors.fromNec(es)
               )
             )
           case Validated.Valid(loginForm) =>
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala	2025-02-02 17:10:45.176789035 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala	2025-02-02 17:10:45.176789035 +0000
@@ -36,7 +36,7 @@
     * @return
     *   Either the validated form as concrete type T or a list of form errors.
     */
-  def validate(data: Map[String, String]): ValidatedNel[FormErrors, T]
+  def validate(data: Map[String, String]): ValidatedNec[FormErrors, T]
 
 }
 
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala	2025-02-02 17:10:45.176789035 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala	2025-02-02 17:10:45.176789035 +0000
@@ -18,15 +18,27 @@
   object FormErrors {
     val empty: FormErrors = Map.empty[FormField, List[FormFieldError]]
 
+    /** Create a single FormErrors instance from a given non empty chain of FormErrors which is usually
+      * returned from validators.
+      *
+      * @param errors
+      *   A non empty chain of FormErrors.
+      * @return
+      *   A single FormErrors instance containing all the errors.
+      */
+    def fromNec(errors: NonEmptyChain[FormErrors]): FormErrors =
+      errors.toList.fold(FormErrors.empty)(_ combine _)
+
     /** Create a single FormErrors instance from a given non empty list of FormErrors which is usually
       * returned from validators.
       *
-      * @param es
+      * @param errors
       *   A non empty list of FormErrors.
       * @return
       *   A single FormErrors instance containing all the errors.
       */
-    def fromNel(es: NonEmptyList[FormErrors]): FormErrors = es.toList.fold(FormErrors.empty)(_ combine _)
+    def fromNel(errors: NonEmptyList[FormErrors]): FormErrors =
+      errors.toList.fold(FormErrors.empty)(_ combine _)
   }
 
   opaque type FormField = String
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala	2025-02-02 17:10:45.172789028 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala	2025-02-02 17:10:45.176789035 +0000
@@ -27,16 +27,16 @@
   val fieldName: FormField     = FormField("name")
   val fieldPassword: FormField = FormField("password")
 
-  override def validate(data: Map[String, String]): ValidatedNel[FormErrors, LoginForm] = {
+  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, LoginForm] = {
     val genericError = FormFieldError("Invalid credentials!") // We just return a generic error.
-    val name: ValidatedNel[FormErrors, Username] = data
+    val name: ValidatedNec[FormErrors, Username] = data
       .get(fieldName)
-      .fold(genericError.invalidNel)(s => Username.from(s).fold(genericError.invalidNel)(_.validNel))
-      .leftMap(es => NonEmptyList.of(Map(fieldName -> es.toList)))
-    val password: ValidatedNel[FormErrors, Password] = data
+      .fold(genericError.invalidNec)(s => Username.from(s).fold(genericError.invalidNec)(_.validNec))
+      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+    val password: ValidatedNec[FormErrors, Password] = data
       .get(fieldPassword)
-      .fold(genericError.invalidNel)(s => Password.from(s).fold(genericError.invalidNel)(_.validNel))
-      .leftMap(es => NonEmptyList.of(Map(fieldPassword -> es.toList)))
+      .fold(genericError.invalidNec)(s => Password.from(s).fold(genericError.invalidNec)(_.validNec))
+      .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList)))
     (name, password).mapN { case (n, pw) =>
       LoginForm(n, pw)
     }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala	2025-02-02 17:10:45.172789028 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala	2025-02-02 17:10:45.176789035 +0000
@@ -13,25 +13,64 @@
 import cats.syntax.all._
 import de.smederee.hub.forms._
 import de.smederee.hub.forms.types._
+import org.http4s.Uri
 
 /** Data container for the form to create a new VCS repository.
   *
   * @param name
-  *   The name of the repository which will be created as a folder on disk. It must not be a duplicate of an
-  *   already existing one.
+  *   The name of the repository. A repository name must start with a letter or number and must contain only
+  *   alphanumeric ASCII characters as well as minus or underscore signs. It must be between 2 and 64
+  *   characters long.
+  * @param isPrivate
+  *   A flag indicating if this repository is private i.e. only visible / accessible for accounts with
+  *   appropriate permissions.
+  * @param description
+  *   An optional short text description of the repository.
+  * @param website
+  *   An optional uri pointing to a website related to the repository / project.
   */
-final case class NewVcsRepositoryForm(name: VcsRepositoryName)
+final case class NewVcsRepositoryForm(
+    name: VcsRepositoryName,
+    isPrivate: Boolean,
+    description: Option[VcsRepositoryDescription],
+    website: Option[Uri]
+)
 
 object NewVcsRepositoryForm extends FormValidator[NewVcsRepositoryForm] {
-  val fieldName: FormField = FormField("name")
+  val fieldDescription: FormField = FormField("description")
+  val fieldIsPrivate: FormField   = FormField("is_private")
+  val fieldName: FormField        = FormField("name")
+  val fieldWebsite: FormField     = FormField("website")
 
-  override def validate(data: Map[String, String]): ValidatedNel[FormErrors, NewVcsRepositoryForm] = {
-    val name: ValidatedNel[FormErrors, VcsRepositoryName] = data
+  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, NewVcsRepositoryForm] = {
+    val name = data
       .get(fieldName)
-      .fold(FormFieldError("No repository name given!").invalidNel)(s =>
-        VcsRepositoryName.from(s).fold(FormFieldError("Invalid repository name!").invalidNel)(_.validNel)
+      .fold(FormFieldError("No repository name given!").invalidNec)(s =>
+        VcsRepositoryName.from(s).fold(FormFieldError("Invalid repository name!").invalidNec)(_.validNec)
       )
-      .leftMap(es => NonEmptyList.of(Map(fieldName -> es.toList)))
-    name.map(NewVcsRepositoryForm.apply)
+      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+    val privateFlag: ValidatedNec[FormErrors, Boolean] =
+      data.get(fieldIsPrivate).fold(false.validNec)(s => s.matches("true").validNec)
+    val description = data
+      .get(fieldDescription)
+      .fold(Option.empty[VcsRepositoryDescription].validNec)(s =>
+        VcsRepositoryDescription
+          .from(s)
+          .fold(FormFieldError("Invalid repository description!").invalidNec)(descr => Option(descr).validNec)
+      )
+      .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
+    val website = data
+      .get(fieldWebsite)
+      .fold(Option.empty[Uri].validNec)(s =>
+        Uri
+          .fromString(s)
+          .toOption
+          .fold(FormFieldError("Invalid website URI!").invalidNec)(uri => Option(uri).validNec)
+      )
+      .leftMap(es => NonEmptyChain.of(Map(fieldWebsite -> es.toList)))
+    (name, privateFlag, description, website).mapN {
+      case (validName, isPrivate, validDescription, validWebsite) =>
+        NewVcsRepositoryForm(validName, isPrivate, validDescription, validWebsite)
+    }
   }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala	2025-02-02 17:10:45.172789028 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala	2025-02-02 17:10:45.176789035 +0000
@@ -32,23 +32,23 @@
   val fieldName: FormField     = FormField("name")
   val fieldEmail: FormField    = FormField("email")
   val fieldPassword: FormField = FormField("password")
-  override def validate(data: Map[String, String]): ValidatedNel[FormErrors, SignupForm] = {
-    val email: ValidatedNel[FormErrors, Email] = data
+  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, SignupForm] = {
+    val email: ValidatedNec[FormErrors, Email] = data
       .get(fieldEmail)
-      .fold(FormFieldError("No email address given!").invalidNel)(s =>
-        Email.from(s).fold(FormFieldError("Invalid email address!").invalidNel)(_.validNel)
+      .fold(FormFieldError("No email address given!").invalidNec)(s =>
+        Email.from(s).fold(FormFieldError("Invalid email address!").invalidNec)(_.validNec)
       )
-      .leftMap(es => NonEmptyList.of(Map(fieldEmail -> es.toList)))
-    val name: ValidatedNel[FormErrors, Username] = data
+      .leftMap(es => NonEmptyChain.of(Map(fieldEmail -> es.toList)))
+    val name: ValidatedNec[FormErrors, Username] = data
       .get(fieldName)
-      .fold(FormFieldError("No username given!").invalidNel)(s =>
-        Username.from(s).fold(FormFieldError("Invalid username!").invalidNel)(_.validNel)
+      .fold(FormFieldError("No username given!").invalidNec)(s =>
+        Username.from(s).fold(FormFieldError("Invalid username!").invalidNec)(_.validNec)
       )
-      .leftMap(es => NonEmptyList.of(Map(fieldName -> es.toList)))
-    val password: ValidatedNel[FormErrors, Password] = data
+      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+    val password: ValidatedNec[FormErrors, Password] = data
       .get(fieldPassword)
-      .fold("No password given!".invalidNel)(Password.validate)
-      .leftMap(es => NonEmptyList.of(Map(fieldPassword -> es.toList.map(FormFieldError.apply))))
+      .fold("No password given!".invalidNec)(Password.validate)
+      .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList.map(FormFieldError.apply))))
     (email, name, password).mapN { case (validEmail, validName, validPassword) =>
       SignupForm(validName, validEmail, validPassword)
     }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala	2025-02-02 17:10:45.172789028 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala	2025-02-02 17:10:45.176789035 +0000
@@ -64,13 +64,13 @@
           for {
             duplicateEmail <- repo.findEmail(signupForm.email)
             duplicateName  <- repo.findUsername(signupForm.name)
-            validatedEmail = duplicateEmail.fold(signupForm.email.validNel[FormErrors])(_ =>
+            validatedEmail = duplicateEmail.fold(signupForm.email.validNec[FormErrors])(_ =>
               Map(
                 SignupForm.fieldEmail -> List(FormFieldError("This email address is already in use!"))
-              ).invalidNel
+              ).invalidNec
             )
-            validatedName = duplicateName.fold(signupForm.name.validNel[FormErrors])(_ =>
-              Map(SignupForm.fieldName -> List(FormFieldError("This username is already in use!"))).invalidNel
+            validatedName = duplicateName.fold(signupForm.name.validNec[FormErrors])(_ =>
+              Map(SignupForm.fieldName -> List(FormFieldError("This username is already in use!"))).invalidNec
             )
             validatedEmailAndName = (validatedEmail, validatedName).mapN { case (_, _) =>
               signupForm: SignupForm
@@ -83,7 +83,7 @@
               views.html
                 .signup()(signupPath, csrf, "Smederee - Sign up for an account".some)(
                   formData,
-                  FormErrors.fromNel(es)
+                  FormErrors.fromNec(es)
                 )
             )
           case Validated.Valid(innerValidation) =>
@@ -93,7 +93,7 @@
                   views.html
                     .signup()(signupPath, csrf, "Smederee - Sign up for an account".some)(
                       formData,
-                      FormErrors.fromNel(es)
+                      FormErrors.fromNec(es)
                     )
                 )
               case Validated.Valid(signupForm) =>
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-02-02 17:10:45.176789035 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-02-02 17:10:45.176789035 +0000
@@ -97,7 +97,7 @@
                 views.html
                   .createRepository()(createRepoPath, csrf, "Smederee - Create a new repository".some, user)(
                     formData,
-                    FormErrors.fromNel(es)
+                    FormErrors.fromNec(es)
                   )
               )
             case Validated.Valid(newVcsRepository) =>
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala	2025-02-02 17:10:45.172789028 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala	2025-02-02 17:10:45.176789035 +0000
@@ -69,29 +69,29 @@
     * @return
     *   Either a list of errors or the validated repository name.
     */
-  def validate(s: String): ValidatedNel[String, VcsRepositoryName] =
+  def validate(s: String): ValidatedNec[String, VcsRepositoryName] =
     Option(s).map(_.trim.nonEmpty) match {
       case Some(true) =>
         val input = s.trim
         val miniumLength =
           if (input.length > 1)
-            input.validNel
+            input.validNec
           else
-            "Repository name too short (min. 2 characters)!".invalidNel
+            "Repository name too short (min. 2 characters)!".invalidNec
         val maximumLength =
           if (input.length < 65)
-            input.validNel
+            input.validNec
           else
-            "Repository name too long (max. 64 characters)!".invalidNel
+            "Repository name too long (max. 64 characters)!".invalidNec
         val validFormat =
           if (Format.matches(input))
-            input.validNel
+            input.validNec
           else
-            "Repository name must start with a letter or number and is only allowed to contain alphanumeric characters plus .".invalidNel
+            "Repository name must start with a letter or number and is only allowed to contain alphanumeric characters plus .".invalidNec
         (miniumLength, maximumLength, validFormat).mapN { case (_, _, name) =>
           name
         }
-      case _ => "Repository name must not be empty!".invalidNel
+      case _ => "Repository name must not be empty!".invalidNec
     }
 }
 
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala	2025-02-02 17:10:45.176789035 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala	2025-02-02 17:10:45.176789035 +0000
@@ -9,7 +9,7 @@
 
 package de.smederee.hub.forms
 
-import cats.data.NonEmptyList
+import cats.data._
 import cats.syntax.all._
 import de.smederee.hub.forms.types._
 
@@ -22,6 +22,31 @@
     assert(noErrors.isEmpty)
   }
 
+  test("FormErrors.fromNec must collapse given instances into a single one") {
+    val a = NonEmptyChain.of(
+      FormErrors.empty + (FormField("a") -> List(FormFieldError("Error a1"))),
+      FormErrors.empty + (FormField("b") -> List(FormFieldError("Error b1")))
+    )
+    val b = NonEmptyChain.of(FormErrors.empty)
+    val c = NonEmptyChain.of(
+      Map(
+        FormField("a") -> List(FormFieldError("Error a2")),
+        FormField("b") -> List(FormFieldError("Error b2"))
+      ),
+      Map(FormField("b") -> List(FormFieldError("Error b3")))
+    )
+    val expected = Map(
+      FormField("a") -> List(FormFieldError("Error a1"), FormFieldError("Error a2")),
+      FormField("b") -> List(
+        FormFieldError("Error b1"),
+        FormFieldError("Error b2"),
+        FormFieldError("Error b3")
+      )
+    )
+    val combined = FormErrors.fromNec(a |+| b |+| c)
+    assertEquals(combined, expected)
+  }
+
   test("FormErrors.fromNel must collapse given instances into a single one") {
     val a = NonEmptyList.of(
       FormErrors.empty + (FormField("a") -> List(FormFieldError("Error a1"))),