use super::identifier_for;
use super::types::{
NonRoundtrip, TY_ARRAY, TY_OBJECT, TypeMode, apply_type_annotation, entry_from_nu_value,
is_kdl_literal,
};
use kdl::{KdlDocument, KdlNode};
use nu_engine::command_prelude::*;
const ANON: &str = "-";
pub(crate) fn jik_document_to_value(
document: &KdlDocument,
span: Span,
ignore_types: bool,
) -> Result<Value, ShellError> {
let nodes = document.nodes();
if nodes.is_empty() {
return Err(ShellError::CantConvert {
to_type: "JiK value".into(),
from_type: "empty KDL document".into(),
span,
help: Some(
"JiK expects a single top-level node; use --format nodes for documents".into(),
),
});
}
if nodes.len() > 1 {
return Err(ShellError::CantConvert {
to_type: "JiK value".into(),
from_type: "multi-node KDL document".into(),
span,
help: Some(
"JiK expects a single top-level node (use --format nodes for full documents)"
.into(),
),
});
}
jik_node_to_value(&nodes[0], span, ignore_types)
}
fn jik_node_to_value(node: &KdlNode, span: Span, ignore_types: bool) -> Result<Value, ShellError> {
let node_ty = node.ty().map(|t| t.value());
if let Some(ty) = node_ty
&& ty != TY_ARRAY
&& ty != TY_OBJECT
&& !ignore_types
&& !is_known_value_type(ty)
{
return Err(unknown_jik_type(ty, span));
}
let entries = node.entries();
let children = node.children().map(|d| d.nodes()).unwrap_or(&[]);
let has_props = entries.iter().any(|e| e.name().is_some());
let has_args = entries.iter().any(|e| e.name().is_none());
let all_children_anon = children.iter().all(|c| c.name().value() == ANON);
let any_children = !children.is_empty();
let is_array_annotated = node_ty == Some(TY_ARRAY);
let is_object_annotated = node_ty == Some(TY_OBJECT);
if entries.is_empty() && !any_children {
return match node_ty {
Some(TY_ARRAY) => Ok(Value::list(vec![], span)),
Some(TY_OBJECT) => Ok(Value::record(Record::new(), span)),
_ => Err(ShellError::CantConvert {
to_type: "JiK value".into(),
from_type: "empty KDL node without (array) or (object)".into(),
span,
help: Some(
"empty array is `(array)-`; empty object is `(object)-`. Or use --format nodes."
.into(),
),
}),
};
}
if !has_props && !any_children && entries.len() == 1 && entries[0].name().is_none() {
if is_array_annotated {
let item = entry_to_jik_literal(&entries[0], span, ignore_types)?;
return Ok(Value::list(vec![item], span));
}
if is_object_annotated {
return Err(invalid_jik(
"object node cannot be a single bare argument",
span,
));
}
return entry_to_jik_literal(&entries[0], span, ignore_types);
}
let looks_like_array =
!has_props && (has_args || any_children) && all_children_anon && !is_object_annotated;
let looks_like_object = !has_args && (has_props || any_children) && !is_array_annotated;
if has_args && has_props {
return Err(invalid_jik(
"JiK node cannot mix unnamed arguments and properties",
span,
));
}
if is_array_annotated || looks_like_array {
if has_props || !all_children_anon {
return Err(invalid_jik(
"JiK array nodes may only have unnamed arguments and '-' children",
span,
));
}
return jik_array_to_value(entries, children, span, ignore_types);
}
if is_object_annotated || looks_like_object {
if has_args {
return Err(invalid_jik(
"JiK object nodes may only have properties and children",
span,
));
}
return jik_object_to_value(entries, children, span, ignore_types);
}
if has_args && any_children && !all_children_anon {
return Err(invalid_jik("JiK array children must be named '-'", span));
}
Err(invalid_jik(
"node is not valid JSON-in-KDL (try --format nodes)",
span,
))
}
fn jik_array_to_value(
entries: &[kdl::KdlEntry],
children: &[KdlNode],
span: Span,
ignore_types: bool,
) -> Result<Value, ShellError> {
let mut items = Vec::new();
for entry in entries {
if entry.name().is_some() {
return Err(invalid_jik("JiK array cannot have properties", span));
}
items.push(entry_to_jik_literal(entry, span, ignore_types)?);
}
for child in children {
if child.name().value() != ANON {
return Err(invalid_jik("JiK array children must be named '-'", span));
}
items.push(jik_node_to_value(child, span, ignore_types)?);
}
Ok(Value::list(items, span))
}
fn jik_object_to_value(
entries: &[kdl::KdlEntry],
children: &[KdlNode],
span: Span,
ignore_types: bool,
) -> Result<Value, ShellError> {
let mut record = Record::new();
let mut keys = std::collections::HashSet::new();
for entry in entries {
let Some(name) = entry.name() else {
return Err(invalid_jik(
"JiK object cannot have unnamed arguments",
span,
));
};
let key = name.value().to_string();
if !keys.insert(key.clone()) {
return Err(invalid_jik(
&format!("duplicate object key '{key}' in JiK node"),
span,
));
}
record.insert(key, entry_to_jik_literal(entry, span, ignore_types)?);
}
for child in children {
let key = child.name().value().to_string();
if !keys.insert(key.clone()) {
return Err(invalid_jik(
&format!("duplicate object key '{key}' in JiK node"),
span,
));
}
record.insert(key, jik_node_to_value(child, span, ignore_types)?);
}
Ok(record.into_value(span))
}
fn entry_to_jik_literal(
entry: &kdl::KdlEntry,
span: Span,
ignore_types: bool,
) -> Result<Value, ShellError> {
let ty = entry.ty().map(|t| t.value());
if let Some(t) = ty
&& (t == TY_ARRAY || t == TY_OBJECT)
{
return Err(invalid_jik(
"array/object type annotations belong on nodes, not entry values",
span,
));
}
let typed = apply_type_annotation(entry.value(), ty, span, ignore_types, TypeMode::Strict)?;
Ok(typed.into_value(span))
}
fn is_known_value_type(ty: &str) -> bool {
matches!(
ty,
super::types::TY_FILESIZE
| super::types::TY_DURATION
| super::types::TY_TIMESTAMP
| super::types::TY_DATETIME
| super::types::TY_BINARY
| super::types::TY_GLOB
| super::types::TY_RANGE
| super::types::TY_CELL_PATH
)
}
fn unknown_jik_type(ty: &str, span: Span) -> ShellError {
ShellError::UnsupportedInput {
msg: format!(
"unknown KDL type annotation '{ty}' in JiK mode; use --ignore-types or --format nodes"
),
input: "value originates from here".into(),
msg_span: span,
input_span: span,
}
}
fn invalid_jik(msg: &str, span: Span) -> ShellError {
ShellError::CantConvert {
to_type: "JiK / Nushell value".into(),
from_type: "KDL".into(),
span,
help: Some(format!(
"{msg}. Hint: use `from kdl --format nodes` for full KDL documents."
)),
}
}
pub(crate) fn value_to_jik_document(
value: &Value,
non_roundtrip: &NonRoundtrip,
call_span: Span,
) -> Result<KdlDocument, ShellError> {
let node = value_to_jik_node(value, ANON, non_roundtrip, call_span)?;
let mut document = KdlDocument::new();
document.nodes_mut().push(node);
Ok(document)
}
fn value_to_jik_node(
value: &Value,
name: &str,
non_roundtrip: &NonRoundtrip,
call_span: Span,
) -> Result<KdlNode, ShellError> {
match value {
Value::List { vals, .. } => list_to_jik_node(vals, name, non_roundtrip, call_span),
Value::Record { val, .. } => record_to_jik_node(val, name, non_roundtrip, call_span),
literal => {
let mut node = KdlNode::new(identifier_for(name));
node.push(entry_from_nu_value(
literal,
None,
non_roundtrip,
call_span,
)?);
Ok(node)
}
}
}
fn list_to_jik_node(
vals: &[Value],
name: &str,
non_roundtrip: &NonRoundtrip,
call_span: Span,
) -> Result<KdlNode, ShellError> {
let mut node = KdlNode::new(identifier_for(name));
if vals.is_empty() {
node.set_ty(identifier_for(TY_ARRAY));
return Ok(node);
}
if vals.len() == 1 && is_kdl_literal(&vals[0]) {
node.set_ty(identifier_for(TY_ARRAY));
node.push(entry_from_nu_value(
&vals[0],
None,
non_roundtrip,
call_span,
)?);
return Ok(node);
}
if vals.iter().all(is_kdl_literal) {
for val in vals {
node.push(entry_from_nu_value(val, None, non_roundtrip, call_span)?);
}
return Ok(node);
}
let mut child_doc = KdlDocument::new();
for val in vals {
child_doc
.nodes_mut()
.push(value_to_jik_node(val, ANON, non_roundtrip, call_span)?);
}
*node.ensure_children() = child_doc;
Ok(node)
}
fn record_to_jik_node(
record: &Record,
name: &str,
non_roundtrip: &NonRoundtrip,
call_span: Span,
) -> Result<KdlNode, ShellError> {
let mut node = KdlNode::new(identifier_for(name));
if record.is_empty() {
node.set_ty(identifier_for(TY_OBJECT));
return Ok(node);
}
let mut child_doc = KdlDocument::new();
let mut has_children = false;
for (key, value) in record.iter() {
if is_kdl_literal(value) {
node.push(entry_from_nu_value(
value,
Some(key),
non_roundtrip,
call_span,
)?);
} else {
has_children = true;
child_doc
.nodes_mut()
.push(value_to_jik_node(value, key, non_roundtrip, call_span)?);
}
}
if has_children {
*node.ensure_children() = child_doc;
}
Ok(node)
}