1use std::collections::HashMap;
20use std::sync::Arc as Rc;
21
22use crate::nan_value::{Arena, ArenaEntry, NanIntExt, NanValue};
23use crate::value::RuntimeError;
24
25pub const TYPE_NAME: &str = "BranchPath";
26const FIELD: &str = "dewey";
27
28pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
29 let root_value = make_path("", arena);
33 let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(3);
34 members.push((Rc::from("Root"), root_value));
35 for method in &["child", "parse"] {
36 let idx = arena.push_builtin(&format!("{}.{}", TYPE_NAME, method));
37 members.push((Rc::from(*method), NanValue::new_builtin(idx)));
38 }
39 let ns_idx = arena.push(ArenaEntry::Namespace {
40 name: Rc::from(TYPE_NAME),
41 members,
42 });
43 global.insert(TYPE_NAME.to_string(), NanValue::new_namespace(ns_idx));
44}
45
46pub fn call_nv(
47 name: &str,
48 args: &[NanValue],
49 arena: &mut Arena,
50) -> Option<Result<NanValue, RuntimeError>> {
51 match name {
52 "BranchPath.child" => Some(child_nv(args, arena)),
53 "BranchPath.parse" => Some(parse_nv(args, arena)),
54 _ => None,
55 }
56}
57
58fn make_path(dewey: &str, arena: &mut Arena) -> NanValue {
59 let type_id = arena
60 .find_type_id(TYPE_NAME)
61 .expect("BranchPath type must be registered via register_service_types");
62 let dewey_value = NanValue::new_string_value(dewey, arena);
63 let record_idx = arena.push_record(type_id, vec![dewey_value]);
64 NanValue::new_record(record_idx)
65}
66
67fn read_dewey(value: NanValue, arena: &Arena, method: &str) -> Result<String, RuntimeError> {
68 if !value.is_record() {
69 return Err(RuntimeError::Error(format!(
70 "{}: argument must be a BranchPath, got non-record value",
71 method
72 )));
73 }
74 let (type_id, fields) = arena.get_record(value.arena_index());
75 let expected = arena
76 .find_type_id(TYPE_NAME)
77 .expect("BranchPath type must be registered");
78 if type_id != expected {
79 return Err(RuntimeError::Error(format!(
80 "{}: argument must be a BranchPath, got a different record type",
81 method
82 )));
83 }
84 let Some(field) = fields.first() else {
85 return Err(RuntimeError::Error(format!(
86 "{}: BranchPath record is missing its `{}` field",
87 method, FIELD
88 )));
89 };
90 if !field.is_string() {
91 return Err(RuntimeError::Error(format!(
92 "{}: BranchPath.{} must be a String",
93 method, FIELD
94 )));
95 }
96 Ok(arena.get_string_value(*field).to_string())
97}
98
99fn child_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
100 if args.len() != 2 {
101 return Err(RuntimeError::Error(format!(
102 "BranchPath.child() takes 2 arguments (path, idx), got {}",
103 args.len()
104 )));
105 }
106 let parent = read_dewey(args[0], arena, "BranchPath.child")?;
107 if !args[1].is_int() {
108 return Err(RuntimeError::Error(
109 "BranchPath.child: `idx` must be an Int".to_string(),
110 ));
111 }
112 let idx = args[1].as_aver_int(arena);
113 let Some(idx) = idx.to_usize() else {
114 return Err(RuntimeError::Error(format!(
115 "BranchPath.child: `idx` must be a non-negative, machine-sized Int, got {}",
116 idx
117 )));
118 };
119 let dewey = if parent.is_empty() {
120 idx.to_string()
121 } else {
122 format!("{}.{}", parent, idx)
123 };
124 Ok(make_path(&dewey, arena))
125}
126
127fn parse_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
128 if args.len() != 1 {
129 return Err(RuntimeError::Error(format!(
130 "BranchPath.parse() takes 1 argument, got {}",
131 args.len()
132 )));
133 }
134 if !args[0].is_string() {
135 return Err(RuntimeError::Error(
136 "BranchPath.parse: argument must be a String".to_string(),
137 ));
138 }
139 let raw = arena.get_string_value(args[0]).to_string();
140 if !is_valid_dewey(&raw) {
141 return Err(RuntimeError::Error(format!(
142 "BranchPath.parse: `{}` is not a valid dewey-decimal path (expected empty string for root or dot-separated non-negative integers like \"0\", \"2.0\")",
143 raw
144 )));
145 }
146 Ok(make_path(&raw, arena))
147}
148
149fn is_valid_dewey(s: &str) -> bool {
150 if s.is_empty() {
151 return true;
152 }
153 s.split('.')
154 .all(|seg| !seg.is_empty() && seg.bytes().all(|b| b.is_ascii_digit()))
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 fn fresh_arena() -> Arena {
162 let mut arena = Arena::new();
163 crate::vm::register_service_types(&mut arena);
164 arena
165 }
166
167 #[test]
168 fn root_is_empty_dewey() {
169 let mut arena = fresh_arena();
170 let path = make_path("", &mut arena);
171 let dewey = read_dewey(path, &arena, "test").unwrap();
172 assert_eq!(dewey, "");
173 }
174
175 #[test]
176 fn child_of_root_is_index() {
177 let mut arena = fresh_arena();
178 let root = make_path("", &mut arena);
179 let idx = NanValue::new_int(3, &mut arena);
180 let child = child_nv(&[root, idx], &mut arena).unwrap();
181 let dewey = read_dewey(child, &arena, "test").unwrap();
182 assert_eq!(dewey, "3");
183 }
184
185 #[test]
186 fn nested_child_joins_with_dot() {
187 let mut arena = fresh_arena();
188 let root = make_path("", &mut arena);
189 let two = NanValue::new_int(2, &mut arena);
190 let branch2 = child_nv(&[root, two], &mut arena).unwrap();
191 let zero = NanValue::new_int(0, &mut arena);
192 let nested = child_nv(&[branch2, zero], &mut arena).unwrap();
193 let dewey = read_dewey(nested, &arena, "test").unwrap();
194 assert_eq!(dewey, "2.0");
195 }
196
197 #[test]
198 fn parse_accepts_valid_dewey() {
199 let mut arena = fresh_arena();
200 for &raw in &["", "0", "1", "2.0", "10.3.4"] {
201 let s = NanValue::new_string_value(raw, &mut arena);
202 let path = parse_nv(&[s], &mut arena).expect(raw);
203 let dewey = read_dewey(path, &arena, "test").unwrap();
204 assert_eq!(dewey, raw);
205 }
206 }
207
208 #[test]
209 fn parse_rejects_non_dewey() {
210 let mut arena = fresh_arena();
211 for &raw in &[".", "1.", ".1", "1..2", "abc", "1.a", "-1", " ", "1.2.3."] {
212 let s = NanValue::new_string_value(raw, &mut arena);
213 let result = parse_nv(&[s], &mut arena);
214 assert!(result.is_err(), "{} should be rejected", raw);
215 }
216 }
217
218 #[test]
219 fn child_rejects_negative_idx() {
220 let mut arena = fresh_arena();
221 let root = make_path("", &mut arena);
222 let neg = NanValue::new_int(-1, &mut arena);
223 let result = child_nv(&[root, neg], &mut arena);
224 assert!(result.is_err());
225 }
226
227 #[test]
228 fn child_rejects_non_path_first_arg() {
229 let mut arena = fresh_arena();
230 let garbage = NanValue::new_string_value("not a path", &mut arena);
231 let idx = NanValue::new_int(0, &mut arena);
232 let result = child_nv(&[garbage, idx], &mut arena);
233 assert!(result.is_err());
234 }
235}