use std::fmt::Write as _;
use crate::ast::{SelectQuery, Span};
use crate::lexer::{SpannedTokens, Token, lex_spanned};
use crate::parser::{ParseError, SyntaxFault, parse_spanned, unquote};
use crate::printer::{escape_string, to_aql};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectiveKind {
Endpoint,
Organisation,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Directive {
pub kind: DirectiveKind,
pub variable: Option<String>,
pub ids: Vec<String>,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Federated {
pub directive: Option<Directive>,
pub query: SelectQuery,
}
pub fn parse_federated(src: &str) -> Result<Federated, ParseError> {
let stream = lex_spanned(src)?;
let Some(from) = stream.tokens().iter().position(|t| *t == Token::From) else {
return parse_spanned(&stream).map(|query| Federated {
directive: None,
query,
});
};
let keyword = from.saturating_add(1);
let kind = match stream.tokens().get(keyword) {
Some(Token::Identifier(word)) if word.eq_ignore_ascii_case("endpoint") => {
DirectiveKind::Endpoint
}
Some(Token::Identifier(word)) if word.eq_ignore_ascii_case("organisation") => {
DirectiveKind::Organisation
}
_ => {
return parse_spanned(&stream).map(|query| Federated {
directive: None,
query,
});
}
};
let (directive, contains) = directive(&stream, kind, keyword)?;
let query = parse_spanned(&stream.without(keyword..contains.saturating_add(1)))?;
Ok(Federated {
directive: Some(directive),
query,
})
}
fn directive(
stream: &SpannedTokens,
kind: DirectiveKind,
keyword: usize,
) -> Result<(Directive, usize), ParseError> {
let tokens = stream.tokens();
let mut at = keyword.saturating_add(1);
let variable = match tokens.get(at) {
Some(Token::Identifier(name)) => {
at = at.saturating_add(1);
Some(name.clone())
}
_ => None,
};
expect(stream, at, &Token::LeftBracket)?;
let mut ids = Vec::new();
loop {
at = at.saturating_add(1);
match tokens.get(at) {
Some(Token::String(raw)) => ids.push(unquote(raw)),
_ => return Err(fault(stream, at)),
}
at = at.saturating_add(1);
match tokens.get(at) {
Some(Token::Comma) => {}
Some(Token::RightBracket) => break,
_ => return Err(fault(stream, at)),
}
}
let close = at;
let contains = close.saturating_add(1);
if !matches!(stream.tokens().get(contains), Some(Token::Contains(_))) {
return Err(fault(stream, contains));
}
let directive = Directive {
kind,
variable,
ids,
span: Span::new(stream.byte_span(&(keyword..close.saturating_add(1)))),
};
Ok((directive, contains))
}
fn expect(stream: &SpannedTokens, at: usize, wanted: &Token) -> Result<(), ParseError> {
if stream.tokens().get(at) == Some(wanted) {
Ok(())
} else {
Err(fault(stream, at))
}
}
fn fault(stream: &SpannedTokens, at: usize) -> ParseError {
let tokens = at..at.saturating_add(1);
ParseError::Syntax {
faults: vec![SyntaxFault {
bytes: Some(stream.byte_span(&tokens)),
found: stream.tokens().get(at).cloned(),
tokens,
}],
}
}
#[must_use]
pub fn to_federated_aql(federated: &Federated) -> String {
let Some(directive) = &federated.directive else {
return to_aql(&federated.query, None);
};
let mut prefix = String::from(match directive.kind {
DirectiveKind::Endpoint => "ENDPOINT",
DirectiveKind::Organisation => "ORGANISATION",
});
if let Some(variable) = &directive.variable {
let _ = write!(prefix, " {variable}");
}
prefix.push_str(" [");
for (i, id) in directive.ids.iter().enumerate() {
if i > 0 {
prefix.push_str(", ");
}
let _ = write!(prefix, "'{}'", escape_string(id));
}
prefix.push_str("] CONTAINS ");
to_aql(&federated.query, Some(&prefix))
}