Original commit: 5bc3b7b3a3
This commit is contained in:
Danilo Guanabara 2020-05-04 15:59:43 -03:00 committed by GitHub
parent 8abda0e5b8
commit 817fd766c6
12 changed files with 1030 additions and 0 deletions

12
gui/docs/enso-protocol.md Normal file
View 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.

View File

@ -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",

View File

@ -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",

View File

@ -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" }

View 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"] }

View 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(),
());
}
}

View 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;

View 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
);
}
}

View 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>;

View File

@ -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;

View 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);
}
)*
}
}
}

View 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.");
}
}