decision_cockpit 0.1.0

Layer — product decision memory with MCP tools and an embedded review dashboard
Documentation
//! Helpers for reading fields out of a candidate's free-form JSON payload.

use chrono::{DateTime, Utc};
use serde_json::Value;

use crate::error::{AppError, AppResult};

/// Require a non-empty string field.
pub fn require_str(payload: &Value, field: &str) -> AppResult<String> {
    match payload.get(field) {
        Some(Value::String(s)) if !s.trim().is_empty() => Ok(s.trim().to_string()),
        Some(Value::String(_)) | None => Err(AppError::Validation(format!(
            "payload field `{field}` is required and must be a non-empty string"
        ))),
        Some(_) => Err(AppError::Validation(format!(
            "payload field `{field}` must be a string"
        ))),
    }
}

/// Read an optional string field; blank strings become `None`.
pub fn opt_str(payload: &Value, field: &str) -> Option<String> {
    match payload.get(field) {
        Some(Value::String(s)) if !s.trim().is_empty() => Some(s.trim().to_string()),
        _ => None,
    }
}

/// Read an optional RFC 3339 timestamp field.
pub fn opt_datetime(payload: &Value, field: &str) -> AppResult<Option<DateTime<Utc>>> {
    match payload.get(field) {
        Some(Value::String(s)) if !s.trim().is_empty() => {
            let parsed = DateTime::parse_from_rfc3339(s.trim()).map_err(|_| {
                AppError::Validation(format!(
                    "payload field `{field}` must be an RFC 3339 timestamp"
                ))
            })?;
            Ok(Some(parsed.with_timezone(&Utc)))
        }
        Some(Value::Null) | None => Ok(None),
        Some(_) => Err(AppError::Validation(format!(
            "payload field `{field}` must be a string timestamp"
        ))),
    }
}