1use std::collections::HashMap;
5use memchr::memchr;
6use crate::value::*;
7use crate::rng;
8
9pub(crate) const MAX_SYNX_INPUT_BYTES: usize = 16 * 1024 * 1024;
14
15const MAX_LINE_STARTS: usize = 2_000_000;
17
18const MAX_PARSE_NESTING_DEPTH: usize = 128;
20
21const MAX_MULTILINE_BLOCK_BYTES: usize = 1024 * 1024;
23
24const MAX_LIST_ITEMS: usize = 1_048_576;
26
27const MAX_INCLUDE_DIRECTIVES: usize = 4096;
29
30const MAX_CONSTRAINT_ENUM_PARTS: usize = 4096;
32
33const MAX_MARKER_CHAIN_SEGMENTS: usize = 512;
35
36pub(crate) fn clamp_synx_text(text: &str) -> &str {
38 if text.len() <= MAX_SYNX_INPUT_BYTES {
39 return text;
40 }
41 let slice = &text.as_bytes()[..MAX_SYNX_INPUT_BYTES];
42 let end = core::str::from_utf8(slice)
43 .map(|s| s.len())
44 .unwrap_or_else(|e| e.valid_up_to());
45 &text[..end]
46}
47
48fn find_parse_end_bytes(bytes: &[u8]) -> usize {
51 let max_newlines = MAX_LINE_STARTS.saturating_sub(1);
52 let mut seen_newlines = 0usize;
53 let mut scan = 0usize;
54 while scan < bytes.len() {
55 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
56 if seen_newlines >= max_newlines {
57 return scan + rel;
58 }
59 seen_newlines += 1;
60 scan += rel + 1;
61 } else {
62 break;
63 }
64 }
65 bytes.len()
66}
67
68pub fn parse(text: &str) -> ParseResult {
70 let text = clamp_synx_text(text);
71 let parse_end = find_parse_end_bytes(text.as_bytes());
72 let text = &text[..parse_end];
73 let bytes = text.as_bytes();
74
75 let mut line_starts: Vec<usize> = Vec::new();
76 line_starts.push(0);
77 let mut scan = 0usize;
78 while scan < bytes.len() {
79 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
80 let pos = scan + rel;
81 line_starts.push(pos + 1);
82 scan = pos + 1;
83 } else {
84 break;
85 }
86 }
87 let line_count = line_starts.len();
88
89 let mut root = HashMap::new();
90 let mut stack: Vec<(i32, StackEntry)> = vec![(-1, StackEntry::Root)];
91 let mut mode = Mode::Static;
92 let mut locked = false;
93 let mut tool = false;
94 let mut schema = false;
95 let mut metadata: HashMap<String, MetaMap> = HashMap::new();
96 let mut includes: Vec<IncludeDirective> = Vec::new();
97
98 let mut block: Option<BlockState> = None;
99 let mut list: Option<ListState> = None;
100 let mut in_block_comment = false;
101
102 let mut i = 0;
103 while i < line_count {
104 let start = line_starts[i];
106 let end = if i + 1 < line_count { line_starts[i + 1] - 1 } else { bytes.len() };
107 let end = if end > start && end > 0 && bytes.get(end - 1) == Some(&b'\r') { end - 1 } else { end };
109 let raw = &text[start..end];
110
111 let trimmed = raw.trim();
112
113 if trimmed == "!active" {
115 mode = Mode::Active;
116 i += 1;
117 continue;
118 }
119 if trimmed == "!lock" {
120 locked = true;
121 i += 1;
122 continue;
123 }
124 if trimmed == "!tool" {
125 tool = true;
126 i += 1;
127 continue;
128 }
129 if trimmed == "!schema" {
130 schema = true;
131 i += 1;
132 continue;
133 }
134 if trimmed.starts_with("!include ") {
135 if includes.len() < MAX_INCLUDE_DIRECTIVES {
136 let rest = trimmed[9..].trim();
137 let mut parts = rest.splitn(2, char::is_whitespace);
138 let path = parts.next().unwrap_or("").to_string();
139 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
140 let name = path.rsplit(&['/', '\\'][..]).next().unwrap_or(&path);
142 name.strip_suffix(".synx").or_else(|| name.strip_suffix(".SYNX")).unwrap_or(name).to_string()
143 });
144 includes.push(IncludeDirective { path, alias });
145 }
146 i += 1;
147 continue;
148 }
149 if trimmed.starts_with("#!mode:") {
150 let declared = trimmed.splitn(2, ':').nth(1).unwrap_or("static").trim();
151 mode = if declared == "active" { Mode::Active } else { Mode::Static };
152 i += 1;
153 continue;
154 }
155
156 if trimmed == "###" {
158 in_block_comment = !in_block_comment;
159 i += 1;
160 continue;
161 }
162 if in_block_comment {
163 i += 1;
164 continue;
165 }
166
167 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
169 i += 1;
170 continue;
171 }
172
173 let indent = (raw.len() - raw.trim_start().len()) as i32;
174
175 if let Some(ref mut blk) = block {
177 if indent > blk.indent {
178 if blk.content.len() < MAX_MULTILINE_BLOCK_BYTES {
179 if !blk.content.is_empty() {
180 blk.content.push('\n');
181 }
182 let room = MAX_MULTILINE_BLOCK_BYTES.saturating_sub(blk.content.len());
183 if room > 0 {
184 let n = trimmed.len().min(room);
185 blk.content.push_str(&trimmed[..n]);
186 }
187 }
188 i += 1;
189 continue;
190 } else {
191 let content = std::mem::take(&mut blk.content);
192 let blk_key = blk.key.clone();
193 let blk_stack_idx = blk.stack_idx;
194 block = None;
195 insert_value(&mut root, &stack, blk_stack_idx, &blk_key, Value::String(content));
196 }
197 }
198
199 if trimmed.starts_with("- ") {
201 if let Some(ref mut lst) = list {
202 if indent > lst.indent {
203 if lst.items.len() < MAX_LIST_ITEMS {
204 let val_str = strip_comment(trimmed[2..].trim());
205 lst.items.push(cast(&val_str));
206 }
207 i += 1;
208 continue;
209 }
210 }
211 } else if let Some(ref lst) = list {
212 if indent <= lst.indent {
213 let items = list.take().unwrap();
214 let arr = Value::Array(items.items);
215 insert_value(&mut root, &stack, items.stack_idx, &items.key, arr);
216 }
217 }
218
219 if let Some(parsed) = parse_line(trimmed) {
221 while stack.len() > 1 && stack.last().unwrap().0 >= indent {
223 stack.pop();
224 }
225
226 let parent_idx = stack.len() - 1;
227
228 if mode == Mode::Active
230 && (!parsed.markers.is_empty()
231 || parsed.constraints.is_some()
232 || parsed.type_hint.is_some())
233 {
234 let path = build_path(&stack);
235 let meta_map = metadata.entry(path).or_default();
236 meta_map.insert(
237 parsed.key.clone(),
238 Meta {
239 markers: parsed.markers.clone(),
240 args: parsed.marker_args.clone(),
241 type_hint: parsed.type_hint.clone(),
242 constraints: parsed.constraints.clone(),
243 },
244 );
245 }
246
247 let is_block = parsed.value == "|";
248 let is_list_marker = parsed.markers.iter().any(|m| {
249 matches!(m.as_str(), "random" | "unique" | "geo" | "join")
250 });
251
252 if is_block {
253 insert_value(
254 &mut root,
255 &stack,
256 parent_idx,
257 &parsed.key,
258 Value::String(String::new()),
259 );
260 block = Some(BlockState {
261 indent,
262 key: parsed.key,
263 content: String::new(),
264 stack_idx: parent_idx,
265 });
266 } else if is_list_marker && parsed.value.is_empty() {
267 list = Some(ListState {
268 indent,
269 key: parsed.key,
270 items: Vec::new(),
271 stack_idx: parent_idx,
272 });
273 } else if parsed.value.is_empty() {
274 let mut peek = i + 1;
276 while peek < line_count {
277 let ps = line_starts[peek];
278 let pe = if peek + 1 < line_count {
279 line_starts[peek + 1] - 1
280 } else {
281 bytes.len()
282 };
283 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
284 let pt = text[ps..pe].trim();
285 if !pt.is_empty() {
286 break;
287 }
288 peek += 1;
289 }
290
291 if peek < line_count {
292 let ps = line_starts[peek];
293 let pe = if peek + 1 < line_count {
294 line_starts[peek + 1] - 1
295 } else {
296 bytes.len()
297 };
298 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
299 let pt = text[ps..pe].trim();
300 if pt.starts_with("- ") {
301 list = Some(ListState {
302 indent,
303 key: parsed.key,
304 items: Vec::new(),
305 stack_idx: parent_idx,
306 });
307 i += 1;
308 continue;
309 }
310 }
311
312 insert_value(
313 &mut root,
314 &stack,
315 parent_idx,
316 &parsed.key,
317 Value::Object(HashMap::new()),
318 );
319 if stack.len() < MAX_PARSE_NESTING_DEPTH {
323 stack.push((indent, StackEntry::Key(parsed.key)));
324 }
325 } else {
326 let value = if let Some(ref hint) = parsed.type_hint {
327 cast_typed(&parsed.value, hint)
328 } else {
329 cast(&parsed.value)
330 };
331 insert_value(&mut root, &stack, parent_idx, &parsed.key, value);
332 }
333 }
334
335 i += 1;
336 }
337
338 if let Some(blk) = block {
340 insert_value(
341 &mut root,
342 &stack,
343 blk.stack_idx,
344 &blk.key,
345 Value::String(blk.content),
346 );
347 }
348
349 if let Some(lst) = list {
351 let arr = Value::Array(lst.items);
352 insert_value(&mut root, &stack, lst.stack_idx, &lst.key, arr);
353 }
354
355 let parsed_root = Value::Object(root);
356
357 ParseResult {
361 root: parsed_root,
362 mode,
363 locked,
364 tool,
365 schema,
366 metadata,
367 includes,
368 }
369}
370
371pub fn reshape_tool_output(root: &Value, schema: bool) -> Value {
383 let map = match root {
384 Value::Object(m) => m,
385 _ => return root.clone(),
386 };
387
388 if schema {
389 let mut tools = Vec::new();
391 let mut keys: Vec<&String> = map.keys().collect();
393 keys.sort();
394 for key in keys {
395 let val = &map[key];
396 let mut def = HashMap::new();
397 def.insert("name".to_string(), Value::String(key.clone()));
398 def.insert("params".to_string(), val.clone());
399 tools.push(Value::Object(def));
400 }
401 let mut out = HashMap::new();
402 out.insert("tools".to_string(), Value::Array(tools));
403 Value::Object(out)
404 } else {
405 if map.is_empty() {
407 let mut out = HashMap::new();
408 out.insert("tool".to_string(), Value::Null);
409 out.insert("params".to_string(), Value::Object(HashMap::new()));
410 return Value::Object(out);
411 }
412
413 let mut keys: Vec<&String> = map.keys().collect();
416 keys.sort();
417 let tool_key = keys[0];
418 let tool_value = &map[tool_key];
419
420 let params = match tool_value {
421 Value::Object(m) => Value::Object(m.clone()),
422 _ => Value::Object(HashMap::new()),
424 };
425
426 let mut out = HashMap::new();
427 out.insert("tool".to_string(), Value::String(tool_key.clone()));
428 out.insert("params".to_string(), params);
429 Value::Object(out)
430 }
431}
432
433#[derive(Debug)]
436enum StackEntry {
437 Root,
438 Key(String),
439}
440
441struct BlockState {
442 indent: i32,
443 key: String,
444 content: String,
445 stack_idx: usize,
446}
447
448struct ListState {
449 indent: i32,
450 key: String,
451 items: Vec<Value>,
452 stack_idx: usize,
453}
454
455struct ParsedLine {
456 key: String,
457 type_hint: Option<String>,
458 value: String,
459 markers: Vec<String>,
460 marker_args: Vec<String>,
461 constraints: Option<Constraints>,
462}
463
464fn parse_line(trimmed: &str) -> Option<ParsedLine> {
467 if trimmed.is_empty()
468 || trimmed.starts_with('#')
469 || trimmed.starts_with("//")
470 || trimmed.starts_with("- ")
471 {
472 return None;
473 }
474
475 let bytes = trimmed.as_bytes();
476 let len = bytes.len();
477
478 let first = bytes[0];
479 if first == b'[' || first == b':' || first == b'-' || first == b'#' || first == b'/' || first == b'(' {
480 return None;
481 }
482
483 let mut pos = 0;
485 while pos < len {
486 let ch = bytes[pos];
487 if ch == b' ' || ch == b'\t' || ch == b'[' || ch == b':' || ch == b'(' {
488 break;
489 }
490 pos += 1;
491 }
492 let key = trimmed[..pos].to_string();
493
494 let mut type_hint = None;
496 if pos < len && bytes[pos] == b'(' {
497 let start = pos + 1;
498 if let Some(c) = trimmed[start..].find(')') {
499 type_hint = Some(trimmed[start..start + c].to_string());
500 pos = start + c + 1;
501 } else {
502 pos += 1;
503 }
504 }
505
506 let mut constraints = None;
508 if pos < len && bytes[pos] == b'[' {
509 if let Some(close) = trimmed[pos..].find(']') {
510 let constraint_str = &trimmed[pos + 1..pos + close];
511 constraints = Some(parse_constraints(constraint_str));
512 pos += close + 1;
513 } else {
514 pos += 1;
515 }
516 }
517
518 let mut markers = Vec::new();
520 let mut marker_args = Vec::new();
521 if pos < len && bytes[pos] == b':' {
522 let marker_start = pos + 1;
523 let mut marker_end = marker_start;
524 while marker_end < len && bytes[marker_end] != b' ' && bytes[marker_end] != b'\t' {
525 marker_end += 1;
526 }
527 let chain = &trimmed[marker_start..marker_end];
528 markers = chain
529 .split(':')
530 .take(MAX_MARKER_CHAIN_SEGMENTS)
531 .map(|s| s.to_string())
532 .collect();
533 pos = marker_end;
534 }
535
536 while pos < len && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
538 pos += 1;
539 }
540
541 let mut raw_value = if pos < len {
543 strip_comment(&trimmed[pos..])
544 } else {
545 String::new()
546 };
547
548 if markers.contains(&"random".to_string()) && !raw_value.is_empty() {
550 let parts: Vec<&str> = raw_value.split_whitespace().collect();
551 let nums: Vec<String> = parts
552 .iter()
553 .filter(|s| s.parse::<f64>().is_ok())
554 .map(|s| s.to_string())
555 .collect();
556 if !nums.is_empty() {
557 marker_args = nums;
558 raw_value.clear();
559 }
560 }
561
562 Some(ParsedLine {
563 key,
564 type_hint,
565 value: raw_value,
566 markers,
567 marker_args,
568 constraints,
569 })
570}
571
572fn parse_constraints(raw: &str) -> Constraints {
575 let mut c = Constraints::default();
576 for part in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
577 if part == "required" {
578 c.required = true;
579 } else if part == "readonly" {
580 c.readonly = true;
581 } else if let Some(colon) = part.find(':') {
582 let key = part[..colon].trim();
583 let val = part[colon + 1..].trim();
584 match key {
585 "min" => c.min = val.parse().ok(),
586 "max" => c.max = val.parse().ok(),
587 "type" => c.type_name = Some(val.to_string()),
588 "pattern" => c.pattern = Some(val.to_string()),
589 "enum" => {
590 c.enum_values = Some(
591 val.split('|')
592 .take(MAX_CONSTRAINT_ENUM_PARTS)
593 .map(|s| s.to_string())
594 .collect(),
595 );
596 }
597 _ => {}
598 }
599 }
600 }
601 c
602}
603
604fn cast(val: &str) -> Value {
607 if val.len() >= 2 {
610 let bytes = val.as_bytes();
611 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
612 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
613 {
614 return Value::String(val[1..val.len() - 1].to_string());
615 }
616 }
617
618 match val {
619 "true" => Value::Bool(true),
620 "false" => Value::Bool(false),
621 "null" => Value::Null,
622 _ => {
623 let bytes = val.as_bytes();
624 let len = bytes.len();
625 if len == 0 {
626 return Value::String(String::new());
627 }
628
629 let mut start = 0;
630 if bytes[0] == b'-' {
631 if len == 1 {
632 return Value::String(val.to_string());
633 }
634 start = 1;
635 }
636
637 if bytes[start] >= b'0' && bytes[start] <= b'9' {
638 let mut dot_pos = None;
639 let mut all_numeric = true;
640 for j in start..len {
641 if bytes[j] == b'.' {
642 if dot_pos.is_some() {
643 all_numeric = false;
644 break;
645 }
646 dot_pos = Some(j);
647 } else if bytes[j] < b'0' || bytes[j] > b'9' {
648 all_numeric = false;
649 break;
650 }
651 }
652 if all_numeric {
653 if let Some(dp) = dot_pos {
654 if dp > start && dp < len - 1 {
655 if let Ok(f) = val.parse::<f64>() {
656 return Value::Float(f);
657 }
658 }
659 } else if let Ok(n) = val.parse::<i64>() {
660 return Value::Int(n);
661 }
662 }
663 }
664
665 Value::String(val.to_string())
666 }
667 }
668}
669
670fn cast_typed(val: &str, hint: &str) -> Value {
671 match hint {
672 "int" => Value::Int(val.parse().unwrap_or(0)),
673 "float" => Value::Float(val.parse().unwrap_or(0.0)),
674 "bool" => Value::Bool(val.trim() == "true"),
675 "string" => Value::String(val.to_string()),
676 "random" | "random:int" => Value::Int(rng::random_i64()),
677 "random:float" => Value::Float(rng::random_f64_01()),
678 "random:bool" => Value::Bool(rng::random_bool()),
679 _ => cast(val),
680 }
681}
682
683fn strip_comment(val: &str) -> String {
684 let mut result = val.to_string();
685 if let Some(idx) = result.find(" //") {
686 result.truncate(idx);
687 }
688 if let Some(idx) = result.find(" #") {
689 result.truncate(idx);
690 }
691 result.trim_end().to_string()
692}
693
694fn build_path(stack: &[(i32, StackEntry)]) -> String {
697 let mut parts = Vec::new();
698 for (_, entry) in stack.iter().skip(1) {
699 if let StackEntry::Key(ref k) = entry {
700 parts.push(k.as_str());
701 }
702 }
703 parts.join(".")
704}
705
706fn insert_value(
707 root: &mut HashMap<String, Value>,
708 stack: &[(i32, StackEntry)],
709 parent_idx: usize,
710 key: &str,
711 value: Value,
712) {
713 if let Some(target) = navigate_to_parent(root, stack, parent_idx) {
714 target.insert(key.to_string(), value);
715 }
716 }
720
721fn navigate_to_parent<'a>(
722 root: &'a mut HashMap<String, Value>,
723 stack: &[(i32, StackEntry)],
724 target_idx: usize,
725) -> Option<&'a mut HashMap<String, Value>> {
726 if target_idx == 0 {
727 return Some(root);
728 }
729
730 let path: Vec<&str> = stack
731 .iter()
732 .skip(1)
733 .take(target_idx)
734 .filter_map(|(_, entry)| match entry {
735 StackEntry::Key(k) => Some(k.as_str()),
736 _ => None,
737 })
738 .collect();
739
740 let mut current = root as *mut HashMap<String, Value>;
751 for key in path {
752 let child = unsafe { (*current).get_mut(key) };
753 match child {
754 Some(Value::Object(map)) => current = map as *mut HashMap<String, Value>,
755 _ => return None, }
757 }
758 Some(unsafe { &mut *current })
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn test_simple_key_value() {
767 let data = parse("name Wario\nage 30\nactive true\nscore 99.5\nempty null");
768 let root = data.root.as_object().unwrap();
769 assert_eq!(root["name"], Value::String("Wario".into()));
770 assert_eq!(root["age"], Value::Int(30));
771 assert_eq!(root["active"], Value::Bool(true));
772 assert_eq!(root["score"], Value::Float(99.5));
773 assert_eq!(root["empty"], Value::Null);
774 assert_eq!(data.mode, Mode::Static);
775 }
776
777 #[test]
778 fn test_nested_objects() {
779 let data = parse("server\n host 0.0.0.0\n port 8080\n ssl\n enabled true");
780 let root = data.root.as_object().unwrap();
781 let server = root["server"].as_object().unwrap();
782 assert_eq!(server["host"], Value::String("0.0.0.0".into()));
783 assert_eq!(server["port"], Value::Int(8080));
784 let ssl = server["ssl"].as_object().unwrap();
785 assert_eq!(ssl["enabled"], Value::Bool(true));
786 }
787
788 #[test]
789 fn test_lists() {
790 let data = parse("inventory\n - Sword\n - Shield\n - Potion");
791 let root = data.root.as_object().unwrap();
792 let inv = root["inventory"].as_array().unwrap();
793 assert_eq!(inv.len(), 3);
794 assert_eq!(inv[0], Value::String("Sword".into()));
795 }
796
797 #[test]
798 fn test_multiline_block() {
799 let data = parse("rules |\n Rule one.\n Rule two.\n Rule three.");
800 let root = data.root.as_object().unwrap();
801 assert_eq!(
802 root["rules"],
803 Value::String("Rule one.\nRule two.\nRule three.".into())
804 );
805 }
806
807 #[test]
808 fn test_comments() {
809 let data = parse("# comment\nname Wario # inline\nage 30 // inline");
810 let root = data.root.as_object().unwrap();
811 assert_eq!(root["name"], Value::String("Wario".into()));
812 assert_eq!(root["age"], Value::Int(30));
813 }
814
815 #[test]
816 fn test_active_mode() {
817 let data = parse("!active\nprice 100\ntax:calc price * 0.2");
818 assert_eq!(data.mode, Mode::Active);
819 let root = data.root.as_object().unwrap();
820 assert_eq!(root["price"], Value::Int(100));
821 assert_eq!(root["tax"], Value::String("price * 0.2".into()));
823 let meta = data.metadata.get("").unwrap();
825 assert!(meta.contains_key("tax"));
826 assert_eq!(meta["tax"].markers, vec!["calc"]);
827 }
828
829 #[test]
830 fn test_markers_env_default() {
831 let data = parse("!active\nport:env:default:3000 PORT");
832 let meta = data.metadata.get("").unwrap();
833 assert_eq!(meta["port"].markers, vec!["env", "default", "3000"]);
834 }
835
836 #[test]
837 fn test_type_hint() {
838 let data = parse("zip(string) 90210");
839 let root = data.root.as_object().unwrap();
840 assert_eq!(root["zip"], Value::String("90210".into()));
841 }
842
843 #[test]
844 fn test_constraints() {
845 let data = parse("!active\nname[min:3, max:30, required] Wario");
846 let meta = data.metadata.get("").unwrap();
847 let c = meta["name"].constraints.as_ref().unwrap();
848 assert_eq!(c.min, Some(3.0));
849 assert_eq!(c.max, Some(30.0));
850 assert!(c.required);
851 }
852
853 #[test]
854 fn test_random_weights() {
855 let data = parse("!active\ntier:random 90 5 5");
856 let meta = data.metadata.get("").unwrap();
857 assert_eq!(meta["tier"].markers, vec!["random"]);
858 assert_eq!(meta["tier"].args, vec!["90", "5", "5"]);
859 }
860
861 #[test]
862 fn test_tool_directive_flags() {
863 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
864 assert!(data.tool);
865 assert!(!data.schema);
866 assert_eq!(data.mode, Mode::Static);
867 let root = data.root.as_object().unwrap();
869 let ws = root["web_search"].as_object().unwrap();
870 assert_eq!(ws["query"], Value::String("test".into()));
871 assert_eq!(ws["lang"], Value::String("ru".into()));
872 }
873
874 #[test]
875 fn test_tool_schema_flags() {
876 let data = parse("!tool\n!schema\nweb_search\n query string\n");
877 assert!(data.tool);
878 assert!(data.schema);
879 }
880
881 #[test]
882 fn test_parse_caps_nesting_depth() {
883 let mut s = String::new();
886 for i in 0..(MAX_PARSE_NESTING_DEPTH as usize + 64) {
887 s.push_str(&" ".repeat(i));
888 s.push_str(&format!("k{i}\n"));
889 }
890
891 let data = parse(&s);
892 let mut cur = data.root.as_object().unwrap();
893 let mut depth = 0usize;
894 loop {
896 if cur.len() != 1 {
897 break;
898 }
899 let (_, v) = cur.iter().next().unwrap();
900 match v {
901 Value::Object(next) => {
902 depth += 1;
903 cur = next;
904 }
905 _ => break,
906 }
907 }
908
909 assert!(depth <= MAX_PARSE_NESTING_DEPTH);
910 }
911
912 #[test]
913 fn test_tool_call_reshape() {
914 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
915 let shaped = reshape_tool_output(&data.root, false);
916 let m = shaped.as_object().unwrap();
917 assert_eq!(m["tool"], Value::String("web_search".into()));
918 let params = m["params"].as_object().unwrap();
919 assert_eq!(params["query"], Value::String("test".into()));
920 assert_eq!(params["lang"], Value::String("ru".into()));
921 }
922
923 #[test]
924 fn test_tool_schema_reshape() {
925 let data = parse("!tool\n!schema\nweb_search\n query string\n lang string\nmemory_write\n path string\n value string\n");
926 let shaped = reshape_tool_output(&data.root, true);
927 let m = shaped.as_object().unwrap();
928 let tools = m["tools"].as_array().unwrap();
929 assert_eq!(tools.len(), 2);
930 let t0 = tools[0].as_object().unwrap();
932 assert_eq!(t0["name"], Value::String("memory_write".into()));
933 let p0 = t0["params"].as_object().unwrap();
934 assert_eq!(p0["path"], Value::String("string".into()));
935 let t1 = tools[1].as_object().unwrap();
936 assert_eq!(t1["name"], Value::String("web_search".into()));
937 }
938
939 #[test]
940 fn test_tool_empty() {
941 let data = parse("!tool\n");
942 assert!(data.tool);
943 let shaped = reshape_tool_output(&data.root, false);
944 let m = shaped.as_object().unwrap();
945 assert_eq!(m["tool"], Value::Null);
946 }
947
948 #[test]
949 fn test_tool_with_active() {
950 let data = parse("!tool\n!active\nweb_search\n port:env:default:8080 PORT\n");
951 assert!(data.tool);
952 assert_eq!(data.mode, Mode::Active);
953 let meta = data.metadata.get("web_search").unwrap();
955 assert_eq!(meta["port"].markers, vec!["env", "default", "8080"]);
956 }
957}