use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use links_notation::{parse_lino_to_links, LiNo};
use std::collections::{HashMap, HashSet};
use std::fmt;
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),
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
}
}
(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 {
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)
}
}
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()])
}
}
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> {
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 {
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 == "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())),
}
}
}
}
}
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 decode(notation: &str) -> Result<LinoValue, CodecError> {
DEFAULT_CODEC.with(|codec| codec.borrow_mut().decode(notation))
}
pub mod format {
use super::{parse_lino_to_links, LiNo};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatError {
MissingField(String),
InvalidInput(String),
}
impl std::fmt::Display for FormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FormatError::MissingField(field) => write!(f, "Missing required field: {}", field),
FormatError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
}
}
}
impl std::error::Error for FormatError {}
pub fn escape_reference(value: &str) -> String {
let needs_escaping = value.chars().any(|c| {
c.is_whitespace() || c == '(' || c == ')' || c == '\'' || c == '"' || c == ':'
}) || value.contains('\n');
if !needs_escaping {
return value.to_string();
}
let has_single = value.contains('\'');
let has_double = value.contains('"');
if has_single && !has_double {
return format!("\"{}\"", value);
}
if has_double && !has_single {
return format!("'{}'", value);
}
if has_single && has_double {
let single_count = value.chars().filter(|&c| c == '\'').count();
let double_count = value.chars().filter(|&c| c == '"').count();
if double_count < single_count {
let escaped = value.replace('"', "\"\"");
return format!("\"{}\"", escaped);
}
let escaped = value.replace('\'', "''");
return format!("'{}'", escaped);
}
format!("'{}'", value)
}
pub fn unescape_reference(s: &str) -> String {
s.replace("\"\"", "\"").replace("''", "'")
}
fn format_indented_value(value: &str) -> String {
let has_single = value.contains('\'');
let has_double = value.contains('"');
if has_double && !has_single {
return format!("'{}'", value);
}
if has_single && !has_double {
return format!("\"{}\"", value);
}
if has_single && has_double {
let escaped = value.replace('\'', "''");
return format!("'{}'", escaped);
}
format!("\"{}\"", value)
}
pub fn format_indented<S: ::std::hash::BuildHasher>(
id: &str,
obj: &HashMap<String, String, S>,
indent: &str,
) -> Result<String, FormatError> {
if id.is_empty() {
return Err(FormatError::MissingField("id".to_string()));
}
let mut lines = vec![id.to_string()];
for (key, value) in obj {
let escaped_key = escape_reference(key);
let formatted_value = format_indented_value(value);
lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
}
Ok(lines.join("\n"))
}
pub fn format_indented_ordered(
id: &str,
pairs: &[(&str, &str)],
indent: &str,
) -> Result<String, FormatError> {
if id.is_empty() {
return Err(FormatError::MissingField("id".to_string()));
}
let mut lines = vec![id.to_string()];
for (key, value) in pairs {
let escaped_key = escape_reference(key);
let formatted_value = format_indented_value(value);
lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
}
Ok(lines.join("\n"))
}
pub fn parse_indented(text: &str) -> Result<(String, HashMap<String, String>), FormatError> {
if text.is_empty() {
return Err(FormatError::InvalidInput(
"text is required for parse_indented".to_string(),
));
}
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return Err(FormatError::InvalidInput(
"text must have at least one line (the identifier)".to_string(),
));
}
let non_empty_lines: Vec<&str> = lines
.iter()
.filter(|l| !l.trim().is_empty())
.copied()
.collect();
if non_empty_lines.is_empty() {
return Err(FormatError::InvalidInput(
"text must have at least one non-empty line (the identifier)".to_string(),
));
}
let first_line = non_empty_lines[0].trim();
let lino_text = if first_line.ends_with(':') {
non_empty_lines.join("\n")
} else {
format!("{}:\n{}", first_line, non_empty_lines[1..].join("\n"))
};
let parsed = parse_lino_to_links(&lino_text)
.map_err(|e| FormatError::InvalidInput(format!("Parse error: {:?}", e)))?;
if parsed.is_empty() {
return Err(FormatError::InvalidInput(
"Failed to parse indented Links Notation".to_string(),
));
}
let main_link = &parsed[0];
let (result_id, values) = match main_link {
LiNo::Link { id, values } => (id.clone().unwrap_or_default(), values),
LiNo::Ref(id) => (id.clone(), &vec![]),
};
let mut obj = HashMap::new();
for child in values {
if let LiNo::Link {
values: child_values,
..
} = child
{
if child_values.len() == 2 {
let key_ref = &child_values[0];
let value_ref = &child_values[1];
let key = match key_ref {
LiNo::Ref(k) => k.clone(),
LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
};
let value = match value_ref {
LiNo::Ref(v) => v.clone(),
LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
};
obj.insert(key, value);
}
}
}
Ok((result_id, obj))
}
}
#[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())
);
}
}