use std::{
collections::HashMap,
path::PathBuf,
};
use http::Method;
use crate::{
request::{RequestProtocol, VariableStore},
schema::SchemaRegistry,
};
use super::{
Rule, context,
declarations::{
DeclarationSpanIndex, parse_request_collection, register_declaration,
remember_declaration_span, validate_schema_registry,
},
legacy_header::normalize_legacy_collection_header,
preprocessor,
protocol::{
SessionRequestTarget, ensure_session_protocol_compatible,
infer_request_protocol, infer_syntax_ws_send_kind,
is_graphql_document_content_type, remember_session_target,
resolve_request_target, syntax_protocol_context_json,
validate_graphql_variables_json, validate_mcp_object_json,
},
request::{
ScannedRequest, parse_assertions, parse_response_captures,
scan_request_block, validate_fragment_guards,
validate_graphql_request, validate_mcp_call,
validate_protocol_directives, validate_sse_action,
validate_ws_action,
},
spans::template_error_to_span,
variables::{
assign_global_variable_for_validation,
assign_request_variable_for_validation,
},
};
#[derive(Debug, Clone)]
pub struct SyntaxRequestSummary {
pub index: usize,
pub description: String,
pub method: String,
pub url: String,
pub protocol: String,
pub protocol_context: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct SyntaxSummary {
pub name: String,
pub description: String,
pub requests: Vec<SyntaxRequestSummary>,
}
pub fn inspect_collection_syntax(
input: &str,
working_dir: PathBuf,
) -> Result<SyntaxSummary, pest::error::Error<Rule>> {
let preprocessed = preprocessor::preprocess(input, &working_dir).map_err(|err| {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: err.to_string(),
},
pest::Span::new(input, 0, input.len()).unwrap(),
)
})?;
let preprocessed = normalize_legacy_collection_header(preprocessed.as_str());
let collection = parse_request_collection(preprocessed.as_str())?;
let mut name = String::new();
let mut description = String::new();
let mut requests = Vec::new();
let mut global_store = VariableStore::new();
let mut schema_registry = SchemaRegistry::default();
let mut declaration_spans = DeclarationSpanIndex::new();
let mut session_targets: HashMap<String, SessionRequestTarget> = HashMap::new();
for pair in collection.into_inner() {
match pair.as_rule() {
Rule::collection_name => {
name = pair.as_str().trim().to_string();
}
Rule::collection_description => {
description.push_str(pair.as_str().trim());
}
Rule::variable => {
let span = pair.as_span();
let mut inner_pairs = pair.into_inner();
let key = inner_pairs.next().unwrap().as_str().trim().to_string();
let value = inner_pairs.next().unwrap().as_str().to_string();
assign_global_variable_for_validation(&mut global_store, key, value.as_str())
.map_err(|err| template_error_to_span(err, span.clone()))?;
}
Rule::header | Rule::query | Rule::callback => {}
Rule::declaration => {
remember_declaration_span(&mut declaration_spans, &pair);
register_declaration(&mut schema_registry, pair)?;
}
Rule::requests => {
validate_schema_registry(
&schema_registry,
preprocessed.as_str(),
&declaration_spans,
)?;
for (index, request_pair) in pair.into_inner().enumerate() {
requests.push(inspect_request_syntax(
index,
request_pair,
&global_store,
&schema_registry,
&mut session_targets,
)?);
}
}
_ => {}
}
}
Ok(SyntaxSummary {
name,
description,
requests,
})
}
fn inspect_request_syntax(
index: usize,
pair: pest::iterators::Pair<Rule>,
collection_store: &VariableStore,
schema_registry: &SchemaRegistry,
session_targets: &mut HashMap<String, SessionRequestTarget>,
) -> Result<SyntaxRequestSummary, pest::error::Error<Rule>> {
let scanned_request = scan_request_block(
pair,
collection_store,
Vec::new(),
|store, key, raw_value, span| {
assign_request_variable_for_validation(store, key, raw_value)
.map_err(|err| template_error_to_span(err, span))
},
)?;
let has_graphql_directives = scanned_request.has_graphql_directives();
let has_mcp_directives = scanned_request.has_mcp_directives();
let has_ws_directives = scanned_request.has_ws_directives();
let has_sse_directives = scanned_request.has_sse_directives();
let body_seen = scanned_request.body_seen();
let ScannedRequest {
request_span,
store,
description_raw: description,
method,
url_raw: url,
header_raw: _,
query_raw: _,
form_text_raw,
form_file_raw,
protocol_raw,
session_name_raw,
operation_name_raw,
graphql_variables_raw,
mcp_call_raw,
mcp_tool_raw,
mcp_arguments_raw,
ws_send_raw,
sse_receive_seen,
sse_within_raw,
mcp_protocol_version_raw,
mcp_client_name_raw,
mcp_client_version_raw,
mcp_capabilities_raw,
body_raw: _,
body_content_type_raw,
callback_src_raw: _,
dependencies: _,
response_capture_raw,
assertion_raw,
fragment_includes_raw: _,
fragment_guard_raw,
} = scanned_request;
let scalar_map = store.clone_scalars();
let session_name = session_name_raw
.as_ref()
.map(|(raw, _)| context::inject_from_variable(raw.as_str(), &scalar_map));
let protocol = infer_request_protocol(
protocol_raw
.as_ref()
.map(|(protocol, span)| (protocol.as_str(), span.clone())),
session_name.as_deref(),
has_mcp_directives,
has_ws_directives,
has_sse_directives,
session_targets,
)?;
ensure_session_protocol_compatible(
session_name.as_deref(),
protocol,
session_targets,
request_span.clone(),
)?;
validate_protocol_directives(
protocol,
has_graphql_directives,
has_mcp_directives,
has_ws_directives,
has_sse_directives,
request_span.clone(),
)?;
let ws_send_kind = if matches!(protocol, RequestProtocol::Ws) {
infer_syntax_ws_send_kind(
request_span.clone(),
ws_send_raw
.as_ref()
.map(|(raw, span)| (raw.as_str(), span.clone())),
body_content_type_raw
.as_ref()
.map(|(raw, span)| (raw.as_str(), span.clone())),
body_seen,
&scalar_map,
)?
} else {
None
};
let (method, url) = resolve_request_target(
method,
url,
session_name.as_deref(),
protocol,
session_targets,
request_span.clone(),
)?;
if matches!(protocol, RequestProtocol::Graphql) {
validate_graphql_request(
request_span.clone(),
method.clone(),
!form_text_raw.is_empty() || !form_file_raw.is_empty(),
body_seen,
body_content_type_raw
.as_ref()
.map(|(value, _)| is_graphql_document_content_type(value.as_str()))
.unwrap_or(false),
body_content_type_raw
.as_ref()
.map(|(_, span)| span.clone())
.unwrap_or_else(|| request_span.clone()),
|| {
if let Some((raw, span)) = graphql_variables_raw.as_ref() {
validate_graphql_variables_json(raw.as_str(), span.clone())?;
}
Ok(())
},
)?;
}
if matches!(protocol, RequestProtocol::Mcp) {
if method != Method::POST {
return Err(pest::error::Error::<Rule>::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "MCP-over-HTTP requests currently require POST".to_string(),
},
request_span.clone(),
));
}
if body_seen || body_content_type_raw.is_some() {
let span = body_content_type_raw
.as_ref()
.map(|(_, span)| span.clone())
.unwrap_or_else(|| request_span.clone());
return Err(pest::error::Error::<Rule>::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "MCP-over-HTTP requests do not support explicit body or content type blocks".to_string(),
},
span,
));
}
validate_mcp_call(
request_span.clone(),
mcp_call_raw.as_ref().map(|(value, _)| value.as_str()),
mcp_tool_raw.is_some(),
mcp_arguments_raw
.as_ref()
.map(|(value, span)| (value.as_str(), span.clone())),
mcp_protocol_version_raw.is_some(),
mcp_client_name_raw.is_some(),
mcp_client_version_raw.is_some(),
mcp_capabilities_raw
.as_ref()
.map(|(value, span)| (value.as_str(), span.clone())),
mcp_call_raw
.as_ref()
.map(|(_, span)| span.clone())
.unwrap_or_else(|| request_span.clone()),
|raw, field, span| validate_mcp_object_json(raw, field, span),
)?;
}
if matches!(protocol, RequestProtocol::Sse) {
validate_sse_action(
request_span.clone(),
session_name.is_some(),
method.clone(),
(body_seen || body_content_type_raw.is_some()).then(|| {
(
"SSE requests do not support explicit body or content type blocks",
body_content_type_raw
.as_ref()
.map(|(_, span)| span.clone())
.unwrap_or_else(|| request_span.clone()),
)
}),
sse_receive_seen,
sse_within_raw
.as_ref()
.map(|(within, span)| (within.as_str(), span.clone())),
sse_within_raw
.as_ref()
.map(|(_, span)| span.clone())
.unwrap_or_else(|| request_span.clone()),
)?;
}
if matches!(protocol, RequestProtocol::Ws) {
validate_ws_action(
request_span.clone(),
session_name.is_some(),
method.clone(),
None,
sse_receive_seen,
ws_send_raw.is_some(),
ws_send_raw
.as_ref()
.map(|(_, span)| span.clone())
.unwrap_or_else(|| request_span.clone()),
ws_send_kind,
body_seen,
body_content_type_raw
.as_ref()
.map(|(_, span)| span.clone())
.or_else(|| ws_send_raw.as_ref().map(|(_, span)| span.clone()))
.unwrap_or_else(|| request_span.clone()),
(body_seen || body_content_type_raw.is_some()).then(|| {
(
if sse_receive_seen {
"WebSocket receive steps do not support explicit body or content type blocks"
} else {
"WebSocket open steps do not support explicit body or content type blocks"
},
body_content_type_raw
.as_ref()
.map(|(_, span)| span.clone())
.unwrap_or_else(|| request_span.clone()),
)
}),
sse_within_raw
.as_ref()
.map(|(within, span)| (within.as_str(), span.clone())),
)?;
}
let _ = parse_response_captures(response_capture_raw, &scalar_map)?;
let _ = parse_assertions(assertion_raw, &scalar_map, schema_registry)?;
validate_fragment_guards(fragment_guard_raw, &scalar_map, schema_registry)?;
let description = if description.trim().is_empty() {
"[No Description]".to_string()
} else {
description.trim().to_string()
};
let protocol_context = syntax_protocol_context_json(
protocol,
session_name.as_deref(),
operation_name_raw.as_ref().map(|(value, _)| value.as_str()),
graphql_variables_raw.as_ref().map(|(value, _)| value.as_str()),
mcp_call_raw.as_ref().map(|(value, _)| value.as_str()),
mcp_tool_raw.as_ref().map(|(value, _)| value.as_str()),
mcp_arguments_raw.as_ref().map(|(value, _)| value.as_str()),
mcp_protocol_version_raw
.as_ref()
.map(|(value, _)| value.as_str()),
mcp_client_name_raw.as_ref().map(|(value, _)| value.as_str()),
mcp_client_version_raw
.as_ref()
.map(|(value, _)| value.as_str()),
mcp_capabilities_raw.as_ref().map(|(value, _)| value.as_str()),
sse_receive_seen,
ws_send_kind,
sse_within_raw.as_ref().map(|(value, _)| value.as_str()),
);
remember_session_target(
session_targets,
session_name.as_deref(),
protocol,
&method,
&url,
);
Ok(SyntaxRequestSummary {
index,
description,
method: method.as_str().to_string(),
url,
protocol: protocol.as_str().to_string(),
protocol_context,
})
}