use super::super::super::value_codec;
use super::super::support::{
remove_indices_preserving_order, set_object_string_field, status_list_text,
string_list_item_text, type_mismatch,
};
use super::resolve_nested_root;
use super::traverse::{default_value_for_node, descend_mut, ensure_node_path_mut};
use crate::cmd::edit::ArtifactType;
use crate::cmd::edit::path::FieldPath;
use crate::cmd::edit::rules::{NestedNodeKind, NestedNodeRule, Verb, nested_status_list_spec};
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult};
use serde_json::Value;
struct NestedListTarget<'a> {
node: &'static NestedNodeRule,
item_rule: &'static NestedNodeRule,
list: &'a mut Vec<Value>,
}
pub fn add_nested_list_value(
artifact: ArtifactType,
doc: &mut Value,
fp: &FieldPath,
value: &str,
id: &str,
) -> DiagnosticResult<()> {
if fp.has_terminal_index() {
return Err(Diagnostic::new(
DiagnosticCode::E0817PathTypeMismatch,
format!(
"Cannot add to indexed path '{}' (use set/remove for a specific element)",
fp
),
id,
));
}
let NestedListTarget {
node,
item_rule,
list,
} = nested_list_target_mut(
artifact,
doc,
fp,
Verb::Add,
id,
Some(format!("Field '{}' is not a list; cannot add to it", fp)),
)?;
match item_rule.kind {
NestedNodeKind::Scalar => {
if !list.iter().any(|v| v.as_str() == Some(value)) {
list.push(Value::String(value.to_string()));
}
}
NestedNodeKind::Object => {
if let Some(codec) = node.value_codec {
let item = value_codec::decode(codec, value)?;
let encoded = value_codec::encode(codec, &item, id)?;
let duplicate = list.iter().any(|existing| {
value_codec::encode(codec, existing, id).is_ok_and(|current| current == encoded)
});
if !duplicate {
list.push(item);
}
return Ok(());
}
let Some(text_key) = node.text_key else {
return Err(plain_string_for_structured_list(fp, id));
};
let duplicate = list.iter().any(|item| {
item.as_object()
.and_then(|obj| obj.get(text_key))
.and_then(Value::as_str)
== Some(value)
});
if !duplicate {
let mut item = default_value_for_node(item_rule);
let obj = item
.as_object_mut()
.ok_or_else(|| type_mismatch("Expected object list item", id))?;
obj.insert(text_key.to_string(), Value::String(value.to_string()));
list.push(item);
}
}
NestedNodeKind::List => {
return Err(plain_string_for_structured_list(fp, id));
}
}
Ok(())
}
fn plain_string_for_structured_list(fp: &FieldPath, id: &str) -> Diagnostic {
Diagnostic::new(
DiagnosticCode::E0817PathTypeMismatch,
format!(
"Field '{fp}' requires structured list items and cannot be appended with a plain string"
),
id,
)
}
pub fn remove_nested_list_values<F>(
artifact: ArtifactType,
doc: &mut Value,
fp: &FieldPath,
id: &str,
resolve: F,
) -> DiagnosticResult<Vec<String>>
where
F: FnOnce(
&[&str],
Option<crate::cmd::edit::rules::NestedListValueCodec>,
) -> DiagnosticResult<Vec<usize>>,
{
let NestedListTarget {
node,
item_rule,
list,
} = nested_list_target_mut(artifact, doc, fp, Verb::Remove, id, None)?;
let texts = list_item_texts(node, item_rule, list, id)?;
let text_refs = texts.iter().map(String::as_str).collect::<Vec<_>>();
let indices = resolve(&text_refs, node.value_codec)?;
value_codec::validate_remaining(
node.value_codec,
list.len().saturating_sub(indices.len()),
id,
)?;
let removed = remove_indices_preserving_order(list, indices, |val| {
list_item_text(node, item_rule, val, id)
})?;
Ok(removed)
}
pub fn tick_nested_list_item_with_matcher<F>(
artifact: ArtifactType,
doc: &mut Value,
fp: &FieldPath,
id: &str,
new_status: &str,
resolve: F,
) -> DiagnosticResult<String>
where
F: FnOnce(&[&str]) -> DiagnosticResult<Vec<usize>>,
{
let NestedListTarget {
node,
item_rule,
list,
} = nested_list_target_mut(artifact, doc, fp, Verb::Tick, id, None)?;
if item_rule.kind != NestedNodeKind::Object {
return Err(type_mismatch(
"Expected object entries in tickable list",
id,
));
}
let spec = nested_status_list_spec(node)
.ok_or_else(|| type_mismatch("Expected status list rule for tickable list", id))?;
let texts = list_item_texts(node, item_rule, list, id)?;
let text_refs = texts.iter().map(String::as_str).collect::<Vec<_>>();
let idx = resolve(&text_refs)?[0];
let text = texts[idx].clone();
set_object_string_field(
&mut list[idx],
spec.status_key,
new_status,
"Expected object entries in tickable list",
id,
)?;
Ok(text)
}
fn list_item_texts(
node: &NestedNodeRule,
item_rule: &NestedNodeRule,
list: &[Value],
id: &str,
) -> DiagnosticResult<Vec<String>> {
list.iter()
.map(|item| list_item_text(node, item_rule, item, id))
.collect()
}
fn list_item_text(
node: &NestedNodeRule,
item_rule: &NestedNodeRule,
item: &Value,
id: &str,
) -> DiagnosticResult<String> {
if let Some(codec) = node.value_codec {
return value_codec::encode(codec, item, id);
}
match item_rule.kind {
NestedNodeKind::Scalar => {
string_list_item_text(item, "Expected string items in list", id).map(str::to_string)
}
NestedNodeKind::Object => {
let text_key = node
.text_key
.ok_or_else(|| type_mismatch("Expected text_key for object list", id))?;
status_list_text(item, text_key, id).map(str::to_string)
}
NestedNodeKind::List => Err(type_mismatch("Expected scalar or object items in list", id)),
}
}
fn nested_list_target_mut<'a>(
artifact: ArtifactType,
doc: &'a mut Value,
fp: &FieldPath,
verb: Verb,
id: &str,
not_list_message: Option<String>,
) -> DiagnosticResult<NestedListTarget<'a>> {
let root_name = &fp.segments[0].name;
let rule = resolve_nested_root(artifact, root_name, id)?;
let root_value = ensure_node_path_mut(doc, rule.content_path, rule.node, id)?;
let (node, slot) = descend_mut(
rule.node,
root_value,
&fp.segments[0],
&fp.segments[1..],
verb,
id,
)?;
if node.kind != NestedNodeKind::List {
let message =
not_list_message.unwrap_or_else(|| "Expected array for list field".to_string());
return Err(type_mismatch(&message, id));
}
let item_rule = node
.item
.ok_or_else(|| type_mismatch("List node missing item rule", id))?;
let list = slot
.as_array_mut()
.ok_or_else(|| type_mismatch("Expected array for list field", id))?;
Ok(NestedListTarget {
node,
item_rule,
list,
})
}