use std::collections::HashSet;
use crate::error::{AgmError, ErrorCode, ErrorLocation};
use crate::model::imports::ImportEntry;
use super::lexer::{Line, LineKind};
pub(crate) struct FieldTracker {
seen: HashSet<String>,
}
impl FieldTracker {
pub(crate) fn new() -> Self {
Self {
seen: HashSet::new(),
}
}
pub(crate) fn track(&mut self, field_name: &str) -> bool {
!self.seen.insert(field_name.to_owned())
}
}
pub(crate) const STRUCTURED_FIELD_NAMES: &[&str] = &[
"code",
"code_blocks",
"verify",
"agent_context",
"parallel_groups",
"memory",
"load_profiles",
];
pub(crate) fn is_structured_field(name: &str) -> bool {
STRUCTURED_FIELD_NAMES.contains(&name)
}
pub(crate) fn parse_indented_list(lines: &[Line], pos: &mut usize) -> Vec<String> {
let mut items = Vec::new();
while *pos < lines.len() {
match &lines[*pos].kind {
LineKind::ListItem(value) => {
items.push(value.clone());
*pos += 1;
}
LineKind::Comment | LineKind::TestExpectHeader(_) => {
*pos += 1;
}
LineKind::Blank => {
let mut lookahead = *pos + 1;
while lookahead < lines.len() {
match &lines[lookahead].kind {
LineKind::Blank => lookahead += 1,
LineKind::Comment | LineKind::TestExpectHeader(_) => {
lookahead += 1;
}
LineKind::ListItem(_) => break,
_ => {
return items;
}
}
}
if lookahead < lines.len() {
if let LineKind::ListItem(_) = &lines[lookahead].kind {
*pos += 1; continue;
}
}
break;
}
_ => break,
}
}
items
}
pub(crate) fn parse_block(lines: &[Line], pos: &mut usize) -> String {
let base_indent = {
let mut base = 0usize;
let mut i = *pos;
while i < lines.len() {
match &lines[i].kind {
LineKind::IndentedLine(_) | LineKind::ListItem(_) => {
base = lines[i].indent;
break;
}
LineKind::Blank => {
i += 1;
}
_ => break,
}
}
base
};
let mut parts: Vec<String> = Vec::new();
while *pos < lines.len() {
match &lines[*pos].kind {
LineKind::IndentedLine(_) | LineKind::ListItem(_) => {
let raw = &lines[*pos].raw;
let stripped = if raw.len() >= base_indent {
raw[base_indent..].to_owned()
} else {
raw.trim_start().to_owned()
};
parts.push(stripped);
*pos += 1;
}
LineKind::Blank => {
let mut lookahead = *pos + 1;
while lookahead < lines.len() {
match &lines[lookahead].kind {
LineKind::Blank => lookahead += 1,
LineKind::IndentedLine(_) | LineKind::ListItem(_) => break,
_ => {
return finish_block(parts);
}
}
}
if lookahead < lines.len() {
match &lines[lookahead].kind {
LineKind::IndentedLine(_) | LineKind::ListItem(_) => {
parts.push(String::new()); *pos += 1;
continue;
}
_ => {}
}
}
break;
}
_ => break,
}
}
finish_block(parts)
}
fn finish_block(mut parts: Vec<String>) -> String {
while parts.last().is_some_and(|s| s.is_empty()) {
parts.pop();
}
parts.join("\n")
}
pub(crate) fn parse_imports(
items: &[String],
line_number: usize,
errors: &mut Vec<AgmError>,
) -> Vec<crate::model::imports::ImportEntry> {
let mut result = Vec::new();
for item in items {
match item.parse::<ImportEntry>() {
Ok(entry) => result.push(entry),
Err(_) => {
errors.push(AgmError::new(
ErrorCode::P001,
format!("Invalid import entry: {item:?}"),
ErrorLocation::new(None, Some(line_number), None),
));
}
}
}
result
}
pub(crate) fn collect_structured_raw(lines: &[Line], pos: &mut usize) -> String {
let mut parts: Vec<String> = Vec::new();
while *pos < lines.len() {
match &lines[*pos].kind {
LineKind::ScalarField(_, _)
| LineKind::InlineListField(_, _)
| LineKind::FieldStart(_)
| LineKind::ListItem(_)
| LineKind::IndentedLine(_)
| LineKind::BodyMarker => {
if lines[*pos].indent > 0
|| matches!(
&lines[*pos].kind,
LineKind::ListItem(_) | LineKind::IndentedLine(_)
)
{
parts.push(lines[*pos].raw.clone());
*pos += 1;
} else {
break;
}
}
LineKind::Blank => {
let mut lookahead = *pos + 1;
while lookahead < lines.len() {
if matches!(&lines[lookahead].kind, LineKind::Blank) {
lookahead += 1;
} else {
break;
}
}
let has_more = lookahead < lines.len()
&& matches!(
&lines[lookahead].kind,
LineKind::ScalarField(_, _)
| LineKind::InlineListField(_, _)
| LineKind::FieldStart(_)
| LineKind::ListItem(_)
| LineKind::IndentedLine(_)
)
&& lines[lookahead].indent > 0;
if has_more {
parts.push(lines[*pos].raw.clone());
*pos += 1;
} else {
break;
}
}
_ => break,
}
}
parts.join("\n")
}
pub(crate) fn skip_field_body(lines: &[Line], pos: &mut usize) {
collect_structured_raw(lines, pos);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::lexer::lex;
#[test]
fn test_field_tracker_new_not_duplicate() {
let mut tracker = FieldTracker::new();
assert!(!tracker.track("summary"));
}
#[test]
fn test_field_tracker_second_call_is_duplicate() {
let mut tracker = FieldTracker::new();
tracker.track("summary");
assert!(tracker.track("summary"));
}
#[test]
fn test_field_tracker_different_fields_not_duplicate() {
let mut tracker = FieldTracker::new();
tracker.track("summary");
assert!(!tracker.track("detail"));
}
#[test]
fn test_is_structured_field_known_returns_true() {
assert!(is_structured_field("code"));
assert!(is_structured_field("verify"));
assert!(is_structured_field("memory"));
}
#[test]
fn test_is_structured_field_unknown_returns_false() {
assert!(!is_structured_field("summary"));
assert!(!is_structured_field("detail"));
}
#[test]
fn test_parse_indented_list_basic() {
let input = " - item1\n - item2\n - item3\n";
let lines = lex(input).unwrap();
let mut pos = 0;
let items = parse_indented_list(&lines, &mut pos);
assert_eq!(items, vec!["item1", "item2", "item3"]);
assert_eq!(pos, 3);
}
#[test]
fn test_parse_indented_list_stops_at_non_list() {
let input = " - item1\nsummary: foo\n";
let lines = lex(input).unwrap();
let mut pos = 0;
let items = parse_indented_list(&lines, &mut pos);
assert_eq!(items, vec!["item1"]);
assert_eq!(pos, 1);
}
#[test]
fn test_parse_block_basic() {
let input = " This is block text.\n Second line.\n";
let lines = lex(input).unwrap();
let mut pos = 0;
let text = parse_block(&lines, &mut pos);
assert_eq!(text, "This is block text.\nSecond line.");
}
#[test]
fn test_parse_block_strips_base_indent() {
let input = " indented four\n second line\n";
let lines = lex(input).unwrap();
let mut pos = 0;
let text = parse_block(&lines, &mut pos);
assert_eq!(text, "indented four\nsecond line");
}
}