graphddb_runtime 0.7.5

Rust runtime for GraphDDB — interprets the language-neutral IR (manifest.json + operations.json) and executes the validated access patterns against DynamoDB.
Documentation
//! Spec-version guard — a port of `_validate_spec_version` in
//! `python/graphddb_runtime/runtime.py`.
//!
//! The IR is a stable execution contract; a document NEWER than this runtime
//! understands could silently mis-execute unknown binding forms, so version skew
//! fails LOUDLY. Rule (`<major>.<minor>` vs [`SPEC_VERSION_SUPPORTED`]): major must
//! equal; minor must be `<=`; a missing/malformed version is rejected.

use serde_json::Value as Json;

use crate::errors::GraphDDBError;

/// The operation-IR (spec) version this runtime implements.
pub const SPEC_VERSION_SUPPORTED: &str = "1.1";

/// Validate a document's `version` field against [`SPEC_VERSION_SUPPORTED`].
pub fn validate_spec_version(document: &Json, label: &str) -> Result<(), GraphDDBError> {
    let version = document.get("version").and_then(Json::as_str);
    let version = match version {
        Some(v) if is_major_minor(v) => v,
        other => {
            let shown = other
                .map(|s| format!("'{s}'"))
                .unwrap_or_else(|| "None".to_string());
            return Err(GraphDDBError::new(format!(
                "{label}: missing or malformed spec version {shown} — expected \
                 '<major>.<minor>' (this runtime supports {SPEC_VERSION_SUPPORTED}). \
                 Regenerate the document with a matching graphddb generator."
            )));
        }
    };
    let (major, minor) = split(version);
    let (smajor, sminor) = split(SPEC_VERSION_SUPPORTED);
    if major != smajor || minor > sminor {
        return Err(GraphDDBError::new(format!(
            "{label}: document spec version '{version}' is not supported by this runtime \
             (supports major {smajor}, minor <= {sminor}). A newer document may carry \
             binding forms this runtime would silently mis-execute; upgrade graphddb_runtime \
             or regenerate the document with a matching generator version."
        )));
    }
    Ok(())
}

fn is_major_minor(v: &str) -> bool {
    let mut parts = v.split('.');
    match (parts.next(), parts.next(), parts.next()) {
        (Some(a), Some(b), None) => {
            !a.is_empty()
                && !b.is_empty()
                && a.bytes().all(|c| c.is_ascii_digit())
                && b.bytes().all(|c| c.is_ascii_digit())
        }
        _ => false,
    }
}

fn split(v: &str) -> (u32, u32) {
    let mut parts = v.split('.');
    let major = parts.next().unwrap().parse().unwrap();
    let minor = parts.next().unwrap().parse().unwrap();
    (major, minor)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn accepts_supported_and_older() {
        assert!(validate_spec_version(&json!({"version": "1.1"}), "d").is_ok());
        assert!(validate_spec_version(&json!({"version": "1.0"}), "d").is_ok());
    }

    #[test]
    fn rejects_newer_minor_and_major_and_missing() {
        assert!(validate_spec_version(&json!({"version": "1.2"}), "d").is_err());
        assert!(validate_spec_version(&json!({"version": "2.0"}), "d").is_err());
        assert!(validate_spec_version(&json!({"version": "bad"}), "d").is_err());
        assert!(validate_spec_version(&json!({}), "d").is_err());
    }
}