use std::path::Path;
use pest_derive::Parser;
use serde_json::{Value, json};
use crate::error::{
HenDiagnostic, HenDiagnosticPosition, HenDiagnosticRange,
HenDiagnosticSuggestion, HenError, HenErrorKind,
};
mod collection;
pub mod context;
mod declarations;
mod legacy_header;
mod oauth;
mod preprocessor;
mod protocol;
mod redaction;
mod request;
mod spans;
mod syntax;
mod variables;
pub use collection::parse_collection;
pub use collection::parse_collection_with_environment;
pub use syntax::{
SyntaxDeclarationSummary, SyntaxFragmentIncludeSummary,
SyntaxInspectRequestSummary, SyntaxInspectResult,
SyntaxPositionSummary, SyntaxRangeSummary, SyntaxSummary,
SyntaxSymbolTable, SyntaxRequestSummary, inspect_collection_editor_support,
inspect_collection_syntax,
inspect_collection_syntax_tolerant,
};
pub(crate) use variables::{SecretValueCache, resolve_runtime_scalar};
pub use variables::eval_shell_script;
#[derive(Parser)]
#[grammar = "src/parser/grammar.pest"]
struct CollectionParser;
pub fn preprocess_only(input: &str, working_dir: &Path) -> Result<String, String> {
preprocessor::preprocess(input, working_dir).map_err(|err| err.to_string())
}
pub(crate) fn parse_error_to_hen_error(
input: &str,
working_dir: &Path,
error: pest::error::Error<Rule>,
path: Option<&Path>,
) -> HenError {
if let pest::error::ErrorVariant::CustomError { message } = &error.variant {
if let Some(diagnostic) = preprocessor::structured_diagnostic_for_message(
input,
working_dir,
message,
path,
) {
return HenError::new(HenErrorKind::Parse, "Failed to parse hen file")
.with_diagnostic(diagnostic)
.with_detail(error.to_string());
}
if let Some(diagnostic) = suggested_reference_diagnostic_for_message(
input,
working_dir,
&error,
message,
path,
) {
return HenError::new(HenErrorKind::Parse, "Failed to parse hen file")
.with_diagnostic(diagnostic)
.with_detail(error.to_string());
}
}
if let Some(diagnostic) = declarations::structured_diagnostic_for_parse_error(
input,
working_dir,
&error,
path,
) {
return HenError::new(HenErrorKind::Parse, "Failed to parse hen file")
.with_diagnostic(diagnostic)
.with_detail(error.to_string());
}
HenError::from_pest_error(error, path)
}
fn suggested_reference_diagnostic_for_message(
input: &str,
working_dir: &Path,
error: &pest::error::Error<Rule>,
message: &str,
path: Option<&Path>,
) -> Option<HenDiagnostic> {
let preprocessed = preprocessor::preprocess(input, working_dir).ok()?;
let preprocessed = legacy_header::normalize_legacy_collection_header(preprocessed.as_str());
let collection = declarations::parse_request_collection(preprocessed.as_str()).ok()?;
if let Some(missing_oauth_profile) = message
.strip_prefix("request references unknown OAuth profile '")
.and_then(|value| value.strip_suffix("'."))
{
let available_profiles = oauth_profile_names(collection.clone());
if available_profiles.is_empty() {
return None;
}
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
add_available_names_data(&mut diagnostic, &available_profiles);
let suggestion_range = diagnostic.location.range.clone();
diagnostic.suggestions = available_profiles
.iter()
.take(3)
.map(|name| HenDiagnosticSuggestion {
kind: "replaceRange".to_string(),
label: format!("Replace with '{}'", name),
range: Some(suggestion_range.clone()),
text: Some(name.clone()),
})
.collect();
if let Some(Value::Object(map)) = diagnostic.data.as_mut() {
map.insert("symbolName".to_string(), json!(missing_oauth_profile));
}
return Some(diagnostic);
}
if let Some((request_name, missing_dependency)) = unknown_dependency_details(message) {
let mut available_requests = request_names(collection.clone());
available_requests.retain(|name| name != &request_name);
if available_requests.is_empty() {
return None;
}
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
add_available_names_data(&mut diagnostic, &available_requests);
let suggestion_range = diagnostic.location.range.clone();
diagnostic.suggestions = available_requests
.iter()
.take(3)
.map(|name| HenDiagnosticSuggestion {
kind: "replaceRange".to_string(),
label: format!("Replace with '{}'", name),
range: Some(suggestion_range.clone()),
text: Some(name.clone()),
})
.collect();
if let Some(Value::Object(map)) = diagnostic.data.as_mut() {
map.insert("ownerName".to_string(), json!(request_name));
map.insert("symbolName".to_string(), json!(missing_dependency));
}
return Some(diagnostic);
}
if let Some(missing_target) = message
.strip_prefix("Unknown schema validation target '")
.and_then(|value| value.strip_suffix('\''))
{
let available_targets = declaration_names(collection);
if available_targets.is_empty() {
return None;
}
let mut diagnostic = HenDiagnostic::from_pest_error(error, path);
add_available_names_data(&mut diagnostic, &available_targets);
let suggestion_range = schema_target_suggestion_range(
preprocessed.as_str(),
diagnostic.location.range.start.line,
missing_target,
)
.unwrap_or_else(|| diagnostic.location.range.clone());
diagnostic.suggestions = available_targets
.iter()
.take(3)
.map(|name| HenDiagnosticSuggestion {
kind: "replaceRange".to_string(),
label: format!("Replace with '{}'", name),
range: Some(suggestion_range.clone()),
text: Some(name.clone()),
})
.collect();
if let Some(Value::Object(map)) = diagnostic.data.as_mut() {
map.insert("symbolName".to_string(), json!(missing_target));
}
return Some(diagnostic);
}
None
}
fn oauth_profile_names(collection: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
let mut names = Vec::new();
for pair in collection.into_inner() {
if pair.as_rule() != Rule::oauth_block {
continue;
}
let Some(name) = pair
.into_inner()
.next()
.map(|value| value.as_str().trim().to_string())
else {
continue;
};
names.push(name);
}
names.sort();
names.dedup();
names
}
fn request_names(collection: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
let mut names = Vec::new();
for pair in collection.into_inner() {
if pair.as_rule() != Rule::requests {
continue;
}
for request in pair.into_inner() {
let mut description = String::new();
for item in request.into_inner() {
if item.as_rule() == Rule::description {
description.push_str(item.as_str().trim());
}
}
if description.trim().is_empty() {
names.push("[No Description]".to_string());
} else {
names.push(description.trim().to_string());
}
}
}
names.sort();
names.dedup();
names
}
fn declaration_names(collection: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
let mut names = Vec::new();
for pair in collection.into_inner() {
if pair.as_rule() != Rule::declaration {
continue;
}
let Some(name) = pair
.into_inner()
.next()
.and_then(|value| value.into_inner().next())
.map(|value| value.as_str().trim().to_string())
else {
continue;
};
names.push(name);
}
names.sort();
names.dedup();
names
}
fn unknown_dependency_details(message: &str) -> Option<(String, String)> {
let remainder = message.strip_prefix("Request '")?;
let (request, remainder) = remainder.split_once("' declares unknown dependency '")?;
let dependency = remainder.strip_suffix("'.")?;
Some((request.to_string(), dependency.to_string()))
}
fn schema_target_suggestion_range(
source: &str,
line: usize,
target: &str,
) -> Option<HenDiagnosticRange> {
let line_text = source.lines().nth(line)?;
let operator_index = line_text.rfind("===")?;
let after_operator = &line_text[operator_index + 3..];
let leading_whitespace = after_operator.len() - after_operator.trim_start().len();
let search_start = operator_index + 3 + leading_whitespace;
let search_space = &line_text[search_start..];
let relative_start = if search_space.starts_with(target) {
0
} else {
search_space.find(target)?
};
let start_character = search_start + relative_start;
let end_character = start_character + target.len();
Some(HenDiagnosticRange {
start: HenDiagnosticPosition {
line,
character: start_character,
},
end: HenDiagnosticPosition {
line,
character: end_character,
},
})
}
fn add_available_names_data(diagnostic: &mut HenDiagnostic, available_names: &[String]) {
match diagnostic.data.as_mut() {
Some(Value::Object(map)) => {
map.insert("availableNames".to_string(), json!(available_names));
}
_ => {
diagnostic.data = Some(json!({
"availableNames": available_names,
}));
}
}
}
#[cfg(test)]
mod tests;