#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Warning {
pub message: String,
pub line: Option<usize>,
}
impl Warning {
pub fn new(message: impl Into<String>) -> Self {
Self { message: message.into(), line: None }
}
pub fn at(message: impl Into<String>, line: usize) -> Self {
Self { message: message.into(), line: Some(line) }
}
pub fn maybe_at(message: impl Into<String>, line: Option<usize>) -> Self {
Self { message: message.into(), line }
}
}
pub fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
impl Warning {
pub fn to_json(&self) -> String {
let line = match self.line {
Some(line) => line.to_string(),
None => "null".to_string(),
};
format!("{{\"message\": {}, \"line\": {line}}}", json_string(&self.message))
}
}
impl std::fmt::Display for Warning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.line {
Some(line) => write!(f, "line {line}: {}", self.message),
None => write!(f, "{}", self.message),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Number(f64),
Text(String),
Bool(bool),
List(Vec<Value>),
Map(Vec<(String, Value)>),
}
impl Value {
pub fn to_display(&self) -> String {
match self {
Value::Number(n) => {
if n.fract() == 0.0 {
format!("{}", *n as i64)
} else {
format!("{n}")
}
}
Value::Text(s) => s.clone(),
Value::Bool(b) => b.to_string(),
Value::List(items) => items
.iter()
.map(Value::to_display)
.collect::<Vec<_>>()
.join(", "),
Value::Map(entries) => entries
.iter()
.map(|(k, v)| format!("{k}: {}", v.to_display()))
.collect::<Vec<_>>()
.join(", "),
}
}
pub fn as_number(&self) -> Option<f64> {
match self {
Value::Number(n) => Some(*n),
_ => None,
}
}
pub fn as_list(&self) -> Option<&[Value]> {
match self {
Value::List(items) => Some(items),
_ => None,
}
}
pub fn is_truthy(&self) -> bool {
match self {
Value::Number(n) => *n != 0.0,
Value::Text(s) => !s.is_empty(),
Value::Bool(b) => *b,
Value::List(items) => !items.is_empty(),
Value::Map(entries) => !entries.is_empty(),
}
}
pub fn as_map(&self) -> Option<&[(String, Value)]> {
match self {
Value::Map(entries) => Some(entries),
_ => None,
}
}
pub fn to_rhai_literal(&self) -> String {
match self {
Value::Number(n) => {
if n.fract() == 0.0 {
format!("{}", *n as i64)
} else {
format!("{n}")
}
}
Value::Text(s) => {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
out
}
Value::Bool(b) => b.to_string(),
Value::List(items) => {
let inner: Vec<_> = items.iter().map(Value::to_rhai_literal).collect();
format!("[{}]", inner.join(", "))
}
Value::Map(entries) => {
let inner: Vec<_> = entries
.iter()
.map(|(k, v)| format!("{k}: {}", v.to_rhai_literal()))
.collect();
format!("#{{{}}}", inner.join(", "))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn displays_and_coerces() {
assert_eq!(Value::Number(82.0).to_display(), "82"); assert_eq!(Value::Number(8.2).to_display(), "8.2");
assert_eq!(
Value::List(vec![Value::Text("a".into()), Value::Number(2.0)]).to_display(),
"a, 2"
);
assert!(Value::Number(1.0).is_truthy());
assert!(!Value::Number(0.0).is_truthy());
assert!(!Value::Text(String::new()).is_truthy());
assert!(!Value::List(Vec::new()).is_truthy());
}
#[test]
fn serializes_rhai_literals() {
assert_eq!(Value::Number(3.0).to_rhai_literal(), "3");
assert_eq!(Value::Number(2.5).to_rhai_literal(), "2.5");
assert_eq!(Value::Bool(true).to_rhai_literal(), "true");
assert_eq!(Value::Text("Charlie".into()).to_rhai_literal(), "\"Charlie\"");
assert_eq!(
Value::Text("say \"hi\"\\n".into()).to_rhai_literal(),
"\"say \\\"hi\\\"\\\\n\""
);
assert_eq!(
Value::List(vec![Value::Number(1.0), Value::Text("a".into())]).to_rhai_literal(),
"[1, \"a\"]"
);
}
}