Skip to main content

lsp_max_protocol/
core.rs

1use crate::{ConformanceVector, MaxCodeAction, MaxDiagnostic, PolicyState, SnapshotId};
2use lsp_types_max::{ClientCapabilities, ServerCapabilities};
3use serde::{Deserialize, Serialize};
4
5// ---------------------------------------------------------------------------
6// InstanceId — newtype for LSP instance identifiers
7// ---------------------------------------------------------------------------
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct InstanceId(pub String);
11
12impl From<String> for InstanceId {
13    fn from(s: String) -> Self {
14        InstanceId(s)
15    }
16}
17
18impl From<&str> for InstanceId {
19    fn from(s: &str) -> Self {
20        InstanceId(s.to_string())
21    }
22}
23
24impl PartialEq<str> for InstanceId {
25    fn eq(&self, other: &str) -> bool {
26        self.0 == other
27    }
28}
29impl PartialEq<InstanceId> for str {
30    fn eq(&self, other: &InstanceId) -> bool {
31        self == other.0
32    }
33}
34
35impl std::fmt::Display for InstanceId {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}", self.0)
38    }
39}
40
41// ---------------------------------------------------------------------------
42// GateId / ReceiptObligation
43// ---------------------------------------------------------------------------
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
46pub struct GateId(pub String);
47
48impl std::fmt::Display for GateId {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(f, "{}", self.0)
51    }
52}
53
54impl From<String> for GateId {
55    fn from(s: String) -> Self {
56        Self(s)
57    }
58}
59
60impl From<&str> for GateId {
61    fn from(s: &str) -> Self {
62        Self(s.to_owned())
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ReceiptObligation {
68    pub required_receipts: Vec<String>,
69}
70
71// ---------------------------------------------------------------------------
72// MaxCapabilityVector / CapabilityGap
73// ---------------------------------------------------------------------------
74
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct MaxCapabilityVector {
77    pub client: ClientCapabilities,
78    pub server: ServerCapabilities,
79    pub negotiated: serde_json::Value,
80    pub experimental: serde_json::Value,
81    pub gaps: Vec<CapabilityGap>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CapabilityGap {
86    pub capability_path: String,
87    pub reason: String,
88}
89
90// ---------------------------------------------------------------------------
91// Receipt
92// ---------------------------------------------------------------------------
93
94#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct Receipt {
96    pub receipt_id: String,
97    pub hash: String,
98    /// Hash of the immediately preceding receipt in the instance ledger.
99    /// `None` for genesis (first) receipts only.  All subsequent receipts
100    /// must set this to close the Merkle chain and make `verify_instance_ledger`
101    /// meaningful for non-LSP_1 instances.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub prev_receipt_hash: Option<String>,
104}
105
106// ---------------------------------------------------------------------------
107// AnalysisBundle
108// ---------------------------------------------------------------------------
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct AnalysisBundle {
112    pub snapshot_id: SnapshotId,
113    pub capability_vector: MaxCapabilityVector,
114    pub diagnostics: Vec<MaxDiagnostic>,
115    pub actions: Vec<MaxCodeAction>,
116    pub conformance_vector: ConformanceVector,
117    pub receipts: Vec<Receipt>,
118}
119
120impl Default for AnalysisBundle {
121    fn default() -> Self {
122        Self {
123            snapshot_id: SnapshotId(String::new()),
124            capability_vector: MaxCapabilityVector {
125                client: lsp_types_max::ClientCapabilities::default(),
126                server: lsp_types_max::ServerCapabilities::default(),
127                negotiated: serde_json::Value::Null,
128                experimental: serde_json::Value::Null,
129                gaps: Vec::new(),
130            },
131            diagnostics: Vec::new(),
132            actions: Vec::new(),
133            conformance_vector: ConformanceVector::default(),
134            receipts: Vec::new(),
135        }
136    }
137}
138
139// ---------------------------------------------------------------------------
140// LspStateModel
141// ---------------------------------------------------------------------------
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct LspStateModel {
145    pub instance_id: InstanceId,
146    pub phase: String, // e.g. "Uninitialized", "Initializing", "Initialized", etc.
147    pub diagnostics: Vec<MaxDiagnostic>,
148    pub receipts: Vec<Receipt>,
149    pub policy_state: Option<PolicyState>,
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::ReceiptPlan;
156
157    #[test]
158    fn receipt_genesis_has_no_prev_hash() {
159        let r = Receipt {
160            receipt_id: "r-0".to_string(),
161            hash: "abc123".to_string(),
162            prev_receipt_hash: None,
163        };
164        assert!(
165            r.prev_receipt_hash.is_none(),
166            "genesis receipt must have no prev_receipt_hash"
167        );
168    }
169
170    #[test]
171    fn receipt_chain_links_prev_hash() {
172        let genesis = Receipt {
173            receipt_id: "r-0".to_string(),
174            hash: "hash0".to_string(),
175            prev_receipt_hash: None,
176        };
177        let next = Receipt {
178            receipt_id: "r-1".to_string(),
179            hash: "hash1".to_string(),
180            prev_receipt_hash: Some(genesis.hash.clone()),
181        };
182        assert_eq!(
183            next.prev_receipt_hash.as_deref(),
184            Some("hash0"),
185            "second receipt must link to genesis hash"
186        );
187    }
188
189    #[test]
190    fn receipt_plan_is_satisfied_by() {
191        let plan = ReceiptPlan {
192            expected_receipts: vec!["r-0".to_string(), "r-1".to_string()],
193        };
194        let receipts = [
195            Receipt {
196                receipt_id: "r-0".to_string(),
197                hash: "h0".to_string(),
198                prev_receipt_hash: None,
199            },
200            Receipt {
201                receipt_id: "r-1".to_string(),
202                hash: "h1".to_string(),
203                prev_receipt_hash: Some("h0".to_string()),
204            },
205        ];
206        let actual_ids: Vec<&str> = receipts.iter().map(|r| r.receipt_id.as_str()).collect();
207        for expected in &plan.expected_receipts {
208            assert!(
209                actual_ids.contains(&expected.as_str()),
210                "receipt {} missing",
211                expected
212            );
213        }
214    }
215
216    #[test]
217    fn analysis_bundle_is_empty() {
218        let bundle = AnalysisBundle::default();
219        assert!(bundle.diagnostics.is_empty());
220        assert!(bundle.actions.is_empty());
221        assert!(bundle.receipts.is_empty());
222        assert!(bundle.conformance_vector.admitted.is_empty());
223    }
224
225    #[test]
226    fn instance_id_from_str_and_display() {
227        let id = InstanceId::from("test-instance");
228        assert_eq!(id.to_string(), "test-instance");
229        assert_eq!(id.0, "test-instance");
230    }
231
232    #[test]
233    fn gate_id_from_str_and_display() {
234        let gate: GateId = GateId::from("gate-42");
235        assert_eq!(gate.to_string(), "gate-42");
236    }
237
238    #[test]
239    fn receipt_serde_roundtrip_omits_none_prev_hash() {
240        let r = Receipt {
241            receipt_id: "r-0".to_string(),
242            hash: "deadbeef".to_string(),
243            prev_receipt_hash: None,
244        };
245        let json = serde_json::to_string(&r).expect("serialize");
246        assert!(
247            !json.contains("prev_receipt_hash"),
248            "None must be omitted: {}",
249            json
250        );
251        let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
252        assert!(r2.prev_receipt_hash.is_none());
253    }
254
255    #[test]
256    fn receipt_serde_roundtrip_includes_prev_hash_when_set() {
257        let r = Receipt {
258            receipt_id: "r-1".to_string(),
259            hash: "cafebabe".to_string(),
260            prev_receipt_hash: Some("deadbeef".to_string()),
261        };
262        let json = serde_json::to_string(&r).expect("serialize");
263        let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
264        assert_eq!(r2.prev_receipt_hash.as_deref(), Some("deadbeef"));
265    }
266}