Skip to main content

openapi_nexus/parser/
parser.rs

1//! OpenAPI specification parser
2
3use std::fs;
4use std::path::Path;
5
6use tracing::{debug, error};
7
8use super::error::ParseError;
9use super::serde_error::SerdeErrorExtractor;
10use crate::ParsedSpec;
11use crate::spec::{OpenApiV30Spec, OpenApiV31Spec, OpenApiV32Spec};
12
13fn extract_error_context(content: &str, error_msg: &str) -> Vec<String> {
14    let (line, column) = SerdeErrorExtractor::new(error_msg).extract_location();
15
16    if line > 0 {
17        debug!("Error at line {}, column {}", line, column);
18
19        let lines: Vec<&str> = content.lines().collect();
20        if line <= lines.len() {
21            let error_line_idx = line - 1;
22            let start_line = error_line_idx.saturating_sub(5);
23            let end_line = (error_line_idx + 10).min(lines.len());
24
25            debug!(
26                "Raw content around error (lines {} to {}):",
27                start_line + 1,
28                end_line
29            );
30            for (i, line_content) in lines.iter().enumerate().take(end_line).skip(start_line) {
31                let line_num = i + 1;
32                let is_error_line = line_num == line;
33                let marker = if is_error_line { ">>>" } else { "   " };
34                debug!("{} {} | {}", marker, line_num, line_content);
35            }
36
37            if error_line_idx + 1 < lines.len() {
38                let next_line = lines[error_line_idx + 1];
39                debug!("Next line after error: {}", next_line);
40                debug!(
41                    "Next line indentation: {} spaces",
42                    next_line.chars().take_while(|c| *c == ' ').count()
43                );
44            }
45
46            let error_line = lines[line - 1];
47            vec![
48                format!("Error at line {}: {}", line, error_line),
49                format!("Column: {}", column),
50            ]
51        } else {
52            vec![format!("Error: {}", error_msg)]
53        }
54    } else {
55        vec![format!("Error: {}", error_msg)]
56    }
57}
58
59/// Detect the OpenAPI version from a JSON value.
60fn detect_version_json(value: &serde_json::Value) -> Result<&str, ParseError> {
61    value
62        .get("openapi")
63        .and_then(|v| v.as_str())
64        .ok_or(ParseError::MissingVersionField)
65}
66
67/// Detect the OpenAPI version from a YAML value.
68fn detect_version_yaml(value: &serde_norway::Value) -> Result<String, ParseError> {
69    let mapping = value.as_mapping().ok_or(ParseError::MissingVersionField)?;
70    for (k, v) in mapping {
71        if k.as_str() == Some("openapi") {
72            if let Some(s) = v.as_str() {
73                return Ok(s.to_string());
74            }
75            // Could be a number like 3.0 (YAML parses unquoted 3.0 as float)
76            if let Some(f) = v.as_f64() {
77                // Preserve at least one decimal place: 3.0 → "3.0", not "3"
78                let s = format!("{f}");
79                if s.contains('.') {
80                    return Ok(s);
81                }
82                return Ok(format!("{f:.1}"));
83            }
84        }
85    }
86    Err(ParseError::MissingVersionField)
87}
88
89/// Classify a version string into a major.minor bucket.
90fn classify_version(version: &str) -> Result<OpenApiMajorMinor, ParseError> {
91    if version.starts_with("3.0") {
92        Ok(OpenApiMajorMinor::V3_0)
93    } else if version.starts_with("3.1") {
94        Ok(OpenApiMajorMinor::V3_1)
95    } else if version.starts_with("3.2") {
96        Ok(OpenApiMajorMinor::V3_2)
97    } else {
98        Err(ParseError::UnsupportedVersion {
99            version: version.to_string(),
100        })
101    }
102}
103
104enum OpenApiMajorMinor {
105    V3_0,
106    V3_1,
107    V3_2,
108}
109
110pub fn parse_content_json(content: &str) -> Result<ParsedSpec, ParseError> {
111    // First parse as generic JSON to get error context and version
112    let value: serde_json::Value = serde_json::from_str(content).map_err(|e| {
113        let error_msg = e.to_string();
114        debug!("Serde error message: {}", error_msg);
115        let context = extract_error_context(content, &error_msg);
116        for line in &context {
117            error!("{}", line);
118        }
119        ParseError::JsonParse { source: e, context }
120    })?;
121
122    let version = detect_version_json(&value)?;
123    match classify_version(version)? {
124        OpenApiMajorMinor::V3_0 => {
125            let spec: OpenApiV30Spec = serde_json::from_value(value).map_err(|e| {
126                debug!("parse error: {}", e);
127                ParseError::OpenApiDeserializeJson { source: e }
128            })?;
129            Ok(ParsedSpec::V30(Box::new(spec)))
130        }
131        OpenApiMajorMinor::V3_1 => {
132            let spec: OpenApiV31Spec = serde_json::from_value(value).map_err(|e| {
133                debug!("parse error: {}", e);
134                ParseError::OpenApiDeserializeJson { source: e }
135            })?;
136            Ok(ParsedSpec::V31(Box::new(spec)))
137        }
138        OpenApiMajorMinor::V3_2 => {
139            let spec: OpenApiV32Spec = serde_json::from_value(value).map_err(|e| {
140                debug!("parse error: {}", e);
141                ParseError::OpenApiDeserializeJson { source: e }
142            })?;
143            Ok(ParsedSpec::V32(Box::new(spec)))
144        }
145    }
146}
147
148pub fn parse_content_yaml(content: &str) -> Result<ParsedSpec, ParseError> {
149    // First parse as generic YAML
150    let value: serde_norway::Value = serde_norway::from_str(content).map_err(|e| {
151        let error_msg = e.to_string();
152        debug!("Serde error message: {}", error_msg);
153        let context = extract_error_context(content, &error_msg);
154        for line in &context {
155            error!("{}", line);
156        }
157        ParseError::YamlParse { source: e, context }
158    })?;
159
160    let version = detect_version_yaml(&value)?;
161    match classify_version(&version)? {
162        OpenApiMajorMinor::V3_0 => {
163            let spec: OpenApiV30Spec = serde_norway::from_value(value).map_err(|e| {
164                debug!("parse error: {}", e);
165                ParseError::OpenApiDeserializeYaml { source: e }
166            })?;
167            Ok(ParsedSpec::V30(Box::new(spec)))
168        }
169        OpenApiMajorMinor::V3_1 => {
170            let spec: OpenApiV31Spec = serde_norway::from_value(value).map_err(|e| {
171                debug!("parse error: {}", e);
172                ParseError::OpenApiDeserializeYaml { source: e }
173            })?;
174            Ok(ParsedSpec::V31(Box::new(spec)))
175        }
176        OpenApiMajorMinor::V3_2 => {
177            let spec: OpenApiV32Spec = serde_norway::from_value(value).map_err(|e| {
178                debug!("parse error: {}", e);
179                ParseError::OpenApiDeserializeYaml { source: e }
180            })?;
181            Ok(ParsedSpec::V32(Box::new(spec)))
182        }
183    }
184}
185
186/// Parse an OpenAPI specification from a file
187pub fn parse_file(path: &Path) -> Result<ParsedSpec, ParseError> {
188    let content = fs::read_to_string(path).map_err(|e| ParseError::FileRead {
189        path: path.to_string_lossy().to_string(),
190        source: e,
191    })?;
192
193    let file_extension = path.extension().and_then(|ext| ext.to_str());
194
195    match file_extension {
196        Some("json") => parse_content_json(&content),
197        Some("yaml") | Some("yml") => parse_content_yaml(&content),
198        Some(ext) => Err(ParseError::UnsupportedFormat {
199            format: ext.to_string(),
200        }),
201        None => Err(ParseError::UnsupportedFormat {
202            format: "<unknown>".to_string(),
203        }),
204    }
205}
206
207/// Parse content as an OpenAPI v3.1 spec specifically (for callers that know the version).
208pub fn parse_content_yaml_v31(content: &str) -> Result<OpenApiV31Spec, ParseError> {
209    let _value: serde_norway::Value = serde_norway::from_str(content).map_err(|e| {
210        let error_msg = e.to_string();
211        let context = extract_error_context(content, &error_msg);
212        ParseError::YamlParse { source: e, context }
213    })?;
214
215    serde_norway::from_str::<OpenApiV31Spec>(content)
216        .map_err(|e| ParseError::OpenApiDeserializeYaml { source: e })
217}
218
219/// Parse content as an OpenAPI v3.1 JSON spec specifically.
220pub fn parse_content_json_v31(content: &str) -> Result<OpenApiV31Spec, ParseError> {
221    let _value: serde_json::Value = serde_json::from_str(content).map_err(|e| {
222        let error_msg = e.to_string();
223        let context = extract_error_context(content, &error_msg);
224        ParseError::JsonParse { source: e, context }
225    })?;
226
227    serde_json::from_str::<OpenApiV31Spec>(content)
228        .map_err(|e| ParseError::OpenApiDeserializeJson { source: e })
229}