mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 00:52:09 +03:00
Add Support for Project Templates (#1902)
Add an ability to create a project from a template
This commit is contained in:
parent
bc96f0e05c
commit
8336a97931
@ -5,6 +5,11 @@
|
||||
- Added support for documenting modules directly
|
||||
([#1900](https://github.com/enso-org/enso/pull/1900)).
|
||||
|
||||
## Tooling
|
||||
|
||||
- Added support for creating projects from a template
|
||||
([#1902](https://github.com/enso-org/enso/pull/1902)).
|
||||
|
||||
# Enso 0.2.16 (2021-07-23)
|
||||
|
||||
## Interpreter/Runtime
|
||||
|
@ -336,6 +336,9 @@ interface ProjectCreateRequest {
|
||||
/** Name of the project to create. */
|
||||
name: String;
|
||||
|
||||
/** The name of the project template to create. */
|
||||
projectTemplate?: String;
|
||||
|
||||
/**
|
||||
* Enso Engine version to use for the project.
|
||||
*
|
||||
|
@ -72,6 +72,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
*/
|
||||
def newProject(
|
||||
name: String,
|
||||
projectTemplate: Option[String],
|
||||
path: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
useSystemJVM: Boolean,
|
||||
@ -90,6 +91,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
path = actualPath,
|
||||
name = name,
|
||||
engineVersion = version,
|
||||
projectTemplate = projectTemplate,
|
||||
authorName = globalConfig.authorName,
|
||||
authorEmail = globalConfig.authorEmail,
|
||||
additionalArguments = additionalArguments
|
||||
|
@ -57,11 +57,19 @@ object LauncherApplication {
|
||||
"PATH specifies where to create the project. If it is not specified, " +
|
||||
"a directory called PROJECT-NAME is created in the current directory."
|
||||
)
|
||||
def projectTemplate = {
|
||||
Opts.optionalParameter[String](
|
||||
"new-project-template",
|
||||
"TEMPLATE-NAME",
|
||||
"Specifies a project template when creating a project."
|
||||
)
|
||||
}
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
|
||||
(
|
||||
nameOpt,
|
||||
pathOpt,
|
||||
projectTemplate,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
@ -70,6 +78,7 @@ object LauncherApplication {
|
||||
(
|
||||
name,
|
||||
path,
|
||||
template,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
@ -78,6 +87,7 @@ object LauncherApplication {
|
||||
Launcher(config).newProject(
|
||||
name = name,
|
||||
path = path,
|
||||
projectTemplate = template,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
|
@ -124,6 +124,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
path = projectPath,
|
||||
name = "ProjectName",
|
||||
engineVersion = defaultEngineVersion,
|
||||
projectTemplate = None,
|
||||
authorName = Some(authorName),
|
||||
authorEmail = Some(authorEmail),
|
||||
additionalArguments = Seq(additionalArgument)
|
||||
@ -151,6 +152,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
path = projectPath,
|
||||
name = "ProjectName2",
|
||||
engineVersion = nightlyVersion,
|
||||
projectTemplate = None,
|
||||
authorName = None,
|
||||
authorEmail = None,
|
||||
additionalArguments = Seq()
|
||||
|
@ -8,13 +8,13 @@ import org.enso.editions.DefaultEdition
|
||||
import org.enso.languageserver.boot
|
||||
import org.enso.languageserver.boot.LanguageServerConfig
|
||||
import org.enso.loggingservice.LogLevel
|
||||
import org.enso.pkg.{Contact, PackageManager}
|
||||
import org.enso.pkg.{Contact, PackageManager, Template}
|
||||
import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext}
|
||||
import org.enso.version.VersionDescription
|
||||
import org.graalvm.polyglot.PolyglotException
|
||||
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
import scala.Console.err
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.Try
|
||||
@ -26,6 +26,7 @@ object Main {
|
||||
private val HELP_OPTION = "help"
|
||||
private val NEW_OPTION = "new"
|
||||
private val PROJECT_NAME_OPTION = "new-project-name"
|
||||
private val PROJECT_TEMPLATE_OPTION = "new-project-template"
|
||||
private val PROJECT_AUTHOR_NAME_OPTION = "new-project-author-name"
|
||||
private val PROJECT_AUTHOR_EMAIL_OPTION = "new-project-author-email"
|
||||
private val REPL_OPTION = "repl"
|
||||
@ -87,6 +88,15 @@ object Main {
|
||||
s"Specifies a project name when creating a project using --$NEW_OPTION."
|
||||
)
|
||||
.build
|
||||
val newProjectTemplateOpt = CliOption.builder
|
||||
.hasArg(true)
|
||||
.numberOfArgs(1)
|
||||
.argName("name")
|
||||
.longOpt(PROJECT_TEMPLATE_OPTION)
|
||||
.desc(
|
||||
s"Specifies a project template when creating a project using --$NEW_OPTION."
|
||||
)
|
||||
.build
|
||||
val newProjectAuthorNameOpt = CliOption.builder
|
||||
.hasArg(true)
|
||||
.numberOfArgs(1)
|
||||
@ -202,6 +212,7 @@ object Main {
|
||||
.addOption(docs)
|
||||
.addOption(newOpt)
|
||||
.addOption(newProjectNameOpt)
|
||||
.addOption(newProjectTemplateOpt)
|
||||
.addOption(newProjectAuthorNameOpt)
|
||||
.addOption(newProjectAuthorEmailOpt)
|
||||
.addOption(lsOption)
|
||||
@ -248,12 +259,14 @@ object Main {
|
||||
*
|
||||
* @param path root path of the newly created project
|
||||
* @param nameOption specifies the name of the created project
|
||||
* @param templateOption specifies the template of the created project
|
||||
* @param authorName if set, sets the name of the author and maintainer
|
||||
* @param authorEmail if set, sets the email of the author and maintainer
|
||||
*/
|
||||
private def createNew(
|
||||
path: String,
|
||||
nameOption: Option[String],
|
||||
templateOption: Option[String],
|
||||
authorName: Option[String],
|
||||
authorEmail: Option[String]
|
||||
): Unit = {
|
||||
@ -270,12 +283,22 @@ object Main {
|
||||
s"Creating a new project $name based on edition [$baseEdition]."
|
||||
)
|
||||
}
|
||||
|
||||
val template = templateOption
|
||||
.map { name =>
|
||||
Template.fromString(name).getOrElse {
|
||||
logger.error(s"Unknown project template name: '$name'.")
|
||||
exitFail()
|
||||
}
|
||||
}
|
||||
|
||||
PackageManager.Default.create(
|
||||
root = root,
|
||||
name = name,
|
||||
edition = Some(edition),
|
||||
authors = authors,
|
||||
maintainers = authors
|
||||
maintainers = authors,
|
||||
template = template.getOrElse(Template.Default)
|
||||
)
|
||||
exitSuccess()
|
||||
}
|
||||
@ -632,7 +655,8 @@ object Main {
|
||||
path = line.getOptionValue(NEW_OPTION),
|
||||
nameOption = Option(line.getOptionValue(PROJECT_NAME_OPTION)),
|
||||
authorName = Option(line.getOptionValue(PROJECT_AUTHOR_NAME_OPTION)),
|
||||
authorEmail = Option(line.getOptionValue(PROJECT_AUTHOR_EMAIL_OPTION))
|
||||
authorEmail = Option(line.getOptionValue(PROJECT_AUTHOR_EMAIL_OPTION)),
|
||||
templateOption = Option(line.getOptionValue(PROJECT_TEMPLATE_OPTION))
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,7 @@ package org.enso.interpreter.runtime.util;
|
||||
import com.oracle.truffle.api.TruffleFile;
|
||||
import org.enso.filesystem.FileSystem;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
@ -53,6 +51,16 @@ public class TruffleFileSystem implements FileSystem<TruffleFile> {
|
||||
return file.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream newInputStream(TruffleFile file) throws IOException {
|
||||
return file.newInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream newOutputStream(TruffleFile file) throws IOException {
|
||||
return file.newOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedWriter newBufferedWriter(TruffleFile file) throws IOException {
|
||||
return file.newBufferedWriter();
|
||||
|
1
lib/scala/pkg/src/main/resources/example/hello.txt
Normal file
1
lib/scala/pkg/src/main/resources/example/hello.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello World!
|
5
lib/scala/pkg/src/main/resources/example/src/Main.enso
Normal file
5
lib/scala/pkg/src/main/resources/example/src/Main.enso
Normal file
@ -0,0 +1,5 @@
|
||||
from Standard.Base import all
|
||||
|
||||
main =
|
||||
operator1 = Enso_Project.root / "hello.txt"
|
||||
File.read operator1
|
@ -1,7 +1,14 @@
|
||||
package org.enso.filesystem
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import java.io.{BufferedReader, BufferedWriter, File, IOException}
|
||||
import java.io.{
|
||||
BufferedReader,
|
||||
BufferedWriter,
|
||||
File,
|
||||
IOException,
|
||||
InputStream,
|
||||
OutputStream
|
||||
}
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.{BasicFileAttributes, FileTime}
|
||||
import java.util.stream
|
||||
@ -64,6 +71,22 @@ trait FileSystem[F] {
|
||||
*/
|
||||
def getName(file: F): String
|
||||
|
||||
/** Creates a new input stream for the given file.
|
||||
*
|
||||
* @param file the file to open.
|
||||
* @return an input stream for `file`.
|
||||
*/
|
||||
@throws[IOException]
|
||||
def newInputStream(file: F): InputStream
|
||||
|
||||
/** Creates a new output stream for the given file.
|
||||
*
|
||||
* @param file the file to open.
|
||||
* @return an output stream for `file`.
|
||||
*/
|
||||
@throws[IOException]
|
||||
def newOutputStream(file: F): OutputStream
|
||||
|
||||
/** Creates a new buffered writer for the given file.
|
||||
*
|
||||
* @param file the file to open.
|
||||
@ -140,6 +163,10 @@ object FileSystem {
|
||||
|
||||
def getName: String = fs.getName(file)
|
||||
|
||||
def newInputStream: InputStream = fs.newInputStream(file)
|
||||
|
||||
def newOutputStream: OutputStream = fs.newOutputStream(file)
|
||||
|
||||
def newBufferedWriter: BufferedWriter = fs.newBufferedWriter(file)
|
||||
|
||||
def newBufferedReader: BufferedReader = fs.newBufferedReader(file)
|
||||
@ -175,6 +202,12 @@ object FileSystem {
|
||||
|
||||
override def getName(file: File): String = file.getName
|
||||
|
||||
override def newInputStream(file: File): InputStream =
|
||||
Files.newInputStream(file.toPath)
|
||||
|
||||
override def newOutputStream(file: File): OutputStream =
|
||||
Files.newOutputStream(file.toPath)
|
||||
|
||||
override def newBufferedWriter(file: File): BufferedWriter =
|
||||
Files.newBufferedWriter(file.toPath)
|
||||
|
||||
|
@ -4,9 +4,9 @@ import cats.Show
|
||||
import org.enso.editions.{Editions, LibraryName}
|
||||
import org.enso.filesystem.FileSystem
|
||||
import org.enso.pkg.validation.NameValidation
|
||||
import java.io.{File, InputStream, OutputStream}
|
||||
import java.net.URI
|
||||
|
||||
import java.io.File
|
||||
import scala.io.Source
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.{Failure, Try, Using}
|
||||
|
||||
@ -48,7 +48,8 @@ case class Package[F](
|
||||
/** Stores the package metadata on the hard drive. If the package does not exist,
|
||||
* creates the required directory structure.
|
||||
*/
|
||||
def save(): Try[Unit] = for {
|
||||
def save(): Try[Unit] =
|
||||
for {
|
||||
_ <- Try {
|
||||
if (!root.exists) createDirectories()
|
||||
if (!sourceDir.exists) createSourceDir()
|
||||
@ -82,19 +83,14 @@ case class Package[F](
|
||||
*/
|
||||
def updateConfig(update: Config => Config): Package[F] = {
|
||||
val newPkg = copy(config = update(config))
|
||||
newPkg.save()
|
||||
newPkg.saveConfig()
|
||||
newPkg
|
||||
}
|
||||
|
||||
/** Creates the sources directory and populates it with a dummy Main file.
|
||||
/** Creates the sources directory.
|
||||
*/
|
||||
def createSourceDir(): Unit = {
|
||||
Try(sourceDir.createDirectories()).getOrElse(throw CouldNotCreateDirectory)
|
||||
val mainCodeSrc = Source.fromResource(Package.mainFileName)
|
||||
val writer = sourceDir.getChild(Package.mainFileName).newBufferedWriter
|
||||
writer.write(mainCodeSrc.mkString)
|
||||
writer.close()
|
||||
mainCodeSrc.close()
|
||||
}
|
||||
|
||||
/** Saves the config metadata into the package configuration file.
|
||||
@ -185,10 +181,12 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
*/
|
||||
def create(
|
||||
root: F,
|
||||
config: Config
|
||||
config: Config,
|
||||
template: Template
|
||||
): Package[F] = {
|
||||
val pkg = Package(root, config, fileSystem)
|
||||
pkg.save()
|
||||
copyResources(pkg, template)
|
||||
pkg
|
||||
}
|
||||
|
||||
@ -197,6 +195,7 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
* @param root the root location of the package.
|
||||
* @param name the name for the new package.
|
||||
* @param version version of the newly-created package.
|
||||
* @param template the template for the new package.
|
||||
* @param edition the edition to use for the project; if not specified, it
|
||||
* will not specify any, meaning that the current default one
|
||||
* will be used
|
||||
@ -207,6 +206,7 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
name: String,
|
||||
namespace: String = "local",
|
||||
version: String = "0.0.1",
|
||||
template: Template = Template.Default,
|
||||
edition: Option[Editions.RawEdition] = None,
|
||||
authors: List[Contact] = List(),
|
||||
maintainers: List[Contact] = List(),
|
||||
@ -222,7 +222,7 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
preferLocalLibraries = true,
|
||||
maintainers = maintainers
|
||||
)
|
||||
create(root, config)
|
||||
create(root, config, template)
|
||||
}
|
||||
|
||||
/** Tries to parse package structure from a given root location.
|
||||
@ -302,6 +302,51 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
val dirname = file.getName
|
||||
NameValidation.normalizeName(dirname)
|
||||
}
|
||||
|
||||
/** Copy the template resources to the package.
|
||||
*
|
||||
* @param template the template to copy the resources from
|
||||
* @param pkg the package to copy the resources to
|
||||
*/
|
||||
private def copyResources(pkg: Package[F], template: Template): Unit =
|
||||
template match {
|
||||
case Template.Default =>
|
||||
val mainEnsoPath = new URI(s"/default/src/${Package.mainFileName}")
|
||||
|
||||
copyResource(
|
||||
mainEnsoPath,
|
||||
pkg.sourceDir.getChild(Package.mainFileName)
|
||||
)
|
||||
|
||||
case Template.Example =>
|
||||
val helloTxtPath = new URI("/example/hello.txt")
|
||||
val mainEnsoPath = new URI(s"/example/src/${Package.mainFileName}")
|
||||
|
||||
copyResource(
|
||||
helloTxtPath,
|
||||
pkg.root.getChild("hello.txt")
|
||||
)
|
||||
copyResource(
|
||||
mainEnsoPath,
|
||||
pkg.sourceDir.getChild(Package.mainFileName)
|
||||
)
|
||||
}
|
||||
|
||||
/** Copy the resource to provided resource.
|
||||
*
|
||||
* @param from the source
|
||||
* @param to the destination
|
||||
*/
|
||||
private def copyResource(from: URI, to: F): Unit = {
|
||||
val fromStream = getClass.getResourceAsStream(from.toString)
|
||||
val toStream = to.newOutputStream
|
||||
try PackageManager.copyStream(fromStream, toStream)
|
||||
finally {
|
||||
fromStream.close()
|
||||
toStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PackageManager {
|
||||
@ -326,6 +371,16 @@ object PackageManager {
|
||||
/** The error indicating that the project name is invalid. */
|
||||
case class InvalidNameException(message: String)
|
||||
extends RuntimeException(message)
|
||||
|
||||
private def copyStream(in: InputStream, out: OutputStream): Unit = {
|
||||
val buffer = Array.ofDim[Byte](4096)
|
||||
var length = in.read(buffer)
|
||||
while (length != -1) {
|
||||
out.write(buffer, 0, length)
|
||||
length = in.read(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** A companion object for static methods on the [[Package]] class.
|
||||
|
30
lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala
Normal file
30
lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala
Normal file
@ -0,0 +1,30 @@
|
||||
package org.enso.pkg
|
||||
|
||||
/** Base trait for the project templates. */
|
||||
sealed trait Template {
|
||||
|
||||
/** The template name. */
|
||||
def name: String
|
||||
}
|
||||
object Template {
|
||||
|
||||
/** Create a template from string.
|
||||
*
|
||||
* @param template the template name
|
||||
* @return the template for the provided name
|
||||
*/
|
||||
def fromString(template: String): Option[Template] =
|
||||
allTemplates.find(_.name == template.toLowerCase)
|
||||
|
||||
/** The default project template. */
|
||||
case object Default extends Template {
|
||||
override val name = "default"
|
||||
}
|
||||
|
||||
/** The example project template. */
|
||||
case object Example extends Template {
|
||||
override val name = "example"
|
||||
}
|
||||
|
||||
val allTemplates = Seq(Default, Example)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package org.enso.projectmanager.protocol
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import io.circe.Json
|
||||
import io.circe.syntax._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
@ -24,6 +25,7 @@ object ProjectManagementApi {
|
||||
case class Params(
|
||||
name: String,
|
||||
version: Option[EnsoVersion],
|
||||
projectTemplate: Option[String],
|
||||
missingComponentAction: Option[MissingComponentAction]
|
||||
)
|
||||
|
||||
|
@ -56,6 +56,7 @@ class ProjectCreateHandler[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
|
||||
progressTracker = self,
|
||||
name = params.name,
|
||||
engineVersion = actualVersion,
|
||||
projectTemplate = params.projectTemplate,
|
||||
missingComponentAction = missingComponentAction
|
||||
)
|
||||
} yield ProjectCreate.Result(projectId)
|
||||
|
@ -14,7 +14,6 @@ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFa
|
||||
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
|
||||
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
|
||||
import org.enso.runtimeversionmanager.runner.Runner
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/** A service for creating new project structures using the runner of the
|
||||
@ -35,6 +34,7 @@ class ProjectCreationService[
|
||||
path: Path,
|
||||
name: String,
|
||||
engineVersion: SemVer,
|
||||
projectTemplate: Option[String],
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, Unit] = Sync[F]
|
||||
.blockingOp {
|
||||
@ -57,7 +57,17 @@ class ProjectCreationService[
|
||||
)
|
||||
|
||||
val settings =
|
||||
runner.newProject(path, name, engineVersion, None, None, Seq()).get
|
||||
runner
|
||||
.newProject(
|
||||
path,
|
||||
name,
|
||||
engineVersion,
|
||||
projectTemplate,
|
||||
None,
|
||||
None,
|
||||
Seq()
|
||||
)
|
||||
.get
|
||||
val jvmSettings = distributionConfiguration.defaultJVMSettings
|
||||
runner.withCommand(settings, jvmSettings) { command =>
|
||||
logger.trace(
|
||||
|
@ -18,6 +18,7 @@ trait ProjectCreationServiceApi[F[+_, +_]] {
|
||||
* @param path path at which to create the project
|
||||
* @param name name of the project
|
||||
* @param engineVersion version of the engine this project is meant for
|
||||
* @param projectTemplate the name of the project template
|
||||
* @param missingComponentAction specifies how to handle missing components
|
||||
*/
|
||||
def createProject(
|
||||
@ -25,6 +26,7 @@ trait ProjectCreationServiceApi[F[+_, +_]] {
|
||||
path: Path,
|
||||
name: String,
|
||||
engineVersion: SemVer,
|
||||
projectTemplate: Option[String],
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, Unit]
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ import org.enso.projectmanager.service.config.GlobalConfigServiceApi
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
|
||||
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
/** Implementation of business logic for project management.
|
||||
@ -82,12 +81,19 @@ class ProjectService[
|
||||
/** @inheritdoc */
|
||||
override def createUserProject(
|
||||
progressTracker: ActorRef,
|
||||
name: String,
|
||||
projectName: String,
|
||||
engineVersion: SemVer,
|
||||
projectTemplate: Option[String],
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, UUID] = for {
|
||||
projectId <- gen.randomUUID()
|
||||
_ <- log.debug("Creating project [{}, {}].", name, projectId)
|
||||
_ <- log.debug(
|
||||
"Creating project [{}, {}, {}].",
|
||||
projectName,
|
||||
projectId,
|
||||
projectTemplate
|
||||
)
|
||||
name <- getNameForNewProject(projectName, projectTemplate)
|
||||
_ <- validateName(name)
|
||||
_ <- checkIfNameExists(name)
|
||||
creationTime <- clock.nowInUtc()
|
||||
@ -111,6 +117,7 @@ class ProjectService[
|
||||
path,
|
||||
name,
|
||||
engineVersion,
|
||||
projectTemplate,
|
||||
missingComponentAction
|
||||
)
|
||||
_ <- log.debug(
|
||||
@ -481,4 +488,35 @@ class ProjectService[
|
||||
)
|
||||
}
|
||||
|
||||
private def getNameForNewProject(
|
||||
projectName: String,
|
||||
projectTemplate: Option[String]
|
||||
): F[ProjectServiceFailure, String] = {
|
||||
def mkName(name: String, suffix: Int): String =
|
||||
s"${name}_${suffix}"
|
||||
def findAvailableName(
|
||||
projectName: String,
|
||||
suffix: Int
|
||||
): F[ProjectRepositoryFailure, String] = {
|
||||
val newName = mkName(projectName, suffix)
|
||||
CovariantFlatMap[F].ifM(repo.exists(newName))(
|
||||
ifTrue = findAvailableName(projectName, suffix + 1),
|
||||
ifFalse = CovariantFlatMap[F].pure(newName)
|
||||
)
|
||||
}
|
||||
|
||||
projectTemplate match {
|
||||
case Some(_) =>
|
||||
CovariantFlatMap[F]
|
||||
.ifM(repo.exists(projectName))(
|
||||
ifTrue = findAvailableName(projectName, 1),
|
||||
ifFalse = CovariantFlatMap[F].pure(projectName)
|
||||
)
|
||||
.mapError(toServiceFailure)
|
||||
case None =>
|
||||
CovariantFlatMap[F].pure(projectName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,8 +19,9 @@ trait ProjectServiceApi[F[+_, +_]] {
|
||||
/** Creates a user project.
|
||||
*
|
||||
* @param progressTracker the actor to send progress updates to
|
||||
* @param name the name of th project
|
||||
* @param name the name of the project
|
||||
* @param engineVersion Enso version to use for the new project
|
||||
* @param projectTemplate the name of the project template
|
||||
* @param missingComponentAction specifies how to handle missing components
|
||||
* @return projectId
|
||||
*/
|
||||
@ -28,6 +29,7 @@ trait ProjectServiceApi[F[+_, +_]] {
|
||||
progressTracker: ActorRef,
|
||||
name: String,
|
||||
engineVersion: SemVer,
|
||||
projectTemplate: Option[String],
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, UUID]
|
||||
|
||||
|
@ -17,12 +17,17 @@ trait ProjectManagementOps { this: BaseServerSpec =>
|
||||
|
||||
def createProject(
|
||||
name: String,
|
||||
projectTemplate: Option[String] = None,
|
||||
missingComponentAction: Option[MissingComponentAction] = None
|
||||
)(implicit client: WsTestClient): UUID = {
|
||||
val fields = Seq("name" -> name.asJson) ++
|
||||
missingComponentAction
|
||||
.map(a => "missingComponentAction" -> a.asJson)
|
||||
.toSeq ++
|
||||
projectTemplate
|
||||
.map(t => "projectTemplate" -> t.asJson)
|
||||
.toSeq
|
||||
|
||||
val params = Json.obj(fields: _*)
|
||||
val request = json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
|
@ -6,10 +6,10 @@ import org.apache.commons.io.FileUtils
|
||||
import org.enso.editions.SemVerJson._
|
||||
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
|
||||
import org.enso.testkit.{FlakySpec, RetrySpec}
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.{Files, Paths}
|
||||
import java.util.UUID
|
||||
|
||||
import scala.io.Source
|
||||
|
||||
class ProjectManagementApiSpec
|
||||
@ -171,6 +171,56 @@ class ProjectManagementApiSpec
|
||||
meta shouldBe Symbol("file")
|
||||
}
|
||||
|
||||
"create project from default template" in {
|
||||
val projectName = "Foo"
|
||||
|
||||
implicit val client = new WsTestClient(address)
|
||||
|
||||
createProject(projectName, projectTemplate = Some("default"))
|
||||
|
||||
val projectDir = new File(userProjectDir, projectName)
|
||||
val packageFile = new File(projectDir, "package.yaml")
|
||||
val mainEnso = Paths.get(projectDir.toString, "src", "Main.enso").toFile
|
||||
val meta = Paths.get(projectDir.toString, ".enso", "project.json").toFile
|
||||
|
||||
packageFile shouldBe Symbol("file")
|
||||
mainEnso shouldBe Symbol("file")
|
||||
meta shouldBe Symbol("file")
|
||||
}
|
||||
|
||||
"create project from example template" in {
|
||||
val projectName = "Foo"
|
||||
|
||||
implicit val client = new WsTestClient(address)
|
||||
|
||||
createProject(projectName, projectTemplate = Some("example"))
|
||||
|
||||
val projectDir = new File(userProjectDir, projectName)
|
||||
val packageFile = new File(projectDir, "package.yaml")
|
||||
val mainEnso = Paths.get(projectDir.toString, "src", "Main.enso").toFile
|
||||
val helloTxt = Paths.get(projectDir.toString, "hello.txt").toFile
|
||||
val meta = Paths.get(projectDir.toString, ".enso", "project.json").toFile
|
||||
|
||||
packageFile shouldBe Symbol("file")
|
||||
mainEnso shouldBe Symbol("file")
|
||||
helloTxt shouldBe Symbol("file")
|
||||
meta shouldBe Symbol("file")
|
||||
}
|
||||
|
||||
"find a name when project is created from template" in {
|
||||
val projectName = "Foo"
|
||||
|
||||
implicit val client = new WsTestClient(address)
|
||||
|
||||
createProject(projectName, projectTemplate = Some("default"))
|
||||
createProject(projectName, projectTemplate = Some("default"))
|
||||
|
||||
val projectDir = new File(userProjectDir, "Foo_1")
|
||||
val packageFile = new File(projectDir, "package.yaml")
|
||||
|
||||
Files.readAllLines(packageFile.toPath) contains "name: Foo_1"
|
||||
}
|
||||
|
||||
"create project with specific version" in {
|
||||
implicit val client = new WsTestClient(address)
|
||||
client.send(json"""
|
||||
|
@ -27,18 +27,20 @@ abstract class ProjectOpenSpecBase
|
||||
|
||||
val blackhole = system.actorOf(blackholeProps)
|
||||
val ordinaryAction = projectService.createUserProject(
|
||||
blackhole,
|
||||
"Proj_1",
|
||||
defaultVersion,
|
||||
MissingComponentAction.Fail
|
||||
progressTracker = blackhole,
|
||||
projectName = "Proj_1",
|
||||
projectTemplate = None,
|
||||
engineVersion = defaultVersion,
|
||||
missingComponentAction = MissingComponentAction.Fail
|
||||
)
|
||||
ordinaryProject = Runtime.default.unsafeRun(ordinaryAction)
|
||||
val brokenName = "Projbroken"
|
||||
val brokenAction = projectService.createUserProject(
|
||||
blackhole,
|
||||
brokenName,
|
||||
defaultVersion,
|
||||
MissingComponentAction.Fail
|
||||
progressTracker = blackhole,
|
||||
projectName = brokenName,
|
||||
projectTemplate = None,
|
||||
engineVersion = defaultVersion,
|
||||
missingComponentAction = MissingComponentAction.Fail
|
||||
)
|
||||
brokenProject = Runtime.default.unsafeRun(brokenAction)
|
||||
|
||||
|
@ -44,6 +44,7 @@ class Runner(
|
||||
path: Path,
|
||||
name: String,
|
||||
engineVersion: SemVer,
|
||||
projectTemplate: Option[String],
|
||||
authorName: Option[String],
|
||||
authorEmail: Option[String],
|
||||
additionalArguments: Seq[String]
|
||||
@ -53,13 +54,15 @@ class Runner(
|
||||
authorName.map(Seq("--new-project-author-name", _)).getOrElse(Seq())
|
||||
val authorEmailOption =
|
||||
authorEmail.map(Seq("--new-project-author-email", _)).getOrElse(Seq())
|
||||
val templateOption =
|
||||
projectTemplate.map(Seq("--new-project-template", _)).getOrElse(Seq())
|
||||
val arguments =
|
||||
Seq(
|
||||
"--new",
|
||||
path.toAbsolutePath.normalize.toString,
|
||||
"--new-project-name",
|
||||
name
|
||||
) ++ authorNameOption ++ authorEmailOption ++ additionalArguments
|
||||
) ++ templateOption ++ authorNameOption ++ authorEmailOption ++ additionalArguments
|
||||
// TODO [RW] reporting warnings to the IDE (#1710)
|
||||
if (Engine.isNightly(engineVersion)) {
|
||||
Logger[Runner].warn(
|
||||
|
Loading…
Reference in New Issue
Block a user