use std::fmt;
use std::iter::Peekable;
use std::path::PathBuf;
use std::str::{Chars, FromStr};
use crate::ast::unescape;
use crate::render::{self, Format, RenderError};
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
None,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Path(PathBuf),
Symbol {
ty: String,
text: String,
},
Ref(String),
List(Vec<Value>),
Set(Vec<Value>),
Map(Vec<(String, Value)>),
Struct {
name: String,
fields: Vec<(String, Value)>,
},
Variant {
ty: String,
name: String,
fields: Option<Vec<(String, Value)>>,
},
}
impl Value {
pub fn get(&self, key: &str) -> Option<&Value> {
let entries = match self {
Value::Struct { fields, .. } => fields,
Value::Map(entries) => entries,
Value::Variant {
fields: Some(fields),
..
} => fields,
_ => return None,
};
entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}
pub fn at(&self, path: &AttrPath) -> Option<&Value> {
let mut current = self;
let mut keys = path.keys().iter();
while let Some(key) = keys.next() {
current = match current {
Value::Variant {
name,
fields: Some(_),
..
} if name == key => match keys.next() {
Some(field) => current.get(field)?,
None => return Some(current),
},
Value::Variant { .. } => return None,
_ => current.get(key)?,
};
}
Some(current)
}
pub fn render(&self, format: Format) -> Result<String, RenderError> {
match format {
Format::Json => Ok(render::json::pretty(self)),
Format::Toml => render::toml::render(self),
Format::Yaml => Ok(render::yaml::render(self)),
}
}
pub fn to_canonical_json(&self) -> String {
render::json::canonical(self)
}
pub(crate) fn describe(&self) -> String {
let mut text = String::new();
describe(self, &mut text);
if text.chars().count() > 40 {
let cut: String = text.chars().take(37).collect();
return format!("{cut}...");
}
text
}
}
fn describe(value: &Value, out: &mut String) {
if out.len() > 60 {
return;
}
match value {
Value::None => out.push_str("none"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => out.push_str(&i.to_string()),
Value::Float(f) => out.push_str(&format!("{f:?}")),
Value::Str(s) => out.push_str(&format!("{s:?}")),
Value::Path(p) => out.push_str(&p.display().to_string()),
Value::Symbol { text, .. } => out.push_str(text),
Value::Ref(label) => out.push_str(label),
Value::List(items) | Value::Set(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
describe(item, out);
}
out.push(']');
}
Value::Map(entries)
| Value::Struct {
fields: entries, ..
} => {
if let Value::Struct { name, .. } = value {
out.push_str(name);
out.push(' ');
}
out.push('{');
for (i, (key, item)) in entries.iter().enumerate() {
out.push_str(if i > 0 { ", " } else { " " });
out.push_str(key);
out.push_str(" = ");
describe(item, out);
}
out.push_str(if entries.is_empty() { "}" } else { " }" });
}
Value::Variant { name, fields, .. } => {
out.push_str(name);
if let Some(fields) = fields {
out.push('(');
for (i, (key, item)) in fields.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(key);
out.push_str(" = ");
describe(item, out);
}
out.push(')');
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AttrPath {
keys: Vec<String>,
}
impl AttrPath {
pub fn root() -> Self {
Self::default()
}
pub fn new<I>(keys: I) -> Self
where
I: IntoIterator,
I::Item: Into<String>,
{
Self {
keys: keys.into_iter().map(Into::into).collect(),
}
}
pub fn keys(&self) -> &[String] {
&self.keys
}
pub fn join(&self, key: impl Into<String>) -> Self {
let mut keys = self.keys.clone();
keys.push(key.into());
Self { keys }
}
}
impl fmt::Display for AttrPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, key) in self.keys.iter().enumerate() {
if i > 0 {
f.write_str(".")?;
}
if !key.is_empty() && key.chars().all(is_bare) {
f.write_str(key)?;
continue;
}
f.write_str("\"")?;
let mut chars = key.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => f.write_str("\\\\")?,
'"' => f.write_str("\\\"")?,
'\n' => f.write_str("\\n")?,
'\t' => f.write_str("\\t")?,
'\r' => f.write_str("\\r")?,
'\0' => f.write_str("\\0")?,
'#' if chars.peek() == Some(&'{') => f.write_str("\\#")?,
c if c.is_control() => write!(f, "\\u{{{:x}}}", u32::from(c))?,
c => write!(f, "{c}")?,
}
}
f.write_str("\"")?;
}
Ok(())
}
}
impl FromStr for AttrPath {
type Err = AttrPathError;
fn from_str(text: &str) -> Result<Self, AttrPathError> {
let mut keys = Vec::new();
if text.is_empty() {
return Ok(Self { keys });
}
let mut chars = text.chars().peekable();
loop {
let key = if chars.next_if_eq(&'"').is_some() {
quoted_key(&mut chars)?
} else {
let mut key = String::new();
while let Some(c) = chars.next_if(|&c| c != '.') {
key.push(c);
}
if key.is_empty() {
return Err(error("empty key".to_owned()));
}
if !key.chars().all(is_bare) {
return Err(error(format!("`{key}` must be quoted")));
}
key
};
keys.push(key);
match chars.next() {
None => return Ok(Self { keys }),
Some('.') => {}
Some(c) => {
return Err(error(format!(
"expected `.` after a quoted key, found `{c}`"
)));
}
}
}
}
}
fn quoted_key(chars: &mut Peekable<Chars<'_>>) -> Result<String, AttrPathError> {
let mut key = String::new();
loop {
match chars.next() {
None => return Err(error("unterminated quoted key".to_owned())),
Some('"') => return Ok(key),
Some('\\') => {
let mut escape = String::from("\\");
escape.extend(chars.next());
if escape == "\\u" {
while let Some(c) = chars.next_if(|&c| c != '"') {
escape.push(c);
if c == '}' {
break;
}
}
}
match unescape(&escape) {
Some(c) => key.push(c),
None if escape.starts_with("\\u") => {
return Err(error(format!("invalid unicode escape `{escape}`")));
}
None => return Err(error(format!("unknown escape `{escape}`"))),
}
}
Some(c) => key.push(c),
}
}
}
fn is_bare(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn error(message: String) -> AttrPathError {
AttrPathError { message }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttrPathError {
message: String,
}
impl fmt::Display for AttrPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for AttrPathError {}
#[cfg(test)]
#[path = "value_tests.rs"]
mod tests;