use super::cirru_validator;
use cirru_parser::Cirru;
use std::fs;
use std::sync::Arc;
pub const ERR_MULTIPLE_INPUT_SOURCES: &str = "Multiple input sources provided. Use only one of: --file/-f, --code/-e, or --json/-j.";
pub const ERR_CONFLICTING_INPUT_FLAGS: &str = "Conflicting input flags: --leaf cannot be used with --json-input.";
pub const ERR_CODE_INPUT_REQUIRED: &str = "Code input required: use --file, --code, or --json";
pub const ERR_JSON_OBJECTS_NOT_SUPPORTED: &str = "JSON objects not supported, use arrays";
pub fn json_value_to_cirru(json: &serde_json::Value) -> Result<Cirru, String> {
match json {
serde_json::Value::String(s) => Ok(Cirru::Leaf(Arc::from(s.as_str()))),
serde_json::Value::Number(n) => Ok(Cirru::Leaf(Arc::from(n.to_string()))),
serde_json::Value::Bool(b) => Ok(Cirru::Leaf(Arc::from(b.to_string()))),
serde_json::Value::Null => Ok(Cirru::Leaf(Arc::from("nil"))),
serde_json::Value::Array(arr) => {
let items: Result<Vec<Cirru>, String> = arr.iter().map(json_value_to_cirru).collect();
Ok(Cirru::List(items?))
}
serde_json::Value::Object(_) => Err(ERR_JSON_OBJECTS_NOT_SUPPORTED.to_string()),
}
}
pub fn json_to_cirru(json_str: &str) -> Result<Cirru, String> {
let json_value: serde_json::Value = serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {e}"))?;
json_value_to_cirru(&json_value)
}
pub fn cirru_to_json_value(c: &Cirru) -> serde_json::Value {
match c {
Cirru::Leaf(s) => serde_json::Value::String(s.to_string()),
Cirru::List(items) => serde_json::Value::Array(items.iter().map(cirru_to_json_value).collect()),
}
}
pub fn cirru_to_json(node: &Cirru) -> String {
serde_json::to_string_pretty(&cirru_to_json_value(node)).unwrap_or_else(|_| "[]".to_string())
}
pub fn format_path_with_separator(path: &[usize], separator: &str) -> String {
path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(separator)
}
pub fn format_path(path: &[usize]) -> String {
format_path_with_separator(path, ".")
}
pub fn format_path_bracketed(path: &[usize]) -> String {
if path.is_empty() {
"root".to_string()
} else {
format!("[{}]", format_path(path))
}
}
pub fn parse_path(path_str: &str) -> Result<Vec<usize>, String> {
if path_str.is_empty() {
return Ok(vec![]);
}
if path_str.contains(',') {
return Err(format!(
"Invalid path '{path_str}': comma separator is no longer supported. Use dot-separated coordinates, e.g. '2.1.0'."
));
}
path_str
.split('.')
.map(|s| s.trim().parse::<usize>().map_err(|e| format!("Invalid path index '{s}': {e}")))
.collect()
}
pub fn validate_input_flags(leaf_input: bool, json_input: bool) -> Result<(), String> {
if leaf_input && json_input {
return Err(ERR_CONFLICTING_INPUT_FLAGS.to_string());
}
Ok(())
}
pub fn validate_input_sources(sources: &[bool]) -> Result<(), String> {
if sources.iter().filter(|&&enabled| enabled).count() > 1 {
Err(ERR_MULTIPLE_INPUT_SOURCES.to_string())
} else {
Ok(())
}
}
pub fn read_code_input(file: &Option<String>, code: &Option<String>, json: &Option<String>) -> Result<Option<String>, String> {
let sources = [file.is_some(), code.is_some(), json.is_some()];
validate_input_sources(&sources)?;
if let Some(path) = file {
let content = fs::read_to_string(path).map_err(|e| format!("Failed to read file '{path}': {e}"))?;
Ok(Some(content.trim().to_string()))
} else if let Some(s) = code {
if s.contains('\n') {
eprintln!("\n⚠️ Note: Inline code contains newlines. Multi-line code in shell can be error-prone.");
eprintln!(" Consider writing to a temporary file and using --file/-f instead.");
eprintln!();
}
Ok(Some(s.trim().to_string()))
} else if let Some(j) = json {
Ok(Some(j.clone()))
} else {
Ok(None)
}
}
pub fn warn_if_single_string_expression(node: &Cirru, input_source: &str) {
if let Cirru::List(items) = node {
if items.len() == 1 {
if let Some(Cirru::Leaf(_)) = items.first() {
eprintln!("\n⚠️ Note: Cirru one-liner input '{input_source}' was parsed as an expression (list with one element).");
eprintln!(" In Cirru syntax, this creates a list containing one element.");
eprintln!(" If you want a leaf node (plain string), use --leaf parameter.");
eprintln!(" Example: --leaf -e '{input_source}' creates a leaf, not an expression.\n");
}
}
}
}
fn warn_if_wrapped_by_parentheses(raw: &str, node: &Cirru) {
let t = raw.trim();
if t.starts_with('(') && t.ends_with(')') {
eprintln!("\n⚠️ Warning: One-liner input appears wrapped by top-level parentheses.");
eprintln!(" Cirru typically avoids wrapping the entire top-level expression with '()'.");
eprintln!(" This extra layer changes call semantics. Prefer removing the outer parentheses.\n");
eprintln!(" JSON echo:");
eprintln!("{}", cirru_to_json(node));
eprintln!();
}
}
pub fn parse_input_to_cirru(
raw: &str,
inline_json: &Option<String>,
json_input: bool,
leaf: bool,
auto_json: bool,
) -> Result<Cirru, String> {
validate_input_flags(leaf, json_input)?;
if let Some(j) = inline_json {
if j.len() > 2000 {
eprintln!("\n⚠️ Note: JSON input is very large ({} chars).", j.len());
eprintln!(" For large definitions, consider using placeholders and submitting in segments.");
eprintln!();
}
let node = json_to_cirru(j)?;
if leaf {
match node {
Cirru::Leaf(_) => Ok(node),
_ => Err("--leaf expects a JSON string (leaf node), but got a non-leaf JSON value.".to_string()),
}
} else {
Ok(node)
}
} else if leaf {
Ok(Cirru::Leaf(Arc::from(raw)))
} else if json_input {
if raw.len() > 2000 {
eprintln!("\n⚠️ Note: JSON input is very large ({} chars).", raw.len());
eprintln!(" For large definitions, consider using placeholders and submitting in segments.");
eprintln!();
}
json_to_cirru(raw)
} else {
if auto_json {
let trimmed = raw.trim();
let looks_like_json_string = trimmed.starts_with('"') && trimmed.ends_with('"');
let looks_like_json_array = trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.contains('"');
if looks_like_json_array || looks_like_json_string {
if trimmed.len() > 2000 {
eprintln!("\n⚠️ Note: JSON input is very large ({} chars).", trimmed.len());
eprintln!(" For large definitions, consider using placeholders and submitting in segments.");
eprintln!();
}
return json_to_cirru(trimmed).map_err(|e| format!("Failed to parse JSON from -e/--code: {e}"));
}
if trimmed.is_empty() {
return Err("Input is empty. Please provide Cirru code or use -j for JSON input.".to_string());
}
if raw.contains('\t') {
return Err(
"Input contains tab characters. Cirru requires spaces for indentation.\n\
Please replace tabs with 2 spaces.\n\
Tip: Use `cat -A file` to check for tabs (shown as ^I)."
.to_string(),
);
}
if raw.len() > 1000 {
eprintln!("\n⚠️ Note: Cirru one-liner input is very large ({} chars).", raw.len());
eprintln!(" For large definitions, consider using placeholders and submitting in segments.");
eprintln!();
}
let result = cirru_parser::parse_expr_one_liner(raw).map_err(|e| format!("Failed to parse Cirru one-liner expression: {e}"))?;
warn_if_wrapped_by_parentheses(raw, &result);
warn_if_single_string_expression(&result, raw);
cirru_validator::validate_cirru_syntax(&result)?;
return Ok(result);
}
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err("Input is empty. Please provide Cirru code or use -j for JSON input.".to_string());
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let after_bracket = &trimmed[1..];
let is_likely_json = after_bracket.starts_with('"')
|| after_bracket.starts_with(' ') && after_bracket.trim_start().starts_with('"')
|| after_bracket.starts_with('\n') && after_bracket.trim_start().starts_with('"');
let is_cirru_list = after_bracket.starts_with(']') || (after_bracket.starts_with(' ') && !after_bracket.trim_start().starts_with('"'));
if is_likely_json && !is_cirru_list {
return Err(
"Input appears to be JSON format (starts with '[\"').\n\
If you want to use JSON input, use one of:\n\
- inline JSON: cr edit def ns/name -j '[\"defn\", ...]'\n\
- inline code: cr edit def ns/name -e '[\"defn\", ...]'\n\
- file JSON: add -J or --json-input (e.g. -f code.json -J).\n\
Note: Cirru's [] list syntax (e.g. '[] 1 2 3') is different and will be parsed correctly."
.to_string(),
);
}
}
if raw.contains('\t') {
return Err(
"Input contains tab characters. Cirru requires spaces for indentation.\n\
Please replace tabs with 2 spaces.\n\
Tip: Use `cat -A file` to check for tabs (shown as ^I)."
.to_string(),
);
}
let parsed = cirru_parser::parse(raw).map_err(|e| {
let err_str = e.to_string();
let mut msg = format!("Failed to parse Cirru text: {err_str}");
if err_str.contains("odd indentation") {
msg.push_str("\n\nCirru requires 2-space indentation. Each nesting level must use exactly 2 spaces.");
msg.push_str("\nExample:\n defn my-fn (x)\n &+ x 1");
} else if err_str.contains("unexpected end of file") {
msg.push_str("\n\nPossible cause: missing closing quotes or unclosed structural pattern.");
}
msg
})?;
if parsed.len() == 1 {
let result = parsed.into_iter().next().unwrap();
warn_if_single_string_expression(&result, raw);
cirru_validator::validate_cirru_syntax(&result)?;
Ok(result)
} else if parsed.is_empty() {
Err("Input parsed as an empty Cirru structure.".to_string())
} else {
for node in &parsed {
cirru_validator::validate_cirru_syntax(node)?;
}
Ok(Cirru::List(parsed))
}
}
}
#[cfg(test)]
mod tests {
use super::{format_path, format_path_bracketed, format_path_with_separator, parse_path};
#[test]
fn rejects_comma_separated_paths() {
let err = parse_path("3,2,1").unwrap_err();
assert!(err.contains("comma separator is no longer supported"));
}
#[test]
fn parses_dot_separated_paths() {
assert_eq!(parse_path("3.2.1").unwrap(), vec![3, 2, 1]);
}
#[test]
fn rejects_mixed_separators() {
assert!(parse_path("3,2.1").is_err());
}
#[test]
fn formats_paths_with_dot_by_default() {
assert_eq!(format_path(&[3, 2, 1]), "3.2.1");
assert_eq!(format_path_bracketed(&[3, 2, 1]), "[3.2.1]");
assert_eq!(format_path_with_separator(&[3, 2, 1], ","), "3,2,1");
}
}