~jan0sch/smederee
Showing details for patch a735c49a4838632116734ae3b37941b97adddf91.
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"))),