use std::collections::{HashMap, HashSet};
use crate::request::{OAuthFieldMapping, OAuthProfile};
use super::Rule;
const ALLOWED_OAUTH_FIELDS: &[&str] = &[
"grant",
"issuer",
"token_url",
"client_id",
"client_secret",
"scope",
"refresh_token",
];
pub(super) fn parse_oauth_block(
pair: pest::iterators::Pair<Rule>,
) -> Result<OAuthProfile, pest::error::Error<Rule>> {
let span = pair.as_span();
let mut inner = pair.into_inner();
let name = inner
.next()
.expect("oauth block should include a name")
.as_str()
.trim()
.to_string();
let mut fields = HashMap::new();
let mut params = HashMap::new();
let mut field_mappings = Vec::new();
let mut mapped_source_fields = HashSet::new();
let mut mapped_target_variables = HashSet::new();
for item in inner {
match item.as_rule() {
Rule::oauth_assignment => {
let item_span = item.as_span();
let mut parts = item.into_inner();
let key = parts
.next()
.expect("oauth assignment should include a key")
.as_str()
.trim()
.to_string();
let value = parts
.next()
.expect("oauth assignment should include a value")
.as_str()
.trim()
.to_string();
if !ALLOWED_OAUTH_FIELDS.iter().any(|candidate| candidate == &key) {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' uses unsupported field '{}'. Supported fields are: {}.",
name,
key,
ALLOWED_OAUTH_FIELDS.join(", ")
),
},
item_span,
));
}
if fields.insert(key.clone(), value).is_some() {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' defines field '{}' more than once.",
name, key
),
},
item_span,
));
}
}
Rule::oauth_param => {
let item_span = item.as_span();
let mut parts = item.into_inner();
let key = parts
.next()
.expect("oauth param should include a key")
.as_str()
.trim()
.to_string();
let value = parts
.next()
.expect("oauth param should include a value")
.as_str()
.trim()
.to_string();
if params.insert(key.clone(), value).is_some() {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' defines param '{}' more than once.",
name, key
),
},
item_span,
));
}
}
Rule::oauth_field_mapping => {
let item_span = item.as_span();
let mut parts = item.into_inner();
let source_field = parts
.next()
.expect("oauth field mapping should include a source field")
.as_str()
.trim()
.to_string();
let target_variable = parts
.next()
.expect("oauth field mapping should include a target variable")
.into_inner()
.next()
.expect("oauth target variable should include an identifier")
.as_str()
.trim()
.to_string();
if !mapped_source_fields.insert(source_field.clone()) {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' maps field '{}' more than once.",
name, source_field
),
},
item_span,
));
}
if !mapped_target_variables.insert(target_variable.clone()) {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' maps more than one field into '${}'.",
name, target_variable
),
},
item_span,
));
}
field_mappings.push(OAuthFieldMapping {
source_field,
target_variable,
});
}
_ => unreachable!("unexpected oauth item: {:?}", item.as_rule()),
}
}
validate_oauth_profile(&name, &fields, span.clone())?;
Ok(OAuthProfile {
name,
grant: fields
.get("grant")
.expect("validated oauth profile should include a grant")
.clone(),
fields,
params,
field_mappings,
})
}
fn validate_oauth_profile(
profile_name: &str,
fields: &HashMap<String, String>,
span: pest::Span<'_>,
) -> Result<(), pest::error::Error<Rule>> {
let grant = fields.get("grant").ok_or_else(|| {
pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!("OAuth profile '{}' requires 'grant = ...'.", profile_name),
},
span.clone(),
)
})?;
let has_issuer = fields.contains_key("issuer");
let has_token_url = fields.contains_key("token_url");
if has_issuer == has_token_url {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' must define exactly one of 'issuer' or 'token_url'.",
profile_name
),
},
span.clone(),
));
}
match grant.as_str() {
"client_credentials" => {
for field in ["client_id", "client_secret"] {
if !fields.contains_key(field) {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' with grant 'client_credentials' requires '{} = ...'.",
profile_name, field
),
},
span.clone(),
));
}
}
}
"refresh_token" => {
if !fields.contains_key("refresh_token") {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' with grant 'refresh_token' requires 'refresh_token = ...'.",
profile_name
),
},
span.clone(),
));
}
}
other => {
return Err(pest::error::Error::new_from_span(
pest::error::ErrorVariant::CustomError {
message: format!(
"OAuth profile '{}' uses unsupported grant '{}'. Supported grants are: client_credentials, refresh_token.",
profile_name, other
),
},
span,
));
}
}
Ok(())
}