1use std::sync::LazyLock;
4
5use regex::Regex;
6
7use crate::error::{AgmError, ErrorCode, ErrorLocation};
8use crate::model::file::Header;
9
10use super::fields::{
11 FieldTracker, parse_block, parse_imports, parse_indented_list, skip_field_body,
12};
13use super::lexer::{Line, LineKind};
14use super::structured::parse_load_profiles;
15
16static AGM_VERSION_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]+\.[0-9]+$").unwrap());
21
22static PACKAGE_RE: LazyLock<Regex> =
23 LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$").unwrap());
24
25fn validate_agm_format(value: &str, line_number: usize, errors: &mut Vec<AgmError>) {
30 if !AGM_VERSION_RE.is_match(value) {
31 errors.push(AgmError::new(
32 ErrorCode::P001,
33 format!("Invalid `agm` format: expected MAJOR.MINOR, got {value:?}"),
34 ErrorLocation::new(None, Some(line_number), None),
35 ));
36 }
37}
38
39fn validate_package_format(value: &str, line_number: usize, errors: &mut Vec<AgmError>) {
40 if !PACKAGE_RE.is_match(value) {
41 errors.push(AgmError::new(
42 ErrorCode::P001,
43 format!("Invalid `package` format: {value:?}"),
44 ErrorLocation::new(None, Some(line_number), None),
45 ));
46 }
47}
48
49fn validate_version_format(value: &str, line_number: usize, errors: &mut Vec<AgmError>) {
50 if semver::Version::parse(value).is_err() {
51 errors.push(AgmError::new(
52 ErrorCode::P001,
53 format!("Invalid `version` (expected semver): {value:?}"),
54 ErrorLocation::new(None, Some(line_number), None),
55 ));
56 }
57}
58
59pub fn parse_header(lines: &[Line], pos: &mut usize, errors: &mut Vec<AgmError>) -> Header {
68 let mut tracker = FieldTracker::new();
69
70 let mut agm: Option<String> = None;
71 let mut package: Option<String> = None;
72 let mut version: Option<String> = None;
73 let mut title: Option<String> = None;
74 let mut owner: Option<String> = None;
75 let mut default_load: Option<String> = None;
76 let mut description: Option<String> = None;
77 let mut status: Option<String> = None;
78 let mut target_runtime: Option<String> = None;
79 let mut tags: Option<Vec<String>> = None;
80 let mut imports_raw: Option<Vec<crate::model::imports::ImportEntry>> = None;
81 let mut load_profiles_parsed: Option<
82 std::collections::BTreeMap<String, crate::model::file::LoadProfile>,
83 > = None;
84
85 while *pos < lines.len() {
86 match &lines[*pos].kind.clone() {
87 LineKind::NodeDeclaration(_) => break,
88
89 LineKind::Blank | LineKind::Comment | LineKind::TestExpectHeader(_) => {
90 *pos += 1;
91 }
92
93 LineKind::ScalarField(key, value) => {
94 let key = key.clone();
95 let value = value.clone();
96 let line_number = lines[*pos].number;
97
98 if tracker.track(&key) {
99 errors.push(AgmError::new(
100 ErrorCode::P006,
101 format!("Duplicate field `{key}` in header"),
102 ErrorLocation::new(None, Some(line_number), None),
103 ));
104 *pos += 1;
105 continue;
106 }
107
108 match key.as_str() {
109 "agm" => {
110 validate_agm_format(&value, line_number, errors);
111 agm = Some(value);
112 }
113 "package" => {
114 validate_package_format(&value, line_number, errors);
115 package = Some(value);
116 }
117 "version" => {
118 validate_version_format(&value, line_number, errors);
119 version = Some(value);
120 }
121 "title" => title = Some(value),
122 "owner" => owner = Some(value),
123 "default_load" => default_load = Some(value),
124 "description" => description = Some(value),
125 "status" => status = Some(value),
126 "target_runtime" => target_runtime = Some(value),
127 _ => {} }
129 *pos += 1;
130 }
131
132 LineKind::InlineListField(key, items) => {
133 let key = key.clone();
134 let items = items.clone();
135 let line_number = lines[*pos].number;
136
137 if tracker.track(&key) {
138 errors.push(AgmError::new(
139 ErrorCode::P006,
140 format!("Duplicate field `{key}` in header"),
141 ErrorLocation::new(None, Some(line_number), None),
142 ));
143 *pos += 1;
144 continue;
145 }
146
147 match key.as_str() {
148 "tags" => tags = Some(items),
149 "imports" => {
150 imports_raw = Some(parse_imports(&items, line_number, errors));
151 }
152 _ => {} }
154 *pos += 1;
155 }
156
157 LineKind::FieldStart(key) => {
158 let key = key.clone();
159 let line_number = lines[*pos].number;
160
161 if tracker.track(&key) {
162 errors.push(AgmError::new(
163 ErrorCode::P006,
164 format!("Duplicate field `{key}` in header"),
165 ErrorLocation::new(None, Some(line_number), None),
166 ));
167 *pos += 1;
168 skip_field_body(lines, pos);
169 continue;
170 }
171
172 *pos += 1; match key.as_str() {
175 "description" => {
176 description = Some(parse_block(lines, pos));
177 }
178 "tags" => {
179 tags = Some(parse_indented_list(lines, pos));
180 }
181 "imports" => {
182 let raw_items = parse_indented_list(lines, pos);
183 imports_raw = Some(parse_imports(&raw_items, line_number, errors));
184 }
185 "load_profiles" => {
186 load_profiles_parsed = Some(parse_load_profiles(lines, pos, errors));
187 }
188 _ => {
189 skip_field_body(lines, pos);
190 }
191 }
192 }
193
194 LineKind::BodyMarker => {
195 *pos += 1;
197 }
198
199 LineKind::ListItem(_) | LineKind::IndentedLine(_) => {
200 errors.push(AgmError::new(
201 ErrorCode::P003,
202 format!(
203 "Unexpected indentation in header at line {}",
204 lines[*pos].number
205 ),
206 ErrorLocation::new(None, Some(lines[*pos].number), None),
207 ));
208 *pos += 1;
209 }
210 }
211 }
212
213 for field in ["agm", "package", "version"] {
215 let is_missing = match field {
216 "agm" => agm.is_none(),
217 "package" => package.is_none(),
218 "version" => version.is_none(),
219 _ => false,
220 };
221 if is_missing {
222 errors.push(AgmError::new(
223 ErrorCode::P001,
224 format!("Missing required header field: '{field}'"),
225 ErrorLocation::new(None, None, None),
226 ));
227 }
228 }
229
230 Header {
231 agm: agm.unwrap_or_default(),
232 package: package.unwrap_or_default(),
233 version: version.unwrap_or_default(),
234 title,
235 owner,
236 imports: imports_raw,
237 default_load,
238 description,
239 tags,
240 status,
241 load_profiles: load_profiles_parsed,
242 target_runtime,
243 }
244}