mirror of
https://github.com/enso-org/enso.git
synced 2024-12-12 07:49:16 +03:00
Project manager client (https://github.com/enso-org/ide/pull/366)
Original commit: 5bc3b7b3a3
This commit is contained in:
parent
8abda0e5b8
commit
817fd766c6
12
gui/docs/enso-protocol.md
Normal file
12
gui/docs/enso-protocol.md
Normal file
@ -0,0 +1,12 @@
|
||||
Enso protocol mainly consists of two services: Project Picker and Language Server. The protocol
|
||||
is defined on top of JSON-RPC 2.0. An up-to-date and complete list of possible operations can be
|
||||
found in the [enso protocol specification document](https://github.com/luna/enso/blob/master/doc/language-server/specification/enso-protocol.md).
|
||||
|
||||
# Setup
|
||||
Follow the contribution guidelines of [Enso repository](https://github.com/luna/enso/blob/master/CONTRIBUTING.md#hacking-on-enso)
|
||||
to setup the project. Once you have all the requirements configured, you are able to run the project
|
||||
manager service with the command bellow:
|
||||
|
||||
luna/enso$ `sbt -java-home $JAVA_HOME -J-Xss10M project-manager/run`
|
||||
|
||||
Where `$JAVA_HOME` is the path where `graalvm-ce-java8-20.0.0` is located.
|
16
gui/src/rust/Cargo.lock
generated
16
gui/src/rust/Cargo.lock
generated
@ -576,6 +576,21 @@ dependencies = [
|
||||
"web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enso-protocol"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"enso-prelude 0.1.0",
|
||||
"futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"json-rpc 0.1.0",
|
||||
"serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"shapely 0.1.0",
|
||||
"utils 0.1.0",
|
||||
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ensogl"
|
||||
version = "0.1.0"
|
||||
@ -1174,6 +1189,7 @@ dependencies = [
|
||||
"data 0.1.0",
|
||||
"enso-frp 0.1.0",
|
||||
"enso-prelude 0.1.0",
|
||||
"enso-protocol 0.1.0",
|
||||
"ensogl 0.1.0",
|
||||
"ensogl-core-msdf-sys 0.1.0",
|
||||
"ensogl-system-web 0.1.0",
|
||||
|
@ -8,6 +8,7 @@ members = [
|
||||
"ide",
|
||||
"ide/ast/impl",
|
||||
"ide/ast/macros",
|
||||
"ide/enso-protocol",
|
||||
"ide/file-manager",
|
||||
"ide/file-manager/mock-server",
|
||||
"ide/json-rpc",
|
||||
|
@ -18,6 +18,7 @@ graph-editor = { version = "0.1.0" , path = "../lib/graph-editor"
|
||||
shapely = { version = "0.1.0" , path = "../lib/shapely/impl" }
|
||||
|
||||
ast = { version = "0.1.0" , path = "ast/impl" }
|
||||
enso-protocol = { version = "0.1.0" , path = "enso-protocol" }
|
||||
file-manager-client = { version = "0.1.0" , path = "file-manager" }
|
||||
json-rpc = { version = "0.1.0" , path = "json-rpc" }
|
||||
parser = { version = "0.1.0" , path = "parser" }
|
||||
|
20
gui/src/rust/ide/enso-protocol/Cargo.toml
Normal file
20
gui/src/rust/ide/enso-protocol/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "enso-protocol"
|
||||
version = "0.1.0"
|
||||
authors = ["Enso Team <contact@luna-lang.org>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
json-rpc = { version = "0.1.0" , path = "../json-rpc" }
|
||||
enso-prelude = { version = "0.1.0" , path = "../../lib/prelude" }
|
||||
shapely = { version = "0.1.0" , path = "../../lib/shapely/impl" }
|
||||
utils = { version = "0.1.0" , path = "../utils" }
|
||||
|
||||
chrono = { version = "0.4" , features = ["serde"] }
|
||||
futures = { version = "0.3.1" }
|
||||
serde = { version = "1.0" , features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
uuid = { version = "0.8" , features = ["serde", "v5"] }
|
404
gui/src/rust/ide/enso-protocol/src/file_manager.rs
Normal file
404
gui/src/rust/ide/enso-protocol/src/file_manager.rs
Normal file
@ -0,0 +1,404 @@
|
||||
//! Client library for the JSON-RPC-based File Manager service.
|
||||
|
||||
//FIXME: We need to review the structures' names in Enso Protocol specification
|
||||
// https://github.com/luna/enso/issues/708
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::types::UTCDateTime;
|
||||
|
||||
use json_rpc::api::Result;
|
||||
use json_rpc::Handler;
|
||||
use json_rpc::make_rpc_methods;
|
||||
use futures::Stream;
|
||||
use serde::Serialize;
|
||||
use serde::Deserialize;
|
||||
use std::future::Future;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Event ===
|
||||
// =============
|
||||
|
||||
/// Event emitted by the File Manager `Client`.
|
||||
pub type Event = json_rpc::handler::Event<Notification>;
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === Path ===
|
||||
// ============
|
||||
|
||||
// FIXME[dg]: We don't want Path with anonymous fields. This needs to be fixed in the File Manager
|
||||
// Client task.
|
||||
/// Path to a file.
|
||||
#[derive(Clone,Debug,Display,Eq,Hash,PartialEq,PartialOrd,Ord)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Shrinkwrap)]
|
||||
pub struct Path(pub String);
|
||||
|
||||
impl Path {
|
||||
/// Wraps a `String`-like entity into a new `Path`.
|
||||
pub fn new(s:impl Str) -> Path {
|
||||
Path(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === Notification ===
|
||||
// ====================
|
||||
|
||||
/// Notification generated by the File Manager.
|
||||
#[derive(Clone,Debug,PartialEq)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag="method", content="params")]
|
||||
pub enum Notification {
|
||||
/// Filesystem event occurred for a watched path.
|
||||
#[serde(rename = "filesystemEvent")]
|
||||
FilesystemEvent(FilesystemEvent),
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === FilesystemEvent ===
|
||||
// =======================
|
||||
|
||||
/// Filesystem event notification, generated by an active file watch.
|
||||
#[derive(Clone,Debug,PartialEq)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FilesystemEvent {
|
||||
/// Path of the file that the event is about.
|
||||
pub path : Path,
|
||||
#[allow(missing_docs)]
|
||||
pub kind : FilesystemEventKind
|
||||
}
|
||||
|
||||
/// Describes kind of filesystem event (was the file created or deleted, etc.)
|
||||
#[derive(Clone,Copy,Debug,PartialEq)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum FilesystemEventKind {
|
||||
/// A new file under path was created.
|
||||
Created,
|
||||
/// Existing file under path was deleted.
|
||||
Deleted,
|
||||
/// File under path was modified.
|
||||
Modified,
|
||||
/// An overflow occurred and some events were lost,
|
||||
Overflow
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === Attributes ===
|
||||
// ==================
|
||||
|
||||
/// Attributes of the file in the filesystem.
|
||||
#[derive(Clone,Copy,Debug,PartialEq)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Attributes{
|
||||
/// When the file was created.
|
||||
pub creation_time : UTCDateTime,
|
||||
/// When the file was last accessed.
|
||||
pub last_access_time : UTCDateTime,
|
||||
/// When the file was last modified.
|
||||
pub last_modified_time : UTCDateTime,
|
||||
/// What kind of file is this.
|
||||
pub file_kind : FileKind,
|
||||
/// Size of the file in bytes.
|
||||
/// (size of files not being `RegularFile`s is unspecified).
|
||||
pub byte_size : u64
|
||||
}
|
||||
|
||||
/// What kind of file (regular, directory, symlink) is this.
|
||||
#[derive(Clone,Copy,Debug,PartialEq)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum FileKind {
|
||||
/// File being a directory.
|
||||
Directory,
|
||||
/// File being a symbolic link.
|
||||
SymbolicLink,
|
||||
/// File being a regular file with opaque content.
|
||||
RegularFile,
|
||||
/// File being none of the above, e.g. a physical device or a pipe.
|
||||
Other
|
||||
}
|
||||
|
||||
make_rpc_methods! {
|
||||
/// An interface containing all the available file management operations.
|
||||
trait API {
|
||||
/// Copies a specified directory to another location.
|
||||
#[MethodInput=CopyDirectoryInput,rpc_name="file/copy",result=copy_directory_result,set_result=set_copy_directory_result]
|
||||
fn copy_directory(&self, from:Path, to:Path) -> ();
|
||||
|
||||
/// Copies a specified file to another location.
|
||||
#[MethodInput=CopyFileInput,rpc_name="file/copy",result=copy_file_result,set_result=set_copy_file_result]
|
||||
fn copy_file(&self, from:Path, to:Path) -> ();
|
||||
|
||||
/// Deletes the specified file.
|
||||
#[MethodInput=DeleteFileInput,rpc_name="file/delete",result=delete_file_result,
|
||||
set_result=set_delete_file_result]
|
||||
fn delete_file(&self, path:Path) -> ();
|
||||
|
||||
/// Check if file exists.
|
||||
#[MethodInput=ExistsInput,rpc_name="file/exists",result=exists_result,
|
||||
set_result=set_exists_result]
|
||||
fn exists(&self, path:Path) -> bool;
|
||||
|
||||
/// List all file-system objects in the specified path.
|
||||
#[MethodInput=ListInput,rpc_name="file/list",result=list_result,set_result=set_list_result]
|
||||
fn list(&self, path:Path) -> Vec<Path>;
|
||||
|
||||
/// Moves directory to another location.
|
||||
#[MethodInput=MoveDirectoryInput,rpc_name="file/move",result=move_directory_result,
|
||||
set_result=set_move_directory_result]
|
||||
fn move_directory(&self, from:Path, to:Path) -> ();
|
||||
|
||||
/// Moves file to another location.
|
||||
#[MethodInput=MoveFileInput,rpc_name="file/move",result=move_file_result,
|
||||
set_result=set_move_file_result]
|
||||
fn move_file(&self, from:Path, to:Path) -> ();
|
||||
|
||||
/// Reads file's content as a String.
|
||||
#[MethodInput=ReadInput,rpc_name="file/read",result=read_result,set_result=set_read_result]
|
||||
fn read(&self, path:Path) -> String;
|
||||
|
||||
/// Gets file's status.
|
||||
#[MethodInput=StatusInput,rpc_name="file/status",result=status_result,set_result=set_status_result]
|
||||
fn status(&self, path:Path) -> Attributes;
|
||||
|
||||
/// Creates a file in the specified path.
|
||||
#[MethodInput=TouchInput,rpc_name="file/touch",result=touch_result,set_result=set_touch_result]
|
||||
fn touch(&self, path:Path) -> ();
|
||||
|
||||
/// Writes String contents to a file in the specified path.
|
||||
#[MethodInput=WriteInput,rpc_name="file/write",result=write_result,set_result=set_write_result]
|
||||
fn write(&self, path:Path, contents:String) -> ();
|
||||
|
||||
/// Watches the specified path.
|
||||
#[MethodInput=CreateWatchInput,rpc_name="file/createWatch",result=create_watch_result,set_result=set_create_watch_result]
|
||||
fn create_watch(&self, path:Path) -> Uuid;
|
||||
|
||||
/// Delete the specified watcher.
|
||||
#[MethodInput=DeleteWatchInput,rpc_name="file/deleteWatch",result=delete_watch_result,
|
||||
set_result=set_delete_watch_result]
|
||||
fn delete_watch(&self, watch_id:Uuid) -> ();
|
||||
}}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::FileKind::RegularFile;
|
||||
|
||||
use futures::task::LocalSpawnExt;
|
||||
use json_rpc::messages::Message;
|
||||
use json_rpc::messages::RequestMessage;
|
||||
use json_rpc::test_util::transport::mock::MockTransport;
|
||||
use serde_json::json;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use utils::test::poll_future_output;
|
||||
use utils::test::poll_stream_output;
|
||||
|
||||
struct Fixture {
|
||||
transport : MockTransport,
|
||||
client : Client,
|
||||
executor : futures::executor::LocalPool,
|
||||
}
|
||||
|
||||
fn setup_file_manager() -> Fixture {
|
||||
let transport = MockTransport::new();
|
||||
let client = Client::new(transport.clone());
|
||||
let executor = futures::executor::LocalPool::new();
|
||||
executor.spawner().spawn_local(client.runner()).unwrap();
|
||||
Fixture {transport,client,executor}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notification() {
|
||||
let mut fixture = setup_file_manager();
|
||||
let mut events = Box::pin(fixture.client.events());
|
||||
assert!(poll_stream_output(&mut events).is_none());
|
||||
|
||||
let expected_notification = FilesystemEvent {
|
||||
path : Path::new("./Main.txt"),
|
||||
kind : FilesystemEventKind::Modified,
|
||||
};
|
||||
let notification_text = r#"{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "filesystemEvent",
|
||||
"params": {"path" : "./Main.txt", "kind" : "Modified"}
|
||||
}"#;
|
||||
fixture.transport.mock_peer_message_text(notification_text);
|
||||
assert!(poll_stream_output(&mut events).is_none());
|
||||
|
||||
fixture.executor.run_until_stalled();
|
||||
|
||||
let event = poll_stream_output(&mut events);
|
||||
if let Some(Event::Notification(n)) = event {
|
||||
assert_eq!(n, Notification::FilesystemEvent(expected_notification));
|
||||
} else {
|
||||
panic!("expected notification event");
|
||||
}
|
||||
}
|
||||
|
||||
/// This function tests making a request using file manager. It
|
||||
/// * creates FM client and uses `make_request` to make a request,
|
||||
/// * checks that request is made for `expected_method`,
|
||||
/// * checks that request input is `expected_input`,
|
||||
/// * mocks receiving a response from server with `result` and
|
||||
/// * checks that FM-returned Future yields `expected_output`.
|
||||
fn test_request<Fun, Fut, T>
|
||||
( make_request:Fun
|
||||
, expected_method:&str
|
||||
, expected_input:Value
|
||||
, result:Value
|
||||
, expected_output:T )
|
||||
where Fun : FnOnce(&mut Client) -> Fut,
|
||||
Fut : Future<Output = Result<T>>,
|
||||
T : Debug + PartialEq {
|
||||
let mut fixture = setup_file_manager();
|
||||
let mut request_future = Box::pin(make_request(&mut fixture.client));
|
||||
|
||||
let request = fixture.transport.expect_message::<RequestMessage<Value>>();
|
||||
assert_eq!(request.method, expected_method);
|
||||
assert_eq!(request.params, expected_input);
|
||||
|
||||
let response = Message::new_success(request.id, result);
|
||||
fixture.transport.mock_peer_message(response);
|
||||
fixture.executor.run_until_stalled();
|
||||
let output = poll_future_output(&mut request_future).unwrap().unwrap();
|
||||
assert_eq!(output, expected_output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requests() {
|
||||
let main = Path::new("./Main.txt");
|
||||
let target = Path::new("./Target.txt");
|
||||
let path_main = json!({"path" : "./Main.txt"});
|
||||
let from_main_to_target = json!({
|
||||
"from" : "./Main.txt",
|
||||
"to" : "./Target.txt"
|
||||
});
|
||||
let true_json = json!(true);
|
||||
let unit_json = json!(null);
|
||||
|
||||
test_request(
|
||||
|client| client.copy_directory(main.clone(), target.clone()),
|
||||
"file/copy",
|
||||
from_main_to_target.clone(),
|
||||
unit_json.clone(),
|
||||
());
|
||||
test_request(
|
||||
|client| client.copy_file(main.clone(), target.clone()),
|
||||
"file/copy",
|
||||
from_main_to_target.clone(),
|
||||
unit_json.clone(),
|
||||
());
|
||||
test_request(
|
||||
|client| client.delete_file(main.clone()),
|
||||
"file/delete",
|
||||
path_main.clone(),
|
||||
unit_json.clone(),
|
||||
());
|
||||
test_request(
|
||||
|client| client.exists(main.clone()),
|
||||
"file/exists",
|
||||
path_main.clone(),
|
||||
true_json,
|
||||
true);
|
||||
|
||||
let list_response_json = json!([ "Bar.txt", "Foo.txt" ]);
|
||||
let list_response_value = vec! [Path::new("Bar.txt"),Path::new("Foo.txt")];
|
||||
test_request(
|
||||
|client| client.list(main.clone()),
|
||||
"file/list",
|
||||
path_main.clone(),
|
||||
list_response_json,
|
||||
list_response_value);
|
||||
test_request(
|
||||
|client| client.move_directory(main.clone(), target.clone()),
|
||||
"file/move",
|
||||
from_main_to_target.clone(),
|
||||
unit_json.clone(),
|
||||
());
|
||||
test_request(
|
||||
|client| client.move_file(main.clone(), target.clone()),
|
||||
"file/move",
|
||||
from_main_to_target.clone(),
|
||||
unit_json.clone(),
|
||||
());
|
||||
test_request(
|
||||
|client| client.read(main.clone()),
|
||||
"file/read",
|
||||
path_main.clone(),
|
||||
json!("Hello world!"),
|
||||
"Hello world!".into());
|
||||
|
||||
let parse_rfc3339 = |s| {
|
||||
chrono::DateTime::parse_from_rfc3339(s).unwrap()
|
||||
};
|
||||
let expected_attributes = Attributes {
|
||||
creation_time : parse_rfc3339("2020-01-07T21:25:26Z"),
|
||||
last_access_time : parse_rfc3339("2020-01-21T22:16:51.123994500+00:00"),
|
||||
last_modified_time : parse_rfc3339("2020-01-07T21:25:26Z"),
|
||||
file_kind : RegularFile,
|
||||
byte_size : 125125,
|
||||
};
|
||||
let sample_attributes_json = json!({
|
||||
"creationTime" : "2020-01-07T21:25:26Z",
|
||||
"lastAccessTime" : "2020-01-21T22:16:51.123994500+00:00",
|
||||
"lastModifiedTime" : "2020-01-07T21:25:26Z",
|
||||
"fileKind" : "RegularFile",
|
||||
"byteSize" : 125125
|
||||
});
|
||||
test_request(
|
||||
|client| client.status(main.clone()),
|
||||
"file/status",
|
||||
path_main.clone(),
|
||||
sample_attributes_json,
|
||||
expected_attributes);
|
||||
test_request(
|
||||
|client| client.touch(main.clone()),
|
||||
"file/touch",
|
||||
path_main.clone(),
|
||||
unit_json.clone(),
|
||||
());
|
||||
test_request(
|
||||
|client| client.write(main.clone(), "Hello world!".into()),
|
||||
"file/write",
|
||||
json!({"path" : "./Main.txt", "contents" : "Hello world!"}),
|
||||
unit_json.clone(),
|
||||
());
|
||||
|
||||
let uuid_value = uuid::Uuid::parse_str("02723954-fbb0-4641-af53-cec0883f260a").unwrap();
|
||||
let uuid_json = json!("02723954-fbb0-4641-af53-cec0883f260a");
|
||||
test_request(
|
||||
|client| client.create_watch(main.clone()),
|
||||
"file/createWatch",
|
||||
path_main.clone(),
|
||||
uuid_json.clone(),
|
||||
uuid_value);
|
||||
let watch_id = json!({
|
||||
"watchId" : "02723954-fbb0-4641-af53-cec0883f260a"
|
||||
});
|
||||
test_request(
|
||||
|client| client.delete_watch(uuid_value.clone()),
|
||||
"file/deleteWatch",
|
||||
watch_id.clone(),
|
||||
unit_json.clone(),
|
||||
());
|
||||
}
|
||||
}
|
16
gui/src/rust/ide/enso-protocol/src/lib.rs
Normal file
16
gui/src/rust/ide/enso-protocol/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
||||
//! Client side implementation of Enso protocol.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![warn(trivial_casts)]
|
||||
#![warn(trivial_numeric_casts)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![warn(unused_qualifications)]
|
||||
#![warn(unsafe_code)]
|
||||
#![warn(missing_copy_implementations)]
|
||||
#![warn(missing_debug_implementations)]
|
||||
|
||||
pub mod types;
|
||||
pub mod file_manager;
|
||||
pub mod project_manager;
|
||||
|
||||
pub use enso_prelude as prelude;
|
384
gui/src/rust/ide/enso-protocol/src/project_manager.rs
Normal file
384
gui/src/rust/ide/enso-protocol/src/project_manager.rs
Normal file
@ -0,0 +1,384 @@
|
||||
//! Client library for the JSON-RPC-based Project Manager service.
|
||||
|
||||
//FIXME: We need to review the structures' names in Enso Protocol specification
|
||||
// https://github.com/luna/enso/issues/708
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::types::UTCDateTime;
|
||||
use json_rpc::api::Result;
|
||||
use json_rpc::Handler;
|
||||
use json_rpc::make_rpc_methods;
|
||||
use futures::Stream;
|
||||
use serde::Serialize;
|
||||
use serde::Deserialize;
|
||||
use std::future::Future;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Event ===
|
||||
// =============
|
||||
|
||||
// Project Manager has no notifications, so we create a dummy Notification type for it.
|
||||
type Notification = ();
|
||||
|
||||
/// Event emitted by the Project Manager `Client`.
|
||||
pub type Event = json_rpc::handler::Event<Notification>;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === RPC Methods ===
|
||||
// ===================
|
||||
|
||||
make_rpc_methods! {
|
||||
/// An interface containing all the available project management operations.
|
||||
trait API {
|
||||
/// Request the project picker to open a specified project. This operation also
|
||||
/// includes spawning an instance of the language server open on the specified project.
|
||||
#[MethodInput=OpenProjectInput,rpc_name="project/open",result=open_project_result,
|
||||
set_result=set_open_project_result]
|
||||
fn open_project(&self, project_id:Uuid) -> OpenProjectResponse;
|
||||
|
||||
/// Request the project picker to close a specified project. This operation
|
||||
/// includes shutting down the language server gracefully so that it can persist state to disk
|
||||
/// as needed.
|
||||
#[MethodInput=CloseProjectInput,rpc_name="project/close",result=close_project_result,
|
||||
set_result=set_close_project_result]
|
||||
fn close_project(&self, project_id:Uuid) -> ();
|
||||
|
||||
/// Request the project picker to list the user's most recently opened projects.
|
||||
#[MethodInput=ListRecentProjectsInput,rpc_name="project/listRecent",
|
||||
result=list_recent_projects_result,set_result=set_list_recent_projects_result]
|
||||
fn list_recent_projects(&self, number_of_projects:u32) -> ProjectListResponse;
|
||||
|
||||
/// Request the creation of a new project.
|
||||
#[MethodInput=CreateProjectInput,rpc_name="project/create",result=create_project_result,
|
||||
set_result=set_create_project_result]
|
||||
fn create_project(&self, name:String) -> CreateProjectResponse;
|
||||
|
||||
/// Request the deletion of a project.
|
||||
#[MethodInput=DeleteProjectInput,rpc_name="project/delete",result=delete_project_result,
|
||||
set_result=set_delete_project_result]
|
||||
fn delete_project(&self, project_id:Uuid) -> ();
|
||||
|
||||
/// Request a list of sample projects that are available to the user.
|
||||
#[MethodInput=ListSamplesInput,rpc_name="project/listSample",result=list_samples_result,
|
||||
set_result=set_list_samples_result]
|
||||
fn list_samples(&self, num_projects:u32) -> ProjectListResponse;
|
||||
}}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
// =============
|
||||
|
||||
/// Address consisting of host and port.
|
||||
#[derive(Debug,Clone,Serialize,Deserialize,PartialEq)]
|
||||
pub struct IpWithSocket {
|
||||
/// Host name.
|
||||
pub host : String,
|
||||
/// Port number.
|
||||
pub port : u16
|
||||
}
|
||||
|
||||
/// Project name.
|
||||
#[derive(Debug,Clone,Serialize,Deserialize,PartialEq,Shrinkwrap)]
|
||||
pub struct ProjectName {
|
||||
#[allow(missing_docs)]
|
||||
pub name : String
|
||||
}
|
||||
|
||||
/// Project information, such as name, its id and last time it was opened.
|
||||
#[derive(Debug,Clone,Serialize,Deserialize,PartialEq)]
|
||||
pub struct ProjectMetadata {
|
||||
/// Project's name.
|
||||
#[serde(flatten)]
|
||||
pub name : ProjectName,
|
||||
/// Project's uuid.
|
||||
pub id : Uuid,
|
||||
/// Last time the project was opened.
|
||||
pub last_opened : Option<UTCDateTime>
|
||||
}
|
||||
|
||||
/// Response of `list_recent_projects` and `list_samples`.
|
||||
#[derive(Debug,Clone,Serialize,Deserialize,PartialEq)]
|
||||
pub struct ProjectListResponse {
|
||||
/// List of projects.
|
||||
pub projects : Vec<ProjectMetadata>
|
||||
}
|
||||
|
||||
/// Response of `create_project`.
|
||||
#[derive(Debug,Clone,Copy,Serialize,Deserialize,PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateProjectResponse {
|
||||
/// Created project uuid.
|
||||
pub project_id : Uuid
|
||||
}
|
||||
|
||||
/// Response of `open_project`.
|
||||
#[derive(Debug,Clone,Serialize,Deserialize,PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenProjectResponse {
|
||||
/// Language server address.
|
||||
pub language_server_address : IpWithSocket
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========================
|
||||
// === MockClient tests ===
|
||||
// ========================
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock_client_tests {
|
||||
use super::*;
|
||||
use chrono::DateTime;
|
||||
use json_rpc::error::RpcError;
|
||||
use json_rpc::messages::Error;
|
||||
use json_rpc::Result;
|
||||
use std::future::Future;
|
||||
use utils::test::poll_future_output;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn error<T>(message:&str) -> Result<T> {
|
||||
let code = 1;
|
||||
let data = None;
|
||||
let message = message.to_string();
|
||||
let error = Error {code,data,message};
|
||||
Err(RpcError::RemoteError(error))
|
||||
}
|
||||
|
||||
fn result<T,F:Future<Output = Result<T>>>(fut:F) -> Result<T> {
|
||||
let mut fut = Box::pin(fut);
|
||||
poll_future_output(&mut fut).expect("Promise isn't ready")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_life_cycle() {
|
||||
let mock_client = MockClient::default();
|
||||
let expected_uuid = Uuid::default();
|
||||
let creation_response = CreateProjectResponse { project_id : expected_uuid.clone() };
|
||||
let host = "localhost".to_string();
|
||||
let port = 30500;
|
||||
let language_server_address = IpWithSocket {host,port};
|
||||
let expected_ip_with_socket = OpenProjectResponse { language_server_address };
|
||||
let open_result = Ok(expected_ip_with_socket.clone());
|
||||
mock_client.set_create_project_result("HelloWorld".into(),Ok(creation_response));
|
||||
mock_client.set_open_project_result(expected_uuid.clone(),open_result);
|
||||
mock_client.set_close_project_result(expected_uuid.clone(),error("Project isn't open."));
|
||||
mock_client.set_delete_project_result(expected_uuid.clone(),error("Project doesn't exist."));
|
||||
|
||||
let delete_result = mock_client.delete_project(expected_uuid.clone());
|
||||
result(delete_result).expect_err("Project shouldn't exist.");
|
||||
|
||||
let creation_response = mock_client.create_project("HelloWorld".into());
|
||||
let uuid = result(creation_response).expect("Couldn't create project").project_id;
|
||||
assert_eq!(uuid, expected_uuid);
|
||||
|
||||
let close_result = result(mock_client.close_project(uuid.clone()));
|
||||
close_result.expect_err("Project shouldn't be open.");
|
||||
|
||||
let ip_with_socket = result(mock_client.open_project(uuid.clone()));
|
||||
let ip_with_socket = ip_with_socket.expect("Couldn't open project");
|
||||
assert_eq!(ip_with_socket, expected_ip_with_socket);
|
||||
|
||||
mock_client.set_close_project_result(expected_uuid.clone(), Ok(()));
|
||||
result(mock_client.close_project(uuid)).expect("Couldn't close project.");
|
||||
|
||||
mock_client.set_delete_project_result(expected_uuid.clone(), Ok(()));
|
||||
result(mock_client.delete_project(uuid)).expect("Couldn't delete project.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_recent_projects() {
|
||||
let mock_client = MockClient::default();
|
||||
let project1 = ProjectMetadata {
|
||||
name : ProjectName { name : "project1".to_string() },
|
||||
id : Uuid::default(),
|
||||
last_opened : Some(DateTime::parse_from_rfc3339("2020-01-07T21:25:26Z").unwrap())
|
||||
};
|
||||
let project2 = ProjectMetadata {
|
||||
name : ProjectName { name : "project2".to_string() },
|
||||
id : Uuid::default(),
|
||||
last_opened : Some(DateTime::parse_from_rfc3339("2020-02-02T13:15:20Z").unwrap())
|
||||
};
|
||||
let expected_recent_projects = ProjectListResponse { projects : vec![project1,project2] };
|
||||
let sample1 = ProjectMetadata {
|
||||
name : ProjectName { name : "sample1".to_string() },
|
||||
id : Uuid::default(),
|
||||
last_opened : Some(DateTime::parse_from_rfc3339("2019-11-23T05:30:12Z").unwrap())
|
||||
};
|
||||
let sample2 = ProjectMetadata {
|
||||
name : ProjectName { name : "sample2".to_string() },
|
||||
id : Uuid::default(),
|
||||
last_opened : Some(DateTime::parse_from_rfc3339("2019-12-25T00:10:58Z").unwrap())
|
||||
};
|
||||
let expected_sample_projects = ProjectListResponse { projects : vec![sample1,sample2] };
|
||||
mock_client.set_list_recent_projects_result(2,Ok(expected_recent_projects.clone()));
|
||||
mock_client.set_list_samples_result(2,Ok(expected_sample_projects.clone()));
|
||||
|
||||
let list_recent_error = "Couldn't get recent projects.";
|
||||
let list_sample_error = "Couldn't get sample projects.";
|
||||
let recent_projects = result(mock_client.list_recent_projects(2)).expect(list_recent_error);
|
||||
assert_eq!(recent_projects, expected_recent_projects);
|
||||
let sample_projects = result(mock_client.list_samples(2)).expect(list_sample_error);
|
||||
assert_eq!(sample_projects, expected_sample_projects);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === Client tests ===
|
||||
// ====================
|
||||
|
||||
#[cfg(test)]
|
||||
mod remote_client_tests {
|
||||
use super::*;
|
||||
|
||||
use chrono::DateTime;
|
||||
use json_rpc::messages::Message;
|
||||
use json_rpc::messages::RequestMessage;
|
||||
use json_rpc::test_util::transport::mock::MockTransport;
|
||||
use serde_json::json;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use utils::test::poll_future_output;
|
||||
use futures::task::LocalSpawnExt;
|
||||
|
||||
struct Fixture {
|
||||
transport : MockTransport,
|
||||
client : Client,
|
||||
executor : futures::executor::LocalPool,
|
||||
}
|
||||
|
||||
fn setup_fm() -> Fixture {
|
||||
let transport = MockTransport::new();
|
||||
let client = Client::new(transport.clone());
|
||||
let executor = futures::executor::LocalPool::new();
|
||||
executor.spawner().spawn_local(client.runner()).unwrap();
|
||||
Fixture {transport,client,executor}
|
||||
}
|
||||
|
||||
/// Tests making a request using file manager:
|
||||
/// * creates PM client and uses `make_request` to make a request
|
||||
/// * checks that request is made for `expected_method`
|
||||
/// * checks that request input is `expected_input`
|
||||
/// * mocks receiving a response from server with `result`
|
||||
/// * checks that FM-returned Future yields `expected_output`
|
||||
fn test_request<Fun, Fut, T>
|
||||
( make_request : Fun
|
||||
, expected_method : &str
|
||||
, expected_input : &Value
|
||||
, result : &Value
|
||||
, expected_output : &T)
|
||||
where Fun : FnOnce(&mut Client) -> Fut,
|
||||
Fut : Future<Output = Result<T>>,
|
||||
T : Debug + PartialEq {
|
||||
let mut fixture = setup_fm();
|
||||
let mut fut = Box::pin(make_request(&mut fixture.client));
|
||||
|
||||
let request = fixture.transport.expect_message::<RequestMessage<Value>>();
|
||||
assert_eq!(request.method, *expected_method);
|
||||
assert_eq!(request.params, *expected_input);
|
||||
|
||||
let response = Message::new_success(request.id, result);
|
||||
fixture.transport.mock_peer_message(response);
|
||||
fixture.executor.run_until_stalled();
|
||||
let output = poll_future_output(&mut fut).unwrap().unwrap();
|
||||
assert_eq!(output, *expected_output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requests() {
|
||||
let unit_json = json!(null);
|
||||
let project_id = Uuid::default();
|
||||
let create_project_response = CreateProjectResponse { project_id };
|
||||
let project_id_json = json!({"projectId":"00000000-0000-0000-0000-000000000000"});
|
||||
let language_server_address = IpWithSocket{host:"localhost".to_string(),port:27015};
|
||||
let ip_with_address = OpenProjectResponse { language_server_address };
|
||||
let ip_with_address_json = json!({
|
||||
"languageServerAddress" : {
|
||||
"host" : "localhost",
|
||||
"port" : 27015
|
||||
}
|
||||
});
|
||||
let project_name = String::from("HelloWorld");
|
||||
let project_name_json = json!({"name":serde_json::to_value(&project_name).unwrap()});
|
||||
let number_of_projects = 2;
|
||||
let number_of_projects_json = json!({"numberOfProjects":number_of_projects});
|
||||
let num_projects_json = json!({"numProjects":number_of_projects});
|
||||
let project1 = ProjectMetadata {
|
||||
name : ProjectName { name : "project1".to_string() },
|
||||
id : Uuid::default(),
|
||||
last_opened : Some(DateTime::parse_from_rfc3339("2020-01-07T21:25:26Z").unwrap())
|
||||
};
|
||||
let project2 = ProjectMetadata {
|
||||
name : ProjectName { name : "project2".to_string() },
|
||||
id : Uuid::default(),
|
||||
last_opened : Some(DateTime::parse_from_rfc3339("2020-02-02T13:15:20Z").unwrap())
|
||||
};
|
||||
let project_list = ProjectListResponse { projects : vec![project1,project2] };
|
||||
let project_list_json = json!({
|
||||
"projects" : [
|
||||
{
|
||||
"id" : "00000000-0000-0000-0000-000000000000",
|
||||
"last_opened" : "2020-01-07T21:25:26+00:00",
|
||||
"name" : "project1"
|
||||
},
|
||||
{
|
||||
"id" : "00000000-0000-0000-0000-000000000000",
|
||||
"last_opened" : "2020-02-02T13:15:20+00:00",
|
||||
"name" : "project2"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
test_request(
|
||||
|client| client.list_recent_projects(number_of_projects),
|
||||
"project/listRecent",
|
||||
&number_of_projects_json,
|
||||
&project_list_json,
|
||||
&project_list
|
||||
);
|
||||
test_request(
|
||||
|client| client.list_samples(number_of_projects),
|
||||
"project/listSample",
|
||||
&num_projects_json,
|
||||
&project_list_json,
|
||||
&project_list
|
||||
);
|
||||
test_request(
|
||||
|client| client.open_project(project_id.clone()),
|
||||
"project/open",
|
||||
&project_id_json,
|
||||
&ip_with_address_json,
|
||||
&ip_with_address
|
||||
);
|
||||
test_request(
|
||||
|client| client.close_project(project_id.clone()),
|
||||
"project/close",
|
||||
&project_id_json,
|
||||
&unit_json,
|
||||
&()
|
||||
);
|
||||
test_request(
|
||||
|client| client.delete_project(project_id.clone()),
|
||||
"project/delete",
|
||||
&project_id_json,
|
||||
&unit_json,
|
||||
&()
|
||||
);
|
||||
test_request(
|
||||
|client| client.create_project(project_name.clone()),
|
||||
"project/create",
|
||||
&project_name_json,
|
||||
&project_id_json,
|
||||
&create_project_response
|
||||
);
|
||||
}
|
||||
}
|
4
gui/src/rust/ide/enso-protocol/src/types.rs
Normal file
4
gui/src/rust/ide/enso-protocol/src/types.rs
Normal file
@ -0,0 +1,4 @@
|
||||
//! Common types of JSON-RPC-based Enso services used by both Project Manager and File Manager.
|
||||
|
||||
/// Time in UTC time zone.
|
||||
pub type UTCDateTime = chrono::DateTime<chrono::FixedOffset>;
|
@ -15,6 +15,7 @@
|
||||
pub mod api;
|
||||
pub mod error;
|
||||
pub mod handler;
|
||||
pub mod macros;
|
||||
pub mod messages;
|
||||
pub mod test_util;
|
||||
pub mod transport;
|
||||
|
136
gui/src/rust/ide/json-rpc/src/macros.rs
Normal file
136
gui/src/rust/ide/json-rpc/src/macros.rs
Normal file
@ -0,0 +1,136 @@
|
||||
//! Helper macros to generate RemoteClient and MockClient.
|
||||
|
||||
// FIXME[dg]: https://github.com/luna/ide/issues/401 We want to make the generated methods to
|
||||
// take references instead of ownership.
|
||||
/// This macro reads a `trait API` item and generates asynchronous methods for RPCs. Each method
|
||||
/// should be signed with `MethodInput`, `rpc_name`, `result` and `set_result` attributes. e.g.:
|
||||
/// ```rust,compile_fail
|
||||
/// make_rpc_method!{
|
||||
/// trait API {
|
||||
/// #[MethodInput=CallMePleaseInput,camelCase=callMePlease,result=call_me_please_result,
|
||||
/// set_result=set_call_me_please_result]
|
||||
/// fn call_me_please(&self, my_number_is:String) -> ();
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This macro generates an `API` trait and creates two structs implementing `API`
|
||||
/// called `Client`, with the actual RPC methods, and `MockClient`, with mocked methods with
|
||||
/// return types setup by:
|
||||
/// ```rust,compile_fail
|
||||
/// fn set_call_me_please_result
|
||||
/// (&mut self, my_number_is:String,result:json_rpc::api::Result<()>) { /* impl */ }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! make_rpc_methods {
|
||||
(
|
||||
$(#[doc = $impl_doc:expr])+
|
||||
trait API {
|
||||
$($(#[doc = $doc:expr])+
|
||||
#[MethodInput=$method_input:ident,rpc_name=$rpc_name:expr,result=$method_result:ident,
|
||||
set_result=$set_result:ident]
|
||||
fn $method:ident(&self $(,$param_name:ident:$param_ty:ty)+) -> $result:ty;
|
||||
)*
|
||||
}
|
||||
) => {
|
||||
// ===========
|
||||
// === API ===
|
||||
// ===========
|
||||
|
||||
$(#[doc = $impl_doc])+
|
||||
pub trait API {
|
||||
$(
|
||||
$(#[doc = $doc])+
|
||||
fn $method(&self $(,$param_name:$param_ty)+)
|
||||
-> std::pin::Pin<Box<dyn Future<Output=Result<$result>>>>;
|
||||
)*
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Client ===
|
||||
// ==============
|
||||
|
||||
$(#[doc = $impl_doc])+
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
/// JSON-RPC protocol handler.
|
||||
handler : RefCell<Handler<Notification>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client that will use given transport.
|
||||
pub fn new(transport:impl json_rpc::Transport + 'static) -> Self {
|
||||
let handler = RefCell::new(Handler::new(transport));
|
||||
Self { handler }
|
||||
}
|
||||
|
||||
/// Asynchronous event stream with notification and errors.
|
||||
///
|
||||
/// On a repeated call, previous stream is closed.
|
||||
pub fn events(&self) -> impl Stream<Item = Event> {
|
||||
self.handler.borrow_mut().handler_event_stream()
|
||||
}
|
||||
|
||||
/// Returns a future that performs any background, asynchronous work needed
|
||||
/// for this Client to correctly work. Should be continually run while the
|
||||
/// `Client` is used. Will end once `Client` is dropped.
|
||||
pub fn runner(&self) -> impl Future<Output = ()> {
|
||||
self.handler.borrow_mut().runner()
|
||||
}
|
||||
}
|
||||
|
||||
impl API for Client {
|
||||
$(fn $method(&self, $($param_name:$param_ty),*)
|
||||
-> std::pin::Pin<Box<dyn Future<Output=Result<$result>>>> {
|
||||
let input = $method_input { $($param_name:$param_name),* };
|
||||
Box::pin(self.handler.borrow().open_request(input))
|
||||
})*
|
||||
}
|
||||
|
||||
$(
|
||||
/// Structure transporting method arguments.
|
||||
#[derive(Serialize,Deserialize,Debug,PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct $method_input {
|
||||
$($param_name : $param_ty),*
|
||||
}
|
||||
|
||||
impl json_rpc::RemoteMethodCall for $method_input {
|
||||
const NAME:&'static str = $rpc_name;
|
||||
type Returned = $result;
|
||||
}
|
||||
)*
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === MockClient ===
|
||||
// ==================
|
||||
|
||||
/// Mock used for tests.
|
||||
#[derive(Debug,Default)]
|
||||
pub struct MockClient {
|
||||
$($method_result : RefCell<HashMap<($($param_ty),+),Result<$result>>>,)*
|
||||
}
|
||||
|
||||
impl API for MockClient {
|
||||
$(fn $method(&self $(,$param_name:$param_ty)+)
|
||||
-> std::pin::Pin<Box<dyn Future<Output=Result<$result>>>> {
|
||||
let mut result = self.$method_result.borrow_mut();
|
||||
let result = result.remove(&($($param_name),+)).unwrap();
|
||||
Box::pin(async move { result })
|
||||
})*
|
||||
}
|
||||
|
||||
impl MockClient {
|
||||
$(
|
||||
/// Sets `$method`'s result to be returned when it is called.
|
||||
pub fn $set_result(&self $(,$param_name:$param_ty)+, result:Result<$result>) {
|
||||
self.$method_result.borrow_mut().insert(($($param_name),+),result);
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
35
gui/src/rust/ide/tests/project_manager.rs
Normal file
35
gui/src/rust/ide/tests/project_manager.rs
Normal file
@ -0,0 +1,35 @@
|
||||
//! Project Manager tests.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enso_protocol::project_manager::API;
|
||||
use enso_protocol::project_manager::Client;
|
||||
use ide::*;
|
||||
use ide::transport::web::WebSocket;
|
||||
|
||||
use wasm_bindgen_test::wasm_bindgen_test_configure;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
|
||||
//#[wasm_bindgen_test::wasm_bindgen_test(async)]
|
||||
#[allow(dead_code)]
|
||||
async fn project_life_cycle() {
|
||||
let ws = WebSocket::new_opened("ws://localhost:30535").await;
|
||||
let ws = ws.expect("Couldn't connect to WebSocket server.");
|
||||
let client = Client::new(ws);
|
||||
let _executor = setup_global_executor();
|
||||
|
||||
executor::global::spawn(client.runner());
|
||||
|
||||
let name = "TestProject".to_string();
|
||||
let creation = client.create_project(name).await.expect("Couldn't create project.");
|
||||
let uuid = creation.project_id;
|
||||
let _address = client.open_project(uuid.clone()).await.expect("Couldn't open project.");
|
||||
client.close_project(uuid.clone()).await.expect("Couldn't close project.");
|
||||
client.delete_project(uuid).await.expect("Couldn't delete project.");
|
||||
client.list_recent_projects(10).await.expect("Couldn't list recent projects.");
|
||||
// FIXME[dg]: project/listSample isn't implemented on the server-side yet.
|
||||
//client.list_samples(10).await.expect("Couldn't list samples.");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user