1use std::borrow::Cow;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11
12use kaish_types::{json_to_value_no_envelope, value_to_json};
13
14use crate::ast::{Value, VarPath, VarSegment};
15
16use super::eval::value_to_string;
17use super::result::ExecResult;
18
19#[derive(Debug, Clone, PartialEq)]
39pub enum PathError {
40 UndefinedRoot(String),
42 Absence(String),
44 Shape(String),
46}
47
48fn type_name(value: &Value) -> &'static str {
50 match value {
51 Value::Null => "null",
52 Value::Bool(_) => "a boolean",
53 Value::Int(_) => "an integer",
54 Value::Float(_) => "a float",
55 Value::String(_) => "a string",
56 Value::Json(serde_json::Value::Array(_)) => "a list",
57 Value::Json(serde_json::Value::Object(_)) => "a record",
58 Value::Json(_) => "a scalar",
59 Value::Bytes(_) => "binary data",
60 }
61}
62
63fn value_as_index(value: &Value) -> Option<i64> {
66 match value {
67 Value::Int(i) => Some(*i),
68 Value::String(s) => s.parse::<i64>().ok(),
69 _ => None,
70 }
71}
72
73#[derive(Debug, Clone, PartialEq)]
80enum Step {
81 Index(usize),
83 Key(String),
85 Slice(usize, usize),
87}
88
89fn classify_index(json: &serde_json::Value, i: i64, path: &str) -> Result<Step, PathError> {
95 let arr = match json {
96 serde_json::Value::Array(a) => a,
97 serde_json::Value::Object(_) => {
98 return Err(PathError::Shape(format!(
99 "${{{path}[{i}]}}: integer index on a record — record keys are strings, use ${{{path}[\"{i}\"]}}"
100 )))
101 }
102 _ => unreachable!("resolve_step guards non-collection containers"),
103 };
104 let len = arr.len() as i64;
105 let idx = if i < 0 { len + i } else { i };
106 if idx < 0 || idx >= len {
107 return Err(PathError::Absence(format!(
108 "${{{path}[{i}]}}: index out of bounds (list length {len})"
109 )));
110 }
111 Ok(Step::Index(idx as usize))
112}
113
114fn classify_key(json: &serde_json::Value, key: &str, path: &str) -> Result<Step, PathError> {
118 match json {
119 serde_json::Value::Object(_) => Ok(Step::Key(key.to_string())),
120 serde_json::Value::Array(_) => Err(PathError::Shape(format!(
121 "${{{path}[{key}]}}: string key on a list — use an integer index"
122 ))),
123 _ => unreachable!("resolve_step guards non-collection containers"),
124 }
125}
126
127fn classify_slice(
131 json: &serde_json::Value,
132 start: Option<i64>,
133 end: Option<i64>,
134 path: &str,
135) -> Result<Step, PathError> {
136 let arr = match json {
137 serde_json::Value::Array(a) => a,
138 serde_json::Value::Object(_) => {
139 return Err(PathError::Shape(format!(
140 "${{{path}[..]}}: cannot slice a record"
141 )))
142 }
143 _ => unreachable!("resolve_step guards non-collection containers"),
144 };
145 let len = arr.len() as i64;
146 let norm = |b: i64| -> i64 {
147 let b = if b < 0 { len + b } else { b };
148 b.clamp(0, len)
149 };
150 let s = start.map(norm).unwrap_or(0);
151 let e = end.map(norm).unwrap_or(len);
152 let (s, e) = if s >= e {
153 (s as usize, s as usize)
154 } else {
155 (s as usize, e as usize)
156 };
157 Ok(Step::Slice(s, e))
158}
159
160fn dotted_access_error(path: &str, field: &str) -> PathError {
164 PathError::Shape(format!(
165 "${{{path}…}}: kaish uses bracket access, not dots — write the key as a subscript: [{field}]"
166 ))
167}
168
169fn render_segment(seg: &VarSegment) -> String {
173 match seg {
174 VarSegment::Index(i) => format!("[{i}]"),
175 VarSegment::Key(k) => format!("[{k}]"),
176 VarSegment::Dynamic(v) => format!("[${v}]"),
177 VarSegment::Slice(a, b) => format!(
178 "[{}:{}]",
179 a.map(|n| n.to_string()).unwrap_or_default(),
180 b.map(|n| n.to_string()).unwrap_or_default()
181 ),
182 VarSegment::Field(f) => format!(".{f}"),
183 }
184}
185
186fn resolve_step(
192 container: &serde_json::Value,
193 seg: &VarSegment,
194 scope: &Scope,
195 path: &str,
196) -> Result<Step, PathError> {
197 if let VarSegment::Field(name) = seg {
201 return Err(dotted_access_error(path, name));
202 }
203
204 if !matches!(
206 container,
207 serde_json::Value::Array(_) | serde_json::Value::Object(_)
208 ) {
209 return Err(PathError::Shape(format!(
210 "${{{path}…}}: cannot subscript {} — it is not a collection",
211 type_name(&json_to_value_no_envelope(container.clone()))
212 )));
213 }
214
215 match seg {
216 VarSegment::Index(i) => classify_index(container, *i, path),
217 VarSegment::Key(k) => classify_key(container, k, path),
218 VarSegment::Slice(start, end) => classify_slice(container, *start, *end, path),
219 VarSegment::Dynamic(var) => {
220 let key_val = scope.get(var).ok_or_else(|| {
224 PathError::UndefinedRoot(format!("${{{path}[${var}]}}: ${var} is not set"))
225 })?;
226 match container {
227 serde_json::Value::Array(_) => {
228 let idx = value_as_index(key_val).ok_or_else(|| {
229 PathError::Shape(format!(
230 "${{{path}[${var}]}}: a list index must be an integer, got \"{}\"",
231 value_to_string(key_val)
232 ))
233 })?;
234 classify_index(container, idx, path)
235 }
236 serde_json::Value::Object(_) => Ok(Step::Key(value_to_string(key_val))),
237 _ => unreachable!("non-collection container guarded above"),
238 }
239 }
240 VarSegment::Field(_) => unreachable!("dotted segment handled above"),
241 }
242}
243
244fn descend<'a>(
250 current: Cow<'a, serde_json::Value>,
251 step: Step,
252 path: &str,
253) -> Result<Cow<'a, serde_json::Value>, PathError> {
254 match step {
255 Step::Slice(s, e) => {
256 let Some(arr) = current.as_array() else {
257 unreachable!("slice classified against an array")
258 };
259 Ok(Cow::Owned(serde_json::Value::Array(arr[s..e].to_vec())))
260 }
261 Step::Index(i) => match current {
262 Cow::Borrowed(j) => {
263 let Some(arr) = j.as_array() else {
264 unreachable!("index classified against an array")
265 };
266 Ok(Cow::Borrowed(&arr[i]))
267 }
268 Cow::Owned(j) => {
269 let Some(arr) = j.as_array() else {
270 unreachable!("index classified against an array")
271 };
272 Ok(Cow::Owned(arr[i].clone()))
273 }
274 },
275 Step::Key(k) => match current {
276 Cow::Borrowed(j) => match j.as_object().and_then(|m| m.get(&k)) {
277 Some(child) => Ok(Cow::Borrowed(child)),
278 None => Err(PathError::Absence(format!("${{{path}[{k}]}}: no such key"))),
279 },
280 Cow::Owned(j) => match j.as_object().and_then(|m| m.get(&k)) {
281 Some(child) => Ok(Cow::Owned(child.clone())),
282 None => Err(PathError::Absence(format!("${{{path}[{k}]}}: no such key"))),
283 },
284 },
285 }
286}
287
288fn descend_mut<'a>(
298 current: &'a mut serde_json::Value,
299 step: Step,
300 path: &str,
301) -> Result<&'a mut serde_json::Value, PathError> {
302 match step {
303 Step::Slice(..) => Err(PathError::Shape(format!(
304 "${{{path}[..]}}: slice lvalues are not supported — index or key paths only"
305 ))),
306 Step::Index(i) => {
307 let Some(arr) = current.as_array_mut() else {
308 unreachable!("index classified against an array")
309 };
310 Ok(&mut arr[i])
311 }
312 Step::Key(k) => {
313 let Some(map) = current.as_object_mut() else {
314 unreachable!("key classified against an object")
315 };
316 match map.get_mut(&k) {
317 Some(child) => Ok(child),
318 None => Err(PathError::Absence(format!(
319 "${{{path}[{k}]}}: no such key — no autovivification, create it first (e.g. `{path}[{k}]={{}}`)"
320 ))),
321 }
322 }
323 }
324}
325
326fn apply_leaf_write(
333 current: &mut serde_json::Value,
334 step: Step,
335 value: serde_json::Value,
336 path: &str,
337) -> Result<(), PathError> {
338 match step {
339 Step::Slice(..) => Err(PathError::Shape(format!(
340 "${{{path}[..]}}: slice lvalues are not supported — index or key paths only"
341 ))),
342 Step::Index(i) => {
343 let Some(arr) = current.as_array_mut() else {
344 unreachable!("index classified against an array")
345 };
346 arr[i] = value;
347 Ok(())
348 }
349 Step::Key(k) => {
350 let Some(map) = current.as_object_mut() else {
351 unreachable!("key classified against an object")
352 };
353 map.insert(k, value);
354 Ok(())
355 }
356 }
357}
358
359fn push_path_error_message(err: PathError, root_name: &str) -> String {
366 match err {
367 PathError::UndefinedRoot(msg) if msg.is_empty() => {
368 format!("push: {root_name} is not defined")
369 }
370 PathError::UndefinedRoot(msg) => format!("push: {msg}"),
371 PathError::Absence(msg) | PathError::Shape(msg) => msg,
372 }
373}
374
375#[derive(Debug, Clone)]
386pub struct Scope {
387 frames: Arc<Vec<HashMap<String, Value>>>,
390 exported: HashSet<String>,
392 last_result: Box<ExecResult>,
400 script_name: String,
402 positional: Vec<String>,
404 error_exit: bool,
406 errexit_suppressed: usize,
409 show_ast: bool,
411 latch_enabled: bool,
413 trash_enabled: bool,
415 trash_max_size: u64,
418 glob_enabled: bool,
420 pid: u64,
426}
427
428impl Scope {
429 pub fn new() -> Self {
434 Self {
435 frames: Arc::new(vec![HashMap::new()]),
436 exported: HashSet::new(),
437 last_result: Box::new(ExecResult::default()),
438 script_name: String::new(),
439 positional: Vec::new(),
440 error_exit: false,
441 errexit_suppressed: 0,
442 show_ast: false,
443 latch_enabled: false,
444 trash_enabled: false,
445 trash_max_size: 10 * 1024 * 1024, glob_enabled: true,
447 pid: 0,
448 }
449 }
450
451 pub fn pid(&self) -> u64 {
453 self.pid
454 }
455
456 pub fn set_pid(&mut self, pid: u64) {
460 self.pid = pid;
461 }
462
463 pub fn push_frame(&mut self) {
465 Arc::make_mut(&mut self.frames).push(HashMap::new());
466 }
467
468 pub fn pop_frame(&mut self) {
472 if self.frames.len() > 1 {
473 Arc::make_mut(&mut self.frames).pop();
474 } else {
475 panic!("cannot pop the root scope frame");
476 }
477 }
478
479 pub fn set(&mut self, name: impl Into<String>, value: Value) {
483 if let Some(frame) = Arc::make_mut(&mut self.frames).last_mut() {
484 frame.insert(name.into(), value);
485 }
486 }
487
488 pub fn set_global(&mut self, name: impl Into<String>, value: Value) {
494 let name = name.into();
495
496 let frames = Arc::make_mut(&mut self.frames);
498 for frame in frames.iter_mut().rev() {
499 if let std::collections::hash_map::Entry::Occupied(mut e) = frame.entry(name.clone()) {
500 e.insert(value);
501 return;
502 }
503 }
504
505 if let Some(frame) = frames.first_mut() {
507 frame.insert(name, value);
508 }
509 }
510
511 pub fn get(&self, name: &str) -> Option<&Value> {
513 for frame in self.frames.iter().rev() {
514 if let Some(value) = frame.get(name) {
515 return Some(value);
516 }
517 }
518 None
519 }
520
521 pub fn remove(&mut self, name: &str) -> Option<Value> {
525 for frame in Arc::make_mut(&mut self.frames).iter_mut().rev() {
526 if let Some(value) = frame.remove(name) {
527 return Some(value);
528 }
529 }
530 None
531 }
532
533 pub fn set_last_result(&mut self, result: ExecResult) {
535 *self.last_result = result;
537 }
538
539 pub fn last_result(&self) -> &ExecResult {
541 &self.last_result
542 }
543
544 pub fn set_positional(&mut self, script_name: impl Into<String>, args: Vec<String>) {
548 self.script_name = script_name.into();
549 self.positional = args;
550 }
551
552 pub fn save_positional(&self) -> (String, Vec<String>) {
556 (self.script_name.clone(), self.positional.clone())
557 }
558
559 pub fn get_positional(&self, n: usize) -> Option<&str> {
563 if n == 0 {
564 if self.script_name.is_empty() {
565 None
566 } else {
567 Some(&self.script_name)
568 }
569 } else {
570 self.positional.get(n - 1).map(|s| s.as_str())
571 }
572 }
573
574 pub fn all_args(&self) -> &[String] {
576 &self.positional
577 }
578
579 pub fn arg_count(&self) -> usize {
581 self.positional.len()
582 }
583
584 pub fn error_exit_enabled(&self) -> bool {
589 self.error_exit && self.errexit_suppressed == 0
590 }
591
592 pub fn set_error_exit(&mut self, enabled: bool) {
594 self.error_exit = enabled;
595 }
596
597 pub fn suppress_errexit(&mut self) {
599 self.errexit_suppressed += 1;
600 }
601
602 pub fn unsuppress_errexit(&mut self) {
604 self.errexit_suppressed = self.errexit_suppressed.saturating_sub(1);
605 }
606
607 pub fn show_ast(&self) -> bool {
609 self.show_ast
610 }
611
612 pub fn set_show_ast(&mut self, enabled: bool) {
614 self.show_ast = enabled;
615 }
616
617 pub fn latch_enabled(&self) -> bool {
619 self.latch_enabled
620 }
621
622 pub fn set_latch_enabled(&mut self, enabled: bool) {
624 self.latch_enabled = enabled;
625 }
626
627 pub fn trash_enabled(&self) -> bool {
629 self.trash_enabled
630 }
631
632 pub fn set_trash_enabled(&mut self, enabled: bool) {
634 self.trash_enabled = enabled;
635 }
636
637 pub fn trash_max_size(&self) -> u64 {
639 self.trash_max_size
640 }
641
642 pub fn set_trash_max_size(&mut self, size: u64) {
644 self.trash_max_size = size;
645 }
646
647 pub fn glob_enabled(&self) -> bool {
649 self.glob_enabled
650 }
651
652 pub fn set_glob_enabled(&mut self, enabled: bool) {
654 self.glob_enabled = enabled;
655 }
656
657 pub fn export(&mut self, name: impl Into<String>) {
661 self.exported.insert(name.into());
662 }
663
664 pub fn is_exported(&self, name: &str) -> bool {
666 self.exported.contains(name)
667 }
668
669 pub fn set_exported(&mut self, name: impl Into<String>, value: Value) {
676 let name = name.into();
677 self.set(&name, value);
678 self.export(name);
679 }
680
681 pub fn set_exported_global(&mut self, name: impl Into<String>, value: Value) {
687 let name = name.into();
688 self.set_global(&name, value);
689 self.export(name);
690 }
691
692 pub fn unexport(&mut self, name: &str) {
694 self.exported.remove(name);
695 }
696
697 pub fn exported_vars(&self) -> Vec<(String, Value)> {
701 let mut result = Vec::new();
702 for name in &self.exported {
703 if let Some(value) = self.get(name) {
704 result.push((name.clone(), value.clone()));
705 }
706 }
707 result.sort_by(|(a, _), (b, _)| a.cmp(b));
708 result
709 }
710
711 pub fn exported_names(&self) -> Vec<&str> {
713 let mut names: Vec<&str> = self.exported.iter().map(|s| s.as_str()).collect();
714 names.sort();
715 names
716 }
717
718 pub fn resolve_path(&self, path: &VarPath) -> Result<Value, PathError> {
735 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
736 return Err(PathError::UndefinedRoot(String::new()));
738 };
739
740 if root_name == "?" {
742 if path.segments.len() == 1 {
743 return Ok(Value::Int(self.last_result.code));
744 }
745 return Err(PathError::Shape(
746 "$? is the POSIX exit code, not a collection — use `kaish-last` for structured data"
747 .to_string(),
748 ));
749 }
750
751 let root = self
752 .get(root_name)
753 .ok_or_else(|| PathError::UndefinedRoot(root_name.clone()))?;
754
755 let subscripts = &path.segments[1..];
758 if subscripts.is_empty() {
759 return Ok(root.clone());
760 }
761
762 if let Some(VarSegment::Field(name)) = subscripts.first() {
767 return Err(dotted_access_error(root_name, name));
768 }
769
770 let root_json = match root {
774 Value::Json(j) => j,
775 other => {
776 return Err(PathError::Shape(format!(
777 "${{{root_name}…}}: cannot subscript {} — it is not a collection",
778 type_name(other)
779 )))
780 }
781 };
782
783 let mut current = Cow::Borrowed(root_json);
788 let mut prefix = root_name.clone();
789 for seg in subscripts {
790 let step = resolve_step(¤t, seg, self, &prefix)?;
791 current = descend(current, step, &prefix)?;
792 prefix.push_str(&render_segment(seg));
793 }
794 Ok(json_to_value_no_envelope(current.into_owned()))
795 }
796
797 pub fn walk_write(&mut self, path: &VarPath, value: Value) -> Result<(), PathError> {
816 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
817 return Err(PathError::UndefinedRoot(String::new()));
818 };
819
820 let root = self
821 .get(root_name)
822 .ok_or_else(|| PathError::UndefinedRoot(root_name.clone()))?;
823
824 let mut root_json = match root {
825 Value::Json(j) => j.clone(),
826 other => {
827 return Err(PathError::Shape(format!(
828 "${{{root_name}…}}: cannot subscript {} — it is not a collection",
829 type_name(other)
830 )))
831 }
832 };
833
834 let subscripts = &path.segments[1..];
835 let Some((last, intermediates)) = subscripts.split_last() else {
836 return Err(PathError::Shape(format!(
840 "{root_name}: assignment target has no subscript"
841 )));
842 };
843
844 let mut current = &mut root_json;
845 let mut prefix = root_name.clone();
846 for seg in intermediates {
847 let step = resolve_step(current, seg, self, &prefix)?;
848 current = descend_mut(current, step, &prefix)?;
849 prefix.push_str(&render_segment(seg));
850 }
851
852 let step = resolve_step(current, last, self, &prefix)?;
853 apply_leaf_write(current, step, value_to_json(&value), &prefix)?;
854
855 self.set_global(root_name.clone(), Value::Json(root_json));
856 Ok(())
857 }
858
859 pub fn walk_append(&mut self, path: &VarPath, values: Vec<Value>) -> Result<(), String> {
870 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
871 return Err("push: target has no root".to_string());
872 };
873 let root_name = root_name.clone();
874 let current = self
875 .get(&root_name)
876 .ok_or_else(|| format!("push: {root_name} is not defined"))?
877 .clone();
878
879 let subscripts = &path.segments[1..];
880 if subscripts.is_empty() {
881 if !matches!(current, Value::Json(serde_json::Value::Array(_))) {
882 return Err(format!("push: {root_name} is not a list ({})", type_name(¤t)));
883 }
884 let Value::Json(serde_json::Value::Array(mut arr)) = current else {
885 unreachable!("checked above")
886 };
887 arr.extend(values.iter().map(value_to_json));
888 self.set_global(root_name, Value::Json(serde_json::Value::Array(arr)));
889 return Ok(());
890 }
891
892 let mut root_json = match current {
893 Value::Json(j) => j,
894 other => {
895 return Err(format!(
896 "push: {root_name}…: cannot subscript {} — it is not a collection",
897 type_name(&other)
898 ))
899 }
900 };
901
902 let mut cur = &mut root_json;
906 let mut prefix = root_name.clone();
907 for seg in subscripts {
908 let step = resolve_step(cur, seg, self, &prefix)
909 .map_err(|e| push_path_error_message(e, &root_name))?;
910 cur = descend_mut(cur, step, &prefix)
911 .map_err(|e| push_path_error_message(e, &root_name))?;
912 prefix.push_str(&render_segment(seg));
913 }
914
915 let serde_json::Value::Array(arr) = cur else {
916 return Err(format!(
917 "push: {prefix} is not a list ({})",
918 type_name(&json_to_value_no_envelope(cur.clone()))
919 ));
920 };
921 arr.extend(values.iter().map(value_to_json));
922 self.set_global(root_name, Value::Json(root_json));
923 Ok(())
924 }
925
926 pub fn contains(&self, name: &str) -> bool {
928 self.get(name).is_some()
929 }
930
931 pub fn all_names(&self) -> Vec<&str> {
933 let mut names: Vec<&str> = self
934 .frames
935 .iter()
936 .flat_map(|f| f.keys().map(|s| s.as_str()))
937 .collect();
938 names.sort();
939 names.dedup();
940 names
941 }
942
943 pub fn all(&self) -> Vec<(String, Value)> {
947 let mut result = std::collections::HashMap::new();
948 for frame in self.frames.iter() {
950 for (name, value) in frame {
951 result.insert(name.clone(), value.clone());
952 }
953 }
954 let mut pairs: Vec<_> = result.into_iter().collect();
955 pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
956 pairs
957 }
958}
959
960impl Default for Scope {
961 fn default() -> Self {
962 Self::new()
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn new_scope_has_one_frame() {
972 let scope = Scope::new();
973 assert_eq!(scope.frames.len(), 1);
974 }
975
976 #[test]
977 fn set_and_get_variable() {
978 let mut scope = Scope::new();
979 scope.set("X", Value::Int(42));
980 assert_eq!(scope.get("X"), Some(&Value::Int(42)));
981 }
982
983 #[test]
984 fn get_nonexistent_returns_none() {
985 let scope = Scope::new();
986 assert_eq!(scope.get("MISSING"), None);
987 }
988
989 #[test]
990 fn inner_frame_shadows_outer() {
991 let mut scope = Scope::new();
992 scope.set("X", Value::Int(1));
993 scope.push_frame();
994 scope.set("X", Value::Int(2));
995 assert_eq!(scope.get("X"), Some(&Value::Int(2)));
996 scope.pop_frame();
997 assert_eq!(scope.get("X"), Some(&Value::Int(1)));
998 }
999
1000 #[test]
1001 fn inner_frame_can_see_outer_vars() {
1002 let mut scope = Scope::new();
1003 scope.set("OUTER", Value::String("visible".into()));
1004 scope.push_frame();
1005 assert_eq!(scope.get("OUTER"), Some(&Value::String("visible".into())));
1006 }
1007
1008 #[test]
1009 fn resolve_simple_path() {
1010 let mut scope = Scope::new();
1011 scope.set("NAME", Value::String("Alice".into()));
1012
1013 let path = VarPath::simple("NAME");
1014 assert_eq!(
1015 scope.resolve_path(&path),
1016 Ok(Value::String("Alice".into()))
1017 );
1018 }
1019
1020 #[test]
1021 fn resolve_bare_last_result_returns_exit_code() {
1022 let mut scope = Scope::new();
1023 scope.set_last_result(ExecResult::failure(127, "not found"));
1024
1025 let path = VarPath {
1026 segments: vec![VarSegment::Field("?".into())],
1027 };
1028 assert_eq!(scope.resolve_path(&path), Ok(Value::Int(127)));
1029 }
1030
1031 #[test]
1032 fn resolve_last_result_field_access_is_rejected() {
1033 let mut scope = Scope::new();
1037 scope.set_last_result(ExecResult::success_with_data(
1038 "1",
1039 Value::Json(serde_json::json!({"count": 5})),
1040 ));
1041
1042 let path = VarPath {
1043 segments: vec![
1044 VarSegment::Field("?".into()),
1045 VarSegment::Field("data".into()),
1046 ],
1047 };
1048 assert!(matches!(
1049 scope.resolve_path(&path),
1050 Err(PathError::Shape(_))
1051 ));
1052 }
1053
1054 #[test]
1055 fn resolve_dotted_access_on_scalar_is_a_loud_error() {
1056 let mut scope = Scope::new();
1057 scope.set("X", Value::Int(42));
1058
1059 let path = VarPath {
1061 segments: vec![
1062 VarSegment::Field("X".into()),
1063 VarSegment::Field("invalid".into()),
1064 ],
1065 };
1066 assert!(matches!(
1067 scope.resolve_path(&path),
1068 Err(PathError::Shape(_))
1069 ));
1070 }
1071
1072 #[test]
1073 fn resolve_undefined_root_is_soft() {
1074 let scope = Scope::new();
1075 let path = VarPath::simple("NOPE");
1076 assert!(matches!(
1077 scope.resolve_path(&path),
1078 Err(PathError::UndefinedRoot(_))
1079 ));
1080 }
1081
1082 fn subscripted(scope: &mut Scope, root: &str, value: serde_json::Value, seg: VarSegment) -> Result<Value, PathError> {
1089 scope.set(root, Value::Json(value));
1090 let path = VarPath {
1091 segments: vec![VarSegment::Field(root.into()), seg],
1092 };
1093 scope.resolve_path(&path)
1094 }
1095
1096 #[test]
1097 fn out_of_bounds_index_is_absence() {
1098 let mut scope = Scope::new();
1099 let r = subscripted(&mut scope, "xs", serde_json::json!([1, 2]), VarSegment::Index(9));
1100 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1101 }
1102
1103 #[test]
1104 fn missing_record_key_is_absence() {
1105 let mut scope = Scope::new();
1106 let r = subscripted(&mut scope, "u", serde_json::json!({"name": "amy"}), VarSegment::Key("nope".into()));
1107 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1108 }
1109
1110 #[test]
1111 fn string_key_on_a_list_is_shape() {
1112 let mut scope = Scope::new();
1113 let r = subscripted(&mut scope, "xs", serde_json::json!([1, 2]), VarSegment::Key("web".into()));
1114 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1115 }
1116
1117 #[test]
1118 fn integer_index_on_a_record_is_shape() {
1119 let mut scope = Scope::new();
1120 let r = subscripted(&mut scope, "u", serde_json::json!({"name": "amy"}), VarSegment::Index(0));
1121 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1122 }
1123
1124 #[test]
1125 fn subscripting_a_scalar_is_shape() {
1126 let mut scope = Scope::new();
1127 scope.set("s", Value::String("hello".into()));
1128 let path = VarPath {
1129 segments: vec![VarSegment::Field("s".into()), VarSegment::Index(0)],
1130 };
1131 assert!(matches!(scope.resolve_path(&path), Err(PathError::Shape(_))));
1132 }
1133
1134 #[test]
1135 fn unset_dynamic_key_is_undefined_root_not_absence() {
1136 let mut scope = Scope::new();
1139 let r = subscripted(
1140 &mut scope,
1141 "r",
1142 serde_json::json!({"name": "amy"}),
1143 VarSegment::Dynamic("k".into()),
1144 );
1145 assert!(matches!(r, Err(PathError::UndefinedRoot(_))), "got: {r:?}");
1146 }
1147
1148 #[test]
1149 fn contains_finds_variable() {
1150 let mut scope = Scope::new();
1151 scope.set("EXISTS", Value::Bool(true));
1152 assert!(scope.contains("EXISTS"));
1153 assert!(!scope.contains("MISSING"));
1154 }
1155
1156 #[test]
1157 fn all_names_lists_variables() {
1158 let mut scope = Scope::new();
1159 scope.set("A", Value::Int(1));
1160 scope.set("B", Value::Int(2));
1161 scope.push_frame();
1162 scope.set("C", Value::Int(3));
1163
1164 let names = scope.all_names();
1165 assert!(names.contains(&"A"));
1166 assert!(names.contains(&"B"));
1167 assert!(names.contains(&"C"));
1168 }
1169
1170 #[test]
1171 #[should_panic(expected = "cannot pop the root scope frame")]
1172 fn pop_root_frame_panics() {
1173 let mut scope = Scope::new();
1174 scope.pop_frame();
1175 }
1176
1177 #[test]
1178 fn positional_params_basic() {
1179 let mut scope = Scope::new();
1180 scope.set_positional("my_tool", vec!["arg1".into(), "arg2".into(), "arg3".into()]);
1181
1182 assert_eq!(scope.get_positional(0), Some("my_tool"));
1184 assert_eq!(scope.get_positional(1), Some("arg1"));
1186 assert_eq!(scope.get_positional(2), Some("arg2"));
1187 assert_eq!(scope.get_positional(3), Some("arg3"));
1188 assert_eq!(scope.get_positional(4), None);
1190 }
1191
1192 #[test]
1193 fn positional_params_empty() {
1194 let scope = Scope::new();
1195 assert_eq!(scope.get_positional(0), None);
1197 assert_eq!(scope.get_positional(1), None);
1198 assert_eq!(scope.arg_count(), 0);
1199 assert!(scope.all_args().is_empty());
1200 }
1201
1202 #[test]
1203 fn all_args_returns_slice() {
1204 let mut scope = Scope::new();
1205 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1206
1207 let args = scope.all_args();
1208 assert_eq!(args, &["a", "b", "c"]);
1209 }
1210
1211 #[test]
1212 fn arg_count_returns_count() {
1213 let mut scope = Scope::new();
1214 scope.set_positional("test", vec!["one".into(), "two".into()]);
1215
1216 assert_eq!(scope.arg_count(), 2);
1217 }
1218
1219 #[test]
1220 fn export_marks_variable() {
1221 let mut scope = Scope::new();
1222 scope.set("X", Value::Int(42));
1223
1224 assert!(!scope.is_exported("X"));
1225 scope.export("X");
1226 assert!(scope.is_exported("X"));
1227 }
1228
1229 #[test]
1230 fn set_exported_sets_and_exports() {
1231 let mut scope = Scope::new();
1232 scope.set_exported("PATH", Value::String("/usr/bin".into()));
1233
1234 assert!(scope.is_exported("PATH"));
1235 assert_eq!(scope.get("PATH"), Some(&Value::String("/usr/bin".into())));
1236 }
1237
1238 #[test]
1239 fn unexport_removes_export_marker() {
1240 let mut scope = Scope::new();
1241 scope.set_exported("VAR", Value::Int(1));
1242 assert!(scope.is_exported("VAR"));
1243
1244 scope.unexport("VAR");
1245 assert!(!scope.is_exported("VAR"));
1246 assert!(scope.get("VAR").is_some());
1248 }
1249
1250 #[test]
1251 fn exported_vars_returns_only_exported_with_values() {
1252 let mut scope = Scope::new();
1253 scope.set_exported("A", Value::Int(1));
1254 scope.set_exported("B", Value::Int(2));
1255 scope.set("C", Value::Int(3)); scope.export("D"); let exported = scope.exported_vars();
1259 assert_eq!(exported.len(), 2);
1260 assert_eq!(exported[0], ("A".to_string(), Value::Int(1)));
1261 assert_eq!(exported[1], ("B".to_string(), Value::Int(2)));
1262 }
1263
1264 #[test]
1265 fn exported_names_returns_sorted_names() {
1266 let mut scope = Scope::new();
1267 scope.export("Z");
1268 scope.export("A");
1269 scope.export("M");
1270
1271 let names = scope.exported_names();
1272 assert_eq!(names, vec!["A", "M", "Z"]);
1273 }
1274
1275 fn write_at(
1279 scope: &mut Scope,
1280 root: &str,
1281 segs: Vec<VarSegment>,
1282 ) -> Result<(), PathError> {
1283 let mut segments = vec![VarSegment::Field(root.into())];
1284 segments.extend(segs);
1285 scope.walk_write(&VarPath { segments }, Value::Int(0))
1286 }
1287
1288 #[test]
1289 fn walk_write_list_index_update() {
1290 let mut scope = Scope::new();
1291 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1292 let path = VarPath {
1293 segments: vec![VarSegment::Field("xs".into()), VarSegment::Index(0)],
1294 };
1295 scope.walk_write(&path, Value::Int(9)).expect("write should succeed");
1296 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!([9, 2, 3]))));
1297 }
1298
1299 #[test]
1300 fn walk_write_negative_index() {
1301 let mut scope = Scope::new();
1302 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1303 let path = VarPath {
1304 segments: vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)],
1305 };
1306 scope.walk_write(&path, Value::Int(7)).expect("write should succeed");
1307 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!([1, 2, 7]))));
1308 }
1309
1310 #[test]
1311 fn walk_write_inserts_a_new_record_key() {
1312 let mut scope = Scope::new();
1313 scope.set("u", Value::Json(serde_json::json!({"port": 8080})));
1314 let path = VarPath {
1315 segments: vec![VarSegment::Field("u".into()), VarSegment::Key("host".into())],
1316 };
1317 scope
1318 .walk_write(&path, Value::String("localhost".into()))
1319 .expect("write should succeed");
1320 assert_eq!(
1321 scope.get("u"),
1322 Some(&Value::Json(serde_json::json!({"port": 8080, "host": "localhost"})))
1323 );
1324 }
1325
1326 #[test]
1327 fn walk_write_deep_path_updates_nested_key() {
1328 let mut scope = Scope::new();
1329 scope.set("s", Value::Json(serde_json::json!({"web": {"port": 8080}})));
1330 let path = VarPath {
1331 segments: vec![
1332 VarSegment::Field("s".into()),
1333 VarSegment::Key("web".into()),
1334 VarSegment::Key("port".into()),
1335 ],
1336 };
1337 scope.walk_write(&path, Value::Int(9000)).expect("write should succeed");
1338 assert_eq!(
1339 scope.get("s"),
1340 Some(&Value::Json(serde_json::json!({"web": {"port": 9000}})))
1341 );
1342 }
1343
1344 #[test]
1345 fn walk_write_out_of_bounds_index_is_absence() {
1346 let mut scope = Scope::new();
1347 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1348 let r = write_at(&mut scope, "xs", vec![VarSegment::Index(9)]);
1349 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1350 }
1351
1352 #[test]
1353 fn walk_write_missing_intermediate_is_absence_no_autoviv() {
1354 let mut scope = Scope::new();
1355 scope.set("s", Value::Json(serde_json::json!({"web": {"port": 8080}})));
1356 let r = write_at(
1357 &mut scope,
1358 "s",
1359 vec![VarSegment::Key("api".into()), VarSegment::Key("port".into())],
1360 );
1361 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1362 assert_eq!(
1364 scope.get("s"),
1365 Some(&Value::Json(serde_json::json!({"web": {"port": 8080}})))
1366 );
1367 }
1368
1369 #[test]
1370 fn walk_write_scalar_root_is_shape() {
1371 let mut scope = Scope::new();
1372 scope.set("y", Value::String("hi".into()));
1373 let r = write_at(&mut scope, "y", vec![VarSegment::Index(0)]);
1374 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1375 }
1376
1377 #[test]
1378 fn walk_write_undefined_root_is_undefined_root() {
1379 let mut scope = Scope::new();
1380 let r = write_at(&mut scope, "z", vec![VarSegment::Index(0)]);
1381 assert!(matches!(r, Err(PathError::UndefinedRoot(_))), "got: {r:?}");
1382 }
1383
1384 #[test]
1385 fn walk_write_slice_lvalue_is_shape() {
1386 let mut scope = Scope::new();
1387 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1388 let r = write_at(&mut scope, "xs", vec![VarSegment::Slice(Some(0), Some(2))]);
1389 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1390 }
1391
1392 #[test]
1395 fn walk_append_extends_a_list_in_place() {
1396 let mut scope = Scope::new();
1397 scope.set("xs", Value::Json(serde_json::json!(["a", "b"])));
1398 scope
1399 .walk_append(&VarPath::simple("xs"), vec![Value::String("c".into())])
1400 .expect("push should succeed");
1401 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!(["a", "b", "c"]))));
1402 }
1403
1404 #[test]
1405 fn walk_append_undefined_target_is_a_loud_error() {
1406 let mut scope = Scope::new();
1407 let r = scope.walk_append(&VarPath::simple("nope"), vec![Value::Int(1)]);
1408 assert!(r.is_err(), "expected a loud error for an undefined target");
1409 }
1410
1411 #[test]
1412 fn walk_append_non_list_target_is_a_loud_error() {
1413 let mut scope = Scope::new();
1414 scope.set("y", Value::String("hi".into()));
1415 let r = scope.walk_append(&VarPath::simple("y"), vec![Value::Int(1)]);
1416 assert!(r.is_err(), "expected a loud error for a non-list target");
1417 }
1418
1419 #[test]
1420 fn walk_append_bracket_path_extends_a_nested_list_in_place() {
1421 let mut scope = Scope::new();
1422 scope.set(
1423 "services",
1424 Value::Json(serde_json::json!({"web": {"tags": ["a"]}})),
1425 );
1426 let path = VarPath {
1427 segments: vec![
1428 VarSegment::Field("services".into()),
1429 VarSegment::Key("web".into()),
1430 VarSegment::Key("tags".into()),
1431 ],
1432 };
1433 scope
1434 .walk_append(&path, vec![Value::String("b".into())])
1435 .expect("bracket-path push should succeed");
1436 assert_eq!(
1437 scope.get("services"),
1438 Some(&Value::Json(serde_json::json!({"web": {"tags": ["a", "b"]}})))
1439 );
1440 }
1441
1442 #[test]
1443 fn walk_append_bracket_path_missing_intermediate_is_a_loud_error() {
1444 let mut scope = Scope::new();
1445 scope.set("services", Value::Json(serde_json::json!({})));
1446 let path = VarPath {
1447 segments: vec![
1448 VarSegment::Field("services".into()),
1449 VarSegment::Key("web".into()),
1450 VarSegment::Key("tags".into()),
1451 ],
1452 };
1453 let r = scope.walk_append(&path, vec![Value::String("x".into())]);
1454 assert!(r.is_err(), "expected a loud error for a missing intermediate");
1455 }
1456
1457 #[test]
1458 fn walk_append_bracket_path_non_list_leaf_is_a_loud_error() {
1459 let mut scope = Scope::new();
1460 scope.set(
1461 "services",
1462 Value::Json(serde_json::json!({"web": {"port": 8080}})),
1463 );
1464 let path = VarPath {
1465 segments: vec![
1466 VarSegment::Field("services".into()),
1467 VarSegment::Key("web".into()),
1468 VarSegment::Key("port".into()),
1469 ],
1470 };
1471 let r = scope.walk_append(&path, vec![Value::Int(1)]);
1472 assert!(r.is_err(), "expected a loud error for a non-list leaf");
1473 }
1474}