Skip to main content

ldp_protocol/types/
payload.rs

1//! LDP payload mode definitions and negotiation types.
2//!
3//! LDP supports multiple payload modes for data exchange:
4//! - Mode 0: Plain text
5//! - Mode 1: Semantic frames (structured JSON)
6//! - Mode 2: Embedding hints (future)
7//! - Mode 3: Semantic graphs (future)
8//!
9//! MVP implements Modes 0 and 1 only.
10
11use serde::{Deserialize, Serialize};
12use std::fmt;
13
14/// Payload mode for LDP message exchange.
15///
16/// Determines how task inputs and outputs are encoded between delegates.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum PayloadMode {
20    /// Mode 0: Plain text. Universal fallback — every delegate supports this.
21    Text,
22
23    /// Mode 1: Semantic frames. Structured JSON with typed fields.
24    /// Maps directly to JamJet's existing `Value` payloads.
25    SemanticFrame,
26
27    /// Mode 2: Embedding hints. Text + embedding vectors for semantic routing.
28    /// Not yet implemented in MVP.
29    EmbeddingHints,
30
31    /// Mode 3: Semantic graphs. RDF-like structured knowledge.
32    /// Not yet implemented in MVP.
33    SemanticGraph,
34}
35
36impl PayloadMode {
37    /// Mode number for wire protocol.
38    pub fn mode_number(&self) -> u8 {
39        match self {
40            PayloadMode::Text => 0,
41            PayloadMode::SemanticFrame => 1,
42            PayloadMode::EmbeddingHints => 2,
43            PayloadMode::SemanticGraph => 3,
44        }
45    }
46
47    /// Whether this mode is implemented in the current version.
48    pub fn is_implemented(&self) -> bool {
49        matches!(self, PayloadMode::Text | PayloadMode::SemanticFrame)
50    }
51}
52
53impl fmt::Display for PayloadMode {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            PayloadMode::Text => write!(f, "text"),
57            PayloadMode::SemanticFrame => write!(f, "semantic_frame"),
58            PayloadMode::EmbeddingHints => write!(f, "embedding_hints"),
59            PayloadMode::SemanticGraph => write!(f, "semantic_graph"),
60        }
61    }
62}
63
64/// Result of payload mode negotiation between two delegates.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct NegotiatedPayload {
67    /// The agreed-upon mode for this session.
68    pub mode: PayloadMode,
69
70    /// Fallback chain if the primary mode fails mid-session.
71    pub fallback_chain: Vec<PayloadMode>,
72}
73
74impl Default for NegotiatedPayload {
75    fn default() -> Self {
76        Self {
77            mode: PayloadMode::SemanticFrame,
78            fallback_chain: vec![PayloadMode::Text],
79        }
80    }
81}
82
83/// Negotiate the best payload mode from two ordered preference lists.
84///
85/// Returns the highest-preference mode supported by both parties,
86/// or `PayloadMode::Text` as the universal fallback.
87pub fn negotiate_payload_mode(
88    initiator_prefs: &[PayloadMode],
89    responder_prefs: &[PayloadMode],
90) -> NegotiatedPayload {
91    // Find the highest-priority mode the initiator prefers that the responder also supports.
92    let agreed = initiator_prefs
93        .iter()
94        .find(|mode| mode.is_implemented() && responder_prefs.contains(mode))
95        .copied()
96        .unwrap_or(PayloadMode::Text);
97
98    // Build fallback chain: lower-priority modes both support.
99    let fallback_chain: Vec<PayloadMode> = initiator_prefs
100        .iter()
101        .filter(|mode| {
102            mode.is_implemented()
103                && **mode != agreed
104                && responder_prefs.contains(mode)
105                && mode.mode_number() < agreed.mode_number()
106        })
107        .copied()
108        .collect();
109
110    NegotiatedPayload {
111        mode: agreed,
112        fallback_chain,
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn negotiate_both_support_semantic_frame() {
122        let initiator = vec![PayloadMode::SemanticFrame, PayloadMode::Text];
123        let responder = vec![PayloadMode::SemanticFrame, PayloadMode::Text];
124        let result = negotiate_payload_mode(&initiator, &responder);
125        assert_eq!(result.mode, PayloadMode::SemanticFrame);
126        assert_eq!(result.fallback_chain, vec![PayloadMode::Text]);
127    }
128
129    #[test]
130    fn negotiate_falls_back_to_text() {
131        let initiator = vec![PayloadMode::SemanticFrame, PayloadMode::Text];
132        let responder = vec![PayloadMode::Text];
133        let result = negotiate_payload_mode(&initiator, &responder);
134        assert_eq!(result.mode, PayloadMode::Text);
135        assert!(result.fallback_chain.is_empty());
136    }
137
138    #[test]
139    fn negotiate_empty_prefs_default_to_text() {
140        let result = negotiate_payload_mode(&[], &[]);
141        assert_eq!(result.mode, PayloadMode::Text);
142    }
143
144    #[test]
145    fn negotiate_skips_unimplemented_modes() {
146        let initiator = vec![
147            PayloadMode::SemanticGraph,
148            PayloadMode::SemanticFrame,
149            PayloadMode::Text,
150        ];
151        let responder = vec![PayloadMode::SemanticGraph, PayloadMode::Text];
152        let result = negotiate_payload_mode(&initiator, &responder);
153        // SemanticGraph is not implemented, so should fall through.
154        assert_eq!(result.mode, PayloadMode::Text);
155    }
156}