use std::collections::HashMap;
use http::Method;
use crate::request::VariableStore;
use crate::parser::declarations::reject_misplaced_declaration;
use crate::parser::redaction::reject_misplaced_redaction_directive;
use super::Rule;
pub(in crate::parser) struct ScannedRequest<'a> {
pub(in crate::parser) request_span: pest::Span<'a>,
pub(in crate::parser) store: VariableStore,
pub(in crate::parser) description_raw: String,
pub(in crate::parser) method: Option<Method>,
pub(in crate::parser) url_raw: Option<String>,
pub(in crate::parser) header_raw: HashMap<String, String>,
pub(in crate::parser) query_raw: Vec<(String, String)>,
pub(in crate::parser) cookie_raw: HashMap<String, String>,
pub(in crate::parser) form_text_raw: HashMap<String, String>,
pub(in crate::parser) form_file_raw: HashMap<String, String>,
pub(in crate::parser) protocol_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) session_name_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) auth_profile_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) operation_name_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) graphql_variables_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) mcp_call_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) mcp_tool_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) mcp_arguments_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) ws_send_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) sse_receive_seen: bool,
pub(in crate::parser) sse_within_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) timeout_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) poll_until_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) poll_every_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) mcp_protocol_version_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) mcp_client_name_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) mcp_client_version_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) mcp_capabilities_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) body_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) body_content_type_raw: Option<(String, pest::Span<'a>)>,
pub(in crate::parser) callback_src_raw: Vec<String>,
pub(in crate::parser) dependencies: Vec<String>,
pub(in crate::parser) dependency_raw: Vec<(String, pest::Span<'a>)>,
pub(in crate::parser) response_capture_raw: Vec<(String, pest::Span<'a>)>,
pub(in crate::parser) assertion_raw: Vec<(String, pest::Span<'a>, Option<String>)>,
pub(in crate::parser) fragment_includes_raw:
Vec<(String, Option<String>, usize, pest::Span<'a>)>,
pub(in crate::parser) fragment_guard_raw: Vec<(String, pest::Span<'a>)>,
}
impl<'a> ScannedRequest<'a> {
pub(in crate::parser) fn has_graphql_directives(&self) -> bool {
self.operation_name_raw.is_some() || self.graphql_variables_raw.is_some()
}
pub(in crate::parser) fn has_mcp_directives(&self) -> bool {
self.mcp_call_raw.is_some()
|| self.mcp_tool_raw.is_some()
|| self.mcp_arguments_raw.is_some()
|| self.mcp_protocol_version_raw.is_some()
|| self.mcp_client_name_raw.is_some()
|| self.mcp_client_version_raw.is_some()
|| self.mcp_capabilities_raw.is_some()
}
pub(in crate::parser) fn has_ws_directives(&self) -> bool {
self.ws_send_raw.is_some()
}
pub(in crate::parser) fn has_sse_directives(&self) -> bool {
self.sse_receive_seen || self.sse_within_raw.is_some()
}
pub(in crate::parser) fn body_seen(&self) -> bool {
self.body_raw.is_some()
}
}
pub(in crate::parser) fn scan_request_block<'a, F>(
pair: pest::iterators::Pair<'a, Rule>,
collection_store: &VariableStore,
initial_callbacks: Vec<String>,
mut assign_request_variable: F,
) -> Result<ScannedRequest<'a>, pest::error::Error<Rule>>
where
F: FnMut(
&mut VariableStore,
String,
&str,
pest::Span<'a>,
) -> Result<(), pest::error::Error<Rule>>,
{
let request_span = pair.as_span();
let mut pending_assertion_label: Option<(String, pest::Span<'a>)> = None;
let mut scanned = ScannedRequest {
request_span: request_span.clone(),
store: collection_store.clone(),
description_raw: String::new(),
method: None,
url_raw: None,
header_raw: HashMap::new(),
query_raw: Vec::new(),
cookie_raw: HashMap::new(),
form_text_raw: HashMap::new(),
form_file_raw: HashMap::new(),
protocol_raw: None,
session_name_raw: None,
auth_profile_raw: None,
operation_name_raw: None,
graphql_variables_raw: None,
mcp_call_raw: None,
mcp_tool_raw: None,
mcp_arguments_raw: None,
ws_send_raw: None,
sse_receive_seen: false,
sse_within_raw: None,
timeout_raw: None,
poll_until_raw: None,
poll_every_raw: None,
mcp_protocol_version_raw: None,
mcp_client_name_raw: None,
mcp_client_version_raw: None,
mcp_capabilities_raw: None,
body_raw: None,
body_content_type_raw: None,
callback_src_raw: initial_callbacks,
dependencies: Vec::new(),
dependency_raw: Vec::new(),
response_capture_raw: Vec::new(),
assertion_raw: Vec::new(),
fragment_includes_raw: Vec::new(),
fragment_guard_raw: Vec::new(),
};
for inner in pair.into_inner() {
if pending_assertion_label.is_some() && !matches!(inner.as_rule(), Rule::assertion) {
let (_, span) = pending_assertion_label.take().unwrap();
return Err(assertion_label_error(
span,
"assertion label must be followed by an assertion",
));
}
match inner.as_rule() {
Rule::description => {
reject_misplaced_declaration(inner.as_str(), inner.as_span())?;
reject_misplaced_redaction_directive(inner.as_str(), inner.as_span())?;
scanned.description_raw.push_str(inner.as_str().trim());
}
Rule::variable => {
let span = inner.as_span();
let mut inner_pairs = inner.into_inner();
let key = inner_pairs.next().unwrap().as_str().trim().to_string();
let raw_value = inner_pairs.next().unwrap().as_str().to_string();
assign_request_variable(&mut scanned.store, key, raw_value.as_str(), span)?;
}
Rule::protocol_directive => {
let span = inner.as_span();
let mut protocol_pairs = inner.into_inner();
scanned.protocol_raw = Some((
protocol_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::session_directive => {
let span = inner.as_span();
let mut session_pairs = inner.into_inner();
scanned.session_name_raw = Some((
session_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::auth_directive => {
let span = inner.as_span();
let mut auth_pairs = inner.into_inner();
scanned.auth_profile_raw = Some((
auth_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::http_method => {
scanned.method = Some(inner.as_str().parse().unwrap());
}
Rule::url => {
scanned.url_raw = Some(inner.as_str().trim().to_string());
}
Rule::header => {
let mut header_pairs = inner.into_inner();
let key = header_pairs.next().unwrap().as_str().trim().to_string();
let value = header_pairs.next().unwrap().as_str().trim().to_string();
scanned.header_raw.insert(key, value);
}
Rule::query => {
let mut query_pairs = inner.into_inner();
let key = query_pairs.next().unwrap().as_str().trim().to_string();
let value = query_pairs.next().unwrap().as_str().trim().to_string();
scanned.query_raw.push((key, value));
}
Rule::cookie => {
let mut cookie_pairs = inner.into_inner();
let key = cookie_pairs.next().unwrap().as_str().trim().to_string();
let value = cookie_pairs.next().unwrap().as_str().trim().to_string();
scanned.cookie_raw.insert(key, value);
}
Rule::operation_directive => {
let span = inner.as_span();
let mut operation_pairs = inner.into_inner();
scanned.operation_name_raw = Some((
operation_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::variables_directive => {
let span = inner.as_span();
let mut variables_pairs = inner.into_inner();
scanned.graphql_variables_raw = Some((
variables_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::call_directive => {
let span = inner.as_span();
let mut call_pairs = inner.into_inner();
scanned.mcp_call_raw = Some((
call_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::tool_directive => {
let span = inner.as_span();
let mut tool_pairs = inner.into_inner();
scanned.mcp_tool_raw = Some((
tool_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::arguments_directive => {
let span = inner.as_span();
let mut arguments_pairs = inner.into_inner();
scanned.mcp_arguments_raw = Some((
arguments_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::send_directive => {
let span = inner.as_span();
let mut send_pairs = inner.into_inner();
scanned.ws_send_raw = Some((
send_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::receive_directive => {
scanned.sse_receive_seen = true;
}
Rule::within_directive => {
let span = inner.as_span();
let mut within_pairs = inner.into_inner();
scanned.sse_within_raw = Some((
within_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::timeout_directive => {
let span = inner.as_span();
let mut timeout_pairs = inner.into_inner();
scanned.timeout_raw = Some((
timeout_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::poll_until_directive => {
let span = inner.as_span();
let mut poll_until_pairs = inner.into_inner();
scanned.poll_until_raw = Some((
poll_until_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::poll_every_directive => {
let span = inner.as_span();
let mut poll_every_pairs = inner.into_inner();
scanned.poll_every_raw = Some((
poll_every_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::protocol_version_directive => {
let span = inner.as_span();
let mut protocol_version_pairs = inner.into_inner();
scanned.mcp_protocol_version_raw = Some((
protocol_version_pairs
.next()
.unwrap()
.as_str()
.trim()
.to_string(),
span,
));
}
Rule::client_name_directive => {
let span = inner.as_span();
let mut client_name_pairs = inner.into_inner();
scanned.mcp_client_name_raw = Some((
client_name_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::client_version_directive => {
let span = inner.as_span();
let mut client_version_pairs = inner.into_inner();
scanned.mcp_client_version_raw = Some((
client_version_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::capabilities_directive => {
let span = inner.as_span();
let mut capabilities_pairs = inner.into_inner();
scanned.mcp_capabilities_raw = Some((
capabilities_pairs.next().unwrap().as_str().trim().to_string(),
span,
));
}
Rule::form => {
let mut form_pairs = inner.into_inner();
let key = form_pairs.next().unwrap().as_str().trim().to_string();
let value = form_pairs.next().unwrap();
match value.as_rule() {
Rule::file => {
let trimmed = value.as_str().trim_start_matches('@').trim().to_string();
scanned.form_file_raw.insert(key, trimmed);
}
Rule::text => {
scanned.form_text_raw.insert(key, value.as_str().to_string());
}
_ => unreachable!("unexpected rule: {:?}", value.as_rule()),
}
}
Rule::body => {
scanned.body_raw = Some((inner.as_str().to_string(), inner.as_span()));
}
Rule::body_content_type => {
scanned.body_content_type_raw =
Some((inner.as_str().trim().to_string(), inner.as_span()));
}
Rule::dependency => {
let dependency = parse_dependency(inner.as_str());
scanned.dependencies.push(dependency.clone());
scanned.dependency_raw.push((dependency, inner.as_span()));
}
Rule::callback => {
scanned
.callback_src_raw
.push(inner.as_str().strip_prefix('!').unwrap().to_string());
}
Rule::response_capture => {
scanned
.response_capture_raw
.push((inner.as_str().to_string(), inner.as_span()));
}
Rule::assertion_label => {
let label = inner
.as_str()
.trim()
.strip_prefix("^:")
.unwrap_or_default()
.trim()
.to_string();
if label.is_empty() {
return Err(assertion_label_error(
inner.as_span(),
"assertion label is empty",
));
}
pending_assertion_label = Some((label, inner.as_span()));
}
Rule::assertion => {
let label = pending_assertion_label.take().map(|(label, _)| label);
scanned
.assertion_raw
.push((inner.as_str().to_string(), inner.as_span(), label));
}
Rule::fragment_include => {
let raw = inner.as_str().to_string();
let (guard, path) = parse_fragment_include(raw.as_str());
if path.is_empty() {
return Err(pest::error::Error::<Rule>::new_from_span(
pest::error::ErrorVariant::CustomError {
message: "fragment include is missing a target".to_string(),
},
inner.as_span(),
));
}
if let Some(guard) = &guard {
scanned
.fragment_guard_raw
.push((guard.clone(), inner.as_span()));
}
scanned
.fragment_includes_raw
.push((path, guard, scanned.assertion_raw.len(), inner.as_span()));
}
_ => unreachable!("unexpected rule: {:?}", inner.as_rule()),
}
}
if let Some((_, span)) = pending_assertion_label {
return Err(assertion_label_error(
span,
"assertion label must be followed by an assertion",
));
}
Ok(scanned)
}
fn assertion_label_error<'a>(span: pest::Span<'a>, message: &str) -> pest::error::Error<Rule> {
pest::error::Error::<Rule>::new_from_span(
pest::error::ErrorVariant::CustomError {
message: message.to_string(),
},
span,
)
}
fn parse_fragment_include(raw: &str) -> (Option<String>, String) {
let trimmed = raw.trim();
if !trimmed.starts_with("<<") && !trimmed.starts_with('[') {
let path = trimmed.trim_start_matches("<<").trim().to_string();
return (None, path);
}
if let Some(remainder) = trimmed.strip_prefix("<<") {
return (None, remainder.trim().to_string());
}
let mut chars = trimmed.chars().peekable();
if chars.next() != Some('[') {
return (None, trimmed.trim().to_string());
}
let mut guard = String::new();
let mut in_single = false;
let mut in_double = false;
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if let Some(next) = chars.next() {
guard.push(next);
}
}
'\'' if !in_double => {
in_single = !in_single;
guard.push(ch);
}
'"' if !in_single => {
in_double = !in_double;
guard.push(ch);
}
']' if !in_single && !in_double => {
let remainder: String = chars.collect();
let remainder = remainder.trim();
let path = remainder.trim_start_matches("<<").trim().to_string();
return (Some(guard.trim().to_string()), path);
}
_ => guard.push(ch),
}
}
(None, trimmed.trim().to_string())
}
fn parse_dependency(raw: &str) -> String {
let remainder = raw
.strip_prefix('>')
.map(|s| s.trim())
.unwrap_or(raw.trim());
let remainder = remainder
.strip_prefix("requires")
.map(|s| s.trim_start())
.unwrap_or(remainder);
let remainder = remainder
.strip_prefix(':')
.map(|s| s.trim_start())
.unwrap_or(remainder);
let name = remainder.trim();
if name.starts_with('"') && name.ends_with('"') && name.len() >= 2 {
name[1..name.len() - 1].trim().to_string()
} else {
name.to_string()
}
}