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
//! Shape-dispatched peer frame families: the DHT wire and the PEX wire.
//!
//! These travel over the mTLS peer transport but are NOT JSON-RPC `method`
//! calls — they are dispatched on a `type` discriminator, not a `method` field,
//! so they are modeled separately from [`crate::method::Method`]. Both belong to
//! the [`Peer`](crate::tier::Tier::Peer) tier.
//!
//! - The **DHT** wire ([`DhtRequest`] / [`DhtResponse`]) is the Kademlia
//!   content-location protocol: `find_node`, `find_providers`, `add_provider`,
//!   `ping`.
//! - The **PEX** wire ([`PexMessage`]) is peer-exchange: `pex_handshake`,
//!   `pex_snapshot`, `pex_delta`, `pex_error`.

use serde::{Deserialize, Serialize};

use crate::types::{PeerAddress, Provider};

// ===========================================================================
// DHT wire (Kademlia content location)
// ===========================================================================

/// A DHT node's routing entry: a `peer_id` (32-byte, hex or base64 per the
/// transport) with its addresses and record TTL.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct DhtNode {
    /// The node's `peer_id` (64-hex).
    pub peer_id: String,
    /// The node's candidate addresses (IPv6-first).
    pub addresses: Vec<PeerAddress>,
    /// The record's absolute expiry (unix seconds), when present.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub expires_at: Option<u64>,
}

/// A DHT request frame, dispatched on `type`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub enum DhtRequest {
    /// Find the nodes closest to `target_id`.
    FindNode {
        /// The requesting node's id (64-hex).
        node_id: String,
        /// The lookup target id (64-hex).
        target_id: String,
    },
    /// Find providers for a content key (plus closer nodes to continue the walk).
    FindProviders {
        /// The requesting node's id (64-hex).
        node_id: String,
        /// The content key = `SHA-256(tag ‖ ids)` (64-hex).
        content_key: String,
    },
    /// Announce that a provider holds a content key.
    AddProvider {
        /// The requesting node's id (64-hex).
        node_id: String,
        /// The content key (64-hex).
        content_key: String,
        /// The provider being announced.
        provider: Provider,
    },
    /// Liveness probe.
    Ping {
        /// The requesting node's id (64-hex).
        node_id: String,
    },
}

/// A DHT response frame.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub enum DhtResponse {
    /// Response to `find_providers`: providers plus closer nodes.
    FindProviders {
        /// Nodes known to hold the content.
        providers: Vec<DhtNode>,
        /// Closer nodes to continue the iterative lookup.
        closer: Vec<DhtNode>,
    },
    /// Response to `find_node`: closer nodes only.
    FindNode {
        /// Closer nodes to continue the iterative lookup.
        closer: Vec<DhtNode>,
    },
    /// Response to `add_provider`.
    AddProvider {
        /// Whether the provider record was stored.
        stored: bool,
    },
    /// Response to `ping`: the responder's own id.
    Ping {
        /// The responding node's id (64-hex).
        node_id: String,
    },
}

// ===========================================================================
// PEX wire (peer exchange)
// ===========================================================================

/// A PEX peer entry: a `peer_id` with addresses, flags, and provenance.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PexPeer {
    /// The peer's id (64-hex).
    pub peer_id: String,
    /// The peer's addresses (IPv6-first).
    pub addresses: Vec<PeerAddress>,
    /// Capability / status flags.
    #[serde(default)]
    pub flags: Vec<String>,
    /// How this entry was learned: `"firsthand"` or `"secondhand"`.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub provenance: Option<String>,
}

/// A PEX message frame, dispatched on `type`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub enum PexMessage {
    /// Handshake: capabilities + wire version.
    PexHandshake {
        /// Advertised capabilities.
        capabilities: Vec<String>,
        /// The PEX wire version.
        version: u32,
    },
    /// A full peer snapshot (≤ 200 peers).
    PexSnapshot {
        /// The known peers.
        peers: Vec<PexPeer>,
    },
    /// An incremental peer delta (≤ 50 added, ≤ 50 removed).
    PexDelta {
        /// Peers added since the last snapshot/delta.
        added: Vec<PexPeer>,
        /// The `peer_id`s removed since the last snapshot/delta (64-hex).
        removed: Vec<String>,
    },
    /// A PEX-level error.
    PexError {
        /// The PEX error code.
        code: u32,
        /// A human-readable message.
        message: String,
    },
}

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

    /// **Proves:** a DHT request dispatches on the `type` tag with snake_case
    /// names matching the canonical wire.
    #[test]
    fn dht_request_tagged_snake_case() {
        let req = DhtRequest::FindProviders {
            node_id: "ab".repeat(32),
            content_key: "cd".repeat(32),
        };
        let v = serde_json::to_value(&req).unwrap();
        assert_eq!(v["type"], "find_providers");
        assert_eq!(serde_json::from_value::<DhtRequest>(v).unwrap(), req);
    }

    /// **Proves:** `ping` round-trips and carries `node_id`.
    #[test]
    fn dht_ping_round_trips() {
        let req = DhtRequest::Ping {
            node_id: "ef".repeat(32),
        };
        let s = serde_json::to_string(&req).unwrap();
        assert!(s.contains("\"type\":\"ping\""));
        assert_eq!(serde_json::from_str::<DhtRequest>(&s).unwrap(), req);
    }

    /// **Proves:** a PEX message dispatches on `type` with snake_case names.
    #[test]
    fn pex_message_tagged() {
        let msg = PexMessage::PexHandshake {
            capabilities: vec!["snapshot".into()],
            version: 1,
        };
        let v = serde_json::to_value(&msg).unwrap();
        assert_eq!(v["type"], "pex_handshake");
        assert_eq!(serde_json::from_value::<PexMessage>(v).unwrap(), msg);
    }

    /// **Proves:** a PEX delta carries added peers + removed ids.
    #[test]
    fn pex_delta_shape() {
        let msg = PexMessage::PexDelta {
            added: vec![PexPeer {
                peer_id: "12".repeat(32),
                addresses: vec![],
                flags: vec![],
                provenance: Some("firsthand".into()),
            }],
            removed: vec!["34".repeat(32)],
        };
        let v = serde_json::to_value(&msg).unwrap();
        assert_eq!(v["type"], "pex_delta");
        assert_eq!(v["added"][0]["provenance"], "firsthand");
        assert_eq!(serde_json::from_value::<PexMessage>(v).unwrap(), msg);
    }
}