use crate::OqError;
use serde_json::Value;
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum InputFormat {
#[default]
Auto,
Json,
Yaml,
Toml,
Toon,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Json,
Yaml,
Toml,
Toon,
}
impl Format {
pub fn name(&self) -> &'static str {
match self {
Format::Json => "json",
Format::Yaml => "yaml",
Format::Toml => "toml",
Format::Toon => "toon",
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"json" => Some(Format::Json),
"yaml" | "yml" => Some(Format::Yaml),
"toml" => Some(Format::Toml),
"toon" => Some(Format::Toon),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum OutputFormat {
Json,
Yaml,
Toml,
#[default]
Toon,
}
impl From<OutputFormat> for Format {
fn from(f: OutputFormat) -> Self {
match f {
OutputFormat::Json => Format::Json,
OutputFormat::Yaml => Format::Yaml,
OutputFormat::Toml => Format::Toml,
OutputFormat::Toon => Format::Toon,
}
}
}
impl InputFormat {
pub fn detect(&self, input: &str) -> Format {
match self {
InputFormat::Json => Format::Json,
InputFormat::Yaml => Format::Yaml,
InputFormat::Toml => Format::Toml,
InputFormat::Toon => Format::Toon,
InputFormat::Auto => detect_format(input),
}
}
}
pub fn detect_format(input: &str) -> Format {
let trimmed = input.trim();
let lines: Vec<&str> = trimmed.lines().collect();
if trimmed.is_empty() {
return Format::Json;
}
let first_line = lines.first().map(|l| l.trim()).unwrap_or("");
if first_line.starts_with('[')
&& first_line.ends_with(']')
&& !first_line.contains(',')
&& !first_line.contains(':')
{
return Format::Toml;
}
if trimmed.starts_with('{') || trimmed.starts_with('[') {
return Format::Json;
}
let has_toml_sections = lines.iter().any(|line| {
let l = line.trim();
l.starts_with('[')
&& l.ends_with(']')
&& !l.starts_with("[[")
&& !l.contains(',')
&& !l.contains(':')
});
let has_toml_array_tables = lines
.iter()
.any(|line| line.trim().starts_with("[[") && line.trim().ends_with("]]"));
let has_toml_assignments = lines.iter().any(|line| {
let l = line.trim();
if l.is_empty() || l.starts_with('#') || l.starts_with('[') {
return false;
}
if let Some(eq_pos) = l.find(" = ") {
let key = &l[..eq_pos];
!key.is_empty()
&& key
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '"')
} else {
false
}
});
if has_toml_sections || has_toml_array_tables || has_toml_assignments {
return Format::Toml;
}
if trimmed.starts_with("---") {
return Format::Yaml;
}
let has_yaml_list = lines.iter().any(|line| {
let l = line.trim();
l.starts_with("- ") || l == "-"
});
let has_yaml_multiline = lines.iter().any(|line| {
let l = line.trim();
l.ends_with(": |") || l.ends_with(": >") || l.ends_with(": |+") || l.ends_with(": |-")
});
let has_yaml_nesting = lines
.iter()
.any(|line| line.starts_with(" ") && line.trim().contains(": "));
if has_yaml_list || has_yaml_multiline || has_yaml_nesting {
return Format::Yaml;
}
if trimmed == "true" || trimmed == "false" || trimmed == "null" {
return Format::Json;
}
if trimmed.parse::<f64>().is_ok() {
return Format::Json;
}
let has_colon_space = lines.iter().any(|line| {
let l = line.trim();
!l.is_empty() && !l.starts_with('#') && l.contains(": ")
});
if has_colon_space {
return Format::Toon;
}
Format::Json
}
pub fn parse_input(input: &str, format: Format) -> Result<Value, OqError> {
match format {
Format::Json => serde_json::from_str(input).map_err(OqError::JsonParse),
Format::Yaml => serde_yaml::from_str(input).map_err(OqError::YamlParse),
Format::Toml => {
let toml_value: toml::Value =
toml::from_str(input).map_err(|e| OqError::TomlParse(e.to_string()))?;
toml_to_json(toml_value)
}
Format::Toon => {
toon_format::decode_default(input).map_err(|e| OqError::ToonParse(e.to_string()))
}
}
}
pub fn parse_auto(input: &str) -> Result<Value, OqError> {
let format = detect_format(input);
parse_input(input, format)
}
fn toml_to_json(value: toml::Value) -> Result<Value, OqError> {
match value {
toml::Value::String(s) => Ok(Value::String(s)),
toml::Value::Integer(i) => Ok(Value::Number(i.into())),
toml::Value::Float(f) => serde_json::Number::from_f64(f)
.map(Value::Number)
.ok_or_else(|| OqError::TomlParse("Invalid float value".to_string())),
toml::Value::Boolean(b) => Ok(Value::Bool(b)),
toml::Value::Datetime(dt) => Ok(Value::String(dt.to_string())),
toml::Value::Array(arr) => {
let json_arr: Result<Vec<_>, _> = arr.into_iter().map(toml_to_json).collect();
Ok(Value::Array(json_arr?))
}
toml::Value::Table(table) => {
let mut map = serde_json::Map::new();
for (k, v) in table {
map.insert(k, toml_to_json(v)?);
}
Ok(Value::Object(map))
}
}
}
fn json_to_toml(value: &Value) -> Result<toml::Value, OqError> {
match value {
Value::Null => Ok(toml::Value::String("null".to_string())),
Value::Bool(b) => Ok(toml::Value::Boolean(*b)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(toml::Value::Integer(i))
} else if let Some(f) = n.as_f64() {
Ok(toml::Value::Float(f))
} else {
Err(OqError::TomlParse("Invalid number".to_string()))
}
}
Value::String(s) => Ok(toml::Value::String(s.clone())),
Value::Array(arr) => {
let toml_arr: Result<Vec<_>, _> = arr.iter().map(json_to_toml).collect();
Ok(toml::Value::Array(toml_arr?))
}
Value::Object(obj) => {
let mut table = toml::map::Map::new();
for (k, v) in obj {
table.insert(k.clone(), json_to_toml(v)?);
}
Ok(toml::Value::Table(table))
}
}
}
pub fn encode_to_format(value: &Value, format: Format) -> Result<String, OqError> {
match format {
Format::Json => serde_json::to_string_pretty(value).map_err(OqError::JsonParse),
Format::Yaml => serde_yaml::to_string(value).map_err(OqError::YamlParse),
Format::Toml => {
let toml_value = json_to_toml(value)?;
toml::to_string_pretty(&toml_value).map_err(|e| OqError::TomlParse(e.to_string()))
}
Format::Toon => to_toon(value),
}
}
pub fn to_toon(value: &Value) -> Result<String, OqError> {
toon_format::encode_default(value).map_err(|e| OqError::ToonParse(e.to_string()))
}
pub fn to_json(value: &Value, pretty: bool) -> Result<String, OqError> {
if pretty {
serde_json::to_string_pretty(value).map_err(OqError::JsonParse)
} else {
serde_json::to_string(value).map_err(OqError::JsonParse)
}
}
pub fn to_yaml(value: &Value) -> Result<String, OqError> {
serde_yaml::to_string(value).map_err(OqError::YamlParse)
}
pub fn to_toml(value: &Value) -> Result<String, OqError> {
let toml_value = json_to_toml(value)?;
toml::to_string_pretty(&toml_value).map_err(|e| OqError::TomlParse(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_json_object() {
assert_eq!(detect_format(r#"{"name": "Ada"}"#), Format::Json);
}
#[test]
fn test_detect_json_array() {
assert_eq!(detect_format(r#"[1, 2, 3]"#), Format::Json);
}
#[test]
fn test_detect_toon_object() {
assert_eq!(detect_format("name: Ada\nage: 30"), Format::Toon);
}
#[test]
fn test_detect_json_literals() {
assert_eq!(detect_format("true"), Format::Json);
assert_eq!(detect_format("false"), Format::Json);
assert_eq!(detect_format("null"), Format::Json);
assert_eq!(detect_format("42"), Format::Json);
assert_eq!(detect_format("3.14"), Format::Json);
}
#[test]
fn test_detect_yaml() {
assert_eq!(detect_format("---\nname: Ada"), Format::Yaml);
assert_eq!(detect_format("- item1\n- item2"), Format::Yaml);
assert_eq!(detect_format("user:\n name: Ada"), Format::Yaml);
}
#[test]
fn test_detect_toml() {
assert_eq!(detect_format("[package]\nname = \"test\""), Format::Toml);
assert_eq!(detect_format("name = \"Alice\""), Format::Toml);
}
#[test]
fn test_parse_json() {
let input = r#"{"name": "Ada", "age": 30}"#;
let result = parse_input(input, Format::Json).unwrap();
assert_eq!(result["name"], "Ada");
assert_eq!(result["age"], 30);
}
#[test]
fn test_parse_yaml() {
let input = "name: Ada\nage: 30";
let result = parse_input(input, Format::Yaml).unwrap();
assert_eq!(result["name"], "Ada");
assert_eq!(result["age"], 30);
}
#[test]
fn test_parse_toml() {
let input = "name = \"Ada\"\nage = 30";
let result = parse_input(input, Format::Toml).unwrap();
assert_eq!(result["name"], "Ada");
assert_eq!(result["age"], 30);
}
#[test]
fn test_parse_toon() {
let input = "name: Ada\nage: 30";
let result = parse_input(input, Format::Toon).unwrap();
assert_eq!(result["name"], "Ada");
assert_eq!(result["age"], 30);
}
#[test]
fn test_roundtrip_formats() {
let json = serde_json::json!({"name": "Ada", "active": true});
let json_str = to_json(&json, false).unwrap();
let back: Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(json, back);
let yaml_str = to_yaml(&json).unwrap();
let back: Value = serde_yaml::from_str(&yaml_str).unwrap();
assert_eq!(json, back);
let toon_str = to_toon(&json).unwrap();
let back = parse_input(&toon_str, Format::Toon).unwrap();
assert_eq!(json, back);
}
}