use haematite_query::stage::shape::{Field, MAX_DEPTH, Selection};
use super::{Input, Refused, Root, Source, Span, Spanned, Stage, first, source, spans};
pub(super) fn stage(text: &str, start: usize) -> Result<Spanned<Stage>, Refused> {
let mut reader = Reader {
text,
rest: text,
start,
};
reader.trim();
let beginning = reader.position();
let selection = reader.selection(0)?;
Ok(Spanned {
value: Stage::Shape(selection),
span: Span {
start: beginning,
end: reader.position(),
},
})
}
pub(super) fn source(
text: &str,
start: usize,
) -> Result<(Source, Span, Option<Selection>), Refused> {
let mut reader = Reader {
text,
rest: text,
start,
};
reader.trim();
let beginning = reader.position();
let mut roots = reader.roots()?;
let span = Span {
start: beginning,
end: reader.position(),
};
if roots.len() > 1 {
return Ok((Source::Shaped(roots), span, None));
}
let root = roots
.pop()
.ok_or_else(|| reader.refused("a shape names a collection root"))?;
Ok((
Source::Collection {
name: root.name,
arguments: root.arguments,
},
span,
Some(root.selection),
))
}
struct Reader<'a> {
text: &'a str,
rest: &'a str,
start: usize,
}
impl Reader<'_> {
const fn position(&self) -> usize {
self.start + self.text.len() - self.rest.len()
}
fn trim(&mut self) {
self.rest = self.rest.trim_start();
}
fn refused(&self, message: impl Into<String>) -> Refused {
Refused::Parse(message.into(), spans::stage(self.text, self.start))
}
fn require(&mut self, token: char, message: &str) -> Result<(), Refused> {
self.trim();
let rest = self
.rest
.strip_prefix(token)
.ok_or_else(|| self.refused(message))?;
self.rest = rest;
Ok(())
}
fn roots(&mut self) -> Result<Vec<Root>, Refused> {
self.require('{', "a shape starts with `{`")?;
let mut roots: Vec<Root> = Vec::new();
loop {
self.trim();
if let Some(rest) = self.rest.strip_prefix('}') {
self.rest = rest;
if roots.is_empty() {
return Err(self.refused("a shape names at least one collection root"));
}
return Ok(roots);
}
let start = self.position();
let mut input = Input {
text: self.text,
rest: self.rest,
};
let parsed = source::collection(&mut input).map_err(|error| self.located(error))?;
let Source::Collection { name, arguments } = parsed else {
return Err(self.refused("a collection root has a nested selection set"));
};
self.rest = input.rest;
let selection = self.selection(1)?;
let span = Span {
start,
end: self.position(),
};
if roots.iter().any(|root| root.name == name) {
return Err(Refused::Parse(
format!("a shape repeats the root {name:?}"),
span,
));
}
roots.push(Root {
name,
arguments,
selection,
span,
});
}
}
fn located(&self, error: Refused) -> Refused {
let span = error.span();
let span = Span {
start: self.start + span.start,
end: self.start + span.end,
};
match error {
Refused::Parse(message, _) => Refused::Parse(message, span),
Refused::WholeRecords(message, _) => Refused::WholeRecords(message, span),
Refused::NotYet(message, _) => Refused::NotYet(message, span),
Refused::Unserved(message, _) => Refused::Unserved(message, span),
Refused::Percentile(message, _) => Refused::Percentile(message, span),
Refused::Flag(message, _) => Refused::Flag(message, span),
Refused::Window(code, message, _) => Refused::Window(code, message, span),
}
}
fn selection(&mut self, depth: usize) -> Result<Selection, Refused> {
let fields = self.fields(depth)?;
Selection::new(fields).map_err(|error| self.refused(error.refusal.message))
}
fn fields(&mut self, depth: usize) -> Result<Vec<Field>, Refused> {
if depth >= MAX_DEPTH {
return Err(self.refused("a shape exceeds 64 selection levels"));
}
self.require('{', "a shape starts with `{`")?;
let mut fields = Vec::new();
loop {
self.trim();
if let Some(rest) = self.rest.strip_prefix('}') {
self.rest = rest;
if fields.is_empty() {
return Err(self.refused("a shape selects at least one field"));
}
return Ok(fields);
}
if self.rest.is_empty() {
return Err(self.refused("a shape is not closed with `}`"));
}
let length = self
.rest
.chars()
.take_while(|character| first(*character))
.map(char::len_utf8)
.sum();
let (name, rest) = self.rest.split_at(length);
if name.is_empty() {
return Err(self.refused("a shape field has a bare name"));
}
let name = name.to_owned();
self.rest = rest;
self.trim();
let selection = if self.rest.starts_with('{') {
Some(self.selection(depth + 1)?)
} else {
None
};
fields.push(Field { name, selection });
}
}
}