WIP: Epiphany Refactor (#85)

This commit is contained in:
Lucien Greathouse
2018-08-26 01:03:53 -07:00
committed by GitHub
parent 80b9b7594b
commit 72bc77f1d5
52 changed files with 1145 additions and 2157 deletions

View File

@@ -8,7 +8,9 @@
<a href="https://travis-ci.org/LPGhatguy/rojo">
<img src="https://api.travis-ci.org/LPGhatguy/rojo.svg?branch=master" alt="Travis-CI Build Status" />
</a>
<img src="https://img.shields.io/badge/latest_version-0.4.12-brightgreen.svg" alt="Current server version" />
<a href="https://crates.io/crates/rojo">
<img src="https://img.shields.io/crates/v/rojo.svg?label=version" alt="Latest server version" />
</a>
<a href="https://lpghatguy.github.io/rojo">
<img src="https://img.shields.io/badge/documentation-website-brightgreen.svg" alt="Rojo Documentation" />
</a>
@@ -41,7 +43,7 @@ There are lots of other tools that sync scripts into Roblox or provide other too
Here are a few, if you're looking for alternatives or supplements to Rojo:
* [rbxmk by Anaminus](https://github.com/anaminus/rbxmk)
* [RbxRefresh by Osyris](https://github.com/osyrisrblx/RbxRefresh)
* [Rofresh](https://github.com/osyrisrblx/rofresh) and [RbxRefresh](https://github.com/osyrisrblx/RbxRefresh) by [Osyris](https://github.com/osyrisrblx)
* [Studio Bridge by Vocksel](https://github.com/vocksel/studio-bridge)
* [RbxSync by evaera](https://github.com/evaera/RbxSync)
* [CodeSync](https://github.com/MemoryPenguin/CodeSync) and [rbx-exteditor](https://github.com/MemoryPenguin/rbx-exteditor) by [MemoryPenguin](https://github.com/MemoryPenguin)

2
integration/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target/
**/*.rs.bk

4
integration/Cargo.lock generated Normal file
View File

@@ -0,0 +1,4 @@
[[package]]
name = "integration"
version = "0.1.0"

6
integration/Cargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[package]
name = "integration"
version = "0.1.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
[dependencies]

2
integration/README.md Normal file
View File

@@ -0,0 +1,2 @@
# Rojo Integration Runner
This is a WIP test runner designed for Rojo. It will eventually start up the Rojo server and plugin and test functionality end-to-end.

32
integration/src/main.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::{
path::Path,
process::Command,
thread,
time::Duration,
};
fn main() {
let plugin_path = Path::new("../plugin");
let server_path = Path::new("../server");
let tests_path = Path::new("../tests");
let server = Command::new("cargo")
.args(&["run", "--", "serve", "../test-projects/empty"])
.current_dir(server_path)
.spawn();
thread::sleep(Duration::from_millis(1000));
// TODO: Wait for server to start responding on the right port
let test_client = Command::new("lua")
.args(&["runTest.lua", "tests/empty.lua"])
.current_dir(plugin_path)
.spawn();
thread::sleep(Duration::from_millis(300));
// TODO: Collect output from the client for success/failure?
println!("Dying!");
}

5
plugin/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Rojo Plugin
This is the source to the Rojo Roblox Studio plugin.
Documentation is WIP.

View File

@@ -0,0 +1,37 @@
--[[
Loads the Rojo plugin and all of its dependencies.
]]
local function loadEnvironment()
-- If you add any dependencies, add them to this table so they'll be loaded!
local LOAD_MODULES = {
{"src", "Rojo"},
{"modules/promise/lib", "Promise"},
{"modules/testez/lib", "TestEZ"},
}
-- This makes sure we can load Lemur and other libraries that depend on init.lua
package.path = package.path .. ";?/init.lua"
-- If this fails, make sure you've run `lua bin/install-dependencies.lua` first!
local lemur = require("modules.lemur")
-- Create a virtual Roblox tree
local habitat = lemur.Habitat.new()
-- We'll put all of our library code and dependencies here
local modules = lemur.Instance.new("Folder")
modules.Name = "Modules"
modules.Parent = habitat.game:GetService("ReplicatedStorage")
-- Load all of the modules specified above
for _, module in ipairs(LOAD_MODULES) do
local container = habitat:loadFromFs(module[1])
container.Name = module[2]
container.Parent = modules
end
return habitat, modules
end
return loadEnvironment

View File

@@ -4,31 +4,31 @@
"partitions": {
"plugin": {
"path": "src",
"target": "ReplicatedStorage.Rojo.modules.Plugin"
"target": "ReplicatedStorage.Rojo.Modules.Plugin"
},
"modules/roact": {
"path": "modules/roact/lib",
"target": "ReplicatedStorage.Rojo.modules.Roact"
"target": "ReplicatedStorage.Rojo.Modules.Roact"
},
"modules/rodux": {
"path": "modules/rodux/lib",
"target": "ReplicatedStorage.Rojo.modules.Rodux"
"target": "ReplicatedStorage.Rojo.Modules.Rodux"
},
"modules/roact-rodux": {
"path": "modules/roact-rodux/lib",
"target": "ReplicatedStorage.Rojo.modules.RoactRodux"
"target": "ReplicatedStorage.Rojo.Modules.RoactRodux"
},
"modules/promise": {
"path": "modules/promise/lib",
"target": "ReplicatedStorage.Rojo.Modules.Promise"
},
"modules/testez": {
"path": "modules/testez/lib",
"target": "ReplicatedStorage.TestEZ"
},
"modules/promise": {
"path": "modules/promise/lib",
"target": "ReplicatedStorage.Rojo.modules.Promise"
},
"tests": {
"path": "tests",
"target": "TestService"
"path": "testBootstrap.server.lua",
"target": "TestService.testBootstrap"
}
}
}

15
plugin/runTest.lua Normal file
View File

@@ -0,0 +1,15 @@
local loadEnvironment = require("loadEnvironment")
local testPath = assert((...), "Please specify a path to a test file.")
local habitat = loadEnvironment()
local testModule = habitat:loadFromFs(testPath)
if testModule == nil then
error("Couldn't find test file at " .. testPath)
end
print("Starting test module.")
habitat:require(testModule)

View File

@@ -2,37 +2,14 @@
Loads our library and all of its dependencies, then runs tests using TestEZ.
]]
-- If you add any dependencies, add them to this table so they'll be loaded!
local LOAD_MODULES = {
{"src", "plugin"},
{"modules/promise/lib", "Promise"},
{"modules/testez/lib", "TestEZ"},
}
local loadEnvironment = require("loadEnvironment")
-- This makes sure we can load Lemur and other libraries that depend on init.lua
package.path = package.path .. ";?/init.lua"
-- If this fails, make sure you've run `lua bin/install-dependencies.lua` first!
local lemur = require("modules.lemur")
-- Create a virtual Roblox tree
local habitat = lemur.Habitat.new()
-- We'll put all of our library code and dependencies here
local Root = lemur.Instance.new("Folder")
Root.Name = "Root"
-- Load all of the modules specified above
for _, module in ipairs(LOAD_MODULES) do
local container = habitat:loadFromFs(module[1])
container.Name = module[2]
container.Parent = Root
end
local habitat, modules = loadEnvironment()
-- Load TestEZ and run our tests
local TestEZ = habitat:require(Root.TestEZ)
local TestEZ = habitat:require(modules.TestEZ)
local results = TestEZ.TestBootstrap:run(Root.plugin, TestEZ.Reporters.TextReporter)
local results = TestEZ.TestBootstrap:run(modules.Rojo, TestEZ.Reporters.TextReporter)
-- Did something go wrong?
if results.failureCount > 0 then

View File

@@ -19,14 +19,15 @@ setmetatable(ApiContext.Error, {
end
})
function ApiContext.new(url, onMessage)
assert(type(url) == "string")
function ApiContext.new(baseUrl, onMessage)
assert(type(baseUrl) == "string")
assert(type(onMessage) == "function")
local context = {
url = url,
baseUrl = baseUrl,
onMessage = onMessage,
serverId = nil,
rootInstanceId = nil,
connected = false,
messageCursor = -1,
partitionRoutes = nil,
@@ -38,7 +39,9 @@ function ApiContext.new(url, onMessage)
end
function ApiContext:connect()
return Http.get(self.url .. "/api/rojo")
local url = ("%s/api/rojo"):format(self.baseUrl)
return Http.get(url)
:andThen(function(response)
local body = response:json()
@@ -61,15 +64,18 @@ function ApiContext:connect()
self.serverId = body.serverId
self.connected = true
self.partitionRoutes = body.partitions
self.rootInstanceId = body.rootInstanceId
end)
end
function ApiContext:readAll()
function ApiContext:read(ids)
if not self.connected then
return Promise.reject()
end
return Http.get(self.url .. "/api/read_all")
local url = ("%s/api/read/%s"):format(self.baseUrl, table.concat(ids, ","))
return Http.get(url)
:andThen(function(response)
local body = response:json()
@@ -92,7 +98,9 @@ function ApiContext:retrieveMessages()
return Promise.reject()
end
return Http.get(self.url .. "/api/subscribe/" .. self.messageCursor)
local url = ("%s/api/subscribe/%s"):format(self.baseUrl, self.messageCursor)
return Http.get(url)
:andThen(function(response)
local body = response:json()

View File

@@ -1,6 +1,6 @@
local HttpService = game:GetService("HttpService")
local HTTP_DEBUG = false
local HTTP_DEBUG = true
local Promise = require(script.Parent.Parent.Promise)

View File

@@ -11,57 +11,7 @@ function Session.new()
setmetatable(self, Session)
local function createFoldersUntil(location, route)
for i = 1, #route - 1 do
local piece = route[i]
local child = location:FindFirstChild(piece)
if child == nil then
child = Instance.new("Folder")
child.Name = piece
child.Parent = location
end
location = child
end
return location
end
local function reify(instancesById, id)
local object = instancesById[tostring(id)]
local instance = Instance.new(object.className)
instance.Name = object.name
for key, property in pairs(object.properties) do
instance[key] = property.value
end
for _, childId in ipairs(object.children) do
reify(instancesById, childId).Parent = instance
end
return instance
end
local api
local function readAll()
print("Reading all...")
return api:readAll()
:andThen(function(response)
for partitionName, partitionRoute in pairs(api.partitionRoutes) do
local parent = createFoldersUntil(game, partitionRoute)
local rootInstanceId = response.partitionInstances[partitionName]
print("Root for", partitionName, "is", rootInstanceId)
reify(response.instances, rootInstanceId).Parent = parent
end
end)
end
api = ApiContext.new(REMOTE_URL, function(message)
if message.type == "InstanceChanged" then
@@ -73,7 +23,9 @@ function Session.new()
end)
api:connect()
:andThen(readAll)
:andThen(function()
return api:read({api.rootInstanceId})
end)
:andThen(function()
return api:retrieveMessages()
end)

View File

@@ -0,0 +1,2 @@
local TestEZ = require(game.ReplicatedStorage.TestEZ)
TestEZ.TestBootstrap:run(game.ReplicatedStorage.Rojo.plugin)

5
plugin/tests/empty.lua Normal file
View File

@@ -0,0 +1,5 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Session = require(ReplicatedStorage.Modules.Rojo.Session)
Session.new()

View File

@@ -1,2 +0,0 @@
local TestEZ = require(game.ReplicatedStorage.TestEZ)
TestEZ.TestBootstrap:run(game.ReplicatedStorage.Rojo.plugin)

473
server/Cargo.lock generated

File diff suppressed because it is too large Load Diff

4
server/README.md Normal file
View File

@@ -0,0 +1,4 @@
# Rojo Server
This is the source to the Rojo server.
Documentation is WIP.

View File

@@ -42,20 +42,7 @@ fn main() {
None => std::env::current_dir().unwrap(),
};
let port = {
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);
librojo::commands::serve(&project_path);
},
_ => {
eprintln!("Please specify a subcommand!");
@@ -63,4 +50,4 @@ fn main() {
process::exit(1);
},
}
}
}

View File

@@ -13,4 +13,4 @@ pub fn init(project_path: &PathBuf) {
process::exit(1);
},
}
}
}

View File

@@ -2,4 +2,4 @@ mod serve;
mod init;
pub use self::serve::*;
pub use self::init::*;
pub use self::init::*;

View File

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

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

@@ -18,4 +18,4 @@ fn it_gives_unique_numbers() {
let b = get_id();
assert!(a != b);
}
}

View File

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

View File

@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::sync::{mpsc, Arc, RwLock, Mutex};
use std::sync::{mpsc, RwLock, Mutex};
use id::{Id, get_id};
@@ -11,17 +11,16 @@ pub enum Message {
},
}
#[derive(Clone)]
pub struct MessageSession {
pub messages: Arc<RwLock<Vec<Message>>>,
pub message_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>,
pub struct MessageQueue {
messages: RwLock<Vec<Message>>,
message_listeners: Mutex<HashMap<Id, mpsc::Sender<()>>>,
}
impl MessageSession {
pub fn new() -> MessageSession {
MessageSession {
messages: Arc::new(RwLock::new(Vec::new())),
message_listeners: Arc::new(Mutex::new(HashMap::new())),
impl MessageQueue {
pub fn new() -> MessageQueue {
MessageQueue {
messages: RwLock::new(Vec::new()),
message_listeners: Mutex::new(HashMap::new()),
}
}
@@ -58,7 +57,20 @@ impl MessageSession {
}
}
pub fn get_message_cursor(&self) -> i32 {
self.messages.read().unwrap().len() as i32 - 1
pub fn get_message_cursor(&self) -> u32 {
self.messages.read().unwrap().len() as u32
}
}
pub fn get_messages_since(&self, cursor: u32) -> (u32, Vec<Message>) {
let messages = self.messages.read().unwrap();
let current_cursor = messages.len() as u32;
// Cursor is out of bounds or there are no new messages
if cursor >= current_cursor {
return (current_cursor, Vec::new());
}
(current_cursor, messages[(cursor as usize)..].to_vec())
}
}

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

@@ -24,4 +24,4 @@ where
} else {
root.join(value)
}
}
}

View File

@@ -1,246 +1,223 @@
use std::collections::HashMap;
use std::fmt;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use rand::{self, Rng};
use std::{
collections::HashMap,
fmt,
fs,
io,
path::{Path, PathBuf},
};
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)]
pub enum ProjectLoadError {
DidNotExist(PathBuf),
FailedToOpen(PathBuf),
FailedToRead(PathBuf),
InvalidJson(PathBuf, serde_json::Error),
// #[serde(rename = "$ignoreUnknown", default = "false")]
// ignore_unknown: bool,
#[serde(flatten)]
children: HashMap<String, SourceProjectNode>,
},
SyncPoint {
#[serde(rename = "$path")]
path: String,
}
}
impl fmt::Display for ProjectLoadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
impl SourceProjectNode {
pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode {
match self {
&ProjectLoadError::InvalidJson(ref project_path, ref serde_err) => {
write!(f, "Found invalid JSON reading project: {}\nError: {}", project_path.display(), serde_err)
SourceProjectNode::Regular { class_name, mut children } => {
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) |
&ProjectLoadError::FailedToRead(ref project_path) => {
write!(f, "Found project file, but failed to read it: {}", project_path.display())
},
&ProjectLoadError::DidNotExist(ref project_path) => {
write!(f, "Could not locate a project file at {}.\nUse 'rojo init' to create one.", project_path.display())
},
}
}
}
SourceProjectNode::SyncPoint { path: source_path } => {
let path = if Path::new(&source_path).is_absolute() {
PathBuf::from(source_path)
} else {
let project_folder_location = project_file_location.parent().unwrap();
project_folder_location.join(source_path)
};
#[derive(Debug)]
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 {
ProjectNode::SyncPoint {
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_path,
name: source_project.name,
serve_port: source_project.serve_port,
partitions,
name: self.name,
tree: tree,
file_location: PathBuf::from(project_file_location),
}
}
fn as_source_project(&self) -> SourceProject {
let mut partitions = HashMap::new();
for partition in self.partitions.values() {
let path = partition.path.strip_prefix(&self.project_path)
.unwrap_or_else(|_| &partition.path)
.to_str()
.unwrap()
.to_string();
let target = partition.target.join(".");
partitions.insert(partition.name.clone(), SourceProjectPartition {
path,
target,
});
}
SourceProject {
partitions,
name: self.name.clone(),
serve_port: self.serve_port,
}
}
/// Initializes a new project inside the given folder path.
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
let location = location.as_ref();
let project_path = location.join(PROJECT_FILENAME);
// We abort if the project file already exists.
fs::metadata(&project_path)
.map_err(|_| ProjectInitError::AlreadyExists)?;
let mut file = File::create(&project_path)
.map_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() {
Some(v) => v.to_string_lossy().into_owned(),
None => "new-project".to_string(),
};
// Generate a random port to run the server on.
let serve_port = rand::thread_rng().gen_range(2000, 49151);
// Configure the project with all of the values we know so far.
let source_project = SourceProject {
name,
serve_port,
partitions: HashMap::new(),
};
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
file.write(serialized.as_bytes())
.map_err(|_| ProjectInitError::FailedToWrite)?;
Ok(Project::from_source_project(source_project, project_path))
}
/// 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> {
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
fs::metadata(&project_path)
.map_err(|_| ProjectLoadError::DidNotExist(project_path.clone()))?;
let mut file = File::open(&project_path)
.map_err(|_| ProjectLoadError::FailedToOpen(project_path.clone()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|_| ProjectLoadError::FailedToRead(project_path.clone()))?;
let source_project = serde_json::from_str(&contents)
.map_err(|e| ProjectLoadError::InvalidJson(project_path.clone(), e))?;
Ok(Project::from_source_project(source_project, project_path))
}
/// Saves the given project file to the given folder with the appropriate name.
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
let mut file = File::create(&project_path)
.map_err(|_| ProjectSaveError::FailedToCreate)?;
let source_project = self.as_source_project();
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
file.write(serialized.as_bytes()).unwrap();
Ok(())
}
}
#[derive(Debug)]
pub enum ProjectLoadExactError {
IoError(io::Error),
JsonError(serde_json::Error),
}
impl fmt::Display for ProjectLoadExactError {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
match self {
ProjectLoadExactError::IoError(inner) => write!(output, "{}", inner),
ProjectLoadExactError::JsonError(inner) => write!(output, "{}", inner),
}
}
}
#[derive(Debug)]
pub enum ProjectInitError {}
impl fmt::Display for ProjectInitError {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
write!(output, "ProjectInitError")
}
}
#[derive(Debug)]
pub enum ProjectLoadFuzzyError {
NotFound,
IoError(io::Error),
JsonError(serde_json::Error),
}
impl From<ProjectLoadExactError> for ProjectLoadFuzzyError {
fn from(error: ProjectLoadExactError) -> ProjectLoadFuzzyError {
match error {
ProjectLoadExactError::IoError(inner) => ProjectLoadFuzzyError::IoError(inner),
ProjectLoadExactError::JsonError(inner) => ProjectLoadFuzzyError::JsonError(inner),
}
}
}
impl fmt::Display for ProjectLoadFuzzyError {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
match self {
ProjectLoadFuzzyError::NotFound => write!(output, "Project not found."),
ProjectLoadFuzzyError::IoError(inner) => write!(output, "{}", inner),
ProjectLoadFuzzyError::JsonError(inner) => write!(output, "{}", inner),
}
}
}
#[derive(Debug)]
pub enum ProjectSaveError {}
impl fmt::Display for ProjectSaveError {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
write!(output, "ProjectSaveError")
}
}
#[derive(Debug)]
pub enum ProjectNode {
Regular {
class_name: String,
children: HashMap<String, ProjectNode>,
// ignore_unknown: bool,
},
SyncPoint {
path: PathBuf,
},
}
#[derive(Debug)]
pub struct Project {
pub name: String,
pub tree: HashMap<String, ProjectNode>,
pub file_location: PathBuf,
}
impl Project {
pub fn init(_project_folder_location: &Path) -> Result<(), ProjectInitError> {
unimplemented!();
}
pub fn locate(start_location: &Path) -> Option<PathBuf> {
// TODO: Check for specific error kinds, convert 'not found' to Result.
let location_metadata = fs::metadata(start_location).ok()?;
// If this is a file, we should assume it's the config we want
if location_metadata.is_file() {
return Some(start_location.to_path_buf());
} else if location_metadata.is_dir() {
let with_file = start_location.join(PROJECT_FILENAME);
match fs::metadata(&with_file) {
Ok(with_file_metadata) => {
if with_file_metadata.is_file() {
return Some(with_file);
} else {
return None;
}
},
Err(_) => {},
}
}
match start_location.parent() {
Some(parent_location) => Self::locate(parent_location),
None => None,
}
}
pub fn load_fuzzy(fuzzy_project_location: &Path) -> Result<Project, ProjectLoadFuzzyError> {
let project_path = Self::locate(fuzzy_project_location)
.ok_or(ProjectLoadFuzzyError::NotFound)?;
Self::load_exact(&project_path).map_err(From::from)
}
pub fn load_exact(project_file_location: &Path) -> Result<Project, ProjectLoadExactError> {
let contents = fs::read_to_string(project_file_location)
.map_err(ProjectLoadExactError::IoError)?;
let parsed: SourceProject = serde_json::from_str(&contents)
.map_err(ProjectLoadExactError::JsonError)?;
Ok(parsed.into_project(project_file_location))
}
pub fn save(&self) -> Result<(), ProjectSaveError> {
let _source_project = self.to_source_project();
unimplemented!();
}
fn to_source_project(&self) -> SourceProject {
unimplemented!();
}
}

View File

@@ -1,7 +1,6 @@
use std::borrow::Cow;
use std::collections::HashMap;
use id::Id;
use id::{Id, get_id};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
@@ -23,46 +22,107 @@ pub struct RbxInstance {
/// Contains all other properties of an Instance.
pub properties: HashMap<String, RbxValue>,
/// The unique ID of the instance
id: Id,
/// All of the children of this instance. Order is relevant to preserve!
pub children: Vec<Id>,
children: Vec<Id>,
/// The parent of the instance, if there is one.
pub parent: Option<Id>,
parent: Option<Id>,
}
// This seems like a really bad idea?
// Why isn't there a blanket impl for this for all T?
impl<'a> From<&'a RbxInstance> for Cow<'a, RbxInstance> {
fn from(instance: &'a RbxInstance) -> Cow<'a, RbxInstance> {
Cow::Borrowed(instance)
impl RbxInstance {
pub fn get_id(&self) -> Id {
self.id
}
}
pub struct Descendants<'a> {
tree: &'a RbxTree,
ids_to_visit: Vec<Id>,
}
impl<'a> Iterator for Descendants<'a> {
type Item = &'a RbxInstance;
fn next(&mut self) -> Option<Self::Item> {
loop {
let id = match self.ids_to_visit.pop() {
Some(id) => id,
None => break,
};
match self.tree.get_instance(id) {
Some(instance) => {
for child_id in &instance.children {
self.ids_to_visit.push(*child_id);
}
return Some(instance);
},
None => continue,
}
}
None
}
}
pub struct RbxTree {
instances: HashMap<Id, RbxInstance>,
pub root_instance_id: Id,
}
impl RbxTree {
pub fn new() -> RbxTree {
let root_instance_id = get_id();
let root_instance = RbxInstance {
name: "game".to_string(),
class_name: "DataModel".to_string(),
properties: HashMap::new(),
id: root_instance_id,
children: Vec::new(),
parent: None,
};
let mut instances = HashMap::new();
instances.insert(root_instance_id, root_instance);
RbxTree {
instances: HashMap::new(),
instances,
root_instance_id,
}
}
pub fn get_instance(&self, id: Id) -> Option<&RbxInstance> {
self.instances.get(&id)
}
pub fn get_all_instances(&self) -> &HashMap<Id, RbxInstance> {
&self.instances
}
pub fn insert_instance(&mut self, id: Id, instance: RbxInstance) {
if let Some(parent_id) = instance.parent {
if let Some(mut parent) = self.instances.get_mut(&parent_id) {
if !parent.children.contains(&id) {
parent.children.push(id);
pub fn insert_instance(&mut self, mut instance: RbxInstance) {
match instance.parent {
Some(parent_id) => {
match self.instances.get_mut(&parent_id) {
Some(mut parent) => {
if !parent.children.contains(&instance.id) {
parent.children.push(instance.id);
}
},
None => {
panic!("Tree consistency error, parent {} was not present in tree.", parent_id);
}
}
}
},
None => {
instance.parent = Some(self.root_instance_id);
},
}
self.instances.insert(id, instance);
self.instances.insert(instance.id, instance);
}
pub fn delete_instance(&mut self, id: Id) -> Vec<Id> {
@@ -101,27 +161,20 @@ impl RbxTree {
ids_deleted
}
pub fn get_instance_and_descendants<'a, 'b, T>(&'a self, id: Id, output: &'b mut HashMap<Id, T>)
where T: From<&'a RbxInstance>
{
let mut ids_to_visit = vec![id];
loop {
let id = match ids_to_visit.pop() {
Some(id) => id,
None => break,
};
match self.instances.get(&id) {
Some(instance) => {
output.insert(id, instance.into());
for child_id in &instance.children {
ids_to_visit.push(*child_id);
}
},
None => continue,
}
pub fn iter_descendants<'a>(&'a self, id: Id) -> Descendants<'a> {
match self.get_instance(id) {
Some(instance) => {
Descendants {
tree: self,
ids_to_visit: instance.children.clone(),
}
},
None => {
Descendants {
tree: self,
ids_to_visit: vec![],
}
},
}
}
}
}

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 +1,121 @@
use std::sync::{mpsc, Arc, RwLock};
use std::thread;
use std::{
sync::{Arc, RwLock, Mutex, mpsc},
thread,
io,
time::Duration,
};
use message_session::MessageSession;
use partition_watcher::PartitionWatcher;
use project::Project;
use rbx_session::RbxSession;
use vfs_session::VfsSession;
use rand;
/// Stub trait for middleware
trait Middleware {
}
use notify::{
self,
DebouncedEvent,
RecommendedWatcher,
RecursiveMode,
Watcher,
};
use ::{
message_queue::MessageQueue,
rbx::RbxTree,
project::{Project, ProjectNode},
vfs::Vfs,
};
const WATCH_TIMEOUT_MS: u64 = 100;
pub struct Session {
pub project: Project,
vfs_session: Arc<RwLock<VfsSession>>,
rbx_session: Arc<RwLock<RbxSession>>,
message_session: MessageSession,
watchers: Vec<PartitionWatcher>,
project: Project,
pub session_id: String,
pub message_queue: Arc<MessageQueue>,
pub tree: Arc<RwLock<RbxTree>>,
vfs: Arc<Mutex<Vfs>>,
watchers: Vec<RecommendedWatcher>,
}
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())));
let session_id = rand::random::<u64>().to_string();
Session {
vfs_session,
rbx_session,
watchers: Vec::new(),
message_session,
session_id,
project,
message_queue: Arc::new(MessageQueue::new()),
tree: Arc::new(RwLock::new(RbxTree::new())),
vfs: Arc::new(Mutex::new(Vfs::new())),
watchers: Vec::new(),
}
}
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 start(&mut self) -> io::Result<()> {
fn add_sync_points(vfs: &mut Vfs, project_node: &ProjectNode) -> io::Result<()> {
match project_node {
ProjectNode::Regular { children, .. } => {
for child in children.values() {
add_sync_points(vfs, child)?;
}
}
});
},
ProjectNode::SyncPoint { path } => {
vfs.add_root(path)?;
},
}
Ok(())
}
{
let mut vfs = self.vfs.lock().unwrap();
for child in self.project.tree.values() {
add_sync_points(&mut vfs, child)?;
}
for root in vfs.get_roots() {
println!("Watching {}", root.display());
let (watch_tx, watch_rx) = mpsc::channel();
let mut watcher = notify::watcher(watch_tx, Duration::from_millis(WATCH_TIMEOUT_MS)).unwrap();
watcher.watch(root, RecursiveMode::Recursive).unwrap();
self.watchers.push(watcher);
let vfs = Arc::clone(&self.vfs);
thread::spawn(move || {
println!("Thread started");
loop {
match watch_rx.recv() {
Ok(event) => {
match event {
DebouncedEvent::Create(path) | DebouncedEvent::Write(path) => {
let mut vfs = vfs.lock().unwrap();
vfs.add_or_update(&path).unwrap();
},
DebouncedEvent::Remove(path) => {
let mut vfs = vfs.lock().unwrap();
vfs.remove(&path);
},
DebouncedEvent::Rename(from_path, to_path) => {
let mut vfs = vfs.lock().unwrap();
vfs.remove(&from_path);
vfs.add_or_update(&to_path).unwrap();
},
_ => continue,
};
},
Err(_) => break,
};
}
println!("Thread stopped");
});
}
}
Ok(())
}
pub fn stop(self) {
pub fn get_project(&self) -> &Project {
&self.project
}
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()
}
}
}

139
server/src/vfs.rs Normal file
View File

@@ -0,0 +1,139 @@
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
fs,
io,
};
#[derive(Debug)]
pub struct Vfs {
contents: HashMap<PathBuf, Vec<u8>>,
items: HashMap<PathBuf, VfsItem>,
roots: HashSet<PathBuf>,
}
impl Vfs {
pub fn new() -> Vfs {
Vfs {
contents: HashMap::new(),
items: HashMap::new(),
roots: HashSet::new(),
}
}
pub fn add_root<'a, 'b>(&'a mut self, root_path: &'b Path) -> io::Result<&'a VfsItem> {
debug_assert!(root_path.is_absolute());
self.roots.insert(root_path.to_path_buf());
VfsItem::get(self, root_path)
}
pub fn get_roots(&self) -> &HashSet<PathBuf> {
&self.roots
}
pub fn get(&mut self, path: &Path) -> Option<&VfsItem> {
debug_assert!(path.is_absolute());
debug_assert!(self.is_valid_path(path));
self.items.get(path)
}
pub fn remove(&mut self, path: &Path) {
debug_assert!(path.is_absolute());
debug_assert!(self.is_valid_path(path));
match self.items.remove(path) {
Some(item) => match item {
VfsItem::File(_) => {
self.contents.remove(path);
},
VfsItem::Directory(VfsDirectory { children, .. }) => {
for child_path in &children {
self.remove(child_path);
}
},
},
None => {},
}
}
pub fn add_or_update<'a, 'b>(&'a mut self, path: &'b Path) -> io::Result<&'a VfsItem> {
debug_assert!(path.is_absolute());
debug_assert!(self.is_valid_path(path));
VfsItem::get(self, path)
}
fn is_valid_path(&self, path: &Path) -> bool {
let mut is_valid_path = false;
for root_path in &self.roots {
if path.starts_with(root_path) {
is_valid_path = true;
break;
}
}
is_valid_path
}
}
#[derive(Debug)]
pub struct VfsFile {
path: PathBuf,
}
#[derive(Debug)]
pub struct VfsDirectory {
path: PathBuf,
children: HashSet<PathBuf>,
}
#[derive(Debug)]
pub enum VfsItem {
File(VfsFile),
Directory(VfsDirectory),
}
impl VfsItem {
fn get<'a, 'b>(vfs: &'a mut Vfs, root_path: &'b Path) -> io::Result<&'a VfsItem> {
let metadata = fs::metadata(root_path)?;
if metadata.is_file() {
let item = VfsItem::File(VfsFile {
path: root_path.to_path_buf(),
});
vfs.items.insert(root_path.to_path_buf(), item);
let contents = fs::read(root_path)?;
vfs.contents.insert(root_path.to_path_buf(), contents);
Ok(vfs.items.get(root_path).unwrap())
} else if metadata.is_dir() {
let mut children = HashSet::new();
for entry in fs::read_dir(root_path)? {
let entry = entry?;
let path = entry.path();
VfsItem::get(vfs, &path)?;
children.insert(path);
}
let item = VfsItem::Directory(VfsDirectory {
path: root_path.to_path_buf(),
children,
});
vfs.items.insert(root_path.to_path_buf(), item);
Ok(vfs.items.get(root_path).unwrap())
} else {
unimplemented!();
}
}
}

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

@@ -1,197 +1,152 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{mpsc, RwLock, Arc};
use std::{
borrow::Cow,
collections::HashMap,
sync::{mpsc, Arc},
};
use rouille::{self, Request, Response};
use id::Id;
use message_session::{MessageSession, Message};
use project::Project;
use rbx::RbxInstance;
use rbx_session::RbxSession;
use session::Session;
/// The set of configuration the web server needs to start.
pub struct WebConfig {
pub port: u64,
pub project: Project,
pub server_id: u64,
pub rbx_session: Arc<RwLock<RbxSession>>,
pub message_session: MessageSession,
}
impl WebConfig {
pub fn from_session(server_id: u64, port: u64, session: &Session) -> WebConfig {
WebConfig {
port,
server_id,
project: session.project.clone(),
rbx_session: session.get_rbx_session(),
message_session: session.get_message_session(),
}
}
}
use ::{
id::Id,
message_queue::Message,
project::Project,
rbx::RbxInstance,
session::Session,
};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerInfoResponse<'a> {
pub server_id: &'a str,
pub session_id: &'a str,
pub server_version: &'a str,
pub protocol_version: u64,
pub partitions: HashMap<String, Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadAllResponse<'a> {
pub server_id: &'a str,
pub message_cursor: i32,
pub instances: Cow<'a, HashMap<Id, RbxInstance>>,
pub partition_instances: Cow<'a, HashMap<String, Id>>,
pub root_instance_id: Id,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadResponse<'a> {
pub server_id: &'a str,
pub message_cursor: i32,
pub session_id: &'a str,
pub message_cursor: u32,
pub instances: HashMap<Id, Cow<'a, RbxInstance>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribeResponse<'a> {
pub server_id: &'a str,
pub message_cursor: i32,
pub session_id: &'a str,
pub message_cursor: u32,
pub messages: Cow<'a, [Message]>,
}
pub struct Server {
config: WebConfig,
session: Arc<Session>,
server_version: &'static str,
server_id: String,
}
impl Server {
pub fn new(config: WebConfig) -> Server {
pub fn new(session: Arc<Session>) -> Server {
Server {
session: session,
server_version: env!("CARGO_PKG_VERSION"),
server_id: config.server_id.to_string(),
config,
}
}
#[allow(unreachable_code)]
pub fn handle_request(&self, request: &Request) -> Response {
router!(request,
(GET) (/) => {
Response::text("Rojo up and running!")
Response::text("Rojo is up and running!")
},
(GET) (/api/rojo) => {
// Get a summary of information about the server.
let mut partitions = HashMap::new();
for partition in self.config.project.partitions.values() {
partitions.insert(partition.name.clone(), partition.target.clone());
}
let tree = self.session.tree.read().unwrap();
Response::json(&ServerInfoResponse {
server_version: self.server_version,
protocol_version: 2,
server_id: &self.server_id,
partitions: partitions,
session_id: &self.session.session_id,
root_instance_id: tree.root_instance_id,
})
},
(GET) (/api/subscribe/{ cursor: i32 }) => {
(GET) (/api/subscribe/{ cursor: u32 }) => {
// Retrieve any messages past the given cursor index, and if
// there weren't any, subscribe to receive any new messages.
let message_queue = Arc::clone(&self.session.message_queue);
// Did the client miss any messages since the last subscribe?
{
let messages = self.config.message_session.messages.read().unwrap();
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
if cursor > messages.len() as i32 {
if new_messages.len() > 0 {
return Response::json(&SubscribeResponse {
server_id: &self.server_id,
session_id: &self.session.session_id,
messages: Cow::Borrowed(&[]),
message_cursor: messages.len() as i32 - 1,
});
}
if cursor < messages.len() as i32 - 1 {
let new_messages = &messages[(cursor + 1) as usize..];
let new_cursor = cursor + new_messages.len() as i32;
return Response::json(&SubscribeResponse {
server_id: &self.server_id,
messages: Cow::Borrowed(new_messages),
message_cursor: new_cursor,
});
})
}
}
let (tx, rx) = mpsc::channel();
let sender_id = self.config.message_session.subscribe(tx);
let sender_id = message_queue.subscribe(tx);
match rx.recv() {
Ok(_) => (),
Err(_) => return Response::text("error!").with_status_code(500),
}
self.config.message_session.unsubscribe(sender_id);
message_queue.unsubscribe(sender_id);
{
let messages = self.config.message_session.messages.read().unwrap();
let new_messages = &messages[(cursor + 1) as usize..];
let new_cursor = cursor + new_messages.len() as i32;
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
Response::json(&SubscribeResponse {
server_id: &self.server_id,
messages: Cow::Borrowed(new_messages),
return Response::json(&SubscribeResponse {
session_id: &self.session.session_id,
messages: Cow::Owned(new_messages),
message_cursor: new_cursor,
})
}
},
(GET) (/api/read_all) => {
let rbx_session = self.config.rbx_session.read().unwrap();
let message_cursor = self.config.message_session.get_message_cursor();
Response::json(&ReadAllResponse {
server_id: &self.server_id,
message_cursor,
instances: Cow::Borrowed(rbx_session.tree.get_all_instances()),
partition_instances: Cow::Borrowed(&rbx_session.partition_instances),
})
},
(GET) (/api/read/{ id_list: String }) => {
let requested_ids = id_list
let message_queue = Arc::clone(&self.session.message_queue);
let requested_ids: Result<Vec<Id>, _> = id_list
.split(",")
.map(str::parse::<Id>)
.collect::<Result<Vec<Id>, _>>();
.map(str::parse)
.collect();
let requested_ids = match requested_ids {
Ok(v) => v,
Ok(id) => id,
Err(_) => return rouille::Response::text("Malformed ID list").with_status_code(400),
};
let rbx_session = self.config.rbx_session.read().unwrap();
let tree = self.session.tree.read().unwrap();
let message_cursor = self.config.message_session.get_message_cursor();
let message_cursor = message_queue.get_message_cursor();
let mut instances = HashMap::new();
for requested_id in &requested_ids {
rbx_session.tree.get_instance_and_descendants(*requested_id, &mut instances);
for &requested_id in &requested_ids {
match tree.get_instance(requested_id) {
Some(instance) => {
instances.insert(instance.get_id(), Cow::Borrowed(instance));
for descendant in tree.iter_descendants(requested_id) {
instances.insert(descendant.get_id(), Cow::Borrowed(descendant));
}
},
None => {},
}
}
Response::json(&ReadResponse {
server_id: &self.server_id,
session_id: &self.session.session_id,
message_cursor,
instances,
})
@@ -200,13 +155,10 @@ impl Server {
_ => Response::empty_404()
)
}
}
/// Start the Rojo web server, taking over the current thread.
#[allow(unreachable_code)]
pub fn start(config: WebConfig) {
let address = format!("localhost:{}", config.port);
let server = Server::new(config);
pub fn listen(self, port: u64) {
let address = format!("localhost:{}", port);
rouille::start_server(address, move |request| server.handle_request(request));
}
rouille::start_server(address, move |request| self.handle_request(request));
}
}

View File

@@ -40,4 +40,4 @@ where
{
let body = read_json_text(&request)?;
serde_json::from_str(&body).ok()?
}
}

View File

@@ -0,0 +1,36 @@
#[macro_use] extern crate lazy_static;
extern crate librojo;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use librojo::{
project::Project,
};
lazy_static! {
static ref TEST_PROJECTS_ROOT: PathBuf = {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../test-projects")
};
}
#[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"
}
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "empty",
"tree": {
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Foo": {
"$path": "lib"
}
}
}
}