use super::{pack_registry_tag, RuntimeError, RuntimeResult, Uuid, Value};
pub(super) fn obj(args: &Value) -> RuntimeResult<&serde_json::Map<String, Value>> {
args.as_object()
.ok_or_else(|| RuntimeError::InvalidInput("op args must be a JSON object".into()))
}
pub(super) fn refuse_pack_registry_tags(tags: &[String], verb: &str) -> RuntimeResult<()> {
let Some(tag) = tags.iter().find_map(|tag| pack_registry_tag(tag)) else {
return Ok(());
};
Err(RuntimeError::InvalidInput(format!(
"{verb} refuses registry tag {tag:?}: registry rows are written only by the owning pack"
)))
}
pub(super) fn require_str<'a>(args: &'a Value, key: &str) -> RuntimeResult<&'a str> {
obj(args)?
.get(key)
.and_then(|v| v.as_str())
.ok_or_else(|| RuntimeError::InvalidInput(format!("missing required field {key:?}")))
}
pub(super) fn require_uuid(args: &Value, key: &str) -> RuntimeResult<Uuid> {
let raw = require_str(args, key)?;
Uuid::parse_str(raw).map_err(|_| {
RuntimeError::InvalidInput(format!(
"{key} must be a full UUID; got {raw:?}. This atomic-plan stage consumes an \
already-resolved record and performs no namespace-scoped search of its own, so a \
short hex prefix cannot be resolved here — resolve it to a full UUID first (e.g. \
via `get`) and pass that."
))
})
}
pub(super) fn optional_str<'a>(args: &'a Value, key: &str) -> Option<&'a str> {
obj(args).ok()?.get(key).and_then(|v| v.as_str())
}
pub(super) fn optional_create_string(args: &Value, key: &str) -> RuntimeResult<Option<String>> {
match obj(args)?.get(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(value)) => Ok(Some(value.clone())),
Some(other) => Err(RuntimeError::InvalidInput(format!(
"{key} must be a string or null, got: {other}"
))),
}
}
pub(super) fn optional_entity_type_patch(
args: &Value,
key: &str,
) -> RuntimeResult<Option<Option<String>>> {
match obj(args)?.get(key) {
None => Ok(None),
Some(Value::Null) => Ok(Some(None)),
Some(Value::String(value)) => Ok(Some(Some(value.clone()))),
Some(other) => Err(RuntimeError::InvalidInput(format!(
"{key} must be a string or null, got: {other}"
))),
}
}
pub(super) fn optional_string_patch(
args: &Value,
key: &str,
) -> RuntimeResult<Option<Option<String>>> {
match obj(args)?.get(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(s)) => Ok(Some(Some(s.clone()))),
Some(other) => Err(RuntimeError::InvalidInput(format!(
"{key} must be a string or null, got: {other}"
))),
}
}
pub(super) fn entity_name_patch(args: &Value) -> RuntimeResult<Option<String>> {
match obj(args)?.get("name") {
None | Some(Value::Null) => Ok(None),
Some(Value::String(s)) => Ok(Some(s.clone())),
Some(other) => Err(RuntimeError::InvalidInput(format!(
"name must be a string, got: {other}"
))),
}
}
pub(super) fn optional_properties(args: &Value, key: &str) -> RuntimeResult<Option<Value>> {
match obj(args)?.get(key) {
None | Some(Value::Null) => Ok(None),
Some(v) => Ok(Some(v.clone())),
}
}
pub(super) fn optional_tags(args: &Value) -> RuntimeResult<Option<Vec<String>>> {
match obj(args)?.get("tags") {
None | Some(Value::Null) => Ok(None),
Some(Value::Array(items)) => {
let mut tags = Vec::with_capacity(items.len());
for item in items {
let s = item.as_str().ok_or_else(|| {
RuntimeError::InvalidInput("tags must be an array of strings".into())
})?;
tags.push(s.to_string());
}
Ok(Some(tags))
}
Some(_) => Err(RuntimeError::InvalidInput(
"tags must be an array of strings".into(),
)),
}
}
pub(super) fn optional_f64(args: &Value, key: &str) -> RuntimeResult<Option<f64>> {
match obj(args)?.get(key) {
None => Ok(None),
Some(Value::Null) => Ok(None),
Some(v) => v
.as_f64()
.map(Some)
.ok_or_else(|| RuntimeError::InvalidInput(format!("{key} must be a number"))),
}
}
pub(super) fn optional_f64_patch(args: &Value, key: &str) -> RuntimeResult<Option<Option<f64>>> {
match obj(args)?.get(key) {
None => Ok(None),
Some(Value::Null) => Ok(Some(None)),
Some(v) => v
.as_f64()
.map(|f| Some(Some(f)))
.ok_or_else(|| RuntimeError::InvalidInput(format!("{key} must be a number"))),
}
}