mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-26 07:36:19 +00:00
WIP: Epiphany Refactor (#85)
This commit is contained in:
committed by
GitHub
parent
80b9b7594b
commit
72bc77f1d5
@@ -8,7 +8,9 @@
|
|||||||
<a href="https://travis-ci.org/LPGhatguy/rojo">
|
<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" />
|
<img src="https://api.travis-ci.org/LPGhatguy/rojo.svg?branch=master" alt="Travis-CI Build Status" />
|
||||||
</a>
|
</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">
|
<a href="https://lpghatguy.github.io/rojo">
|
||||||
<img src="https://img.shields.io/badge/documentation-website-brightgreen.svg" alt="Rojo Documentation" />
|
<img src="https://img.shields.io/badge/documentation-website-brightgreen.svg" alt="Rojo Documentation" />
|
||||||
</a>
|
</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:
|
Here are a few, if you're looking for alternatives or supplements to Rojo:
|
||||||
|
|
||||||
* [rbxmk by Anaminus](https://github.com/anaminus/rbxmk)
|
* [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)
|
* [Studio Bridge by Vocksel](https://github.com/vocksel/studio-bridge)
|
||||||
* [RbxSync by evaera](https://github.com/evaera/RbxSync)
|
* [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)
|
* [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
2
integration/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
4
integration/Cargo.lock
generated
Normal file
4
integration/Cargo.lock
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[[package]]
|
||||||
|
name = "integration"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
6
integration/Cargo.toml
Normal file
6
integration/Cargo.toml
Normal 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
2
integration/README.md
Normal 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
32
integration/src/main.rs
Normal 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
5
plugin/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Rojo Plugin
|
||||||
|
|
||||||
|
This is the source to the Rojo Roblox Studio plugin.
|
||||||
|
|
||||||
|
Documentation is WIP.
|
||||||
37
plugin/loadEnvironment.lua
Normal file
37
plugin/loadEnvironment.lua
Normal 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
|
||||||
Submodule plugin/modules/lemur updated: 86b33cdfb4...96d4166a2d
@@ -4,31 +4,31 @@
|
|||||||
"partitions": {
|
"partitions": {
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"path": "src",
|
"path": "src",
|
||||||
"target": "ReplicatedStorage.Rojo.modules.Plugin"
|
"target": "ReplicatedStorage.Rojo.Modules.Plugin"
|
||||||
},
|
},
|
||||||
"modules/roact": {
|
"modules/roact": {
|
||||||
"path": "modules/roact/lib",
|
"path": "modules/roact/lib",
|
||||||
"target": "ReplicatedStorage.Rojo.modules.Roact"
|
"target": "ReplicatedStorage.Rojo.Modules.Roact"
|
||||||
},
|
},
|
||||||
"modules/rodux": {
|
"modules/rodux": {
|
||||||
"path": "modules/rodux/lib",
|
"path": "modules/rodux/lib",
|
||||||
"target": "ReplicatedStorage.Rojo.modules.Rodux"
|
"target": "ReplicatedStorage.Rojo.Modules.Rodux"
|
||||||
},
|
},
|
||||||
"modules/roact-rodux": {
|
"modules/roact-rodux": {
|
||||||
"path": "modules/roact-rodux/lib",
|
"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": {
|
"modules/testez": {
|
||||||
"path": "modules/testez/lib",
|
"path": "modules/testez/lib",
|
||||||
"target": "ReplicatedStorage.TestEZ"
|
"target": "ReplicatedStorage.TestEZ"
|
||||||
},
|
},
|
||||||
"modules/promise": {
|
|
||||||
"path": "modules/promise/lib",
|
|
||||||
"target": "ReplicatedStorage.Rojo.modules.Promise"
|
|
||||||
},
|
|
||||||
"tests": {
|
"tests": {
|
||||||
"path": "tests",
|
"path": "testBootstrap.server.lua",
|
||||||
"target": "TestService"
|
"target": "TestService.testBootstrap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
plugin/runTest.lua
Normal file
15
plugin/runTest.lua
Normal 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)
|
||||||
@@ -2,37 +2,14 @@
|
|||||||
Loads our library and all of its dependencies, then runs tests using TestEZ.
|
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 loadEnvironment = require("loadEnvironment")
|
||||||
local LOAD_MODULES = {
|
|
||||||
{"src", "plugin"},
|
|
||||||
{"modules/promise/lib", "Promise"},
|
|
||||||
{"modules/testez/lib", "TestEZ"},
|
|
||||||
}
|
|
||||||
|
|
||||||
-- This makes sure we can load Lemur and other libraries that depend on init.lua
|
local habitat, modules = loadEnvironment()
|
||||||
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
|
|
||||||
|
|
||||||
-- Load TestEZ and run our tests
|
-- 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?
|
-- Did something go wrong?
|
||||||
if results.failureCount > 0 then
|
if results.failureCount > 0 then
|
||||||
|
|||||||
@@ -19,14 +19,15 @@ setmetatable(ApiContext.Error, {
|
|||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
||||||
function ApiContext.new(url, onMessage)
|
function ApiContext.new(baseUrl, onMessage)
|
||||||
assert(type(url) == "string")
|
assert(type(baseUrl) == "string")
|
||||||
assert(type(onMessage) == "function")
|
assert(type(onMessage) == "function")
|
||||||
|
|
||||||
local context = {
|
local context = {
|
||||||
url = url,
|
baseUrl = baseUrl,
|
||||||
onMessage = onMessage,
|
onMessage = onMessage,
|
||||||
serverId = nil,
|
serverId = nil,
|
||||||
|
rootInstanceId = nil,
|
||||||
connected = false,
|
connected = false,
|
||||||
messageCursor = -1,
|
messageCursor = -1,
|
||||||
partitionRoutes = nil,
|
partitionRoutes = nil,
|
||||||
@@ -38,7 +39,9 @@ function ApiContext.new(url, onMessage)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:connect()
|
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)
|
:andThen(function(response)
|
||||||
local body = response:json()
|
local body = response:json()
|
||||||
|
|
||||||
@@ -61,15 +64,18 @@ function ApiContext:connect()
|
|||||||
self.serverId = body.serverId
|
self.serverId = body.serverId
|
||||||
self.connected = true
|
self.connected = true
|
||||||
self.partitionRoutes = body.partitions
|
self.partitionRoutes = body.partitions
|
||||||
|
self.rootInstanceId = body.rootInstanceId
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:readAll()
|
function ApiContext:read(ids)
|
||||||
if not self.connected then
|
if not self.connected then
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
end
|
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)
|
:andThen(function(response)
|
||||||
local body = response:json()
|
local body = response:json()
|
||||||
|
|
||||||
@@ -92,7 +98,9 @@ function ApiContext:retrieveMessages()
|
|||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
end
|
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)
|
:andThen(function(response)
|
||||||
local body = response:json()
|
local body = response:json()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local HTTP_DEBUG = false
|
local HTTP_DEBUG = true
|
||||||
|
|
||||||
local Promise = require(script.Parent.Parent.Promise)
|
local Promise = require(script.Parent.Parent.Promise)
|
||||||
|
|
||||||
|
|||||||
@@ -11,57 +11,7 @@ function Session.new()
|
|||||||
|
|
||||||
setmetatable(self, Session)
|
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 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)
|
api = ApiContext.new(REMOTE_URL, function(message)
|
||||||
if message.type == "InstanceChanged" then
|
if message.type == "InstanceChanged" then
|
||||||
@@ -73,7 +23,9 @@ function Session.new()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
api:connect()
|
api:connect()
|
||||||
:andThen(readAll)
|
:andThen(function()
|
||||||
|
return api:read({api.rootInstanceId})
|
||||||
|
end)
|
||||||
:andThen(function()
|
:andThen(function()
|
||||||
return api:retrieveMessages()
|
return api:retrieveMessages()
|
||||||
end)
|
end)
|
||||||
|
|||||||
2
plugin/testBootstrap.server.lua
Normal file
2
plugin/testBootstrap.server.lua
Normal 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
5
plugin/tests/empty.lua
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
local Session = require(ReplicatedStorage.Modules.Rojo.Session)
|
||||||
|
|
||||||
|
Session.new()
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
local TestEZ = require(game.ReplicatedStorage.TestEZ)
|
|
||||||
TestEZ.TestBootstrap:run(game.ReplicatedStorage.Rojo.plugin)
|
|
||||||
473
server/Cargo.lock
generated
473
server/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
4
server/README.md
Normal file
4
server/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Rojo Server
|
||||||
|
This is the source to the Rojo server.
|
||||||
|
|
||||||
|
Documentation is WIP.
|
||||||
@@ -42,20 +42,7 @@ fn main() {
|
|||||||
None => std::env::current_dir().unwrap(),
|
None => std::env::current_dir().unwrap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let port = {
|
librojo::commands::serve(&project_path);
|
||||||
match sub_matches.value_of("port") {
|
|
||||||
Some(source) => match source.parse::<u64>() {
|
|
||||||
Ok(value) => Some(value),
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!("Invalid port '{}'", source);
|
|
||||||
process::exit(1);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
librojo::commands::serve(&project_path, port);
|
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Please specify a subcommand!");
|
eprintln!("Please specify a subcommand!");
|
||||||
|
|||||||
@@ -1,40 +1,39 @@
|
|||||||
use std::path::PathBuf;
|
use std::{
|
||||||
use std::process;
|
path::Path,
|
||||||
use std::fs;
|
process,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use rand;
|
use ::{
|
||||||
|
project::Project,
|
||||||
|
web::Server,
|
||||||
|
session::Session,
|
||||||
|
roblox_studio,
|
||||||
|
};
|
||||||
|
|
||||||
use project::Project;
|
pub fn serve(fuzzy_project_location: &Path) {
|
||||||
use web::{self, WebConfig};
|
let project = match Project::load_fuzzy(fuzzy_project_location) {
|
||||||
use session::Session;
|
Ok(project) => project,
|
||||||
use roblox_studio;
|
Err(error) => {
|
||||||
|
eprintln!("Fatal: {}", error);
|
||||||
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);
|
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let port = override_port.unwrap_or(project.serve_port);
|
println!("Found project at {}", project.file_location.display());
|
||||||
|
|
||||||
println!("Using project {:#?}", project);
|
println!("Using project {:#?}", project);
|
||||||
|
|
||||||
roblox_studio::install_bundled_plugin().unwrap();
|
roblox_studio::install_bundled_plugin().unwrap();
|
||||||
|
|
||||||
let mut session = Session::new(project.clone());
|
let session = Arc::new({
|
||||||
session.start();
|
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);
|
||||||
}
|
}
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,17 +11,13 @@ extern crate regex;
|
|||||||
extern crate tempfile;
|
extern crate tempfile;
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod file_route;
|
|
||||||
pub mod id;
|
pub mod id;
|
||||||
pub mod message_session;
|
pub mod message_queue;
|
||||||
pub mod partition;
|
|
||||||
pub mod partition_watcher;
|
|
||||||
pub mod pathext;
|
pub mod pathext;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod rbx;
|
pub mod rbx;
|
||||||
pub mod rbx_session;
|
pub mod roblox_studio;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod vfs_session;
|
pub mod vfs;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
pub mod web_util;
|
pub mod web_util;
|
||||||
pub mod roblox_studio;
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{mpsc, Arc, RwLock, Mutex};
|
use std::sync::{mpsc, RwLock, Mutex};
|
||||||
|
|
||||||
use id::{Id, get_id};
|
use id::{Id, get_id};
|
||||||
|
|
||||||
@@ -11,17 +11,16 @@ pub enum Message {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
pub struct MessageQueue {
|
||||||
pub struct MessageSession {
|
messages: RwLock<Vec<Message>>,
|
||||||
pub messages: Arc<RwLock<Vec<Message>>>,
|
message_listeners: Mutex<HashMap<Id, mpsc::Sender<()>>>,
|
||||||
pub message_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageSession {
|
impl MessageQueue {
|
||||||
pub fn new() -> MessageSession {
|
pub fn new() -> MessageQueue {
|
||||||
MessageSession {
|
MessageQueue {
|
||||||
messages: Arc::new(RwLock::new(Vec::new())),
|
messages: RwLock::new(Vec::new()),
|
||||||
message_listeners: Arc::new(Mutex::new(HashMap::new())),
|
message_listeners: Mutex::new(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +57,20 @@ impl MessageSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_message_cursor(&self) -> i32 {
|
pub fn get_message_cursor(&self) -> u32 {
|
||||||
self.messages.read().unwrap().len() as i32 - 1
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>,
|
|
||||||
}
|
|
||||||
@@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +1,223 @@
|
|||||||
use std::collections::HashMap;
|
use std::{
|
||||||
use std::fmt;
|
collections::HashMap,
|
||||||
use std::fs::{self, File};
|
fmt,
|
||||||
use std::io::{Read, Write};
|
fs,
|
||||||
use std::path::{Path, PathBuf};
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
use rand::{self, Rng};
|
};
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use partition::Partition;
|
pub static PROJECT_FILENAME: &'static str = "roblox-project.json";
|
||||||
|
|
||||||
pub static PROJECT_FILENAME: &'static str = "rojo.json";
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum SourceProjectNode {
|
||||||
|
Regular {
|
||||||
|
#[serde(rename = "$className")]
|
||||||
|
class_name: String,
|
||||||
|
|
||||||
#[derive(Debug)]
|
// #[serde(rename = "$ignoreUnknown", default = "false")]
|
||||||
pub enum ProjectLoadError {
|
// ignore_unknown: bool,
|
||||||
DidNotExist(PathBuf),
|
|
||||||
FailedToOpen(PathBuf),
|
#[serde(flatten)]
|
||||||
FailedToRead(PathBuf),
|
children: HashMap<String, SourceProjectNode>,
|
||||||
InvalidJson(PathBuf, serde_json::Error),
|
},
|
||||||
|
SyncPoint {
|
||||||
|
#[serde(rename = "$path")]
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ProjectLoadError {
|
impl SourceProjectNode {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode {
|
||||||
match self {
|
match self {
|
||||||
&ProjectLoadError::InvalidJson(ref project_path, ref serde_err) => {
|
SourceProjectNode::Regular { class_name, mut children } => {
|
||||||
write!(f, "Found invalid JSON reading project: {}\nError: {}", project_path.display(), serde_err)
|
let mut new_children = HashMap::new();
|
||||||
|
|
||||||
|
for (node_name, node) in children.drain() {
|
||||||
|
new_children.insert(node_name, node.into_project_node(project_file_location));
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectNode::Regular {
|
||||||
|
class_name,
|
||||||
|
children: new_children,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
&ProjectLoadError::FailedToOpen(ref project_path) |
|
SourceProjectNode::SyncPoint { path: source_path } => {
|
||||||
&ProjectLoadError::FailedToRead(ref project_path) => {
|
let path = if Path::new(&source_path).is_absolute() {
|
||||||
write!(f, "Found project file, but failed to read it: {}", project_path.display())
|
PathBuf::from(source_path)
|
||||||
},
|
} else {
|
||||||
&ProjectLoadError::DidNotExist(ref project_path) => {
|
let project_folder_location = project_file_location.parent().unwrap();
|
||||||
write!(f, "Could not locate a project file at {}.\nUse 'rojo init' to create one.", project_path.display())
|
project_folder_location.join(source_path)
|
||||||
},
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
ProjectNode::SyncPoint {
|
||||||
pub enum ProjectSaveError {
|
|
||||||
FailedToCreate,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ProjectInitError {
|
|
||||||
AlreadyExists,
|
|
||||||
FailedToCreate,
|
|
||||||
FailedToWrite,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ProjectInitError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
&ProjectInitError::AlreadyExists => {
|
|
||||||
write!(f, "A project already exists at that location.")
|
|
||||||
},
|
|
||||||
&ProjectInitError::FailedToCreate |
|
|
||||||
&ProjectInitError::FailedToWrite => {
|
|
||||||
write!(f, "Failed to write to the given location.")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SourceProjectPartition {
|
|
||||||
/// A slash-separated path to a file or folder, relative to the project's
|
|
||||||
/// directory.
|
|
||||||
pub path: String,
|
|
||||||
|
|
||||||
/// A dot-separated route to a Roblox instance, relative to game.
|
|
||||||
pub target: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a Rojo project in the format that's most convenient for users to
|
|
||||||
/// edit. This should generally line up with `Project`, but can diverge when
|
|
||||||
/// there's either compatibility shims or when the data structures that Rojo
|
|
||||||
/// want are too verbose to write in JSON but easy to convert from something
|
|
||||||
/// else.
|
|
||||||
//
|
|
||||||
/// Holds anything that can be configured with `rojo.json`.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(default, rename_all = "camelCase")]
|
|
||||||
pub struct SourceProject {
|
|
||||||
pub name: String,
|
|
||||||
pub serve_port: u64,
|
|
||||||
pub partitions: HashMap<String, SourceProjectPartition>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SourceProject {
|
|
||||||
fn default() -> SourceProject {
|
|
||||||
SourceProject {
|
|
||||||
name: "new-project".to_string(),
|
|
||||||
serve_port: 8000,
|
|
||||||
partitions: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a Rojo project in the format that's convenient for Rojo to work
|
|
||||||
/// with.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Project {
|
|
||||||
/// The path to the project file that this project is associated with.
|
|
||||||
pub project_path: PathBuf,
|
|
||||||
|
|
||||||
/// The name of this project, used for user-facing labels.
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
/// The port that this project will run a web server on.
|
|
||||||
pub serve_port: u64,
|
|
||||||
|
|
||||||
/// All of the project's partitions, laid out in an expanded way.
|
|
||||||
pub partitions: HashMap<String, Partition>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Project {
|
|
||||||
fn from_source_project(source_project: SourceProject, project_path: PathBuf) -> Project {
|
|
||||||
let mut partitions = HashMap::new();
|
|
||||||
|
|
||||||
{
|
|
||||||
let project_directory = project_path.parent().unwrap();
|
|
||||||
|
|
||||||
for (partition_name, partition) in source_project.partitions.into_iter() {
|
|
||||||
let path = project_directory.join(&partition.path);
|
|
||||||
let target = partition.target
|
|
||||||
.split(".")
|
|
||||||
.map(String::from)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
partitions.insert(partition_name.clone(), Partition {
|
|
||||||
path,
|
path,
|
||||||
target,
|
}
|
||||||
name: partition_name,
|
},
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SourceProject {
|
||||||
|
name: String,
|
||||||
|
tree: HashMap<String, SourceProjectNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceProject {
|
||||||
|
pub fn into_project(mut self, project_file_location: &Path) -> Project {
|
||||||
|
let mut tree = HashMap::new();
|
||||||
|
|
||||||
|
for (node_name, node) in self.tree.drain() {
|
||||||
|
tree.insert(node_name, node.into_project_node(project_file_location));
|
||||||
}
|
}
|
||||||
|
|
||||||
Project {
|
Project {
|
||||||
project_path,
|
name: self.name,
|
||||||
name: source_project.name,
|
tree: tree,
|
||||||
serve_port: source_project.serve_port,
|
file_location: PathBuf::from(project_file_location),
|
||||||
partitions,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fn as_source_project(&self) -> SourceProject {
|
|
||||||
let mut partitions = HashMap::new();
|
#[derive(Debug)]
|
||||||
|
pub enum ProjectLoadExactError {
|
||||||
for partition in self.partitions.values() {
|
IoError(io::Error),
|
||||||
let path = partition.path.strip_prefix(&self.project_path)
|
JsonError(serde_json::Error),
|
||||||
.unwrap_or_else(|_| &partition.path)
|
}
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
impl fmt::Display for ProjectLoadExactError {
|
||||||
.to_string();
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
let target = partition.target.join(".");
|
ProjectLoadExactError::IoError(inner) => write!(output, "{}", inner),
|
||||||
|
ProjectLoadExactError::JsonError(inner) => write!(output, "{}", inner),
|
||||||
partitions.insert(partition.name.clone(), SourceProjectPartition {
|
}
|
||||||
path,
|
}
|
||||||
target,
|
}
|
||||||
});
|
|
||||||
}
|
#[derive(Debug)]
|
||||||
|
pub enum ProjectInitError {}
|
||||||
SourceProject {
|
|
||||||
partitions,
|
impl fmt::Display for ProjectInitError {
|
||||||
name: self.name.clone(),
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||||
serve_port: self.serve_port,
|
write!(output, "ProjectInitError")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes a new project inside the given folder path.
|
#[derive(Debug)]
|
||||||
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
pub enum ProjectLoadFuzzyError {
|
||||||
let location = location.as_ref();
|
NotFound,
|
||||||
let project_path = location.join(PROJECT_FILENAME);
|
IoError(io::Error),
|
||||||
|
JsonError(serde_json::Error),
|
||||||
// We abort if the project file already exists.
|
}
|
||||||
fs::metadata(&project_path)
|
|
||||||
.map_err(|_| ProjectInitError::AlreadyExists)?;
|
impl From<ProjectLoadExactError> for ProjectLoadFuzzyError {
|
||||||
|
fn from(error: ProjectLoadExactError) -> ProjectLoadFuzzyError {
|
||||||
let mut file = File::create(&project_path)
|
match error {
|
||||||
.map_err(|_| ProjectInitError::FailedToCreate)?;
|
ProjectLoadExactError::IoError(inner) => ProjectLoadFuzzyError::IoError(inner),
|
||||||
|
ProjectLoadExactError::JsonError(inner) => ProjectLoadFuzzyError::JsonError(inner),
|
||||||
// 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(),
|
impl fmt::Display for ProjectLoadFuzzyError {
|
||||||
};
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
// Generate a random port to run the server on.
|
ProjectLoadFuzzyError::NotFound => write!(output, "Project not found."),
|
||||||
let serve_port = rand::thread_rng().gen_range(2000, 49151);
|
ProjectLoadFuzzyError::IoError(inner) => write!(output, "{}", inner),
|
||||||
|
ProjectLoadFuzzyError::JsonError(inner) => write!(output, "{}", inner),
|
||||||
// Configure the project with all of the values we know so far.
|
}
|
||||||
let source_project = SourceProject {
|
}
|
||||||
name,
|
}
|
||||||
serve_port,
|
|
||||||
partitions: HashMap::new(),
|
#[derive(Debug)]
|
||||||
};
|
pub enum ProjectSaveError {}
|
||||||
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
|
|
||||||
|
impl fmt::Display for ProjectSaveError {
|
||||||
file.write(serialized.as_bytes())
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||||
.map_err(|_| ProjectInitError::FailedToWrite)?;
|
write!(output, "ProjectSaveError")
|
||||||
|
}
|
||||||
Ok(Project::from_source_project(source_project, project_path))
|
}
|
||||||
}
|
|
||||||
|
#[derive(Debug)]
|
||||||
/// Attempts to load a project from the file named PROJECT_FILENAME from the
|
pub enum ProjectNode {
|
||||||
/// given folder.
|
Regular {
|
||||||
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> {
|
class_name: String,
|
||||||
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
children: HashMap<String, ProjectNode>,
|
||||||
|
|
||||||
fs::metadata(&project_path)
|
// ignore_unknown: bool,
|
||||||
.map_err(|_| ProjectLoadError::DidNotExist(project_path.clone()))?;
|
},
|
||||||
|
SyncPoint {
|
||||||
let mut file = File::open(&project_path)
|
path: PathBuf,
|
||||||
.map_err(|_| ProjectLoadError::FailedToOpen(project_path.clone()))?;
|
},
|
||||||
|
}
|
||||||
let mut contents = String::new();
|
|
||||||
|
#[derive(Debug)]
|
||||||
file.read_to_string(&mut contents)
|
pub struct Project {
|
||||||
.map_err(|_| ProjectLoadError::FailedToRead(project_path.clone()))?;
|
pub name: String,
|
||||||
|
pub tree: HashMap<String, ProjectNode>,
|
||||||
let source_project = serde_json::from_str(&contents)
|
pub file_location: PathBuf,
|
||||||
.map_err(|e| ProjectLoadError::InvalidJson(project_path.clone(), e))?;
|
}
|
||||||
|
|
||||||
Ok(Project::from_source_project(source_project, project_path))
|
impl Project {
|
||||||
}
|
pub fn init(_project_folder_location: &Path) -> Result<(), ProjectInitError> {
|
||||||
|
unimplemented!();
|
||||||
/// 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));
|
pub fn locate(start_location: &Path) -> Option<PathBuf> {
|
||||||
|
// TODO: Check for specific error kinds, convert 'not found' to Result.
|
||||||
let mut file = File::create(&project_path)
|
let location_metadata = fs::metadata(start_location).ok()?;
|
||||||
.map_err(|_| ProjectSaveError::FailedToCreate)?;
|
|
||||||
|
// If this is a file, we should assume it's the config we want
|
||||||
let source_project = self.as_source_project();
|
if location_metadata.is_file() {
|
||||||
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
|
return Some(start_location.to_path_buf());
|
||||||
|
} else if location_metadata.is_dir() {
|
||||||
file.write(serialized.as_bytes()).unwrap();
|
let with_file = start_location.join(PROJECT_FILENAME);
|
||||||
|
|
||||||
Ok(())
|
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!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use id::Id;
|
use id::{Id, get_id};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@@ -23,46 +22,107 @@ pub struct RbxInstance {
|
|||||||
/// Contains all other properties of an Instance.
|
/// Contains all other properties of an Instance.
|
||||||
pub properties: HashMap<String, RbxValue>,
|
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!
|
/// 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.
|
/// The parent of the instance, if there is one.
|
||||||
pub parent: Option<Id>,
|
parent: Option<Id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This seems like a really bad idea?
|
impl RbxInstance {
|
||||||
// Why isn't there a blanket impl for this for all T?
|
pub fn get_id(&self) -> Id {
|
||||||
impl<'a> From<&'a RbxInstance> for Cow<'a, RbxInstance> {
|
self.id
|
||||||
fn from(instance: &'a RbxInstance) -> Cow<'a, RbxInstance> {
|
}
|
||||||
Cow::Borrowed(instance)
|
}
|
||||||
|
|
||||||
|
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 {
|
pub struct RbxTree {
|
||||||
instances: HashMap<Id, RbxInstance>,
|
instances: HashMap<Id, RbxInstance>,
|
||||||
|
pub root_instance_id: Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RbxTree {
|
impl RbxTree {
|
||||||
pub fn new() -> 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 {
|
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> {
|
pub fn get_all_instances(&self) -> &HashMap<Id, RbxInstance> {
|
||||||
&self.instances
|
&self.instances
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_instance(&mut self, id: Id, instance: RbxInstance) {
|
pub fn insert_instance(&mut self, mut instance: RbxInstance) {
|
||||||
if let Some(parent_id) = instance.parent {
|
match instance.parent {
|
||||||
if let Some(mut parent) = self.instances.get_mut(&parent_id) {
|
Some(parent_id) => {
|
||||||
if !parent.children.contains(&id) {
|
match self.instances.get_mut(&parent_id) {
|
||||||
parent.children.push(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> {
|
pub fn delete_instance(&mut self, id: Id) -> Vec<Id> {
|
||||||
@@ -101,27 +161,20 @@ impl RbxTree {
|
|||||||
ids_deleted
|
ids_deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_instance_and_descendants<'a, 'b, T>(&'a self, id: Id, output: &'b mut HashMap<Id, T>)
|
pub fn iter_descendants<'a>(&'a self, id: Id) -> Descendants<'a> {
|
||||||
where T: From<&'a RbxInstance>
|
match self.get_instance(id) {
|
||||||
{
|
Some(instance) => {
|
||||||
let mut ids_to_visit = vec![id];
|
Descendants {
|
||||||
|
tree: self,
|
||||||
loop {
|
ids_to_visit: instance.children.clone(),
|
||||||
let id = match ids_to_visit.pop() {
|
}
|
||||||
Some(id) => id,
|
},
|
||||||
None => break,
|
None => {
|
||||||
};
|
Descendants {
|
||||||
|
tree: self,
|
||||||
match self.instances.get(&id) {
|
ids_to_visit: vec![],
|
||||||
Some(instance) => {
|
}
|
||||||
output.insert(id, instance.into());
|
},
|
||||||
|
|
||||||
for child_id in &instance.children {
|
|
||||||
ids_to_visit.push(*child_id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => continue,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +1,121 @@
|
|||||||
use std::sync::{mpsc, Arc, RwLock};
|
use std::{
|
||||||
use std::thread;
|
sync::{Arc, RwLock, Mutex, mpsc},
|
||||||
|
thread,
|
||||||
|
io,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use message_session::MessageSession;
|
use rand;
|
||||||
use partition_watcher::PartitionWatcher;
|
|
||||||
use project::Project;
|
|
||||||
use rbx_session::RbxSession;
|
|
||||||
use vfs_session::VfsSession;
|
|
||||||
|
|
||||||
/// Stub trait for middleware
|
use notify::{
|
||||||
trait Middleware {
|
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 struct Session {
|
||||||
pub project: Project,
|
project: Project,
|
||||||
vfs_session: Arc<RwLock<VfsSession>>,
|
pub session_id: String,
|
||||||
rbx_session: Arc<RwLock<RbxSession>>,
|
pub message_queue: Arc<MessageQueue>,
|
||||||
message_session: MessageSession,
|
pub tree: Arc<RwLock<RbxTree>>,
|
||||||
watchers: Vec<PartitionWatcher>,
|
vfs: Arc<Mutex<Vfs>>,
|
||||||
|
watchers: Vec<RecommendedWatcher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(project: Project) -> Session {
|
pub fn new(project: Project) -> Session {
|
||||||
let message_session = MessageSession::new();
|
let session_id = rand::random::<u64>().to_string();
|
||||||
let vfs_session = Arc::new(RwLock::new(VfsSession::new(project.clone())));
|
|
||||||
let rbx_session = Arc::new(RwLock::new(RbxSession::new(project.clone(), vfs_session.clone(), message_session.clone())));
|
|
||||||
|
|
||||||
Session {
|
Session {
|
||||||
vfs_session,
|
session_id,
|
||||||
rbx_session,
|
|
||||||
watchers: Vec::new(),
|
|
||||||
message_session,
|
|
||||||
project,
|
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) {
|
pub fn start(&mut self) -> io::Result<()> {
|
||||||
{
|
fn add_sync_points(vfs: &mut Vfs, project_node: &ProjectNode) -> io::Result<()> {
|
||||||
let mut vfs_session = self.vfs_session.write().unwrap();
|
match project_node {
|
||||||
vfs_session.read_partitions();
|
ProjectNode::Regular { children, .. } => {
|
||||||
}
|
for child in children.values() {
|
||||||
|
add_sync_points(vfs, child)?;
|
||||||
{
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
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
139
server/src/vfs.rs
Normal 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!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +1,152 @@
|
|||||||
use std::borrow::Cow;
|
use std::{
|
||||||
use std::collections::HashMap;
|
borrow::Cow,
|
||||||
use std::sync::{mpsc, RwLock, Arc};
|
collections::HashMap,
|
||||||
|
sync::{mpsc, Arc},
|
||||||
|
};
|
||||||
|
|
||||||
use rouille::{self, Request, Response};
|
use rouille::{self, Request, Response};
|
||||||
|
|
||||||
use id::Id;
|
use ::{
|
||||||
use message_session::{MessageSession, Message};
|
id::Id,
|
||||||
use project::Project;
|
message_queue::Message,
|
||||||
use rbx::RbxInstance;
|
project::Project,
|
||||||
use rbx_session::RbxSession;
|
rbx::RbxInstance,
|
||||||
use session::Session;
|
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ServerInfoResponse<'a> {
|
pub struct ServerInfoResponse<'a> {
|
||||||
pub server_id: &'a str,
|
pub session_id: &'a str,
|
||||||
pub server_version: &'a str,
|
pub server_version: &'a str,
|
||||||
pub protocol_version: u64,
|
pub protocol_version: u64,
|
||||||
pub partitions: HashMap<String, Vec<String>>,
|
pub root_instance_id: Id,
|
||||||
}
|
|
||||||
|
|
||||||
#[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>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ReadResponse<'a> {
|
pub struct ReadResponse<'a> {
|
||||||
pub server_id: &'a str,
|
pub session_id: &'a str,
|
||||||
pub message_cursor: i32,
|
pub message_cursor: u32,
|
||||||
pub instances: HashMap<Id, Cow<'a, RbxInstance>>,
|
pub instances: HashMap<Id, Cow<'a, RbxInstance>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SubscribeResponse<'a> {
|
pub struct SubscribeResponse<'a> {
|
||||||
pub server_id: &'a str,
|
pub session_id: &'a str,
|
||||||
pub message_cursor: i32,
|
pub message_cursor: u32,
|
||||||
pub messages: Cow<'a, [Message]>,
|
pub messages: Cow<'a, [Message]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
config: WebConfig,
|
session: Arc<Session>,
|
||||||
server_version: &'static str,
|
server_version: &'static str,
|
||||||
server_id: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub fn new(config: WebConfig) -> Server {
|
pub fn new(session: Arc<Session>) -> Server {
|
||||||
Server {
|
Server {
|
||||||
|
session: session,
|
||||||
server_version: env!("CARGO_PKG_VERSION"),
|
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 {
|
pub fn handle_request(&self, request: &Request) -> Response {
|
||||||
router!(request,
|
router!(request,
|
||||||
(GET) (/) => {
|
(GET) (/) => {
|
||||||
Response::text("Rojo up and running!")
|
Response::text("Rojo is up and running!")
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/api/rojo) => {
|
(GET) (/api/rojo) => {
|
||||||
// Get a summary of information about the server.
|
// Get a summary of information about the server.
|
||||||
|
|
||||||
let mut partitions = HashMap::new();
|
let tree = self.session.tree.read().unwrap();
|
||||||
|
|
||||||
for partition in self.config.project.partitions.values() {
|
|
||||||
partitions.insert(partition.name.clone(), partition.target.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
Response::json(&ServerInfoResponse {
|
Response::json(&ServerInfoResponse {
|
||||||
server_version: self.server_version,
|
server_version: self.server_version,
|
||||||
protocol_version: 2,
|
protocol_version: 2,
|
||||||
server_id: &self.server_id,
|
session_id: &self.session.session_id,
|
||||||
partitions: partitions,
|
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
|
// Retrieve any messages past the given cursor index, and if
|
||||||
// there weren't any, subscribe to receive any new messages.
|
// 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?
|
// 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 {
|
return Response::json(&SubscribeResponse {
|
||||||
server_id: &self.server_id,
|
session_id: &self.session.session_id,
|
||||||
messages: Cow::Borrowed(&[]),
|
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,
|
message_cursor: new_cursor,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
let sender_id = self.config.message_session.subscribe(tx);
|
let sender_id = message_queue.subscribe(tx);
|
||||||
|
|
||||||
match rx.recv() {
|
match rx.recv() {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(_) => return Response::text("error!").with_status_code(500),
|
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_cursor, new_messages) = message_queue.get_messages_since(cursor);
|
||||||
let new_messages = &messages[(cursor + 1) as usize..];
|
|
||||||
let new_cursor = cursor + new_messages.len() as i32;
|
|
||||||
|
|
||||||
Response::json(&SubscribeResponse {
|
return Response::json(&SubscribeResponse {
|
||||||
server_id: &self.server_id,
|
session_id: &self.session.session_id,
|
||||||
messages: Cow::Borrowed(new_messages),
|
messages: Cow::Owned(new_messages),
|
||||||
message_cursor: new_cursor,
|
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 }) => {
|
(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(",")
|
.split(",")
|
||||||
.map(str::parse::<Id>)
|
.map(str::parse)
|
||||||
.collect::<Result<Vec<Id>, _>>();
|
.collect();
|
||||||
|
|
||||||
let requested_ids = match requested_ids {
|
let requested_ids = match requested_ids {
|
||||||
Ok(v) => v,
|
Ok(id) => id,
|
||||||
Err(_) => return rouille::Response::text("Malformed ID list").with_status_code(400),
|
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();
|
let mut instances = HashMap::new();
|
||||||
|
|
||||||
for requested_id in &requested_ids {
|
for &requested_id in &requested_ids {
|
||||||
rbx_session.tree.get_instance_and_descendants(*requested_id, &mut instances);
|
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 {
|
Response::json(&ReadResponse {
|
||||||
server_id: &self.server_id,
|
session_id: &self.session.session_id,
|
||||||
message_cursor,
|
message_cursor,
|
||||||
instances,
|
instances,
|
||||||
})
|
})
|
||||||
@@ -200,13 +155,10 @@ impl Server {
|
|||||||
_ => Response::empty_404()
|
_ => Response::empty_404()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the Rojo web server, taking over the current thread.
|
pub fn listen(self, port: u64) {
|
||||||
#[allow(unreachable_code)]
|
let address = format!("localhost:{}", port);
|
||||||
pub fn start(config: WebConfig) {
|
|
||||||
let address = format!("localhost:{}", config.port);
|
|
||||||
let server = Server::new(config);
|
|
||||||
|
|
||||||
rouille::start_server(address, move |request| server.handle_request(request));
|
rouille::start_server(address, move |request| self.handle_request(request));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
36
server/tests/read_projects.rs
Normal file
36
server/tests/read_projects.rs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
test-projects/empty/roblox-project.json
Normal file
4
test-projects/empty/roblox-project.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "empty",
|
||||||
|
"tree": {}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "empty",
|
|
||||||
"servePort": 23456,
|
|
||||||
"partitions": {}
|
|
||||||
}
|
|
||||||
26
test-projects/foo.json
Normal file
26
test-projects/foo.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- a.lua
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- a.server.lua
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- b.client.lua
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "one-partition",
|
|
||||||
"servePort": 23456,
|
|
||||||
"partitions": {
|
|
||||||
"lib": {
|
|
||||||
"path": "lib",
|
|
||||||
"target": "ReplicatedStorage.OnePartition"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- foo.lua
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "partition-to-file",
|
|
||||||
"servePort": 23456,
|
|
||||||
"partitions": {
|
|
||||||
"lib": {
|
|
||||||
"path": "foo.lua",
|
|
||||||
"target": "ReplicatedStorage.bar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
test-projects/single-sync-point/lib/main.lua
Normal file
0
test-projects/single-sync-point/lib/main.lua
Normal file
11
test-projects/single-sync-point/roblox-project.json
Normal file
11
test-projects/single-sync-point/roblox-project.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "empty",
|
||||||
|
"tree": {
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"$className": "ReplicatedStorage",
|
||||||
|
"Foo": {
|
||||||
|
"$path": "lib"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user