File Reads for the Language Server (#559)

File Reads for the Language Server
This commit is contained in:
Łukasz Olczak 2020-02-26 18:03:14 +01:00 committed by GitHub
parent 75127d07b9
commit fe471314ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 288 additions and 44 deletions

View File

@ -1111,9 +1111,9 @@ null
##### Errors ##### Errors
- **FileSystemError(errorCode=1000)** This error signals generic file system errors. - [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable file-system error.
- **ContentRootNotFoundError(errorCode=1001)** The error informs that the requested content root cannot be found. - [`ContentRootNotFoundError`](#contentrootnotfounderror) to signal that the requested content root cannot be found.
- **AccessDeniedError(errorCode=1002)** It signals that a user doesn't have access to a resource. - [`AccessDeniedError`](#accessdeniederror) to signal that a user doesn't have access to a resource.
#### `file/read` #### `file/read`
This requests that the file manager component reads the contents of a specified This requests that the file manager component reads the contents of a specified
@ -1142,7 +1142,11 @@ return the contents from the in-memory buffer rather than the file on disk.
``` ```
##### Errors ##### Errors
TBC
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable file-system error.
- [`ContentRootNotFoundError`](#contentrootnotfounderror) to signal that the requested content root cannot be found.
- [`AccessDeniedError`](#accessdeniederror) to signal that a user doesn't have access to a resource.
- [`FileNotFound`](#filenotfound) informs that file cannot be found.
#### `file/create` #### `file/create`
This request asks the file manager to create the specified file system object. This request asks the file manager to create the specified file system object.
@ -1692,3 +1696,43 @@ TBC
### Errors - Language Server ### Errors - Language Server
The language server component also has its own set of errors. This section is The language server component also has its own set of errors. This section is
not a complete specification and will be updated as new errors are added. not a complete specification and will be updated as new errors are added.
##### `FileSystemError`
This error signals generic file system errors.
```typescript
"error" : {
"code" : 1000,
"message" : "File '/foo/bar' exists but is a directory"
}
```
##### `ContentRootNotFoundError`
The error informs that the requested content root cannot be found.
```typescript
"error" : {
"code" : 1001,
"message" : "Content root not found"
}
```
##### `AccessDeniedError`
It signals that a user doesn't have access to a resource.
```typescript
"error" : {
"code" : 1002,
"message" : "Access denied"
}
```
##### `FileNotFound`
It signals that requested file doesn't exist.
```typescript
"error" : {
"code" : 1003,
"message" : "File not found"
}
```

View File

@ -7,15 +7,10 @@ import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import org.enso.languageserver.ClientApi._ import org.enso.languageserver.ClientApi._
import org.enso.languageserver.data.{CapabilityRegistration, Client} import org.enso.languageserver.data.{CapabilityRegistration, Client}
import org.enso.languageserver.filemanager.FileManagerApi.{ import org.enso.languageserver.filemanager.FileManagerApi.{FileRead, _}
FileSystemError,
FileWrite,
FileWriteParams
}
import org.enso.languageserver.filemanager.FileManagerProtocol.FileWriteResult import org.enso.languageserver.filemanager.FileManagerProtocol.FileWriteResult
import org.enso.languageserver.filemanager.{ import org.enso.languageserver.filemanager.{
FileManagerProtocol, FileManagerProtocol,
FileSystemFailure,
FileSystemFailureMapper FileSystemFailureMapper
} }
import org.enso.languageserver.jsonrpc.Errors.ServiceError import org.enso.languageserver.jsonrpc.Errors.ServiceError
@ -69,6 +64,7 @@ object ClientApi {
.registerRequest(AcquireCapability) .registerRequest(AcquireCapability)
.registerRequest(ReleaseCapability) .registerRequest(ReleaseCapability)
.registerRequest(FileWrite) .registerRequest(FileWrite)
.registerRequest(FileRead)
.registerNotification(ForceReleaseCapability) .registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability) .registerNotification(GrantCapability)
@ -123,22 +119,57 @@ class ClientController(
server ! LanguageProtocol.ReleaseCapability(clientId, params.id) server ! LanguageProtocol.ReleaseCapability(clientId, params.id)
sender ! ResponseResult(ReleaseCapability, id, Unused) sender ! ResponseResult(ReleaseCapability, id, Unused)
case Request(FileWrite, id, params: FileWriteParams) => case Request(FileWrite, id, params: FileWrite.Params) =>
(server ? FileManagerProtocol.FileWrite(params.path, params.content)) writeFile(webActor, id, params)
.onComplete {
case Success(FileWriteResult(Right(()))) =>
webActor ! ResponseResult(FileWrite, id, Unused)
case Success(FileWriteResult(Left(failure))) => case Request(FileRead, id, params: FileRead.Params) =>
webActor ! ResponseError( readFile(webActor, id, params)
Some(id),
FileSystemFailureMapper.mapFailure(failure)
)
case Failure(th) => }
log.error("An exception occurred during writing to a file", th)
webActor ! ResponseError(Some(id), ServiceError) private def readFile(
} webActor: ActorRef,
id: Id,
params: FileRead.Params
): Unit = {
(server ? FileManagerProtocol.FileRead(params.path)).onComplete {
case Success(
FileManagerProtocol.FileReadResult(Right(content: String))
) =>
webActor ! ResponseResult(FileRead, id, FileRead.Result(content))
case Success(FileManagerProtocol.FileReadResult(Left(failure))) =>
webActor ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
)
case Failure(th) =>
log.error("An exception occurred during reading a file", th)
webActor ! ResponseError(Some(id), ServiceError)
}
}
private def writeFile(
webActor: ActorRef,
id: Id,
params: FileWrite.Params
): Unit = {
(server ? FileManagerProtocol.FileWrite(params.path, params.contents))
.onComplete {
case Success(FileWriteResult(Right(()))) =>
webActor ! ResponseResult(FileWrite, id, Unused)
case Success(FileWriteResult(Left(failure))) =>
webActor ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
)
case Failure(th) =>
log.error("An exception occurred during writing to a file", th)
webActor ! ResponseError(Some(id), ServiceError)
}
} }
} }

View File

@ -6,6 +6,8 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
import cats.effect.IO import cats.effect.IO
import org.enso.languageserver.data._ import org.enso.languageserver.data._
import org.enso.languageserver.filemanager.FileManagerProtocol.{ import org.enso.languageserver.filemanager.FileManagerProtocol.{
FileRead,
FileReadResult,
FileWrite, FileWrite,
FileWriteResult FileWriteResult
} }
@ -134,5 +136,19 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO])
} yield () } yield ()
sender ! FileWriteResult(result) sender ! FileWriteResult(result)
case FileRead(path) =>
val result =
for {
rootPath <- config.findContentRoot(path.rootId)
content <- fs.read(path.toFile(rootPath)).unsafeRunSync()
} yield content
sender ! FileReadResult(result)
} }
/* Note [Usage of unsafe methods]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It invokes side-effecting function, all exceptions are caught and
explicitly returned as left side of disjunction.
*/
} }

View File

@ -16,15 +16,30 @@ import org.enso.languageserver.jsonrpc.{
object FileManagerApi { object FileManagerApi {
case object FileWrite extends Method("file/write") { case object FileWrite extends Method("file/write") {
case class Params(path: Path, contents: String)
implicit val hasParams = new HasParams[this.type] { implicit val hasParams = new HasParams[this.type] {
type Params = FileWriteParams type Params = FileWrite.Params
} }
implicit val hasResult = new HasResult[this.type] { implicit val hasResult = new HasResult[this.type] {
type Result = Unused.type type Result = Unused.type
} }
} }
case class FileWriteParams(path: Path, content: String) case object FileRead extends Method("file/read") {
case class Params(path: Path)
case class Result(contents: String)
implicit val hasParams = new HasParams[this.type] {
type Params = FileRead.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = FileRead.Result
}
}
case class FileSystemError(override val message: String) case class FileSystemError(override val message: String)
extends Error(1000, message) extends Error(1000, message)
@ -34,4 +49,6 @@ object FileManagerApi {
case object AccessDeniedError extends Error(1002, "Access denied") case object AccessDeniedError extends Error(1002, "Access denied")
case object FileNotFoundError extends Error(1003, "File not found")
} }

View File

@ -17,4 +17,18 @@ object FileManagerProtocol {
*/ */
case class FileWriteResult(result: Either[FileSystemFailure, Unit]) case class FileWriteResult(result: Either[FileSystemFailure, Unit])
/**
* Requests the Language Server read a file.
*
* @param path a path to a file
*/
case class FileRead(path: Path)
/**
* Returns a result of reading a file.
*
* @param result either file system failure or content of a file
*/
case class FileReadResult(result: Either[FileSystemFailure, String])
} }

View File

@ -1,6 +1,6 @@
package org.enso.languageserver.filemanager package org.enso.languageserver.filemanager
import java.io.{File, IOException} import java.io.{File, FileNotFoundException, IOException}
import java.nio.file._ import java.nio.file._
import cats.effect.Sync import cats.effect.Sync
@ -25,20 +25,33 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] {
file: File, file: File,
content: String content: String
): F[Either[FileSystemFailure, Unit]] = ): F[Either[FileSystemFailure, Unit]] =
Sync[F].delay { writeStringToFile(file, content) } Sync[F].delay {
Either
.catchOnly[IOException] {
FileUtils.write(file, content, "UTF-8")
}
.leftMap(errorHandling)
}
private def writeStringToFile( /**
file: File, * Reads the contents of a textual file.
content: String *
): Either[FileSystemFailure, Unit] = * @param file path to the file
Either * @return either [[FileSystemFailure]] or the content of a file as a String
.catchOnly[IOException]( */
FileUtils.write(file, content, "UTF-8") override def read(file: File): F[Either[FileSystemFailure, String]] =
) Sync[F].delay {
.leftMap { Either
case _: AccessDeniedException => AccessDenied .catchOnly[IOException] {
case ex => GenericFileSystemFailure(ex.getMessage) FileUtils.readFileToString(file, "UTF-8")
} }
.map(_ => ()) .leftMap(errorHandling)
}
private val errorHandling: IOException => FileSystemFailure = {
case _: FileNotFoundException => FileNotFound
case _: AccessDeniedException => AccessDenied
case ex => GenericFileSystemFailure(ex.getMessage)
}
} }

View File

@ -14,11 +14,19 @@ trait FileSystemApi[F[_]] {
* *
* @param file path to the file * @param file path to the file
* @param content a textual content of the file * @param content a textual content of the file
* @return either FileSystemFailure or Unit * @return either [[FileSystemFailure]] or Unit
*/ */
def write( def write(
file: File, file: File,
content: String content: String
): F[Either[FileSystemFailure, Unit]] ): F[Either[FileSystemFailure, Unit]]
/**
* Reads the contents of a textual file.
*
* @param file path to the file
* @return either [[FileSystemFailure]] or the content of a file as a String
*/
def read(file: File): F[Either[FileSystemFailure, String]]
} }

View File

@ -15,6 +15,11 @@ case object ContentRootNotFound extends FileSystemFailure
*/ */
case object AccessDenied extends FileSystemFailure case object AccessDenied extends FileSystemFailure
/**
* Signals that the file cannot be found.
*/
case object FileNotFound extends FileSystemFailure
/** /**
* Signals file system specific errors. * Signals file system specific errors.
* *

View File

@ -3,16 +3,24 @@ package org.enso.languageserver.filemanager
import org.enso.languageserver.filemanager.FileManagerApi.{ import org.enso.languageserver.filemanager.FileManagerApi.{
AccessDeniedError, AccessDeniedError,
ContentRootNotFoundError, ContentRootNotFoundError,
FileNotFoundError,
FileSystemError FileSystemError
} }
import org.enso.languageserver.jsonrpc.Error import org.enso.languageserver.jsonrpc.Error
object FileSystemFailureMapper { object FileSystemFailureMapper {
/**
* Maps [[FileSystemFailure]] into JSON RPC error.
*
* @param fileSystemFailure file system specific failure
* @return JSON RPC error
*/
def mapFailure(fileSystemFailure: FileSystemFailure): Error = def mapFailure(fileSystemFailure: FileSystemFailure): Error =
fileSystemFailure match { fileSystemFailure match {
case ContentRootNotFound => ContentRootNotFoundError case ContentRootNotFound => ContentRootNotFoundError
case AccessDenied => AccessDeniedError case AccessDenied => AccessDeniedError
case FileNotFound => FileNotFoundError
case GenericFileSystemFailure(reason) => FileSystemError(reason) case GenericFileSystemFailure(reason) => FileSystemError(reason)
} }

View File

@ -240,7 +240,7 @@ class WebSocketServerTest
"rootId": $testContentRootId, "rootId": $testContentRootId,
"segments": [ "foo", "bar", "baz.txt" ] "segments": [ "foo", "bar", "baz.txt" ]
}, },
"content": "123456789" "contents": "123456789"
} }
} }
""") """)
@ -267,7 +267,7 @@ class WebSocketServerTest
"rootId": ${UUID.randomUUID()}, "rootId": ${UUID.randomUUID()},
"segments": [ "foo", "bar", "baz.txt" ] "segments": [ "foo", "bar", "baz.txt" ]
}, },
"content": "123456789" "contents": "123456789"
} }
} }
""") """)
@ -283,6 +283,74 @@ class WebSocketServerTest
client.expectNoMessage() client.expectNoMessage()
} }
"read a file content" in {
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "file/write",
"id": 4,
"params": {
"path": {
"rootId": $testContentRootId,
"segments": [ "foo.txt" ]
},
"contents": "123456789"
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 4,
"result": null
}
""")
client.send(json"""
{ "jsonrpc": "2.0",
"method": "file/read",
"id": 5,
"params": {
"path": {
"rootId": $testContentRootId,
"segments": [ "foo.txt" ]
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 5,
"result": { "contents": "123456789" }
}
""")
}
"return FileNotFoundError if a file doesn't exist" in {
val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "file/read",
"id": 6,
"params": {
"path": {
"rootId": $testContentRootId,
"segments": [ "bar.txt" ]
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 6,
"error" : {
"code" : 1003,
"message" : "File not found"
}
}
""")
}
} }
class WsTestClient(address: String) { class WsTestClient(address: String) {

View File

@ -48,6 +48,26 @@ class FileSystemSpec extends AnyFlatSpec with Matchers {
readTxtFile(path) shouldBe content readTxtFile(path) shouldBe content
} }
it should "return FileNotFound failure if the file doesn't exist" in new TestCtx {
//given
val path = Paths.get(testDirPath.toString, "foo.txt")
//when
val result = objectUnderTest.read(path.toFile).unsafeRunSync()
//then
result shouldBe Left(FileNotFound)
}
it should "read a file content" in new TestCtx {
//given
val path = Paths.get(testDirPath.toString, "foo.txt")
val content = "123456789"
objectUnderTest.write(path.toFile, content).unsafeRunSync()
//when
val result = objectUnderTest.read(path.toFile).unsafeRunSync()
//then
result shouldBe Right(content)
}
def readTxtFile(path: Path): String = { def readTxtFile(path: Path): String = {
val buffer = Source.fromFile(path.toFile) val buffer = Source.fromFile(path.toFile)
val content = buffer.getLines().mkString val content = buffer.getLines().mkString