forked from rojo-rbx/rojo
merge impl-v2: server
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
149
server/Cargo.lock
generated
149
server/Cargo.lock
generated
@@ -26,7 +26,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.9"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -55,7 +55,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.0.1"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -102,7 +102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -135,8 +135,8 @@ version = "2.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -145,7 +145,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "1.7.0"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -236,7 +236,7 @@ name = "filetime"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -264,7 +264,7 @@ name = "fuchsia-zircon"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -283,7 +283,7 @@ name = "gzip-header"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -299,7 +299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-normalization 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -347,7 +347,7 @@ name = "log"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -385,8 +385,8 @@ version = "1.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -438,7 +438,7 @@ name = "net2"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -524,33 +524,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.7.21"
|
||||
version = "0.7.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.7.21"
|
||||
version = "0.7.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.7.21"
|
||||
version = "0.7.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.7.21"
|
||||
version = "0.7.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -559,7 +559,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.3.6"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -570,7 +570,7 @@ name = "quote"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -608,19 +608,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "0.2.10"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.5.5"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -636,17 +636,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rojo"
|
||||
version = "0.4.11"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempfile 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -662,9 +663,9 @@ dependencies = [
|
||||
"multipart 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -693,37 +694,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.42"
|
||||
version = "1.0.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.42"
|
||||
version = "1.0.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.16"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -748,10 +739,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.13.1"
|
||||
version = "0.13.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -765,6 +756,18 @@ dependencies = [
|
||||
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.2.14"
|
||||
@@ -863,7 +866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -985,23 +988,23 @@ dependencies = [
|
||||
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
|
||||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
"checksum ascii 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae7d751998c189c1d4468cf0a39bb2eae052a9c58d50ebb3b9591ee3813ad50"
|
||||
"checksum atty 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6609a866dd1a1b2d0ee1362195bf3e4f6438abb2d80120b83b1e1f4fb6476dd0"
|
||||
"checksum atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1"
|
||||
"checksum base64 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5032d51da2741729bfdaeb2664d9b8c6d9fd1e2b90715c660b6def36628499c2"
|
||||
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||
"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf"
|
||||
"checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789"
|
||||
"checksum brotli-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cb50f54b2e0c671b7ef1637a76237ebacbb293be179440d5d65ca288e42116bb"
|
||||
"checksum brotli2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ea9d0bbab1235017a09226b079ed733bca4bf9ecb6b6102bd01aac79ea082dca"
|
||||
"checksum buf_redux 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949"
|
||||
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
||||
"checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87"
|
||||
"checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27"
|
||||
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
|
||||
"checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18"
|
||||
"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
|
||||
"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6"
|
||||
"checksum chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87"
|
||||
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
|
||||
"checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7"
|
||||
"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
|
||||
"checksum deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)" = "32c8120d981901a9970a3a1c97cf8b630e0fa8c3ca31e75b6fd6fd5f9f427b31"
|
||||
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
||||
"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
|
||||
@@ -1046,33 +1049,33 @@ dependencies = [
|
||||
"checksum num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dee092fcdf725aee04dd7da1d21debff559237d49ef1cb3e69bcb8ece44c7364"
|
||||
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
|
||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||
"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc"
|
||||
"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f"
|
||||
"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03"
|
||||
"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2"
|
||||
"checksum proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "49b6a521dc81b643e9a51e0d1cf05df46d5a2f3c0280ea72bcb68276ba64a118"
|
||||
"checksum phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "7d37a244c75a9748e049225155f56dbcb98fe71b192fd25fd23cb914b5ad62f2"
|
||||
"checksum phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "4e4048fe7dd7a06b8127ecd6d3803149126e9b33c7558879846da3a63f734f2b"
|
||||
"checksum phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "05a079dd052e7b674d21cb31cbb6c05efd56a2cd2827db7692e2f1a507ebd998"
|
||||
"checksum phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c2261d544c2bb6aa3b10022b0be371b9c7c64f762ef28c6f5d4f1ef6d97b5930"
|
||||
"checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7"
|
||||
"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
|
||||
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
|
||||
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
|
||||
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb"
|
||||
"checksum regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bd90079345f4a4c3409214734ae220fd773c6f2e8a543d07370c6c1c369cfbfb"
|
||||
"checksum regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75ecf88252dce580404a22444fc7d626c01815debba56a7f4f536772a5ff19d3"
|
||||
"checksum regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8f1ac0f60d675cc6cf13a20ec076568254472551051ad5dd050364d70671bf6b"
|
||||
"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5"
|
||||
"checksum rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc1f8407af80b0630983b2c1f1860dda1960fdec8d3ee75ba8db14937756d3a0"
|
||||
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
|
||||
"checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637"
|
||||
"checksum serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "a73973861352c932ed1365ce22b32467ce260ac4c8db11cf750ce56334ff2dcf"
|
||||
"checksum serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b392c5a0cebb98121454531c50e60e2ffe0fbeb1a44da277da2d681d08d7dc0b"
|
||||
"checksum serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d30c4596450fd7bbda79ef15559683f9a79ac0193ea819db90000d7e1cae794"
|
||||
"checksum serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "8c6c4e049dc657a99e394bd85c22acbf97356feeec6dbf44150f2dcf79fb3118"
|
||||
"checksum serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "21924cc18e5281f232a17c040355fac97732b42cf019c24996a1642bcb169cdb"
|
||||
"checksum serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "9c624a90bec6fe9bc60d275d7af71c72c26b24cd6c6776d8e344dc4044caa3e2"
|
||||
"checksum serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f3ad6d546e765177cf3dded3c2e424a8040f870083a0e64064746b958ece9cb1"
|
||||
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
|
||||
"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
|
||||
"checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e"
|
||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||
"checksum syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "91b52877572087400e83d24b9178488541e3d535259e04ff17a63df1e5ceff59"
|
||||
"checksum syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)" = "61b8f1b737f929c6516ba46a3133fd6d5215ad8a62f66760f851f7048aebedfb"
|
||||
"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||
"checksum tempfile 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8cddbd26c5686ece823b507f304c8f188daef548b4cb753512d929ce478a093c"
|
||||
"checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
||||
@@ -1084,7 +1087,7 @@ dependencies = [
|
||||
"checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d"
|
||||
"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33"
|
||||
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
||||
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
|
||||
"checksum unicode-normalization 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90d662d111b0dbb08a180f2761026cba648c258023c355954a7c00e00e354636"
|
||||
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
|
||||
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "0.4.11"
|
||||
version = "0.5.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "A tool to create robust Roblox projects"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/LPGhatguy/rojo"
|
||||
|
||||
[lib]
|
||||
name = "librojo"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rojo"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.27.1"
|
||||
rouille = "2.1.0"
|
||||
rouille = "2.1"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
notify = "4.0.0"
|
||||
rand = "0.3"
|
||||
regex = "0.2"
|
||||
rand = "0.4"
|
||||
regex = "1.0"
|
||||
lazy_static = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
#[macro_use] extern crate serde_derive;
|
||||
#[macro_use] extern crate rouille;
|
||||
#[macro_use] extern crate clap;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate notify;
|
||||
extern crate rand;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate regex;
|
||||
|
||||
pub mod web;
|
||||
pub mod core;
|
||||
pub mod project;
|
||||
pub mod pathext;
|
||||
pub mod vfs;
|
||||
pub mod rbx;
|
||||
pub mod plugin;
|
||||
pub mod plugins;
|
||||
pub mod commands;
|
||||
extern crate librojo;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
use pathext::canonicalish;
|
||||
use librojo::pathext::canonicalish;
|
||||
|
||||
fn main() {
|
||||
let matches = clap_app!(rojo =>
|
||||
@@ -40,26 +24,15 @@ fn main() {
|
||||
(@arg port: --port +takes_value "The port to listen on. Defaults to 8000.")
|
||||
)
|
||||
|
||||
(@subcommand pack =>
|
||||
(about: "Packs the project into a GUI installer bundle. NOT YET IMPLEMENTED!")
|
||||
(@arg PROJECT: "Path to the project to pack. Defaults to the current directory.")
|
||||
)
|
||||
|
||||
(@arg verbose: --verbose "Enable extended logging.")
|
||||
).get_matches();
|
||||
|
||||
let verbose = match matches.occurrences_of("verbose") {
|
||||
0 => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
match matches.subcommand() {
|
||||
("init", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
||||
let full_path = canonicalish(project_path);
|
||||
|
||||
commands::init(&full_path);
|
||||
librojo::commands::init(&full_path);
|
||||
},
|
||||
("serve", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
@@ -82,11 +55,7 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
commands::serve(&project_path, verbose, port);
|
||||
},
|
||||
("pack", _) => {
|
||||
eprintln!("'rojo pack' is not yet implemented!");
|
||||
process::exit(1);
|
||||
librojo::commands::serve(&project_path, port);
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Please specify a subcommand!");
|
||||
|
||||
@@ -1,98 +1,37 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::fs;
|
||||
|
||||
use rand;
|
||||
|
||||
use project::{Project, ProjectLoadError};
|
||||
use plugin::{PluginChain};
|
||||
use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin};
|
||||
use vfs::{VfsSession, VfsWatcher};
|
||||
use web;
|
||||
use project::Project;
|
||||
use web::{self, WebConfig};
|
||||
use session::Session;
|
||||
|
||||
pub fn serve(project_path: &PathBuf, verbose: bool, port: Option<u64>) {
|
||||
pub fn serve(project_dir: &PathBuf, override_port: Option<u64>) {
|
||||
let server_id = rand::random::<u64>();
|
||||
|
||||
let project = match Project::load(project_path) {
|
||||
Ok(project) => {
|
||||
println!("Using project \"{}\" from {}", project.name, project_path.display());
|
||||
project
|
||||
let project = match Project::load(project_dir) {
|
||||
Ok(v) => {
|
||||
println!("Using project from {}", fs::canonicalize(project_dir).unwrap().display());
|
||||
v
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
ProjectLoadError::InvalidJson(serde_err) => {
|
||||
eprintln!("Project contained invalid JSON!");
|
||||
eprintln!("{}", project_path.display());
|
||||
eprintln!("Error: {}", serde_err);
|
||||
|
||||
process::exit(1);
|
||||
},
|
||||
ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => {
|
||||
eprintln!("Found project file, but failed to read it!");
|
||||
eprintln!("Check the permissions of the project file at {}", project_path.display());
|
||||
|
||||
process::exit(1);
|
||||
},
|
||||
ProjectLoadError::DidNotExist => {
|
||||
eprintln!("Found no project file! Create one using 'rojo init'");
|
||||
eprintln!("Checked for a project at {}", project_path.display());
|
||||
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
eprintln!("{}", err);
|
||||
process::exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
if project.partitions.len() == 0 {
|
||||
println!("");
|
||||
println!("This project has no partitions and will not do anything when served!");
|
||||
println!("This is usually a mistake -- edit rojo.json!");
|
||||
println!("");
|
||||
}
|
||||
let port = override_port.unwrap_or(project.serve_port);
|
||||
|
||||
lazy_static! {
|
||||
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
|
||||
Box::new(ScriptPlugin::new()),
|
||||
Box::new(JsonModelPlugin::new()),
|
||||
Box::new(DefaultPlugin::new()),
|
||||
]);
|
||||
}
|
||||
println!("Using project {:#?}", project);
|
||||
|
||||
let vfs = {
|
||||
let mut vfs = VfsSession::new(&PLUGIN_CHAIN);
|
||||
let mut session = Session::new(project.clone());
|
||||
session.start();
|
||||
|
||||
for (name, project_partition) in &project.partitions {
|
||||
let path = {
|
||||
let given_path = Path::new(&project_partition.path);
|
||||
let web_config = WebConfig::from_session(server_id, port, &session);
|
||||
|
||||
if given_path.is_absolute() {
|
||||
given_path.to_path_buf()
|
||||
} else {
|
||||
project_path.join(given_path)
|
||||
}
|
||||
};
|
||||
println!("Server listening on port {}", port);
|
||||
|
||||
vfs.insert_partition(name, path);
|
||||
}
|
||||
|
||||
Arc::new(Mutex::new(vfs))
|
||||
};
|
||||
|
||||
{
|
||||
let vfs = vfs.clone();
|
||||
thread::spawn(move || {
|
||||
VfsWatcher::new(vfs).start();
|
||||
});
|
||||
}
|
||||
|
||||
let web_config = web::WebConfig {
|
||||
verbose,
|
||||
port: port.unwrap_or(project.serve_port),
|
||||
server_id,
|
||||
};
|
||||
|
||||
println!("Server listening on port {}", web_config.port);
|
||||
|
||||
web::start(web_config, project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
||||
web::start(web_config);
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub type Route = Vec<String>;
|
||||
89
server/src/file_route.rs
Normal file
89
server/src/file_route.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
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());
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// This function is totally wrong and should be handled by middleware, heh.
|
||||
pub fn name(&self, partition: &Partition) -> String { // I guess??
|
||||
if self.route.len() == 0 {
|
||||
// This FileRoute refers to the partition itself
|
||||
|
||||
if partition.target.len() == 0 {
|
||||
// We're targeting the game!
|
||||
"game".to_string()
|
||||
} else {
|
||||
partition.target.last().unwrap().clone()
|
||||
}
|
||||
} else {
|
||||
// This FileRoute refers to an item in a partition
|
||||
self.route.last().unwrap().clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
21
server/src/id.rs
Normal file
21
server/src/id.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// A unique identifier, not guaranteed to be generated in any order.
|
||||
pub type Id = usize;
|
||||
|
||||
lazy_static! {
|
||||
static ref LAST_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
}
|
||||
|
||||
/// Generate a new ID, which has no defined ordering.
|
||||
pub fn get_id() -> Id {
|
||||
LAST_ID.fetch_add(1, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_gives_unique_numbers() {
|
||||
let a = get_id();
|
||||
let b = get_id();
|
||||
|
||||
assert!(a != b);
|
||||
}
|
||||
26
server/src/lib.rs
Normal file
26
server/src/lib.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
#[macro_use] extern crate serde_derive;
|
||||
#[macro_use] extern crate rouille;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate notify;
|
||||
extern crate rand;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate regex;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate tempfile;
|
||||
|
||||
pub mod commands;
|
||||
pub mod file_route;
|
||||
pub mod id;
|
||||
pub mod message_session;
|
||||
pub mod partition;
|
||||
pub mod partition_watcher;
|
||||
pub mod pathext;
|
||||
pub mod project;
|
||||
pub mod rbx;
|
||||
pub mod rbx_session;
|
||||
pub mod session;
|
||||
pub mod vfs_session;
|
||||
pub mod web;
|
||||
pub mod web_util;
|
||||
64
server/src/message_session.rs
Normal file
64
server/src/message_session.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{mpsc, Arc, RwLock, Mutex};
|
||||
|
||||
use id::{Id, get_id};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Message {
|
||||
InstanceChanged {
|
||||
id: Id,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessageSession {
|
||||
pub messages: Arc<RwLock<Vec<Message>>>,
|
||||
pub message_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl MessageSession {
|
||||
pub fn new() -> MessageSession {
|
||||
MessageSession {
|
||||
messages: Arc::new(RwLock::new(Vec::new())),
|
||||
message_listeners: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_messages(&self, new_messages: &[Message]) {
|
||||
let message_listeners = self.message_listeners.lock().unwrap();
|
||||
|
||||
{
|
||||
let mut messages = self.messages.write().unwrap();
|
||||
messages.extend_from_slice(new_messages);
|
||||
}
|
||||
|
||||
{
|
||||
for listener in message_listeners.values() {
|
||||
listener.send(()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, sender: mpsc::Sender<()>) -> Id {
|
||||
let id = get_id();
|
||||
|
||||
{
|
||||
let mut message_listeners = self.message_listeners.lock().unwrap();
|
||||
message_listeners.insert(id, sender);
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&self, id: Id) {
|
||||
{
|
||||
let mut message_listeners = self.message_listeners.lock().unwrap();
|
||||
message_listeners.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_message_cursor(&self) -> i32 {
|
||||
self.messages.read().unwrap().len() as i32 - 1
|
||||
}
|
||||
}
|
||||
13
server/src/partition.rs
Normal file
13
server/src/partition.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
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>,
|
||||
}
|
||||
65
server/src/partition_watcher.rs
Normal file
65
server/src/partition_watcher.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
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;
|
||||
|
||||
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(100)).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,60 +0,0 @@
|
||||
use rbx::RbxInstance;
|
||||
use vfs::VfsItem;
|
||||
use core::Route;
|
||||
|
||||
pub enum TransformFileResult {
|
||||
Value(Option<RbxInstance>),
|
||||
Pass,
|
||||
|
||||
// TODO: Error case
|
||||
}
|
||||
|
||||
pub enum FileChangeResult {
|
||||
MarkChanged(Option<Vec<Route>>),
|
||||
Pass,
|
||||
}
|
||||
|
||||
pub trait Plugin {
|
||||
/// Invoked when a file is read from the filesystem and needs to be turned
|
||||
/// into a Roblox instance.
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult;
|
||||
|
||||
/// Invoked when a file changes on the filesystem. The result defines what
|
||||
/// routes are marked as needing to be refreshed.
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult;
|
||||
}
|
||||
|
||||
/// A set of plugins that are composed in order.
|
||||
pub struct PluginChain {
|
||||
plugins: Vec<Box<Plugin + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl PluginChain {
|
||||
pub fn new(plugins: Vec<Box<Plugin + Send + Sync>>) -> PluginChain {
|
||||
PluginChain {
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_file(&self, vfs_item: &VfsItem) -> Option<RbxInstance> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.transform_file(self, vfs_item) {
|
||||
TransformFileResult::Value(rbx_item) => return rbx_item,
|
||||
TransformFileResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn handle_file_change(&self, route: &Route) -> Option<Vec<Route>> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.handle_file_change(route) {
|
||||
FileChangeResult::MarkChanged(changes) => return changes,
|
||||
FileChangeResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
|
||||
use rbx::{RbxInstance, RbxValue};
|
||||
use vfs::VfsItem;
|
||||
|
||||
/// A plugin with simple transforms:
|
||||
/// * Directories become Folder instances
|
||||
/// * Files become StringValue objects with 'Value' as their contents
|
||||
pub struct DefaultPlugin;
|
||||
|
||||
impl DefaultPlugin {
|
||||
pub fn new() -> DefaultPlugin {
|
||||
DefaultPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for DefaultPlugin {
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
properties.insert("Value".to_string(), RbxValue::String {
|
||||
value: contents.clone(),
|
||||
});
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: vfs_item.name().clone(),
|
||||
class_name: "StringValue".to_string(),
|
||||
children: Vec::new(),
|
||||
properties,
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
&VfsItem::Dir { ref children, .. } => {
|
||||
let mut rbx_children = Vec::new();
|
||||
|
||||
for (_, child_item) in children {
|
||||
match plugins.transform_file(child_item) {
|
||||
Some(rbx_item) => {
|
||||
rbx_children.push(rbx_item);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: vfs_item.name().clone(),
|
||||
class_name: "*".to_string(),
|
||||
children: rbx_children,
|
||||
properties: HashMap::new(),
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult {
|
||||
FileChangeResult::MarkChanged(Some(vec![route.clone()]))
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
use regex::Regex;
|
||||
use serde_json;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
|
||||
use rbx::RbxInstance;
|
||||
use vfs::VfsItem;
|
||||
|
||||
lazy_static! {
|
||||
static ref JSON_MODEL_PATTERN: Regex = Regex::new(r"^(.*?)\.model\.json$").unwrap();
|
||||
}
|
||||
|
||||
static JSON_MODEL_INIT: &'static str = "init.model.json";
|
||||
|
||||
pub struct JsonModelPlugin;
|
||||
|
||||
impl JsonModelPlugin {
|
||||
pub fn new() -> JsonModelPlugin {
|
||||
JsonModelPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for JsonModelPlugin {
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let rbx_name = match JSON_MODEL_PATTERN.captures(vfs_item.name()) {
|
||||
Some(captures) => captures.get(1).unwrap().as_str().to_string(),
|
||||
None => return TransformFileResult::Pass,
|
||||
};
|
||||
|
||||
let mut rbx_item: RbxInstance = match serde_json::from_str(contents) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to parse JSON Model File named {}: {}", vfs_item.name(), e);
|
||||
|
||||
return TransformFileResult::Pass; // This should be an error in the future!
|
||||
},
|
||||
};
|
||||
|
||||
rbx_item.route = Some(vfs_item.route().to_vec());
|
||||
rbx_item.name = rbx_name;
|
||||
|
||||
TransformFileResult::Value(Some(rbx_item))
|
||||
},
|
||||
&VfsItem::Dir { ref children, .. } => {
|
||||
let init_item = match children.get(JSON_MODEL_INIT) {
|
||||
Some(v) => v,
|
||||
None => return TransformFileResult::Pass,
|
||||
};
|
||||
|
||||
let mut rbx_item = match self.transform_file(plugins, init_item) {
|
||||
TransformFileResult::Value(Some(item)) => item,
|
||||
TransformFileResult::Value(None) | TransformFileResult::Pass => {
|
||||
eprintln!("Inconsistency detected in JsonModelPlugin!");
|
||||
return TransformFileResult::Pass;
|
||||
},
|
||||
};
|
||||
|
||||
rbx_item.name.clear();
|
||||
rbx_item.name.push_str(vfs_item.name());
|
||||
rbx_item.route = Some(vfs_item.route().to_vec());
|
||||
|
||||
for (child_name, child_item) in children {
|
||||
if child_name == init_item.name() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match plugins.transform_file(child_item) {
|
||||
Some(child_rbx_item) => {
|
||||
rbx_item.children.push(child_rbx_item);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
TransformFileResult::Value(Some(rbx_item))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult {
|
||||
let leaf = match route.last() {
|
||||
Some(v) => v,
|
||||
None => return FileChangeResult::Pass,
|
||||
};
|
||||
|
||||
let is_init = leaf == JSON_MODEL_INIT;
|
||||
|
||||
if is_init {
|
||||
let mut changed = route.clone();
|
||||
changed.pop();
|
||||
|
||||
FileChangeResult::MarkChanged(Some(vec![changed]))
|
||||
} else {
|
||||
FileChangeResult::Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod default_plugin;
|
||||
mod script_plugin;
|
||||
mod json_model_plugin;
|
||||
|
||||
pub use self::default_plugin::*;
|
||||
pub use self::script_plugin::*;
|
||||
pub use self::json_model_plugin::*;
|
||||
@@ -1,121 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
|
||||
use rbx::{RbxInstance, RbxValue};
|
||||
use vfs::VfsItem;
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVER_PATTERN: Regex = Regex::new(r"^(.*?)\.server\.lua$").unwrap();
|
||||
static ref CLIENT_PATTERN: Regex = Regex::new(r"^(.*?)\.client\.lua$").unwrap();
|
||||
static ref MODULE_PATTERN: Regex = Regex::new(r"^(.*?)\.lua$").unwrap();
|
||||
}
|
||||
|
||||
static SERVER_INIT: &'static str = "init.server.lua";
|
||||
static CLIENT_INIT: &'static str = "init.client.lua";
|
||||
static MODULE_INIT: &'static str = "init.lua";
|
||||
|
||||
pub struct ScriptPlugin;
|
||||
|
||||
impl ScriptPlugin {
|
||||
pub fn new() -> ScriptPlugin {
|
||||
ScriptPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for ScriptPlugin {
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let name = vfs_item.name();
|
||||
|
||||
let (class_name, rbx_name) = {
|
||||
if let Some(captures) = SERVER_PATTERN.captures(name) {
|
||||
("Script".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else if let Some(captures) = CLIENT_PATTERN.captures(name) {
|
||||
("LocalScript".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else if let Some(captures) = MODULE_PATTERN.captures(name) {
|
||||
("ModuleScript".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else {
|
||||
return TransformFileResult::Pass;
|
||||
}
|
||||
};
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
properties.insert("Source".to_string(), RbxValue::String {
|
||||
value: contents.clone(),
|
||||
});
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: rbx_name,
|
||||
class_name: class_name,
|
||||
children: Vec::new(),
|
||||
properties,
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
&VfsItem::Dir { ref children, .. } => {
|
||||
let init_item = {
|
||||
let maybe_item = children.get(SERVER_INIT)
|
||||
.or(children.get(CLIENT_INIT))
|
||||
.or(children.get(MODULE_INIT));
|
||||
|
||||
match maybe_item {
|
||||
Some(v) => v,
|
||||
None => return TransformFileResult::Pass,
|
||||
}
|
||||
};
|
||||
|
||||
let mut rbx_item = match self.transform_file(plugins, init_item) {
|
||||
TransformFileResult::Value(Some(item)) => item,
|
||||
_ => {
|
||||
eprintln!("Inconsistency detected in ScriptPlugin!");
|
||||
return TransformFileResult::Pass;
|
||||
},
|
||||
};
|
||||
|
||||
rbx_item.name.clear();
|
||||
rbx_item.name.push_str(vfs_item.name());
|
||||
rbx_item.route = Some(vfs_item.route().to_vec());
|
||||
|
||||
for (child_name, child_item) in children {
|
||||
if child_name == init_item.name() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match plugins.transform_file(child_item) {
|
||||
Some(child_rbx_item) => {
|
||||
rbx_item.children.push(child_rbx_item);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
TransformFileResult::Value(Some(rbx_item))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult {
|
||||
let leaf = match route.last() {
|
||||
Some(v) => v,
|
||||
None => return FileChangeResult::Pass,
|
||||
};
|
||||
|
||||
let is_init = leaf == SERVER_INIT
|
||||
|| leaf == CLIENT_INIT
|
||||
|| leaf == MODULE_INIT;
|
||||
|
||||
if is_init {
|
||||
let mut changed = route.clone();
|
||||
changed.pop();
|
||||
|
||||
FileChangeResult::MarkChanged(Some(vec![changed]))
|
||||
} else {
|
||||
FileChangeResult::Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,39 @@ use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rand::{self, Rng};
|
||||
|
||||
use serde_json;
|
||||
|
||||
use partition::Partition;
|
||||
|
||||
pub static PROJECT_FILENAME: &'static str = "rojo.json";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProjectLoadError {
|
||||
DidNotExist,
|
||||
FailedToOpen,
|
||||
FailedToRead,
|
||||
InvalidJson(serde_json::Error),
|
||||
DidNotExist(PathBuf),
|
||||
FailedToOpen(PathBuf),
|
||||
FailedToRead(PathBuf),
|
||||
InvalidJson(PathBuf, serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for ProjectLoadError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
&ProjectLoadError::InvalidJson(ref project_path, ref serde_err) => {
|
||||
write!(f, "Found invalid JSON reading project: {}\nError: {}", project_path.display(), serde_err)
|
||||
},
|
||||
&ProjectLoadError::FailedToOpen(ref project_path) |
|
||||
&ProjectLoadError::FailedToRead(ref project_path) => {
|
||||
write!(f, "Found project file, but failed to read it: {}", project_path.display())
|
||||
},
|
||||
&ProjectLoadError::DidNotExist(ref project_path) => {
|
||||
write!(f, "Could not locate a project file at {}.\nUse 'rojo init' to create one.", project_path.display())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -34,16 +55,17 @@ impl fmt::Display for ProjectInitError {
|
||||
&ProjectInitError::AlreadyExists => {
|
||||
write!(f, "A project already exists at that location.")
|
||||
},
|
||||
&ProjectInitError::FailedToCreate | &ProjectInitError::FailedToWrite => {
|
||||
&ProjectInitError::FailedToCreate |
|
||||
&ProjectInitError::FailedToWrite => {
|
||||
write!(f, "Failed to write to the given location.")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProjectPartition {
|
||||
pub struct SourceProjectPartition {
|
||||
/// A slash-separated path to a file or folder, relative to the project's
|
||||
/// directory.
|
||||
pub path: String,
|
||||
@@ -52,43 +74,114 @@ pub struct ProjectPartition {
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
/// Represents a project configured by a user for use with Rojo. Holds anything
|
||||
/// that can be configured with `rojo.json`.
|
||||
///
|
||||
/// In the future, this object will hold dependency information and other handy
|
||||
/// configurables
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
/// 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 Project {
|
||||
pub struct SourceProject {
|
||||
pub name: String,
|
||||
pub serve_port: u64,
|
||||
pub partitions: HashMap<String, ProjectPartition>,
|
||||
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 {
|
||||
/// Creates a new empty Project object with the given name.
|
||||
pub fn new<T: Into<String>>(name: T) -> 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,
|
||||
target,
|
||||
name: partition_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Project {
|
||||
name: name.into(),
|
||||
..Default::default()
|
||||
project_path,
|
||||
name: source_project.name,
|
||||
serve_port: source_project.serve_port,
|
||||
partitions,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_source_project(&self) -> SourceProject {
|
||||
let mut partitions = HashMap::new();
|
||||
|
||||
for partition in self.partitions.values() {
|
||||
let path = partition.path.strip_prefix(&self.project_path)
|
||||
.unwrap_or_else(|_| &partition.path)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let target = partition.target.join(".");
|
||||
|
||||
partitions.insert(partition.name.clone(), SourceProjectPartition {
|
||||
path,
|
||||
target,
|
||||
});
|
||||
}
|
||||
|
||||
SourceProject {
|
||||
partitions,
|
||||
name: self.name.clone(),
|
||||
serve_port: self.serve_port,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a new project inside the given folder path.
|
||||
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
||||
let location = location.as_ref();
|
||||
let package_path = location.join(PROJECT_FILENAME);
|
||||
let project_path = location.join(PROJECT_FILENAME);
|
||||
|
||||
// We abort if the project file already exists.
|
||||
match fs::metadata(&package_path) {
|
||||
Ok(_) => return Err(ProjectInitError::AlreadyExists),
|
||||
Err(_) => {},
|
||||
}
|
||||
fs::metadata(&project_path)
|
||||
.map_err(|_| ProjectInitError::AlreadyExists)?;
|
||||
|
||||
let mut file = match File::create(&package_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Err(ProjectInitError::FailedToCreate),
|
||||
};
|
||||
let mut file = File::create(&project_path)
|
||||
.map_err(|_| ProjectInitError::FailedToCreate)?;
|
||||
|
||||
// Try to give the project a meaningful name.
|
||||
// If we can't, we'll just fall back to a default.
|
||||
@@ -97,69 +190,57 @@ impl Project {
|
||||
None => "new-project".to_string(),
|
||||
};
|
||||
|
||||
// Generate a random port to run the server on.
|
||||
let serve_port = rand::thread_rng().gen_range(2000, 49151);
|
||||
|
||||
// Configure the project with all of the values we know so far.
|
||||
let project = Project::new(name);
|
||||
let serialized = serde_json::to_string_pretty(&project).unwrap();
|
||||
let source_project = SourceProject {
|
||||
name,
|
||||
serve_port,
|
||||
partitions: HashMap::new(),
|
||||
};
|
||||
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
|
||||
|
||||
match file.write(serialized.as_bytes()) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(ProjectInitError::FailedToWrite),
|
||||
}
|
||||
file.write(serialized.as_bytes())
|
||||
.map_err(|_| ProjectInitError::FailedToWrite)?;
|
||||
|
||||
Ok(project)
|
||||
Ok(Project::from_source_project(source_project, project_path))
|
||||
}
|
||||
|
||||
/// Attempts to load a project from the file named PROJECT_FILENAME from the
|
||||
/// given folder.
|
||||
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> {
|
||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
|
||||
match fs::metadata(&package_path) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(ProjectLoadError::DidNotExist),
|
||||
}
|
||||
fs::metadata(&project_path)
|
||||
.map_err(|_| ProjectLoadError::DidNotExist(project_path.clone()))?;
|
||||
|
||||
let mut file = match File::open(&package_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Err(ProjectLoadError::FailedToOpen),
|
||||
};
|
||||
let mut file = File::open(&project_path)
|
||||
.map_err(|_| ProjectLoadError::FailedToOpen(project_path.clone()))?;
|
||||
|
||||
let mut contents = String::new();
|
||||
|
||||
match file.read_to_string(&mut contents) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(ProjectLoadError::FailedToRead),
|
||||
}
|
||||
file.read_to_string(&mut contents)
|
||||
.map_err(|_| ProjectLoadError::FailedToRead(project_path.clone()))?;
|
||||
|
||||
match serde_json::from_str(&contents) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => return Err(ProjectLoadError::InvalidJson(e)),
|
||||
}
|
||||
let source_project = serde_json::from_str(&contents)
|
||||
.map_err(|e| ProjectLoadError::InvalidJson(project_path.clone(), e))?;
|
||||
|
||||
Ok(Project::from_source_project(source_project, project_path))
|
||||
}
|
||||
|
||||
/// Saves the given project file to the given folder with the appropriate name.
|
||||
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
|
||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
|
||||
let mut file = match File::create(&package_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Err(ProjectSaveError::FailedToCreate),
|
||||
};
|
||||
let mut file = File::create(&project_path)
|
||||
.map_err(|_| ProjectSaveError::FailedToCreate)?;
|
||||
|
||||
let serialized = serde_json::to_string_pretty(self).unwrap();
|
||||
let source_project = self.as_source_project();
|
||||
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
|
||||
|
||||
file.write(serialized.as_bytes()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Project {
|
||||
fn default() -> Project {
|
||||
Project {
|
||||
name: "new-project".to_string(),
|
||||
serve_port: 8000,
|
||||
partitions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,116 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents data about a Roblox instance
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
use id::Id;
|
||||
|
||||
// TODO: Switch to enum to represent more value types
|
||||
pub type RbxValue = String;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RbxInstance {
|
||||
/// Maps to the `Name` property on Instance.
|
||||
pub name: String,
|
||||
|
||||
/// Maps to the `ClassName` property on Instance.
|
||||
pub class_name: String,
|
||||
|
||||
#[serde(default = "Vec::new")]
|
||||
pub children: Vec<RbxInstance>,
|
||||
|
||||
#[serde(default = "HashMap::new")]
|
||||
/// Contains all other properties of an Instance.
|
||||
pub properties: HashMap<String, RbxValue>,
|
||||
|
||||
/// The route that this instance was generated from, if there was one.
|
||||
pub route: Option<Vec<String>>,
|
||||
/// All of the children of this instance. Order is relevant to preserve!
|
||||
pub children: Vec<Id>,
|
||||
|
||||
pub parent: Option<Id>,
|
||||
}
|
||||
|
||||
/// Any kind value that can be used by Roblox
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase", tag = "Type")]
|
||||
pub enum RbxValue {
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
String {
|
||||
value: String,
|
||||
},
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
Bool {
|
||||
value: bool,
|
||||
},
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
Number {
|
||||
value: f64,
|
||||
},
|
||||
|
||||
// TODO: Compound types like Vector3
|
||||
// This seems like a really bad idea?
|
||||
// Why isn't there a blanket impl for this for all T?
|
||||
impl<'a> From<&'a RbxInstance> for Cow<'a, RbxInstance> {
|
||||
fn from(instance: &'a RbxInstance) -> Cow<'a, RbxInstance> {
|
||||
Cow::Borrowed(instance)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RbxTree {
|
||||
instances: HashMap<Id, RbxInstance>,
|
||||
}
|
||||
|
||||
impl RbxTree {
|
||||
pub fn new() -> RbxTree {
|
||||
RbxTree {
|
||||
instances: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_all_instances(&self) -> &HashMap<Id, RbxInstance> {
|
||||
&self.instances
|
||||
}
|
||||
|
||||
pub fn insert_instance(&mut self, id: Id, instance: RbxInstance) {
|
||||
if let Some(parent_id) = instance.parent {
|
||||
if let Some(mut parent) = self.instances.get_mut(&parent_id) {
|
||||
if !parent.children.contains(&id) {
|
||||
parent.children.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.instances.insert(id, instance);
|
||||
}
|
||||
|
||||
pub fn delete_instance(&mut self, id: Id) -> Vec<Id> {
|
||||
let mut ids_to_visit = vec![id];
|
||||
let mut ids_deleted = Vec::new();
|
||||
|
||||
for instance in self.instances.values_mut() {
|
||||
match instance.children.iter().position(|&v| v == id) {
|
||||
Some(index) => {
|
||||
instance.children.remove(index);
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let id = match ids_to_visit.pop() {
|
||||
Some(id) => id,
|
||||
None => break,
|
||||
};
|
||||
|
||||
match self.instances.get(&id) {
|
||||
Some(instance) => ids_to_visit.extend_from_slice(&instance.children),
|
||||
None => continue,
|
||||
}
|
||||
|
||||
self.instances.remove(&id);
|
||||
ids_deleted.push(id);
|
||||
}
|
||||
|
||||
ids_deleted
|
||||
}
|
||||
|
||||
pub fn get_instance<'a, 'b, T>(&'a self, id: Id, output: &'b mut HashMap<Id, T>)
|
||||
where T: From<&'a RbxInstance>
|
||||
{
|
||||
let mut ids_to_visit = vec![id];
|
||||
|
||||
loop {
|
||||
let id = match ids_to_visit.pop() {
|
||||
Some(id) => id,
|
||||
None => break,
|
||||
};
|
||||
|
||||
match self.instances.get(&id) {
|
||||
Some(instance) => {
|
||||
output.insert(id, instance.into());
|
||||
|
||||
for child_id in &instance.children {
|
||||
ids_to_visit.push(*child_id);
|
||||
}
|
||||
},
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
231
server/src/rbx_session.rs
Normal file
231
server/src/rbx_session.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
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};
|
||||
use vfs_session::{VfsSession, FileItem, FileChange};
|
||||
|
||||
// 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) = {
|
||||
// TODO: Root instances have an empty route
|
||||
let file_name = route.route.last().unwrap();
|
||||
|
||||
fn strip_suffix<'a>(source: &'a str, suffix: &'static str) -> &'a str {
|
||||
&source[..source.len() - suffix.len()]
|
||||
}
|
||||
|
||||
if file_name.ends_with(".client.lua") {
|
||||
("LocalScript", "Source", strip_suffix(&file_name, ".client.lua"))
|
||||
} else if file_name.ends_with(".server.lua") {
|
||||
("Script", "Source", strip_suffix(&file_name, ".server.lua"))
|
||||
} else if file_name.ends_with(".lua") {
|
||||
("ModuleScript", "Source", strip_suffix(&file_name, ".lua"))
|
||||
} else {
|
||||
// TODO: Error/warn/skip instead of falling back
|
||||
("StringValue", "Value", file_name.as_str())
|
||||
}
|
||||
};
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert(property_key.to_string(), contents.clone());
|
||||
|
||||
tree.insert_instance(primary_id, RbxInstance {
|
||||
name: name.to_string(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
tree.insert_instance(primary_id, RbxInstance {
|
||||
name: route.name(partition).to_string(),
|
||||
class_name: "Folder".to_string(),
|
||||
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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
95
server/src/session.rs
Normal file
95
server/src/session.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::sync::{mpsc, Arc, RwLock};
|
||||
use std::thread;
|
||||
|
||||
use message_session::MessageSession;
|
||||
use partition_watcher::PartitionWatcher;
|
||||
use project::Project;
|
||||
use rbx_session::RbxSession;
|
||||
use vfs_session::VfsSession;
|
||||
|
||||
/// Stub trait for middleware
|
||||
trait Middleware {
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub project: Project,
|
||||
vfs_session: Arc<RwLock<VfsSession>>,
|
||||
rbx_session: Arc<RwLock<RbxSession>>,
|
||||
message_session: MessageSession,
|
||||
watchers: Vec<PartitionWatcher>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(project: Project) -> Session {
|
||||
let message_session = MessageSession::new();
|
||||
let vfs_session = Arc::new(RwLock::new(VfsSession::new(project.clone())));
|
||||
let rbx_session = Arc::new(RwLock::new(RbxSession::new(project.clone(), vfs_session.clone(), message_session.clone())));
|
||||
|
||||
Session {
|
||||
vfs_session,
|
||||
rbx_session,
|
||||
watchers: Vec::new(),
|
||||
message_session,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
{
|
||||
let mut vfs_session = self.vfs_session.write().unwrap();
|
||||
vfs_session.read_partitions();
|
||||
}
|
||||
|
||||
{
|
||||
let mut rbx_session = self.rbx_session.write().unwrap();
|
||||
rbx_session.read_partitions();
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
for partition in self.project.partitions.values() {
|
||||
let watcher = PartitionWatcher::start_new(partition.clone(), tx.clone());
|
||||
|
||||
self.watchers.push(watcher);
|
||||
}
|
||||
|
||||
{
|
||||
let vfs_session = self.vfs_session.clone();
|
||||
let rbx_session = self.rbx_session.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(change) => {
|
||||
{
|
||||
let mut vfs_session = vfs_session.write().unwrap();
|
||||
vfs_session.handle_change(&change);
|
||||
}
|
||||
|
||||
{
|
||||
let mut rbx_session = rbx_session.write().unwrap();
|
||||
rbx_session.handle_change(&change);
|
||||
}
|
||||
},
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(self) {
|
||||
}
|
||||
|
||||
pub fn get_vfs_session(&self) -> Arc<RwLock<VfsSession>> {
|
||||
self.vfs_session.clone()
|
||||
}
|
||||
|
||||
pub fn get_rbx_session(&self) -> Arc<RwLock<RbxSession>> {
|
||||
self.rbx_session.clone()
|
||||
}
|
||||
|
||||
pub fn get_message_session(&self) -> MessageSession {
|
||||
self.message_session.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod vfs_session;
|
||||
mod vfs_item;
|
||||
mod vfs_watcher;
|
||||
|
||||
pub use self::vfs_session::*;
|
||||
pub use self::vfs_item::*;
|
||||
pub use self::vfs_watcher::*;
|
||||
@@ -1,36 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A VfsItem represents either a file or directory as it came from the filesystem.
|
||||
///
|
||||
/// The interface here is intentionally simplified to make it easier to traverse
|
||||
/// files that have been read into memory.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum VfsItem {
|
||||
File {
|
||||
route: Vec<String>,
|
||||
file_name: String,
|
||||
contents: String,
|
||||
},
|
||||
Dir {
|
||||
route: Vec<String>,
|
||||
file_name: String,
|
||||
children: HashMap<String, VfsItem>,
|
||||
},
|
||||
}
|
||||
|
||||
impl VfsItem {
|
||||
pub fn name(&self) -> &String {
|
||||
match self {
|
||||
&VfsItem::File { ref file_name , .. } => file_name,
|
||||
&VfsItem::Dir { ref file_name , .. } => file_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route(&self) -> &[String] {
|
||||
match self {
|
||||
&VfsItem::File { ref route, .. } => route,
|
||||
&VfsItem::Dir { ref route, .. } => route,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
use plugin::PluginChain;
|
||||
use vfs::VfsItem;
|
||||
|
||||
/// Represents a virtual layer over multiple parts of the filesystem.
|
||||
///
|
||||
/// Paths in this system are represented as slices of strings, and are always
|
||||
/// relative to a partition, which is an absolute path into the real filesystem.
|
||||
pub struct VfsSession {
|
||||
/// Contains all of the partitions mounted by the Vfs.
|
||||
///
|
||||
/// These must be absolute paths!
|
||||
partitions: HashMap<String, PathBuf>,
|
||||
|
||||
/// A chronologically-sorted list of routes that changed since the Vfs was
|
||||
/// created, along with a timestamp denoting when.
|
||||
change_history: Vec<VfsChange>,
|
||||
|
||||
/// When the Vfs was initialized; used for change tracking.
|
||||
start_time: Instant,
|
||||
|
||||
plugin_chain: &'static PluginChain,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VfsChange {
|
||||
timestamp: f64,
|
||||
route: Vec<String>,
|
||||
}
|
||||
|
||||
impl VfsSession {
|
||||
pub fn new(plugin_chain: &'static PluginChain) -> VfsSession {
|
||||
VfsSession {
|
||||
partitions: HashMap::new(),
|
||||
start_time: Instant::now(),
|
||||
change_history: Vec::new(),
|
||||
plugin_chain,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_partitions(&self) -> &HashMap<String, PathBuf> {
|
||||
&self.partitions
|
||||
}
|
||||
|
||||
pub fn insert_partition<P: Into<PathBuf>>(&mut self, name: &str, path: P) {
|
||||
let path = path.into();
|
||||
|
||||
assert!(path.is_absolute());
|
||||
|
||||
self.partitions.insert(name.to_string(), path.into());
|
||||
}
|
||||
|
||||
fn route_to_path(&self, route: &[String]) -> Option<PathBuf> {
|
||||
let (partition_name, rest) = match route.split_first() {
|
||||
Some((first, rest)) => (first, rest),
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let partition = match self.partitions.get(partition_name) {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// It's possible that the partition points to a file if `rest` is empty.
|
||||
// Joining "" onto a path will put a trailing slash on, which causes
|
||||
// file reads to fail.
|
||||
let full_path = if rest.is_empty() {
|
||||
partition.clone()
|
||||
} else {
|
||||
let joined = rest.join("/");
|
||||
let relative = Path::new(&joined);
|
||||
|
||||
partition.join(relative)
|
||||
};
|
||||
|
||||
Some(full_path)
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
let reader = match fs::read_dir(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
let mut children = HashMap::new();
|
||||
|
||||
for entry in reader {
|
||||
let entry = match entry {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
let name = path.file_name().unwrap().to_string_lossy().into_owned();
|
||||
|
||||
let mut child_route = route.iter().cloned().collect::<Vec<_>>();
|
||||
child_route.push(name.clone());
|
||||
|
||||
match self.read_path(&child_route, &path) {
|
||||
Ok(child_item) => {
|
||||
children.insert(name, child_item);
|
||||
},
|
||||
Err(_) => {},
|
||||
}
|
||||
}
|
||||
|
||||
let file_name = path.file_name().unwrap().to_string_lossy().into_owned();
|
||||
|
||||
Ok(VfsItem::Dir {
|
||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
||||
file_name,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_file<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
let mut file = match File::open(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
|
||||
match file.read_to_string(&mut contents) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(()),
|
||||
}
|
||||
|
||||
let file_name = path.file_name().unwrap().to_string_lossy().into_owned();
|
||||
|
||||
Ok(VfsItem::File {
|
||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
||||
file_name,
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_path<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let metadata = match fs::metadata(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
if metadata.is_dir() {
|
||||
self.read_dir(route, path)
|
||||
} else if metadata.is_file() {
|
||||
self.read_file(route, path)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current time, used for logging timestamps for file changes.
|
||||
pub fn current_time(&self) -> f64 {
|
||||
let elapsed = self.start_time.elapsed();
|
||||
|
||||
elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9
|
||||
}
|
||||
|
||||
/// Register a new change to the filesystem at the given timestamp and VFS
|
||||
/// route.
|
||||
pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) {
|
||||
match self.plugin_chain.handle_file_change(&route) {
|
||||
Some(routes) => {
|
||||
for route in routes {
|
||||
self.change_history.push(VfsChange {
|
||||
timestamp,
|
||||
route,
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect a list of changes that occured since the given timestamp.
|
||||
pub fn changes_since(&self, timestamp: f64) -> &[VfsChange] {
|
||||
let mut marker: Option<usize> = None;
|
||||
|
||||
for (index, value) in self.change_history.iter().enumerate().rev() {
|
||||
if value.timestamp >= timestamp {
|
||||
marker = Some(index);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(index) = marker {
|
||||
&self.change_history[index..]
|
||||
} else {
|
||||
&self.change_history[..0]
|
||||
}
|
||||
}
|
||||
|
||||
/// Read an item from the filesystem using the given VFS route.
|
||||
pub fn read(&self, route: &[String]) -> Result<VfsItem, ()> {
|
||||
match self.route_to_path(route) {
|
||||
Some(path) => self.read_path(route, &path),
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self, _route: &[String], _item: VfsItem) -> Result<(), ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn delete(&self, _route: &[String]) -> Result<(), ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use pathext::path_to_route;
|
||||
use vfs::VfsSession;
|
||||
|
||||
/// An object that registers watchers on the real filesystem and relays those
|
||||
/// changes to the virtual filesystem layer.
|
||||
pub struct VfsWatcher {
|
||||
vfs: Arc<Mutex<VfsSession>>,
|
||||
}
|
||||
|
||||
impl VfsWatcher {
|
||||
pub fn new(vfs: Arc<Mutex<VfsSession>>) -> VfsWatcher {
|
||||
VfsWatcher {
|
||||
vfs,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_watcher(
|
||||
vfs: Arc<Mutex<VfsSession>>,
|
||||
rx: mpsc::Receiver<DebouncedEvent>,
|
||||
partition_name: String,
|
||||
root_path: PathBuf,
|
||||
) {
|
||||
loop {
|
||||
let event = rx.recv().unwrap();
|
||||
|
||||
let mut vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
match event {
|
||||
DebouncedEvent::Write(ref change_path) |
|
||||
DebouncedEvent::Create(ref change_path) |
|
||||
DebouncedEvent::Remove(ref change_path) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, change_path) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", change_path.display());
|
||||
}
|
||||
},
|
||||
DebouncedEvent::Rename(ref from_change, ref to_change) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, from_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", from_change.display());
|
||||
}
|
||||
|
||||
if let Some(mut route) = path_to_route(&root_path, to_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", to_change.display());
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(self) {
|
||||
let mut watchers = Vec::new();
|
||||
|
||||
// Create an extra scope so that `vfs` gets dropped and unlocked
|
||||
{
|
||||
let vfs = self.vfs.lock().unwrap();
|
||||
|
||||
for (ref partition_name, ref root_path) in vfs.get_partitions() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_millis(200))
|
||||
.expect("Unable to create watcher! This is a bug in Rojo.");
|
||||
|
||||
match watcher.watch(&root_path, RecursiveMode::Recursive) {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
panic!("Unable to watch partition {}, with path {}! Make sure that it's a file or directory.", partition_name, root_path.display());
|
||||
},
|
||||
}
|
||||
|
||||
watchers.push(watcher);
|
||||
|
||||
{
|
||||
let partition_name = partition_name.to_string();
|
||||
let root_path = root_path.to_path_buf();
|
||||
let vfs = self.vfs.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
Self::start_watcher(vfs, rx, partition_name, root_path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
thread::park();
|
||||
}
|
||||
}
|
||||
}
|
||||
242
server/src/vfs_session.rs
Normal file
242
server/src/vfs_session.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
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,225 +1,212 @@
|
||||
use std::io::Read;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{mpsc, RwLock, Arc};
|
||||
|
||||
use rouille;
|
||||
use serde;
|
||||
use serde_json;
|
||||
use rouille::{self, Request, Response};
|
||||
|
||||
use id::Id;
|
||||
use message_session::{MessageSession, Message};
|
||||
use project::Project;
|
||||
use vfs::{VfsSession, VfsChange};
|
||||
use rbx::RbxInstance;
|
||||
use plugin::PluginChain;
|
||||
|
||||
static MAX_BODY_SIZE: usize = 25 * 1024 * 1024; // 25 MiB
|
||||
use rbx_session::RbxSession;
|
||||
use session::Session;
|
||||
|
||||
/// The set of configuration the web server needs to start.
|
||||
pub struct WebConfig {
|
||||
pub port: u64,
|
||||
pub verbose: bool,
|
||||
pub project: Project,
|
||||
pub server_id: u64,
|
||||
pub rbx_session: Arc<RwLock<RbxSession>>,
|
||||
pub message_session: MessageSession,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ServerInfo<'a> {
|
||||
pub struct ServerInfoResponse<'a> {
|
||||
pub server_id: &'a str,
|
||||
pub server_version: &'a str,
|
||||
pub protocol_version: u64,
|
||||
pub partitions: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadAllResponse<'a> {
|
||||
pub server_id: &'a str,
|
||||
pub message_cursor: i32,
|
||||
pub instances: Cow<'a, HashMap<Id, RbxInstance>>,
|
||||
pub partition_instances: Cow<'a, HashMap<String, Id>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadResponse<'a> {
|
||||
pub server_id: &'a str,
|
||||
pub message_cursor: i32,
|
||||
pub instances: HashMap<Id, Cow<'a, RbxInstance>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubscribeResponse<'a> {
|
||||
pub server_id: &'a str,
|
||||
pub message_cursor: i32,
|
||||
pub messages: Cow<'a, [Message]>,
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
config: WebConfig,
|
||||
server_version: &'static str,
|
||||
protocol_version: u64,
|
||||
server_id: &'a str,
|
||||
project: &'a Project,
|
||||
current_time: f64,
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ReadResult<'a> {
|
||||
items: Vec<Option<RbxInstance>>,
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangesResult<'a> {
|
||||
changes: &'a [VfsChange],
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WriteSpecifier {
|
||||
route: String,
|
||||
item: RbxInstance,
|
||||
}
|
||||
|
||||
fn json<T: serde::Serialize>(value: T) -> rouille::Response {
|
||||
let data = serde_json::to_string(&value).unwrap();
|
||||
rouille::Response::from_data("application/json", data)
|
||||
}
|
||||
|
||||
/// Pulls text that may be JSON out of a Rouille Request object.
|
||||
///
|
||||
/// Doesn't do any actual parsing -- all this method does is verify the content
|
||||
/// type of the request and read the request's body.
|
||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
// Bail out if the request body isn't marked as JSON
|
||||
match request.header("Content-Type") {
|
||||
Some(header) => if !header.starts_with("application/json") {
|
||||
return None;
|
||||
},
|
||||
None => return None,
|
||||
impl Server {
|
||||
pub fn new(config: WebConfig) -> Server {
|
||||
Server {
|
||||
server_version: env!("CARGO_PKG_VERSION"),
|
||||
server_id: config.server_id.to_string(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
let body = match request.data() {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
||||
let mut out = Vec::new();
|
||||
match body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return None,
|
||||
}
|
||||
|
||||
// If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to
|
||||
// process it.
|
||||
if out.len() > MAX_BODY_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed = match String::from_utf8(out) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
||||
fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let body = match read_json_text(&request) {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let parsed = match serde_json::from_str(&body) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
// TODO: Change return type to some sort of Result
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// Start the Rojo web server and park our current thread.
|
||||
pub fn start(config: WebConfig, project: Project, plugin_chain: &'static PluginChain, vfs: Arc<Mutex<VfsSession>>) {
|
||||
let address = format!("localhost:{}", config.port);
|
||||
|
||||
let server_id = config.server_id.to_string();
|
||||
|
||||
rouille::start_server(address, move |request| {
|
||||
pub fn handle_request(&self, request: &Request) -> Response {
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
Response::text("Rojo up and running!")
|
||||
},
|
||||
|
||||
(GET) (/api/rojo) => {
|
||||
// Get a summary of information about the server.
|
||||
|
||||
let current_time = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
let mut partitions = HashMap::new();
|
||||
|
||||
vfs.current_time()
|
||||
};
|
||||
|
||||
json(ServerInfo {
|
||||
server_version: env!("CARGO_PKG_VERSION"),
|
||||
protocol_version: 1,
|
||||
server_id: &server_id,
|
||||
project: &project,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(GET) (/changes/{ last_time: f64 }) => {
|
||||
// Get the list of changes since the given time.
|
||||
|
||||
let vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
let changes = vfs.changes_since(last_time);
|
||||
|
||||
json(ChangesResult {
|
||||
changes,
|
||||
server_id: &server_id,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(POST) (/read) => {
|
||||
// Read some instances from the server according to a JSON
|
||||
// format body.
|
||||
|
||||
let read_request: Vec<Vec<String>> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
};
|
||||
|
||||
// Read the files off of the filesystem that the client
|
||||
// requested.
|
||||
let (items, current_time) = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for route in &read_request {
|
||||
match vfs.read(&route) {
|
||||
Ok(v) => items.push(Some(v)),
|
||||
Err(_) => items.push(None),
|
||||
}
|
||||
}
|
||||
|
||||
(items, current_time)
|
||||
};
|
||||
|
||||
// Transform all of our VfsItem objects into Roblox instances
|
||||
// the client can use.
|
||||
let rbx_items = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
match *item {
|
||||
Some(ref item) => plugin_chain.transform_file(item),
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if config.verbose {
|
||||
println!("Got read request: {:?}", read_request);
|
||||
println!("Responding with:\n\t{:?}", rbx_items);
|
||||
for partition in self.config.project.partitions.values() {
|
||||
partitions.insert(partition.name.clone(), partition.target.clone());
|
||||
}
|
||||
|
||||
json(ReadResult {
|
||||
server_id: &server_id,
|
||||
items: rbx_items,
|
||||
current_time,
|
||||
Response::json(&ServerInfoResponse {
|
||||
server_version: self.server_version,
|
||||
protocol_version: 2,
|
||||
server_id: &self.server_id,
|
||||
partitions: partitions,
|
||||
})
|
||||
},
|
||||
|
||||
(POST) (/write) => {
|
||||
// Not yet implemented.
|
||||
(GET) (/api/subscribe/{ cursor: i32 }) => {
|
||||
// Retrieve any messages past the given cursor index, and if
|
||||
// there weren't any, subscribe to receive any new messages.
|
||||
|
||||
let _write_request: Vec<WriteSpecifier> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
};
|
||||
// Did the client miss any messages since the last subscribe?
|
||||
{
|
||||
let messages = self.config.message_session.messages.read().unwrap();
|
||||
|
||||
rouille::Response::empty_404()
|
||||
if cursor > messages.len() as i32 {
|
||||
return Response::json(&SubscribeResponse {
|
||||
server_id: &self.server_id,
|
||||
messages: Cow::Borrowed(&[]),
|
||||
message_cursor: messages.len() as i32 - 1,
|
||||
});
|
||||
}
|
||||
|
||||
if cursor < messages.len() as i32 - 1 {
|
||||
let new_messages = &messages[(cursor + 1) as usize..];
|
||||
let new_cursor = cursor + new_messages.len() as i32;
|
||||
|
||||
return Response::json(&SubscribeResponse {
|
||||
server_id: &self.server_id,
|
||||
messages: Cow::Borrowed(new_messages),
|
||||
message_cursor: new_cursor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let sender_id = self.config.message_session.subscribe(tx);
|
||||
|
||||
match rx.recv() {
|
||||
Ok(_) => (),
|
||||
Err(_) => return Response::text("error!").with_status_code(500),
|
||||
}
|
||||
|
||||
self.config.message_session.unsubscribe(sender_id);
|
||||
|
||||
{
|
||||
let messages = self.config.message_session.messages.read().unwrap();
|
||||
let new_messages = &messages[(cursor + 1) as usize..];
|
||||
let new_cursor = cursor + new_messages.len() as i32;
|
||||
|
||||
Response::json(&SubscribeResponse {
|
||||
server_id: &self.server_id,
|
||||
messages: Cow::Borrowed(new_messages),
|
||||
message_cursor: new_cursor,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
_ => rouille::Response::empty_404()
|
||||
(GET) (/api/read_all) => {
|
||||
let rbx_session = self.config.rbx_session.read().unwrap();
|
||||
|
||||
let message_cursor = self.config.message_session.get_message_cursor();
|
||||
|
||||
Response::json(&ReadAllResponse {
|
||||
server_id: &self.server_id,
|
||||
message_cursor,
|
||||
instances: Cow::Borrowed(rbx_session.tree.get_all_instances()),
|
||||
partition_instances: Cow::Borrowed(&rbx_session.partition_instances),
|
||||
})
|
||||
},
|
||||
|
||||
(GET) (/api/read/{ id_list: String }) => {
|
||||
let requested_ids = id_list
|
||||
.split(",")
|
||||
.map(str::parse::<Id>)
|
||||
.collect::<Result<Vec<Id>, _>>();
|
||||
|
||||
let requested_ids = match requested_ids {
|
||||
Ok(v) => v,
|
||||
Err(_) => return rouille::Response::text("Malformed ID list").with_status_code(400),
|
||||
};
|
||||
|
||||
let rbx_session = self.config.rbx_session.read().unwrap();
|
||||
|
||||
let message_cursor = self.config.message_session.get_message_cursor();
|
||||
|
||||
let mut instances = HashMap::new();
|
||||
|
||||
for requested_id in &requested_ids {
|
||||
rbx_session.tree.get_instance(*requested_id, &mut instances);
|
||||
}
|
||||
|
||||
Response::json(&ReadResponse {
|
||||
server_id: &self.server_id,
|
||||
message_cursor,
|
||||
instances,
|
||||
})
|
||||
},
|
||||
|
||||
_ => Response::empty_404()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the Rojo web server, taking over the current thread.
|
||||
#[allow(unreachable_code)]
|
||||
pub fn start(config: WebConfig) {
|
||||
let address = format!("localhost:{}", config.port);
|
||||
let server = Server::new(config);
|
||||
|
||||
rouille::start_server(address, move |request| server.handle_request(request));
|
||||
}
|
||||
|
||||
43
server/src/web_util.rs
Normal file
43
server/src/web_util.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::io::Read;
|
||||
|
||||
use rouille;
|
||||
use serde;
|
||||
use serde_json;
|
||||
|
||||
static MAX_BODY_SIZE: usize = 100 * 1024 * 1024; // 100 MiB
|
||||
|
||||
/// Pulls text that may be JSON out of a Rouille Request object.
|
||||
///
|
||||
/// Doesn't do any actual parsing -- all this method does is verify the content
|
||||
/// type of the request and read the request's body.
|
||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
// Bail out if the request body isn't marked as JSON
|
||||
let content_type = request.header("Content-Type")?;
|
||||
|
||||
if !content_type.starts_with("application/json") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let body = request.data()?;
|
||||
|
||||
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
||||
let mut out = Vec::new();
|
||||
body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out).ok()?;
|
||||
|
||||
// If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to
|
||||
// process it.
|
||||
if out.len() > MAX_BODY_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
String::from_utf8(out).ok()
|
||||
}
|
||||
|
||||
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
||||
pub fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let body = read_json_text(&request)?;
|
||||
serde_json::from_str(&body).ok()?
|
||||
}
|
||||
5
server/test-projects/empty/rojo.json
Normal file
5
server/test-projects/empty/rojo.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "empty",
|
||||
"servePort": 23456,
|
||||
"partitions": {}
|
||||
}
|
||||
1
server/test-projects/one-partition/lib/a.lua
Normal file
1
server/test-projects/one-partition/lib/a.lua
Normal file
@@ -0,0 +1 @@
|
||||
-- a.lua
|
||||
1
server/test-projects/one-partition/lib/a.server.lua
Normal file
1
server/test-projects/one-partition/lib/a.server.lua
Normal file
@@ -0,0 +1 @@
|
||||
-- a.server.lua
|
||||
1
server/test-projects/one-partition/lib/b.client.lua
Normal file
1
server/test-projects/one-partition/lib/b.client.lua
Normal file
@@ -0,0 +1 @@
|
||||
-- b.client.lua
|
||||
10
server/test-projects/one-partition/rojo.json
Normal file
10
server/test-projects/one-partition/rojo.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "one-partition",
|
||||
"servePort": 23456,
|
||||
"partitions": {
|
||||
"lib": {
|
||||
"path": "lib",
|
||||
"target": "ReplicatedStorage.OnePartition"
|
||||
}
|
||||
}
|
||||
}
|
||||
22
server/tests/test_util/mod.rs
Normal file
22
server/tests/test_util/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use rouille::Request;
|
||||
|
||||
use librojo::web::Server;
|
||||
|
||||
pub trait HttpTestUtil {
|
||||
fn get_string(&self, url: &str) -> String;
|
||||
}
|
||||
|
||||
impl HttpTestUtil for Server {
|
||||
fn get_string(&self, url: &str) -> String {
|
||||
let info_request = Request::fake_http("GET", url, vec![], vec![]);
|
||||
let response = self.handle_request(&info_request);
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
|
||||
let (mut reader, _) = response.data.into_reader_and_size();
|
||||
let mut body = String::new();
|
||||
reader.read_to_string(&mut body).unwrap();
|
||||
|
||||
body
|
||||
}
|
||||
}
|
||||
170
server/tests/web.rs
Normal file
170
server/tests/web.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
extern crate rouille;
|
||||
extern crate serde_json;
|
||||
extern crate serde;
|
||||
|
||||
extern crate librojo;
|
||||
|
||||
mod test_util;
|
||||
use test_util::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use librojo::{
|
||||
session::Session,
|
||||
project::Project,
|
||||
web::{Server, WebConfig, ServerInfoResponse, ReadResponse, ReadAllResponse},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let project_path = {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("test-projects/empty");
|
||||
path
|
||||
};
|
||||
|
||||
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 project_path = {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("test-projects/one-partition");
|
||||
path
|
||||
};
|
||||
|
||||
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 body = server.get_string("/api/read_all");
|
||||
let response = serde_json::from_str::<ReadAllResponse>(&body).unwrap();
|
||||
|
||||
let partition_id = *response.partition_instances.get("lib").unwrap();
|
||||
|
||||
assert_eq!(response.server_id, "0");
|
||||
assert_eq!(response.message_cursor, -1);
|
||||
assert_eq!(response.instances.len(), 4); // root and three children
|
||||
|
||||
let mut found_root = false;
|
||||
let mut found_module = false;
|
||||
let mut found_client = false;
|
||||
let mut found_server = false;
|
||||
|
||||
for (id, instance) in response.instances.iter() {
|
||||
match instance.class_name.as_str() {
|
||||
// TOOD: Should partition roots (and other directories) be some
|
||||
// magical object instead of Folder?
|
||||
"Folder" => {
|
||||
assert!(!found_root);
|
||||
found_root = true;
|
||||
|
||||
assert_eq!(*id, partition_id);
|
||||
|
||||
// TODO: Should this actually equal the last part of the
|
||||
// partition's target?
|
||||
assert_eq!(instance.name, "OnePartition");
|
||||
|
||||
assert_eq!(instance.properties.len(), 0);
|
||||
assert_eq!(instance.parent, None);
|
||||
assert_eq!(instance.children.len(), 3);
|
||||
},
|
||||
"ModuleScript" => {
|
||||
assert!(!found_module);
|
||||
found_module = true;
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert("Source".to_string(), "-- a.lua".to_string());
|
||||
|
||||
assert_eq!(instance.name, "a");
|
||||
assert_eq!(instance.properties, properties);
|
||||
assert_eq!(instance.parent, Some(partition_id));
|
||||
assert_eq!(instance.children.len(), 0);
|
||||
},
|
||||
"LocalScript" => {
|
||||
assert!(!found_client);
|
||||
found_client = true;
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert("Source".to_string(), "-- b.client.lua".to_string());
|
||||
|
||||
assert_eq!(instance.name, "b");
|
||||
assert_eq!(instance.properties, properties);
|
||||
assert_eq!(instance.parent, Some(partition_id));
|
||||
assert_eq!(instance.children.len(), 0);
|
||||
},
|
||||
"Script" => {
|
||||
assert!(!found_server);
|
||||
found_server = true;
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert("Source".to_string(), "-- a.server.lua".to_string());
|
||||
|
||||
assert_eq!(instance.name, "a");
|
||||
assert_eq!(instance.properties, properties);
|
||||
assert_eq!(instance.parent, Some(partition_id));
|
||||
assert_eq!(instance.children.len(), 0);
|
||||
},
|
||||
_ => panic!("Unexpected instance!"),
|
||||
}
|
||||
}
|
||||
|
||||
assert!(found_root);
|
||||
assert!(found_module);
|
||||
assert!(found_client);
|
||||
assert!(found_server);
|
||||
}
|
||||
|
||||
// TODO: Test /read
|
||||
// TODO: Test /subscribe
|
||||
}
|
||||
Reference in New Issue
Block a user