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)]
39#[non_exhaustive]
40pub enum PathError {
41 UndefinedRoot(String),
43 Absence(String),
45 Shape(String),
47}
48
49fn type_name(value: &Value) -> &'static str {
51 match value {
52 Value::Null => "null",
53 Value::Bool(_) => "a boolean",
54 Value::Int(_) => "an integer",
55 Value::Float(_) => "a float",
56 Value::String(_) => "a string",
57 Value::Json(serde_json::Value::Array(_)) => "a list",
58 Value::Json(serde_json::Value::Object(_)) => "a record",
59 Value::Json(_) => "a scalar",
60 Value::Bytes(_) => "binary data",
61 }
62}
63
64fn value_as_index(value: &Value) -> Option<i64> {
67 match value {
68 Value::Int(i) => Some(*i),
69 Value::String(s) => s.parse::<i64>().ok(),
70 _ => None,
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
81enum Step {
82 Index(usize),
84 Key(String),
86 Slice(usize, usize),
88}
89
90fn classify_index(json: &serde_json::Value, i: i64, path: &str) -> Result<Step, PathError> {
96 let arr = match json {
97 serde_json::Value::Array(a) => a,
98 serde_json::Value::Object(_) => {
99 return Err(PathError::Shape(format!(
100 "${{{path}[{i}]}}: integer index on a record — record keys are strings, use ${{{path}[\"{i}\"]}}"
101 )))
102 }
103 _ => unreachable!("resolve_step guards non-collection containers"),
104 };
105 let len = arr.len() as i64;
106 let idx = if i < 0 { len + i } else { i };
107 if idx < 0 || idx >= len {
108 return Err(PathError::Absence(format!(
109 "${{{path}[{i}]}}: index out of bounds (list length {len})"
110 )));
111 }
112 Ok(Step::Index(idx as usize))
113}
114
115fn classify_key(json: &serde_json::Value, key: &str, path: &str) -> Result<Step, PathError> {
119 match json {
120 serde_json::Value::Object(_) => Ok(Step::Key(key.to_string())),
121 serde_json::Value::Array(_) => Err(PathError::Shape(format!(
122 "${{{path}[{key}]}}: string key on a list — use an integer index"
123 ))),
124 _ => unreachable!("resolve_step guards non-collection containers"),
125 }
126}
127
128fn classify_slice(
132 json: &serde_json::Value,
133 start: Option<i64>,
134 end: Option<i64>,
135 path: &str,
136) -> Result<Step, PathError> {
137 let len = match json {
140 serde_json::Value::Array(a) => a.len() as i64,
141 serde_json::Value::String(s) => s.chars().count() as i64,
142 serde_json::Value::Object(_) => {
143 return Err(PathError::Shape(format!(
144 "${{{path}[..]}}: cannot slice a record"
145 )))
146 }
147 _ => unreachable!("resolve_step guards non-sliceable containers"),
148 };
149 let norm = |b: i64| -> i64 {
150 let b = if b < 0 { len + b } else { b };
151 b.clamp(0, len)
152 };
153 let s = start.map(norm).unwrap_or(0);
154 let e = end.map(norm).unwrap_or(len);
155 let (s, e) = if s >= e {
156 (s as usize, s as usize)
157 } else {
158 (s as usize, e as usize)
159 };
160 Ok(Step::Slice(s, e))
161}
162
163fn dotted_access_error(path: &str, field: &str) -> PathError {
167 PathError::Shape(format!(
168 "${{{path}…}}: kaish uses bracket access, not dots — write the key as a subscript: [{field}]"
169 ))
170}
171
172fn render_segment(seg: &VarSegment) -> String {
176 match seg {
177 VarSegment::Index(i) => format!("[{i}]"),
178 VarSegment::Key(k) => format!("[{k}]"),
179 VarSegment::Dynamic(v) => format!("[${v}]"),
180 VarSegment::Slice(a, b) => format!(
181 "[{}:{}]",
182 a.map(|n| n.to_string()).unwrap_or_default(),
183 b.map(|n| n.to_string()).unwrap_or_default()
184 ),
185 VarSegment::Field(f) => format!(".{f}"),
186 }
187}
188
189fn resolve_step(
195 container: &serde_json::Value,
196 seg: &VarSegment,
197 scope: &Scope,
198 path: &str,
199) -> Result<Step, PathError> {
200 if let VarSegment::Field(name) = seg {
204 return Err(dotted_access_error(path, name));
205 }
206
207 if matches!(container, serde_json::Value::String(_)) {
212 return match seg {
213 VarSegment::Slice(start, end) => classify_slice(container, *start, *end, path),
214 _ => Err(PathError::Shape(format!(
215 "${{{path}…}}: cannot subscript a string — slice it instead, \
216 e.g. ${{{path}[0:5]}} for the first five characters"
217 ))),
218 };
219 }
220
221 if !matches!(
223 container,
224 serde_json::Value::Array(_) | serde_json::Value::Object(_)
225 ) {
226 return Err(PathError::Shape(format!(
227 "${{{path}…}}: cannot subscript {} — it is not a collection",
228 type_name(&json_to_value_no_envelope(container.clone()))
229 )));
230 }
231
232 match seg {
233 VarSegment::Index(i) => classify_index(container, *i, path),
234 VarSegment::Key(k) => classify_key(container, k, path),
235 VarSegment::Slice(start, end) => classify_slice(container, *start, *end, path),
236 VarSegment::Dynamic(var) => {
237 let key_val = scope.get(var).ok_or_else(|| {
241 PathError::UndefinedRoot(format!("${{{path}[${var}]}}: ${var} is not set"))
242 })?;
243 match container {
244 serde_json::Value::Array(_) => {
245 let idx = value_as_index(key_val).ok_or_else(|| {
246 PathError::Shape(format!(
247 "${{{path}[${var}]}}: a list index must be an integer, got \"{}\"",
248 value_to_string(key_val)
249 ))
250 })?;
251 classify_index(container, idx, path)
252 }
253 serde_json::Value::Object(_) => Ok(Step::Key(value_to_string(key_val))),
254 _ => unreachable!("non-collection container guarded above"),
255 }
256 }
257 VarSegment::Field(_) => unreachable!("dotted segment handled above"),
258 }
259}
260
261fn descend<'a>(
267 current: Cow<'a, serde_json::Value>,
268 step: Step,
269 path: &str,
270) -> Result<Cow<'a, serde_json::Value>, PathError> {
271 match step {
272 Step::Slice(s, e) => match current.as_ref() {
273 serde_json::Value::Array(arr) => {
274 Ok(Cow::Owned(serde_json::Value::Array(arr[s..e].to_vec())))
275 }
276 serde_json::Value::String(text) => Ok(Cow::Owned(serde_json::Value::String(
278 text.chars().skip(s).take(e - s).collect(),
279 ))),
280 _ => unreachable!("slice classified against an array or string"),
281 },
282 Step::Index(i) => match current {
283 Cow::Borrowed(j) => {
284 let Some(arr) = j.as_array() else {
285 unreachable!("index classified against an array")
286 };
287 Ok(Cow::Borrowed(&arr[i]))
288 }
289 Cow::Owned(j) => {
290 let Some(arr) = j.as_array() else {
291 unreachable!("index classified against an array")
292 };
293 Ok(Cow::Owned(arr[i].clone()))
294 }
295 },
296 Step::Key(k) => match current {
297 Cow::Borrowed(j) => match j.as_object().and_then(|m| m.get(&k)) {
298 Some(child) => Ok(Cow::Borrowed(child)),
299 None => Err(PathError::Absence(format!("${{{path}[{k}]}}: no such key"))),
300 },
301 Cow::Owned(j) => match j.as_object().and_then(|m| m.get(&k)) {
302 Some(child) => Ok(Cow::Owned(child.clone())),
303 None => Err(PathError::Absence(format!("${{{path}[{k}]}}: no such key"))),
304 },
305 },
306 }
307}
308
309fn descend_mut<'a>(
319 current: &'a mut serde_json::Value,
320 step: Step,
321 path: &str,
322) -> Result<&'a mut serde_json::Value, PathError> {
323 match step {
324 Step::Slice(..) => Err(PathError::Shape(format!(
325 "${{{path}[..]}}: slice lvalues are not supported — index or key paths only"
326 ))),
327 Step::Index(i) => {
328 let Some(arr) = current.as_array_mut() else {
329 unreachable!("index classified against an array")
330 };
331 Ok(&mut arr[i])
332 }
333 Step::Key(k) => {
334 let Some(map) = current.as_object_mut() else {
335 unreachable!("key classified against an object")
336 };
337 match map.get_mut(&k) {
338 Some(child) => Ok(child),
339 None => Err(PathError::Absence(format!(
340 "${{{path}[{k}]}}: no such key — no autovivification, create it first (e.g. `{path}[{k}]={{}}`)"
341 ))),
342 }
343 }
344 }
345}
346
347fn apply_leaf_write(
354 current: &mut serde_json::Value,
355 step: Step,
356 value: serde_json::Value,
357 path: &str,
358) -> Result<(), PathError> {
359 match step {
360 Step::Slice(..) => Err(PathError::Shape(format!(
361 "${{{path}[..]}}: slice lvalues are not supported — index or key paths only"
362 ))),
363 Step::Index(i) => {
364 let Some(arr) = current.as_array_mut() else {
365 unreachable!("index classified against an array")
366 };
367 arr[i] = value;
368 Ok(())
369 }
370 Step::Key(k) => {
371 let Some(map) = current.as_object_mut() else {
372 unreachable!("key classified against an object")
373 };
374 map.insert(k, value);
375 Ok(())
376 }
377 }
378}
379
380fn push_path_error_message(err: PathError, root_name: &str) -> String {
387 match err {
388 PathError::UndefinedRoot(msg) if msg.is_empty() => {
389 format!("push: {root_name} is not defined")
390 }
391 PathError::UndefinedRoot(msg) => format!("push: {msg}"),
392 PathError::Absence(msg) | PathError::Shape(msg) => msg,
393 }
394}
395
396#[derive(Debug, Clone)]
407pub struct Scope {
408 frames: Arc<Vec<HashMap<String, Value>>>,
411 exported: HashSet<String>,
413 last_result: Box<ExecResult>,
421 last_cmdsubst_code: Option<i64>,
428 script_name: String,
430 positional: Vec<String>,
432 error_exit: bool,
434 errexit_suppressed: usize,
437 show_ast: bool,
439 trash_enabled: bool,
441 trash_max_size: u64,
444 glob_enabled: bool,
446 pipefail_enabled: bool,
449 pid: u64,
455}
456
457impl Scope {
458 pub fn new() -> Self {
463 Self {
464 frames: Arc::new(vec![HashMap::new()]),
465 exported: HashSet::new(),
466 last_result: Box::new(ExecResult::default()),
467 last_cmdsubst_code: None,
468 script_name: String::new(),
469 positional: Vec::new(),
470 error_exit: false,
471 errexit_suppressed: 0,
472 show_ast: false,
473 trash_enabled: false,
474 trash_max_size: 10 * 1024 * 1024, glob_enabled: true,
476 pipefail_enabled: false,
477 pid: 0,
478 }
479 }
480
481 pub fn pid(&self) -> u64 {
483 self.pid
484 }
485
486 pub fn set_pid(&mut self, pid: u64) {
490 self.pid = pid;
491 }
492
493 pub fn push_frame(&mut self) {
495 Arc::make_mut(&mut self.frames).push(HashMap::new());
496 }
497
498 pub fn pop_frame(&mut self) {
502 if self.frames.len() > 1 {
503 Arc::make_mut(&mut self.frames).pop();
504 } else {
505 panic!("cannot pop the root scope frame");
506 }
507 }
508
509 pub fn set(&mut self, name: impl Into<String>, value: Value) {
519 let name = crate::ast::normalize_name(name.into());
520 if let Some(frame) = Arc::make_mut(&mut self.frames).last_mut() {
521 frame.insert(name, value);
522 }
523 }
524
525 pub fn set_global(&mut self, name: impl Into<String>, value: Value) {
531 let name = crate::ast::normalize_name(name.into());
532
533 let frames = Arc::make_mut(&mut self.frames);
535 for frame in frames.iter_mut().rev() {
536 if let std::collections::hash_map::Entry::Occupied(mut e) = frame.entry(name.clone()) {
537 e.insert(value);
538 return;
539 }
540 }
541
542 if let Some(frame) = frames.first_mut() {
544 frame.insert(name, value);
545 }
546 }
547
548 pub fn get(&self, name: &str) -> Option<&Value> {
550 let normalized;
551 let name = if name.is_ascii() {
552 name
553 } else {
554 normalized = crate::ast::normalize_name(name.to_string());
555 normalized.as_str()
556 };
557 for frame in self.frames.iter().rev() {
558 if let Some(value) = frame.get(name) {
559 return Some(value);
560 }
561 }
562 None
563 }
564
565 pub fn remove(&mut self, name: &str) -> Option<Value> {
569 let normalized;
570 let name = if name.is_ascii() {
571 name
572 } else {
573 normalized = crate::ast::normalize_name(name.to_string());
574 normalized.as_str()
575 };
576 for frame in Arc::make_mut(&mut self.frames).iter_mut().rev() {
577 if let Some(value) = frame.remove(name) {
578 return Some(value);
579 }
580 }
581 None
582 }
583
584 pub fn set_last_result(&mut self, result: ExecResult) {
586 *self.last_result = result;
588 }
589
590 pub fn last_result(&self) -> &ExecResult {
592 &self.last_result
593 }
594
595 pub fn note_cmdsubst_code(&mut self, code: i64) {
599 self.last_cmdsubst_code = Some(code);
600 }
601
602 pub fn clear_cmdsubst_code(&mut self) {
606 self.last_cmdsubst_code = None;
607 }
608
609 pub fn take_cmdsubst_code(&mut self) -> Option<i64> {
611 self.last_cmdsubst_code.take()
612 }
613
614 pub fn set_positional(&mut self, script_name: impl Into<String>, args: Vec<String>) {
618 self.script_name = script_name.into();
619 self.positional = args;
620 }
621
622 pub fn save_positional(&self) -> (String, Vec<String>) {
626 (self.script_name.clone(), self.positional.clone())
627 }
628
629 pub fn get_positional(&self, n: usize) -> Option<&str> {
633 if n == 0 {
634 if self.script_name.is_empty() {
635 None
636 } else {
637 Some(&self.script_name)
638 }
639 } else {
640 self.positional.get(n - 1).map(|s| s.as_str())
641 }
642 }
643
644 pub fn all_args(&self) -> &[String] {
646 &self.positional
647 }
648
649 pub fn arg_count(&self) -> usize {
651 self.positional.len()
652 }
653
654 pub fn error_exit_enabled(&self) -> bool {
659 self.error_exit && self.errexit_suppressed == 0
660 }
661
662 pub fn error_exit_flag(&self) -> bool {
670 self.error_exit
671 }
672
673 pub fn set_error_exit(&mut self, enabled: bool) {
675 self.error_exit = enabled;
676 }
677
678 pub fn suppress_errexit(&mut self) {
680 self.errexit_suppressed += 1;
681 }
682
683 pub fn unsuppress_errexit(&mut self) {
685 self.errexit_suppressed = self.errexit_suppressed.saturating_sub(1);
686 }
687
688 pub fn show_ast(&self) -> bool {
690 self.show_ast
691 }
692
693 pub fn set_show_ast(&mut self, enabled: bool) {
695 self.show_ast = enabled;
696 }
697
698 pub fn pipefail_enabled(&self) -> bool {
700 self.pipefail_enabled
701 }
702
703 pub fn set_pipefail_enabled(&mut self, enabled: bool) {
705 self.pipefail_enabled = enabled;
706 }
707
708 pub fn set_pipestatus(&mut self, codes: &[i64]) {
716 let list = serde_json::Value::Array(
717 codes.iter().map(|c| serde_json::Value::from(*c)).collect(),
718 );
719 self.set_global("PIPESTATUS", Value::Json(list));
720 }
721
722 pub fn pipestatus_rightmost_failure(&self) -> Option<i64> {
730 let Some(Value::Json(serde_json::Value::Array(codes))) = self.get("PIPESTATUS") else {
731 return None;
732 };
733 codes
734 .iter()
735 .filter_map(serde_json::Value::as_i64)
736 .rfind(|c| *c != 0)
737 }
738
739 pub fn trash_enabled(&self) -> bool {
741 self.trash_enabled
742 }
743
744 pub fn set_trash_enabled(&mut self, enabled: bool) {
746 self.trash_enabled = enabled;
747 }
748
749 pub fn trash_max_size(&self) -> u64 {
751 self.trash_max_size
752 }
753
754 pub fn set_trash_max_size(&mut self, size: u64) {
756 self.trash_max_size = size;
757 }
758
759 pub fn glob_enabled(&self) -> bool {
761 self.glob_enabled
762 }
763
764 pub fn set_glob_enabled(&mut self, enabled: bool) {
766 self.glob_enabled = enabled;
767 }
768
769 pub fn export(&mut self, name: impl Into<String>) {
773 self.exported.insert(name.into());
774 }
775
776 pub fn is_exported(&self, name: &str) -> bool {
778 self.exported.contains(name)
779 }
780
781 pub fn set_exported(&mut self, name: impl Into<String>, value: Value) {
788 let name = name.into();
789 self.set(&name, value);
790 self.export(name);
791 }
792
793 pub fn set_exported_global(&mut self, name: impl Into<String>, value: Value) {
799 let name = name.into();
800 self.set_global(&name, value);
801 self.export(name);
802 }
803
804 pub fn unexport(&mut self, name: &str) {
806 self.exported.remove(name);
807 }
808
809 pub fn exported_vars(&self) -> Vec<(String, Value)> {
813 let mut result = Vec::new();
814 for name in &self.exported {
815 if let Some(value) = self.get(name) {
816 result.push((name.clone(), value.clone()));
817 }
818 }
819 result.sort_by(|(a, _), (b, _)| a.cmp(b));
820 result
821 }
822
823 pub fn exported_names(&self) -> Vec<&str> {
825 let mut names: Vec<&str> = self.exported.iter().map(|s| s.as_str()).collect();
826 names.sort();
827 names
828 }
829
830 pub fn resolve_path(&self, path: &VarPath) -> Result<Value, PathError> {
847 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
848 return Err(PathError::UndefinedRoot(String::new()));
850 };
851
852 if root_name == "?" {
854 if path.segments.len() == 1 {
855 return Ok(Value::Int(self.last_result.code));
856 }
857 return Err(PathError::Shape(
858 "$? is the POSIX exit code, not a collection — use `kaish-last` for structured data"
859 .to_string(),
860 ));
861 }
862
863 let root = self
864 .get(root_name)
865 .ok_or_else(|| PathError::UndefinedRoot(root_name.clone()))?;
866
867 let subscripts = &path.segments[1..];
870 if subscripts.is_empty() {
871 return Ok(root.clone());
872 }
873
874 if let Some(VarSegment::Field(name)) = subscripts.first() {
879 return Err(dotted_access_error(root_name, name));
880 }
881
882 let lifted;
889 let root_json = match root {
890 Value::Json(j) => j,
891 Value::String(s) => {
892 lifted = serde_json::Value::String(s.clone());
893 &lifted
894 }
895 other => {
896 return Err(PathError::Shape(format!(
897 "${{{root_name}…}}: cannot subscript {} — it is not a collection",
898 type_name(other)
899 )))
900 }
901 };
902
903 let mut current = Cow::Borrowed(root_json);
908 let mut prefix = root_name.clone();
909 for seg in subscripts {
910 let step = resolve_step(¤t, seg, self, &prefix)?;
911 current = descend(current, step, &prefix)?;
912 prefix.push_str(&render_segment(seg));
913 }
914 Ok(json_to_value_no_envelope(current.into_owned()))
915 }
916
917 pub fn walk_write(&mut self, path: &VarPath, value: Value) -> Result<(), PathError> {
937 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
938 return Err(PathError::UndefinedRoot(String::new()));
939 };
940
941 let root = self
942 .get(root_name)
943 .ok_or_else(|| PathError::UndefinedRoot(root_name.clone()))?;
944
945 let mut root_json = match root {
946 Value::Json(j) => j.clone(),
947 other => {
948 return Err(PathError::Shape(format!(
949 "${{{root_name}…}}: cannot subscript {} — it is not a collection",
950 type_name(other)
951 )))
952 }
953 };
954
955 let subscripts = &path.segments[1..];
956 let Some((last, intermediates)) = subscripts.split_last() else {
957 return Err(PathError::Shape(format!(
961 "{root_name}: assignment target has no subscript"
962 )));
963 };
964
965 let mut current = &mut root_json;
966 let mut prefix = root_name.clone();
967 for seg in intermediates {
968 let step = resolve_step(current, seg, self, &prefix)?;
969 current = descend_mut(current, step, &prefix)?;
970 prefix.push_str(&render_segment(seg));
971 }
972
973 let step = resolve_step(current, last, self, &prefix)?;
974 apply_leaf_write(current, step, value_to_json(&value), &prefix)?;
975
976 self.set_global(root_name.clone(), Value::Json(root_json));
977 Ok(())
978 }
979
980 pub fn walk_append(&mut self, path: &VarPath, values: Vec<Value>) -> Result<(), String> {
992 let Some(VarSegment::Field(root_name)) = path.segments.first() else {
993 return Err("push: target has no root".to_string());
994 };
995 let root_name = root_name.clone();
996 let current = self
997 .get(&root_name)
998 .ok_or_else(|| format!("push: {root_name} is not defined"))?
999 .clone();
1000
1001 let subscripts = &path.segments[1..];
1002 if subscripts.is_empty() {
1003 if !matches!(current, Value::Json(serde_json::Value::Array(_))) {
1004 return Err(format!("push: {root_name} is not a list ({})", type_name(¤t)));
1005 }
1006 let Value::Json(serde_json::Value::Array(mut arr)) = current else {
1007 unreachable!("checked above")
1008 };
1009 arr.extend(values.iter().map(value_to_json));
1010 self.set_global(root_name, Value::Json(serde_json::Value::Array(arr)));
1011 return Ok(());
1012 }
1013
1014 let mut root_json = match current {
1015 Value::Json(j) => j,
1016 other => {
1017 return Err(format!(
1018 "push: {root_name}…: cannot subscript {} — it is not a collection",
1019 type_name(&other)
1020 ))
1021 }
1022 };
1023
1024 let mut cur = &mut root_json;
1028 let mut prefix = root_name.clone();
1029 for seg in subscripts {
1030 let step = resolve_step(cur, seg, self, &prefix)
1031 .map_err(|e| push_path_error_message(e, &root_name))?;
1032 cur = descend_mut(cur, step, &prefix)
1033 .map_err(|e| push_path_error_message(e, &root_name))?;
1034 prefix.push_str(&render_segment(seg));
1035 }
1036
1037 let serde_json::Value::Array(arr) = cur else {
1038 return Err(format!(
1039 "push: {prefix} is not a list ({})",
1040 type_name(&json_to_value_no_envelope(cur.clone()))
1041 ));
1042 };
1043 arr.extend(values.iter().map(value_to_json));
1044 self.set_global(root_name, Value::Json(root_json));
1045 Ok(())
1046 }
1047
1048 pub fn contains(&self, name: &str) -> bool {
1050 self.get(name).is_some()
1051 }
1052
1053 pub fn all_names(&self) -> Vec<&str> {
1055 let mut names: Vec<&str> = self
1056 .frames
1057 .iter()
1058 .flat_map(|f| f.keys().map(|s| s.as_str()))
1059 .collect();
1060 names.sort();
1061 names.dedup();
1062 names
1063 }
1064
1065 pub fn all(&self) -> Vec<(String, Value)> {
1069 let mut result = std::collections::HashMap::new();
1070 for frame in self.frames.iter() {
1072 for (name, value) in frame {
1073 result.insert(name.clone(), value.clone());
1074 }
1075 }
1076 let mut pairs: Vec<_> = result.into_iter().collect();
1077 pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
1078 pairs
1079 }
1080}
1081
1082impl Default for Scope {
1083 fn default() -> Self {
1084 Self::new()
1085 }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use super::*;
1091
1092 #[test]
1093 fn new_scope_has_one_frame() {
1094 let scope = Scope::new();
1095 assert_eq!(scope.frames.len(), 1);
1096 }
1097
1098 #[test]
1099 fn set_and_get_variable() {
1100 let mut scope = Scope::new();
1101 scope.set("X", Value::Int(42));
1102 assert_eq!(scope.get("X"), Some(&Value::Int(42)));
1103 }
1104
1105 #[test]
1106 fn get_nonexistent_returns_none() {
1107 let scope = Scope::new();
1108 assert_eq!(scope.get("MISSING"), None);
1109 }
1110
1111 #[test]
1112 fn inner_frame_shadows_outer() {
1113 let mut scope = Scope::new();
1114 scope.set("X", Value::Int(1));
1115 scope.push_frame();
1116 scope.set("X", Value::Int(2));
1117 assert_eq!(scope.get("X"), Some(&Value::Int(2)));
1118 scope.pop_frame();
1119 assert_eq!(scope.get("X"), Some(&Value::Int(1)));
1120 }
1121
1122 #[test]
1123 fn inner_frame_can_see_outer_vars() {
1124 let mut scope = Scope::new();
1125 scope.set("OUTER", Value::String("visible".into()));
1126 scope.push_frame();
1127 assert_eq!(scope.get("OUTER"), Some(&Value::String("visible".into())));
1128 }
1129
1130 #[test]
1131 fn resolve_simple_path() {
1132 let mut scope = Scope::new();
1133 scope.set("NAME", Value::String("Alice".into()));
1134
1135 let path = VarPath::simple("NAME");
1136 assert_eq!(
1137 scope.resolve_path(&path),
1138 Ok(Value::String("Alice".into()))
1139 );
1140 }
1141
1142 #[test]
1143 fn resolve_bare_last_result_returns_exit_code() {
1144 let mut scope = Scope::new();
1145 scope.set_last_result(ExecResult::failure(127, "not found"));
1146
1147 let path = VarPath {
1148 segments: vec![VarSegment::Field("?".into())],
1149 };
1150 assert_eq!(scope.resolve_path(&path), Ok(Value::Int(127)));
1151 }
1152
1153 #[test]
1154 fn resolve_last_result_field_access_is_rejected() {
1155 let mut scope = Scope::new();
1159 scope.set_last_result(ExecResult::success_with_data(
1160 "1",
1161 Value::Json(serde_json::json!({"count": 5})),
1162 ));
1163
1164 let path = VarPath {
1165 segments: vec![
1166 VarSegment::Field("?".into()),
1167 VarSegment::Field("data".into()),
1168 ],
1169 };
1170 assert!(matches!(
1171 scope.resolve_path(&path),
1172 Err(PathError::Shape(_))
1173 ));
1174 }
1175
1176 #[test]
1177 fn resolve_dotted_access_on_scalar_is_a_loud_error() {
1178 let mut scope = Scope::new();
1179 scope.set("X", Value::Int(42));
1180
1181 let path = VarPath {
1183 segments: vec![
1184 VarSegment::Field("X".into()),
1185 VarSegment::Field("invalid".into()),
1186 ],
1187 };
1188 assert!(matches!(
1189 scope.resolve_path(&path),
1190 Err(PathError::Shape(_))
1191 ));
1192 }
1193
1194 #[test]
1195 fn resolve_undefined_root_is_soft() {
1196 let scope = Scope::new();
1197 let path = VarPath::simple("NOPE");
1198 assert!(matches!(
1199 scope.resolve_path(&path),
1200 Err(PathError::UndefinedRoot(_))
1201 ));
1202 }
1203
1204 fn subscripted(scope: &mut Scope, root: &str, value: serde_json::Value, seg: VarSegment) -> Result<Value, PathError> {
1211 scope.set(root, Value::Json(value));
1212 let path = VarPath {
1213 segments: vec![VarSegment::Field(root.into()), seg],
1214 };
1215 scope.resolve_path(&path)
1216 }
1217
1218 #[test]
1219 fn out_of_bounds_index_is_absence() {
1220 let mut scope = Scope::new();
1221 let r = subscripted(&mut scope, "xs", serde_json::json!([1, 2]), VarSegment::Index(9));
1222 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1223 }
1224
1225 #[test]
1226 fn missing_record_key_is_absence() {
1227 let mut scope = Scope::new();
1228 let r = subscripted(&mut scope, "u", serde_json::json!({"name": "amy"}), VarSegment::Key("nope".into()));
1229 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1230 }
1231
1232 #[test]
1233 fn string_key_on_a_list_is_shape() {
1234 let mut scope = Scope::new();
1235 let r = subscripted(&mut scope, "xs", serde_json::json!([1, 2]), VarSegment::Key("web".into()));
1236 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1237 }
1238
1239 #[test]
1240 fn integer_index_on_a_record_is_shape() {
1241 let mut scope = Scope::new();
1242 let r = subscripted(&mut scope, "u", serde_json::json!({"name": "amy"}), VarSegment::Index(0));
1243 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1244 }
1245
1246 #[test]
1247 fn subscripting_a_scalar_is_shape() {
1248 let mut scope = Scope::new();
1249 scope.set("s", Value::String("hello".into()));
1250 let path = VarPath {
1251 segments: vec![VarSegment::Field("s".into()), VarSegment::Index(0)],
1252 };
1253 assert!(matches!(scope.resolve_path(&path), Err(PathError::Shape(_))));
1254 }
1255
1256 #[test]
1257 fn unset_dynamic_key_is_undefined_root_not_absence() {
1258 let mut scope = Scope::new();
1261 let r = subscripted(
1262 &mut scope,
1263 "r",
1264 serde_json::json!({"name": "amy"}),
1265 VarSegment::Dynamic("k".into()),
1266 );
1267 assert!(matches!(r, Err(PathError::UndefinedRoot(_))), "got: {r:?}");
1268 }
1269
1270 #[test]
1271 fn contains_finds_variable() {
1272 let mut scope = Scope::new();
1273 scope.set("EXISTS", Value::Bool(true));
1274 assert!(scope.contains("EXISTS"));
1275 assert!(!scope.contains("MISSING"));
1276 }
1277
1278 #[test]
1279 fn all_names_lists_variables() {
1280 let mut scope = Scope::new();
1281 scope.set("A", Value::Int(1));
1282 scope.set("B", Value::Int(2));
1283 scope.push_frame();
1284 scope.set("C", Value::Int(3));
1285
1286 let names = scope.all_names();
1287 assert!(names.contains(&"A"));
1288 assert!(names.contains(&"B"));
1289 assert!(names.contains(&"C"));
1290 }
1291
1292 #[test]
1293 #[should_panic(expected = "cannot pop the root scope frame")]
1294 fn pop_root_frame_panics() {
1295 let mut scope = Scope::new();
1296 scope.pop_frame();
1297 }
1298
1299 #[test]
1300 fn positional_params_basic() {
1301 let mut scope = Scope::new();
1302 scope.set_positional("my_tool", vec!["arg1".into(), "arg2".into(), "arg3".into()]);
1303
1304 assert_eq!(scope.get_positional(0), Some("my_tool"));
1306 assert_eq!(scope.get_positional(1), Some("arg1"));
1308 assert_eq!(scope.get_positional(2), Some("arg2"));
1309 assert_eq!(scope.get_positional(3), Some("arg3"));
1310 assert_eq!(scope.get_positional(4), None);
1312 }
1313
1314 #[test]
1315 fn positional_params_empty() {
1316 let scope = Scope::new();
1317 assert_eq!(scope.get_positional(0), None);
1319 assert_eq!(scope.get_positional(1), None);
1320 assert_eq!(scope.arg_count(), 0);
1321 assert!(scope.all_args().is_empty());
1322 }
1323
1324 #[test]
1325 fn all_args_returns_slice() {
1326 let mut scope = Scope::new();
1327 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1328
1329 let args = scope.all_args();
1330 assert_eq!(args, &["a", "b", "c"]);
1331 }
1332
1333 #[test]
1334 fn arg_count_returns_count() {
1335 let mut scope = Scope::new();
1336 scope.set_positional("test", vec!["one".into(), "two".into()]);
1337
1338 assert_eq!(scope.arg_count(), 2);
1339 }
1340
1341 #[test]
1342 fn export_marks_variable() {
1343 let mut scope = Scope::new();
1344 scope.set("X", Value::Int(42));
1345
1346 assert!(!scope.is_exported("X"));
1347 scope.export("X");
1348 assert!(scope.is_exported("X"));
1349 }
1350
1351 #[test]
1352 fn set_exported_sets_and_exports() {
1353 let mut scope = Scope::new();
1354 scope.set_exported("PATH", Value::String("/usr/bin".into()));
1355
1356 assert!(scope.is_exported("PATH"));
1357 assert_eq!(scope.get("PATH"), Some(&Value::String("/usr/bin".into())));
1358 }
1359
1360 #[test]
1361 fn unexport_removes_export_marker() {
1362 let mut scope = Scope::new();
1363 scope.set_exported("VAR", Value::Int(1));
1364 assert!(scope.is_exported("VAR"));
1365
1366 scope.unexport("VAR");
1367 assert!(!scope.is_exported("VAR"));
1368 assert!(scope.get("VAR").is_some());
1370 }
1371
1372 #[test]
1373 fn exported_vars_returns_only_exported_with_values() {
1374 let mut scope = Scope::new();
1375 scope.set_exported("A", Value::Int(1));
1376 scope.set_exported("B", Value::Int(2));
1377 scope.set("C", Value::Int(3)); scope.export("D"); let exported = scope.exported_vars();
1381 assert_eq!(exported.len(), 2);
1382 assert_eq!(exported[0], ("A".to_string(), Value::Int(1)));
1383 assert_eq!(exported[1], ("B".to_string(), Value::Int(2)));
1384 }
1385
1386 #[test]
1387 fn exported_names_returns_sorted_names() {
1388 let mut scope = Scope::new();
1389 scope.export("Z");
1390 scope.export("A");
1391 scope.export("M");
1392
1393 let names = scope.exported_names();
1394 assert_eq!(names, vec!["A", "M", "Z"]);
1395 }
1396
1397 fn write_at(
1401 scope: &mut Scope,
1402 root: &str,
1403 segs: Vec<VarSegment>,
1404 ) -> Result<(), PathError> {
1405 let mut segments = vec![VarSegment::Field(root.into())];
1406 segments.extend(segs);
1407 scope.walk_write(&VarPath { segments }, Value::Int(0))
1408 }
1409
1410 #[test]
1411 fn walk_write_list_index_update() {
1412 let mut scope = Scope::new();
1413 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1414 let path = VarPath {
1415 segments: vec![VarSegment::Field("xs".into()), VarSegment::Index(0)],
1416 };
1417 scope.walk_write(&path, Value::Int(9)).expect("write should succeed");
1418 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!([9, 2, 3]))));
1419 }
1420
1421 #[test]
1422 fn walk_write_negative_index() {
1423 let mut scope = Scope::new();
1424 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1425 let path = VarPath {
1426 segments: vec![VarSegment::Field("xs".into()), VarSegment::Index(-1)],
1427 };
1428 scope.walk_write(&path, Value::Int(7)).expect("write should succeed");
1429 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!([1, 2, 7]))));
1430 }
1431
1432 #[test]
1433 fn walk_write_inserts_a_new_record_key() {
1434 let mut scope = Scope::new();
1435 scope.set("u", Value::Json(serde_json::json!({"port": 8080})));
1436 let path = VarPath {
1437 segments: vec![VarSegment::Field("u".into()), VarSegment::Key("host".into())],
1438 };
1439 scope
1440 .walk_write(&path, Value::String("localhost".into()))
1441 .expect("write should succeed");
1442 assert_eq!(
1443 scope.get("u"),
1444 Some(&Value::Json(serde_json::json!({"port": 8080, "host": "localhost"})))
1445 );
1446 }
1447
1448 #[test]
1449 fn walk_write_deep_path_updates_nested_key() {
1450 let mut scope = Scope::new();
1451 scope.set("s", Value::Json(serde_json::json!({"web": {"port": 8080}})));
1452 let path = VarPath {
1453 segments: vec![
1454 VarSegment::Field("s".into()),
1455 VarSegment::Key("web".into()),
1456 VarSegment::Key("port".into()),
1457 ],
1458 };
1459 scope.walk_write(&path, Value::Int(9000)).expect("write should succeed");
1460 assert_eq!(
1461 scope.get("s"),
1462 Some(&Value::Json(serde_json::json!({"web": {"port": 9000}})))
1463 );
1464 }
1465
1466 #[test]
1467 fn walk_write_out_of_bounds_index_is_absence() {
1468 let mut scope = Scope::new();
1469 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1470 let r = write_at(&mut scope, "xs", vec![VarSegment::Index(9)]);
1471 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1472 }
1473
1474 #[test]
1475 fn walk_write_missing_intermediate_is_absence_no_autoviv() {
1476 let mut scope = Scope::new();
1477 scope.set("s", Value::Json(serde_json::json!({"web": {"port": 8080}})));
1478 let r = write_at(
1479 &mut scope,
1480 "s",
1481 vec![VarSegment::Key("api".into()), VarSegment::Key("port".into())],
1482 );
1483 assert!(matches!(r, Err(PathError::Absence(_))), "got: {r:?}");
1484 assert_eq!(
1486 scope.get("s"),
1487 Some(&Value::Json(serde_json::json!({"web": {"port": 8080}})))
1488 );
1489 }
1490
1491 #[test]
1492 fn walk_write_scalar_root_is_shape() {
1493 let mut scope = Scope::new();
1494 scope.set("y", Value::String("hi".into()));
1495 let r = write_at(&mut scope, "y", vec![VarSegment::Index(0)]);
1496 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1497 }
1498
1499 #[test]
1500 fn walk_write_undefined_root_is_undefined_root() {
1501 let mut scope = Scope::new();
1502 let r = write_at(&mut scope, "z", vec![VarSegment::Index(0)]);
1503 assert!(matches!(r, Err(PathError::UndefinedRoot(_))), "got: {r:?}");
1504 }
1505
1506 #[test]
1507 fn walk_write_slice_lvalue_is_shape() {
1508 let mut scope = Scope::new();
1509 scope.set("xs", Value::Json(serde_json::json!([1, 2, 3])));
1510 let r = write_at(&mut scope, "xs", vec![VarSegment::Slice(Some(0), Some(2))]);
1511 assert!(matches!(r, Err(PathError::Shape(_))), "got: {r:?}");
1512 }
1513
1514 #[test]
1517 fn walk_append_extends_a_list_in_place() {
1518 let mut scope = Scope::new();
1519 scope.set("xs", Value::Json(serde_json::json!(["a", "b"])));
1520 scope
1521 .walk_append(&VarPath::simple("xs"), vec![Value::String("c".into())])
1522 .expect("push should succeed");
1523 assert_eq!(scope.get("xs"), Some(&Value::Json(serde_json::json!(["a", "b", "c"]))));
1524 }
1525
1526 #[test]
1527 fn walk_append_undefined_target_is_a_loud_error() {
1528 let mut scope = Scope::new();
1529 let r = scope.walk_append(&VarPath::simple("nope"), vec![Value::Int(1)]);
1530 assert!(r.is_err(), "expected a loud error for an undefined target");
1531 }
1532
1533 #[test]
1534 fn walk_append_non_list_target_is_a_loud_error() {
1535 let mut scope = Scope::new();
1536 scope.set("y", Value::String("hi".into()));
1537 let r = scope.walk_append(&VarPath::simple("y"), vec![Value::Int(1)]);
1538 assert!(r.is_err(), "expected a loud error for a non-list target");
1539 }
1540
1541 #[test]
1542 fn walk_append_bracket_path_extends_a_nested_list_in_place() {
1543 let mut scope = Scope::new();
1544 scope.set(
1545 "services",
1546 Value::Json(serde_json::json!({"web": {"tags": ["a"]}})),
1547 );
1548 let path = VarPath {
1549 segments: vec![
1550 VarSegment::Field("services".into()),
1551 VarSegment::Key("web".into()),
1552 VarSegment::Key("tags".into()),
1553 ],
1554 };
1555 scope
1556 .walk_append(&path, vec![Value::String("b".into())])
1557 .expect("bracket-path push should succeed");
1558 assert_eq!(
1559 scope.get("services"),
1560 Some(&Value::Json(serde_json::json!({"web": {"tags": ["a", "b"]}})))
1561 );
1562 }
1563
1564 #[test]
1565 fn walk_append_bracket_path_missing_intermediate_is_a_loud_error() {
1566 let mut scope = Scope::new();
1567 scope.set("services", Value::Json(serde_json::json!({})));
1568 let path = VarPath {
1569 segments: vec![
1570 VarSegment::Field("services".into()),
1571 VarSegment::Key("web".into()),
1572 VarSegment::Key("tags".into()),
1573 ],
1574 };
1575 let r = scope.walk_append(&path, vec![Value::String("x".into())]);
1576 assert!(r.is_err(), "expected a loud error for a missing intermediate");
1577 }
1578
1579 #[test]
1580 fn walk_append_bracket_path_non_list_leaf_is_a_loud_error() {
1581 let mut scope = Scope::new();
1582 scope.set(
1583 "services",
1584 Value::Json(serde_json::json!({"web": {"port": 8080}})),
1585 );
1586 let path = VarPath {
1587 segments: vec![
1588 VarSegment::Field("services".into()),
1589 VarSegment::Key("web".into()),
1590 VarSegment::Key("port".into()),
1591 ],
1592 };
1593 let r = scope.walk_append(&path, vec![Value::Int(1)]);
1594 assert!(r.is_err(), "expected a loud error for a non-list leaf");
1595 }
1596}