mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 21:27:15 +03:00
Store Project Manager Metadata in Project Directory (#1120)
Project Manager to stores its metadata inside the project directory, instead of maintaining the global index. This will allow users to move and modify files inside the ~/enso directory.
This commit is contained in:
parent
2da720b1a9
commit
e92b9d0fc0
@ -296,8 +296,8 @@ val splainOptions = Seq(
|
||||
|
||||
// === ZIO ====================================================================
|
||||
|
||||
val zioVersion = "1.0.0-RC18-2"
|
||||
val zioInteropCatsVersion = "2.0.0.0-RC13"
|
||||
val zioVersion = "1.0.1"
|
||||
val zioInteropCatsVersion = "2.1.4.0"
|
||||
val zio = Seq(
|
||||
"dev.zio" %% "zio" % zioVersion,
|
||||
"dev.zio" %% "zio-interop-cats" % zioInteropCatsVersion
|
||||
|
@ -6,7 +6,7 @@ import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest}
|
||||
import akka.stream.OverflowStrategy
|
||||
import akka.stream.scaladsl.{Flow, Sink, Source}
|
||||
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
|
||||
import akka.testkit._
|
||||
import io.circe.{ACursor, Decoder, DecodingFailure, HCursor, Json}
|
||||
import io.circe.parser.parse
|
||||
import org.enso.jsonrpc.{ClientControllerFactory, JsonRpcServer, Protocol}
|
||||
@ -104,7 +104,7 @@ abstract class JsonRpcServerTestKit
|
||||
|
||||
def send(json: Json): Unit = send(json.noSpaces)
|
||||
|
||||
def expectMessage(timeout: FiniteDuration = 3.seconds): String =
|
||||
def expectMessage(timeout: FiniteDuration = 3.seconds.dilated): String =
|
||||
outActor.expectMsgClass[String](timeout, classOf[String])
|
||||
|
||||
def expectJson(json: Json): Assertion = {
|
||||
|
@ -32,11 +32,12 @@ project-manager {
|
||||
storage {
|
||||
projects-root = ${user.home}/enso
|
||||
projects-root=${?PROJECTS_ROOT}
|
||||
project-index-path = ${project-manager.storage.projects-root}/.enso/project-index.json
|
||||
temporary-projects-path = ${project-manager.storage.projects-root}/tmp
|
||||
user-projects-path = ${project-manager.storage.projects-root}/projects
|
||||
tutorials-path = ${project-manager.storage.projects-root}/tutorials
|
||||
tutorials-cache-path = ${project-manager.storage.projects-root}/.tutorials-cache
|
||||
project-metadata-directory = ".enso"
|
||||
project-metadata-file-name = "project.json"
|
||||
}
|
||||
|
||||
timeout {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.projectmanager.boot
|
||||
|
||||
import zio.{Has, ZEnv}
|
||||
import zio.{ExitCode, Has, ZEnv}
|
||||
import zio.blocking.Blocking
|
||||
import zio.clock.Clock
|
||||
import zio.console.Console
|
||||
@ -12,9 +12,9 @@ import zio.system.System
|
||||
*/
|
||||
object Globals {
|
||||
|
||||
val FailureExitCode = 1
|
||||
val FailureExitCode = ExitCode(1)
|
||||
|
||||
val SuccessExitCode = 0
|
||||
val SuccessExitCode = ExitCode(0)
|
||||
|
||||
val ConfigFilename = "application.conf"
|
||||
|
||||
|
@ -3,15 +3,11 @@ package org.enso.projectmanager.boot
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.SystemMaterializer
|
||||
import cats.MonadError
|
||||
import io.circe.generic.auto._
|
||||
import org.enso.jsonrpc.JsonRpcServer
|
||||
import org.enso.projectmanager.boot.configuration.ProjectManagerConfig
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{Async, ErrorChannel, Exec, Sync}
|
||||
import org.enso.projectmanager.infrastructure.file.{
|
||||
BlockingFileSystem,
|
||||
SynchronizedFileStorage
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.languageserver.{
|
||||
LanguageServerGatewayImpl,
|
||||
LanguageServerRegistry,
|
||||
@ -19,10 +15,7 @@ import org.enso.projectmanager.infrastructure.languageserver.{
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.log.Slf4jLogging
|
||||
import org.enso.projectmanager.infrastructure.random.SystemGenerator
|
||||
import org.enso.projectmanager.infrastructure.repository.{
|
||||
ProjectFileRepository,
|
||||
ProjectIndex
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepository
|
||||
import org.enso.projectmanager.infrastructure.time.RealClock
|
||||
import org.enso.projectmanager.protocol.{
|
||||
JsonRpc,
|
||||
@ -40,7 +33,9 @@ import scala.concurrent.ExecutionContext
|
||||
/**
|
||||
* A main module containing all components of the project manager.
|
||||
*/
|
||||
class MainModule[F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Async](
|
||||
class MainModule[
|
||||
F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Applicative: Async
|
||||
](
|
||||
config: ProjectManagerConfig,
|
||||
computeExecutionContext: ExecutionContext
|
||||
)(implicit
|
||||
@ -60,22 +55,18 @@ class MainModule[F[+_, +_]: Sync: ErrorChannel: Exec: CovariantFlatMap: Async](
|
||||
lazy val fileSystem =
|
||||
new BlockingFileSystem[F](config.timeout.ioTimeout)
|
||||
|
||||
lazy val indexStorage = new SynchronizedFileStorage[ProjectIndex, F](
|
||||
config.storage.projectIndexPath,
|
||||
fileSystem
|
||||
)
|
||||
lazy val gen = new SystemGenerator[F]
|
||||
|
||||
lazy val projectValidator = new MonadicProjectValidator[F]()
|
||||
|
||||
lazy val projectRepository =
|
||||
new ProjectFileRepository[F](
|
||||
config.storage,
|
||||
clock,
|
||||
fileSystem,
|
||||
indexStorage
|
||||
gen
|
||||
)
|
||||
|
||||
lazy val gen = new SystemGenerator[F]
|
||||
|
||||
lazy val projectValidator = new MonadicProjectValidator[F]()
|
||||
|
||||
lazy val languageServerRegistry =
|
||||
system.actorOf(
|
||||
LanguageServerRegistry
|
||||
|
@ -99,7 +99,7 @@ object ProjectManager extends App with LazyLogging {
|
||||
success = ZIO.succeed(_)
|
||||
)
|
||||
|
||||
override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] = {
|
||||
override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = {
|
||||
Cli.parse(args.toArray) match {
|
||||
case Right(opts) =>
|
||||
runOpts(opts)
|
||||
@ -114,7 +114,7 @@ object ProjectManager extends App with LazyLogging {
|
||||
* The main function of the application, which will be passed the command-line
|
||||
* arguments to the program and has to return an `IO` with the errors fully handled.
|
||||
*/
|
||||
def runOpts(options: CommandLine): ZIO[ZEnv, Nothing, Int] = {
|
||||
def runOpts(options: CommandLine): ZIO[ZEnv, Nothing, ExitCode] = {
|
||||
if (options.hasOption(Cli.HELP_OPTION)) {
|
||||
ZIO.effectTotal(Cli.printHelp()) *>
|
||||
ZIO.succeed(SuccessExitCode)
|
||||
@ -147,7 +147,9 @@ object ProjectManager extends App with LazyLogging {
|
||||
}
|
||||
}
|
||||
|
||||
private def displayVersion(useJson: Boolean): ZIO[Console, Nothing, Int] = {
|
||||
private def displayVersion(
|
||||
useJson: Boolean
|
||||
): ZIO[Console, Nothing, ExitCode] = {
|
||||
val versionDescription = VersionDescription.make(
|
||||
"Enso Project Manager",
|
||||
includeRuntimeJVMInfo = true
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.enso.projectmanager.boot
|
||||
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import zio.Cause
|
||||
import zio.{Cause, Supervisor}
|
||||
import zio.internal.stacktracer.Tracer
|
||||
import zio.internal.stacktracer.impl.AkkaLineNumbersTracer
|
||||
import zio.internal.tracing.TracingConfig
|
||||
@ -41,4 +41,6 @@ class ZioPlatform(computeExecutionContext: ExecutionContext)
|
||||
if (cause.died)
|
||||
logger.error(cause.prettyPrint)
|
||||
|
||||
override def supervisor: Supervisor[Any] =
|
||||
Supervisor.none
|
||||
}
|
||||
|
@ -32,13 +32,16 @@ object configuration {
|
||||
* A configuration object for properties of project storage.
|
||||
*
|
||||
* @param projectsRoot a project root
|
||||
* @param projectIndexPath a path to the index
|
||||
* @param userProjectsPath a user project root
|
||||
* @param projectMetadataDirectory a directory name containing project
|
||||
* metadata
|
||||
* @param projectMetadataFileName a name of project metadata file
|
||||
*/
|
||||
case class StorageConfig(
|
||||
projectsRoot: File,
|
||||
projectIndexPath: File,
|
||||
userProjectsPath: File
|
||||
userProjectsPath: File,
|
||||
projectMetadataDirectory: String,
|
||||
projectMetadataFileName: String
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,43 @@
|
||||
package org.enso.projectmanager.control.core
|
||||
|
||||
import zio.ZIO
|
||||
|
||||
/**
|
||||
* An applicative functor.
|
||||
*/
|
||||
trait Applicative[F[+_, +_]] {
|
||||
|
||||
/**
|
||||
* Lifts value into the applicative functor.
|
||||
*/
|
||||
def pure[E, A](a: A): F[E, A]
|
||||
|
||||
/**
|
||||
* Applies the function in the applicative context to the given value.
|
||||
*/
|
||||
def ap[E, E1 >: E, A, B](ff: F[E, A => B])(fa: F[E1, A]): F[E1, B]
|
||||
|
||||
/**
|
||||
* Applies function f to the given value.
|
||||
*/
|
||||
def map[E, A, B](fa: F[E, A])(f: A => B): F[E, B]
|
||||
|
||||
/**
|
||||
* Applies the binary function to the effectful falues ma and mb.
|
||||
*/
|
||||
def map2[E, A, B, C](ma: F[E, A], mb: F[E, B])(f: (A, B) => C): F[E, C]
|
||||
}
|
||||
|
||||
object Applicative {
|
||||
|
||||
def apply[F[+_, +_]](implicit applicative: Applicative[F]): Applicative[F] =
|
||||
applicative
|
||||
|
||||
implicit def zioApplicative[R]: Applicative[ZIO[R, +*, +*]] =
|
||||
zioApplicativeInstance.asInstanceOf[Applicative[ZIO[R, +*, +*]]]
|
||||
|
||||
final private[this] val zioApplicativeInstance
|
||||
: Applicative[ZIO[Any, +*, +*]] =
|
||||
new ZioApplicative[Any]
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.enso.projectmanager.control.core
|
||||
|
||||
class ApplicativeFunctionOps[F[+_, +_]: Applicative, E, A, B](
|
||||
ff: F[E, A => B]
|
||||
) {
|
||||
|
||||
/** Alias for [[Applicative.ap]]. */
|
||||
def <*>[E1 <: E](fa: F[E1, A]): F[E, B] =
|
||||
Applicative[F].ap(ff)(fa)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.enso.projectmanager.control.core
|
||||
|
||||
class ApplicativeOps[F[+_, +_]: Applicative, E, A](fa: F[E, A]) {
|
||||
|
||||
/** Compose two actions, discarding any value produced by the first. */
|
||||
def *>[E1 >: E, B](fb: F[E, B]): F[E1, B] =
|
||||
Applicative[F].ap(Applicative[F].map(fa)(_ => (b: B) => b))(fb)
|
||||
|
||||
/** Compose two actions, discarding any value produced by the second. */
|
||||
def <*[E1 >: E, B](fb: F[E1, B]): F[E1, A] =
|
||||
Applicative[F].ap(Applicative[F].map(fb)(_ => (a: A) => a))(fa)
|
||||
}
|
@ -8,7 +8,7 @@ import scala.util.{Either, Left, Right}
|
||||
* A class for covariant effects containing error channel used to chain
|
||||
* computations.
|
||||
*
|
||||
* @tparam F a effectful context
|
||||
* @tparam F an effectful context
|
||||
*/
|
||||
trait CovariantFlatMap[F[+_, +_]] {
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
package org.enso.projectmanager.control.core
|
||||
|
||||
/**
|
||||
* A traversal over a structure with effects.
|
||||
*/
|
||||
trait Traverse[G[_]] {
|
||||
|
||||
/**
|
||||
* Applies the function `f` to each element of the collection.
|
||||
*
|
||||
* @param s a collection of elements
|
||||
* @param f the mapping function
|
||||
* @return the new collection with the function applied to each element
|
||||
*/
|
||||
def traverse[F[+_, +_]: Applicative, E, A, B](s: G[A])(
|
||||
f: A => F[E, B]
|
||||
): F[E, G[B]]
|
||||
}
|
||||
|
||||
object Traverse {
|
||||
|
||||
def apply[G[_]](implicit traverse: Traverse[G]): Traverse[G] =
|
||||
traverse
|
||||
|
||||
implicit object ListTraverse extends Traverse[List] {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def traverse[F[+_, +_]: Applicative, E, A, B](
|
||||
xs: List[A]
|
||||
)(f: A => F[E, B]): F[E, List[B]] =
|
||||
xs.foldRight(Applicative[F].pure[E, List[B]](List())) { (a, facc) =>
|
||||
Applicative[F].map2(f(a), facc)(_ :: _)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.enso.projectmanager.control.core
|
||||
|
||||
import zio.ZIO
|
||||
|
||||
/**
|
||||
* An instance of [[Applicative]] class for ZIO.
|
||||
*/
|
||||
private[core] class ZioApplicative[R] extends Applicative[ZIO[R, +*, +*]] {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def pure[E, A](a: A): ZIO[R, E, A] =
|
||||
ZIO.succeed(a)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def ap[E, E1 >: E, A, B](
|
||||
ff: ZIO[R, E, A => B]
|
||||
)(fa: ZIO[R, E1, A]): ZIO[R, E1, B] =
|
||||
ff.flatMap(f => fa.map(f))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def map[E, A, B](fa: ZIO[R, E, A])(f: A => B): ZIO[R, E, B] =
|
||||
fa.map(f)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def map2[E, A, B, C](ma: ZIO[R, E, A], mb: ZIO[R, E, B])(
|
||||
f: (A, B) => C
|
||||
): ZIO[R, E, C] =
|
||||
ma.flatMap(a => mb.map(b => f(a, b)))
|
||||
}
|
@ -8,10 +8,10 @@ import zio.ZIO
|
||||
private[core] class ZioCovariantFlatMap[R]
|
||||
extends CovariantFlatMap[ZIO[R, +*, +*]] {
|
||||
|
||||
/** @inheritdoc **/
|
||||
/** @inheritdoc */
|
||||
override def pure[A](value: A): ZIO[R, Nothing, A] = ZIO.succeed(value)
|
||||
|
||||
/** @inheritdoc **/
|
||||
/** @inheritdoc */
|
||||
override def flatMap[E1, E2 >: E1, A, B](
|
||||
fa: ZIO[R, E1, A]
|
||||
)(f: A => ZIO[R, E2, B]): ZIO[R, E2, B] =
|
||||
|
@ -12,4 +12,19 @@ object syntax {
|
||||
fa: F[E, A]
|
||||
): CovariantFlatMapOps[F, E, A] = new CovariantFlatMapOps[F, E, A](fa)
|
||||
|
||||
/**
|
||||
* Implicit conversion to [[ApplicativeOps]].
|
||||
*/
|
||||
implicit def toApplicativeOps[F[+_, +_]: Applicative, E, A](
|
||||
fa: F[E, A]
|
||||
): ApplicativeOps[F, E, A] =
|
||||
new ApplicativeOps[F, E, A](fa)
|
||||
|
||||
/**
|
||||
* Implicit conversion to [[ApplicativeFunctionOps]].
|
||||
*/
|
||||
implicit def toApplicativeFunctionOps[F[+_, +_]: Applicative, E, A, B](
|
||||
ff: F[E, A => B]
|
||||
): ApplicativeFunctionOps[F, E, A, B] =
|
||||
new ApplicativeFunctionOps[F, E, A, B](ff)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package org.enso.projectmanager.infrastructure.file
|
||||
import java.io.{File, FileNotFoundException}
|
||||
import java.nio.file.{AccessDeniedException, NoSuchFileException}
|
||||
import java.nio.file.{
|
||||
AccessDeniedException,
|
||||
NoSuchFileException,
|
||||
NotDirectoryException
|
||||
}
|
||||
|
||||
import org.apache.commons.io.{FileExistsException, FileUtils}
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
@ -60,7 +64,7 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel](
|
||||
.mapError(toFsFailure)
|
||||
.timeoutFail(OperationTimeout)(ioTimeout)
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def move(from: File, to: File): F[FileSystemFailure, Unit] =
|
||||
Sync[F]
|
||||
.blockingOp {
|
||||
@ -75,14 +79,28 @@ class BlockingFileSystem[F[+_, +_]: Sync: ErrorChannel](
|
||||
}
|
||||
.mapError(toFsFailure)
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def exists(file: File): F[FileSystemFailure, Boolean] =
|
||||
Sync[F]
|
||||
.blockingOp(file.exists())
|
||||
.mapError(toFsFailure)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def list(directory: File): F[FileSystemFailure, List[File]] =
|
||||
Sync[F]
|
||||
.blockingOp {
|
||||
val res = directory.listFiles()
|
||||
if (res eq null) {
|
||||
throw new NotDirectoryException(s"Not a directory: $directory")
|
||||
} else {
|
||||
res.toList
|
||||
}
|
||||
}
|
||||
.mapError(toFsFailure)
|
||||
|
||||
private val toFsFailure: Throwable => FileSystemFailure = {
|
||||
case _: FileNotFoundException => FileNotFound
|
||||
case _: NotDirectoryException => NotDirectory
|
||||
case _: NoSuchFileException => FileNotFound
|
||||
case _: FileExistsException => FileExists
|
||||
case _: AccessDeniedException => AccessDenied
|
||||
|
@ -54,4 +54,11 @@ trait FileSystem[F[+_, +_]] {
|
||||
*/
|
||||
def exists(file: File): F[FileSystemFailure, Boolean]
|
||||
|
||||
/**
|
||||
* List files in the directory.
|
||||
*
|
||||
* @param directory a path to the directory
|
||||
* @return the directory contents
|
||||
*/
|
||||
def list(directory: File): F[FileSystemFailure, List[File]]
|
||||
}
|
||||
|
@ -17,6 +17,11 @@ object FileSystemFailure {
|
||||
*/
|
||||
case object FileNotFound extends FileSystemFailure
|
||||
|
||||
/**
|
||||
* Signals that the file is not a directory.
|
||||
*/
|
||||
case object NotDirectory extends FileSystemFailure
|
||||
|
||||
/**
|
||||
* Signals that the file already exists.
|
||||
*/
|
||||
|
@ -8,10 +8,8 @@ import io.circe.{Decoder, Encoder}
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Semaphore, Sync}
|
||||
import org.enso.projectmanager.data.Default
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.file.FileStorage._
|
||||
import org.enso.projectmanager.infrastructure.file.FileSystemFailure.FileNotFound
|
||||
import shapeless.{:+:, CNil, _}
|
||||
|
||||
/**
|
||||
@ -22,13 +20,14 @@ import shapeless.{:+:, CNil, _}
|
||||
* @param fileSystem a filesystem algebra
|
||||
* @tparam A a datatype to store
|
||||
*/
|
||||
class SynchronizedFileStorage[A: Encoder: Decoder: Default, F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
class JsonFileStorage[
|
||||
A: Encoder: Decoder,
|
||||
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap
|
||||
](
|
||||
path: File,
|
||||
fileSystem: FileSystem[F]
|
||||
) extends FileStorage[A, F] {
|
||||
|
||||
private val semaphore = Semaphore.unsafeMake[F](1)
|
||||
|
||||
/**
|
||||
* Loads the serialized object from the file.
|
||||
*
|
||||
@ -39,9 +38,6 @@ class SynchronizedFileStorage[A: Encoder: Decoder: Default, F[+_, +_]: Sync: Err
|
||||
.readFile(path)
|
||||
.mapError(Coproduct[LoadFailure](_))
|
||||
.flatMap(tryDecodeFileContents)
|
||||
.recover {
|
||||
case Inr(Inl(FileNotFound)) => Default[A].value
|
||||
}
|
||||
|
||||
private def tryDecodeFileContents(contents: String): F[LoadFailure, A] = {
|
||||
decode[A](contents) match {
|
||||
@ -76,13 +72,11 @@ class SynchronizedFileStorage[A: Encoder: Decoder: Default, F[+_, +_]: Sync: Err
|
||||
f: A => (A, B)
|
||||
): F[CannotDecodeData :+: FileSystemFailure :+: CNil, B] =
|
||||
// format: off
|
||||
semaphore.withPermit {
|
||||
for {
|
||||
index <- load()
|
||||
(updated, output) = f(index)
|
||||
_ <- persist(updated).mapError(Coproduct[LoadFailure](_))
|
||||
} yield output
|
||||
}
|
||||
for {
|
||||
index <- load()
|
||||
(updated, output) = f(index)
|
||||
_ <- persist(updated).mapError(Coproduct[LoadFailure](_))
|
||||
} yield output
|
||||
// format: on
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package org.enso.projectmanager.infrastructure.repository
|
||||
|
||||
import java.io.File
|
||||
|
||||
import io.circe.generic.auto._
|
||||
import org.enso.projectmanager.boot.configuration.StorageConfig
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.file.FileStorage.{
|
||||
CannotDecodeData,
|
||||
LoadFailure
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.file.FileSystemFailure.FileNotFound
|
||||
import org.enso.projectmanager.infrastructure.file.{
|
||||
FileStorage,
|
||||
FileSystem,
|
||||
FileSystemFailure,
|
||||
JsonFileStorage
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.random.Generator
|
||||
import org.enso.projectmanager.infrastructure.time.Clock
|
||||
import org.enso.projectmanager.model.{ProjectKind, ProjectMetadata}
|
||||
import shapeless.{Coproduct, Inl, Inr}
|
||||
|
||||
/**
|
||||
* File based implementation of the project metadata storage.
|
||||
*
|
||||
* @param directory a project directory
|
||||
* @param storageConfig a storage config
|
||||
* @param clock a clock
|
||||
* @param fileSystem a file system abstraction
|
||||
* @param gen a random generator
|
||||
*/
|
||||
final class MetadataFileStorage[
|
||||
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap
|
||||
](
|
||||
directory: File,
|
||||
storageConfig: StorageConfig,
|
||||
clock: Clock[F],
|
||||
fileSystem: FileSystem[F],
|
||||
gen: Generator[F]
|
||||
) extends FileStorage[ProjectMetadata, F] {
|
||||
|
||||
private val file = new JsonFileStorage[ProjectMetadata, F](
|
||||
metadataPath(directory),
|
||||
fileSystem
|
||||
)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def load(): F[LoadFailure, ProjectMetadata] =
|
||||
file
|
||||
.load()
|
||||
.recoverWith {
|
||||
case Inl(CannotDecodeData(_)) =>
|
||||
init.mapError(Coproduct[LoadFailure](_))
|
||||
case Inr(Inl(FileNotFound)) =>
|
||||
init.mapError(Coproduct[LoadFailure](_))
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def persist(metadata: ProjectMetadata): F[FileSystemFailure, Unit] =
|
||||
file.persist(metadata)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def modify[A](
|
||||
f: ProjectMetadata => (ProjectMetadata, A)
|
||||
): F[LoadFailure, A] =
|
||||
file.modify(f)
|
||||
|
||||
private def init: F[FileSystemFailure, ProjectMetadata] =
|
||||
for {
|
||||
metadata <- freshMetadata
|
||||
_ <- file.persist(metadata)
|
||||
} yield metadata
|
||||
|
||||
private def freshMetadata: F[FileSystemFailure, ProjectMetadata] =
|
||||
for {
|
||||
now <- clock.nowInUtc()
|
||||
projectId <- gen.randomUUID()
|
||||
} yield ProjectMetadata(
|
||||
id = projectId,
|
||||
kind = ProjectKind.UserProject,
|
||||
created = now,
|
||||
lastOpened = None
|
||||
)
|
||||
|
||||
private def metadataPath(project: File): File =
|
||||
new File(
|
||||
project,
|
||||
new File(
|
||||
storageConfig.projectMetadataDirectory,
|
||||
storageConfig.projectMetadataFileName
|
||||
).toString
|
||||
)
|
||||
}
|
@ -5,86 +5,103 @@ import java.util.UUID
|
||||
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
import org.enso.projectmanager.boot.configuration.StorageConfig
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.{
|
||||
Applicative,
|
||||
CovariantFlatMap,
|
||||
Traverse
|
||||
}
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.infrastructure.file.{FileStorage, FileSystem}
|
||||
import org.enso.projectmanager.infrastructure.file.FileSystem
|
||||
import org.enso.projectmanager.infrastructure.file.FileSystemFailure.{
|
||||
FileNotFound,
|
||||
NotDirectory
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.random.Generator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailure.{
|
||||
InconsistentStorage,
|
||||
ProjectNotFoundInIndex,
|
||||
StorageFailure
|
||||
}
|
||||
import org.enso.projectmanager.model.Project
|
||||
import org.enso.projectmanager.infrastructure.time.Clock
|
||||
import org.enso.projectmanager.model.{Project, ProjectMetadata}
|
||||
|
||||
/**
|
||||
* File based implementation of the project repository.
|
||||
*
|
||||
* @param storageConfig a storage config
|
||||
* @param clock a clock
|
||||
* @param fileSystem a file system abstraction
|
||||
* @param indexStorage an index storage
|
||||
* @param gen a random generator
|
||||
*/
|
||||
class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
class ProjectFileRepository[
|
||||
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap: Applicative
|
||||
](
|
||||
storageConfig: StorageConfig,
|
||||
clock: Clock[F],
|
||||
fileSystem: FileSystem[F],
|
||||
indexStorage: FileStorage[ProjectIndex, F]
|
||||
gen: Generator[F]
|
||||
) extends ProjectRepository[F] {
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def exists(
|
||||
name: String
|
||||
): F[ProjectRepositoryFailure, Boolean] =
|
||||
indexStorage
|
||||
.load()
|
||||
.map(_.exists(name))
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
getAll().map(_.exists(_.name == PackageManager.Default.normalizeName(name)))
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def find(
|
||||
predicate: Project => Boolean
|
||||
): F[ProjectRepositoryFailure, List[Project]] =
|
||||
indexStorage
|
||||
.load()
|
||||
.map(_.find(predicate))
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
getAll().map(_.filter(predicate))
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def getAll(): F[ProjectRepositoryFailure, List[Project]] =
|
||||
indexStorage
|
||||
.load()
|
||||
.map(_.projects.values.toList)
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
fileSystem
|
||||
.list(storageConfig.userProjectsPath)
|
||||
.recover {
|
||||
case FileNotFound | NotDirectory => Nil
|
||||
}
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
.flatMap(s => Traverse[List].traverse(s)(loadProject).map(_.flatten))
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def findById(
|
||||
projectId: UUID
|
||||
): F[ProjectRepositoryFailure, Option[Project]] =
|
||||
indexStorage
|
||||
.load()
|
||||
.map(_.findById(projectId))
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
getAll().map(_.find(_.id == projectId))
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def create(
|
||||
project: Project
|
||||
): F[ProjectRepositoryFailure, Unit] =
|
||||
// format: off
|
||||
for {
|
||||
projectPath <- findTargetPath(project)
|
||||
projectWithPath = project.copy(path = Some(projectPath.toString))
|
||||
_ <- createProjectStructure(project, projectPath)
|
||||
_ <- update(projectWithPath)
|
||||
projectPath <- findTargetPath(project)
|
||||
_ <- createProjectStructure(project, projectPath)
|
||||
_ <- metadataStorage(projectPath)
|
||||
.persist(ProjectMetadata(project))
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
} yield ()
|
||||
// format: on
|
||||
|
||||
/** @inheritdoc * */
|
||||
override def update(project: Project): F[ProjectRepositoryFailure, Unit] =
|
||||
indexStorage
|
||||
.modify { index =>
|
||||
val updated = index.upsert(project)
|
||||
(updated, ())
|
||||
}
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
private def loadProject(
|
||||
directory: File
|
||||
): F[ProjectRepositoryFailure, Option[Project]] =
|
||||
for {
|
||||
pkgOpt <- loadPackage(directory)
|
||||
meta <- metadataStorage(directory)
|
||||
.load()
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
} yield pkgOpt.map { pkg =>
|
||||
Project(
|
||||
id = meta.id,
|
||||
name = pkg.name,
|
||||
kind = meta.kind,
|
||||
created = meta.created,
|
||||
lastOpened = meta.lastOpened,
|
||||
path = Some(directory.toString)
|
||||
)
|
||||
}
|
||||
|
||||
private def createProjectStructure(
|
||||
project: Project,
|
||||
@ -94,23 +111,22 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
.blockingOp { PackageManager.Default.create(projectPath, project.name) }
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def rename(
|
||||
projectId: UUID,
|
||||
name: String
|
||||
): F[ProjectRepositoryFailure, Unit] = {
|
||||
updateProjectName(projectId, name) *>
|
||||
updatePackageName(projectId, name)
|
||||
}
|
||||
|
||||
private def updatePackageName(
|
||||
projectId: UUID,
|
||||
name: String
|
||||
): F[ProjectRepositoryFailure, Unit] =
|
||||
for {
|
||||
project <- getProject(projectId)
|
||||
_ <- changePacketName(new File(project.path.get), name)
|
||||
} yield ()
|
||||
findById(projectId).flatMap {
|
||||
case Some(project) =>
|
||||
project.path match {
|
||||
case Some(directory) =>
|
||||
renamePackage(new File(directory), name)
|
||||
case None =>
|
||||
ErrorChannel[F].fail(ProjectNotFoundInIndex)
|
||||
}
|
||||
case None =>
|
||||
ErrorChannel[F].fail(ProjectNotFoundInIndex)
|
||||
}
|
||||
|
||||
private def getProject(
|
||||
projectId: UUID
|
||||
@ -129,7 +145,26 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
} yield projectPackage.config.name
|
||||
}
|
||||
|
||||
private def changePacketName(
|
||||
private def loadPackage(
|
||||
projectPath: File
|
||||
): F[ProjectRepositoryFailure, Option[Package[File]]] =
|
||||
Sync[F]
|
||||
.blockingOp { PackageManager.Default.fromDirectory(projectPath) }
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
|
||||
private def getPackage(
|
||||
projectPath: File
|
||||
): F[ProjectRepositoryFailure, Package[File]] =
|
||||
loadPackage(projectPath)
|
||||
.flatMap {
|
||||
case None =>
|
||||
ErrorChannel[F].fail(
|
||||
InconsistentStorage(s"Cannot find package.yaml at $projectPath")
|
||||
)
|
||||
case Some(projectPackage) => CovariantFlatMap[F].pure(projectPackage)
|
||||
}
|
||||
|
||||
private def renamePackage(
|
||||
projectPath: File,
|
||||
name: String
|
||||
): F[ProjectRepositoryFailure, Unit] =
|
||||
@ -142,80 +177,56 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
}
|
||||
|
||||
private def getPackage(
|
||||
projectPath: File
|
||||
): F[ProjectRepositoryFailure, Package[File]] =
|
||||
Sync[F]
|
||||
.blockingOp { PackageManager.Default.fromDirectory(projectPath) }
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
.flatMap {
|
||||
case None =>
|
||||
ErrorChannel[F].fail(
|
||||
InconsistentStorage(s"Cannot find package.yaml at $projectPath")
|
||||
)
|
||||
|
||||
case Some(projectPackage) => CovariantFlatMap[F].pure(projectPackage)
|
||||
}
|
||||
|
||||
private def updateProjectName(
|
||||
projectId: UUID,
|
||||
name: String
|
||||
): F[ProjectRepositoryFailure, Unit] =
|
||||
indexStorage
|
||||
.modify { index =>
|
||||
val updated = index.update(projectId)(_.copy(name = name))
|
||||
(updated, ())
|
||||
}
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
|
||||
/** @inheritdoc * */
|
||||
override def delete(
|
||||
projectId: UUID
|
||||
): F[ProjectRepositoryFailure, Unit] =
|
||||
indexStorage
|
||||
.modify { index =>
|
||||
val maybeProject = index.findById(projectId)
|
||||
index.remove(projectId) -> maybeProject
|
||||
}
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
.flatMap {
|
||||
case None =>
|
||||
ErrorChannel[F].fail(ProjectNotFoundInIndex)
|
||||
|
||||
case Some(project) if project.path.isEmpty =>
|
||||
ErrorChannel[F].fail(
|
||||
InconsistentStorage(
|
||||
"Index cannot contain a user project without path"
|
||||
/** @inheritdoc */
|
||||
def update(project: Project): F[ProjectRepositoryFailure, Unit] =
|
||||
project.path match {
|
||||
case Some(path) =>
|
||||
metadataStorage(new File(path))
|
||||
.persist(
|
||||
ProjectMetadata(
|
||||
id = project.id,
|
||||
kind = project.kind,
|
||||
created = project.created,
|
||||
lastOpened = project.lastOpened
|
||||
)
|
||||
)
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
case None =>
|
||||
ErrorChannel[F].fail(ProjectNotFoundInIndex)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def delete(projectId: UUID): F[ProjectRepositoryFailure, Unit] = {
|
||||
findById(projectId)
|
||||
.flatMap {
|
||||
case Some(project) =>
|
||||
removeProjectStructure(project.path.get)
|
||||
project.path match {
|
||||
case Some(directory) =>
|
||||
fileSystem
|
||||
.removeDir(new File(directory))
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
case None =>
|
||||
ErrorChannel[F].fail(ProjectNotFoundInIndex)
|
||||
}
|
||||
case None =>
|
||||
ErrorChannel[F].fail(ProjectNotFoundInIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private def removeProjectStructure(
|
||||
projectPath: String
|
||||
): F[ProjectRepositoryFailure, Unit] =
|
||||
fileSystem
|
||||
.removeDir(new File(projectPath))
|
||||
.mapError[ProjectRepositoryFailure](failure =>
|
||||
StorageFailure(failure.toString)
|
||||
)
|
||||
|
||||
/** @inheritdoc * */
|
||||
/** @inheritdoc */
|
||||
override def moveProjectToTargetDir(
|
||||
projectId: UUID
|
||||
projectId: UUID,
|
||||
newName: String
|
||||
): F[ProjectRepositoryFailure, File] = {
|
||||
def move(project: Project) =
|
||||
for {
|
||||
targetPath <- findTargetPath(project)
|
||||
targetPath <- findTargetPath(project.copy(name = newName))
|
||||
_ <- moveProjectDir(project, targetPath)
|
||||
_ <- updateProjectDir(projectId, targetPath)
|
||||
} yield targetPath
|
||||
|
||||
for {
|
||||
project <- getProject(projectId)
|
||||
primaryPath = getPrimaryPath(project)
|
||||
primaryPath = new File(storageConfig.userProjectsPath, newName)
|
||||
finalPath <-
|
||||
if (isLocationOk(project.path.get, primaryPath.toString)) {
|
||||
CovariantFlatMap[F].pure(primaryPath)
|
||||
@ -238,17 +249,6 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
}
|
||||
}
|
||||
|
||||
private def updateProjectDir(projectId: UUID, targetPath: File) = {
|
||||
indexStorage
|
||||
.modify { index =>
|
||||
val updated = index.update(projectId)(
|
||||
_.copy(path = Some(targetPath.toString))
|
||||
)
|
||||
(updated, ())
|
||||
}
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
}
|
||||
|
||||
private def moveProjectDir(project: Project, targetPath: File) = {
|
||||
fileSystem
|
||||
.move(new File(project.path.get), targetPath)
|
||||
@ -257,11 +257,6 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
)
|
||||
}
|
||||
|
||||
private def getPrimaryPath(
|
||||
project: Project
|
||||
): File =
|
||||
new File(storageConfig.userProjectsPath, project.name)
|
||||
|
||||
private def findTargetPath(
|
||||
project: Project
|
||||
): F[ProjectRepositoryFailure, File] =
|
||||
@ -290,4 +285,12 @@ class ProjectFileRepository[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
if (number == 0) ""
|
||||
else s"_$number"
|
||||
|
||||
private def metadataStorage(projectPath: File): MetadataFileStorage[F] =
|
||||
new MetadataFileStorage[F](
|
||||
projectPath,
|
||||
storageConfig,
|
||||
clock,
|
||||
fileSystem,
|
||||
gen
|
||||
)
|
||||
}
|
||||
|
@ -84,8 +84,12 @@ trait ProjectRepository[F[+_, +_]] {
|
||||
* Moves project to the target dir.
|
||||
*
|
||||
* @param projectId the project id
|
||||
* @param newName the new project name
|
||||
*/
|
||||
def moveProjectToTargetDir(projectId: UUID): F[ProjectRepositoryFailure, File]
|
||||
def moveProjectToTargetDir(
|
||||
projectId: UUID,
|
||||
newName: String
|
||||
): F[ProjectRepositoryFailure, File]
|
||||
|
||||
/**
|
||||
* Gets a package name for the specified project.
|
||||
|
@ -0,0 +1,36 @@
|
||||
package org.enso.projectmanager.model
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Project metadata entity.
|
||||
*
|
||||
* @param id a project id
|
||||
* @param kind a project kind
|
||||
* @param created a project creation time
|
||||
* @param lastOpened a project last open time
|
||||
*/
|
||||
case class ProjectMetadata(
|
||||
id: UUID,
|
||||
kind: ProjectKind,
|
||||
created: OffsetDateTime,
|
||||
lastOpened: Option[OffsetDateTime]
|
||||
)
|
||||
|
||||
object ProjectMetadata {
|
||||
|
||||
/**
|
||||
* Create an instance from the project entity.
|
||||
*
|
||||
* @param project the project entity
|
||||
* @return the project metadata
|
||||
*/
|
||||
def apply(project: Project): ProjectMetadata =
|
||||
new ProjectMetadata(
|
||||
id = project.id,
|
||||
kind = project.kind,
|
||||
created = project.created,
|
||||
lastOpened = project.lastOpened
|
||||
)
|
||||
}
|
@ -17,21 +17,23 @@ import org.enso.projectmanager.infrastructure.shutdown.ShutdownHook
|
||||
* A hook responsible for moving a project to the target dir.
|
||||
*
|
||||
* @param projectId a project id
|
||||
* @param newName a new project name
|
||||
* @param repo a project repository
|
||||
* @param log a logging facility
|
||||
*/
|
||||
class MoveProjectDirCmd[F[+_, +_]: CovariantFlatMap: ErrorChannel](
|
||||
projectId: UUID,
|
||||
newName: String,
|
||||
repo: ProjectRepository[F],
|
||||
log: Logging[F]
|
||||
) extends ShutdownHook[F] {
|
||||
|
||||
/** @inheritdoc **/
|
||||
/** @inheritdoc */
|
||||
override def execute(): F[Nothing, Unit] = {
|
||||
def go() =
|
||||
for {
|
||||
_ <- log.debug(s"Moving project ${projectId}")
|
||||
dir <- repo.moveProjectToTargetDir(projectId)
|
||||
_ <- log.debug(s"Moving project ${projectId} to $newName")
|
||||
dir <- repo.moveProjectToTargetDir(projectId, newName)
|
||||
_ <- log.info(s"Project $projectId moved to $dir")
|
||||
} yield ()
|
||||
|
||||
|
@ -59,11 +59,11 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
): F[ProjectServiceFailure, UUID] = {
|
||||
// format: off
|
||||
for {
|
||||
_ <- log.debug(s"Creating project $name.")
|
||||
projectId <- gen.randomUUID()
|
||||
_ <- log.debug(s"Creating project $name $projectId.")
|
||||
_ <- validateName(name)
|
||||
_ <- checkIfNameExists(name)
|
||||
creationTime <- clock.nowInUtc()
|
||||
projectId <- gen.randomUUID()
|
||||
project = Project(projectId, name, UserProject, creationTime)
|
||||
_ <- repo.create(project).mapError(toServiceFailure)
|
||||
_ <- log.info(s"Project $project created.")
|
||||
@ -108,7 +108,7 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
_ <- checkIfNameExists(name)
|
||||
oldPackage <- repo.getPackageName(projectId).mapError(toServiceFailure)
|
||||
_ <- repo.rename(projectId, name).mapError(toServiceFailure)
|
||||
_ <- renameProjectDirOrRegisterShutdownHook(projectId)
|
||||
_ <- renameProjectDirOrRegisterShutdownHook(projectId, name)
|
||||
newPackage = PackageManager.Default.normalizeName(name)
|
||||
_ <- refactorProjectName(projectId, oldPackage, newPackage)
|
||||
_ <- log.info(s"Project $projectId renamed.")
|
||||
@ -116,9 +116,10 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
}
|
||||
|
||||
private def renameProjectDirOrRegisterShutdownHook(
|
||||
projectId: UUID
|
||||
projectId: UUID,
|
||||
newName: String
|
||||
): F[ProjectServiceFailure, Unit] = {
|
||||
val cmd = new MoveProjectDirCmd[F](projectId, repo, log)
|
||||
val cmd = new MoveProjectDirCmd[F](projectId, newName, repo, log)
|
||||
CovariantFlatMap[F]
|
||||
.ifM(isServerRunning(projectId))(
|
||||
ifTrue = languageServerGateway.registerShutdownHook(projectId, cmd),
|
||||
|
@ -5,27 +5,21 @@ import java.nio.file.Files
|
||||
import java.time.{OffsetDateTime, ZoneOffset}
|
||||
import java.util.UUID
|
||||
|
||||
import io.circe.generic.auto._
|
||||
import akka.testkit._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.jsonrpc.test.JsonRpcServerTestKit
|
||||
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
|
||||
import org.enso.projectmanager.boot.Globals.{ConfigFilename, ConfigNamespace}
|
||||
import org.enso.projectmanager.boot.configuration._
|
||||
import org.enso.projectmanager.control.effect.ZioEnvExec
|
||||
import org.enso.projectmanager.infrastructure.file.{
|
||||
BlockingFileSystem,
|
||||
SynchronizedFileStorage
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.languageserver.{
|
||||
LanguageServerGatewayImpl,
|
||||
LanguageServerRegistry,
|
||||
ShutdownHookActivator
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.log.Slf4jLogging
|
||||
import org.enso.projectmanager.infrastructure.repository.{
|
||||
ProjectFileRepository,
|
||||
ProjectIndex
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectFileRepository
|
||||
import org.enso.projectmanager.protocol.{
|
||||
JsonRpc,
|
||||
ManagerClientControllerFactory
|
||||
@ -37,6 +31,7 @@ import pureconfig.generic.auto._
|
||||
import zio.interop.catz.core._
|
||||
import zio.{Runtime, Semaphore, ZEnv, ZIO}
|
||||
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class BaseServerSpec extends JsonRpcServerTestKit {
|
||||
@ -53,7 +48,9 @@ class BaseServerSpec extends JsonRpcServerTestKit {
|
||||
val testClock =
|
||||
new ProgrammableClock[ZEnv](OffsetDateTime.now(ZoneOffset.UTC))
|
||||
|
||||
def getGeneratedUUID: UUID = gen.takeFirst()
|
||||
def getGeneratedUUID: UUID = {
|
||||
Await.result(Future(gen.takeFirst())(system.dispatcher), 3.seconds.dilated)
|
||||
}
|
||||
|
||||
lazy val gen = new ObservableGenerator[ZEnv]()
|
||||
|
||||
@ -62,12 +59,11 @@ class BaseServerSpec extends JsonRpcServerTestKit {
|
||||
|
||||
val userProjectDir = new File(testProjectsRoot, "projects")
|
||||
|
||||
val indexFile = new File(testProjectsRoot, "project-index.json")
|
||||
|
||||
lazy val testStorageConfig = StorageConfig(
|
||||
projectsRoot = testProjectsRoot,
|
||||
projectIndexPath = indexFile,
|
||||
userProjectsPath = userProjectDir
|
||||
projectsRoot = testProjectsRoot,
|
||||
userProjectsPath = userProjectDir,
|
||||
projectMetadataDirectory = ".enso",
|
||||
projectMetadataFileName = "project.json"
|
||||
)
|
||||
|
||||
lazy val bootloaderConfig = config.bootloader
|
||||
@ -85,17 +81,12 @@ class BaseServerSpec extends JsonRpcServerTestKit {
|
||||
lazy val storageSemaphore =
|
||||
Runtime.default.unsafeRun(Semaphore.make(1))
|
||||
|
||||
lazy val indexStorage =
|
||||
new SynchronizedFileStorage[ProjectIndex, ZIO[ZEnv, +*, +*]](
|
||||
testStorageConfig.projectIndexPath,
|
||||
fileSystem
|
||||
)
|
||||
|
||||
lazy val projectRepository =
|
||||
new ProjectFileRepository(
|
||||
testStorageConfig,
|
||||
testClock,
|
||||
fileSystem,
|
||||
indexStorage
|
||||
gen
|
||||
)
|
||||
|
||||
lazy val projectValidator = new MonadicProjectValidator[ZIO[ZEnv, *, *]]()
|
||||
|
@ -5,6 +5,7 @@ import java.nio.file.Paths
|
||||
import java.util.UUID
|
||||
|
||||
import io.circe.literal._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.projectmanager.test.Net.tryConnect
|
||||
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
|
||||
import org.enso.testkit.FlakySpec
|
||||
@ -16,6 +17,11 @@ class ProjectManagementApiSpec
|
||||
with FlakySpec
|
||||
with ProjectManagementOps {
|
||||
|
||||
override def beforeEach(): Unit = {
|
||||
super.beforeEach()
|
||||
gen.reset()
|
||||
}
|
||||
|
||||
"project/create" must {
|
||||
|
||||
"check if project name is not empty" taggedAs Flaky in {
|
||||
@ -110,9 +116,11 @@ class ProjectManagementApiSpec
|
||||
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 a project dir with a suffix if a directory is taken" in {
|
||||
@ -313,6 +321,58 @@ class ProjectManagementApiSpec
|
||||
deleteProject(projectId)(client1)
|
||||
}
|
||||
|
||||
"start the Language Server after moving the directory" taggedAs Flaky in {
|
||||
//given
|
||||
val projectName = "foo"
|
||||
implicit val client = new WsTestClient(address)
|
||||
val projectId = createProject(projectName)
|
||||
|
||||
val newName = "bar"
|
||||
val newProjectDir = new File(userProjectDir, newName)
|
||||
FileUtils.moveDirectory(
|
||||
new File(userProjectDir, projectName),
|
||||
newProjectDir
|
||||
)
|
||||
val packageFile = new File(newProjectDir, "package.yaml")
|
||||
val mainEnso =
|
||||
Paths.get(newProjectDir.toString, "src", "Main.enso").toFile
|
||||
val meta =
|
||||
Paths.get(newProjectDir.toString, ".enso", "project.json").toFile
|
||||
|
||||
packageFile shouldBe Symbol("file")
|
||||
mainEnso shouldBe Symbol("file")
|
||||
meta shouldBe Symbol("file")
|
||||
|
||||
//when
|
||||
val socket = openProject(projectId)
|
||||
val languageServerClient =
|
||||
new WsTestClient(s"ws://${socket.host}:${socket.port}")
|
||||
languageServerClient.send(json"""
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "file/read",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": ${UUID.randomUUID()},
|
||||
"segments": ["src", "Main.enso"]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
//then
|
||||
// 'not initialized' response indicates that language server is running
|
||||
languageServerClient.expectJson(json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"id":1,
|
||||
"error":{"code":6001,"message":"Session not initialised"}}
|
||||
""")
|
||||
//teardown
|
||||
closeProject(projectId)
|
||||
deleteProject(projectId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"project/close" must {
|
||||
@ -396,9 +456,9 @@ class ProjectManagementApiSpec
|
||||
"id":0,
|
||||
"result": {
|
||||
"projects": [
|
||||
{"name": "baz", "id": $bazId, "lastOpened": null},
|
||||
{"name": "bar", "id": $barId, "lastOpened": null},
|
||||
{"name": "foo", "id": $fooId, "lastOpened": null}
|
||||
{"name": "Baz", "id": $bazId, "lastOpened": null},
|
||||
{"name": "Bar", "id": $barId, "lastOpened": null},
|
||||
{"name": "Foo", "id": $fooId, "lastOpened": null}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -438,9 +498,9 @@ class ProjectManagementApiSpec
|
||||
"id":0,
|
||||
"result": {
|
||||
"projects": [
|
||||
{"name": "bar", "id": $barId, "lastOpened": $barOpenTime},
|
||||
{"name": "foo", "id": $fooId, "lastOpened": $fooOpenTime},
|
||||
{"name": "baz", "id": $bazId, "lastOpened": null}
|
||||
{"name": "Bar", "id": $barId, "lastOpened": $barOpenTime},
|
||||
{"name": "Foo", "id": $fooId, "lastOpened": $fooOpenTime},
|
||||
{"name": "Baz", "id": $bazId, "lastOpened": null}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -30,4 +30,10 @@ class ObservableGenerator[R] extends Generator[ZIO[R, +*, +*]] {
|
||||
}
|
||||
}
|
||||
|
||||
def reset(): Unit = {
|
||||
this.synchronized {
|
||||
buffer = Vector()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user