use std::cmp::Ordering;
pub(self) use super::common::{self, err};
pub(self) use super::tkn_tree;
use tkn_tree::{parse_it, SyntaxElement, SyntaxNode, SyntaxNodeExtTrait, SyntaxToken, TomlKind};
mod date;
use date::TomlDate;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Toml {
items: Vec<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
pub struct Table {
comment: Option<String>,
header: Heading,
pairs: Vec<KvPair>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
pub struct Heading {
header: String,
seg: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
pub struct InTable {
pairs: Vec<KvPair>,
trailing_comma: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
pub struct KvPair {
comment: Option<String>,
key: Option<String>,
val: Value,
}
impl Ord for KvPair {
fn cmp(&self, other: &KvPair) -> Ordering {
match self.key() {
Some(key) => match other.key() {
Some(k) => key.cmp(k),
None => Ordering::Equal,
},
None => Ordering::Equal,
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Value {
Bool(bool),
Int(i64),
Float(f64),
StrLit(String),
Date(TomlDate),
Array(Vec<Value>),
InlineTable(InTable),
Table(Table),
Comment(String),
KeyValue(Box<KvPair>),
Root,
Eof,
None,
}
impl Eq for Value {}
fn strip_start_end(mut input: String, count: usize) -> String {
for _ in 0..count {
input.remove(0);
input.pop();
}
input
}
fn integer(s: &str) -> err::TomlResult<Value> {
let s = s.replace('_', "");
let cleaned = s.trim_start_matches('+');
if s.starts_with("0x") {
let without_prefix = cleaned.chars().skip(2).collect::<String>();
let z = i64::from_str_radix(&without_prefix, 16);
Ok(Value::Int(z?))
} else if s.starts_with("0o") {
let without_prefix = cleaned.chars().skip(2).collect::<String>();
let z = i64::from_str_radix(&without_prefix, 8);
Ok(Value::Int(z?))
} else if s.starts_with("0b") {
let without_prefix = cleaned.chars().skip(2).collect::<String>();
let z = i64::from_str_radix(&without_prefix, 2);
Ok(Value::Int(z?))
} else {
Ok(Value::Int(cleaned.parse()?))
}
}
fn ws(node: &SyntaxNode) -> bool {
node.kind() != TomlKind::KeyValue
}
fn ws_ele(node: &SyntaxElement) -> bool {
node.kind() != TomlKind::KeyValue
}
fn first_child_not_ws(node: SyntaxNode) -> Option<SyntaxElement> {
node.children_with_tokens().find(ws_ele)
}
impl Value {
fn node_to_value(node: SyntaxNode) -> Value {
if let Some(val) = node.first_child().map(|n| n.into()) {
return val;
}
first_child_not_ws(node)
.map(|n| n.as_token().map(|t| t.clone().into()))
.flatten()
.unwrap()
}
fn node_to_array(node: SyntaxNode) -> Value {
let array = node.children().filter(ws).map(|n| n.into()).collect();
Value::Array(array)
}
fn node_to_array_item(node: SyntaxNode) -> Value {
node.first_child().map(|n| n.into()).unwrap()
}
fn node_to_comment(node: SyntaxNode) -> Value {
Value::Comment(node.token_text())
}
fn node_to_string(node: SyntaxNode) -> Value {
let mut string = node.token_text();
if string.starts_with("\"\"\"") {
string = strip_start_end(string, 3);
} else if string.starts_with('\"') || string.starts_with('\'') {
string = strip_start_end(string, 1);
}
Value::StrLit(string)
}
fn node_to_float(node: SyntaxNode) -> Value {
let float = node.token_text();
let cleaned = float.replace('_', "");
Value::Float(cleaned.parse().unwrap())
}
fn node_to_date(tkn: SyntaxNode) -> Value {
let raw_date = tkn.token_text();
let date = TomlDate::from_str(&raw_date);
Value::Date(date.unwrap())
}
fn token_to_int(tkn: SyntaxToken) -> Value {
let int = tkn.text();
integer(&int).unwrap()
}
fn token_to_bool(tkn: SyntaxToken) -> Value {
let raw_bool = tkn.text();
if raw_bool == "true" {
Value::Bool(true)
} else {
Value::Bool(false)
}
}
}
impl Into<Table> for SyntaxNode {
fn into(self) -> Table {
let header = self.first_child().map(|n| n.into()).unwrap();
let pairs = self.children().skip(1).map(|n| n.into()).collect();
Table {
header,
pairs,
comment: None,
}
}
}
impl Into<Heading> for SyntaxNode {
fn into(self) -> Heading {
let mut header = self.token_text();
if header.contains('[') {
header = header.split('[').collect::<Vec<_>>()[1].to_string();
}
if header.contains(']') {
header = header.split(']').collect::<Vec<_>>()[0].to_string();
}
let seg = header.split('.').map(|s| s.into()).collect::<Vec<_>>();
Heading { header, seg }
}
}
impl Into<InTable> for SyntaxNode {
fn into(self) -> InTable {
let pairs = self.children().map(|n| n.into()).collect();
let trailing_comma = false;
InTable {
pairs,
trailing_comma,
}
}
}
impl Into<KvPair> for SyntaxNode {
fn into(self) -> KvPair {
if self.kind() == TomlKind::Comment {
return KvPair {
key: None,
val: Value::None,
comment: Some(self.token_text()),
};
}
let key = self.first_child().map(|n| n.token_text());
let val = self
.children()
.find(|n| n.kind() == TomlKind::Value || n.kind() == TomlKind::Comment)
.filter(ws)
.map(|n| n.into())
.unwrap_or(Value::Eof);
KvPair {
key,
val,
comment: None,
}
}
}
impl Into<Value> for SyntaxNode {
fn into(self) -> Value {
match self.kind() {
TomlKind::Root => Value::Root,
TomlKind::Table => Value::Table(self.into()),
TomlKind::KeyValue => Value::KeyValue(Box::new(self.into())),
TomlKind::InlineTable => Value::InlineTable(self.into()),
TomlKind::Array => Value::node_to_array(self),
TomlKind::ArrayItem => Value::node_to_array_item(self),
TomlKind::Value => Value::node_to_value(self),
TomlKind::Date => Value::node_to_date(self),
TomlKind::Comment => Value::node_to_comment(self),
TomlKind::Str => Value::node_to_string(self),
TomlKind::Float => Value::node_to_float(self),
_ => unreachable!("may need to add nodes"),
}
}
}
impl Into<Value> for SyntaxToken {
fn into(self) -> Value {
match self.kind() {
TomlKind::Integer => Value::token_to_int(self),
TomlKind::Bool => Value::token_to_bool(self),
_ => unreachable!("may need to add nodes"),
}
}
}
impl Toml {
pub fn new(input: &str) -> Toml {
let root = parse_it(input).expect("parse failed").syntax();
Self {
items: root.children().map(|node| node.into()).collect(),
}
}
}
impl Value {
pub fn as_inline_table(&self) -> Option<&InTable> {
match self {
Value::InlineTable(table) => Some(table),
_ => None,
}
}
pub fn as_table(&self) -> Option<&Table> {
match self {
Value::Table(table) => Some(table),
_ => None,
}
}
pub fn as_key_value(&self) -> Option<&KvPair> {
match self {
Value::KeyValue(kv) => Some(kv),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[Value]> {
match self {
Value::Array(array) => Some(array),
_ => None,
}
}
pub fn sort_string_array(&mut self) {
if let Value::Array(array) = self {
let all_str = array.iter().all(|item| match item {
Value::StrLit(_) => true,
_ => false,
});
if !all_str {
return;
}
array.sort_by(|item, other| match item {
Value::StrLit(s) => match other {
Value::StrLit(o) => s.cmp(o),
_ => unreachable!(),
},
_ => unreachable!(),
})
}
}
}
impl KvPair {
fn key_match(&self, key: &str) -> bool {
self.key.as_ref().map(|k| k == key) == Some(true)
}
pub fn key(&self) -> Option<&str> {
self.key.as_deref()
}
pub fn value(&self) -> &Value {
&self.val
}
pub fn value_mut(&mut self) -> &mut Value {
&mut self.val
}
}
impl Table {
pub fn header(&self) -> &str {
&self.header.header
}
pub fn segments(&self) -> &[String] {
&self.header.seg
}
pub fn item_len(&self) -> usize {
self.pairs.len()
}
pub fn seg_len(&self) -> usize {
self.header.seg.len()
}
pub fn items(&self) -> &[KvPair] {
&self.pairs
}
pub fn get_key_value(&self, key: &str) -> Option<&KvPair> {
self.pairs.iter().find(|pair| pair.key_match(key))
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.pairs
.iter()
.find(|pair| pair.key_match(key))
.map(|pair| pair.value())
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
self.pairs
.iter_mut()
.find(|pair| pair.key_match(key))
.map(|pair| pair.value_mut())
}
pub fn sort(&mut self) {
self.pairs.sort()
}
#[allow(clippy::while_let_loop)]
pub fn combine_comments(&mut self) {
{
let cmt_clone = self.clone();
let mut zipped = cmt_clone.iter().zip(self.iter_mut().skip(1)).peekable();
loop {
if let Some((left, right)) = zipped.next() {
if let Some(comment) = &left.comment {
right.comment = Some(comment.into());
}
} else {
break;
}
}
}
self.pairs
.retain(|kv| !(kv.key().is_none() && kv.value() == &Value::None))
}
pub fn iter(&self) -> impl Iterator<Item = &KvPair> {
self.pairs.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut KvPair> {
self.pairs.iter_mut()
}
}
impl InTable {
pub fn len(&self) -> usize {
self.pairs.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.pairs
.iter()
.find(|pair| pair.key_match(key))
.map(|pair| pair.value())
}
}
impl Toml {
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get_table(&self, heading: &str) -> Option<&Table> {
self.iter()
.find(|val| match val {
Value::Table(tab) => tab.header() == heading,
_ => false,
})
.map(|table| {
if let Value::Table(tab) = table {
Some(tab)
} else {
None
}
})
.flatten()
}
pub fn get_table_mut(&mut self, heading: &str) -> Option<&mut Table> {
self.iter_mut()
.find(|val| match val {
Value::Table(tab) => tab.header() == heading,
_ => false,
})
.map(|table| {
if let Value::Table(tab) = table {
Some(tab)
} else {
None
}
})
.flatten()
}
pub fn get_contains_mut(&mut self, heading: &str) -> Vec<&mut Table> {
self.iter_mut()
.filter(|val| match val {
Value::Table(tab) => tab.header().contains(heading),
_ => false,
})
.flat_map(|table| {
if let Value::Table(tab) = table {
Some(tab)
} else {
None
}
})
.collect()
}
pub fn get_bare_value(&self, key: &str) -> Option<&Value> {
self.iter().find(|val| match val {
Value::KeyValue(kv) => kv.key() == Some(key),
_ => false,
})
}
pub fn sort_matching(&mut self, heading: &str) {
self.items.sort_by(|tab, other| match tab {
Value::Table(tab) => {
if tab.header().contains(heading) {
match other {
Value::Table(other) => {
if other.header().contains(heading) {
tab.segments().last().cmp(&other.segments().last())
} else {
Ordering::Equal
}
}
_ => Ordering::Equal,
}
} else {
Ordering::Equal
}
}
_ => Ordering::Equal,
})
}
#[allow(clippy::while_let_loop)]
pub fn combine_comments(&mut self) {
{
let cmt_clone = self.clone();
let mut zipped = cmt_clone.iter().zip(self.iter_mut().skip(1)).peekable();
loop {
if let Some((left, right)) = zipped.next() {
if let Value::Comment(comment) = left {
match right {
Value::Table(t) => {
t.combine_comments();
t.comment = Some(comment.into())
}
Value::KeyValue(kv) => kv.comment = Some(comment.into()),
Value::Comment(cmt) => cmt.push_str(&format!("{}\n", comment)),
_ => unreachable!("only kv, comments and tables"),
}
}
} else {
break;
}
}
}
self.items.retain(|val| {
if let Value::Comment(_) = val {
false
} else {
true
}
})
}
pub fn iter(&self) -> impl Iterator<Item = &Value> {
self.items.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Value> {
self.items.iter_mut()
}
}
#[cfg(test)]
mod test {
use super::*;
use std::fs::read_to_string;
#[test]
fn comment() {
let file = r#"# comment
[deps]
number = 1234
# comment
alpha = "beta"
"#;
let mut toml = Toml::new(file);
toml.combine_comments();
}
#[test]
fn into_structured() {
let file = r#"[deps]
alpha = "beta"
number = 1234
array = [ true, false, true ]
inline-table = { date = 1988-02-03T10:32:10, }
"#;
let toml = Toml::new(file);
assert!(toml.get_table("deps").is_some());
}
#[test]
fn ftop_file_struc() {
let input = read_to_string("examp/ftop.toml").expect("file read failed");
let parsed = Toml::new(&input);
assert_eq!(parsed.len(), 5);
}
#[test]
fn fend_file_struc() {
let input = read_to_string("examp/fend.toml").expect("file read failed");
let parsed = Toml::new(&input);
assert_eq!(parsed.len(), 6);
}
#[test]
fn seg_file_struc() {
let input = read_to_string("examp/seg.toml").expect("file read failed");
let parsed = Toml::new(&input);
assert_eq!(parsed.len(), 2);
}
#[test]
fn work_file_struc() {
let input = read_to_string("examp/work.toml").expect("file read failed");
let parsed = Toml::new(&input);
let members = parsed
.get_table("workspace")
.unwrap()
.get("members")
.unwrap();
assert_eq!(members.as_array().unwrap().len(), 4);
}
#[test]
fn all_value_types() {
let file = r#"[deps]
alpha = "beta"
number = 1234
array = [ true, false, true ]
inline-table = { date = 1988-02-03T10:32:10, }
"#;
let parsed = Toml::new(file);
assert_eq!(parsed.len(), 1);
let tab = parsed.get_table("deps").unwrap();
assert_eq!(tab.header(), "deps");
assert_eq!(tab.get("number").unwrap(), &Value::Int(1234));
}
#[test]
fn docs() {
let input = "examp = { first = 1, second = 2 }";
let toml = Toml::new(input);
if let Some(Value::KeyValue(kv)) = toml.get_bare_value("examp") {
let inline = kv.value().as_inline_table();
assert_eq!(inline.unwrap().get("second"), Some(&Value::Int(2)));
assert_eq!(inline.unwrap().get("first"), Some(&Value::Int(1)));
} else {
panic!("bare key value not found")
}
}
#[test]
fn merge_comments_ftop() {
let input = read_to_string("examp/ftop.toml").expect("file read failed");
let mut parsed = Toml::new(&input);
parsed.combine_comments();
let parse_cmp = parsed.clone();
assert_eq!(parsed, parse_cmp);
{
let deps = parsed.get_table_mut("dependencies").unwrap();
deps.sort();
}
parsed.sort_matching("dependencies.");
assert_ne!(parsed, parse_cmp);
}
#[test]
fn sort_ftop() {
let input = read_to_string("examp/ftop.toml").expect("file read failed");
let mut parsed = Toml::new(&input);
let parse_cmp = parsed.clone();
assert_eq!(parsed, parse_cmp);
{
let deps = parsed.get_table_mut("dependencies").unwrap();
deps.sort();
}
parsed.sort_matching("dependencies.");
assert_ne!(parsed, parse_cmp);
}
#[test]
fn sort_fend() {
let input = read_to_string("examp/fend.toml").expect("file read failed");
let mut parsed = Toml::new(&input);
let parse_cmp = parsed.clone();
assert_eq!(parsed, parse_cmp);
{
let deps = parsed.get_table_mut("dependencies").unwrap();
deps.sort();
}
parsed.sort_matching("dependencies.");
assert_ne!(parsed, parse_cmp);
}
#[test]
fn sort_win() {
let input = read_to_string("examp/win.toml").expect("file read failed");
let mut parsed = Toml::new(&input);
let parse_cmp = parsed.clone();
assert_eq!(parsed, parse_cmp);
{
let deps = parsed.get_table_mut("dependencies").unwrap();
deps.sort();
}
parsed.sort_matching("dependencies.");
assert_ne!(parsed, parse_cmp);
}
#[test]
fn sort_work() {
let input = read_to_string("examp/work.toml").expect("file read failed");
let mut parsed = Toml::new(&input);
let members = parsed
.get_table_mut("workspace")
.unwrap()
.get_mut("members")
.unwrap();
let mut mem_cmp = members.clone();
assert_eq!(*members, mem_cmp);
members.sort_string_array();
assert_ne!(*members, mem_cmp);
mem_cmp.sort_string_array();
assert_eq!(*members, mem_cmp);
}
}