mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Add Luau-style ref paths migration plan and update related tests
- Introduced a new migration plan for converting `Rojo_Ref_*` attribute paths to Luau require-by-string style, enhancing portability with relative paths. - Updated existing test snapshots to reflect the new path formats, ensuring consistency across various scenarios. - Enhanced the `compute_relative_ref_path` and `resolve_ref_path` functions to support the new path semantics, improving path resolution logic. - Refactored related code in `change_processor`, `syncback`, and `snapshot` modules to accommodate the new reference path handling. These changes improve the robustness and maintainability of reference path management in the project.
This commit is contained in:
parent
d6169c6d84
commit
b8c3520d25
29 changed files with 2147 additions and 179 deletions
829
src/rojo_ref.rs
829
src/rojo_ref.rs
|
|
@ -16,8 +16,9 @@ use serde::{Deserialize, Serialize};
|
|||
pub const REF_ID_ATTRIBUTE_NAME: &str = "Rojo_Id";
|
||||
/// Legacy: Pointer attribute prefix using IDs (kept for backwards compatibility)
|
||||
pub const REF_POINTER_ATTRIBUTE_PREFIX: &str = "Rojo_Target_";
|
||||
/// Prefix for path-based reference attributes. This is the preferred system
|
||||
/// that stores the path to the target instance (e.g., "SoundService/Effects").
|
||||
/// Prefix for path-based reference attributes using Luau require-by-string
|
||||
/// style paths (e.g., `@self/Handle.model.json5`, `./Sibling.luau`,
|
||||
/// `../Uncle/Part.model.json5`, `@game/ReplicatedStorage/Module.luau`).
|
||||
/// No modification to the target instance is needed.
|
||||
pub const REF_PATH_ATTRIBUTE_PREFIX: &str = "Rojo_Ref_";
|
||||
|
||||
|
|
@ -35,9 +36,9 @@ pub fn ref_target_attribute_name(prop_name: &str) -> String {
|
|||
format!("{REF_POINTER_ATTRIBUTE_PREFIX}{prop_name}")
|
||||
}
|
||||
|
||||
/// Escape "/" in an instance name for use in ref paths.
|
||||
/// Uses "\/" as escape sequence. Also escapes existing backslashes to "\\"
|
||||
/// so the escaping is unambiguous.
|
||||
/// Escape "/" in an instance name for use in legacy instance-name paths.
|
||||
/// Only used by `ref_target_path` (for debug/log output via `inst_path`).
|
||||
/// `Rojo_Ref_*` attributes use filesystem names which can't contain `/`.
|
||||
pub fn escape_ref_path_segment(name: &str) -> Cow<'_, str> {
|
||||
if name.contains('/') || name.contains('\\') {
|
||||
Cow::Owned(name.replace('\\', "\\\\").replace('/', "\\/"))
|
||||
|
|
@ -107,15 +108,12 @@ pub fn split_ref_path(path: &str) -> Vec<String> {
|
|||
segments
|
||||
}
|
||||
|
||||
/// Compute the path to a target instance for use in a `Rojo_Ref_*` attribute.
|
||||
/// Returns the slash-separated path from the root (root name excluded).
|
||||
/// Compute a human-readable path to an instance using raw instance names.
|
||||
/// Used for debug/log output via `inst_path()`. NOT used for `Rojo_Ref_*`
|
||||
/// attributes (those use `compute_relative_ref_path` with filesystem names).
|
||||
///
|
||||
/// Instance names containing "/" are escaped as "\/" to prevent ambiguity.
|
||||
///
|
||||
/// **Legacy version:** Uses raw instance names with escaping. New code should
|
||||
/// use `ref_target_path_from_tree()` which uses filesystem names.
|
||||
///
|
||||
/// Example: an instance at `DataModel > Workspace > Part1` returns `"Workspace/Part1"`.
|
||||
/// An instance named `"A/B"` at Workspace returns `"Workspace/A\/B"`.
|
||||
/// Example: `DataModel > Workspace > Part1` returns `"Workspace/Part1"`.
|
||||
pub fn ref_target_path(dom: &WeakDom, target_ref: Ref) -> String {
|
||||
let root_ref = dom.root_ref();
|
||||
let mut components: Vec<Cow<'_, str>> = dom
|
||||
|
|
@ -127,21 +125,18 @@ pub fn ref_target_path(dom: &WeakDom, target_ref: Ref) -> String {
|
|||
components.join("/")
|
||||
}
|
||||
|
||||
/// Compute the filesystem-name-based path to a target instance for use in
|
||||
/// `Rojo_Ref_*` attributes.
|
||||
/// Compute the **absolute** filesystem-name-based path to an instance.
|
||||
///
|
||||
/// Returns a slash-separated path from DataModel's children downward.
|
||||
/// Used as input to `compute_relative_ref_path()` and for RefPathIndex
|
||||
/// indexing. Not written to disk directly -- `compute_relative_ref_path`
|
||||
/// converts absolute paths to the Luau-style relative format.
|
||||
///
|
||||
/// Each path segment is the **full filesystem name** of the instance:
|
||||
/// - `Folder "Foo"` → `"Foo"` (directory name)
|
||||
/// - `ModuleScript "Foo"` → `"Foo.luau"` (slug + extension)
|
||||
/// - `Script "Foo"` (server) → `"Foo.server.luau"`
|
||||
/// - Dedup'd `ModuleScript "Foo"` → `"Foo~2.luau"`
|
||||
///
|
||||
/// Filesystem names are derived from `instigating_source` paths (the actual
|
||||
/// file/dir on disk). For instances without filesystem backing (inside .rbxm
|
||||
/// files, newly added), falls back to the instance name.
|
||||
///
|
||||
/// Since filesystem names can't contain `/` (slugified to `_`), path splitting
|
||||
/// is a simple `split('/')` -- no escaping needed.
|
||||
pub fn ref_target_path_from_tree(tree: &crate::snapshot::RojoTree, target_ref: Ref) -> String {
|
||||
let dom = tree.inner();
|
||||
let root_ref = dom.root_ref();
|
||||
|
|
@ -195,6 +190,108 @@ pub fn ref_target_path_from_tree(tree: &crate::snapshot::RojoTree, target_ref: R
|
|||
components.join("/")
|
||||
}
|
||||
|
||||
/// Compute a Luau require-by-string style path from a source instance to a
|
||||
/// target instance. Both inputs are **absolute** ref paths (slash-separated
|
||||
/// filesystem names from DataModel's children downward).
|
||||
///
|
||||
/// Returns a prefixed path following Roblox's require-by-string semantics:
|
||||
/// - `@self` when target == source
|
||||
/// - `@self/...` when target is a descendant of source
|
||||
/// - `./...` when source and target share the same parent (1 level up)
|
||||
/// - `../...` chains for navigation within the same service (2+ levels up)
|
||||
/// - `@game/...` for cross-service refs or when target is an ancestor of source
|
||||
///
|
||||
/// Reference: <https://create.roblox.com/docs/reference/engine/globals/LuaGlobals#require>
|
||||
pub fn compute_relative_ref_path(source_abs: &str, target_abs: &str) -> String {
|
||||
if source_abs == target_abs {
|
||||
return "@self".to_string();
|
||||
}
|
||||
|
||||
let source_prefix = format!("{source_abs}/");
|
||||
if target_abs.starts_with(&source_prefix) {
|
||||
return format!("@self/{}", &target_abs[source_prefix.len()..]);
|
||||
}
|
||||
|
||||
let source_parts: Vec<&str> = source_abs.split('/').collect();
|
||||
let target_parts: Vec<&str> = target_abs.split('/').collect();
|
||||
|
||||
let common_len = source_parts
|
||||
.iter()
|
||||
.zip(target_parts.iter())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.count();
|
||||
|
||||
if common_len == 0 {
|
||||
return format!("@game/{target_abs}");
|
||||
}
|
||||
|
||||
let ups = source_parts.len() - common_len;
|
||||
let remaining_parts = &target_parts[common_len..];
|
||||
|
||||
if remaining_parts.is_empty() {
|
||||
return format!("@game/{target_abs}");
|
||||
}
|
||||
|
||||
let remaining = remaining_parts.join("/");
|
||||
|
||||
if ups == 1 {
|
||||
format!("./{remaining}")
|
||||
} else {
|
||||
let mut result = String::with_capacity(3 * (ups - 1) + remaining.len());
|
||||
for _ in 0..(ups - 1) {
|
||||
result.push_str("../");
|
||||
}
|
||||
result.push_str(&remaining);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a relative (Luau-style) ref path back to an absolute ref path
|
||||
/// using only string manipulation. `source_abs` is the absolute ref path of
|
||||
/// the instance that owns the attribute.
|
||||
///
|
||||
/// Returns `None` if the path navigates above the root.
|
||||
pub fn resolve_ref_path_to_absolute(path: &str, source_abs: &str) -> Option<String> {
|
||||
if let Some(rest) = path.strip_prefix("@game/") {
|
||||
return Some(rest.to_string());
|
||||
}
|
||||
if path == "@game" {
|
||||
return Some(String::new());
|
||||
}
|
||||
if path == "@self" {
|
||||
return Some(source_abs.to_string());
|
||||
}
|
||||
|
||||
let (mut parts, rest) = if let Some(rest) = path.strip_prefix("@self/") {
|
||||
(source_abs.split('/').collect::<Vec<_>>(), rest)
|
||||
} else if let Some(rest) = path.strip_prefix("./") {
|
||||
let mut p: Vec<&str> = source_abs.split('/').collect();
|
||||
p.pop();
|
||||
(p, rest)
|
||||
} else if let Some(rest) = path.strip_prefix("../") {
|
||||
let mut p: Vec<&str> = source_abs.split('/').collect();
|
||||
p.pop();
|
||||
if p.pop().is_none() {
|
||||
return None;
|
||||
}
|
||||
(p, rest)
|
||||
} else {
|
||||
return Some(path.to_string());
|
||||
};
|
||||
|
||||
for segment in rest.split('/') {
|
||||
if segment == ".." {
|
||||
if parts.pop().is_none() {
|
||||
return None;
|
||||
}
|
||||
} else if !segment.is_empty() {
|
||||
parts.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
Some(parts.join("/"))
|
||||
}
|
||||
|
||||
/// Extract a string value from a Variant that may be String or BinaryString.
|
||||
/// Returns None with a warning for non-string types or invalid UTF-8.
|
||||
/// Used by compute_ref_properties and defer_ref_properties for parsing
|
||||
|
|
@ -300,10 +397,15 @@ impl RefPathIndex {
|
|||
|
||||
/// Scan all `.meta.json5`, `.model.json5`, `.meta.json`, `.model.json`
|
||||
/// files under `root` for existing `Rojo_Ref_*` attributes and index them.
|
||||
/// This ensures pre-existing ref attributes (from CLI syncback, manual
|
||||
/// edits, or previous sessions) are indexed so rename path updates work
|
||||
/// from the first rename.
|
||||
pub fn populate_from_dir(&mut self, root: &Path) {
|
||||
///
|
||||
/// Relative paths (prefixed with `@self/`, `./`, `../`) are resolved to
|
||||
/// absolute using `tree` so the index always stores absolute target paths.
|
||||
/// This ensures prefix-based lookup works correctly for rename updates.
|
||||
pub fn populate_from_dir(
|
||||
&mut self,
|
||||
root: &Path,
|
||||
tree: &crate::snapshot::RojoTree,
|
||||
) {
|
||||
let mut count = 0usize;
|
||||
let mut dirs_to_visit = vec![root.to_path_buf()];
|
||||
while let Some(dir) = dirs_to_visit.pop() {
|
||||
|
|
@ -328,13 +430,25 @@ impl RefPathIndex {
|
|||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let source_abs = tree
|
||||
.get_ids_at_path(&path)
|
||||
.first()
|
||||
.map(|&id| crate::ref_target_path_from_tree(tree, id))
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Ok(bytes) = std::fs::read(&path) {
|
||||
if let Ok(val) = crate::json::from_slice::<serde_json::Value>(&bytes) {
|
||||
if let Some(attrs) = val.get("attributes").and_then(|a| a.as_object()) {
|
||||
for (key, value) in attrs {
|
||||
if key.starts_with(crate::REF_PATH_ATTRIBUTE_PREFIX) {
|
||||
if let Some(path_str) = value.as_str() {
|
||||
self.add(path_str, &path);
|
||||
let resolved = crate::resolve_ref_path_to_absolute(
|
||||
path_str,
|
||||
&source_abs,
|
||||
)
|
||||
.unwrap_or_else(|| path_str.to_string());
|
||||
self.add(&resolved, &path);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -712,4 +826,665 @@ mod tests {
|
|||
// Should be deduplicated to just one entry
|
||||
assert_eq!(results.len(), 1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// compute_relative_ref_path tests -- @self
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn relative_self_reference() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Model", "Workspace/Model"),
|
||||
"@self"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_direct_child() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Model", "Workspace/Model/Handle.model.json5"),
|
||||
"@self/Handle.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_nested_descendant() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Model",
|
||||
"Workspace/Model/SubFolder/Part.model.json5"
|
||||
),
|
||||
"@self/SubFolder/Part.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_deeply_nested() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Model", "Workspace/Model/A/B/C/D.luau"),
|
||||
"@self/A/B/C/D.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_tool_handle() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Tool", "Workspace/Tool/Handle.model.json5"),
|
||||
"@self/Handle.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_init_style_child() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Model", "Workspace/Model/Init"),
|
||||
"@self/Init"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_nested_sound() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Gun", "Workspace/Gun/Sounds/Fire.model.json5"),
|
||||
"@self/Sounds/Fire.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_slugified_child() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Model",
|
||||
"Workspace/Model/Hey_Bro.server.luau"
|
||||
),
|
||||
"@self/Hey_Bro.server.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_self_dedup_child() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Model", "Workspace/Model/Data~2"),
|
||||
"@self/Data~2"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// compute_relative_ref_path tests -- ./
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn relative_dot_sibling_script() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Folder/ScriptA.server.luau",
|
||||
"Workspace/Folder/ScriptB.server.luau"
|
||||
),
|
||||
"./ScriptB.server.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_sibling_module() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Folder/ScriptA.server.luau",
|
||||
"Workspace/Folder/Config.luau"
|
||||
),
|
||||
"./Config.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_sibling_directory() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Folder/ScriptA.server.luau",
|
||||
"Workspace/Folder/SubFolder"
|
||||
),
|
||||
"./SubFolder"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_sibling_then_descend() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Folder/ScriptA.server.luau",
|
||||
"Workspace/Folder/SubFolder/Deep.luau"
|
||||
),
|
||||
"./SubFolder/Deep.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_object_value_sibling() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Folder/ObjectValue.model.json5",
|
||||
"Workspace/Folder/Target.model.json5"
|
||||
),
|
||||
"./Target.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_beam_attachment0() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Beams/Beam.model.json5",
|
||||
"Workspace/Beams/Att1.model.json5"
|
||||
),
|
||||
"./Att1.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_beam_attachment1() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Beams/Beam.model.json5",
|
||||
"Workspace/Beams/Att2.model.json5"
|
||||
),
|
||||
"./Att2.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_slugified_sibling() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Folder/A.luau",
|
||||
"Workspace/Folder/Hey_Bro.server.luau"
|
||||
),
|
||||
"./Hey_Bro.server.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_dedup_sibling_folder() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Folder/A.luau", "Workspace/Folder/Data~2"),
|
||||
"./Data~2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dot_dedup_sibling_model() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Folder/A.luau",
|
||||
"Workspace/Folder/Data~3.model.json5"
|
||||
),
|
||||
"./Data~3.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// compute_relative_ref_path tests -- ../
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_2_ups() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/A/Script.server.luau",
|
||||
"Workspace/B/Part.model.json5"
|
||||
),
|
||||
"../B/Part.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_2_ups_directory() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A/Script.server.luau", "Workspace/B"),
|
||||
"../B"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_3_ups() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/A/B/Script.server.luau",
|
||||
"Workspace/C/Part.model.json5"
|
||||
),
|
||||
"../../C/Part.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_4_ups() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/A/B/C/Script.luau",
|
||||
"Workspace/D/E/Part.model.json5"
|
||||
),
|
||||
"../../../D/E/Part.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_5_ups() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/A/B/C/D/Script.luau",
|
||||
"Workspace/E/Part.model.json5"
|
||||
),
|
||||
"../../../../E/Part.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_peer_model() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Models/Car/Body.model.json5",
|
||||
"Workspace/Models/Truck/Body.model.json5"
|
||||
),
|
||||
"../Truck/Body.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_cousin() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A/Deep/Script.luau", "Workspace/A/Other.luau"),
|
||||
"../Other.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_beam_uncle_folder() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/A/B/Beam.model.json5",
|
||||
"Workspace/A/C/Att.model.json5"
|
||||
),
|
||||
"../C/Att.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_cross_system() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Systems/Combat/Hitbox.server.luau",
|
||||
"Workspace/Systems/Audio/HitSound.model.json5"
|
||||
),
|
||||
"../Audio/HitSound.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_same_zone() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Map/Zone1/Spawns/Spawn1.model.json5",
|
||||
"Workspace/Map/Zone1/Props/Tree.model.json5"
|
||||
),
|
||||
"../Props/Tree.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_dotdot_cross_zone() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Map/Zone1/Spawns/Spawn1.model.json5",
|
||||
"Workspace/Map/Zone2/Flag.model.json5"
|
||||
),
|
||||
"../../Zone2/Flag.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// compute_relative_ref_path tests -- @game/
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn relative_game_cross_service_classic() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/Script.server.luau",
|
||||
"ReplicatedStorage/Module.luau"
|
||||
),
|
||||
"@game/ReplicatedStorage/Module.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_to_server_storage() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Script.server.luau", "ServerStorage/Data.luau"),
|
||||
"@game/ServerStorage/Data.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_sss_to_rs() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"ServerScriptService/Main.server.luau",
|
||||
"ReplicatedStorage/Shared/Utils.luau"
|
||||
),
|
||||
"@game/ReplicatedStorage/Shared/Utils.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_client_to_rs() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"StarterGui/ScreenGui/Button.client.luau",
|
||||
"ReplicatedStorage/UI/Theme.luau"
|
||||
),
|
||||
"@game/ReplicatedStorage/UI/Theme.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_to_service_itself() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A/B/C/Script.luau", "Lighting"),
|
||||
"@game/Lighting"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_ancestor_own_service() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A/B/C/Script.luau", "Workspace"),
|
||||
"@game/Workspace"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_ancestor_grandparent() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A/B/Script.luau", "Workspace/A"),
|
||||
"@game/Workspace/A"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_ancestor_great_grandparent() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A/B/C/Script.luau", "Workspace/A/B"),
|
||||
"@game/Workspace/A/B"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_ancestor_own_service_single_level() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("ReplicatedStorage/A.luau", "ReplicatedStorage"),
|
||||
"@game/ReplicatedStorage"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_game_to_sound_service() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Script.luau", "SoundService/BGM.model.json5"),
|
||||
"@game/SoundService/BGM.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// compute_relative_ref_path tests -- special names
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn relative_special_windows_reserved() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Folder/A.luau", "Workspace/Other/CON_.luau"),
|
||||
"../Other/CON_.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_special_into_dedup_folder() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A.luau", "Workspace/Data~2/Child.luau"),
|
||||
"./Data~2/Child.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_special_init_style_child() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Model", "Workspace/Model/Scripts"),
|
||||
"@self/Scripts"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_special_init_style_both() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Scripts", "Workspace/Scripts/Foo"),
|
||||
"@self/Foo"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// compute_relative_ref_path tests -- tricky edge cases
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn relative_edge_service_own_child() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace", "Workspace/Model.model.json5"),
|
||||
"@self/Model.model.json5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_edge_service_to_service() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace", "ReplicatedStorage/Module.luau"),
|
||||
"@game/ReplicatedStorage/Module.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_edge_service_self_ref() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace", "Workspace"),
|
||||
"@self"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_edge_service_to_different_service() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace", "Lighting"),
|
||||
"@game/Lighting"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_edge_segment_prefix_ambiguity() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/A", "Workspace/AB"),
|
||||
"./AB"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_edge_same_stem_different_ext() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path("Workspace/Foo.server.luau", "Workspace/Foo.luau"),
|
||||
"./Foo.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_edge_deep_close_cousins() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/A/B/C/D/E/F/Script.luau",
|
||||
"Workspace/A/B/C/D/E/G/Target.luau"
|
||||
),
|
||||
"../G/Target.luau"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_edge_deep_far_apart() {
|
||||
assert_eq!(
|
||||
compute_relative_ref_path(
|
||||
"Workspace/A/B/C/D/E/F/Script.luau",
|
||||
"Workspace/X/Target.luau"
|
||||
),
|
||||
"../../../../../../X/Target.luau"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// resolve_ref_path_to_absolute tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_game_prefix() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("@game/ReplicatedStorage/Module.luau", "Workspace/Script"),
|
||||
Some("ReplicatedStorage/Module.luau".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_self_no_path() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("@self", "Workspace/Model"),
|
||||
Some("Workspace/Model".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_self_with_child() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("@self/Handle.model.json5", "Workspace/Model"),
|
||||
Some("Workspace/Model/Handle.model.json5".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_self_nested() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("@self/A/B/C", "Workspace/Model"),
|
||||
Some("Workspace/Model/A/B/C".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_dot_sibling() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("./Sibling.luau", "Workspace/Folder/Script.luau"),
|
||||
Some("Workspace/Folder/Sibling.luau".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_dot_sibling_descend() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("./Sub/Deep.luau", "Workspace/Folder/Script.luau"),
|
||||
Some("Workspace/Folder/Sub/Deep.luau".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_dotdot_2_ups() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("../B/Part.model.json5", "Workspace/A/Script.luau"),
|
||||
Some("Workspace/B/Part.model.json5".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_dotdot_3_ups() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("../../C/Part.model.json5", "Workspace/A/B/Script.luau"),
|
||||
Some("Workspace/C/Part.model.json5".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_dotdot_cross_service_via_root() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute(
|
||||
"../../ReplicatedStorage/Module.luau",
|
||||
"Workspace/A/Script.luau"
|
||||
),
|
||||
Some("ReplicatedStorage/Module.luau".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_bare_path_legacy() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute(
|
||||
"Workspace/Model/Handle.model.json5",
|
||||
"anything/here"
|
||||
),
|
||||
Some("Workspace/Model/Handle.model.json5".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_dotdot_mid_path() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("@self/Sub/../Handle.model.json5", "Workspace/Model"),
|
||||
Some("Workspace/Model/Handle.model.json5".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_abs_dotdot_above_root_returns_none() {
|
||||
assert_eq!(
|
||||
resolve_ref_path_to_absolute("../../X", "Workspace"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_then_compute_round_trip() {
|
||||
let source = "Workspace/A/B/Script.luau";
|
||||
let target = "Workspace/C/Part.model.json5";
|
||||
let relative = compute_relative_ref_path(source, target);
|
||||
assert_eq!(relative, "../../C/Part.model.json5");
|
||||
let resolved = resolve_ref_path_to_absolute(&relative, source).unwrap();
|
||||
assert_eq!(resolved, target);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_then_compute_round_trip_self() {
|
||||
let source = "Workspace/Model";
|
||||
let target = "Workspace/Model/Handle.model.json5";
|
||||
let relative = compute_relative_ref_path(source, target);
|
||||
assert_eq!(relative, "@self/Handle.model.json5");
|
||||
let resolved = resolve_ref_path_to_absolute(&relative, source).unwrap();
|
||||
assert_eq!(resolved, target);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_then_compute_round_trip_game() {
|
||||
let source = "Workspace/Script.luau";
|
||||
let target = "ReplicatedStorage/Module.luau";
|
||||
let relative = compute_relative_ref_path(source, target);
|
||||
assert_eq!(relative, "@game/ReplicatedStorage/Module.luau");
|
||||
let resolved = resolve_ref_path_to_absolute(&relative, source).unwrap();
|
||||
assert_eq!(resolved, target);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_then_compute_round_trip_dot() {
|
||||
let source = "Workspace/Folder/A.luau";
|
||||
let target = "Workspace/Folder/B.luau";
|
||||
let relative = compute_relative_ref_path(source, target);
|
||||
assert_eq!(relative, "./B.luau");
|
||||
let resolved = resolve_ref_path_to_absolute(&relative, source).unwrap();
|
||||
assert_eq!(resolved, target);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue