dig-rpc-types 0.2.1

Canonical DIG-node JSON-RPC interface: request/response types, the method enum + tier classification, the error-code taxonomy, and an OpenRPC 1.2.6 document generator. The single source of truth both DIG node implementations depend on. Pure types — no I/O, no async, no server logic.
Documentation
//! OpenRPC 1.2.6 document generation.
//!
//! [`openrpc_document`] builds the [OpenRPC 1.2.6](https://spec.open-rpc.org/)
//! service description from the crate's own [`Method`] catalogue,
//! [`Tier`](crate::tier::Tier) map, and [`ErrorCode`] taxonomy — so the
//! machine-discoverable contract can never
//! drift from the types both node implementations compile against. It is the
//! single source served by [`rpc.discover`](crate::method::Method::RpcDiscover)
//! and summarized by `dig.health` / `dig.methods`.
//!
//! The document is a [`serde_json::Value`] (no OpenRPC-typed dependency): every
//! method is emitted with its wire name, summary, an `x-dig-tier` extension, and
//! an `x-dig-peer-reachable` extension; the components section carries the full
//! error catalogue keyed by machine code.

use serde_json::{json, Value};

use crate::error::ErrorCode;
use crate::method::Method;

/// The OpenRPC spec version this document conforms to.
pub const OPENRPC_VERSION: &str = "1.2.6";

/// Build the OpenRPC 1.2.6 document describing the DIG-node RPC surface.
///
/// `version` is the serving node's own software/API version, embedded as
/// `info.version`. The `methods` array lists [`Method::ALL`] with per-method
/// tier + peer-reachability extensions; the `components.x-dig-errors` object
/// lists every [`ErrorCode`] with its machine code, numeric code, and default
/// message.
pub fn openrpc_document(version: &str) -> Value {
    json!({
        "openrpc": OPENRPC_VERSION,
        "info": {
            "title": "DIG Node RPC",
            "version": version,
            "description":
                "The canonical DIG-node JSON-RPC surface: content reads, peer \
                 discovery + range serving, and loopback control. Tiers and \
                 error codes are defined by the dig-rpc-types crate.",
        },
        "methods": Method::ALL.iter().map(|m| method_object(*m)).collect::<Vec<_>>(),
        "components": {
            // The canonical error catalogue, keyed by machine code, so an agent
            // can branch on `data.code` after discovery.
            "x-dig-errors": ErrorCode::ALL.iter().map(|c| json!({
                "machineCode": c.machine_code(),
                "code": c.code(),
                "message": c.default_message(),
                "origin": c.default_origin(),
            })).collect::<Vec<_>>(),
        },
        // The peer-surface allowlist, so a discovering peer knows which methods
        // it may call over mTLS.
        "x-dig-peer-allowlist": Method::peer_reachable_names(),
    })
}

/// Build one OpenRPC method object for `m`.
fn method_object(m: Method) -> Value {
    json!({
        "name": m.name(),
        "summary": m.summary(),
        "params": [],
        "result": {
            "name": "result",
            "schema": { "type": "object" },
        },
        // DIG extensions: the method's tier and whether it is peer-reachable.
        "x-dig-tier": m.tier().as_str(),
        "x-dig-peer-reachable": m.is_peer_reachable(),
    })
}

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

    /// **Proves:** the document declares OpenRPC 1.2.6 and embeds the version.
    #[test]
    fn document_header() {
        let doc = openrpc_document("9.9.9");
        assert_eq!(doc["openrpc"], "1.2.6");
        assert_eq!(doc["info"]["version"], "9.9.9");
    }

    /// **Proves:** every catalogue method appears exactly once, with its tier +
    /// peer-reachability extension.
    /// **Catches:** a generator that drops methods or drifts tier/allowlist
    /// data from the [`Method`] table.
    #[test]
    fn all_methods_present_with_extensions() {
        let doc = openrpc_document("1.0.0");
        let methods = doc["methods"].as_array().unwrap();
        assert_eq!(methods.len(), Method::ALL.len());
        for m in Method::ALL {
            let obj = methods
                .iter()
                .find(|o| o["name"] == m.name())
                .unwrap_or_else(|| panic!("missing method {}", m.name()));
            assert_eq!(obj["x-dig-tier"], m.tier().as_str());
            assert_eq!(obj["x-dig-peer-reachable"], m.is_peer_reachable());
        }
    }

    /// **Proves:** the error catalogue lists every code with numeric + machine
    /// forms.
    /// **Catches:** an error code missing from the generated discovery doc.
    #[test]
    fn error_catalogue_complete() {
        let doc = openrpc_document("1.0.0");
        let errors = doc["components"]["x-dig-errors"].as_array().unwrap();
        assert_eq!(errors.len(), ErrorCode::ALL.len());
        // Spot-check the resolved collision: onion keeps -32020, control at -32030.
        assert!(errors
            .iter()
            .any(|e| e["code"] == -32020 && e["machineCode"] == "ONION_CIRCUIT_UNAVAILABLE"));
        assert!(errors
            .iter()
            .any(|e| e["code"] == -32030 && e["machineCode"] == "UNAUTHORIZED"));
    }

    /// **Proves:** the peer allowlist extension matches the method table.
    #[test]
    fn peer_allowlist_extension() {
        let doc = openrpc_document("1.0.0");
        let allow = doc["x-dig-peer-allowlist"].as_array().unwrap();
        assert_eq!(allow.len(), Method::peer_reachable_names().len());
        assert!(allow.iter().any(|n| n == "dig.getContent"));
        assert!(!allow.iter().any(|n| n == "cache.clear"));
    }
}