liminal-sdk 0.20.20

Application-facing SDK traits for liminal messaging clients
Documentation
//! The one substitution the seat's `flow` and a line's stages share.
//!
//! `{"$prev": "<JSON pointer>"}` and `{"$item": "<JSON pointer>"}` objects are filled from
//! the previous reply and a fan-out's element (SDK-017 R4; NET-031 R1, R5).

use alloc::format;
use alloc::string::String;

use serde_json::{Map, Value};

/// The key naming a pointer into the previous reply.
pub const PREV: &str = "$prev";

/// The key naming a pointer into a fan-out's element.
pub const ITEM: &str = "$item";

/// `input` with every `{"$prev": pointer}` object replaced from `prev`.
///
/// # Errors
/// The pointer that names nothing in `prev`, or a `$prev` with no previous reply.
pub fn fill(input: &Value, prev: Option<&Value>) -> Result<Value, String> {
    fill_with(input, prev, None)
}

/// `input` with every `{"$prev": pointer}` object replaced from `prev` and every
/// `{"$item": pointer}` object from `item`.
///
/// # Errors
/// The pointer that names nothing, a `$prev` with no previous reply, or an `$item` outside
/// a fan-out.
pub fn fill_with(
    input: &Value,
    prev: Option<&Value>,
    item: Option<&Value>,
) -> Result<Value, String> {
    match input {
        Value::Object(object) => {
            if let Some(pointer) = sole(object, PREV) {
                let prev = prev.ok_or_else(|| format!("{PREV} {pointer:?} in the first step"))?;
                return prev.pointer(pointer).cloned().ok_or_else(|| {
                    format!("{PREV} {pointer:?} names nothing in the previous reply")
                });
            }
            if let Some(pointer) = sole(object, ITEM) {
                let item = item.ok_or_else(|| format!("{ITEM} {pointer:?} outside a fan-out"))?;
                return item
                    .pointer(pointer)
                    .cloned()
                    .ok_or_else(|| format!("{ITEM} {pointer:?} names nothing in the element"));
            }
            let mut filled = Map::with_capacity(object.len());
            for (key, value) in object {
                filled.insert(key.clone(), fill_with(value, prev, item)?);
            }
            Ok(Value::Object(filled))
        }
        Value::Array(items) => items
            .iter()
            .map(|value| fill_with(value, prev, item))
            .collect::<Result<_, _>>()
            .map(Value::Array),
        other => Ok(other.clone()),
    }
}

/// Whether `input` names `$prev` anywhere, so a first stage that does is refused before it
/// is kept (NET-031 R1).
#[must_use]
pub fn names_prev(input: &Value) -> bool {
    match input {
        Value::Object(object) => sole(object, PREV).is_some() || object.values().any(names_prev),
        Value::Array(items) => items.iter().any(names_prev),
        _ => false,
    }
}

/// The pointer of an object whose one member is `key` with a string value.
fn sole<'a>(object: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
    match (object.len(), object.get(key)) {
        (1, Some(Value::String(pointer))) => Some(pointer),
        _ => None,
    }
}

#[cfg(test)]
#[path = "prev_tests.rs"]
mod tests;