use std::path::PathBuf;
use clap::Parser;
use indexmap::IndexMap;
use serde::Deserialize;
use memstead_base::EntityId;
use memstead_base::ops::RelateArg;
use memstead_base::{CreateEntityArgs, vcs::Actor};
use crate::CliError;
use crate::output::{ExitKind, print_json, print_markdown};
use crate::setup::CliContext;
#[derive(Parser, Debug)]
pub struct Args {
#[arg(long = "from", value_name = "FILE")]
pub from: PathBuf,
#[arg(long = "dry-run")]
pub dry_run: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct EntryPayload {
title: String,
entity_type: String,
#[serde(default)]
mem: Option<String>,
#[serde(default)]
sections: IndexMap<String, String>,
#[serde(default)]
metadata: IndexMap<String, String>,
#[serde(default)]
relations: Vec<RelationPayload>,
#[serde(default)]
anchors: Vec<memstead_base::anchor::AnchorInput>,
#[serde(default)]
note: Option<String>,
#[serde(default)]
id: Option<String>,
#[serde(default)]
#[allow(dead_code)]
expected_hash: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RelationPayload {
to: String,
#[serde(rename = "type")]
rel_type: String,
#[serde(default)]
description: Option<String>,
}
pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
let entry_values = super::batch::parse_batch_envelope(&args.from, "creates")?;
let mut entries: Vec<EntryPayload> = Vec::with_capacity(entry_values.len());
for (idx, entry_value) in entry_values.into_iter().enumerate() {
match serde_json::from_value::<EntryPayload>(entry_value) {
Ok(entry) => entries.push(entry),
Err(e) => {
return Err(CliError::new(
ExitKind::Validation,
"INVALID_INPUT",
format!("entry {idx}: invalid shape — {e}"),
)
.with_details(serde_json::json!({
"entry_index": idx,
"parser_error": e.to_string(),
}))
.into());
}
}
}
let mut engine = crate::setup::full_engine(ctx)?;
let creates: Vec<(CreateEntityArgs, Option<String>)> = entries
.into_iter()
.enumerate()
.map(|(idx, entry)| build_create_args(&engine, idx, entry))
.collect::<anyhow::Result<Vec<_>>>()?;
let result = engine
.batch_create(
creates,
Actor::Cli,
Some(&crate::setup::cli_client_id()),
args.dry_run,
)
.map_err(CliError::from_engine_op)?;
let mem_changed = engine.take_mem_changed_notices();
if result.applied {
if ctx.json {
let mut body = serde_json::to_value(&result).unwrap_or(serde_json::Value::Null);
crate::commands::merge_mem_changed_json(&mut body, &mem_changed);
print_json(&body)?;
} else {
let mut md = super::batch::render_batch_markdown("create", &result);
md.push_str(&crate::commands::render_mem_changed_block(&mem_changed));
print_markdown(&md);
}
return Ok(());
}
if !ctx.json {
print_markdown(&super::batch::render_batch_markdown("create", &result));
}
Err(super::batch::batch_refused_error("create", &result).into())
}
fn build_create_args(
engine: &memstead_base::Engine,
idx: usize,
entry: EntryPayload,
) -> anyhow::Result<(CreateEntityArgs, Option<String>)> {
let mem = match entry.mem {
Some(v) => v,
None => match engine.default_writable_mem() {
Some(name) => name.to_string(),
None => {
return Err(CliError::new(
ExitKind::Generic,
"NO_WRITABLE_MEM",
format!("entry {idx}: no writable mem loaded — set `mem` in the entry"),
)
.into());
}
},
};
if let Some(template_id) = entry.id.as_deref() {
let derived =
memstead_base::entity::id::validate_and_derive_slug(&entry.title).map_err(|e| {
CliError::new(
ExitKind::Validation,
"INVALID_TITLE",
format!("entry {idx}: {e}"),
)
})?;
let derived_slug = derived.slug;
let slug_part = template_id
.rsplit_once("--")
.map(|(_, s)| s)
.unwrap_or(template_id);
if slug_part != derived_slug {
return Err(CliError::new(
ExitKind::Validation,
"INVALID_INPUT",
format!(
"entry {idx}: template `id` {template_id:?} does not match the id derived \
from the title (slug {derived_slug:?}) — create derives identity from the \
title; fix the template's id or title"
),
)
.into());
}
}
let note = entry.note;
Ok((
CreateEntityArgs {
anchors: entry.anchors,
mem,
title: entry.title,
entity_type: entry.entity_type,
sections: entry.sections,
metadata: entry.metadata,
relations: entry
.relations
.into_iter()
.map(|r| RelateArg {
to: EntityId::canonical(&r.to),
rel_type: r.rel_type,
description: r.description,
})
.collect(),
dry_run: false,
},
note,
))
}