Begin epiphany rewrite

This commit is contained in:
Lucien Greathouse 2018-08-19 17:43:11 -07:00
parent 80b9b7594b
commit 1a2ee07d37
21 changed files with 245 additions and 1670 deletions

View file

@ -42,20 +42,7 @@ fn main() {
None => std::env::current_dir().unwrap(), None => std::env::current_dir().unwrap(),
}; };
let port = { librojo::commands::serve(&project_path);
match sub_matches.value_of("port") {
Some(source) => match source.parse::<u64>() {
Ok(value) => Some(value),
Err(_) => {
eprintln!("Invalid port '{}'", source);
process::exit(1);
},
},
None => None,
}
};
librojo::commands::serve(&project_path, port);
}, },
_ => { _ => {
eprintln!("Please specify a subcommand!"); eprintln!("Please specify a subcommand!");

View file

@ -1,40 +1,36 @@
use std::path::PathBuf; use std::path::Path;
use std::process; use std::process;
use std::fs; use std::fs;
use rand; use rand;
use project::Project; use project::Project;
use web::{self, WebConfig}; // use web::{self, WebConfig};
use session::Session; // use session::Session;
use roblox_studio; use roblox_studio;
pub fn serve(project_dir: &PathBuf, override_port: Option<u64>) { pub fn serve(fuzzy_project_location: &Path) {
let server_id = rand::random::<u64>(); let server_id = rand::random::<u64>();
let project = match Project::load(project_dir) { let project = match Project::load_fuzzy(fuzzy_project_location) {
Ok(v) => { Ok(project) => {
println!("Using project from {}", fs::canonicalize(project_dir).unwrap().display()); println!("Using project from {}", fs::canonicalize(&project.file_location).unwrap().display());
v project
}, },
Err(err) => { Err(error) => {
eprintln!("{}", err); eprintln!("{}", error);
process::exit(1); process::exit(1);
}, },
}; };
let port = override_port.unwrap_or(project.serve_port);
println!("Using project {:#?}", project); println!("Using project {:#?}", project);
roblox_studio::install_bundled_plugin().unwrap(); roblox_studio::install_bundled_plugin().unwrap();
let mut session = Session::new(project.clone()); // let mut session = Session::new(project.clone());
session.start(); // session.start();
let web_config = WebConfig::from_session(server_id, port, &session); // let web_config = WebConfig::from_session(server_id, port, &session);
println!("Server listening on port {}", port); // web::start(web_config);
web::start(web_config);
} }

View file

@ -1,166 +0,0 @@
use std::path::{Path, PathBuf, Component};
use partition::Partition;
// TODO: Change backing data structure to use a single allocation with slices
// taken out of it for each portion
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FileRoute {
pub partition: String,
pub route: Vec<String>,
}
impl FileRoute {
pub fn from_path(path: &Path, partition: &Partition) -> Option<FileRoute> {
assert!(path.is_absolute());
assert!(path.starts_with(&partition.path));
let relative_path = path.strip_prefix(&partition.path).ok()?;
let mut route = Vec::new();
for component in relative_path.components() {
match component {
Component::Normal(piece) => {
route.push(piece.to_string_lossy().into_owned());
},
_ => panic!("Unexpected path component: {:?}", component),
}
}
Some(FileRoute {
partition: partition.name.clone(),
route,
})
}
pub fn parent(&self) -> Option<FileRoute> {
if self.route.len() == 0 {
return None;
}
let mut new_route = self.route.clone();
new_route.pop();
Some(FileRoute {
partition: self.partition.clone(),
route: new_route,
})
}
/// Creates a PathBuf out of the `FileRoute` based on the given partition
/// `Path`.
pub fn to_path_buf(&self, partition_path: &Path) -> PathBuf {
let mut result = partition_path.to_path_buf();
for route_piece in &self.route {
result.push(route_piece);
}
result
}
/// Creates a version of the FileRoute with the given extra pieces appended
/// to the end.
pub fn extended_with(&self, pieces: &[&str]) -> FileRoute {
let mut result = self.clone();
for piece in pieces {
result.route.push(piece.to_string());
}
result
}
pub fn file_name(&self, partition: &Partition) -> String {
if self.route.len() == 0 {
partition.path.file_name().unwrap().to_str().unwrap().to_string()
} else {
self.route.last().unwrap().clone()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(windows)]
const ROOT_PATH: &'static str = "C:\\";
#[cfg(not(windows))]
const ROOT_PATH: &'static str = "/";
#[test]
fn from_path_empty() {
let path = Path::new(ROOT_PATH).join("a/b/c");
let partition = Partition {
name: "foo".to_string(),
path: path.clone(),
target: vec![],
};
let route = FileRoute::from_path(&path, &partition);
assert_eq!(route, Some(FileRoute {
partition: "foo".to_string(),
route: vec![],
}));
}
#[test]
fn from_path_non_empty() {
let partition_path = Path::new(ROOT_PATH).join("a/b/c");
let inside_path = partition_path.join("d");
let partition = Partition {
name: "bar".to_string(),
path: partition_path,
target: vec![],
};
let route = FileRoute::from_path(&inside_path, &partition);
assert_eq!(route, Some(FileRoute {
partition: "bar".to_string(),
route: vec!["d".to_string()],
}));
}
#[test]
fn file_name_empty_route() {
let partition_path = Path::new(ROOT_PATH).join("a/b/c");
let partition = Partition {
name: "bar".to_string(),
path: partition_path,
target: vec![],
};
let route = FileRoute {
partition: "bar".to_string(),
route: vec![],
};
assert_eq!(route.file_name(&partition), "c");
}
#[test]
fn file_name_non_empty_route() {
let partition_path = Path::new(ROOT_PATH).join("a/b/c");
let partition = Partition {
name: "bar".to_string(),
path: partition_path,
target: vec![],
};
let route = FileRoute {
partition: "bar".to_string(),
route: vec!["foo".to_string(), "hello.lua".to_string()],
};
assert_eq!(route.file_name(&partition), "hello.lua");
}
}

View file

@ -11,17 +11,11 @@ extern crate regex;
extern crate tempfile; extern crate tempfile;
pub mod commands; pub mod commands;
pub mod file_route;
pub mod id; pub mod id;
pub mod message_session; pub mod message_session;
pub mod partition;
pub mod partition_watcher;
pub mod pathext; pub mod pathext;
pub mod project; pub mod project;
pub mod rbx; pub mod rbx;
pub mod rbx_session; // pub mod web;
pub mod session;
pub mod vfs_session;
pub mod web;
pub mod web_util; pub mod web_util;
pub mod roblox_studio; pub mod roblox_studio;

View file

@ -1,13 +0,0 @@
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq)]
pub struct Partition {
/// The unique name of this partition, used for debugging.
pub name: String,
/// The path on the filesystem that this partition maps to.
pub path: PathBuf,
/// The route to the Roblox instance that this partition maps to.
pub target: Vec<String>,
}

View file

@ -1,67 +0,0 @@
use std::sync::mpsc::{channel, Sender};
use std::time::Duration;
use std::thread;
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher, watcher};
use partition::Partition;
use vfs_session::FileChange;
use file_route::FileRoute;
const WATCH_TIMEOUT_MS: u64 = 100;
pub struct PartitionWatcher {
pub watcher: RecommendedWatcher,
}
impl PartitionWatcher {
pub fn start_new(partition: Partition, tx: Sender<FileChange>) -> PartitionWatcher {
let (watch_tx, watch_rx) = channel();
let mut watcher = watcher(watch_tx, Duration::from_millis(WATCH_TIMEOUT_MS)).unwrap();
watcher.watch(&partition.path, RecursiveMode::Recursive).unwrap();
thread::spawn(move || {
loop {
match watch_rx.recv() {
Ok(event) => {
let file_change = match event {
DebouncedEvent::Create(path) => {
let route = FileRoute::from_path(&path, &partition).unwrap();
FileChange::Created(route)
},
DebouncedEvent::Write(path) => {
let route = FileRoute::from_path(&path, &partition).unwrap();
FileChange::Updated(route)
},
DebouncedEvent::Remove(path) => {
let route = FileRoute::from_path(&path, &partition).unwrap();
FileChange::Deleted(route)
},
DebouncedEvent::Rename(from_path, to_path) => {
let from_route = FileRoute::from_path(&from_path, &partition).unwrap();
let to_route = FileRoute::from_path(&to_path, &partition).unwrap();
FileChange::Moved(from_route, to_route)
},
_ => continue,
};
match tx.send(file_change) {
Ok(_) => {},
Err(_) => break,
}
},
Err(_) => break,
};
}
});
PartitionWatcher {
watcher,
}
}
pub fn stop(self) {
}
}

View file

@ -1,246 +1,177 @@
use std::collections::HashMap; use std::{
use std::fmt; collections::HashMap,
use std::fs::{self, File}; fmt,
use std::io::{Read, Write}; fs,
use std::path::{Path, PathBuf}; io,
path::{Path, PathBuf},
use rand::{self, Rng}; };
use serde_json; use serde_json;
use partition::Partition; pub static PROJECT_FILENAME: &'static str = "roblox-project.json";
pub static PROJECT_FILENAME: &'static str = "rojo.json"; #[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum SourceProjectNode {
Regular {
#[serde(rename = "$className")]
class_name: String,
#[derive(Debug)] // #[serde(rename = "$ignoreUnknown", default = "false")]
pub enum ProjectLoadError { // ignore_unknown: bool,
DidNotExist(PathBuf),
FailedToOpen(PathBuf), #[serde(flatten)]
FailedToRead(PathBuf), children: HashMap<String, SourceProjectNode>,
InvalidJson(PathBuf, serde_json::Error), },
SyncPoint {
#[serde(rename = "$path")]
path: String,
}
} }
impl fmt::Display for ProjectLoadError { impl SourceProjectNode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode {
match self { match self {
&ProjectLoadError::InvalidJson(ref project_path, ref serde_err) => { SourceProjectNode::Regular { class_name, mut children } => {
write!(f, "Found invalid JSON reading project: {}\nError: {}", project_path.display(), serde_err) let mut new_children = HashMap::new();
for (node_name, node) in children.drain() {
new_children.insert(node_name, node.into_project_node(project_file_location));
}
ProjectNode::Regular {
class_name,
children: new_children,
}
}, },
&ProjectLoadError::FailedToOpen(ref project_path) | SourceProjectNode::SyncPoint { path: source_path } => {
&ProjectLoadError::FailedToRead(ref project_path) => { let path = if Path::new(&source_path).is_absolute() {
write!(f, "Found project file, but failed to read it: {}", project_path.display()) PathBuf::from(source_path)
}, } else {
&ProjectLoadError::DidNotExist(ref project_path) => { let project_folder_location = project_file_location.parent().unwrap();
write!(f, "Could not locate a project file at {}.\nUse 'rojo init' to create one.", project_path.display()) project_folder_location.join(source_path)
}, };
}
}
}
#[derive(Debug)] ProjectNode::SyncPoint {
pub enum ProjectSaveError {
FailedToCreate,
}
#[derive(Debug)]
pub enum ProjectInitError {
AlreadyExists,
FailedToCreate,
FailedToWrite,
}
impl fmt::Display for ProjectInitError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
&ProjectInitError::AlreadyExists => {
write!(f, "A project already exists at that location.")
},
&ProjectInitError::FailedToCreate |
&ProjectInitError::FailedToWrite => {
write!(f, "Failed to write to the given location.")
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceProjectPartition {
/// A slash-separated path to a file or folder, relative to the project's
/// directory.
pub path: String,
/// A dot-separated route to a Roblox instance, relative to game.
pub target: String,
}
/// Represents a Rojo project in the format that's most convenient for users to
/// edit. This should generally line up with `Project`, but can diverge when
/// there's either compatibility shims or when the data structures that Rojo
/// want are too verbose to write in JSON but easy to convert from something
/// else.
//
/// Holds anything that can be configured with `rojo.json`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SourceProject {
pub name: String,
pub serve_port: u64,
pub partitions: HashMap<String, SourceProjectPartition>,
}
impl Default for SourceProject {
fn default() -> SourceProject {
SourceProject {
name: "new-project".to_string(),
serve_port: 8000,
partitions: HashMap::new(),
}
}
}
/// Represents a Rojo project in the format that's convenient for Rojo to work
/// with.
#[derive(Debug, Clone)]
pub struct Project {
/// The path to the project file that this project is associated with.
pub project_path: PathBuf,
/// The name of this project, used for user-facing labels.
pub name: String,
/// The port that this project will run a web server on.
pub serve_port: u64,
/// All of the project's partitions, laid out in an expanded way.
pub partitions: HashMap<String, Partition>,
}
impl Project {
fn from_source_project(source_project: SourceProject, project_path: PathBuf) -> Project {
let mut partitions = HashMap::new();
{
let project_directory = project_path.parent().unwrap();
for (partition_name, partition) in source_project.partitions.into_iter() {
let path = project_directory.join(&partition.path);
let target = partition.target
.split(".")
.map(String::from)
.collect::<Vec<_>>();
partitions.insert(partition_name.clone(), Partition {
path, path,
target,
name: partition_name,
});
} }
},
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SourceProject {
name: String,
tree: HashMap<String, SourceProjectNode>,
}
impl SourceProject {
pub fn into_project(mut self, project_file_location: &Path) -> Project {
let mut tree = HashMap::new();
for (node_name, node) in self.tree.drain() {
tree.insert(node_name, node.into_project_node(project_file_location));
} }
Project { Project {
project_path, name: self.name,
name: source_project.name, tree: tree,
serve_port: source_project.serve_port, file_location: PathBuf::from(project_file_location),
partitions,
} }
} }
}
fn as_source_project(&self) -> SourceProject {
let mut partitions = HashMap::new(); #[derive(Debug)]
pub enum ProjectLoadExactError {
for partition in self.partitions.values() { IoError(io::Error),
let path = partition.path.strip_prefix(&self.project_path) JsonError(serde_json::Error),
.unwrap_or_else(|_| &partition.path) }
.to_str()
.unwrap() impl fmt::Display for ProjectLoadExactError {
.to_string(); fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
match self {
let target = partition.target.join("."); ProjectLoadExactError::IoError(inner) => write!(output, "{}", inner),
ProjectLoadExactError::JsonError(inner) => write!(output, "{}", inner),
partitions.insert(partition.name.clone(), SourceProjectPartition { }
path, }
target, }
});
} #[derive(Debug)]
pub enum ProjectInitError {}
SourceProject {
partitions, impl fmt::Display for ProjectInitError {
name: self.name.clone(), fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
serve_port: self.serve_port, write!(output, "ProjectInitError")
} }
} }
/// Initializes a new project inside the given folder path. #[derive(Debug)]
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> { pub enum ProjectLoadFuzzyError {}
let location = location.as_ref();
let project_path = location.join(PROJECT_FILENAME); impl fmt::Display for ProjectLoadFuzzyError {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
// We abort if the project file already exists. write!(output, "ProjectLoadFuzzyError")
fs::metadata(&project_path) }
.map_err(|_| ProjectInitError::AlreadyExists)?; }
let mut file = File::create(&project_path) #[derive(Debug)]
.map_err(|_| ProjectInitError::FailedToCreate)?; pub enum ProjectSaveError {}
// Try to give the project a meaningful name. impl fmt::Display for ProjectSaveError {
// If we can't, we'll just fall back to a default. fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
let name = match location.file_name() { write!(output, "ProjectSaveError")
Some(v) => v.to_string_lossy().into_owned(), }
None => "new-project".to_string(), }
};
#[derive(Debug)]
// Generate a random port to run the server on. pub enum ProjectNode {
let serve_port = rand::thread_rng().gen_range(2000, 49151); Regular {
class_name: String,
// Configure the project with all of the values we know so far. children: HashMap<String, ProjectNode>,
let source_project = SourceProject {
name, // ignore_unknown: bool,
serve_port, },
partitions: HashMap::new(), SyncPoint {
}; path: PathBuf,
let serialized = serde_json::to_string_pretty(&source_project).unwrap(); },
}
file.write(serialized.as_bytes())
.map_err(|_| ProjectInitError::FailedToWrite)?; #[derive(Debug)]
pub struct Project {
Ok(Project::from_source_project(source_project, project_path)) pub name: String,
} pub tree: HashMap<String, ProjectNode>,
pub file_location: PathBuf,
/// Attempts to load a project from the file named PROJECT_FILENAME from the }
/// given folder.
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> { impl Project {
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME)); pub fn init(_project_folder_location: &Path) -> Result<(), ProjectInitError> {
unimplemented!();
fs::metadata(&project_path) }
.map_err(|_| ProjectLoadError::DidNotExist(project_path.clone()))?;
pub fn locate(_start_location: &Path) -> Option<PathBuf> {
let mut file = File::open(&project_path) unimplemented!();
.map_err(|_| ProjectLoadError::FailedToOpen(project_path.clone()))?; }
let mut contents = String::new(); pub fn load_fuzzy(_fuzzy_project_location: &Path) -> Result<Project, ProjectLoadFuzzyError> {
unimplemented!();
file.read_to_string(&mut contents) }
.map_err(|_| ProjectLoadError::FailedToRead(project_path.clone()))?;
pub fn load_exact(project_file_location: &Path) -> Result<Project, ProjectLoadExactError> {
let source_project = serde_json::from_str(&contents) let contents = fs::read_to_string(project_file_location)
.map_err(|e| ProjectLoadError::InvalidJson(project_path.clone(), e))?; .map_err(ProjectLoadExactError::IoError)?;
Ok(Project::from_source_project(source_project, project_path)) let parsed: SourceProject = serde_json::from_str(&contents)
} .map_err(ProjectLoadExactError::JsonError)?;
/// Saves the given project file to the given folder with the appropriate name. Ok(parsed.into_project(project_file_location))
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> { }
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
pub fn save(&self) -> Result<(), ProjectSaveError> {
let mut file = File::create(&project_path) unimplemented!();
.map_err(|_| ProjectSaveError::FailedToCreate)?; }
let source_project = self.as_source_project(); fn to_source_project(&self) -> SourceProject {
let serialized = serde_json::to_string_pretty(&source_project).unwrap(); unimplemented!();
}
file.write(serialized.as_bytes()).unwrap();
Ok(())
}
} }

View file

@ -1,306 +0,0 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use file_route::FileRoute;
use id::{Id, get_id};
use message_session::{Message, MessageSession};
use partition::Partition;
use project::Project;
use rbx::{RbxInstance, RbxTree, RbxValue};
use vfs_session::{VfsSession, FileItem, FileChange};
static SERVICES: &'static [&'static str] = &[
"Chat",
"Lighting",
"LocalizationService",
"Players",
"ReplicatedFirst",
"ReplicatedStorage",
"ServerScriptService",
"ServerStorage",
"SoundService",
"StarterGui",
"StarterPack",
"StarterPlayer",
"TestService",
"Workspace",
];
fn get_partition_target_class_name(target: &[String]) -> &'static str {
match target.len() {
1 => {
let target_name = &target[0];
for &service in SERVICES {
if service == target_name {
return service;
}
}
"Folder"
},
2 => {
"Folder"
},
_ => "Folder",
}
}
// TODO: Rethink data structure and insertion/update behavior. Maybe break some
// pieces off into a new object?
fn file_to_instances(
file_item: &FileItem,
partition: &Partition,
tree: &mut RbxTree,
instances_by_route: &mut HashMap<FileRoute, Id>,
parent_id: Option<Id>,
) -> (Id, Vec<Id>) {
match file_item {
FileItem::File { contents, route } => {
let primary_id = match instances_by_route.get(&file_item.get_route()) {
Some(&id) => id,
None => {
let id = get_id();
instances_by_route.insert(route.clone(), id);
id
},
};
// This is placeholder logic; this whole function is!
let (class_name, property_key, name) = {
let file_name = match route.route.last() {
Some(v) => v.to_string(),
None => partition.path.file_name().unwrap().to_str().unwrap().to_string()
};
let use_partition_name = route.route.len() == 0;
let partition_name = partition.target.last().unwrap();
fn strip_suffix<'a>(source: &'a str, suffix: &'static str) -> String {
source[..source.len() - suffix.len()].to_string()
}
if file_name.ends_with(".client.lua") {
let name = if use_partition_name {
partition_name.clone()
} else {
strip_suffix(&file_name, ".client.lua")
};
("LocalScript", "Source", name)
} else if file_name.ends_with(".server.lua") {
let name = if use_partition_name {
partition_name.clone()
} else {
strip_suffix(&file_name, ".server.lua")
};
("Script", "Source", name)
} else if file_name.ends_with(".lua") {
let name = if use_partition_name {
partition_name.clone()
} else {
strip_suffix(&file_name, ".lua")
};
("ModuleScript", "Source", name)
} else {
let name = if use_partition_name {
partition_name.clone()
} else {
file_name
};
// TODO: Error/warn/skip instead of falling back
("StringValue", "Value", name)
}
};
let mut properties = HashMap::new();
properties.insert(property_key.to_string(), RbxValue::String { value: contents.clone() });
tree.insert_instance(primary_id, RbxInstance {
name,
class_name: class_name.to_string(),
properties,
children: Vec::new(),
parent: parent_id,
});
(primary_id, vec![primary_id])
},
FileItem::Directory { children, route } => {
let primary_id = match instances_by_route.get(&file_item.get_route()) {
Some(&id) => id,
None => {
let id = get_id();
instances_by_route.insert(route.clone(), id);
id
},
};
let mut child_ids = Vec::new();
let mut changed_ids = vec![primary_id];
for child_file_item in children.values() {
let (child_id, mut child_changed_ids) = file_to_instances(child_file_item, partition, tree, instances_by_route, Some(primary_id));
child_ids.push(child_id);
changed_ids.push(child_id);
// TODO: Should I stop using drain on Vecs of Copyable types?
for id in child_changed_ids.drain(..) {
changed_ids.push(id);
}
}
let class_name = get_partition_target_class_name(&route.route).to_string();
let name = if route.route.len() == 0 {
partition.target.last().unwrap().clone()
} else {
route.file_name(partition)
};
tree.insert_instance(primary_id, RbxInstance {
name,
class_name,
properties: HashMap::new(),
children: child_ids,
parent: parent_id,
});
(primary_id, changed_ids)
},
}
}
pub struct RbxSession {
project: Project,
vfs_session: Arc<RwLock<VfsSession>>,
message_session: MessageSession,
/// The RbxInstance that represents each partition.
// TODO: Can this be removed in favor of instances_by_route?
pub partition_instances: HashMap<String, Id>,
/// Keeps track of all of the instances in the tree
pub tree: RbxTree,
/// A map from files in the VFS to instances loaded in the session.
instances_by_route: HashMap<FileRoute, Id>,
}
impl RbxSession {
pub fn new(project: Project, vfs_session: Arc<RwLock<VfsSession>>, message_session: MessageSession) -> RbxSession {
RbxSession {
project,
vfs_session,
message_session,
partition_instances: HashMap::new(),
tree: RbxTree::new(),
instances_by_route: HashMap::new(),
}
}
pub fn read_partitions(&mut self) {
let vfs_session_arc = self.vfs_session.clone();
let vfs_session = vfs_session_arc.read().unwrap();
for partition in self.project.partitions.values() {
let route = FileRoute {
partition: partition.name.clone(),
route: Vec::new(),
};
let file_item = vfs_session.get_by_route(&route).unwrap();
let parent_id = match route.parent() {
Some(parent_route) => match self.instances_by_route.get(&parent_route) {
Some(&parent_id) => Some(parent_id),
None => None,
},
None => None,
};
let (root_id, _) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id);
self.partition_instances.insert(partition.name.clone(), root_id);
}
}
pub fn handle_change(&mut self, change: &FileChange) {
let vfs_session_arc = self.vfs_session.clone();
let vfs_session = vfs_session_arc.read().unwrap();
match change {
FileChange::Created(route) | FileChange::Updated(route) => {
let file_item = vfs_session.get_by_route(route).unwrap();
let partition = self.project.partitions.get(&route.partition).unwrap();
let parent_id = match route.parent() {
Some(parent_route) => match self.instances_by_route.get(&parent_route) {
Some(&parent_id) => Some(parent_id),
None => None,
},
None => None,
};
let (_, changed_ids) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id);
let messages = changed_ids
.iter()
.map(|&id| Message::InstanceChanged { id })
.collect::<Vec<_>>();
self.message_session.push_messages(&messages);
},
FileChange::Deleted(route) => {
match self.instances_by_route.get(route) {
Some(&id) => {
self.tree.delete_instance(id);
self.instances_by_route.remove(route);
self.message_session.push_messages(&[Message::InstanceChanged { id }]);
},
None => (),
}
},
FileChange::Moved(from_route, to_route) => {
let mut messages = Vec::new();
match self.instances_by_route.get(from_route) {
Some(&id) => {
self.tree.delete_instance(id);
self.instances_by_route.remove(from_route);
messages.push(Message::InstanceChanged { id });
},
None => (),
}
let file_item = vfs_session.get_by_route(to_route).unwrap();
let partition = self.project.partitions.get(&to_route.partition).unwrap();
let parent_id = match to_route.parent() {
Some(parent_route) => match self.instances_by_route.get(&parent_route) {
Some(&parent_id) => Some(parent_id),
None => None,
},
None => None,
};
let (_, changed_ids) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id);
for id in changed_ids {
messages.push(Message::InstanceChanged { id });
}
self.message_session.push_messages(&messages);
},
}
}
}

View file

@ -1,95 +0,0 @@
use std::sync::{mpsc, Arc, RwLock};
use std::thread;
use message_session::MessageSession;
use partition_watcher::PartitionWatcher;
use project::Project;
use rbx_session::RbxSession;
use vfs_session::VfsSession;
/// Stub trait for middleware
trait Middleware {
}
pub struct Session {
pub project: Project,
vfs_session: Arc<RwLock<VfsSession>>,
rbx_session: Arc<RwLock<RbxSession>>,
message_session: MessageSession,
watchers: Vec<PartitionWatcher>,
}
impl Session {
pub fn new(project: Project) -> Session {
let message_session = MessageSession::new();
let vfs_session = Arc::new(RwLock::new(VfsSession::new(project.clone())));
let rbx_session = Arc::new(RwLock::new(RbxSession::new(project.clone(), vfs_session.clone(), message_session.clone())));
Session {
vfs_session,
rbx_session,
watchers: Vec::new(),
message_session,
project,
}
}
pub fn start(&mut self) {
{
let mut vfs_session = self.vfs_session.write().unwrap();
vfs_session.read_partitions();
}
{
let mut rbx_session = self.rbx_session.write().unwrap();
rbx_session.read_partitions();
}
let (tx, rx) = mpsc::channel();
for partition in self.project.partitions.values() {
let watcher = PartitionWatcher::start_new(partition.clone(), tx.clone());
self.watchers.push(watcher);
}
{
let vfs_session = self.vfs_session.clone();
let rbx_session = self.rbx_session.clone();
thread::spawn(move || {
loop {
match rx.recv() {
Ok(change) => {
{
let mut vfs_session = vfs_session.write().unwrap();
vfs_session.handle_change(&change);
}
{
let mut rbx_session = rbx_session.write().unwrap();
rbx_session.handle_change(&change);
}
},
Err(_) => break,
}
}
});
}
}
pub fn stop(self) {
}
pub fn get_vfs_session(&self) -> Arc<RwLock<VfsSession>> {
self.vfs_session.clone()
}
pub fn get_rbx_session(&self) -> Arc<RwLock<RbxSession>> {
self.rbx_session.clone()
}
pub fn get_message_session(&self) -> MessageSession {
self.message_session.clone()
}
}

View file

@ -1,242 +0,0 @@
use std::collections::HashMap;
use std::io::Read;
use std::fs::{self, File};
use std::mem;
use file_route::FileRoute;
use project::Project;
/// Represents a file or directory that has been read from the filesystem.
#[derive(Debug, Clone)]
pub enum FileItem {
File {
contents: String,
route: FileRoute,
},
Directory {
children: HashMap<String, FileItem>,
route: FileRoute,
},
}
impl FileItem {
pub fn get_route(&self) -> &FileRoute {
match self {
FileItem::File { route, .. } => route,
FileItem::Directory { route, .. } => route,
}
}
}
#[derive(Debug, Clone)]
pub enum FileChange {
Created(FileRoute),
Deleted(FileRoute),
Updated(FileRoute),
Moved(FileRoute, FileRoute),
}
pub struct VfsSession {
pub project: Project,
/// The in-memory files associated with each partition.
pub partition_files: HashMap<String, FileItem>,
}
impl VfsSession {
pub fn new(project: Project) -> VfsSession {
VfsSession {
project,
partition_files: HashMap::new(),
}
}
pub fn read_partitions(&mut self) {
for partition_name in self.project.partitions.keys() {
let route = FileRoute {
partition: partition_name.clone(),
route: Vec::new(),
};
let file_item = self.read(&route).expect("Couldn't load partitions");
self.partition_files.insert(partition_name.clone(), file_item);
}
}
pub fn handle_change(&mut self, change: &FileChange) -> Option<()> {
match change {
FileChange::Created(route) | FileChange::Updated(route) => {
let new_item = self.read(&route).ok()?;
self.set_file_item(new_item);
},
FileChange::Deleted(route) => {
self.delete_route(&route);
},
FileChange::Moved(from_route, to_route) => {
let new_item = self.read(&to_route).ok()?;
self.delete_route(&from_route);
self.set_file_item(new_item);
},
}
None
}
pub fn get_by_route(&self, route: &FileRoute) -> Option<&FileItem> {
let partition = self.partition_files.get(&route.partition)?;
let mut current = partition;
for piece in &route.route {
match current {
FileItem::File { .. } => return None,
FileItem::Directory { children, .. } => {
current = children.get(piece)?;
},
}
}
Some(current)
}
pub fn get_by_route_mut(&mut self, route: &FileRoute) -> Option<&mut FileItem> {
let mut current = self.partition_files.get_mut(&route.partition)?;
for piece in &route.route {
let mut next = match { current } {
FileItem::File { .. } => return None,
FileItem::Directory { children, .. } => {
children.get_mut(piece)?
},
};
current = next;
}
Some(current)
}
pub fn set_file_item(&mut self, item: FileItem) {
match self.get_by_route_mut(item.get_route()) {
Some(existing) => {
mem::replace(existing, item);
return;
},
None => {},
}
if item.get_route().route.len() > 0 {
let mut parent_route = item.get_route().clone();
let child_name = parent_route.route.pop().unwrap();
let mut parent_children = HashMap::new();
parent_children.insert(child_name, item);
let parent_item = FileItem::Directory {
route: parent_route,
children: parent_children,
};
self.set_file_item(parent_item);
} else {
self.partition_files.insert(item.get_route().partition.clone(), item);
}
}
pub fn delete_route(&mut self, route: &FileRoute) -> Option<()> {
if route.route.len() == 0 {
self.partition_files.remove(&route.partition);
return Some(());
}
let mut current = self.partition_files.get_mut(&route.partition)?;
for i in 0..(route.route.len() - 1) {
let piece = &route.route[i];
let mut next = match { current } {
FileItem::File { .. } => return None,
FileItem::Directory { children, .. } => {
children.get_mut(piece)?
},
};
current = next;
}
match current {
FileItem::Directory { children, .. } => {
children.remove(route.route.last().unwrap().as_str());
},
_ => {},
}
Some(())
}
fn read(&self, route: &FileRoute) -> Result<FileItem, ()> {
let partition_path = &self.project.partitions.get(&route.partition)
.ok_or(())?.path;
let path = route.to_path_buf(partition_path);
let metadata = fs::metadata(path)
.map_err(|_| ())?;
if metadata.is_dir() {
self.read_directory(route)
} else if metadata.is_file() {
self.read_file(route)
} else {
Err(())
}
}
fn read_file(&self, route: &FileRoute) -> Result<FileItem, ()> {
let partition_path = &self.project.partitions.get(&route.partition)
.ok_or(())?.path;
let path = route.to_path_buf(partition_path);
let mut file = File::open(path)
.map_err(|_| ())?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|_| ())?;
Ok(FileItem::File {
contents,
route: route.clone(),
})
}
fn read_directory(&self, route: &FileRoute) -> Result<FileItem, ()> {
let partition_path = &self.project.partitions.get(&route.partition)
.ok_or(())?.path;
let path = route.to_path_buf(partition_path);
let reader = fs::read_dir(path)
.map_err(|_| ())?;
let mut children = HashMap::new();
for entry in reader {
let entry = entry
.map_err(|_| ())?;
let path = entry.path();
let name = path.file_name().unwrap().to_string_lossy().into_owned();
let child_route = route.extended_with(&[&name]);
let child_item = self.read(&child_route)?;
children.insert(name, child_item);
}
Ok(FileItem::Directory {
children,
route: route.clone(),
})
}
}

View file

@ -0,0 +1,38 @@
#[macro_use] extern crate lazy_static;
extern crate librojo;
use std::{
collections::HashMap,
path::PathBuf,
};
use librojo::{
project::Project,
};
lazy_static! {
static ref TEST_PROJECTS_ROOT: PathBuf = {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../test-projects");
path
};
}
#[test]
fn foo() {
let project_file_location = TEST_PROJECTS_ROOT.join("foo.json");
let project = Project::load_exact(&project_file_location).unwrap();
assert_eq!(project.name, "foo");
assert_eq!(project.tree.len(), 1);
}
#[test]
fn empty() {
let project_file_location = TEST_PROJECTS_ROOT.join("empty/roblox-project.json");
let project = Project::load_exact(&project_file_location).unwrap();
assert_eq!(project.name, "empty");
assert_eq!(project.tree.len(), 0);
}

View file

@ -1,483 +0,0 @@
#[macro_use] extern crate lazy_static;
extern crate rouille;
extern crate serde_json;
extern crate serde;
extern crate tempfile;
extern crate walkdir;
extern crate librojo;
mod test_util;
use test_util::*;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::{File, remove_file};
use std::io::Write;
use std::path::PathBuf;
use librojo::{
session::Session,
project::Project,
web::{Server, WebConfig, ServerInfoResponse, ReadResponse, ReadAllResponse, SubscribeResponse},
rbx::RbxValue,
};
lazy_static! {
static ref TEST_PROJECTS_ROOT: PathBuf = {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../test-projects");
path
};
}
#[test]
fn empty() {
let original_project_path = TEST_PROJECTS_ROOT.join("empty");
let project_tempdir = tempfile::tempdir().unwrap();
let project_path = project_tempdir.path();
copy_recursive(&original_project_path, &project_path).unwrap();
let project = Project::load(&project_path).unwrap();
let mut session = Session::new(project.clone());
session.start();
let web_config = WebConfig::from_session(0, project.serve_port, &session);
let server = Server::new(web_config);
{
let body = server.get_string("/api/rojo");
let response = serde_json::from_str::<ServerInfoResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.protocol_version, 2);
assert_eq!(response.partitions.len(), 0);
}
{
let body = server.get_string("/api/read_all");
let response = serde_json::from_str::<ReadAllResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, -1);
assert_eq!(response.instances.len(), 0);
}
{
let body = server.get_string("/api/read/0");
let response = serde_json::from_str::<ReadResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, -1);
assert_eq!(response.instances.len(), 0);
}
}
#[test]
fn one_partition() {
let original_project_path = TEST_PROJECTS_ROOT.join("one-partition");
let project_tempdir = tempfile::tempdir().unwrap();
let project_path = project_tempdir.path();
copy_recursive(&original_project_path, &project_path).unwrap();
let project = Project::load(&project_path).unwrap();
let mut session = Session::new(project.clone());
session.start();
let web_config = WebConfig::from_session(0, project.serve_port, &session);
let server = Server::new(web_config);
{
let body = server.get_string("/api/rojo");
let response = serde_json::from_str::<ServerInfoResponse>(&body).unwrap();
let mut partitions = HashMap::new();
partitions.insert("lib".to_string(), vec!["ReplicatedStorage".to_string(), "OnePartition".to_string()]);
assert_eq!(response.server_id, "0");
assert_eq!(response.protocol_version, 2);
assert_eq!(response.partitions, partitions);
}
let initial_body = server.get_string("/api/read_all");
let initial_response = {
let response = serde_json::from_str::<ReadAllResponse>(&initial_body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, -1);
assert_eq!(response.instances.len(), 4);
let partition_id = *response.partition_instances.get("lib").unwrap();
let mut root_id = None;
let mut module_id = None;
let mut client_id = None;
let mut server_id = None;
for (id, instance) in response.instances.iter() {
match (instance.name.as_str(), instance.class_name.as_str()) {
// TOOD: Should partition roots (and other directories) be some
// magical object instead of Folder?
// TODO: Should this name actually equal the last part of the
// partition's target?
("OnePartition", "Folder") => {
assert!(root_id.is_none());
root_id = Some(*id);
assert_eq!(*id, partition_id);
assert_eq!(instance.properties.len(), 0);
assert_eq!(instance.parent, None);
assert_eq!(instance.children.len(), 3);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
("a", "ModuleScript") => {
assert!(module_id.is_none());
module_id = Some(*id);
let mut properties = HashMap::new();
properties.insert("Source".to_string(), RbxValue::String { value: "-- a.lua".to_string() });
assert_eq!(instance.properties, properties);
assert_eq!(instance.parent, Some(partition_id));
assert_eq!(instance.children.len(), 0);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
("b", "LocalScript") => {
assert!(client_id.is_none());
client_id = Some(*id);
let mut properties = HashMap::new();
properties.insert("Source".to_string(), RbxValue::String { value: "-- b.client.lua".to_string() });
assert_eq!(instance.properties, properties);
assert_eq!(instance.parent, Some(partition_id));
assert_eq!(instance.children.len(), 0);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
("a", "Script") => {
assert!(server_id.is_none());
server_id = Some(*id);
let mut properties = HashMap::new();
properties.insert("Source".to_string(), RbxValue::String { value: "-- a.server.lua".to_string() });
assert_eq!(instance.properties, properties);
assert_eq!(instance.parent, Some(partition_id));
assert_eq!(instance.children.len(), 0);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
_ => {
panic!("Unexpected instance named {} of class {}", instance.name, instance.class_name);
},
}
}
let root_id = root_id.unwrap();
let module_id = module_id.unwrap();
let client_id = client_id.unwrap();
let server_id = server_id.unwrap();
{
let root_instance = response.instances.get(&root_id).unwrap();
assert!(root_instance.children.contains(&module_id));
assert!(root_instance.children.contains(&client_id));
assert!(root_instance.children.contains(&server_id));
}
response
};
{
let temp_name = project_path.join("lib/c.client.lua");
{
let mut file = File::create(&temp_name).unwrap();
file.write_all(b"-- c.client.lua").unwrap();
}
{
// Block until Rojo detects the addition of our temp file
let body = server.get_string("/api/subscribe/-1");
let response = serde_json::from_str::<SubscribeResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, 0);
assert_eq!(response.messages.len(), 1);
// TODO: Read which instance was changed and try to access it with
// /read
}
let body = server.get_string("/api/read_all");
let response = serde_json::from_str::<ReadAllResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, 0);
assert_eq!(response.instances.len(), 5);
let partition_id = *response.partition_instances.get("lib").unwrap();
let mut root_id = None;
let mut module_id = None;
let mut client_id = None;
let mut server_id = None;
let mut new_id = None;
for (id, instance) in response.instances.iter() {
match (instance.name.as_str(), instance.class_name.as_str()) {
// TOOD: Should partition roots (and other directories) be some
// magical object instead of Folder?
// TODO: Should this name actually equal the last part of the
// partition's target?
("OnePartition", "Folder") => {
assert!(root_id.is_none());
root_id = Some(*id);
assert_eq!(*id, partition_id);
assert_eq!(instance.properties.len(), 0);
assert_eq!(instance.parent, None);
assert_eq!(instance.children.len(), 4);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
("a", "ModuleScript") => {
assert!(module_id.is_none());
module_id = Some(*id);
let mut properties = HashMap::new();
properties.insert("Source".to_string(), RbxValue::String { value: "-- a.lua".to_string() });
assert_eq!(instance.properties, properties);
assert_eq!(instance.parent, Some(partition_id));
assert_eq!(instance.children.len(), 0);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
("b", "LocalScript") => {
assert!(client_id.is_none());
client_id = Some(*id);
let mut properties = HashMap::new();
properties.insert("Source".to_string(), RbxValue::String { value: "-- b.client.lua".to_string() });
assert_eq!(instance.properties, properties);
assert_eq!(instance.parent, Some(partition_id));
assert_eq!(instance.children.len(), 0);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
("a", "Script") => {
assert!(server_id.is_none());
server_id = Some(*id);
let mut properties = HashMap::new();
properties.insert("Source".to_string(), RbxValue::String { value: "-- a.server.lua".to_string() });
assert_eq!(instance.properties, properties);
assert_eq!(instance.parent, Some(partition_id));
assert_eq!(instance.children.len(), 0);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
("c", "LocalScript") => {
assert!(new_id.is_none());
new_id = Some(*id);
let mut properties = HashMap::new();
properties.insert("Source".to_string(), RbxValue::String { value: "-- c.client.lua".to_string() });
assert_eq!(instance.properties, properties);
assert_eq!(instance.parent, Some(partition_id));
assert_eq!(instance.children.len(), 0);
let single_body = server.get_string(&format!("/api/read/{}", id));
let single_response = serde_json::from_str::<ReadResponse>(&single_body).unwrap();
let single_instance = single_response.instances.get(id).unwrap();
assert_eq!(single_instance, &Cow::Borrowed(instance));
},
_ => {},
}
}
let root_id = root_id.unwrap();
let module_id = module_id.unwrap();
let client_id = client_id.unwrap();
let server_id = server_id.unwrap();
let new_id = new_id.unwrap();
let root_instance = response.instances.get(&root_id).unwrap();
assert!(root_instance.children.contains(&module_id));
assert!(root_instance.children.contains(&client_id));
assert!(root_instance.children.contains(&server_id));
assert!(root_instance.children.contains(&new_id));
remove_file(&temp_name).unwrap();
}
{
// Block until Rojo detects the removal of our temp file
let body = server.get_string("/api/subscribe/0");
let response = serde_json::from_str::<SubscribeResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, 1);
assert_eq!(response.messages.len(), 1);
}
{
// Everything should be back to the initial state!
let body = server.get_string("/api/read_all");
let response = serde_json::from_str::<ReadAllResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, 1);
assert_eq!(response.instances.len(), 4);
assert_eq!(response.instances, initial_response.instances);
}
// TODO: Test to change existing instance
}
#[test]
fn partition_to_file() {
let original_project_path = TEST_PROJECTS_ROOT.join("partition-to-file");
let project_tempdir = tempfile::tempdir().unwrap();
let project_path = project_tempdir.path();
copy_recursive(&original_project_path, &project_path).unwrap();
let project = Project::load(&project_path).unwrap();
let mut session = Session::new(project.clone());
session.start();
let web_config = WebConfig::from_session(0, project.serve_port, &session);
let server = Server::new(web_config);
{
let body = server.get_string("/api/rojo");
let response = serde_json::from_str::<ServerInfoResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.protocol_version, 2);
assert_eq!(response.partitions.len(), 1);
}
{
let body = server.get_string("/api/read_all");
let response = serde_json::from_str::<ReadAllResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, -1);
assert_eq!(response.instances.len(), 1);
let (id, instance) = response.instances.iter().next().unwrap();
assert_eq!(instance.name, "bar");
assert_eq!(instance.class_name, "ModuleScript");
assert_eq!(instance.properties.get("Source"), Some(&RbxValue::String { value: "-- foo.lua".to_string() }));
assert_eq!(instance.children.len(), 0);
assert_eq!(instance.parent, None);
let body = server.get_string(&format!("/api/read/{}", id));
let response = serde_json::from_str::<ReadResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, -1);
assert_eq!(response.instances.len(), 1);
let single_instance = response.instances.values().next().unwrap();
assert_eq!(&Cow::Borrowed(instance), single_instance);
}
let file_path = project_path.join("foo.lua");
{
let mut file = File::create(file_path).unwrap();
file.write_all(b"-- modified").unwrap();
}
{
// Block until Rojo detects our file being modified
let body = server.get_string("/api/subscribe/-1");
let response = serde_json::from_str::<SubscribeResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, 0);
assert_eq!(response.messages.len(), 1);
}
{
let body = server.get_string("/api/read/0");
let response = serde_json::from_str::<ReadResponse>(&body).unwrap();
assert_eq!(response.server_id, "0");
assert_eq!(response.message_cursor, 0);
assert_eq!(response.instances.len(), 1);
let instance = response.instances.values().next().unwrap();
assert_eq!(instance.name, "bar");
assert_eq!(instance.class_name, "ModuleScript");
assert_eq!(instance.properties.get("Source"), Some(&RbxValue::String { value: "-- modified".to_string() }));
assert_eq!(instance.children.len(), 0);
assert_eq!(instance.parent, None);
}
}

View file

@ -0,0 +1,4 @@
{
"name": "empty",
"tree": {}
}

View file

@ -1,5 +0,0 @@
{
"name": "empty",
"servePort": 23456,
"partitions": {}
}

26
test-projects/foo.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "foo",
"tree": {
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Rojo": {
"$className": "Folder",
"Plugin": {
"$path": "lib"
},
"Roact": {
"$path": "modules/roact/lib"
},
"Rodux": {
"$path": "modules/rodux/lib"
},
"RoactRodux": {
"$path": "modules/roact-rodux/lib"
},
"Promise": {
"$path": "modules/promise/lib"
}
}
}
}
}

View file

@ -1 +0,0 @@
-- a.lua

View file

@ -1 +0,0 @@
-- a.server.lua

View file

@ -1 +0,0 @@
-- b.client.lua

View file

@ -1,10 +0,0 @@
{
"name": "one-partition",
"servePort": 23456,
"partitions": {
"lib": {
"path": "lib",
"target": "ReplicatedStorage.OnePartition"
}
}
}

View file

@ -1 +0,0 @@
-- foo.lua

View file

@ -1,10 +0,0 @@
{
"name": "partition-to-file",
"servePort": 23456,
"partitions": {
"lib": {
"path": "foo.lua",
"target": "ReplicatedStorage.bar"
}
}
}