From 492beccd3ae120713a00ead3b749245481fbbdd4 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Thu, 11 Mar 2021 19:07:29 +0300 Subject: [PATCH 1/2] feat: project-manager pkg validation --- .../src/main/scala/org/enso/pkg/Package.scala | 72 ++------ .../pkg/validation/InvalidNameError.scala | 11 ++ .../enso/pkg/validation/NameValidation.scala | 76 ++++++++ .../org/enso/pkg/NameSanitizationSpec.scala | 46 +++-- .../repository/ProjectFileRepository.scala | 5 +- .../service/MonadicProjectValidator.scala | 43 ++--- .../service/ProjectService.scala | 30 +-- .../service/ValidationFailure.scala | 4 + .../ProjectCreateDefaultToLatestSpec.scala | 2 +- .../ProjectCreateMissingComponentsSpec.scala | 2 +- .../protocol/ProjectManagementApiSpec.scala | 173 ++++++++++++++---- .../ProjectOpenMissingComponentsSpec.scala | 4 +- 12 files changed, 319 insertions(+), 149 deletions(-) create mode 100644 lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala create mode 100644 lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala index 3c566062add3..60c67d440583 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala @@ -6,6 +6,7 @@ import cats.Show import scala.jdk.CollectionConverters._ import org.enso.filesystem.FileSystem +import org.enso.pkg.validation.NameValidation import scala.io.Source import scala.util.{Failure, Try, Using} @@ -202,7 +203,7 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { maintainers: List[Contact] = List() ): Package[F] = { val config = Config( - name = normalizeName(name), + name = NameValidation.normalizeName(name), version = version, ensoVersion = ensoVersion, license = "", @@ -281,57 +282,6 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { existing.getOrElse(create(root, generateName(root))) } - /** Checks if a character is allowed in a project name. - * - * @param char the char to validate - * @return `true` if it's allowed, `false` otherwise - */ - private def isAllowedNameCharacter(char: Char): Boolean = { - char.isLetterOrDigit || char == '_' - } - - /** Takes a name containing letters, digits, and `_` characters and makes it - * a proper `Upper_Snake_Case` name. - * - * @param string the input string - * @return the transformed string - */ - private def toUpperSnakeCase(string: String): String = { - val beginMarker = '#' - val chars = string.toList - val charPairs = (beginMarker :: chars).zip(chars) - charPairs - .map { case (previous, current) => - if (previous == beginMarker) { - current.toString - } else if (previous.isLower && current.isUpper) { - s"_$current" - } else if (previous.isLetter && current.isDigit) { - s"_$current" - } else if (previous == '_' && current == '_') { - "" - } else if (previous.isDigit && current.isLetter) { - s"_${current.toUpper}" - } else { - current.toString - } - } - .mkString("") - } - - /** Transforms the given string into a valid package name (i.e. a CamelCased identifier). - * - * @param name the original name. - * @return the transformed name conforming to the specification. - */ - def normalizeName(name: String): String = { - val startingWithLetter = - if (name.length == 0 || !name(0).isLetter) "Project_" ++ name else name - val startingWithUppercase = startingWithLetter.capitalize - val onlyAlphanumeric = startingWithUppercase.filter(isAllowedNameCharacter) - toUpperSnakeCase(onlyAlphanumeric) - } - /** Generates a name for the package, by normalizing the last segment of its root path. * * @param file the root location of the package. @@ -339,32 +289,32 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { */ def generateName(file: F): String = { val dirname = file.getName - normalizeName(dirname) + NameValidation.normalizeName(dirname) } } object PackageManager { val Default = new PackageManager[File]()(FileSystem.Default) - /** A general exception indicating that a package cannot be loaded. - */ + /** A general exception indicating that a package cannot be loaded. */ class PackageLoadingException(message: String, cause: Throwable) extends RuntimeException(message, cause) { - /** @inheritdoc - */ + /** @inheritdoc */ override def toString: String = message } - /** The error indicating that the requested package does not exist. - */ + /** The error indicating that the requested package does not exist. */ case class PackageNotFound() extends PackageLoadingException(s"The package file does not exist.", null) - /** The error indicating that the package exists, but cannot be loaded. - */ + /** The error indicating that the package exists, but cannot be loaded. */ case class PackageLoadingFailure(message: String, cause: Throwable) extends PackageLoadingException(message, cause) + + /** The error indicating that the project name is invalid. */ + case class InvalidNameException(message: String) + extends RuntimeException(message) } /** A companion object for static methods on the [[Package]] class. diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala new file mode 100644 index 000000000000..199a2c498920 --- /dev/null +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala @@ -0,0 +1,11 @@ +package org.enso.pkg.validation + +sealed trait InvalidNameError +object InvalidNameError { + + case object Empty extends InvalidNameError + case object ShouldStartWithCapitalLetter extends InvalidNameError + case class ContainsInvalidCharacters(invalidCharacters: Set[Char]) + extends InvalidNameError + case class ShouldBeUpperSnakeCase(validName: String) extends InvalidNameError +} diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala new file mode 100644 index 000000000000..26a327553576 --- /dev/null +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala @@ -0,0 +1,76 @@ +package org.enso.pkg.validation + +import scala.collection.immutable.ListSet + +object NameValidation { + + /** Transforms the given string into a valid package name (i.e. a CamelCased identifier). + * + * @param name the original name. + * @return the transformed name conforming to the specification. + */ + def normalizeName(name: String): String = { + val startingWithLetter = + if (name.isEmpty || !name.head.isLetter) "Project_" ++ name else name + val startingWithUppercase = startingWithLetter.capitalize + val onlyAlphanumeric = startingWithUppercase.filter(isAllowedNameCharacter) + toUpperSnakeCase(onlyAlphanumeric) + } + + def validateName(name: String): Either[InvalidNameError, String] = + if (name.isEmpty) { + Left(InvalidNameError.Empty) + } else if (!name.head.isLetter || !name.head.isUpper) { + Left(InvalidNameError.ShouldStartWithCapitalLetter) + } else if (!name.forall(isAllowedNameCharacter)) { + val invalidCharacters = name.filterNot(isAllowedNameCharacter) + Left( + InvalidNameError.ContainsInvalidCharacters( + ListSet(invalidCharacters: _*) + ) + ) + } else if (name != toUpperSnakeCase(name)) { + Left(InvalidNameError.ShouldBeUpperSnakeCase(toUpperSnakeCase(name))) + } else { + Right(name) + } + + /** Checks if a character is allowed in a project name. + * + * @param char the char to validate + * @return `true` if it's allowed, `false` otherwise + */ + private def isAllowedNameCharacter(char: Char): Boolean = { + char.isLetterOrDigit || char == '_' + } + + /** Takes a name containing letters, digits, and `_` characters and makes it + * a proper `Upper_Snake_Case` name. + * + * @param string the input string + * @return the transformed string + */ + private def toUpperSnakeCase(string: String): String = { + val beginMarker = '#' + val chars = string.toList + val charPairs = (beginMarker :: chars).zip(chars) + charPairs + .map { case (previous, current) => + if (previous == beginMarker) { + current.toString + } else if (previous.isLower && current.isUpper) { + s"_$current" + } else if (previous.isLetter && current.isDigit) { + s"_$current" + } else if (previous == '_' && current == '_') { + "" + } else if (previous.isDigit && current.isLetter) { + s"_${current.toUpper}" + } else { + current.toString + } + } + .mkString("") + } + +} diff --git a/lib/scala/pkg/src/test/scala/org/enso/pkg/NameSanitizationSpec.scala b/lib/scala/pkg/src/test/scala/org/enso/pkg/NameSanitizationSpec.scala index 7e37a3e624dd..14892ee23c09 100644 --- a/lib/scala/pkg/src/test/scala/org/enso/pkg/NameSanitizationSpec.scala +++ b/lib/scala/pkg/src/test/scala/org/enso/pkg/NameSanitizationSpec.scala @@ -1,23 +1,45 @@ package org.enso.pkg -import java.io.File - -import org.enso.filesystem.FileSystem +import org.enso.pkg.validation.{InvalidNameError, NameValidation} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import scala.collection.immutable.ListSet + class NameSanitizationSpec extends AnyWordSpec with Matchers { - "Creating a new project" should { - "sanitize the name of the project" in { - implicit val fileSystem: FileSystem[File] = FileSystem.Default - val manager = new PackageManager() + "Name Validation" should { + + "sanitize the name of the project" in { + normalizeName("My_Project") shouldEqual "My_Project" + normalizeName("My___Project") shouldEqual "My_Project" + normalizeName("myProject") shouldEqual "My_Project" + normalizeName("myPro??^ject123") shouldEqual "My_Project_123" + normalizeName("???%$6543lib") shouldEqual "Project_6543_Lib" + } - manager.normalizeName("My_Project") shouldEqual "My_Project" - manager.normalizeName("My___Project") shouldEqual "My_Project" - manager.normalizeName("myProject") shouldEqual "My_Project" - manager.normalizeName("myPro??^ject123") shouldEqual "My_Project_123" - manager.normalizeName("???%$6543lib") shouldEqual "Project_6543_Lib" + "validate the project name" in { + validateName("My") shouldEqual Right("My") + validateName("My_Project") shouldEqual Right("My_Project") + validateName("") shouldEqual Left(InvalidNameError.Empty) + validateName("My___Project") shouldEqual Left( + InvalidNameError.ShouldBeUpperSnakeCase("My_Project") + ) + validateName("FooBar") shouldEqual Left( + InvalidNameError.ShouldBeUpperSnakeCase("Foo_Bar") + ) + validateName("myProject") shouldEqual Left( + InvalidNameError.ShouldStartWithCapitalLetter + ) + validateName("MyPro??^ject123") shouldEqual Left( + InvalidNameError.ContainsInvalidCharacters(ListSet('?', '^')) + ) } } + + private def normalizeName(name: String): String = + NameValidation.normalizeName(name) + + private def validateName(name: String): Either[InvalidNameError, String] = + NameValidation.validateName(name) } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/repository/ProjectFileRepository.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/repository/ProjectFileRepository.scala index d37f191572e4..6e85289384c9 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/repository/ProjectFileRepository.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/repository/ProjectFileRepository.scala @@ -48,7 +48,7 @@ class ProjectFileRepository[ override def exists( name: String ): F[ProjectRepositoryFailure, Boolean] = - getAll().map(_.exists(_.name == PackageManager.Default.normalizeName(name))) + getAll().map(_.exists(_.name == name)) /** @inheritdoc */ override def find( @@ -165,11 +165,10 @@ class ProjectFileRepository[ private def renamePackage( projectPath: File, - name: String + newName: String ): F[ProjectRepositoryFailure, Unit] = getPackage(projectPath) .flatMap { projectPackage => - val newName = PackageManager.Default.normalizeName(name) Sync[F] .blockingOp { projectPackage.rename(newName) } .map(_ => ()) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/MonadicProjectValidator.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/MonadicProjectValidator.scala index 024d5f974c4b..59a6ebab845a 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/MonadicProjectValidator.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/MonadicProjectValidator.scala @@ -2,42 +2,37 @@ package org.enso.projectmanager.service import cats.MonadError import cats.implicits._ -import org.enso.projectmanager.service.ValidationFailure.{ - EmptyName, - NameContainsForbiddenCharacter -} +import org.enso.pkg.validation.{InvalidNameError, NameValidation} -/** MTL implementation of the project validator. - */ +/** MTL implementation of the project validator. */ class MonadicProjectValidator[F[_, _]](implicit M: MonadError[F[ValidationFailure, *], ValidationFailure] ) extends ProjectValidator[F] { - import M._ - - private val validCharSpec: Char => Boolean = { char => - char.isLetterOrDigit || char == '_' || char == '-' - } - /** Validates a project name. * * @param name the project name * @return either validation failure or success */ override def validateName(name: String): F[ValidationFailure, Unit] = - checkIfNonEmptyName(name) *> checkCharacters(name) - - private def checkIfNonEmptyName(name: String): F[ValidationFailure, Unit] = - if (name.trim.isEmpty) { - raiseError(EmptyName) - } else { - unit + M.fromEither { + NameValidation + .validateName(name) + .leftMap(toValidationFailure) + .map(_ => ()) } - private def checkCharacters(name: String): F[ValidationFailure, Unit] = { - val forbiddenChars = name.toCharArray.filterNot(validCharSpec).toSet - if (forbiddenChars.isEmpty) unit - else raiseError(NameContainsForbiddenCharacter(forbiddenChars)) - } + /** Convert project name error to validation error. */ + private def toValidationFailure(error: InvalidNameError): ValidationFailure = + error match { + case InvalidNameError.Empty => + ValidationFailure.EmptyName + case InvalidNameError.ShouldStartWithCapitalLetter => + ValidationFailure.NameShouldStartWithCapitalLetter + case InvalidNameError.ContainsInvalidCharacters(invalidCharacters) => + ValidationFailure.NameContainsForbiddenCharacter(invalidCharacters) + case InvalidNameError.ShouldBeUpperSnakeCase(validName) => + ValidationFailure.NameShouldBeUpperSnakeCase(validName) + } } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala index 4097e95c3eb1..87936d7a1b24 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala @@ -5,7 +5,6 @@ import java.util.UUID import akka.actor.ActorRef import cats.MonadError import nl.gn0s1s.bump.SemVer -import org.enso.pkg.PackageManager import org.enso.projectmanager.control.core.{ Applicative, CovariantFlatMap, @@ -39,7 +38,9 @@ import org.enso.projectmanager.model.ProjectKind.UserProject import org.enso.projectmanager.service.ProjectServiceFailure._ import org.enso.projectmanager.service.ValidationFailure.{ EmptyName, - NameContainsForbiddenCharacter + NameContainsForbiddenCharacter, + NameShouldBeUpperSnakeCase, + NameShouldStartWithCapitalLetter } import org.enso.projectmanager.service.config.GlobalConfigServiceApi import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure @@ -129,19 +130,18 @@ class ProjectService[ /** @inheritdoc */ override def renameProject( projectId: UUID, - name: String + newPackage: String ): F[ProjectServiceFailure, Unit] = { for { - _ <- log.debug(s"Renaming project $projectId to $name.") - _ <- validateName(name) + _ <- log.debug(s"Renaming project $projectId to $newPackage.") + _ <- validateName(newPackage) _ <- checkIfProjectExists(projectId) - _ <- checkIfNameExists(name) + _ <- checkIfNameExists(newPackage) oldPackage <- repo.getPackageName(projectId).mapError(toServiceFailure) - _ <- repo.rename(projectId, name).mapError(toServiceFailure) - _ <- renameProjectDirOrRegisterShutdownHook(projectId, name) - newPackage = PackageManager.Default.normalizeName(name) - _ <- refactorProjectName(projectId, oldPackage, newPackage) - _ <- log.info(s"Project $projectId renamed.") + _ <- repo.rename(projectId, newPackage).mapError(toServiceFailure) + _ <- renameProjectDirOrRegisterShutdownHook(projectId, newPackage) + _ <- refactorProjectName(projectId, oldPackage, newPackage) + _ <- log.info(s"Project $projectId renamed.") } yield () } @@ -369,6 +369,14 @@ class ProjectService[ ProjectServiceFailure.ValidationFailure( s"Project name contains forbidden characters: ${chars.mkString(",")}" ) + case NameShouldStartWithCapitalLetter => + ProjectServiceFailure.ValidationFailure( + "Project name should start with a capital letter" + ) + case NameShouldBeUpperSnakeCase(validName) => + ProjectServiceFailure.ValidationFailure( + s"Project name should be in upper snake case: $validName" + ) } } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala index 9a520e09c05d..23443e166d95 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala @@ -10,6 +10,8 @@ object ValidationFailure { */ case object EmptyName extends ValidationFailure + case object NameShouldStartWithCapitalLetter extends ValidationFailure + /** Signals that a project name contains forbidden characters. * * @param characters a forbidden characters in the provided project name @@ -17,4 +19,6 @@ object ValidationFailure { case class NameContainsForbiddenCharacter(characters: Set[Char]) extends ValidationFailure + case class NameShouldBeUpperSnakeCase(validName: String) + extends ValidationFailure } diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateDefaultToLatestSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateDefaultToLatestSpec.scala index d891067e81c5..c84781ec5db6 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateDefaultToLatestSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateDefaultToLatestSpec.scala @@ -12,7 +12,7 @@ class ProjectCreateDefaultToLatestSpec extends BaseServerSpec { "method": "project/create", "id": 1, "params": { - "name": "testproj", + "name": "Testproj", "missingComponentAction": "Install" } } diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateMissingComponentsSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateMissingComponentsSpec.scala index 4d5b232a090a..047e01fa8417 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateMissingComponentsSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectCreateMissingComponentsSpec.scala @@ -22,7 +22,7 @@ class ProjectCreateMissingComponentsSpec "method": "project/create", "id": 1, "params": { - "name": "testproj", + "name": "Testproj", "missingComponentAction": $missingComponentAction, "version": $version } diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala index 8b90919c023a..878ed67832a7 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala @@ -48,14 +48,14 @@ class ProjectManagementApiSpec """) } - "validate project name" in { + "validate project name for forbidden characters" in { val client = new WsTestClient(address) client.send(json""" { "jsonrpc": "2.0", "method": "project/create", "id": 1, "params": { - "name": "enso-test-project4/#$$%^@!" + "name": "Enso-test-roject4/#$$%^@!" } } """) @@ -64,7 +64,51 @@ class ProjectManagementApiSpec "id":1, "error":{ "code":4001, - "message":"Project name contains forbidden characters: %,!,@,#,$$,^,/" + "message":"Project name contains forbidden characters: -,/,#,$$,%,^,@,!" + } + } + """) + } + + "validate project name should start with a capital letter" in { + val client = new WsTestClient(address) + client.send(json""" + { "jsonrpc": "2.0", + "method": "project/create", + "id": 1, + "params": { + "name": "enso-test-project" + } + } + """) + client.expectJson(json""" + {"jsonrpc":"2.0", + "id":1, + "error":{ + "code":4001, + "message":"Project name should start with a capital letter" + } + } + """) + } + + "validate project name should be in upper snake case" in { + val client = new WsTestClient(address) + client.send(json""" + { "jsonrpc": "2.0", + "method": "project/create", + "id": 1, + "params": { + "name": "EnsoTestProject" + } + } + """) + client.expectJson(json""" + {"jsonrpc":"2.0", + "id":1, + "error":{ + "code":4001, + "message":"Project name should be in upper snake case: Enso_Test_Project" } } """) @@ -77,7 +121,7 @@ class ProjectManagementApiSpec "method": "project/create", "id": 1, "params": { - "name": "foo" + "name": "Foo" } } """) @@ -95,7 +139,7 @@ class ProjectManagementApiSpec "method": "project/create", "id": 2, "params": { - "name": "foo" + "name": "Foo" } } """) @@ -112,7 +156,7 @@ class ProjectManagementApiSpec } "create project structure" in { - val projectName = "foo" + val projectName = "Foo" implicit val client = new WsTestClient(address) @@ -135,7 +179,7 @@ class ProjectManagementApiSpec "method": "project/create", "id": 1, "params": { - "name": "foo", + "name": "Foo", "version": "0.0.1" } } @@ -152,7 +196,7 @@ class ProjectManagementApiSpec } "create a project dir with a suffix if a directory is taken" in { - val projectName = "foo" + val projectName = "Foo" val projectDir = new File(userProjectDir, projectName) val projectDirWithSuffix1 = new File(userProjectDir, projectName + "_1") val projectDirWithSuffix2 = new File(userProjectDir, projectName + "_2") @@ -197,7 +241,7 @@ class ProjectManagementApiSpec "fail when project is running" taggedAs Flaky in { //given implicit val client = new WsTestClient(address) - val projectId = createProject("foo") + val projectId = createProject("Foo") openProject(projectId) //when client.send(json""" @@ -228,7 +272,7 @@ class ProjectManagementApiSpec "remove project structure" in { //given - val projectName = "to-remove" + val projectName = "To_Remove" implicit val client = new WsTestClient(address) val projectId = createProject(projectName) val projectDir = new File(userProjectDir, projectName) @@ -284,7 +328,7 @@ class ProjectManagementApiSpec "start the Language Server if not running" taggedAs Flaky in { //given - val projectName = "to-remove" + val projectName = "To_Remove" implicit val client = new WsTestClient(address) val projectId = createProject(projectName) //when @@ -318,7 +362,7 @@ class ProjectManagementApiSpec "not start new Language Server if one is running" taggedAs Flaky in { val client1 = new WsTestClient(address) - val projectId = createProject("foo")(client1) + val projectId = createProject("Foo")(client1) //when val socket1 = openProject(projectId)(client1) val client2 = new WsTestClient(address) @@ -351,7 +395,7 @@ class ProjectManagementApiSpec "start the Language Server after moving the directory" taggedAs Flaky in { //given - val projectName = "foo" + val projectName = "Foo" implicit val client = new WsTestClient(address) val projectId = createProject(projectName) @@ -432,7 +476,7 @@ class ProjectManagementApiSpec "close project when the requester is the only client" taggedAs Flaky in { //given implicit val client = new WsTestClient(address) - val projectId = createProject("foo") + val projectId = createProject("Foo") val socket = openProject(projectId) val languageServerClient = new WsTestClient(s"ws://${socket.host}:${socket.port}") @@ -464,11 +508,11 @@ class ProjectManagementApiSpec "return a list sorted by creation time if none of projects was opened" in { implicit val client = new WsTestClient(address) //given - val fooId = createProject("foo") + val fooId = createProject("Foo") testClock.moveTimeForward() - val barId = createProject("bar") + val barId = createProject("Bar") testClock.moveTimeForward() - val bazId = createProject("baz") + val bazId = createProject("Baz") //when client.send(json""" { "jsonrpc": "2.0", @@ -514,8 +558,8 @@ class ProjectManagementApiSpec "returned sorted list of recently opened projects" in { implicit val client = new WsTestClient(address) //given - val fooId = createProject("foo") - val barId = createProject("bar") + val fooId = createProject("Foo") + val barId = createProject("Bar") testClock.moveTimeForward() openProject(fooId) val fooOpenTime = testClock.currentTime @@ -523,7 +567,7 @@ class ProjectManagementApiSpec openProject(barId) val barOpenTime = testClock.currentTime testClock.moveTimeForward() - val bazId = createProject("baz") + val bazId = createProject("Baz") //when client.send(json""" { "jsonrpc": "2.0", @@ -578,7 +622,8 @@ class ProjectManagementApiSpec "rename a project and move project dir" in { implicit val client = new WsTestClient(address) //given - val projectId = createProject("foo") + val newProjectName = "Bar" + val projectId = createProject("Foo") //when client.send(json""" { "jsonrpc": "2.0", @@ -586,7 +631,7 @@ class ProjectManagementApiSpec "id": 0, "params": { "projectId": $projectId, - "name": "bar" + "name": $newProjectName } } """) @@ -598,7 +643,7 @@ class ProjectManagementApiSpec "result": null } """) - val projectDir = new File(userProjectDir, "bar") + val projectDir = new File(userProjectDir, newProjectName) val packageFile = new File(projectDir, "package.yaml") val buffer = Source.fromFile(packageFile) val lines = buffer.getLines() @@ -609,8 +654,8 @@ class ProjectManagementApiSpec } "create a project dir with a suffix if a directory is taken" taggedAs Flaky in { - val oldProjectName = "foobar" - val newProjectName = "foo" + val oldProjectName = "Foobar" + val newProjectName = "Foo" implicit val client = new WsTestClient(address) //given val projectId = createProject(oldProjectName) @@ -648,8 +693,10 @@ class ProjectManagementApiSpec "reply with an error when the project with the same name exists" in { //given implicit val client = new WsTestClient(address) - val projectId = createProject("foo") - val existingProjectId = createProject("bar") + val oldProjectName = "Foo" + val newProjectName = "Bar" + val projectId = createProject(oldProjectName) + val existingProjectId = createProject(newProjectName) //when client.send(json""" { "jsonrpc": "2.0", @@ -657,7 +704,7 @@ class ProjectManagementApiSpec "id": 0, "params": { "projectId": $projectId, - "name": "bar" + "name": $newProjectName } } """) @@ -687,7 +734,7 @@ class ProjectManagementApiSpec "id": 0, "params": { "projectId": ${UUID.randomUUID()}, - "name": "bar" + "name": "Bar" } } """) @@ -706,7 +753,7 @@ class ProjectManagementApiSpec "check if project name is not empty" in { //given implicit val client = new WsTestClient(address) - val projectId = createProject("foo") + val projectId = createProject("Foo") //when client.send(json""" { "jsonrpc": "2.0", @@ -729,10 +776,68 @@ class ProjectManagementApiSpec deleteProject(projectId) } - "validate project name" in { + "validate project name for forbidden characters" in { + //given + implicit val client = new WsTestClient(address) + val projectId = createProject("Foo") + //when + client.send(json""" + { "jsonrpc": "2.0", + "method": "project/rename", + "id": 0, + "params": { + "projectId": $projectId, + "name": "Enso-test-project4/#$$%^@!" + } + } + """) + //then + client.expectJson(json""" + {"jsonrpc":"2.0", + "id":0, + "error":{ + "code":4001, + "message":"Project name contains forbidden characters: -,/,#,$$,%,^,@,!" + } + } + """) + //teardown + deleteProject(projectId) + } + + "validate project name should start with a capital letter" in { + //given + implicit val client = new WsTestClient(address) + val projectId = createProject("Foo") + //when + client.send(json""" + { "jsonrpc": "2.0", + "method": "project/rename", + "id": 0, + "params": { + "projectId": $projectId, + "name": "enso-test-project" + } + } + """) + //then + client.expectJson(json""" + {"jsonrpc":"2.0", + "id":0, + "error":{ + "code":4001, + "message":"Project name should start with a capital letter" + } + } + """) + //teardown + deleteProject(projectId) + } + + "validate project name should be in upper snake case" in { //given implicit val client = new WsTestClient(address) - val projectId = createProject("foo") + val projectId = createProject("Foo") //when client.send(json""" { "jsonrpc": "2.0", @@ -740,7 +845,7 @@ class ProjectManagementApiSpec "id": 0, "params": { "projectId": $projectId, - "name": "luna-test-project4/#$$%^@!" + "name": "EnsoTestProject" } } """) @@ -750,7 +855,7 @@ class ProjectManagementApiSpec "id":0, "error":{ "code":4001, - "message":"Project name contains forbidden characters: %,!,@,#,$$,^,/" + "message":"Project name should be in upper snake case: Enso_Test_Project" } } """) diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenMissingComponentsSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenMissingComponentsSpec.scala index 328a5af18be2..1f9e351c2531 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenMissingComponentsSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenMissingComponentsSpec.scala @@ -30,12 +30,12 @@ class ProjectOpenMissingComponentsSpec val blackhole = system.actorOf(blackholeProps) val ordinaryAction = projectService.createUserProject( blackhole, - "proj1", + "Proj_1", ordinaryVersion, MissingComponentAction.Fail ) ordinaryProject = Runtime.default.unsafeRun(ordinaryAction) - val brokenName = "projbroken" + val brokenName = "Projbroken" val brokenAction = projectService.createUserProject( blackhole, brokenName, From c269be59b98104b55ec7ebc97f77b0a01f609c4a Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Thu, 11 Mar 2021 19:47:37 +0300 Subject: [PATCH 2/2] doc: add missing --- .../enso/pkg/validation/InvalidNameError.scala | 16 +++++++++++++++- .../org/enso/pkg/validation/NameValidation.scala | 5 +++++ .../service/ValidationFailure.scala | 11 +++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala index 199a2c498920..4c70524765cb 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/InvalidNameError.scala @@ -1,11 +1,25 @@ package org.enso.pkg.validation +/** Base trait for a project name validation errors. */ sealed trait InvalidNameError object InvalidNameError { - case object Empty extends InvalidNameError + /** Indicates that project name is empty. */ + case object Empty extends InvalidNameError + + /** Indicates that project name should start with a capital letter. */ case object ShouldStartWithCapitalLetter extends InvalidNameError + + /** Indicates that projet name contains invalid characters. + * + * @param invalidCharacters the list of invalid characters + */ case class ContainsInvalidCharacters(invalidCharacters: Set[Char]) extends InvalidNameError + + /** Indicates that project name should be in upper shane case. + * + * @param validName initial project name rewritten in upper snake case + */ case class ShouldBeUpperSnakeCase(validName: String) extends InvalidNameError } diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala index 26a327553576..aefb4dc74be9 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/validation/NameValidation.scala @@ -17,6 +17,11 @@ object NameValidation { toUpperSnakeCase(onlyAlphanumeric) } + /** Validate the project name. + * + * @param name the project name to validate + * @return either a validation error or a project name if it's valid + */ def validateName(name: String): Either[InvalidNameError, String] = if (name.isEmpty) { Left(InvalidNameError.Empty) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala index 23443e166d95..1403ec058c1e 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ValidationFailure.scala @@ -1,15 +1,14 @@ package org.enso.projectmanager.service -/** Base trait for validations failures. - */ +/** Base trait for validations failures. */ sealed trait ValidationFailure object ValidationFailure { - /** Signals that a user provided empty name. - */ + /** Signals that a user provided empty name. */ case object EmptyName extends ValidationFailure + /** Signals that project name should start with a capital letter. */ case object NameShouldStartWithCapitalLetter extends ValidationFailure /** Signals that a project name contains forbidden characters. @@ -19,6 +18,10 @@ object ValidationFailure { case class NameContainsForbiddenCharacter(characters: Set[Char]) extends ValidationFailure + /** Signals that project name should be in upper shake case. + * + * @param validName initial project name rewritten in upper snake case + */ case class NameShouldBeUpperSnakeCase(validName: String) extends ValidationFailure }