1use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::error::ParseError;
12use crate::framing::project_dto;
13use crate::limits::Limits;
14use crate::logs::{validate_log_record, LogRecord};
15use crate::marker::MAX_SAFE_INTEGER;
16use crate::roles::{valid_capability, Capability, ADAPTER_CAPABILITIES};
17use crate::tree::Snapshot;
18use crate::validate::{validate_snapshot, validate_tree_delta};
19
20pub const PROTOCOL_ID: &str = "termwright/1";
22pub const PROTOCOL_V2_ID: &str = "termwright/2";
24
25pub const PROTOCOL_VERSION: u8 = 1;
27
28const MAX_IDENTIFIER_LENGTH: usize = 1024;
30
31const ERROR_CODES: [&str; 5] = [
32 "bad-token",
33 "bad-version",
34 "malformed",
35 "limit-exceeded",
36 "internal",
37];
38
39const LIMIT_FIELDS: [&str; 11] = [
40 "maxFrameBytes",
41 "maxSnapshotBytes",
42 "maxNodes",
43 "maxDepth",
44 "maxStringBytes",
45 "maxRelationTargets",
46 "maxQueuedFrames",
47 "maxPendingWaiters",
48 "maxSessions",
49 "maxLogRecordBytes",
50 "maxLogQueue",
51];
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct AdapterInfo {
56 pub name: String,
58 pub version: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct Hello {
65 #[serde(rename = "type")]
67 pub kind: String,
68 pub protocol: String,
70 pub token: String,
72 pub adapter: AdapterInfo,
74 pub capabilities: Vec<Capability>,
76 #[serde(skip_serializing_if = "Option::is_none")]
82 pub probe: Option<ProbeInfo>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "kebab-case")]
92pub enum ProbeIdentityKind {
93 Stable,
95 FrameLocal,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct ProbeInfo {
103 pub framework: String,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub framework_version: Option<String>,
108 pub probe_version: String,
110 pub identity_kind: ProbeIdentityKind,
112 pub capabilities: Vec<String>,
114}
115
116impl Hello {
117 pub fn new(token: &str, name: &str, version: &str, capabilities: Vec<Capability>) -> Self {
119 Self {
120 kind: "hello".into(),
121 protocol: PROTOCOL_ID.into(),
122 token: token.to_owned(),
123 adapter: AdapterInfo {
124 name: name.to_owned(),
125 version: version.to_owned(),
126 },
127 capabilities,
128 probe: None,
129 }
130 }
131
132 pub fn new_v2(
134 token: &str,
135 name: &str,
136 version: &str,
137 mut capabilities: Vec<Capability>,
138 ) -> Self {
139 if !capabilities.contains(&Capability::QualifiedObservations) {
140 capabilities.push(Capability::QualifiedObservations);
141 }
142 let mut hello = Self::new(token, name, version, capabilities);
143 hello.protocol = PROTOCOL_V2_ID.into();
144 hello
145 }
146
147 #[must_use]
149 pub fn with_probe(mut self, probe: ProbeInfo) -> Self {
150 self.probe = Some(probe);
151 self
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157pub struct MarkerConfig {
158 pub enabled: bool,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct LogBudget {
168 pub enabled: bool,
170 pub max_records_per_second: i64,
172 pub burst: i64,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct HelloAck {
180 #[serde(rename = "type")]
182 pub kind: String,
183 pub protocol: String,
185 pub session_id: String,
187 pub limits: Limits,
189 pub subscribe: String,
191 pub marker: MarkerConfig,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub logs: Option<LogBudget>,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200pub struct RevisionCommit {
201 #[serde(rename = "type")]
203 pub kind: &'static str,
204 pub revision: i64,
206}
207
208impl RevisionCommit {
209 pub fn new(revision: i64) -> Self {
211 Self {
212 kind: "revision-commit",
213 revision,
214 }
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
220pub struct SnapshotMessage<'a> {
221 #[serde(rename = "type")]
223 pub kind: &'static str,
224 pub snapshot: &'a Snapshot,
226}
227
228impl<'a> SnapshotMessage<'a> {
229 pub fn new(snapshot: &'a Snapshot) -> Self {
231 Self {
232 kind: "snapshot",
233 snapshot,
234 }
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct GetTree {
242 #[serde(rename = "type")]
244 pub kind: String,
245 pub request_id: i64,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub revision: Option<i64>,
250}
251
252#[derive(Debug, Clone, Serialize)]
254#[serde(rename_all = "camelCase")]
255pub struct GetTreeResult {
256 #[serde(rename = "type")]
258 pub kind: &'static str,
259 pub request_id: i64,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub snapshot: Option<Box<serde_json::value::RawValue>>,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub error: Option<String>,
267}
268
269impl GetTreeResult {
270 pub fn found(request_id: i64, snapshot: Box<serde_json::value::RawValue>) -> Self {
272 Self {
273 kind: "get-tree-result",
274 request_id,
275 snapshot: Some(snapshot),
276 error: None,
277 }
278 }
279
280 pub fn missing(request_id: i64, detail: impl Into<String>) -> Self {
282 Self {
283 kind: "get-tree-result",
284 request_id,
285 snapshot: None,
286 error: Some(detail.into()),
287 }
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize)]
293pub struct LogMessage<'a> {
294 #[serde(rename = "type")]
296 pub kind: &'static str,
297 pub record: &'a LogRecord,
299}
300
301impl<'a> LogMessage<'a> {
302 pub fn new(record: &'a LogRecord) -> Self {
304 Self {
305 kind: "log",
306 record,
307 }
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313pub struct ProtocolErrorMessage {
314 #[serde(rename = "type")]
316 pub kind: String,
317 pub code: String,
319 pub message: String,
321}
322
323impl ProtocolErrorMessage {
324 pub fn new(code: &str, message: impl Into<String>) -> Self {
326 Self {
327 kind: "error".into(),
328 code: code.to_owned(),
329 message: message.into(),
330 }
331 }
332}
333
334pub fn default_capabilities() -> Vec<Capability> {
336 vec![
337 Capability::Tree,
338 Capability::Bounds,
339 Capability::AbsoluteBounds,
340 Capability::States,
341 Capability::Actions,
342 Capability::RenderRevisions,
343 ]
344}
345
346fn project(value: &Value, limits: &Limits) -> Result<(), ParseError> {
349 project_dto(value, limits.max_depth).map_err(|violation| {
350 if violation.code == "dto-depth" {
351 ParseError::new("limit-exceeded", violation.to_string())
352 } else {
353 ParseError::malformed(violation.to_string())
354 }
355 })
356}
357
358fn as_message(value: &Value) -> Result<(&Map<String, Value>, &str), ParseError> {
359 let object = value
360 .as_object()
361 .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
362 let kind = object
363 .get("type")
364 .and_then(Value::as_str)
365 .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
366 Ok((object, kind))
367}
368
369fn required_keys(object: &Map<String, Value>, required: &[&str]) -> Result<(), ParseError> {
371 for key in required {
372 if !object.contains_key(*key) {
373 return Err(ParseError::malformed(format!("missing field \"{key}\"")));
374 }
375 }
376 Ok(())
377}
378
379fn require_keys(
380 object: &Map<String, Value>,
381 required: &[&str],
382 optional: &[&str],
383) -> Result<(), ParseError> {
384 for key in required {
385 if !object.contains_key(*key) {
386 return Err(ParseError::malformed(format!("missing field \"{key}\"")));
387 }
388 }
389 for key in object.keys() {
390 if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
391 return Err(ParseError::malformed(format!("unrecognized key \"{key}\"")));
392 }
393 }
394 Ok(())
395}
396
397fn identifier(object: &Map<String, Value>, key: &str, allow_empty: bool) -> Result<(), ParseError> {
398 let Some(text) = object.get(key).and_then(Value::as_str) else {
399 return Err(ParseError::malformed(format!("{key}: expected a string")));
400 };
401 if text.len() > MAX_IDENTIFIER_LENGTH {
402 return Err(ParseError::malformed(format!(
403 "{key}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
404 )));
405 }
406 if !allow_empty && text.is_empty() {
407 return Err(ParseError::malformed(format!(
408 "{key}: expected a non-empty string"
409 )));
410 }
411 Ok(())
412}
413
414fn whole_number(object: &Map<String, Value>, key: &str, positive: bool) -> Result<(), ParseError> {
415 let number = object
416 .get(key)
417 .and_then(Value::as_i64)
418 .filter(|n| n.abs() <= MAX_SAFE_INTEGER);
419 match number {
420 Some(number) if positive && number > 0 => Ok(()),
421 Some(number) if !positive && number >= 0 => Ok(()),
422 _ if positive => Err(ParseError::malformed(format!(
423 "{key}: expected a positive safe integer"
424 ))),
425 _ => Err(ParseError::malformed(format!(
426 "{key}: expected a non-negative safe integer"
427 ))),
428 }
429}
430
431fn check_embedded_snapshot(value: &Value, limits: &Limits) -> Result<(), ParseError> {
432 match validate_snapshot(value, limits) {
433 Ok(()) => Ok(()),
434 Err(error) => {
435 let code = match error.code {
436 "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
437 _ => "malformed",
438 };
439 Err(ParseError::new(code, format!("snapshot {error}")))
440 }
441 }
442}
443
444fn check_log_budget(value: &Value) -> Result<(), ParseError> {
446 let budget = value
447 .as_object()
448 .ok_or_else(|| ParseError::malformed("logs: expected an object"))?;
449 required_keys(budget, &["enabled", "maxRecordsPerSecond", "burst"])?;
450 if !budget["enabled"].is_boolean() {
451 return Err(ParseError::malformed("logs.enabled: expected a boolean"));
452 }
453 whole_number(budget, "maxRecordsPerSecond", true)?;
454 whole_number(budget, "burst", false)
455}
456
457fn check_error_message(object: &Map<String, Value>, strict: bool) -> Result<(), ParseError> {
458 if strict {
459 require_keys(object, &["type", "code", "message"], &[])?;
460 } else {
461 required_keys(object, &["type", "code", "message"])?;
462 }
463 let code = object
464 .get("code")
465 .and_then(Value::as_str)
466 .unwrap_or_default();
467 if !ERROR_CODES.contains(&code) {
468 return Err(ParseError::malformed("code: unknown error code"));
469 }
470 identifier(object, "message", true)
471}
472
473fn check_protocol_field(object: &Map<String, Value>) -> Result<(), ParseError> {
474 match object.get("protocol").and_then(Value::as_str) {
475 Some(protocol) if protocol != PROTOCOL_ID && protocol != PROTOCOL_V2_ID => Err(
476 ParseError::new("bad-version", format!("unsupported protocol {protocol}")),
477 ),
478 _ => Ok(()),
479 }
480}
481
482pub fn parse_adapter_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
491 project(value, limits)?;
492 let (object, kind) = as_message(value)?;
493
494 match kind {
495 "hello" => {
496 check_protocol_field(object)?;
497 require_keys(
498 object,
499 &["type", "protocol", "token", "adapter", "capabilities"],
500 &[],
501 )?;
502 identifier(object, "token", false)?;
503 let adapter = object
504 .get("adapter")
505 .and_then(Value::as_object)
506 .ok_or_else(|| ParseError::malformed("adapter: expected an object"))?;
507 require_keys(adapter, &["name", "version"], &[])?;
508 identifier(adapter, "name", false)?;
509 identifier(adapter, "version", false)?;
510 let capabilities = object
511 .get("capabilities")
512 .and_then(Value::as_array)
513 .ok_or_else(|| ParseError::malformed("capabilities: expected an array"))?;
514 if capabilities.len() > ADAPTER_CAPABILITIES.len() {
515 return Err(ParseError::malformed("capabilities: too many entries"));
516 }
517 for item in capabilities {
518 match item.as_str() {
519 Some(name) if valid_capability(name) => {}
520 _ => return Err(ParseError::malformed("capabilities: unknown capability")),
521 }
522 }
523 let protocol = object
524 .get("protocol")
525 .and_then(Value::as_str)
526 .unwrap_or_default();
527 let qualified = capabilities
528 .iter()
529 .any(|item| item.as_str() == Some("qualified-observations"));
530 let pointer_grid = capabilities
531 .iter()
532 .any(|item| item.as_str() == Some("pointer-hit-grid"));
533 if (protocol == PROTOCOL_V2_ID) != qualified {
534 return Err(ParseError::malformed(
535 "termwright/2 and qualified-observations must be negotiated together",
536 ));
537 }
538 if pointer_grid && !qualified {
539 return Err(ParseError::malformed(
540 "pointer-hit-grid requires qualified-observations",
541 ));
542 }
543 Ok(())
544 }
545 "revision-commit" => {
546 require_keys(object, &["type", "revision"], &[])?;
547 whole_number(object, "revision", true)
548 }
549 "snapshot" => {
550 require_keys(object, &["type", "snapshot"], &[])?;
551 check_embedded_snapshot(&object["snapshot"], limits)
552 }
553 "get-tree-result" => {
554 require_keys(object, &["type", "requestId"], &["snapshot", "error"])?;
555 whole_number(object, "requestId", false)?;
556 let has_snapshot = object.contains_key("snapshot");
557 let has_error = object.contains_key("error");
558 if has_snapshot == has_error {
559 return Err(ParseError::malformed(
560 "exactly one of snapshot or error must be present",
561 ));
562 }
563 if has_error {
564 return identifier(object, "error", true);
565 }
566 check_embedded_snapshot(&object["snapshot"], limits)
567 }
568 "tree-delta" => match validate_tree_delta(value, limits) {
569 Ok(()) => Ok(()),
570 Err(error) => {
571 let code = match error.code {
572 "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
573 _ => "malformed",
574 };
575 Err(ParseError::new(code, format!("tree-delta {error}")))
576 }
577 },
578 "log" => {
579 require_keys(object, &["type", "record"], &[])?;
580 check_embedded_log_record(&object["record"], limits)
581 }
582 "error" => check_error_message(object, true),
583 _ => Err(ParseError::malformed("unknown or missing message type")),
584 }
585}
586
587fn check_embedded_log_record(value: &Value, limits: &Limits) -> Result<(), ParseError> {
590 match validate_log_record(value, limits) {
591 Ok(()) => Ok(()),
592 Err(error) => {
593 let code = match error.code {
594 "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
595 _ => "malformed",
596 };
597 Err(ParseError::new(code, format!("log record {error}")))
598 }
599 }
600}
601
602pub fn parse_driver_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
619 project(value, limits)?;
620 let (object, kind) = as_message(value)?;
621
622 match kind {
623 "hello-ack" => {
624 check_protocol_field(object)?;
625 required_keys(
626 object,
627 &[
628 "type",
629 "protocol",
630 "sessionId",
631 "limits",
632 "subscribe",
633 "marker",
634 ],
635 )?;
636 identifier(object, "sessionId", false)?;
637 let limits_object = object
638 .get("limits")
639 .and_then(Value::as_object)
640 .ok_or_else(|| ParseError::malformed("limits: expected an object"))?;
641 required_keys(limits_object, &LIMIT_FIELDS)?;
644 for field in LIMIT_FIELDS {
645 whole_number(limits_object, field, true)?;
646 }
647 match object.get("subscribe").and_then(Value::as_str) {
648 Some("snapshots") | Some("revisions") | Some("diffs") => {}
649 _ => {
650 return Err(ParseError::malformed(
651 "subscribe: expected 'snapshots', 'revisions' or 'diffs'",
652 ))
653 }
654 }
655 let marker = object
656 .get("marker")
657 .and_then(Value::as_object)
658 .ok_or_else(|| ParseError::malformed("marker: expected an object"))?;
659 required_keys(marker, &["enabled"])?;
660 if !marker["enabled"].is_boolean() {
661 return Err(ParseError::malformed("marker.enabled: expected a boolean"));
662 }
663 if let Some(logs) = object.get("logs") {
664 check_log_budget(logs)?;
665 }
666 Ok(())
667 }
668 "get-tree" => {
669 required_keys(object, &["type", "requestId"])?;
670 whole_number(object, "requestId", false)?;
671 if object.contains_key("revision") {
672 whole_number(object, "revision", true)?;
673 }
674 Ok(())
675 }
676 "error" => check_error_message(object, false),
677 _ => Err(ParseError::malformed("unknown or missing message type")),
678 }
679}