use std::fmt::{Debug, Display, Formatter};
use std::num::NonZeroU32;
#[derive(Debug, Clone, PartialEq)]
pub struct Location {
pub source_name: String,
pub resource: String,
pub line: Option<NonZeroU32>,
pub column: Option<NonZeroU32>,
pub length: Option<NonZeroU32>,
}
fn position(value: usize) -> Option<NonZeroU32> {
NonZeroU32::new(u32::try_from(value).unwrap_or(u32::MAX))
}
impl Location {
pub fn at(
source_name: &str,
resource: &str,
line: Option<usize>,
column: Option<usize>,
length: Option<usize>,
) -> Self {
Self {
source_name: source_name.to_string(),
resource: resource.to_string(),
line: line.and_then(position),
column: column.and_then(position),
length: length.and_then(position),
}
}
pub fn with_length(mut self, length: usize) -> Self {
self.length = position(length);
self
}
}
impl Display for Location {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.resource.is_empty() {
write!(f, "{}", self.source_name)?;
} else {
write!(f, "{}:{}", self.source_name, self.resource)?;
}
match (self.line, self.column) {
(Some(line), Some(column)) => write!(f, ":{line}:{column}"),
(Some(line), None) => write!(f, ":{line}"),
_ => Ok(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ValueType {
Bool,
Int,
Float,
String,
List,
Map,
}
impl Display for ValueType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Bool => "boolean",
Self::Int => "integer",
Self::Float => "float",
Self::String => "string",
Self::List => "list",
Self::Map => "map",
})
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Map {
entries: Vec<(String, LocatedValue)>,
}
impl Map {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn contains_key(&self, key: &str) -> bool {
for index in (0..self.entries.len()).rev() {
if self.entries[index].0 == key {
return true;
}
}
false
}
pub fn get(&self, key: &str) -> Option<&LocatedValue> {
for index in (0..self.entries.len()).rev() {
if self.entries[index].0 == key {
return Some(&self.entries[index].1);
}
}
None
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut LocatedValue> {
let mut found = None;
for index in (0..self.entries.len()).rev() {
if self.entries[index].0 == key {
found = Some(index);
break;
}
}
if let Some(index) = found {
Some(&mut self.entries[index].1)
} else {
None
}
}
pub fn insert(&mut self, key: String, value: LocatedValue) -> Option<LocatedValue> {
let old = self.remove(&key);
self.entries.push((key, value));
old
}
pub fn remove(&mut self, key: &str) -> Option<LocatedValue> {
let mut found = None;
for index in (0..self.entries.len()).rev() {
if self.entries[index].0 == key {
found = Some(index);
break;
}
}
if let Some(index) = found {
Some(self.entries.remove(index).1)
} else {
None
}
}
pub fn entries(&self) -> &[(String, LocatedValue)] {
&self.entries
}
pub fn entries_mut(&mut self) -> &mut Vec<(String, LocatedValue)> {
&mut self.entries
}
}
impl Display for Map {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let alternate = f.alternate();
let mut map = f.debug_map();
for (key, value) in &self.entries {
if alternate {
map.entry(key, &format_args!("{:#}", value));
} else {
map.entry(key, &format_args!("{}", value));
}
}
map.finish()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Bool(bool),
Int(isize),
Float(f64),
String(String),
List(Vec<LocatedValue>),
Map(Map),
}
#[derive(Debug, Clone, PartialEq)]
pub struct LocatedValue {
pub value: Value,
pub location: Location,
}
impl Display for LocatedValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
let mut map = f.debug_map();
map.entry(&"value", &format_args!("{:#}", self.value));
map.entry(
&"location",
&format_args!("{:?}", self.location.to_string()),
);
map.finish()
} else {
write!(f, "{}", self.value)
}
}
}
impl AsRef<Value> for Value {
fn as_ref(&self) -> &Value {
self
}
}
impl AsRef<Value> for LocatedValue {
fn as_ref(&self) -> &Value {
&self.value
}
}
impl Value {
pub fn new_map() -> Self {
Self::Map(Map::new())
}
pub fn new_list() -> Self {
Self::List(Vec::new())
}
pub fn new_string() -> Self {
Self::String(String::new())
}
pub fn is_bool(&self) -> bool {
matches!(self, Self::Bool(_))
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(value) => Some(*value),
_ => None,
}
}
pub fn into_bool(self) -> Option<bool> {
match self {
Self::Bool(value) => Some(value),
_ => None,
}
}
pub fn bool_mut(&mut self) -> Option<&mut bool> {
match self {
Self::Bool(value) => Some(value),
_ => None,
}
}
pub fn is_int(&self) -> bool {
matches!(self, Self::Int(_))
}
pub fn as_int(&self) -> Option<isize> {
match self {
Self::Int(value) => Some(*value),
_ => None,
}
}
pub fn into_int(self) -> Option<isize> {
match self {
Self::Int(value) => Some(value),
_ => None,
}
}
pub fn int_mut(&mut self) -> Option<&mut isize> {
match self {
Self::Int(value) => Some(value),
_ => None,
}
}
pub fn is_float(&self) -> bool {
matches!(self, Self::Float(_))
}
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(value) => Some(*value),
_ => None,
}
}
pub fn into_float(self) -> Option<f64> {
match self {
Self::Float(value) => Some(value),
_ => None,
}
}
pub fn float_mut(&mut self) -> Option<&mut f64> {
match self {
Self::Float(value) => Some(value),
_ => None,
}
}
pub fn is_string(&self) -> bool {
matches!(self, Self::String(_))
}
pub fn as_string(&self) -> Option<&String> {
match self {
Self::String(value) => Some(value),
_ => None,
}
}
pub fn into_string(self) -> Option<String> {
match self {
Self::String(value) => Some(value),
_ => None,
}
}
pub fn string_mut(&mut self) -> Option<&mut String> {
match self {
Self::String(value) => Some(value),
_ => None,
}
}
pub fn is_list(&self) -> bool {
matches!(self, Self::List(_))
}
pub fn as_list(&self) -> Option<&Vec<LocatedValue>> {
match self {
Self::List(value) => Some(value),
_ => None,
}
}
pub fn into_list(self) -> Option<Vec<LocatedValue>> {
match self {
Self::List(value) => Some(value),
_ => None,
}
}
pub fn list_mut(&mut self) -> Option<&mut Vec<LocatedValue>> {
match self {
Self::List(value) => Some(value),
_ => None,
}
}
pub fn is_map(&self) -> bool {
matches!(self, Self::Map(_))
}
pub fn as_map(&self) -> Option<&Map> {
match self {
Self::Map(value) => Some(value),
_ => None,
}
}
pub fn into_map(self) -> Option<Map> {
match self {
Self::Map(value) => Some(value),
_ => None,
}
}
pub fn map_mut(&mut self) -> Option<&mut Map> {
match self {
Self::Map(value) => Some(value),
_ => None,
}
}
pub fn type_name(&self) -> ValueType {
match self {
Self::Bool(_) => ValueType::Bool,
Self::Int(_) => ValueType::Int,
Self::Float(_) => ValueType::Float,
Self::String(_) => ValueType::String,
Self::List(_) => ValueType::List,
Self::Map(_) => ValueType::Map,
}
}
}
impl Display for Value {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bool(value) => write!(f, "{value}"),
Self::Int(value) => write!(f, "{value}"),
Self::Float(value) => write!(f, "{value}"),
Self::String(value) => write!(f, "{value:?}"),
Self::List(values) => {
let alternate = f.alternate();
let mut list = f.debug_list();
for value in values {
if alternate {
list.entry(&format_args!("{:#}", value));
} else {
list.entry(&format_args!("{}", value));
}
}
list.finish()
}
Self::Map(value) => Display::fmt(value, f),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn located_string(text: &str) -> LocatedValue {
LocatedValue {
value: Value::String(text.to_string()),
location: Location::at("file", "test", None, None, None),
}
}
#[test]
fn as_ref_value_accepts_all_forms() {
fn take<V: AsRef<Value>>(value: V) -> Value {
value.as_ref().clone()
}
let value = Value::Int(7);
let located = LocatedValue {
value: Value::Int(7),
location: Location::at("file", "test", None, None, None),
};
assert_eq!(take(value.clone()), value); assert_eq!(take(&value), value); assert_eq!(take(located.clone()), value); assert_eq!(take(&located), value); }
#[test]
fn last_key_wins() {
let mut map = Map::new();
map.insert("foo".to_string(), located_string("first"));
map.insert("foo".to_string(), located_string("second"));
assert_eq!(map.get("foo").unwrap().value.as_string().unwrap(), "second");
}
#[test]
fn default_display_is_compact() {
let value = LocatedValue {
value: Value::String("hello".to_string()),
location: Location::at("file", "config.yaml", Some(2), Some(5), None),
};
let message = value.to_string();
assert!(!message.contains('\n'));
assert!(!message.starts_with('@'));
assert_eq!(message, "\"hello\"");
}
#[test]
fn alternate_display_shows_location_and_multiline() {
let value = LocatedValue {
value: Value::String("hello".to_string()),
location: Location::at("file", "config.yaml", Some(2), Some(5), None),
};
let message = format!("{value:#}");
assert_eq!(
message,
"{\n \"value\": \"hello\",\n \"location\": \"file:config.yaml:2:5\",\n}"
);
assert!(!message.contains('@'));
}
}