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/2";
23
24/// The current major version.
25pub const PROTOCOL_VERSION: u8 = 2;
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/2`.
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. Optional for older/custom protocol-v2 probes.
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 let Some(instrumentation) = &self.instrumentation {
255            for (index, capability) in instrumentation.degraded_capabilities.iter().enumerate() {
256                if instrumentation.degraded_capabilities[..index].contains(capability) {
257                    return Err(Violation::new("schema", "duplicate degraded capability"));
258                }
259            }
260            if instrumentation.semantic_class == ProbeSemanticClass::B
261                && (!instrumentation
262                    .degraded_capabilities
263                    .contains(&DegradedSessionCapability::IntendedGeometry)
264                    || !instrumentation
265                        .degraded_capabilities
266                        .contains(&DegradedSessionCapability::ClippedGeometry))
267            {
268                return Err(Violation::new(
269                    "schema",
270                    "semantic class B requires intended-geometry and clipped-geometry degradations",
271                ));
272            }
273        }
274        Ok(())
275    }
276}
277
278impl Hello {
279    /// Build a handshake for this adapter.
280    pub fn new(token: &str, name: &str, version: &str, capabilities: Vec<Capability>) -> Self {
281        Self {
282            kind: "hello".into(),
283            protocol: PROTOCOL_ID.into(),
284            token: token.to_owned(),
285            adapter: AdapterInfo {
286                name: name.to_owned(),
287                version: version.to_owned(),
288            },
289            capabilities,
290            probe: None,
291            providers: Vec::new(),
292        }
293    }
294
295    /// Attach a probe's declaration to this handshake.
296    #[must_use]
297    pub fn with_probe(mut self, probe: ProbeInfo) -> Self {
298        self.probe = Some(probe);
299        self
300    }
301
302    /// Attach application evidence declarations before the hello is sent.
303    #[must_use]
304    pub fn with_providers(mut self, providers: Vec<EvidenceProviderRegistration>) -> Self {
305        self.providers = providers;
306        self
307    }
308}
309
310/// Whether the adapter should emit render markers.
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312pub struct MarkerConfig {
313    /// Whether the adapter should emit render markers.
314    pub enabled: bool,
315}
316
317/// The log-channel allowance, sent only when the adapter announced the `logs`
318/// capability. Absent means logs are disabled: an adapter that receives no
319/// budget must not emit log messages at all.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct LogBudget {
323    /// Whether the driver wants log records at all.
324    pub enabled: bool,
325    /// Sustained ceiling on records per second.
326    pub max_records_per_second: i64,
327    /// Records allowed in a burst on top of the sustained rate.
328    pub burst: i64,
329}
330
331/// The driver's reply: session id, negotiated limits, what to push.
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase")]
334pub struct HelloAck {
335    /// Wire discriminator (`type` on the wire).
336    #[serde(rename = "type")]
337    pub kind: String,
338    /// Protocol identifier; must be `termwright/2`.
339    pub protocol: String,
340    /// Session this snapshot belongs to.
341    pub session_id: String,
342    /// Ceilings the driver imposes for this session.
343    pub limits: Limits,
344    /// What the driver wants pushed: snapshots or revisions.
345    pub subscribe: String,
346    /// Whether render markers are wanted.
347    pub marker: MarkerConfig,
348    /// Log-channel budget; `None` means logs are disabled.
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub logs: Option<LogBudget>,
351}
352
353/// Announces that a render was committed to the terminal.
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
355pub struct RevisionCommit {
356    /// Wire discriminator (`type` on the wire).
357    #[serde(rename = "type")]
358    pub kind: &'static str,
359    /// Render revision, strictly increasing per session.
360    pub revision: i64,
361}
362
363impl RevisionCommit {
364    /// Commit `revision`.
365    pub fn new(revision: i64) -> Self {
366        Self {
367            kind: "revision-commit",
368            revision,
369        }
370    }
371}
372
373/// Carries a full tree for one revision.
374#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
375pub struct SnapshotMessage<'a> {
376    /// Wire discriminator (`type` on the wire).
377    #[serde(rename = "type")]
378    pub kind: &'static str,
379    /// The tree being carried.
380    pub snapshot: &'a Snapshot,
381}
382
383impl<'a> SnapshotMessage<'a> {
384    /// Wrap a snapshot in its envelope.
385    pub fn new(snapshot: &'a Snapshot) -> Self {
386        Self {
387            kind: "snapshot",
388            snapshot,
389        }
390    }
391}
392
393/// Carries one application log record to the driver.
394#[derive(Debug, Clone, PartialEq, Serialize)]
395pub struct LogMessage<'a> {
396    /// Wire discriminator (`type` on the wire).
397    #[serde(rename = "type")]
398    pub kind: &'static str,
399    /// The record.
400    pub record: &'a LogRecord,
401}
402
403impl<'a> LogMessage<'a> {
404    /// Wrap a record in its envelope.
405    pub fn new(record: &'a LogRecord) -> Self {
406        Self {
407            kind: "log",
408            record,
409        }
410    }
411}
412
413/// Terminal error: the sender closes after emitting it.
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct ProtocolErrorMessage {
416    /// Wire discriminator (`type` on the wire).
417    #[serde(rename = "type")]
418    pub kind: String,
419    /// One of the five wire error codes.
420    pub code: String,
421    /// Human-readable detail; never carries the token.
422    pub message: String,
423}
424
425impl ProtocolErrorMessage {
426    /// Build an error message with one of the five wire codes.
427    pub fn new(code: &str, message: impl Into<String>) -> Self {
428        Self {
429            kind: "error".into(),
430            code: code.to_owned(),
431            message: message.into(),
432        }
433    }
434}
435
436/// Capabilities for a tree-publishing adapter with qualified geometry.
437pub fn default_capabilities() -> Vec<Capability> {
438    vec![
439        Capability::Tree,
440        Capability::IntendedGeometry,
441        Capability::ClippedGeometry,
442        Capability::States,
443        Capability::Actions,
444        Capability::RenderRevisions,
445    ]
446}
447
448// -- parsing ---------------------------------------------------------------
449
450fn project(value: &Value, limits: &Limits) -> Result<(), ParseError> {
451    project_dto(value, limits.max_depth).map_err(|violation| {
452        if violation.code == "dto-depth" {
453            ParseError::new("limit-exceeded", violation.to_string())
454        } else {
455            ParseError::malformed(violation.to_string())
456        }
457    })
458}
459
460fn as_message(value: &Value) -> Result<(&Map<String, Value>, &str), ParseError> {
461    let object = value
462        .as_object()
463        .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
464    let kind = object
465        .get("type")
466        .and_then(Value::as_str)
467        .ok_or_else(|| ParseError::malformed("unknown or missing message type"))?;
468    Ok((object, kind))
469}
470
471/// Check that every required key is present, tolerating unknown ones.
472fn required_keys(object: &Map<String, Value>, required: &[&str]) -> Result<(), ParseError> {
473    for key in required {
474        if !object.contains_key(*key) {
475            return Err(ParseError::malformed(format!("missing field \"{key}\"")));
476        }
477    }
478    Ok(())
479}
480
481fn require_keys(
482    object: &Map<String, Value>,
483    required: &[&str],
484    optional: &[&str],
485) -> Result<(), ParseError> {
486    for key in required {
487        if !object.contains_key(*key) {
488            return Err(ParseError::malformed(format!("missing field \"{key}\"")));
489        }
490    }
491    for key in object.keys() {
492        if !required.contains(&key.as_str()) && !optional.contains(&key.as_str()) {
493            return Err(ParseError::malformed(format!("unrecognized key \"{key}\"")));
494        }
495    }
496    Ok(())
497}
498
499fn identifier(object: &Map<String, Value>, key: &str, allow_empty: bool) -> Result<(), ParseError> {
500    let Some(text) = object.get(key).and_then(Value::as_str) else {
501        return Err(ParseError::malformed(format!("{key}: expected a string")));
502    };
503    if text.len() > MAX_IDENTIFIER_LENGTH {
504        return Err(ParseError::malformed(format!(
505            "{key}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
506        )));
507    }
508    if !allow_empty && text.is_empty() {
509        return Err(ParseError::malformed(format!(
510            "{key}: expected a non-empty string"
511        )));
512    }
513    Ok(())
514}
515
516fn whole_number(object: &Map<String, Value>, key: &str, positive: bool) -> Result<(), ParseError> {
517    let number = object
518        .get(key)
519        .and_then(Value::as_i64)
520        .filter(|n| n.abs() <= MAX_SAFE_INTEGER);
521    match number {
522        Some(number) if positive && number > 0 => Ok(()),
523        Some(number) if !positive && number >= 0 => Ok(()),
524        _ if positive => Err(ParseError::malformed(format!(
525            "{key}: expected a positive safe integer"
526        ))),
527        _ => Err(ParseError::malformed(format!(
528            "{key}: expected a non-negative safe integer"
529        ))),
530    }
531}
532
533fn check_embedded_snapshot(value: &Value, limits: &Limits) -> Result<(), ParseError> {
534    match validate_snapshot(value, limits) {
535        Ok(()) => Ok(()),
536        Err(error) => {
537            let code = match error.code {
538                "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
539                _ => "malformed",
540            };
541            Err(ParseError::new(code, format!("snapshot {error}")))
542        }
543    }
544}
545
546/// Validate the optional log-channel budget carried by `hello-ack`.
547fn check_log_budget(value: &Value) -> Result<(), ParseError> {
548    let budget = value
549        .as_object()
550        .ok_or_else(|| ParseError::malformed("logs: expected an object"))?;
551    required_keys(budget, &["enabled", "maxRecordsPerSecond", "burst"])?;
552    if !budget["enabled"].is_boolean() {
553        return Err(ParseError::malformed("logs.enabled: expected a boolean"));
554    }
555    whole_number(budget, "maxRecordsPerSecond", true)?;
556    whole_number(budget, "burst", false)
557}
558
559fn check_error_message(object: &Map<String, Value>, strict: bool) -> Result<(), ParseError> {
560    if strict {
561        require_keys(object, &["type", "code", "message"], &[])?;
562    } else {
563        required_keys(object, &["type", "code", "message"])?;
564    }
565    let code = object
566        .get("code")
567        .and_then(Value::as_str)
568        .unwrap_or_default();
569    if !ERROR_CODES.contains(&code) {
570        return Err(ParseError::malformed("code: unknown error code"));
571    }
572    identifier(object, "message", true)
573}
574
575fn check_protocol_field(object: &Map<String, Value>) -> Result<(), ParseError> {
576    match object.get("protocol").and_then(Value::as_str) {
577        Some(protocol) if protocol != PROTOCOL_ID => Err(ParseError::new(
578            "bad-version",
579            format!("unsupported protocol {protocol}"),
580        )),
581        _ => Ok(()),
582    }
583}
584
585/// Validate one adapter → driver message.
586///
587/// Strict: an unknown field from an adapter is a protocol error, not an
588/// extension. See [`parse_driver_message`] for the other direction.
589///
590/// # Errors
591/// Returns a [`ParseError`] whose `code` is `bad-version`, `malformed` or
592/// `limit-exceeded`.
593pub fn parse_adapter_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
594    project(value, limits)?;
595    let (object, kind) = as_message(value)?;
596
597    match kind {
598        "hello" => {
599            check_protocol_field(object)?;
600            require_keys(
601                object,
602                &["type", "protocol", "token", "adapter", "capabilities"],
603                &["probe", "providers"],
604            )?;
605            identifier(object, "token", false)?;
606            let adapter = object
607                .get("adapter")
608                .and_then(Value::as_object)
609                .ok_or_else(|| ParseError::malformed("adapter: expected an object"))?;
610            require_keys(adapter, &["name", "version"], &[])?;
611            identifier(adapter, "name", false)?;
612            identifier(adapter, "version", false)?;
613            let capabilities = object
614                .get("capabilities")
615                .and_then(Value::as_array)
616                .ok_or_else(|| ParseError::malformed("capabilities: expected an array"))?;
617            if capabilities.len() > ADAPTER_CAPABILITIES.len() {
618                return Err(ParseError::malformed("capabilities: too many entries"));
619            }
620            for item in capabilities {
621                match item.as_str() {
622                    Some(name) if valid_capability(name) => {}
623                    _ => return Err(ParseError::malformed("capabilities: unknown capability")),
624                }
625            }
626            Ok(())
627        }
628        "revision-commit" => {
629            require_keys(object, &["type", "revision"], &[])?;
630            whole_number(object, "revision", true)
631        }
632        "snapshot" => {
633            require_keys(object, &["type", "snapshot"], &[])?;
634            check_embedded_snapshot(&object["snapshot"], limits)
635        }
636        "log" => {
637            require_keys(object, &["type", "record"], &[])?;
638            check_embedded_log_record(&object["record"], limits)
639        }
640        "error" => check_error_message(object, true),
641        _ => Err(ParseError::malformed("unknown or missing message type")),
642    }
643}
644
645/// Map a record failure onto the wire taxonomy: capacity failures are
646/// `limit-exceeded`, the rest are `malformed`.
647fn check_embedded_log_record(value: &Value, limits: &Limits) -> Result<(), ParseError> {
648    match validate_log_record(value, limits) {
649        Ok(()) => Ok(()),
650        Err(error) => {
651            let code = match error.code {
652                "bytes" | "count" | "depth" | "string-bytes" => "limit-exceeded",
653                _ => "malformed",
654            };
655            Err(ParseError::new(code, format!("log record {error}")))
656        }
657    }
658}
659
660/// Validate one driver → adapter message.
661///
662/// Driver traffic is read tolerantly: unknown fields in the envelope and in
663/// the driver's nested objects (`marker`, `logs`, `limits`) are ignored and
664/// passed through to the caller, so a newer driver can add a field without
665/// breaking an adapter published before it existed.
666///
667/// The asymmetry is about who is speaking, not about the message: adapter
668/// traffic crosses an untrusted boundary, where an unknown field is a signal
669/// rather than an extension. Tolerance is not leniency either — known fields
670/// keep their types, and the closed sets (message types, error codes,
671/// `subscribe`, roles, actions) stay closed in both directions.
672///
673/// # Errors
674/// Returns a [`ParseError`] whose `code` is `bad-version`, `malformed` or
675/// `limit-exceeded`.
676pub fn parse_driver_message(value: &Value, limits: &Limits) -> Result<(), ParseError> {
677    project(value, limits)?;
678    let (object, kind) = as_message(value)?;
679
680    match kind {
681        "hello-ack" => {
682            check_protocol_field(object)?;
683            required_keys(
684                object,
685                &[
686                    "type",
687                    "protocol",
688                    "sessionId",
689                    "limits",
690                    "subscribe",
691                    "marker",
692                ],
693            )?;
694            identifier(object, "sessionId", false)?;
695            let limits_object = object
696                .get("limits")
697                .and_then(Value::as_object)
698                .ok_or_else(|| ParseError::malformed("limits: expected an object"))?;
699            // Required keys must all be present, but unknown ones are
700            // ignored: see the note on `Limits`.
701            required_keys(limits_object, &LIMIT_FIELDS)?;
702            for field in LIMIT_FIELDS {
703                whole_number(limits_object, field, true)?;
704            }
705            match object.get("subscribe").and_then(Value::as_str) {
706                Some("snapshots") | Some("revisions") => {}
707                _ => {
708                    return Err(ParseError::malformed(
709                        "subscribe: expected 'snapshots' or 'revisions'",
710                    ))
711                }
712            }
713            let marker = object
714                .get("marker")
715                .and_then(Value::as_object)
716                .ok_or_else(|| ParseError::malformed("marker: expected an object"))?;
717            required_keys(marker, &["enabled"])?;
718            if !marker["enabled"].is_boolean() {
719                return Err(ParseError::malformed("marker.enabled: expected a boolean"));
720            }
721            if let Some(logs) = object.get("logs") {
722                check_log_budget(logs)?;
723            }
724            Ok(())
725        }
726        "error" => check_error_message(object, false),
727        _ => Err(ParseError::malformed("unknown or missing message type")),
728    }
729}