use std::fmt::{Debug, Display, Formatter};
use std::num::NonZeroU32;
use tanzim_source::Source;
#[derive(Debug, Clone, PartialEq)]
pub struct Location {
pub source: Source,
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 in_source(
source: Source,
line: Option<usize>,
column: Option<usize>,
length: Option<usize>,
) -> Self {
Self {
source,
line: line.and_then(position),
column: column.and_then(position),
length: length.and_then(position),
}
}
pub fn at(
source_name: &str,
resource: &str,
line: Option<usize>,
column: Option<usize>,
length: Option<usize>,
) -> Self {
Self::in_source(
Source::named(source_name).with_resource(resource),
line,
column,
length,
)
}
pub fn source_name(&self) -> &str {
self.source.source()
}
pub fn resource(&self) -> &str {
self.source.resource()
}
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 {
let resource = self.source.resource();
if resource.is_empty() {
write!(f, "{}", self.source.source())?;
} else {
write!(f, "{}:{}", self.source.source(), 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,
Null,
Comment,
}
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",
Self::Null => "null",
Self::Comment => "comment",
})
}
}
#[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),
Null,
Comment(String),
}
#[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 is_null(&self) -> bool {
matches!(self, Self::Null)
}
pub fn is_comment(&self) -> bool {
matches!(self, Self::Comment(_))
}
pub fn as_comment(&self) -> Option<&str> {
match self {
Self::Comment(value) => Some(value),
_ => None,
}
}
pub fn into_comment(self) -> Option<String> {
match self {
Self::Comment(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,
Self::Null => ValueType::Null,
Self::Comment(_) => ValueType::Comment,
}
}
}
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),
Self::Null => f.write_str("null"),
Self::Comment(value) => f.write_str(value),
}
}
}
#[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('@'));
}
#[test]
fn value_accessors_and_constructors() {
let mut value = Value::Bool(true);
assert!(value.is_bool());
assert_eq!(value.as_bool(), Some(true));
assert_eq!(value.type_name(), ValueType::Bool);
if let Some(flag) = value.bool_mut() {
*flag = false;
}
assert_eq!(value.into_bool(), Some(false));
let list = Value::new_list();
assert!(list.is_list());
let map = Value::new_map();
assert!(map.is_map());
let text = Value::new_string();
assert!(text.is_string());
}
#[test]
fn map_remove_get_mut_and_display() {
let mut map = Map::new();
map.insert("a".to_string(), located_string("one"));
map.insert("b".to_string(), located_string("two"));
assert_eq!(map.len(), 2);
assert!(map.contains_key("a"));
assert!(map.get_mut("b").is_some());
let removed = map.remove("a");
assert!(removed.is_some());
assert!(!map.contains_key("a"));
let compact = format!("{map}");
assert!(compact.contains("b"));
let detailed = format!("{map:#}");
assert!(detailed.contains("location"));
}
#[test]
fn location_display_and_with_length() {
let location = Location::at("file", "", Some(1), Some(2), None).with_length(3);
assert_eq!(location.to_string(), "file:1:2");
let resourceful = Location::at("file", "cfg.yml", Some(4), None, None);
assert_eq!(resourceful.to_string(), "file:cfg.yml:4");
}
#[test]
fn value_list_and_map_display_modes() {
let list = Value::List(vec![located_string("a"), located_string("b")]);
assert!(format!("{list}").contains("a"));
assert!(format!("{list:#}").contains("location"));
let mut map = Map::new();
map.insert("k".to_string(), located_string("v"));
let map_value = Value::Map(map);
assert!(format!("{map_value}").contains("k"));
}
}