Skip to content

Commit

Permalink
Project manager (enso-org#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
kustosz authored Jul 10, 2019
1 parent e3ec0fe commit c2a60eb
Show file tree
Hide file tree
Showing 9 changed files with 650 additions and 1 deletion.
26 changes: 25 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ lazy val enso = (project in file("."))
.aggregate(
syntax,
pkg,
interpreter
interpreter,
projectManager
)

// Sub-Projects
Expand Down Expand Up @@ -90,3 +91,26 @@ lazy val interpreter = (project in file("interpreter"))
bench := (test in Benchmark).value,
parallelExecution in Benchmark := false
)

val akkaActor = "com.typesafe.akka" %% "akka-actor" % "2.5.23"
val akkaStream = "com.typesafe.akka" %% "akka-stream" % "2.5.23"
val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.8"
val akkaSpray = "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.8"
val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % "2.5.23"

val akka = Seq(akkaActor, akkaStream, akkaHttp, akkaSpray, akkaTyped)

val circe = Seq("circe-core", "circe-generic", "circe-yaml").map(
"io.circe" %% _ % "0.10.0"
)

lazy val projectManager = (project in file("project-manager"))
.settings(
(Compile / mainClass) := Some("org.enso.projectmanager.Server")
)
.settings(
libraryDependencies ++= akka,
libraryDependencies ++= circe,
libraryDependencies += "io.spray" %% "spray-json" % "1.3.5"
)
.dependsOn(pkg)
19 changes: 19 additions & 0 deletions project-manager/src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
project-manager {
server {
host = "0.0.0.0"
port = 30535
timeout = 10 seconds
}

storage {
projects-root = ${user.home}/enso
temporary-projects-path = ${project-manager.storage.projects-root}/tmp
local-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
}

tutorials {
github-organisation = "luna-packages"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.enso.projectmanager

import java.util.UUID

import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.model.Uri.Path
import akka.http.scaladsl.server.PathMatcher0
import akka.http.scaladsl.server.PathMatcher1
import akka.http.scaladsl.server.PathMatchers.JavaUUID
import org.enso.projectmanager.model.ProjectId

class RouteHelper {

val tutorials: String = "tutorials"
val projects: String = "projects"
val thumb: String = "thumb"

val tutorialsPath: Path = Path / tutorials
val tutorialsPathMatcher: PathMatcher0 = tutorials

val projectsPath: Path = Path / projects
val projectsPathMatcher: PathMatcher0 = projects

def projectPath(id: ProjectId): Path = projectsPath / id.toString

val projectPathMatcher: PathMatcher1[ProjectId] =
(projectsPathMatcher / JavaUUID).map(ProjectId)

def thumbPath(id: ProjectId): Path = projectPath(id) / thumb
val thumbPathMatcher: PathMatcher1[ProjectId] = projectPathMatcher / thumb

def uriFor(base: Uri, path: Path): Uri = base.withPath(path)
}
189 changes: 189 additions & 0 deletions project-manager/src/main/scala/org/enso/projectmanager/Server.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package org.enso.projectmanager

import java.io.File
import java.util.UUID
import java.util.concurrent.TimeUnit

import akka.actor.ActorSystem
import akka.actor.Scheduler
import com.typesafe.config.ConfigFactory
import akka.actor.typed.ActorRef
import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.scaladsl.AskPattern._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpResponse
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.server.Route
import akka.stream.ActorMaterializer
import akka.util.Timeout
import org.enso.projectmanager.api.ProjectFactory
import org.enso.projectmanager.api.ProjectJsonSupport
import org.enso.projectmanager.model.Project
import org.enso.projectmanager.model.ProjectId
import org.enso.projectmanager.services.CreateTemporary
import org.enso.projectmanager.services.CreateTemporaryResponse
import org.enso.projectmanager.services.GetProjectById
import org.enso.projectmanager.services.GetProjectResponse
import org.enso.projectmanager.services.ListProjectsRequest
import org.enso.projectmanager.services.ListProjectsResponse
import org.enso.projectmanager.services.ListTutorialsRequest
import org.enso.projectmanager.services.ProjectsCommand
import org.enso.projectmanager.services.ProjectsService
import org.enso.projectmanager.services.StorageManager
import org.enso.projectmanager.services.TutorialsDownloader

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Failure
import scala.util.Success

case class Server(
host: String,
port: Int,
repository: ActorRef[ProjectsCommand],
routeHelper: RouteHelper,
apiFactory: ProjectFactory
)(implicit val system: ActorSystem,
implicit val executor: ExecutionContext,
implicit val materializer: ActorMaterializer,
implicit val askTimeout: Timeout)
extends Directives
with ProjectJsonSupport {

implicit val scheduler: Scheduler = system.scheduler

def projectDoesNotExistResponse(id: ProjectId): HttpResponse =
HttpResponse(StatusCodes.NotFound, entity = s"Project $id does not exist")

def thumbDoesNotExistResponse: HttpResponse =
HttpResponse(StatusCodes.NotFound, entity = "Thumbnail does not exist")

def withSuccess[T](
fut: Future[T],
errorResponse: HttpResponse = HttpResponse(StatusCodes.InternalServerError)
)(successHandler: T => Route
): Route = {
onComplete(fut) {
case Success(r) => successHandler(r)
case Failure(_) => complete(errorResponse)
}
}

def withProject(id: ProjectId)(route: Project => Route): Route = {
val projectFuture =
repository
.ask(
(ref: ActorRef[GetProjectResponse]) => GetProjectById(id, ref)
)
.map(_.project)
withSuccess(projectFuture) {
case Some(project) => route(project)
case None => complete(projectDoesNotExistResponse(id))
}
}

def listProjectsWith(
reqBuilder: ActorRef[ListProjectsResponse] => ProjectsCommand
)(baseUri: Uri
): Route = {
val projectsFuture = repository.ask(reqBuilder)
withSuccess(projectsFuture) { projectsResponse =>
val response = projectsResponse.projects.toSeq.map {
case (id, project) => apiFactory.fromModel(id, project, baseUri)
}
complete(response)
}
}

def createProject(baseUri: Uri): Route = {
val projectFuture = repository.ask(
(ref: ActorRef[CreateTemporaryResponse]) =>
CreateTemporary("NewProject", ref)
)
withSuccess(projectFuture) { response =>
complete(apiFactory.fromModel(response.id, response.project, baseUri))
}
}

def getThumb(projectId: ProjectId): Route = {
withProject(projectId) { project =>
if (project.pkg.hasThumb) getFromFile(project.pkg.thumbFile)
else complete(thumbDoesNotExistResponse)
}
}

val route: Route = ignoreTrailingSlash {
path(routeHelper.projectsPathMatcher)(
(get & extractUri)(listProjectsWith(ListProjectsRequest)) ~
(post & extractUri)(createProject)
) ~
(get & path(routeHelper.tutorialsPathMatcher) & extractUri)(
listProjectsWith(ListTutorialsRequest)
) ~
(get & path(routeHelper.thumbPathMatcher))(getThumb)
}

def serve: Future[Http.ServerBinding] = {
Http().bindAndHandle(route, host, port)
}
}

object Server {

def main(args: Array[String]) {

val config = ConfigFactory.load.getConfig("project-manager")
val serverConfig = config.getConfig("server")
val storageConfig = config.getConfig("storage")

val host = serverConfig.getString("host")
val port = serverConfig.getInt("port")

val timeout =
FiniteDuration(
serverConfig.getDuration("timeout").toNanos,
TimeUnit.NANOSECONDS
)

implicit val system: ActorSystem = ActorSystem("project-manager")
implicit val executor: ExecutionContext = system.dispatcher
implicit val materializer: ActorMaterializer = ActorMaterializer()
implicit val askTimeout: Timeout = new Timeout(timeout)

val localProjectsPath =
new File(storageConfig.getString("local-projects-path"))
val tmpProjectsPath = new File(
storageConfig.getString("temporary-projects-path")
)
val tutorialsPath =
new File(storageConfig.getString("tutorials-path"))
val tutorialsCachePath =
new File(storageConfig.getString("tutorials-cache-path"))

val tutorialsDownloader =
TutorialsDownloader(
tutorialsPath,
tutorialsCachePath,
config.getString("tutorials.github-organisation")
)
val storageManager = StorageManager(
localProjectsPath,
tmpProjectsPath,
tutorialsPath
)

val repoActor = system.spawn(
ProjectsService.behavior(storageManager, tutorialsDownloader),
"projects-repository"
)

val routeHelper = new RouteHelper
val apiFactory = ProjectFactory(routeHelper)

val server = Server(host, port, repoActor, routeHelper, apiFactory)
server.serve
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.enso.projectmanager.api

import java.util.UUID

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.Uri
import org.enso.projectmanager.RouteHelper
import org.enso.projectmanager.model
import org.enso.projectmanager.model.ProjectId
import spray.json.DefaultJsonProtocol

case class Project(
id: String,
name: String,
path: String,
thumb: Option[String],
persisted: Boolean)

case class ProjectFactory(routeHelper: RouteHelper) {

def fromModel(
id: ProjectId,
project: model.Project,
baseUri: Uri
): Project = {
val thumbUri =
if (project.hasThumb)
Some(routeHelper.uriFor(baseUri, routeHelper.thumbPath(id)))
else None
Project(
id.toString,
project.pkg.name,
project.pkg.root.getAbsolutePath,
thumbUri.map(_.toString),
project.isPersistent
)
}
}

trait ProjectJsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val projectFormat = jsonFormat5(Project.apply)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.enso.projectmanager.model

import java.util.UUID

import org.enso.pkg.Package

import scala.collection.immutable.HashMap

sealed trait ProjectType {
def isPersistent: Boolean
}
case object Local extends ProjectType {
override def isPersistent: Boolean = true
}
case object Tutorial extends ProjectType {
override def isPersistent: Boolean = false
}
case object Temporary extends ProjectType {
override def isPersistent: Boolean = false
}

case class ProjectId(uid: UUID) {
override def toString: String = uid.toString
}

case class Project(kind: ProjectType, pkg: Package) {
def isPersistent: Boolean = kind.isPersistent
def hasThumb: Boolean = pkg.hasThumb
}

case class ProjectsRepository(projects: HashMap[ProjectId, Project]) {

def getById(id: ProjectId): Option[Project] = {
projects.get(id)
}

def insert(project: Project): (ProjectId, ProjectsRepository) = {
val id = ProjectsRepository.generateId
val newRepo = copy(projects = projects + (id -> project))
(id, newRepo)
}
}

case object ProjectsRepository {

def apply(projects: Seq[Project]): ProjectsRepository = {
ProjectsRepository(HashMap(projects.map(generateId -> _): _*))
}

def generateId: ProjectId = ProjectId(UUID.randomUUID)
}
Loading

0 comments on commit c2a60eb

Please sign in to comment.