use saphyr::Scalar;
use saphyr_parser::{Event, Parser, Span};
use super::error::ConfigError;
#[derive(Debug, Clone)]
pub struct Node {
pub line: usize,
pub value: Value,
}
#[derive(Debug, Clone)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Seq(Vec<Node>),
Map(Vec<(Key, Node)>),
}
#[derive(Debug, Clone)]
pub struct Key {
pub name: String,
pub line: usize,
}
pub fn parse(file: &str, text: &str) -> Result<Node, ConfigError> {
let mut events = Vec::new();
for event in Parser::new_from_str(text) {
match event {
Ok((event, span)) => events.push((event, span)),
Err(e) => return Err(ConfigError::at(file, e.marker().line(), e.info())),
}
}
let mut builder = Builder {
file,
events: events.into_iter().peekable(),
};
builder.document()
}
struct Builder<'a, I: Iterator<Item = (Event<'a>, Span)>> {
file: &'a str,
events: std::iter::Peekable<I>,
}
impl<'a, I: Iterator<Item = (Event<'a>, Span)>> Builder<'a, I> {
fn document(&mut self) -> Result<Node, ConfigError> {
let mut root = None;
while let Some((event, span)) = self.events.next() {
match event {
Event::StreamStart | Event::DocumentEnd | Event::StreamEnd | Event::Nothing => {}
Event::DocumentStart(_) if root.is_some() => {
return Err(ConfigError::at(
self.file,
span.start.line(),
"expected a single YAML document",
));
}
Event::DocumentStart(_) => root = Some(self.node()?),
_ => {
return Err(ConfigError::at(
self.file,
span.start.line(),
"unexpected YAML event",
));
}
}
}
Ok(root.unwrap_or(Node {
line: 1,
value: Value::Null,
}))
}
fn node(&mut self) -> Result<Node, ConfigError> {
let Some((event, span)) = self.events.next() else {
return Err(ConfigError::at(self.file, 1, "unexpected end of YAML"));
};
let line = span.start.line();
let value = match event {
Event::Scalar(text, style, _, tag) => {
match Scalar::parse_from_cow_and_metadata(text, style, tag.as_ref()) {
Some(Scalar::Null) => Value::Null,
Some(Scalar::Boolean(b)) => Value::Bool(b),
Some(Scalar::Integer(i)) => Value::Int(i),
Some(Scalar::FloatingPoint(f)) => Value::Float(f.into_inner()),
Some(Scalar::String(s)) => Value::Str(s.into_owned()),
None => {
return Err(ConfigError::at(
self.file,
line,
"invalid scalar for its tag",
));
}
}
}
Event::SequenceStart(..) => {
let mut items = Vec::new();
while !matches!(self.events.peek(), Some((Event::SequenceEnd, _))) {
items.push(self.node()?);
}
self.events.next();
Value::Seq(items)
}
Event::MappingStart(..) => {
let mut entries: Vec<(Key, Node)> = Vec::new();
while !matches!(self.events.peek(), Some((Event::MappingEnd, _))) {
let key = self.node()?;
let name = match key.value {
Value::Str(s) => s,
Value::Int(i) => i.to_string(),
Value::Bool(b) => b.to_string(),
_ => {
return Err(ConfigError::at(
self.file,
key.line,
"mapping keys must be plain strings",
));
}
};
if let Some((previous, _)) = entries.iter().find(|(k, _)| k.name == name) {
return Err(ConfigError::at(
self.file,
key.line,
format!(
"duplicate key `{name}` (first defined at line {})",
previous.line
),
));
}
let value = self.node()?;
entries.push((
Key {
name,
line: key.line,
},
value,
));
}
self.events.next();
Value::Map(entries)
}
Event::Alias(_) => {
return Err(ConfigError::at(
self.file,
line,
"YAML aliases are not supported",
));
}
_ => return Err(ConfigError::at(self.file, line, "unexpected YAML event")),
};
Ok(Node { line, value })
}
}
impl Node {
pub fn describe(&self) -> &'static str {
match self.value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Int(_) => "an integer",
Value::Float(_) => "a number",
Value::Str(_) => "a string",
Value::Seq(_) => "a list",
Value::Map(_) => "a mapping",
}
}
fn expected(&self, file: &str, what: &str, expected: &str) -> ConfigError {
ConfigError::at(
file,
self.line,
format!("`{what}` must be {expected}, found {}", self.describe()),
)
}
pub fn text(self, file: &str, what: &str) -> Result<String, ConfigError> {
match self.value {
Value::Str(s) if !s.trim().is_empty() => Ok(s),
_ => Err(self.expected(file, what, "a non-empty string")),
}
}
pub fn boolean(self, file: &str, what: &str) -> Result<bool, ConfigError> {
match self.value {
Value::Bool(b) => Ok(b),
_ => Err(self.expected(file, what, "true or false")),
}
}
pub fn integer(self, file: &str, what: &str) -> Result<i64, ConfigError> {
match self.value {
Value::Int(i) => Ok(i),
_ => Err(self.expected(file, what, "an integer")),
}
}
pub fn number(self, file: &str, what: &str) -> Result<f64, ConfigError> {
match self.value {
Value::Int(i) => Ok(i as f64),
Value::Float(f) => Ok(f),
_ => Err(self.expected(file, what, "a number")),
}
}
pub fn list(self, file: &str, what: &str) -> Result<Vec<Node>, ConfigError> {
match self.value {
Value::Seq(items) => Ok(items),
_ => Err(self.expected(file, what, "a list")),
}
}
pub fn fields<'f>(
self,
file: &'f str,
what: &str,
allowed: &[&str],
) -> Result<Fields<'f>, ConfigError> {
let Value::Map(entries) = self.value else {
return Err(self.expected(file, what, "a mapping"));
};
if let Some((key, _)) = entries
.iter()
.find(|(k, _)| !allowed.contains(&k.name.as_str()))
{
return Err(ConfigError::at(
file,
key.line,
format!(
"unknown key `{}` in {what} (expected one of: {})",
key.name,
allowed.join(", ")
),
));
}
Ok(Fields {
file,
line: self.line,
what: what.to_string(),
entries,
})
}
pub fn into_json(self) -> serde_json::Value {
use serde_json::Value as J;
match self.value {
Value::Null => J::Null,
Value::Bool(b) => J::Bool(b),
Value::Int(i) => J::from(i),
Value::Float(f) => J::from(f),
Value::Str(s) => J::String(s),
Value::Seq(items) => J::Array(items.into_iter().map(Node::into_json).collect()),
Value::Map(entries) => J::Object(
entries
.into_iter()
.map(|(k, v)| (k.name, v.into_json()))
.collect(),
),
}
}
}
pub struct Fields<'f> {
file: &'f str,
pub line: usize,
pub what: String,
entries: Vec<(Key, Node)>,
}
impl Fields<'_> {
pub fn optional(&mut self, key: &str) -> Option<Node> {
let index = self.entries.iter().position(|(k, _)| k.name == key)?;
Some(self.entries.remove(index).1)
}
pub fn required(&mut self, key: &str) -> Result<Node, ConfigError> {
self.optional(key).ok_or_else(|| self.missing(key))
}
pub fn missing(&self, key: &str) -> ConfigError {
ConfigError::at(
self.file,
self.line,
format!("{} is missing the required field `{key}`", self.what),
)
}
}