Skip to main content

termwright_protocol/
messages.rs

1//! Wire messages: typed builders for what an adapter sends, checked parsers
2//! for what it receives.
3//!
4//! The adapter pushes commits, the driver issues requests, and either side may
5//! send an error and close. Everything is validated against the active limits
6//! before it is retained; failures are returned, never raised.
7
8use 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;
19use crate::Violation;
20
21/// The wire protocol identifier both sides must agree on.
22pub const PROTOCOL_ID: &str = "termwright/3";
23
24/// The current major version.
25pub const PROTOCOL_VERSION: u8 = 3;
26
27/// Longest token, identifier or free-text message accepted.
28const MAX_IDENTIFIER_LENGTH: usize = 1024;
29
30const ERROR_CODES: [&str; 7] = [
31    "bad-token",
32    "bad-version",
33    "malformed",
34    "limit-exceeded",
35    "duplicate-semantic-key",
36    "adapter-guarantee-violation",
37    "internal",
38];
39
40const LIMIT_FIELDS: [&str; 11] = [
41    "maxFrameBytes",
42    "maxSnapshotBytes",
43    "maxNodes",
44    "maxDepth",
45    "maxStringBytes",
46    "maxRelationTargets",
47    "maxQueuedFrames",
48    "maxPendingWaiters",
49    "maxSessions",
50    "maxLogRecordBytes",
51    "maxLogQueue",
52];
53
54/// Identifies the adapter implementation to the driver.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct AdapterInfo {
57    /// Accessible name; empty when the node has none.
58    pub name: String,
59    /// Adapter version string.
60    pub version: String,
61}
62
63/// The adapter's handshake: sent exactly once, before anything else.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct Hello {
66    /// Wire discriminator (`type` on the wire).
67    #[serde(rename = "type")]
68    pub kind: String,
69    /// Protocol identifier; must be `termwright/3`.
70    pub protocol: String,
71    /// Per-launch session token from the environment.
72    pub token: String,
73    /// Adapter name and version.
74    pub adapter: AdapterInfo,
75    /// What this adapter can provide.
76    pub capabilities: Vec<Capability>,
77    /// Present when the sender is a probe rather than a hand-written adapter.
78    ///
79    /// Carries what the probe can actually observe — framework and versions,
80    /// the best identity it can produce, and its optional abilities — so the
81    /// driver negotiates against measured capability rather than a floor.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub probe: Option<ProbeInfo>,
84    /// Application providers frozen before this handshake.
85    #[serde(default, skip_serializing_if = "Vec::is_empty")]
86    pub providers: Vec<EvidenceProviderRegistration>,
87}
88
89/// Application evidence producer frozen into hello negotiation.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct EvidenceProviderRegistration {
93    /// Stable provider identity.
94    pub id: String,
95    /// Provider implementation version.
96    pub version: String,
97    /// `native` or `declared`.
98    pub method: String,
99    /// Closed provider capability names frozen into the handshake.
100    pub capabilities: Vec<String>,
101}
102
103/// How an object's identity behaves across frames.
104///
105/// `FrameLocal` is a legitimate answer, not a degraded one: in immediate mode
106/// the widget is consumed by the render and nothing survives to be named
107/// again. A consumer must not correlate frame-local values between frames.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "kebab-case")]
110pub enum ProbeIdentityKind {
111    /// Identities survive across frames and may be correlated.
112    Stable,
113    /// Identities are meaningful only within their own frame.
114    FrameLocal,
115}
116
117/// Strongest injection tier that actually engaged for this probe run.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119pub enum ProbeInjectionTier {
120    /// Public framework hook; no injection.
121    T0,
122    /// Add-only compilation unit.
123    T1,
124    /// Append-only source mutation.
125    T2,
126    /// Exact source/control-flow instrumentation.
127    T3,
128}
129
130/// Whether the semantic tree includes authoritative framework geometry.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132pub enum ProbeSemanticClass {
133    /// Semantic tree with framework geometry.
134    A,
135    /// Semantic tree without authoritative framework geometry.
136    B,
137}
138
139/// Closed session-capability vocabulary used for named runtime degradation.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "kebab-case")]
142pub enum DegradedSessionCapability {
143    /// Semantic tree publication.
144    SemanticTree,
145    /// Identity correlation across frames.
146    StableIdentity,
147    /// Framework-intended geometry.
148    IntendedGeometry,
149    /// Geometry clipped by framework ancestors.
150    ClippedGeometry,
151    /// Physically painted terminal region.
152    PaintedRegion,
153    /// Pointer target geometry.
154    PointerGeometry,
155    /// Authoritative pointer hit testing.
156    PointerHitTesting,
157    /// Framework focus state.
158    Focus,
159    /// Framework scroll state and actions.
160    Scroll,
161    /// Authoritative render ordering.
162    RenderOrder,
163    /// Semantic action strategies.
164    ActionStrategies,
165    /// Keyboard input transport.
166    KeyboardInput,
167    /// Pointer input transport.
168    PointerInput,
169    /// Focus input transport.
170    FocusInput,
171    /// Causally paired semantic and terminal revisions.
172    PairedRevisions,
173    /// Enumeration of screens other than the currently committed one.
174    InactiveScreenTree,
175    /// Children hidden behind an application-defined container abstraction.
176    CustomContainerEnumeration,
177}
178
179/// Runtime attachment facts declared in a first-party probe handshake.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct ProbeInstrumentation {
183    /// Strongest injection tier used by this concrete run.
184    pub highest_tier: ProbeInjectionTier,
185    /// Geometry completeness class of the emitted tree.
186    pub semantic_class: ProbeSemanticClass,
187    /// Named session capabilities intentionally unavailable in this integration.
188    pub degraded_capabilities: Vec<DegradedSessionCapability>,
189}
190
191/// What a probe says about itself when it attaches.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct ProbeInfo {
195    /// Framework name, e.g. `ratatui`.
196    pub framework: String,
197    /// Version of the framework, when the probe can determine it.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub framework_version: Option<String>,
200    /// Version of the probe itself, so a mismatch is diagnosable.
201    pub probe_version: String,
202    /// The best identity this probe can offer for any object.
203    pub identity_kind: ProbeIdentityKind,
204    /// Optional abilities, from the protocol's closed set.
205    pub capabilities: Vec<String>,
206    /// Runtime injection facts for a custom probe.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub instrumentation: Option<ProbeInstrumentation>,
209}
210
211impl ProbeInfo {
212    /// Validate the closed probe declaration before any hello reaches the wire.
213    pub fn validate(&self) -> Result<(), Violation> {
214        const CAPABILITIES: &[&str] = &[
215            "stable-identity",
216            "intended-rect",
217            "visible-rect",
218            "operations",
219            "annotations",
220            "frame-begin",
221            "paint-order",
222        ];
223        if self.framework.is_empty() || self.probe_version.is_empty() {
224            return Err(Violation::new(
225                "schema",
226                "probe framework and probeVersion must be non-empty",
227            ));
228        }
229        for (index, capability) in self.capabilities.iter().enumerate() {
230            if !CAPABILITIES.contains(&capability.as_str()) {
231                return Err(Violation::new(
232                    "schema",
233                    format!("unknown probe capability {capability}"),
234                ));
235            }
236            if self.capabilities[..index].contains(capability) {
237                return Err(Violation::new(
238                    "schema",
239                    format!("duplicate probe capability {capability}"),
240                ));
241            }
242        }
243        if self.identity_kind == ProbeIdentityKind::FrameLocal
244            && self
245                .capabilities
246                .iter()
247                .any(|capability| capability == "stable-identity")
248        {
249            return Err(Violation::new(
250                "schema",
251                "frame-local identity cannot advertise stable-identity",
252            ));
253        }
254        if self.instrumentation.is_none() {
255            return Err(Violation::new(
256                "schema",
257                "instrumentation is required for every probe",
258            ));
259        }
260        if let Some(instrumentation) = &self.instrumentation {
261            for (index, capability) in instrumentation.degraded_capabilities.iter().enumerate() {
262                if instrumentation.degraded_capabilities[..index].contains(capability) {
263                    return Err(Violation::new("schema", "duplicate degraded capability"));
264                }
265            }
266            if instrumentation.semantic_class == ProbeSemanticClass::B
267                && (!instrumentation
268                    .degraded_capabilities
269                    .contains(&DegradedSessionCapability::IntendedGeometry)
270                    || !instrumentation
271                        .degraded_capabilities
272                        .contains(&DegradedSessionCapability::ClippedGeometry))
273            {
274                return Err(Violation::new(
275                    "schema",
276                    "semantic class B requires intended-geometry and clipped-geometry degradations",
277                ));
278            }
279        }
280        Ok(())
281    }
282}
283
284impl Hello {
285    /// Build a handshake for this adapter.
286    pub fn new(token: &str, name: &str, version: &str, capabilities: Vec<Capability>) -> Self {
287        Self {
288            kind: "hello".into(),
289            protocol: PROTOCOL_ID.into(),
290            token: token.to_owned(),
291            adapter: AdapterInfo {
292                name: name.to_owned(),
293                version: version.to_owned(),
294            },
295            capabilities,
296            probe: None,
297            providers: Vec::new(),
298        }
299    }
300
301    /// Attach a probe's declaration to this handshake.
302    #[must_use]
303    pub fn with_probe(mut self, probe: ProbeInfo) -> Self {
304        self.probe = Some(probe);
305        self
306    }
307
308    /// Attach application evidence declarations before the hello is sent.
309    #[must_use]
310    pub fn with_providers(mut self, providers: Vec<EvidenceProviderRegistration>) -> Self {
311        self.providers = providers;
312        self
313    }
314}
315
316/// Whether the adapter should emit render markers.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
318pub struct MarkerConfig {
319    /// Whether the adapter should emit render markers.
320    pub enabled: bool,
321}
322
323/// The log-channel allowance, sent only when the adapter announced the `logs`
324/// capability. Absent means logs are disabled: an adapter that receives no
325/// budget must not emit log messages at all.
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct LogBudget {
329    /// Whether the driver wants log records at all.
330    pub enabled: bool,
331    /// Sustained ceiling on records per second.
332    pub max_records_per_second: i64,
333    /// Records allowed in a burst on top of the sustained rate.
334    pub burst: i64,
335}
336
337/// The driver's reply: session id, negotiated limits, what to push.
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct HelloAck {
341    /// Wire discriminator (`type` on the wire).
342    #[serde(rename = "type")]
343    pub kind: String,
344    /// Protocol identifier; must be `termwright/3`.
345    pub protocol: String,
346    /// Session this snapshot belongs to.
347    pub session_id: String,
348    /// Ceilings the driver imposes for this session.
349    pub limits: Limits,
350    /// The semantic publication channel selected by the driver.
351    pub subscribe: String,
352    /// Whether render markers are wanted.
353    pub marker: MarkerConfig,
354    /// Log-channel budget; `None` means logs are disabled.
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub logs: Option<LogBudget>,
357}
358
359/// Announces that a render was committed to the terminal.
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
361pub struct RevisionCommit {
362    /// Wire discriminator (`type` on the wire).
363    #[serde(rename = "type")]
364    pub kind: &'static str,
365    /// Render revision, strictly increasing per session.
366    pub revision: i64,
367}
368
369impl RevisionCommit {
370    /// Commit `revision`.
371    pub fn new(revision: i64) -> Self {
372        Self {
373            kind: "revision-commit",
374            revision,
375        }
376    }
377}
378
379/// Carries a full tree for one revision.
380#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
381pub struct SemanticFullMessage<'a> {
382    /// Wire discriminator (`type` on the wire).
383    #[serde(rename = "type")]
384    pub kind: &'static str,
385    /// The tree being carried.
386    pub snapshot: &'a Snapshot,
387}
388
389impl<'a> SemanticFullMessage<'a> {
390    /// Wrap a snapshot in its envelope.
391    pub fn new(snapshot: &'a Snapshot) -> Self {
392        Self {
393            kind: "semantic-full",
394            snapshot,
395        }
396    }
397}
398
399/// Carries one application log record to the driver.
400#[derive(Debug, Clone, PartialEq, Serialize)]
401pub struct LogMessage<'a> {
402    /// Wire discriminator (`type` on the wire).
403    #[serde(rename = "type")]
404    pub kind: &'static str,
405    /// The record.
406    pub record: &'a LogRecord,
407}
408
409impl<'a> LogMessage<'a> {
410    /// Wrap a record in its envelope.
411    pub fn new(record: &'a LogRecord) -> Self {
412        Self {
413            kind: "log",
414            record,
415        }
416    }
417}
418
419/// Terminal error: the sender closes after emitting it.
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421pub struct ProtocolErrorMessage {
422    /// Wire discriminator (`type` on the wire).
423    #[serde(rename = "type")]
424    pub kind: String,
425    /// One of the five wire error codes.
426    pub code: String,
427    /// Human-readable detail; never carries the token.
428    pub message: String,
429}
430
431impl ProtocolErrorMessage {
432    /// Build an error message with one of the five wire codes.
433    pub fn new(code: &str, message: impl Into<String>) -> Self {
434        Self {
435            kind: "error".into(),
436            code: code.to_owned(),
437            message: message.into(),
438        }
439    }
440}
441
442/// Capabilities for a tree-publishing adapter with qualified geometry.
443pub fn default_capabilities() -> Vec<Capability> {
444    vec![
445        Capability::Tree,
446        Capability::IntendedGeometry,
447        Capability::ClippedGeometry,
448        Capability::States,
449        Capability::Actions,
450        Capability::RenderRevisions,
451    ]
452}
453
454// -- parsing ---------------------------------------------------------------
455
456fn project(value: &Value, limits: &Limits) -> Result<(), ParseError> {
457    project_dto(value, limits.max_depth).map_err(|violation| {
458        if violation.code == "dto-depth" {
459            ParseError::new("limit-exceeded", violation.to_string())
460        } else {
461            ParseError::malformed(violation.to_string())
462        }
463    })
464}
465
466fn as_message(value: &Value) -> Result<(&Map<String, Value>, &str), ParseError> {
467    let object = value
468        .as_object()
469        .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
470    let kind = object
471        .get("type")
472        .and_then(Value::as_str)
473        .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
474    Ok((object, kind))
475}
476
477/// Check that every required key is present, tolerating unknown ones.
478fn required_keys(object: &Map<String, Value>, required: &[&str]) -> Result<(), ParseError> {
479    for key in required {
480        if !object.contains_key(*key) {
481            return Err(ParseError::malformed(format!("missing field \"{key}\"")));
482        }
483    }
484    Ok(())
485}
486
487fn require_keys(
488    object: &Map<String, Value>,
489    required: &[&str],
490    optional: &[&str],
491) -> Result<(), ParseError> {
492    for key in required {
493        if !object.contains_key(*key) {
494            return Err(ParseError::malformed(format!("missing field \"{key}\"")));
495        }
496    }
497    for key in object.keys() {
498        if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
499            return Err(ParseError::malformed(format!("unrecognized key \"{key}\"")));
500        }
501    }
502    Ok(())
503}
504
505fn identifier(object: &Map<String, Value>, key: &str, allow_empty: bool) -> Result<(), ParseError> {
506    let Some(text) = object.get(key).and_then(Value::as_str) else {
507        return Err(ParseError::malformed(format!("{key}: expected a string")));
508    };
509    if text.len() > MAX_IDENTIFIER_LENGTH {
510        return Err(ParseError::malformed(format!(
511            "{key}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
512        )));
513    }
514    if !allow_empty && text.is_empty() {
515        return Err(ParseError::malformed(format!(
516            "{key}: expected a non-empty string"
517        )));
518    }
519    Ok(())
520}
521
522fn whole_number(object: &Map<String, Value>, key: &str, positive: bool) -> Result<(), ParseError> {
523    let number = object
524        .get(key)
525        .and_then(Value::as_i64)
526        .filter(|n| n.abs() <= MAX_SAFE_INTEGER);
527    match number {
528        Some(number) if positive && number > 0 => Ok(()),
529        Some(number) if !positive && number >= 0 => Ok(()),
530        _ if positive => Err(ParseError::malformed(format!(
531            "{key}: expected a positive safe integer"
532        ))),
533        _ => Err(ParseError::malformed(format!(
534            "{key}: expected a non-negative safe integer"
535        ))),
536    }
537}
538
539fn check_embedded_snapshot(value: &Value, limits: &Limits) -> Result<(), ParseError> {
540    match validate_snapshot(value, limits) {
541        Ok(()) => Ok(()),
542        Err(error) => {
543            let code = match error.code {
544                "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
545                _ => "malformed",
546            };
547            Err(ParseError::new(code, format!("snapshot {error}")))
548        }
549    }
550}
551
552/// Validate the optional log-channel budget carried by `hello-ack`.
553fn check_log_budget(value: &Value) -> Result<(), ParseError> {
554    let budget = value
555        .as_object()
556        .ok_or_else(|| ParseError::malformed("logs: expected an object"))?;
557    required_keys(budget, &["enabled", "maxRecordsPerSecond", "burst"])?;
558    if !budget["enabled"].is_boolean() {
559        return Err(ParseError::malformed("logs.enabled: expected a boolean"));
560    }
561    whole_number(budget, "maxRecordsPerSecond", true)?;
562    whole_number(budget, "burst", false)
563}
564
565fn check_error_message(object: &Map<String, Value>, strict: bool) -> Result<(), ParseError> {
566    if strict {
567        require_keys(object, &["type", "code", "message"], &[])?;
568    } else {
569        required_keys(object, &["type", "code", "message"])?;
570    }
571    let code = object
572        .get("code")
573        .and_then(Value::as_str)
574        .unwrap_or_default();
575    if !ERROR_CODES.contains(&code) {
576        return Err(ParseError::malformed("code: unknown error code"));
577    }
578    identifier(object, "message", true)
579}
580
581fn check_protocol_field(object: &Map<String, Value>) -> Result<(), ParseError> {
582    match object.get("protocol").and_then(Value::as_str) {
583        Some(protocol) if protocol != PROTOCOL_ID => Err(ParseError::new(
584            "bad-version",
585            format!("unsupported protocol {protocol}"),
586        )),
587        _ => Ok(()),
588    }
589}
590
591/// Validate one adapter → driver message.
592///
593/// Strict: an unknown field from an adapter is a protocol error, not an
594/// extension. See [`parse_driver_message`] for the other direction.
595///
596/// # Errors
597/// Returns a [`ParseError`] whose `code` is `bad-version`, `malformed` or
598/// `limit-exceeded`.
599pub fn parse_adapter_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
600    project(value, limits)?;
601    let (object, kind) = as_message(value)?;
602
603    match kind {
604        "hello" => {
605            check_protocol_field(object)?;
606            require_keys(
607                object,
608                &["type", "protocol", "token", "adapter", "capabilities"],
609                &["probe", "providers"],
610            )?;
611            identifier(object, "token", false)?;
612            let adapter = object
613                .get("adapter")
614                .and_then(Value::as_object)
615                .ok_or_else(|| ParseError::malformed("adapter: expected an object"))?;
616            require_keys(adapter, &["name", "version"], &[])?;
617            identifier(adapter, "name", false)?;
618            identifier(adapter, "version", false)?;
619            let capabilities = object
620                .get("capabilities")
621                .and_then(Value::as_array)
622                .ok_or_else(|| ParseError::malformed("capabilities: expected an array"))?;
623            if capabilities.len() > ADAPTER_CAPABILITIES.len() {
624                return Err(ParseError::malformed("capabilities: too many entries"));
625            }
626            for item in capabilities {
627                match item.as_str() {
628                    Some(name) if valid_capability(name) => {}
629                    _ => return Err(ParseError::malformed("capabilities: unknown capability")),
630                }
631            }
632            Ok(())
633        }
634        "revision-commit" => {
635            require_keys(object, &["type", "revision"], &[])?;
636            whole_number(object, "revision", true)
637        }
638        "semantic-full" => {
639            require_keys(object, &["type", "snapshot"], &[])?;
640            check_embedded_snapshot(&object["snapshot"], limits)
641        }
642        "log" => {
643            require_keys(object, &["type", "record"], &[])?;
644            check_embedded_log_record(&object["record"], limits)
645        }
646        "error" => check_error_message(object, true),
647        _ => Err(ParseError::malformed("unknown or missing message type")),
648    }
649}
650
651/// Map a record failure onto the wire taxonomy: capacity failures are
652/// `limit-exceeded`, the rest are `malformed`.
653fn check_embedded_log_record(value: &Value, limits: &Limits) -> Result<(), ParseError> {
654    match validate_log_record(value, limits) {
655        Ok(()) => Ok(()),
656        Err(error) => {
657            let code = match error.code {
658                "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
659                _ => "malformed",
660            };
661            Err(ParseError::new(code, format!("log record {error}")))
662        }
663    }
664}
665
666/// Validate one driver → adapter message.
667///
668/// Driver traffic is read tolerantly: unknown fields in the envelope and in
669/// the driver's nested objects (`marker`, `logs`, `limits`) are ignored and
670/// passed through to the caller, so a newer driver can add a field without
671/// breaking an adapter published before it existed.
672///
673/// The asymmetry is about who is speaking, not about the message: adapter
674/// traffic crosses an untrusted boundary, where an unknown field is a signal
675/// rather than an extension. Tolerance is not leniency either — known fields
676/// keep their types, and the closed sets (message types, error codes,
677/// `subscribe`, roles, actions) stay closed in both directions.
678///
679/// # Errors
680/// Returns a [`ParseError`] whose `code` is `bad-version`, `malformed` or
681/// `limit-exceeded`.
682pub fn parse_driver_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
683    project(value, limits)?;
684    let (object, kind) = as_message(value)?;
685
686    match kind {
687        "hello-ack" => {
688            check_protocol_field(object)?;
689            required_keys(
690                object,
691                &[
692                    "type",
693                    "protocol",
694                    "sessionId",
695                    "limits",
696                    "subscribe",
697                    "marker",
698                ],
699            )?;
700            identifier(object, "sessionId", false)?;
701            let limits_object = object
702                .get("limits")
703                .and_then(Value::as_object)
704                .ok_or_else(|| ParseError::malformed("limits: expected an object"))?;
705            // Required keys must all be present, but unknown ones are
706            // ignored: see the note on `Limits`.
707            required_keys(limits_object, &LIMIT_FIELDS)?;
708            for field in LIMIT_FIELDS {
709                whole_number(limits_object, field, true)?;
710            }
711            match object.get("subscribe").and_then(Value::as_str) {
712                Some("semantic") => {}
713                _ => return Err(ParseError::malformed("subscribe: expected 'semantic'")),
714            }
715            let marker = object
716                .get("marker")
717                .and_then(Value::as_object)
718                .ok_or_else(|| ParseError::malformed("marker: expected an object"))?;
719            required_keys(marker, &["enabled"])?;
720            if !marker["enabled"].is_boolean() {
721                return Err(ParseError::malformed("marker.enabled: expected a boolean"));
722            }
723            if let Some(logs) = object.get("logs") {
724                check_log_budget(logs)?;
725            }
726            Ok(())
727        }
728        "semantic-resync-request" => {
729            required_keys(
730                object,
731                &["type", "sessionId", "expectedBaseRevision", "reason"],
732            )?;
733            identifier(object, "sessionId", false)?;
734            if !object["expectedBaseRevision"].is_null() {
735                whole_number(object, "expectedBaseRevision", true)?;
736            }
737            match object["reason"].as_str() {
738                Some("base-mismatch" | "missing-base" | "driver-reset") => Ok(()),
739                _ => Err(ParseError::malformed(
740                    "reason: unknown semantic resync reason",
741                )),
742            }
743        }
744        "error" => check_error_message(object, false),
745        _ => Err(ParseError::malformed("unknown or missing message type")),
746    }
747}