use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use links_notation::{LiNo, parse_lino_to_links};
use std::collections::{HashMap, HashSet};
use std::fmt;
pub mod debug;
#[cfg(feature = "serde_json")]
pub mod json;
pub mod readable;
pub use readable::{BASE64_MARKER, DEFAULT_INDENT, ESCAPED_MARKER, OBJECT_MARKER};
mod type_ids {
pub const NULL: &str = "null";
pub const BOOL: &str = "bool";
pub const INT: &str = "int";
pub const FLOAT: &str = "float";
pub const STR: &str = "str";
pub const ARRAY: &str = "array";
pub const OBJECT: &str = "object";
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecError {
ParseError(String),
DecodeError(String),
UnknownType(String),
}
impl fmt::Display for CodecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CodecError::ParseError(msg) => write!(f, "Parse error: {}", msg),
CodecError::DecodeError(msg) => write!(f, "Decode error: {}", msg),
CodecError::UnknownType(t) => write!(f, "Unknown type marker: {}", t),
}
}
}
impl std::error::Error for CodecError {}
#[derive(Debug, Clone)]
pub enum LinoValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
#[cfg(feature = "serde_json")]
JsonNumber(serde_json::Number),
String(String),
Array(Vec<LinoValue>),
Object(Vec<(String, LinoValue)>),
}
impl PartialEq for LinoValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(LinoValue::Null, LinoValue::Null) => true,
(LinoValue::Bool(a), LinoValue::Bool(b)) => a == b,
(LinoValue::Int(a), LinoValue::Int(b)) => a == b,
(LinoValue::Float(a), LinoValue::Float(b)) => {
if a.is_nan() && b.is_nan() {
true
} else {
a == b
}
}
#[cfg(feature = "serde_json")]
(LinoValue::JsonNumber(a), LinoValue::JsonNumber(b)) => a == b,
(LinoValue::String(a), LinoValue::String(b)) => a == b,
(LinoValue::Array(a), LinoValue::Array(b)) => a == b,
(LinoValue::Object(a), LinoValue::Object(b)) => {
if a.len() != b.len() {
return false;
}
let a_map: HashMap<&str, &LinoValue> =
a.iter().map(|(k, v)| (k.as_str(), v)).collect();
let b_map: HashMap<&str, &LinoValue> =
b.iter().map(|(k, v)| (k.as_str(), v)).collect();
a_map == b_map
}
_ => false,
}
}
}
impl LinoValue {
pub fn object<I, K, V>(iter: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<LinoValue>,
{
LinoValue::Object(
iter.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
pub fn array<I, V>(iter: I) -> Self
where
I: IntoIterator<Item = V>,
V: Into<LinoValue>,
{
LinoValue::Array(iter.into_iter().map(|v| v.into()).collect())
}
pub fn is_null(&self) -> bool {
matches!(self, LinoValue::Null)
}
pub fn as_bool(&self) -> Option<bool> {
match self {
LinoValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_int(&self) -> Option<i64> {
match self {
LinoValue::Int(i) => Some(*i),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
LinoValue::Float(f) => Some(*f),
LinoValue::Int(i) => Some(*i as f64),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
LinoValue::String(s) => Some(s),
_ => None,
}
}
pub fn as_array(&self) -> Option<&Vec<LinoValue>> {
match self {
LinoValue::Array(a) => Some(a),
_ => None,
}
}
pub fn as_object(&self) -> Option<&Vec<(String, LinoValue)>> {
match self {
LinoValue::Object(o) => Some(o),
_ => None,
}
}
pub fn get(&self, key: &str) -> Option<&LinoValue> {
match self {
LinoValue::Object(o) => o.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
pub fn get_index(&self, index: usize) -> Option<&LinoValue> {
match self {
LinoValue::Array(a) => a.get(index),
_ => None,
}
}
}
impl From<()> for LinoValue {
fn from(_: ()) -> Self {
LinoValue::Null
}
}
impl From<bool> for LinoValue {
fn from(b: bool) -> Self {
LinoValue::Bool(b)
}
}
impl From<i32> for LinoValue {
fn from(i: i32) -> Self {
LinoValue::Int(i as i64)
}
}
impl From<i64> for LinoValue {
fn from(i: i64) -> Self {
LinoValue::Int(i)
}
}
impl From<f64> for LinoValue {
fn from(f: f64) -> Self {
LinoValue::Float(f)
}
}
impl From<&str> for LinoValue {
fn from(s: &str) -> Self {
LinoValue::String(s.to_string())
}
}
impl From<String> for LinoValue {
fn from(s: String) -> Self {
LinoValue::String(s)
}
}
impl<T: Into<LinoValue>> From<Vec<T>> for LinoValue {
fn from(v: Vec<T>) -> Self {
LinoValue::Array(v.into_iter().map(|x| x.into()).collect())
}
}
impl<T: Into<LinoValue>> From<Option<T>> for LinoValue {
fn from(opt: Option<T>) -> Self {
match opt {
Some(v) => v.into(),
None => LinoValue::Null,
}
}
}
pub struct ObjectCodec {
encode_counter: usize,
encode_memo: HashMap<String, String>,
needs_id: HashSet<String>,
all_definitions: Vec<(String, LiNo<String>)>,
decode_memo: HashMap<String, LinoValue>,
all_links: Vec<LiNo<String>>,
}
impl Default for ObjectCodec {
fn default() -> Self {
Self::new()
}
}
impl ObjectCodec {
pub fn new() -> Self {
ObjectCodec {
encode_counter: 0,
encode_memo: HashMap::new(),
needs_id: HashSet::new(),
all_definitions: Vec::new(),
decode_memo: HashMap::new(),
all_links: Vec::new(),
}
}
fn reset_encode_state(&mut self) {
self.encode_counter = 0;
self.encode_memo.clear();
self.needs_id.clear();
self.all_definitions.clear();
}
fn reset_decode_state(&mut self) {
self.decode_memo.clear();
self.all_links.clear();
}
fn make_link(&self, parts: &[&str]) -> LiNo<String> {
let values: Vec<LiNo<String>> = parts.iter().map(|p| LiNo::Ref(p.to_string())).collect();
LiNo::Link { id: None, values }
}
fn make_ref(&self, id: &str) -> LiNo<String> {
LiNo::Ref(id.to_string())
}
fn object_key(&self, value: &LinoValue) -> String {
format!("{:p}", value)
}
fn find_objects_needing_ids(&mut self, value: &LinoValue, seen: &mut HashMap<String, bool>) {
match value {
LinoValue::Array(arr) => {
let key = self.object_key(value);
if seen.contains_key(&key) {
self.needs_id.insert(key);
return;
}
seen.insert(key, true);
for item in arr {
self.find_objects_needing_ids(item, seen);
}
}
LinoValue::Object(obj) => {
let key = self.object_key(value);
if seen.contains_key(&key) {
self.needs_id.insert(key);
return;
}
seen.insert(key, true);
for (_, v) in obj {
self.find_objects_needing_ids(v, seen);
}
}
_ => {}
}
}
pub fn encode(&mut self, value: &LinoValue) -> String {
readable::encode(value, DEFAULT_INDENT)
}
pub fn encode_with_indent(&mut self, value: &LinoValue, indent: &str) -> String {
readable::encode(value, indent)
}
pub fn encode_line(&mut self, value: &LinoValue) -> String {
readable::encode_line(value)
}
pub fn encode_compact(&mut self, value: &LinoValue) -> String {
self.reset_encode_state();
let mut seen = HashMap::new();
self.find_objects_needing_ids(value, &mut seen);
let mut visited = HashSet::new();
let main_link = self.encode_value(value, &mut visited, 0);
if !self.all_definitions.is_empty() {
let mut all_links = vec![main_link];
for (ref_id, link) in &self.all_definitions {
let main_id = match &all_links[0] {
LiNo::Link { id: Some(id), .. } => Some(id.clone()),
_ => None,
};
if main_id.as_ref() != Some(ref_id) {
all_links.push(link.clone());
}
}
all_links
.iter()
.map(Self::format_link)
.collect::<Vec<_>>()
.join("\n")
} else {
Self::format_link(&main_link)
}
}
pub fn encode_obfuscated(&mut self, value: &LinoValue) -> String {
self.encode_compact(value)
}
fn format_link(link: &LiNo<String>) -> String {
match link {
LiNo::Ref(s) => s.clone(),
LiNo::Link { id, values } => {
let inner: Vec<String> = values.iter().map(Self::format_link).collect();
if let Some(link_id) = id {
if inner.is_empty() {
format!("({}:)", link_id)
} else {
format!("({}: {})", link_id, inner.join(" "))
}
} else if inner.is_empty() {
"()".to_string()
} else {
format!("({})", inner.join(" "))
}
}
}
}
fn encode_value(
&mut self,
value: &LinoValue,
visited: &mut HashSet<String>,
depth: usize,
) -> LiNo<String> {
match value {
LinoValue::Null => self.make_link(&[type_ids::NULL]),
LinoValue::Bool(b) => {
if *b {
self.make_link(&[type_ids::BOOL, "true"])
} else {
self.make_link(&[type_ids::BOOL, "false"])
}
}
LinoValue::Int(i) => self.make_link(&[type_ids::INT, &i.to_string()]),
LinoValue::Float(f) => {
if f.is_nan() {
self.make_link(&[type_ids::FLOAT, "NaN"])
} else if f.is_infinite() {
if f.is_sign_positive() {
self.make_link(&[type_ids::FLOAT, "Infinity"])
} else {
self.make_link(&[type_ids::FLOAT, "-Infinity"])
}
} else {
self.make_link(&[type_ids::FLOAT, &f.to_string()])
}
}
#[cfg(feature = "serde_json")]
LinoValue::JsonNumber(n) => self.make_link(&["json_number", &n.to_string()]),
LinoValue::String(s) => {
let b64_encoded = BASE64.encode(s.as_bytes());
self.make_link(&[type_ids::STR, &b64_encoded])
}
LinoValue::Array(arr) => {
let obj_key = self.object_key(value);
if let Some(ref_id) = self.encode_memo.get(&obj_key).cloned() {
return self.make_ref(&ref_id);
}
let needs_id = self.needs_id.contains(&obj_key);
if needs_id {
if visited.contains(&obj_key) {
if let Some(ref_id) = self.encode_memo.get(&obj_key) {
return self.make_ref(ref_id);
}
}
let ref_id = format!("obj_{}", self.encode_counter);
self.encode_counter += 1;
self.encode_memo.insert(obj_key.clone(), ref_id.clone());
visited.insert(obj_key.clone());
let mut parts: Vec<LiNo<String>> = vec![LiNo::Ref(type_ids::ARRAY.to_string())];
for item in arr {
let item_link = self.encode_value(item, visited, depth + 1);
parts.push(item_link);
}
let definition = LiNo::Link {
id: Some(ref_id.clone()),
values: parts,
};
if depth > 0 {
self.all_definitions.push((ref_id.clone(), definition));
return self.make_ref(&ref_id);
}
definition
} else {
let mut parts: Vec<LiNo<String>> = vec![LiNo::Ref(type_ids::ARRAY.to_string())];
for item in arr {
let item_link = self.encode_value(item, visited, depth + 1);
parts.push(item_link);
}
LiNo::Link {
id: None,
values: parts,
}
}
}
LinoValue::Object(obj) => {
let obj_key = self.object_key(value);
if let Some(ref_id) = self.encode_memo.get(&obj_key).cloned() {
return self.make_ref(&ref_id);
}
let needs_id = self.needs_id.contains(&obj_key);
if needs_id {
if visited.contains(&obj_key) {
if let Some(ref_id) = self.encode_memo.get(&obj_key) {
return self.make_ref(ref_id);
}
}
let ref_id = format!("obj_{}", self.encode_counter);
self.encode_counter += 1;
self.encode_memo.insert(obj_key.clone(), ref_id.clone());
visited.insert(obj_key.clone());
let mut parts: Vec<LiNo<String>> =
vec![LiNo::Ref(type_ids::OBJECT.to_string())];
for (k, v) in obj {
let key_link =
self.encode_value(&LinoValue::String(k.clone()), visited, depth + 1);
let value_link = self.encode_value(v, visited, depth + 1);
let pair = LiNo::Link {
id: None,
values: vec![key_link, value_link],
};
parts.push(pair);
}
let definition = LiNo::Link {
id: Some(ref_id.clone()),
values: parts,
};
if depth > 0 {
self.all_definitions.push((ref_id.clone(), definition));
return self.make_ref(&ref_id);
}
definition
} else {
let mut parts: Vec<LiNo<String>> =
vec![LiNo::Ref(type_ids::OBJECT.to_string())];
for (k, v) in obj {
let key_link =
self.encode_value(&LinoValue::String(k.clone()), visited, depth + 1);
let value_link = self.encode_value(v, visited, depth + 1);
let pair = LiNo::Link {
id: None,
values: vec![key_link, value_link],
};
parts.push(pair);
}
LiNo::Link {
id: None,
values: parts,
}
}
}
}
}
pub fn decode(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
if notation.trim().is_empty() {
return Ok(LinoValue::Null);
}
if is_compact_notation(notation) {
crate::debug::trace("codec.decode", || "compact notation detected".to_string());
return self.decode_compact(notation);
}
crate::debug::trace("codec.decode", || "readable notation detected".to_string());
readable::decode(notation)
}
pub fn decode_line(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
readable::decode_line(notation)
}
pub fn decode_compact(&mut self, notation: &str) -> Result<LinoValue, CodecError> {
self.reset_decode_state();
let links = parse_lino_to_links(notation)
.map_err(|e| CodecError::ParseError(format!("{:?}", e)))?;
if links.is_empty() {
return Ok(LinoValue::Null);
}
if links.len() > 1 {
self.all_links = links.clone();
}
self.decode_link(&links[0])
}
fn decode_link(&mut self, link: &LiNo<String>) -> Result<LinoValue, CodecError> {
match link {
LiNo::Ref(id) => {
if let Some(value) = self.decode_memo.get(id) {
return Ok(value.clone());
}
if id.starts_with("obj_") && !self.all_links.is_empty() {
for other_link in self.all_links.clone() {
if let LiNo::Link {
id: Some(link_id), ..
} = &other_link
{
if link_id == id {
return self.decode_link(&other_link);
}
}
}
let result = LinoValue::Array(vec![]);
self.decode_memo.insert(id.clone(), result.clone());
return Ok(result);
}
match id.as_str() {
type_ids::NULL => return Ok(LinoValue::Null),
type_ids::ARRAY => return Ok(LinoValue::Array(vec![])),
type_ids::OBJECT => return Ok(LinoValue::Object(vec![])),
type_ids::STR => return Ok(LinoValue::String(String::new())),
_ => {}
}
Ok(LinoValue::String(id.clone()))
}
LiNo::Link { id, values } => {
let self_ref_id = id.as_ref().filter(|i| i.starts_with("obj_"));
if let Some(ref_id) = self_ref_id {
if let Some(value) = self.decode_memo.get(ref_id) {
return Ok(value.clone());
}
}
if values.is_empty() {
return Ok(LinoValue::Null);
}
let type_marker = match &values[0] {
LiNo::Ref(t) => t.as_str(),
LiNo::Link { .. } => return Ok(LinoValue::Null),
};
match type_marker {
#[cfg(feature = "serde_json")]
"json_number" => crate::json::decode_compact_number(values),
type_ids::NULL => Ok(LinoValue::Null),
type_ids::BOOL => {
if values.len() > 1 {
if let LiNo::Ref(val) = &values[1] {
return Ok(LinoValue::Bool(val.eq_ignore_ascii_case("true")));
}
}
Ok(LinoValue::Bool(false))
}
type_ids::INT => {
if values.len() > 1 {
if let LiNo::Ref(val) = &values[1] {
if let Ok(i) = val.parse::<i64>() {
return Ok(LinoValue::Int(i));
}
}
}
Ok(LinoValue::Int(0))
}
type_ids::FLOAT => {
if values.len() > 1 {
if let LiNo::Ref(val) = &values[1] {
return match val.as_str() {
"NaN" => Ok(LinoValue::Float(f64::NAN)),
"Infinity" => Ok(LinoValue::Float(f64::INFINITY)),
"-Infinity" => Ok(LinoValue::Float(f64::NEG_INFINITY)),
s => {
if let Ok(f) = s.parse::<f64>() {
Ok(LinoValue::Float(f))
} else {
Ok(LinoValue::Float(0.0))
}
}
};
}
}
Ok(LinoValue::Float(0.0))
}
type_ids::STR => {
if values.len() > 1 {
if let LiNo::Ref(b64_str) = &values[1] {
if let Ok(bytes) = BASE64.decode(b64_str) {
if let Ok(s) = String::from_utf8(bytes) {
return Ok(LinoValue::String(s));
}
}
return Ok(LinoValue::String(b64_str.clone()));
}
}
Ok(LinoValue::String(String::new()))
}
type_ids::ARRAY => {
let result_array = LinoValue::Array(vec![]);
if let Some(ref_id) = self_ref_id {
self.decode_memo.insert(ref_id.clone(), result_array);
}
let mut items = Vec::new();
for item_link in values.iter().skip(1) {
let decoded = self.decode_link(item_link)?;
items.push(decoded);
}
let result = LinoValue::Array(items);
if let Some(ref_id) = self_ref_id {
self.decode_memo.insert(ref_id.clone(), result.clone());
}
Ok(result)
}
type_ids::OBJECT => {
let result_object = LinoValue::Object(vec![]);
if let Some(ref_id) = self_ref_id {
self.decode_memo.insert(ref_id.clone(), result_object);
}
let mut obj = Vec::new();
for pair_link in values.iter().skip(1) {
if let LiNo::Link { values: pair, .. } = pair_link {
if pair.len() >= 2 {
let key = self.decode_link(&pair[0])?;
let value = self.decode_link(&pair[1])?;
if let LinoValue::String(k) = key {
obj.push((k, value));
}
}
}
}
let result = LinoValue::Object(obj);
if let Some(ref_id) = self_ref_id {
self.decode_memo.insert(ref_id.clone(), result.clone());
}
Ok(result)
}
unknown => Err(CodecError::UnknownType(unknown.to_string())),
}
}
}
}
}
const COMPACT_TYPE_MARKERS: &[&str] = &[
type_ids::NULL,
"None",
type_ids::BOOL,
type_ids::INT,
type_ids::FLOAT,
type_ids::STR,
type_ids::ARRAY,
"list",
type_ids::OBJECT,
"dict",
#[cfg(feature = "serde_json")]
"json_number",
];
const EMPTY_BODY_MARKERS: [&str; 2] = [type_ids::NULL, "None"];
fn is_compact_notation(notation: &str) -> bool {
let Some(first_line) = notation.lines().map(str::trim).find(|l| !l.is_empty()) else {
return false;
};
let Some(rest) = first_line.strip_prefix('(') else {
return false;
};
let (mut marker, mut rest) = split_token(rest.trim_start());
if let Some(id) = marker.strip_suffix(':') {
if !id.starts_with("obj_") {
return false;
}
(marker, rest) = split_token(rest.trim_start());
}
if !COMPACT_TYPE_MARKERS.contains(&marker) {
return false;
}
if EMPTY_BODY_MARKERS.contains(&marker) {
return rest.trim_start().starts_with(')');
}
true
}
fn split_token(input: &str) -> (&str, &str) {
let end = input
.find(|c: char| c.is_whitespace() || c == '(' || c == ')')
.unwrap_or(input.len());
input.split_at(end)
}
thread_local! {
static DEFAULT_CODEC: std::cell::RefCell<ObjectCodec> = std::cell::RefCell::new(ObjectCodec::new());
}
pub fn encode(value: &LinoValue) -> String {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode(value))
}
pub fn encode_with_indent(value: &LinoValue, indent: &str) -> String {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_with_indent(value, indent))
}
pub fn encode_line(value: &LinoValue) -> String {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_line(value))
}
pub fn decode_line(notation: &str) -> Result<LinoValue, CodecError> {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().decode_line(notation))
}
pub fn encode_compact(value: &LinoValue) -> String {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_compact(value))
}
pub fn encode_obfuscated(value: &LinoValue) -> String {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().encode_obfuscated(value))
}
pub fn decode(notation: &str) -> Result<LinoValue, CodecError> {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().decode(notation))
}
pub mod format;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_roundtrip_null() {
let original = LinoValue::Null;
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_roundtrip_bool() {
for value in [true, false] {
let original = LinoValue::Bool(value);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
}
#[test]
fn test_roundtrip_int() {
let test_values: Vec<i64> = vec![0, 1, -1, 42, -42, 123456789, -123456789];
for value in test_values {
let original = LinoValue::Int(value);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.as_int(), Some(value));
}
}
#[test]
fn test_roundtrip_float() {
let test_values: Vec<f64> = vec![0.0, 1.0, -1.0, 3.14, -3.14, 0.123456789, -999.999];
for value in test_values {
let original = LinoValue::Float(value);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
let decoded_f = decoded.as_float().unwrap();
assert!((decoded_f - value).abs() < 0.0001);
}
}
#[test]
fn test_float_special_values() {
let inf = LinoValue::Float(f64::INFINITY);
let encoded = encode(&inf);
let decoded = decode(&encoded).unwrap();
let decoded_f = decoded.as_float().unwrap();
assert!(decoded_f.is_infinite());
assert!(decoded_f.is_sign_positive());
let neg_inf = LinoValue::Float(f64::NEG_INFINITY);
let encoded = encode(&neg_inf);
let decoded = decode(&encoded).unwrap();
let decoded_f = decoded.as_float().unwrap();
assert!(decoded_f.is_infinite());
assert!(decoded_f.is_sign_negative());
let nan = LinoValue::Float(f64::NAN);
let encoded = encode(&nan);
let decoded = decode(&encoded).unwrap();
let decoded_f = decoded.as_float().unwrap();
assert!(decoded_f.is_nan());
}
#[test]
fn test_roundtrip_string() {
let test_values = [
"",
"hello",
"hello world",
"Hello, World!",
"multi\nline\nstring",
"tab\tseparated",
"special chars: @#$%^&*()",
];
for value in test_values {
let original = LinoValue::String(value.to_string());
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.as_str(), Some(value));
}
}
#[test]
fn test_string_with_quotes() {
let test_values = [
"string with 'single quotes'",
"string with \"double quotes\"",
"string with \"both\" 'quotes'",
];
for value in test_values {
let original = LinoValue::String(value.to_string());
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.as_str(), Some(value));
}
}
#[test]
fn test_unicode_string() {
let value = "unicode: 你好世界 🌍";
let original = LinoValue::String(value.to_string());
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.as_str(), Some(value));
}
#[test]
fn test_roundtrip_empty_array() {
let original = LinoValue::Array(vec![]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_roundtrip_simple_array() {
let original = LinoValue::Array(vec![
LinoValue::Int(1),
LinoValue::Int(2),
LinoValue::Int(3),
]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_roundtrip_mixed_array() {
let original = LinoValue::Array(vec![
LinoValue::Int(1),
LinoValue::String("hello".to_string()),
LinoValue::Bool(true),
LinoValue::Null,
]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_nested_arrays() {
let original = LinoValue::Array(vec![LinoValue::Array(vec![])]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
let original2 = LinoValue::Array(vec![
LinoValue::Array(vec![LinoValue::Int(1), LinoValue::Int(2)]),
LinoValue::Array(vec![LinoValue::Int(3), LinoValue::Int(4)]),
]);
let encoded2 = encode(&original2);
let decoded2 = decode(&encoded2).unwrap();
assert_eq!(decoded2, original2);
}
#[test]
fn test_roundtrip_empty_object() {
let original = LinoValue::Object(vec![]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_roundtrip_simple_object() {
let original = LinoValue::object([("a", LinoValue::Int(1)), ("b", LinoValue::Int(2))]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_nested_objects() {
let original = LinoValue::object([(
"user",
LinoValue::object([
("name", LinoValue::String("Alice".to_string())),
(
"address",
LinoValue::object([
("city", LinoValue::String("NYC".to_string())),
("zip", LinoValue::String("10001".to_string())),
]),
),
]),
)]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_complex_structure() {
let original = LinoValue::object([
("id", LinoValue::Int(123)),
("name", LinoValue::String("Test Object".to_string())),
("active", LinoValue::Bool(true)),
(
"tags",
LinoValue::array([
LinoValue::String("tag1".to_string()),
LinoValue::String("tag2".to_string()),
LinoValue::String("tag3".to_string()),
]),
),
(
"metadata",
LinoValue::object([
("created", LinoValue::String("2025-01-01".to_string())),
("modified", LinoValue::Null),
("count", LinoValue::Int(42)),
]),
),
(
"items",
LinoValue::array([
LinoValue::object([
("id", LinoValue::Int(1)),
("value", LinoValue::String("first".to_string())),
]),
LinoValue::object([
("id", LinoValue::Int(2)),
("value", LinoValue::String("second".to_string())),
]),
]),
),
]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_list_of_dicts() {
let original = LinoValue::array([
LinoValue::object([
("name", LinoValue::String("Alice".to_string())),
("age", LinoValue::Int(30)),
]),
LinoValue::object([
("name", LinoValue::String("Bob".to_string())),
("age", LinoValue::Int(25)),
]),
]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_dict_of_lists() {
let original = LinoValue::object([
(
"numbers",
LinoValue::array([LinoValue::Int(1), LinoValue::Int(2), LinoValue::Int(3)]),
),
(
"strings",
LinoValue::array([
LinoValue::String("a".to_string()),
LinoValue::String("b".to_string()),
LinoValue::String("c".to_string()),
]),
),
]);
let encoded = encode(&original);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
}
#[cfg(test)]
mod format_tests {
use super::format::*;
use std::collections::HashMap;
#[test]
fn test_escape_reference_simple_string() {
assert_eq!(escape_reference("hello"), "hello");
assert_eq!(escape_reference("world"), "world");
}
#[test]
fn test_escape_reference_string_with_spaces() {
let result = escape_reference("hello world");
assert!(result.starts_with('\'') || result.starts_with('"'));
assert!(result.contains("hello world"));
}
#[test]
fn test_escape_reference_string_with_single_quotes() {
let result = escape_reference("it's");
assert_eq!(result, "\"it's\"");
}
#[test]
fn test_escape_reference_string_with_double_quotes() {
let result = escape_reference("he said \"hello\"");
assert_eq!(result, "'he said \"hello\"'");
}
#[test]
fn test_unescape_reference_doubled_quotes() {
assert_eq!(
unescape_reference("he said \"\"hello\"\""),
"he said \"hello\""
);
assert_eq!(unescape_reference("it''s"), "it's");
}
#[test]
fn test_format_indented_ordered_basic() {
let pairs = [
("uuid", "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019"),
("status", "executed"),
("command", "echo test"),
("exitCode", "0"),
];
let result =
format_indented_ordered("6dcf4c1b-ff3f-482c-95ab-711ea7d1b019", &pairs, " ").unwrap();
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines[0], "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
assert_eq!(lines[1], " uuid \"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\"");
assert_eq!(lines[2], " status \"executed\"");
assert_eq!(lines[3], " command \"echo test\"");
assert_eq!(lines[4], " exitCode \"0\"");
}
#[test]
fn test_format_indented_value_with_quotes() {
let pairs = [("message", "He said \"hello\"")];
let result = format_indented_ordered("test-id", &pairs, " ").unwrap();
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines[1], " message 'He said \"hello\"'");
}
#[test]
fn test_format_indented_requires_id() {
let mut obj = HashMap::new();
obj.insert("key".to_string(), "value".to_string());
let result = format_indented("", &obj, " ");
assert!(result.is_err());
}
#[test]
fn test_parse_indented_basic() {
let text = "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\n uuid \"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019\"\n status \"executed\"\n exitCode \"0\"";
let (id, obj) = parse_indented(text).unwrap();
assert_eq!(id, "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
assert_eq!(
obj.get("uuid"),
Some(&"6dcf4c1b-ff3f-482c-95ab-711ea7d1b019".to_string())
);
assert_eq!(obj.get("status"), Some(&"executed".to_string()));
assert_eq!(obj.get("exitCode"), Some(&"0".to_string()));
}
#[test]
fn test_parse_indented_with_quotes() {
let text = "test-id\n message 'He said \"hello\"'";
let (id, obj) = parse_indented(text).unwrap();
assert_eq!(id, "test-id");
assert_eq!(obj.get("message"), Some(&"He said \"hello\"".to_string()));
}
#[test]
fn test_parse_indented_empty_lines_skipped() {
let text = "test-id\n\n key \"value\"\n\n another \"value2\"";
let (id, obj) = parse_indented(text).unwrap();
assert_eq!(id, "test-id");
assert_eq!(obj.get("key"), Some(&"value".to_string()));
assert_eq!(obj.get("another"), Some(&"value2".to_string()));
}
#[test]
fn test_parse_indented_requires_text() {
let result = parse_indented("");
assert!(result.is_err());
}
#[test]
fn test_roundtrip_format_indented() {
let pairs = [
("uuid", "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019"),
("status", "executed"),
("command", "echo test"),
("exitCode", "0"),
];
let formatted =
format_indented_ordered("6dcf4c1b-ff3f-482c-95ab-711ea7d1b019", &pairs, " ").unwrap();
let (parsed_id, parsed_obj) = parse_indented(&formatted).unwrap();
assert_eq!(parsed_id, "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019");
for (key, value) in pairs {
assert_eq!(parsed_obj.get(key), Some(&value.to_string()));
}
}
#[test]
fn test_roundtrip_with_quotes() {
let pairs = [("message", "He said \"hello\"")];
let formatted = format_indented_ordered("test-id", &pairs, " ").unwrap();
let (parsed_id, parsed_obj) = parse_indented(&formatted).unwrap();
assert_eq!(parsed_id, "test-id");
assert_eq!(
parsed_obj.get("message"),
Some(&"He said \"hello\"".to_string())
);
}
}