mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 14:15:24 +00:00
Reorganize some of the unwieldly parts of the codebase
* Moved commands into their own folder to reduce `bin.rs`'s size * Moved all of the VFS-related functionality into its own folder * Documented a lot of functions, including a few very obscure ones
This commit is contained in:
@@ -22,7 +22,7 @@ The `rojo serve` command uses three major components:
|
|||||||
### Transform Plugins
|
### Transform Plugins
|
||||||
Transform plugins (or filter plugins?) can interject in three places:
|
Transform plugins (or filter plugins?) can interject in three places:
|
||||||
* Transform a `VfsItem` that's being read into an `RbxInstance` in the VFS
|
* Transform a `VfsItem` that's being read into an `RbxInstance` in the VFS
|
||||||
* Transform an `Rbxitem` that's being written into a `VfsItem` in the VFS
|
* Transform an `RbxInstance` that's being written into a `VfsItem` in the VFS
|
||||||
* Transform a file change into paths that need to be updated in the VFS watcher
|
* Transform a file change into paths that need to be updated in the VFS watcher
|
||||||
|
|
||||||
The plan is to have several built-in plugins that can be rearranged/configured in project settings:
|
The plan is to have several built-in plugins that can be rearranged/configured in project settings:
|
||||||
|
|||||||
152
src/bin.rs
152
src/bin.rs
@@ -1,15 +1,7 @@
|
|||||||
#[macro_use]
|
#[macro_use] extern crate serde_derive;
|
||||||
extern crate serde_derive;
|
#[macro_use] extern crate rouille;
|
||||||
|
#[macro_use] extern crate clap;
|
||||||
#[macro_use]
|
#[macro_use] extern crate lazy_static;
|
||||||
extern crate rouille;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate clap;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
|
|
||||||
extern crate notify;
|
extern crate notify;
|
||||||
extern crate rand;
|
extern crate rand;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
@@ -21,22 +13,15 @@ pub mod core;
|
|||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod pathext;
|
pub mod pathext;
|
||||||
pub mod vfs;
|
pub mod vfs;
|
||||||
pub mod vfs_watch;
|
|
||||||
pub mod rbx;
|
pub mod rbx;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod plugins;
|
pub mod plugins;
|
||||||
|
pub mod commands;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::process;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use core::Config;
|
|
||||||
use pathext::canonicalish;
|
use pathext::canonicalish;
|
||||||
use project::{Project, ProjectLoadError};
|
|
||||||
use plugin::{PluginChain};
|
|
||||||
use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin};
|
|
||||||
use vfs::Vfs;
|
|
||||||
use vfs_watch::VfsWatcher;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = clap_app!(rojo =>
|
let matches = clap_app!(rojo =>
|
||||||
@@ -68,27 +53,13 @@ fn main() {
|
|||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let server_id = rand::random::<u64>();
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
println!("Server ID: {}", server_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
("init", sub_matches) => {
|
("init", sub_matches) => {
|
||||||
let sub_matches = sub_matches.unwrap();
|
let sub_matches = sub_matches.unwrap();
|
||||||
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
||||||
let full_path = canonicalish(project_path);
|
let full_path = canonicalish(project_path);
|
||||||
|
|
||||||
match Project::init(&full_path) {
|
commands::init(&full_path);
|
||||||
Ok(_) => {
|
|
||||||
println!("Created new empty project at {}", full_path.display());
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to create new project.\n{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
("serve", sub_matches) => {
|
("serve", sub_matches) => {
|
||||||
let sub_matches = sub_matches.unwrap();
|
let sub_matches = sub_matches.unwrap();
|
||||||
@@ -98,126 +69,29 @@ fn main() {
|
|||||||
None => std::env::current_dir().unwrap(),
|
None => std::env::current_dir().unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if verbose {
|
|
||||||
println!("Attempting to locate project at {}", project_path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
let project = match Project::load(&project_path) {
|
|
||||||
Ok(v) => {
|
|
||||||
println!("Using project from {}", project_path.display());
|
|
||||||
v
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
match err {
|
|
||||||
ProjectLoadError::InvalidJson(serde_err) => {
|
|
||||||
eprintln!(
|
|
||||||
"Found invalid JSON!\nProject in: {}\nError: {}",
|
|
||||||
project_path.display(),
|
|
||||||
serde_err,
|
|
||||||
);
|
|
||||||
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => {
|
|
||||||
eprintln!("Found project file, but failed to read it!");
|
|
||||||
eprintln!(
|
|
||||||
"Check the permissions of the project file at\n{}",
|
|
||||||
project_path.display(),
|
|
||||||
);
|
|
||||||
|
|
||||||
std::process::exit(1);
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
// Any other error is fine; use the default project.
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Found no project file, using default project...");
|
|
||||||
Project::default()
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let port = {
|
let port = {
|
||||||
match sub_matches.value_of("port") {
|
match sub_matches.value_of("port") {
|
||||||
Some(source) => match source.parse::<u64>() {
|
Some(source) => match source.parse::<u64>() {
|
||||||
Ok(value) => value,
|
Ok(value) => Some(value),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("Invalid port '{}'", source);
|
eprintln!("Invalid port '{}'", source);
|
||||||
std::process::exit(1);
|
process::exit(1);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
None => project.serve_port,
|
None => None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_static! {
|
commands::serve(&project_path, verbose, port);
|
||||||
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
|
|
||||||
Box::new(ScriptPlugin::new()),
|
|
||||||
Box::new(JsonModelPlugin::new()),
|
|
||||||
Box::new(DefaultPlugin::new()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
port,
|
|
||||||
verbose,
|
|
||||||
server_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
println!("Loading VFS...");
|
|
||||||
}
|
|
||||||
|
|
||||||
let vfs = {
|
|
||||||
let mut vfs = Vfs::new(config.clone(), &PLUGIN_CHAIN);
|
|
||||||
|
|
||||||
for (name, project_partition) in &project.partitions {
|
|
||||||
let path = {
|
|
||||||
let given_path = Path::new(&project_partition.path);
|
|
||||||
|
|
||||||
if given_path.is_absolute() {
|
|
||||||
given_path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
project_path.join(given_path)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
println!(
|
|
||||||
"Partition '{}': {} @ {}",
|
|
||||||
name,
|
|
||||||
project_partition.target,
|
|
||||||
project_partition.path
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
vfs.partitions.insert(name.clone(), path);
|
|
||||||
}
|
|
||||||
|
|
||||||
Arc::new(Mutex::new(vfs))
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let vfs = vfs.clone();
|
|
||||||
let config = config.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
VfsWatcher::new(config, vfs).start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Server listening on port {}", port);
|
|
||||||
|
|
||||||
web::start(config.clone(), project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
|
||||||
},
|
},
|
||||||
("pack", _) => {
|
("pack", _) => {
|
||||||
eprintln!("'rojo pack' is not yet implemented!");
|
eprintln!("'rojo pack' is not yet implemented!");
|
||||||
std::process::exit(1);
|
process::exit(1);
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Please specify a subcommand!");
|
eprintln!("Please specify a subcommand!");
|
||||||
eprintln!("Try 'rojo help' for information.");
|
eprintln!("Try 'rojo help' for information.");
|
||||||
std::process::exit(1);
|
process::exit(1);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/commands/init.rs
Normal file
16
src/commands/init.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use project::Project;
|
||||||
|
|
||||||
|
pub fn init(project_path: &PathBuf) {
|
||||||
|
match Project::init(project_path) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Created new empty project at {}", project_path.display());
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create new project.\n{}", e);
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/commands/mod.rs
Normal file
5
src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod serve;
|
||||||
|
mod init;
|
||||||
|
|
||||||
|
pub use self::serve::*;
|
||||||
|
pub use self::init::*;
|
||||||
114
src/commands/serve.rs
Normal file
114
src/commands/serve.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use rand;
|
||||||
|
|
||||||
|
use project::{Project, ProjectLoadError};
|
||||||
|
use plugin::{PluginChain};
|
||||||
|
use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin};
|
||||||
|
use vfs::{VfsSession, VfsWatcher};
|
||||||
|
use web;
|
||||||
|
|
||||||
|
pub fn serve(project_path: &PathBuf, verbose: bool, port: Option<u64>) {
|
||||||
|
let server_id = rand::random::<u64>();
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!("Attempting to locate project at {}", project_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = match Project::load(project_path) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Using project from {}", project_path.display());
|
||||||
|
v
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
match err {
|
||||||
|
ProjectLoadError::InvalidJson(serde_err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Found invalid JSON!\nProject in: {}\nError: {}",
|
||||||
|
project_path.display(),
|
||||||
|
serde_err,
|
||||||
|
);
|
||||||
|
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => {
|
||||||
|
eprintln!("Found project file, but failed to read it!");
|
||||||
|
eprintln!(
|
||||||
|
"Check the permissions of the project file at\n{}",
|
||||||
|
project_path.display(),
|
||||||
|
);
|
||||||
|
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// Any other error is fine; use the default project.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Found no project file, using default project...");
|
||||||
|
Project::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let web_config = web::WebConfig {
|
||||||
|
verbose,
|
||||||
|
port: port.unwrap_or(project.serve_port),
|
||||||
|
server_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
|
||||||
|
Box::new(ScriptPlugin::new()),
|
||||||
|
Box::new(JsonModelPlugin::new()),
|
||||||
|
Box::new(DefaultPlugin::new()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!("Loading VFS...");
|
||||||
|
}
|
||||||
|
|
||||||
|
let vfs = {
|
||||||
|
let mut vfs = VfsSession::new(&PLUGIN_CHAIN);
|
||||||
|
|
||||||
|
for (name, project_partition) in &project.partitions {
|
||||||
|
let path = {
|
||||||
|
let given_path = Path::new(&project_partition.path);
|
||||||
|
|
||||||
|
if given_path.is_absolute() {
|
||||||
|
given_path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
project_path.join(given_path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!(
|
||||||
|
"Partition '{}': {} @ {}",
|
||||||
|
name,
|
||||||
|
project_partition.target,
|
||||||
|
project_partition.path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
vfs.partitions.insert(name.clone(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arc::new(Mutex::new(vfs))
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let vfs = vfs.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
VfsWatcher::new(vfs).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Server listening on port {}", web_config.port);
|
||||||
|
|
||||||
|
web::start(web_config, project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
||||||
|
}
|
||||||
@@ -1,8 +1 @@
|
|||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Config {
|
|
||||||
pub port: u64,
|
|
||||||
pub verbose: bool,
|
|
||||||
pub server_id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Route = Vec<String>;
|
pub type Route = Vec<String>;
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ use rbx::RbxInstance;
|
|||||||
use vfs::VfsItem;
|
use vfs::VfsItem;
|
||||||
use core::Route;
|
use core::Route;
|
||||||
|
|
||||||
// TODO: Add error case?
|
|
||||||
pub enum TransformFileResult {
|
pub enum TransformFileResult {
|
||||||
Value(Option<RbxInstance>),
|
Value(Option<RbxInstance>),
|
||||||
Pass,
|
Pass,
|
||||||
|
|
||||||
|
// TODO: Error case
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum RbxChangeResult {
|
pub enum RbxChangeResult {
|
||||||
Write(Option<VfsItem>),
|
Write(Option<VfsItem>),
|
||||||
Pass,
|
Pass,
|
||||||
|
|
||||||
|
// TODO: Error case
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum FileChangeResult {
|
pub enum FileChangeResult {
|
||||||
@@ -19,11 +22,20 @@ pub enum FileChangeResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait Plugin {
|
pub trait Plugin {
|
||||||
|
/// Invoked when a file is read from the filesystem and needs to be turned
|
||||||
|
/// into a Roblox instance.
|
||||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult;
|
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult;
|
||||||
|
|
||||||
|
/// Invoked when a Roblox Instance change is reported by the Roblox Studio
|
||||||
|
/// plugin and needs to be turned into a file to save.
|
||||||
fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> RbxChangeResult;
|
fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> RbxChangeResult;
|
||||||
|
|
||||||
|
/// Invoked when a file changes on the filesystem. The result defines what
|
||||||
|
/// routes are marked as needing to be refreshed.
|
||||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult;
|
fn handle_file_change(&self, route: &Route) -> FileChangeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A set of plugins that are composed in order.
|
||||||
pub struct PluginChain {
|
pub struct PluginChain {
|
||||||
plugins: Vec<Box<Plugin + Send + Sync>>,
|
plugins: Vec<Box<Plugin + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,19 @@ impl fmt::Display for ProjectInitError {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ProjectPartition {
|
pub struct ProjectPartition {
|
||||||
|
/// A slash-separated path to a file or folder, relative to the project's
|
||||||
|
/// directory.
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
|
||||||
|
/// A dot-separated route to a Roblox instance, relative to game.
|
||||||
pub target: String,
|
pub target: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a project configured by a user for use with Rojo. Holds anything
|
||||||
|
/// that can be configured with `rojo.json`.
|
||||||
|
///
|
||||||
|
/// In the future, this object will hold dependency information and other handy
|
||||||
|
/// configurables
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
@@ -57,6 +66,7 @@ pub struct Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
|
/// Creates a new empty Project object with the given name.
|
||||||
pub fn new<T: Into<String>>(name: T) -> Project {
|
pub fn new<T: Into<String>>(name: T) -> Project {
|
||||||
Project {
|
Project {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
@@ -64,10 +74,12 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initializes a new project inside the given folder path.
|
||||||
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
||||||
let location = location.as_ref();
|
let location = location.as_ref();
|
||||||
let package_path = location.join(PROJECT_FILENAME);
|
let package_path = location.join(PROJECT_FILENAME);
|
||||||
|
|
||||||
|
// We abort if the project file already exists.
|
||||||
match fs::metadata(&package_path) {
|
match fs::metadata(&package_path) {
|
||||||
Ok(_) => return Err(ProjectInitError::AlreadyExists),
|
Ok(_) => return Err(ProjectInitError::AlreadyExists),
|
||||||
Err(_) => {},
|
Err(_) => {},
|
||||||
@@ -78,11 +90,14 @@ impl Project {
|
|||||||
Err(_) => return Err(ProjectInitError::FailedToCreate),
|
Err(_) => return Err(ProjectInitError::FailedToCreate),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Try to give the project a meaningful name.
|
||||||
|
// If we can't, we'll just fall back to a default.
|
||||||
let name = match location.file_name() {
|
let name = match location.file_name() {
|
||||||
Some(v) => v.to_string_lossy().into_owned(),
|
Some(v) => v.to_string_lossy().into_owned(),
|
||||||
None => "new-project".to_string(),
|
None => "new-project".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Configure the project with all of the values we know so far.
|
||||||
let project = Project::new(name);
|
let project = Project::new(name);
|
||||||
let serialized = serde_json::to_string_pretty(&project).unwrap();
|
let serialized = serde_json::to_string_pretty(&project).unwrap();
|
||||||
|
|
||||||
@@ -94,6 +109,8 @@ impl Project {
|
|||||||
Ok(project)
|
Ok(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> {
|
||||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||||
|
|
||||||
@@ -120,6 +137,7 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves the given project file to the given folder with the appropriate name.
|
||||||
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
|
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
|
||||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||||
|
|
||||||
@@ -139,7 +157,7 @@ impl Project {
|
|||||||
impl Default for Project {
|
impl Default for Project {
|
||||||
fn default() -> Project {
|
fn default() -> Project {
|
||||||
Project {
|
Project {
|
||||||
name: "some-project".to_string(),
|
name: "new-project".to_string(),
|
||||||
serve_port: 8000,
|
serve_port: 8000,
|
||||||
partitions: HashMap::new(),
|
partitions: HashMap::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Represents data about a Roblox instance
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RbxInstance {
|
pub struct RbxInstance {
|
||||||
@@ -9,10 +10,14 @@ pub struct RbxInstance {
|
|||||||
pub properties: HashMap<String, RbxValue>,
|
pub properties: HashMap<String, RbxValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Any kind value that can be used by Roblox
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase", tag = "type")]
|
#[serde(rename_all = "camelCase", tag = "type")]
|
||||||
pub enum RbxValue {
|
pub enum RbxValue {
|
||||||
String {
|
String {
|
||||||
value: String,
|
value: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: Other primitives
|
||||||
|
// TODO: Compound types like Vector3
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/vfs/mod.rs
Normal file
7
src/vfs/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
mod vfs_session;
|
||||||
|
mod vfs_item;
|
||||||
|
mod vfs_watcher;
|
||||||
|
|
||||||
|
pub use self::vfs_session::*;
|
||||||
|
pub use self::vfs_item::*;
|
||||||
|
pub use self::vfs_watcher::*;
|
||||||
31
src/vfs/vfs_item.rs
Normal file
31
src/vfs/vfs_item.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// A VfsItem represents either a file or directory as it came from the filesystem.
|
||||||
|
///
|
||||||
|
/// The interface here is intentionally simplified to make it easier to traverse
|
||||||
|
/// files that have been read into memory.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", tag = "type")]
|
||||||
|
pub enum VfsItem {
|
||||||
|
File {
|
||||||
|
route: Vec<String>,
|
||||||
|
contents: String,
|
||||||
|
},
|
||||||
|
Dir {
|
||||||
|
route: Vec<String>,
|
||||||
|
children: HashMap<String, VfsItem>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfsItem {
|
||||||
|
pub fn name(&self) -> &String {
|
||||||
|
self.route().last().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn route(&self) -> &[String] {
|
||||||
|
match self {
|
||||||
|
&VfsItem::File { ref route, .. } => route,
|
||||||
|
&VfsItem::Dir { ref route, .. } => route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ use std::io::Read;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use core::Config;
|
|
||||||
use plugin::PluginChain;
|
use plugin::PluginChain;
|
||||||
|
use vfs::VfsItem;
|
||||||
|
|
||||||
/// Represents a virtual layer over multiple parts of the filesystem.
|
/// Represents a virtual layer over multiple parts of the filesystem.
|
||||||
///
|
///
|
||||||
/// Paths in this system are represented as slices of strings, and are always
|
/// Paths in this system are represented as slices of strings, and are always
|
||||||
/// relative to a partition, which is an absolute path into the real filesystem.
|
/// relative to a partition, which is an absolute path into the real filesystem.
|
||||||
pub struct Vfs {
|
pub struct VfsSession {
|
||||||
/// Contains all of the partitions mounted by the Vfs.
|
/// Contains all of the partitions mounted by the Vfs.
|
||||||
///
|
///
|
||||||
/// These must be absolute paths!
|
/// These must be absolute paths!
|
||||||
@@ -25,8 +25,6 @@ pub struct Vfs {
|
|||||||
pub change_history: Vec<VfsChange>,
|
pub change_history: Vec<VfsChange>,
|
||||||
|
|
||||||
plugin_chain: &'static PluginChain,
|
plugin_chain: &'static PluginChain,
|
||||||
|
|
||||||
config: Config,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -36,44 +34,13 @@ pub struct VfsChange {
|
|||||||
route: Vec<String>,
|
route: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A VfsItem represents either a file or directory as it came from the filesystem.
|
impl VfsSession {
|
||||||
///
|
pub fn new(plugin_chain: &'static PluginChain) -> VfsSession {
|
||||||
/// The interface here is intentionally simplified to make it easier to traverse
|
VfsSession {
|
||||||
/// files that have been read into memory.
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase", tag = "type")]
|
|
||||||
pub enum VfsItem {
|
|
||||||
File {
|
|
||||||
route: Vec<String>,
|
|
||||||
contents: String,
|
|
||||||
},
|
|
||||||
Dir {
|
|
||||||
route: Vec<String>,
|
|
||||||
children: HashMap<String, VfsItem>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VfsItem {
|
|
||||||
pub fn name(&self) -> &String {
|
|
||||||
self.route().last().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn route(&self) -> &[String] {
|
|
||||||
match self {
|
|
||||||
&VfsItem::File { ref route, .. } => route,
|
|
||||||
&VfsItem::Dir { ref route, .. } => route,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vfs {
|
|
||||||
pub fn new(config: Config, plugin_chain: &'static PluginChain) -> Vfs {
|
|
||||||
Vfs {
|
|
||||||
partitions: HashMap::new(),
|
partitions: HashMap::new(),
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
change_history: Vec::new(),
|
change_history: Vec::new(),
|
||||||
plugin_chain,
|
plugin_chain,
|
||||||
config,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,16 +152,8 @@ impl Vfs {
|
|||||||
/// Register a new change to the filesystem at the given timestamp and VFS
|
/// Register a new change to the filesystem at the given timestamp and VFS
|
||||||
/// route.
|
/// route.
|
||||||
pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) {
|
pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) {
|
||||||
if self.config.verbose {
|
|
||||||
println!("Received change {:?}, running through plugins...", route);
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.plugin_chain.handle_file_change(&route) {
|
match self.plugin_chain.handle_file_change(&route) {
|
||||||
Some(routes) => {
|
Some(routes) => {
|
||||||
if self.config.verbose {
|
|
||||||
println!("Adding changes from plugin: {:?}", routes);
|
|
||||||
}
|
|
||||||
|
|
||||||
for route in routes {
|
for route in routes {
|
||||||
self.change_history.push(VfsChange {
|
self.change_history.push(VfsChange {
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -4,24 +4,21 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
|
|
||||||
use core::Config;
|
|
||||||
use pathext::path_to_route;
|
use pathext::path_to_route;
|
||||||
use vfs::Vfs;
|
use vfs::VfsSession;
|
||||||
|
|
||||||
/// An object that registers watchers on the real filesystem and relays those
|
/// An object that registers watchers on the real filesystem and relays those
|
||||||
/// changes to the virtual filesystem layer.
|
/// changes to the virtual filesystem layer.
|
||||||
pub struct VfsWatcher {
|
pub struct VfsWatcher {
|
||||||
vfs: Arc<Mutex<Vfs>>,
|
vfs: Arc<Mutex<VfsSession>>,
|
||||||
watchers: Vec<RecommendedWatcher>,
|
watchers: Vec<RecommendedWatcher>,
|
||||||
config: Config,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VfsWatcher {
|
impl VfsWatcher {
|
||||||
pub fn new(config: Config, vfs: Arc<Mutex<Vfs>>) -> VfsWatcher {
|
pub fn new(vfs: Arc<Mutex<VfsSession>>) -> VfsWatcher {
|
||||||
VfsWatcher {
|
VfsWatcher {
|
||||||
vfs,
|
vfs,
|
||||||
watchers: Vec::new(),
|
watchers: Vec::new(),
|
||||||
config,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +42,6 @@ impl VfsWatcher {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let vfs = self.vfs.clone();
|
let vfs = self.vfs.clone();
|
||||||
let config = self.config.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
@@ -53,10 +49,6 @@ impl VfsWatcher {
|
|||||||
let mut vfs = vfs.lock().unwrap();
|
let mut vfs = vfs.lock().unwrap();
|
||||||
let current_time = vfs.current_time();
|
let current_time = vfs.current_time();
|
||||||
|
|
||||||
if config.verbose {
|
|
||||||
println!("FS event {:?}", event);
|
|
||||||
}
|
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
DebouncedEvent::Write(ref change_path) |
|
DebouncedEvent::Write(ref change_path) |
|
||||||
DebouncedEvent::Create(ref change_path) |
|
DebouncedEvent::Create(ref change_path) |
|
||||||
41
src/web.rs
41
src/web.rs
@@ -5,14 +5,20 @@ use rouille;
|
|||||||
use serde;
|
use serde;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use core::Config;
|
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use vfs::{Vfs, VfsChange};
|
use vfs::{VfsSession, VfsChange};
|
||||||
use rbx::RbxInstance;
|
use rbx::RbxInstance;
|
||||||
use plugin::PluginChain;
|
use plugin::PluginChain;
|
||||||
|
|
||||||
static MAX_BODY_SIZE: usize = 25 * 1024 * 1024; // 25 MiB
|
static MAX_BODY_SIZE: usize = 25 * 1024 * 1024; // 25 MiB
|
||||||
|
|
||||||
|
/// The set of configuration the web server needs to start.
|
||||||
|
pub struct WebConfig {
|
||||||
|
pub port: u64,
|
||||||
|
pub verbose: bool,
|
||||||
|
pub server_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ServerInfo<'a> {
|
struct ServerInfo<'a> {
|
||||||
@@ -51,7 +57,12 @@ fn json<T: serde::Serialize>(value: T) -> rouille::Response {
|
|||||||
rouille::Response::from_data("application/json", data)
|
rouille::Response::from_data("application/json", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pulls text that may be JSON out of a Rouille Request object.
|
||||||
|
///
|
||||||
|
/// Doesn't do any actual parsing -- all this method does is verify the content
|
||||||
|
/// type of the request and read the request's body.
|
||||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||||
|
// Bail out if the request body isn't marked as JSON
|
||||||
match request.header("Content-Type") {
|
match request.header("Content-Type") {
|
||||||
Some(header) => if !header.starts_with("application/json") {
|
Some(header) => if !header.starts_with("application/json") {
|
||||||
return None;
|
return None;
|
||||||
@@ -64,14 +75,15 @@ fn read_json_text(request: &rouille::Request) -> Option<String> {
|
|||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
match body.take(MAX_BODY_SIZE.saturating_add(1) as u64)
|
match body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out) {
|
||||||
.read_to_end(&mut out)
|
|
||||||
{
|
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
Err(_) => return None,
|
Err(_) => return None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to
|
||||||
|
// process it.
|
||||||
if out.len() > MAX_BODY_SIZE {
|
if out.len() > MAX_BODY_SIZE {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -84,6 +96,7 @@ fn read_json_text(request: &rouille::Request) -> Option<String> {
|
|||||||
Some(parsed)
|
Some(parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
||||||
fn read_json<T>(request: &rouille::Request) -> Option<T>
|
fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
@@ -98,10 +111,13 @@ where
|
|||||||
Err(_) => return None,
|
Err(_) => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Change return type to some sort of Result
|
||||||
|
|
||||||
Some(parsed)
|
Some(parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChain, vfs: Arc<Mutex<Vfs>>) {
|
/// Start the Rojo web server and park our current thread.
|
||||||
|
pub fn start(config: WebConfig, project: Project, plugin_chain: &'static PluginChain, vfs: Arc<Mutex<VfsSession>>) {
|
||||||
let address = format!("localhost:{}", config.port);
|
let address = format!("localhost:{}", config.port);
|
||||||
|
|
||||||
let server_id = config.server_id.to_string();
|
let server_id = config.server_id.to_string();
|
||||||
@@ -109,6 +125,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
|||||||
rouille::start_server(address, move |request| {
|
rouille::start_server(address, move |request| {
|
||||||
router!(request,
|
router!(request,
|
||||||
(GET) (/) => {
|
(GET) (/) => {
|
||||||
|
// Get a summary of information about the server.
|
||||||
|
|
||||||
let current_time = {
|
let current_time = {
|
||||||
let vfs = vfs.lock().unwrap();
|
let vfs = vfs.lock().unwrap();
|
||||||
|
|
||||||
@@ -125,6 +143,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
|||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/changes/{ last_time: f64 }) => {
|
(GET) (/changes/{ last_time: f64 }) => {
|
||||||
|
// Get the list of changes since the given time.
|
||||||
|
|
||||||
let vfs = vfs.lock().unwrap();
|
let vfs = vfs.lock().unwrap();
|
||||||
let current_time = vfs.current_time();
|
let current_time = vfs.current_time();
|
||||||
let changes = vfs.changes_since(last_time);
|
let changes = vfs.changes_since(last_time);
|
||||||
@@ -137,11 +157,16 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
|||||||
},
|
},
|
||||||
|
|
||||||
(POST) (/read) => {
|
(POST) (/read) => {
|
||||||
|
// Read some instances from the server according to a JSON
|
||||||
|
// format body.
|
||||||
|
|
||||||
let read_request: Vec<Vec<String>> = match read_json(&request) {
|
let read_request: Vec<Vec<String>> = match read_json(&request) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return rouille::Response::empty_400(),
|
None => return rouille::Response::empty_400(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Read the files off of the filesystem that the client
|
||||||
|
// requested.
|
||||||
let (items, current_time) = {
|
let (items, current_time) = {
|
||||||
let vfs = vfs.lock().unwrap();
|
let vfs = vfs.lock().unwrap();
|
||||||
|
|
||||||
@@ -159,6 +184,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
|||||||
(items, current_time)
|
(items, current_time)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Transform all of our VfsItem objects into Roblox instances
|
||||||
|
// the client can use.
|
||||||
let rbx_items = items
|
let rbx_items = items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
@@ -182,6 +209,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
|||||||
},
|
},
|
||||||
|
|
||||||
(POST) (/write) => {
|
(POST) (/write) => {
|
||||||
|
// Not yet implemented.
|
||||||
|
|
||||||
let _write_request: Vec<WriteSpecifier> = match read_json(&request) {
|
let _write_request: Vec<WriteSpecifier> = match read_json(&request) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return rouille::Response::empty_400(),
|
None => return rouille::Response::empty_400(),
|
||||||
|
|||||||
Reference in New Issue
Block a user