use lino_objects_codec::LinoValue;
use serde_json::{Map, Number, Value};
#[must_use]
pub fn to_lino(value: &Value) -> LinoValue {
match value {
Value::Null => LinoValue::Null,
Value::Bool(flag) => LinoValue::Bool(*flag),
Value::Number(number) => number.as_i64().map_or_else(
|| number.as_f64().map_or(LinoValue::Null, LinoValue::Float),
LinoValue::Int,
),
Value::String(text) => LinoValue::String(text.clone()),
Value::Array(items) => LinoValue::Array(items.iter().map(to_lino).collect()),
Value::Object(fields) => LinoValue::object(
fields
.iter()
.map(|(key, child)| (key.as_str(), to_lino(child))),
),
}
}
#[must_use]
pub fn from_lino(value: &LinoValue) -> Value {
match value {
LinoValue::Null => Value::Null,
LinoValue::Bool(flag) => Value::Bool(*flag),
LinoValue::Int(number) => Value::Number((*number).into()),
LinoValue::Float(number) => Number::from_f64(*number).map_or(Value::Null, Value::Number),
LinoValue::String(text) => Value::String(text.clone()),
LinoValue::Array(items) => Value::Array(items.iter().map(from_lino).collect()),
LinoValue::Object(fields) => Value::Object(
fields
.iter()
.map(|(key, child)| (key.clone(), from_lino(child)))
.collect::<Map<_, _>>(),
),
}
}
pub fn encode<T: serde::Serialize>(value: &T) -> Result<String, serde_json::Error> {
let json = serde_json::to_value(value)?;
Ok(lino_objects_codec::encode(&to_lino(&json)))
}
pub fn encode_line<T: serde::Serialize>(value: &T) -> Result<String, serde_json::Error> {
let json = serde_json::to_value(value)?;
let mut line = String::new();
write_line(&mut line, &json);
Ok(line)
}
const ARRAY_MARKER: &str = "#a";
const OBJECT_MARKER: &str = "#o";
const EMPTY_STRING: &str = "%z";
fn write_line(out: &mut String, value: &Value) {
match value {
Value::Null => out.push_str("()"),
Value::Bool(flag) => out.push_str(if *flag { "true" } else { "false" }),
Value::Number(number) => out.push_str(&number.to_string()),
Value::String(text) => write_quoted(out, text),
Value::Array(items) => {
out.push('(');
out.push_str(ARRAY_MARKER);
for item in items {
out.push(' ');
write_line(out, item);
}
out.push(')');
}
Value::Object(fields) => {
out.push('(');
out.push_str(OBJECT_MARKER);
for (name, field) in fields {
out.push_str(" (");
write_quoted(out, name);
out.push(' ');
write_line(out, field);
out.push(')');
}
out.push(')');
}
}
}
fn write_quoted(out: &mut String, text: &str) {
if text.is_empty() {
out.push('"');
out.push_str(EMPTY_STRING);
out.push('"');
return;
}
out.push('"');
for character in text.chars() {
match character {
'%' => out.push_str("%25"),
'"' => out.push_str("%22"),
'\\' => out.push_str("%5C"),
'(' => out.push_str("%28"),
')' => out.push_str("%29"),
'\n' => out.push_str("%0A"),
'\r' => out.push_str("%0D"),
'\t' => out.push_str("%09"),
other => out.push(other),
}
}
out.push('"');
}
fn unescape(text: &str) -> String {
if text == EMPTY_STRING {
return String::new();
}
let mut out = String::with_capacity(text.len());
let mut characters = text.chars();
while let Some(character) = characters.next() {
if character != '%' {
out.push(character);
continue;
}
let mut digits = characters.clone();
let pair: String = digits.by_ref().take(2).collect();
match u8::from_str_radix(&pair, 16) {
Ok(byte) if pair.len() == 2 => {
out.push(char::from(byte));
characters = digits;
}
_ => out.push('%'),
}
}
out
}
#[must_use]
pub fn decode_line(text: &str) -> Option<Value> {
let text = text.trim();
if text.is_empty() {
return None;
}
if is_json(text) {
return serde_json::from_str(text).ok();
}
let mut characters = text.chars().peekable();
if characters.peek() == Some(&'(') {
return read_value(&mut characters);
}
read_marked_value(&mut characters)
}
fn read_marked_value(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Value> {
skip_spaces(characters);
match characters.peek()? {
'"' => read_quoted_escaped(characters).map(Value::String),
'(' => read_group(characters),
_ => read_bare(characters),
}
}
fn read_value(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Value> {
skip_spaces(characters);
match characters.peek()? {
'"' => read_quoted(characters).map(Value::String),
'(' => read_group(characters),
_ => read_bare(characters),
}
}
fn read_group(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Value> {
characters.next();
read_group_body(characters)
}
fn read_group_body(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Value> {
skip_spaces(characters);
if characters.peek() == Some(&'#') {
characters.next();
let marker = characters.next()?;
return match marker {
'a' => read_marked_array(characters),
'o' => read_marked_object(characters),
_ => None,
};
}
read_unmarked_group_body(characters)
}
fn read_marked_array(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Value> {
let mut items = Vec::new();
loop {
skip_spaces(characters);
match characters.peek() {
None => return None,
Some(')') => {
characters.next();
return Some(Value::Array(items));
}
Some(_) => items.push(read_marked_value(characters)?),
}
}
}
fn read_marked_object(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Value> {
let mut fields = serde_json::Map::new();
loop {
skip_spaces(characters);
match characters.peek() {
Some(')') => {
characters.next();
return Some(Value::Object(fields));
}
Some('(') => {
characters.next();
skip_spaces(characters);
let name = read_quoted_escaped(characters)?;
let value = read_marked_value(characters)?;
skip_spaces(characters);
if characters.next() != Some(')') {
return None;
}
fields.insert(name, value);
}
_ => return None,
}
}
}
fn read_unmarked_group_body(
characters: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> Option<Value> {
let mut pairs: Vec<(String, Value)> = Vec::new();
let mut items: Vec<Value> = Vec::new();
let mut keyed = true;
loop {
skip_spaces(characters);
match characters.peek() {
None => return None,
Some(')') => {
characters.next();
break;
}
Some('(') => {
match read_pair(characters)? {
Group::Pair(name, value) => pairs.push((name, value)),
Group::NotAPair(value) => {
keyed = false;
items.push(value);
}
}
}
Some(_) => {
keyed = false;
items.push(read_value(characters)?);
}
}
}
if keyed && !pairs.is_empty() {
return Some(Value::Object(pairs.into_iter().collect()));
}
if pairs.is_empty() && items.is_empty() {
return Some(Value::Null);
}
Some(Value::Array(items))
}
enum Group {
Pair(String, Value),
NotAPair(Value),
}
fn read_pair(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Group> {
characters.next();
if characters.peek() != Some(&':') {
return read_group_body(characters).map(Group::NotAPair);
}
characters.next();
let name = read_quoted(characters)?;
let value = read_value(characters)?;
skip_spaces(characters);
if characters.next() != Some(')') {
return None;
}
Some(Group::Pair(name, value))
}
fn read_quoted(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<String> {
read_quoted_raw(characters)
}
fn read_quoted_raw(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<String> {
if characters.next() != Some('"') {
return None;
}
let mut text = String::new();
loop {
match characters.next()? {
'"' => return Some(text),
'\\' => match characters.next()? {
'n' => text.push('\n'),
'r' => text.push('\r'),
other => text.push(other),
},
other => text.push(other),
}
}
}
fn read_quoted_escaped(
characters: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> Option<String> {
read_quoted_raw(characters).as_deref().map(unescape)
}
fn read_bare(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<Value> {
let mut text = String::new();
while let Some(character) = characters.peek() {
if character.is_whitespace() || *character == ')' || *character == '(' {
break;
}
text.push(*character);
characters.next();
}
match text.as_str() {
"" => None,
"true" => Some(Value::Bool(true)),
"false" => Some(Value::Bool(false)),
"null" => Some(Value::Null),
number => number.parse::<serde_json::Number>().ok().map(Value::Number),
}
}
fn skip_spaces(characters: &mut std::iter::Peekable<std::str::Chars<'_>>) {
while characters
.peek()
.is_some_and(|character| character.is_whitespace())
{
characters.next();
}
}
pub fn decode<T: serde::de::DeserializeOwned>(text: &str) -> Result<T, serde_json::Error> {
if is_json(text) {
return serde_json::from_str(text);
}
lino_objects_codec::decode(text).map_or_else(
|_| serde_json::from_str(text),
|value| serde_json::from_value(from_lino(&value)),
)
}
fn is_json(text: &str) -> bool {
text.trim_start().starts_with(['{', '['])
}
#[cfg(test)]
#[path = "lino_json_tests.rs"]
mod tests;