use std::sync::Arc;
use clap::Parser;
use serde_json::json;
use memstead_base::render;
use memstead_schema::Schema;
use crate::CliError;
use crate::output::{ExitKind, print_json, print_markdown};
use crate::setup::CliContext;
#[derive(Parser, Debug)]
pub struct Args {
pub name: Option<String>,
#[arg(long)]
pub mem: Option<String>,
}
pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
let resolved = resolve_schema(ctx, args.mem.as_deref())?;
let schema = &resolved.schema;
let (schema_name, schema_version) = schema.id();
let schema_label = format!("{schema_name}@{schema_version}");
let notice = resolved.condition.as_ref().map(|c| c.notice());
let md = match args.name.as_deref() {
None | Some("") => {
let mut out = render::render_type_catalog_markdown_for(schema);
out.insert_str(0, &format!("**Schema:** `{schema_label}`\n\n"));
if let Some(n) = ¬ice {
out.insert_str(0, &format!("{n}\n\n"));
}
out
}
Some(name) => match schema.get_type(name) {
Some(td) => {
let mut out = render::render_type_info_markdown(&td);
out.insert_str(0, &format!("**Schema:** `{schema_label}`\n\n"));
if let Some(n) = ¬ice {
out.insert_str(0, &format!("{n}\n\n"));
}
out
}
None => {
let mut known: Vec<&str> = schema.types.keys().map(String::as_str).collect();
known.sort();
let message = match ¬ice {
Some(n) => format!(
"Unknown type: {name} (schema {schema_label}). Known types: {}\n\n{n}",
known.join(", ")
),
None => format!(
"Unknown type: {name} (schema {schema_label}). Known types: {}",
known.join(", ")
),
};
return Err(
CliError::new(ExitKind::Generic, "UNKNOWN_ENTITY_TYPE", message)
.with_details(json!({
"name": name,
"schema_ref": schema_label,
"declared": known,
"fallback": resolved.condition.as_ref().map(|c| json!({
"code": c.code(),
"detail": c.notice(),
})),
}))
.into(),
);
}
},
};
if ctx.json {
print_json(&json!({
"markdown": md,
"schema": schema_label,
"fallback": resolved.condition.as_ref().map(|c| json!({
"code": c.code(),
"detail": c.notice(),
})),
}))?;
} else {
print_markdown(&md);
}
Ok(())
}
fn resolve_schema(ctx: &CliContext, mem: Option<&str>) -> anyhow::Result<Resolved> {
let engine = match ctx.cli_engine() {
Ok(e) => e,
Err(_) => return Ok(Resolved::cold_start()),
};
let engine: memstead_base::Engine = engine.into_base();
let writable: Vec<&str> = engine.writable_mem_names();
let all_loaded: Vec<&str> = engine.mem_names();
let resolved_mem: &str = match mem {
Some(name) => {
if !all_loaded.contains(&name) {
if engine.quarantine_reason(name).is_some() {
return Err(CliError::from_engine_op(engine.unknown_mem_error(name)).into());
}
let known = if all_loaded.is_empty() {
"no mems loaded".to_string()
} else {
format!("known mems: [{}]", all_loaded.join(", "))
};
return Err(CliError {
code: "UNKNOWN_MEM",
kind: ExitKind::NotFound,
message: format!("unknown mem: {name} — {known}"),
details: Some(json!({ "mem": name, "known_mems": all_loaded })),
}
.into());
}
name
}
None => match writable.len() {
0 => {
let quarantined: Vec<QuarantineNote> = engine
.quarantined_mems()
.iter()
.map(|q| QuarantineNote {
mem: q.mount.mem.clone(),
reason_code: q.reason_code.clone(),
reason: q.reason_message.clone(),
})
.collect();
return Ok(Resolved {
schema: Schema::builtin_default(),
condition: if quarantined.is_empty() {
Some(FallbackCondition::NoWritableMem)
} else {
Some(FallbackCondition::AllQuarantined { quarantined })
},
});
}
1 => writable[0],
_ => {
let schemas = engine.schemas();
let schema_id = |v: &str| {
schemas
.get(v)
.map(|s| (s.manifest.name.clone(), s.version.clone()))
};
let first = schema_id(writable[0]);
let all_same = first.is_some() && writable.iter().all(|v| schema_id(v) == first);
if all_same {
writable[0]
} else {
return Err(CliError::new(
ExitKind::Validation,
"AMBIGUOUS_MEM",
format!(
"writable mems pin different schemas ([{}]) — pass `--mem <name>` to pick one",
writable.join(", ")
),
)
.with_details(json!({ "mems": writable }))
.into());
}
}
},
};
match engine.schemas().get(resolved_mem).cloned() {
Some(schema) => Ok(Resolved {
schema,
condition: None,
}),
None => Ok(Resolved {
schema: Schema::builtin_default(),
condition: Some(FallbackCondition::MemHasNoSchema {
mem: resolved_mem.to_string(),
}),
}),
}
}
struct Resolved {
schema: Arc<Schema>,
condition: Option<FallbackCondition>,
}
impl Resolved {
fn cold_start() -> Self {
Self {
schema: Schema::builtin_default(),
condition: None,
}
}
}
struct QuarantineNote {
mem: String,
reason_code: String,
reason: String,
}
enum FallbackCondition {
AllQuarantined { quarantined: Vec<QuarantineNote> },
NoWritableMem,
MemHasNoSchema { mem: String },
}
impl FallbackCondition {
fn notice(&self) -> String {
match self {
Self::AllQuarantined { quarantined } => {
let mut out = String::from(
"**No mem is serving in this workspace** — the schema below is the \
engine built-in default, not this workspace's own. \
Quarantined:\n",
);
for q in quarantined {
out.push_str(&format!(
"\n- `{}` ({}): {}\n",
q.mem, q.reason_code, q.reason
));
}
out
}
Self::NoWritableMem => "**No writable mem is loaded in this workspace** — the \
schema below is the engine built-in default, not this workspace's own."
.to_string(),
Self::MemHasNoSchema { mem } => format!(
"**Mem `{mem}` carries no schema entry** — the schema below is the engine \
built-in default, not this mem's own."
),
}
}
fn code(&self) -> &'static str {
match self {
Self::AllQuarantined { .. } => "ALL_MEMS_QUARANTINED",
Self::NoWritableMem => "NO_WRITABLE_MEM",
Self::MemHasNoSchema { .. } => "MEM_HAS_NO_SCHEMA",
}
}
}