Skip to main content

devicerail_protocol/
schema.rs

1//! Deterministic JSON Schema documents for the public wire DTOs.
2//!
3//! This module is only available with the opt-in `schema` feature. Runtime
4//! protocol consumers do not need to compile the schema generator.
5
6use schemars::{JsonSchema, generate::SchemaSettings};
7use serde_json::Value;
8
9use crate::{
10    ActionCall, ActionDefinition, ActionExecution, ActionOutcome, ActionProtection, ActionResult,
11    AssetRef, ClearElementArguments, ClearElementResult, CoordinateFallbackReason,
12    DeviceCapabilitiesResult, DeviceConnectResult, DeviceDisconnectResult, DeviceExecuteParams,
13    DeviceExecuteResult, DeviceId, DeviceInfo, DeviceObserveResult, DeviceSelectParams,
14    DeviceSelectResult, DevicesListResult, ElementSelector, ElementTarget, ErrorInfo, EventId,
15    EventSequence, EventStreamCursor, EventStreamEndpoint, EventStreamEpoch,
16    EventStreamOriginPolicy, EventSubscriptionId, EventsClearResult, EventsListParams,
17    EventsListResult, EventsStreamEventNotification, EventsStreamEventParams,
18    EventsStreamOpenParams, EventsStreamOpenResult, EventsStreamTerminalNotification,
19    EventsStreamTerminalParams, EventsStreamTermination, EventsSubscribeParams,
20    EventsSubscribeResult, FeatureOffer, FeatureSelection, FindElementArguments, FindElementResult,
21    HelloParams, HelloResult, JsonRpcVersion, ManualActionArguments, ManualActionStep,
22    ManualRecording, MediaFrame, MediaStreamCaptureParams, MediaStreamCaptureResult,
23    MediaStreamEndParams, MediaStreamEndResult, MediaStreamId, MediaStreamInfo, MediaStreamKind,
24    MediaStreamStartParams, MediaStreamStartResult, Observation, PeerInfo, Platform,
25    ProtocolIncompatibilityReason, ProtocolOffer, ProtocolRange, ProtocolSelection,
26    ProtocolVersion, RecordedActionCall, RequestCancelParams, RequestCancelResult,
27    RequestCancelStatus, RequestTimeoutMs, RpcError, RpcId, RpcParams, RpcRequest, RpcResponse,
28    RpcServerMessage, RpcServerNotification, ScreenshotOmissionReason, SessionCurrentResult,
29    SessionEndParams, SessionEndResult, SessionExport, SessionExportParams, SessionExportResult,
30    SessionId, SessionInfo, SessionOutcome, SessionStartResult, SessionState, SessionTargetParams,
31    SessionsListResult, SetElementValueArguments, SetElementValueResult, SystemDescribeResult,
32    TapElementArguments, TapElementResult, TestEvent, TestEventPayload, TextMatch, TextMatchMode,
33    TransportInfo, UiContextKind, UiContextRef, UiContextSelector, UiNode, UiNodeRef, UiRect,
34    UiSnapshot, UiSnapshotGetParams, UiSnapshotGetResult, UiSnapshotOmissionReason, UiSnapshotRef,
35    Verdict, VerdictRecordParams, VerdictRecordResult, VerdictStatus, Viewport,
36    WaitForElementArguments, WaitForElementCondition, WaitForElementResult,
37};
38
39pub const SCHEMA_SET_VERSION: u16 = 1;
40pub const SCHEMA_DIRECTORY: &str = "v1";
41pub const JSON_SCHEMA_DRAFT: &str = "https://json-schema.org/draft/2020-12/schema";
42
43#[derive(Clone, Debug, PartialEq)]
44pub struct SchemaDocument {
45    pub name: &'static str,
46    pub file_name: &'static str,
47    pub schema_id: String,
48    pub schema: Value,
49}
50
51/// Generates every public protocol schema in stable file-name order.
52pub fn documents() -> Vec<SchemaDocument> {
53    vec![
54        document::<ActionCall>("ActionCall", "action-call.schema.json"),
55        document::<ActionDefinition>("ActionDefinition", "action-definition.schema.json"),
56        document::<ActionExecution>("ActionExecution", "action-execution.schema.json"),
57        document::<ActionOutcome>("ActionOutcome", "action-outcome.schema.json"),
58        document::<ActionProtection>("ActionProtection", "action-protection.schema.json"),
59        document::<ActionResult>("ActionResult", "action-result.schema.json"),
60        document::<AssetRef>("AssetRef", "asset-ref.schema.json"),
61        document::<ClearElementArguments>(
62            "ClearElementArguments",
63            "clear-element-arguments.schema.json",
64        ),
65        document::<ClearElementResult>("ClearElementResult", "clear-element-result.schema.json"),
66        document::<CoordinateFallbackReason>(
67            "CoordinateFallbackReason",
68            "coordinate-fallback-reason.schema.json",
69        ),
70        document::<DeviceCapabilitiesRequestSchema>(
71            "DeviceCapabilitiesRequest",
72            "device-capabilities-request.schema.json",
73        ),
74        document::<DeviceCapabilitiesResponseSchema>(
75            "DeviceCapabilitiesResponse",
76            "device-capabilities-response.schema.json",
77        ),
78        document::<DeviceCapabilitiesResult>(
79            "DeviceCapabilitiesResult",
80            "device-capabilities-result.schema.json",
81        ),
82        document::<DeviceConnectRequestSchema>(
83            "DeviceConnectRequest",
84            "device-connect-request.schema.json",
85        ),
86        document::<DeviceConnectResponseSchema>(
87            "DeviceConnectResponse",
88            "device-connect-response.schema.json",
89        ),
90        document::<DeviceConnectResult>("DeviceConnectResult", "device-connect-result.schema.json"),
91        document::<DeviceDisconnectRequestSchema>(
92            "DeviceDisconnectRequest",
93            "device-disconnect-request.schema.json",
94        ),
95        document::<DeviceDisconnectResponseSchema>(
96            "DeviceDisconnectResponse",
97            "device-disconnect-response.schema.json",
98        ),
99        document::<DeviceDisconnectResult>(
100            "DeviceDisconnectResult",
101            "device-disconnect-result.schema.json",
102        ),
103        document::<DeviceExecuteParams>("DeviceExecuteParams", "device-execute-params.schema.json"),
104        document::<DeviceExecuteRequestSchema>(
105            "DeviceExecuteRequest",
106            "device-execute-request.schema.json",
107        ),
108        document::<DeviceExecuteResponseSchema>(
109            "DeviceExecuteResponse",
110            "device-execute-response.schema.json",
111        ),
112        document::<DeviceExecuteResult>("DeviceExecuteResult", "device-execute-result.schema.json"),
113        document::<DeviceId>("DeviceId", "device-id.schema.json"),
114        document::<DeviceInfo>("DeviceInfo", "device-info.schema.json"),
115        document::<DeviceObserveRequestSchema>(
116            "DeviceObserveRequest",
117            "device-observe-request.schema.json",
118        ),
119        document::<DeviceObserveResponseSchema>(
120            "DeviceObserveResponse",
121            "device-observe-response.schema.json",
122        ),
123        document::<DeviceObserveResult>("DeviceObserveResult", "device-observe-result.schema.json"),
124        document::<DeviceSelectParams>("DeviceSelectParams", "device-select-params.schema.json"),
125        document::<DeviceSelectRequestSchema>(
126            "DeviceSelectRequest",
127            "device-select-request.schema.json",
128        ),
129        document::<DeviceSelectResponseSchema>(
130            "DeviceSelectResponse",
131            "device-select-response.schema.json",
132        ),
133        document::<DeviceSelectResult>("DeviceSelectResult", "device-select-result.schema.json"),
134        document::<DevicesListRequestSchema>(
135            "DevicesListRequest",
136            "devices-list-request.schema.json",
137        ),
138        document::<DevicesListResponseSchema>(
139            "DevicesListResponse",
140            "devices-list-response.schema.json",
141        ),
142        document::<DevicesListResult>("DevicesListResult", "devices-list-result.schema.json"),
143        document::<ElementSelector>("ElementSelector", "element-selector.schema.json"),
144        document::<ElementTarget>("ElementTarget", "element-target.schema.json"),
145        document::<ErrorInfo>("ErrorInfo", "error-info.schema.json"),
146        document::<EventId>("EventId", "event-id.schema.json"),
147        document::<EventSequence>("EventSequence", "event-sequence.schema.json"),
148        document::<EventStreamCursor>("EventStreamCursor", "event-stream-cursor.schema.json"),
149        document::<EventStreamEndpoint>("EventStreamEndpoint", "event-stream-endpoint.schema.json"),
150        document::<EventStreamEpoch>("EventStreamEpoch", "event-stream-epoch.schema.json"),
151        document::<EventStreamOriginPolicy>(
152            "EventStreamOriginPolicy",
153            "event-stream-origin-policy.schema.json",
154        ),
155        document::<EventSubscriptionId>("EventSubscriptionId", "event-subscription-id.schema.json"),
156        document::<EventsClearRequestSchema>(
157            "EventsClearRequest",
158            "events-clear-request.schema.json",
159        ),
160        document::<EventsClearResponseSchema>(
161            "EventsClearResponse",
162            "events-clear-response.schema.json",
163        ),
164        document::<EventsClearResult>("EventsClearResult", "events-clear-result.schema.json"),
165        document::<EventsListParams>("EventsListParams", "events-list-params.schema.json"),
166        document::<EventsListRequestSchema>("EventsListRequest", "events-list-request.schema.json"),
167        document::<EventsListResponseSchema>(
168            "EventsListResponse",
169            "events-list-response.schema.json",
170        ),
171        document::<EventsListResult>("EventsListResult", "events-list-result.schema.json"),
172        document::<EventsStreamEventNotification>(
173            "EventsStreamEventNotification",
174            "events-stream-event-notification.schema.json",
175        ),
176        document::<EventsStreamEventParams>(
177            "EventsStreamEventParams",
178            "events-stream-event-params.schema.json",
179        ),
180        document::<EventsStreamOpenParams>(
181            "EventsStreamOpenParams",
182            "events-stream-open-params.schema.json",
183        ),
184        document::<EventsStreamOpenRequestSchema>(
185            "EventsStreamOpenRequest",
186            "events-stream-open-request.schema.json",
187        ),
188        document::<EventsStreamOpenResponseSchema>(
189            "EventsStreamOpenResponse",
190            "events-stream-open-response.schema.json",
191        ),
192        document::<EventsStreamOpenResult>(
193            "EventsStreamOpenResult",
194            "events-stream-open-result.schema.json",
195        ),
196        document::<EventsStreamTerminalNotification>(
197            "EventsStreamTerminalNotification",
198            "events-stream-terminal-notification.schema.json",
199        ),
200        document::<EventsStreamTerminalParams>(
201            "EventsStreamTerminalParams",
202            "events-stream-terminal-params.schema.json",
203        ),
204        document::<EventsStreamTermination>(
205            "EventsStreamTermination",
206            "events-stream-termination.schema.json",
207        ),
208        document::<EventsSubscribeParams>(
209            "EventsSubscribeParams",
210            "events-subscribe-params.schema.json",
211        ),
212        document::<EventsSubscribeRequestSchema>(
213            "EventsSubscribeRequest",
214            "events-subscribe-request.schema.json",
215        ),
216        document::<EventsSubscribeResponseSchema>(
217            "EventsSubscribeResponse",
218            "events-subscribe-response.schema.json",
219        ),
220        document::<EventsSubscribeResult>(
221            "EventsSubscribeResult",
222            "events-subscribe-result.schema.json",
223        ),
224        document::<FeatureOffer>("FeatureOffer", "feature-offer.schema.json"),
225        document::<FeatureSelection>("FeatureSelection", "feature-selection.schema.json"),
226        document::<FindElementArguments>(
227            "FindElementArguments",
228            "find-element-arguments.schema.json",
229        ),
230        document::<FindElementResult>("FindElementResult", "find-element-result.schema.json"),
231        document::<HelloParams>("HelloParams", "hello-params.schema.json"),
232        document::<HelloResult>("HelloResult", "hello-result.schema.json"),
233        document::<JsonRpcVersion>("JsonRpcVersion", "json-rpc-version.schema.json"),
234        document::<ManualActionArguments>(
235            "ManualActionArguments",
236            "manual-action-arguments.schema.json",
237        ),
238        document::<ManualActionStep>("ManualActionStep", "manual-action-step.schema.json"),
239        document::<ManualRecording>("ManualRecording", "manual-recording.schema.json"),
240        document::<MediaFrame>("MediaFrame", "media-frame.schema.json"),
241        document::<MediaStreamCaptureParams>(
242            "MediaStreamCaptureParams",
243            "media-stream-capture-params.schema.json",
244        ),
245        document::<MediaStreamCaptureRequestSchema>(
246            "MediaStreamCaptureRequest",
247            "media-stream-capture-request.schema.json",
248        ),
249        document::<MediaStreamCaptureResponseSchema>(
250            "MediaStreamCaptureResponse",
251            "media-stream-capture-response.schema.json",
252        ),
253        document::<MediaStreamCaptureResult>(
254            "MediaStreamCaptureResult",
255            "media-stream-capture-result.schema.json",
256        ),
257        document::<MediaStreamEndParams>(
258            "MediaStreamEndParams",
259            "media-stream-end-params.schema.json",
260        ),
261        document::<MediaStreamEndRequestSchema>(
262            "MediaStreamEndRequest",
263            "media-stream-end-request.schema.json",
264        ),
265        document::<MediaStreamEndResponseSchema>(
266            "MediaStreamEndResponse",
267            "media-stream-end-response.schema.json",
268        ),
269        document::<MediaStreamEndResult>(
270            "MediaStreamEndResult",
271            "media-stream-end-result.schema.json",
272        ),
273        document::<MediaStreamId>("MediaStreamId", "media-stream-id.schema.json"),
274        document::<MediaStreamInfo>("MediaStreamInfo", "media-stream-info.schema.json"),
275        document::<MediaStreamKind>("MediaStreamKind", "media-stream-kind.schema.json"),
276        document::<MediaStreamStartParams>(
277            "MediaStreamStartParams",
278            "media-stream-start-params.schema.json",
279        ),
280        document::<MediaStreamStartRequestSchema>(
281            "MediaStreamStartRequest",
282            "media-stream-start-request.schema.json",
283        ),
284        document::<MediaStreamStartResponseSchema>(
285            "MediaStreamStartResponse",
286            "media-stream-start-response.schema.json",
287        ),
288        document::<MediaStreamStartResult>(
289            "MediaStreamStartResult",
290            "media-stream-start-result.schema.json",
291        ),
292        document::<Observation>("Observation", "observation.schema.json"),
293        document::<PeerInfo>("PeerInfo", "peer-info.schema.json"),
294        document::<Platform>("Platform", "platform.schema.json"),
295        document::<ProtocolIncompatibilityReason>(
296            "ProtocolIncompatibilityReason",
297            "protocol-incompatibility-reason.schema.json",
298        ),
299        document::<ProtocolOffer>("ProtocolOffer", "protocol-offer.schema.json"),
300        document::<ProtocolRange>("ProtocolRange", "protocol-range.schema.json"),
301        document::<ProtocolSelection>("ProtocolSelection", "protocol-selection.schema.json"),
302        document::<ProtocolVersion>("ProtocolVersion", "protocol-version.schema.json"),
303        document::<RecordedActionCall>("RecordedActionCall", "recorded-action-call.schema.json"),
304        document::<RequestCancelParams>("RequestCancelParams", "request-cancel-params.schema.json"),
305        document::<RequestCancelRequestSchema>(
306            "RequestCancelRequest",
307            "request-cancel-request.schema.json",
308        ),
309        document::<RequestCancelResponseSchema>(
310            "RequestCancelResponse",
311            "request-cancel-response.schema.json",
312        ),
313        document::<RequestCancelResult>("RequestCancelResult", "request-cancel-result.schema.json"),
314        document::<RequestCancelStatus>("RequestCancelStatus", "request-cancel-status.schema.json"),
315        document::<RequestTimeoutMs>("RequestTimeoutMs", "request-timeout-ms.schema.json"),
316        document::<RpcError>("RpcError", "rpc-error.schema.json"),
317        document::<RpcId>("RpcId", "rpc-id.schema.json"),
318        document::<RpcParams>("RpcParams", "rpc-params.schema.json"),
319        document::<RpcRequest>("RpcRequest", "rpc-request.schema.json"),
320        document::<RpcResponse>("RpcResponse", "rpc-response.schema.json"),
321        document::<RpcServerMessage>("RpcServerMessage", "rpc-server-message.schema.json"),
322        document::<RpcServerNotification>(
323            "RpcServerNotification",
324            "rpc-server-notification.schema.json",
325        ),
326        document::<ScreenshotOmissionReason>(
327            "ScreenshotOmissionReason",
328            "screenshot-omission-reason.schema.json",
329        ),
330        document::<SessionCurrentRequestSchema>(
331            "SessionCurrentRequest",
332            "session-current-request.schema.json",
333        ),
334        document::<SessionCurrentResponseSchema>(
335            "SessionCurrentResponse",
336            "session-current-response.schema.json",
337        ),
338        document::<SessionCurrentResult>(
339            "SessionCurrentResult",
340            "session-current-result.schema.json",
341        ),
342        document::<SessionEndParams>("SessionEndParams", "session-end-params.schema.json"),
343        document::<SessionEndRequestSchema>("SessionEndRequest", "session-end-request.schema.json"),
344        document::<SessionEndResponseSchema>(
345            "SessionEndResponse",
346            "session-end-response.schema.json",
347        ),
348        document::<SessionEndResult>("SessionEndResult", "session-end-result.schema.json"),
349        document::<SessionExportParams>("SessionExportParams", "session-export-params.schema.json"),
350        document::<SessionExportRequestSchema>(
351            "SessionExportRequest",
352            "session-export-request.schema.json",
353        ),
354        document::<SessionExportResponseSchema>(
355            "SessionExportResponse",
356            "session-export-response.schema.json",
357        ),
358        document::<SessionExportResult>("SessionExportResult", "session-export-result.schema.json"),
359        document::<SessionExport>("SessionExport", "session-export.schema.json"),
360        document::<SessionId>("SessionId", "session-id.schema.json"),
361        document::<SessionInfo>("SessionInfo", "session-info.schema.json"),
362        document::<SessionOutcome>("SessionOutcome", "session-outcome.schema.json"),
363        document::<SessionStartRequestSchema>(
364            "SessionStartRequest",
365            "session-start-request.schema.json",
366        ),
367        document::<SessionStartResponseSchema>(
368            "SessionStartResponse",
369            "session-start-response.schema.json",
370        ),
371        document::<SessionStartResult>("SessionStartResult", "session-start-result.schema.json"),
372        document::<SessionState>("SessionState", "session-state.schema.json"),
373        document::<SessionTargetParams>("SessionTargetParams", "session-target-params.schema.json"),
374        document::<SessionsListRequestSchema>(
375            "SessionsListRequest",
376            "sessions-list-request.schema.json",
377        ),
378        document::<SessionsListResponseSchema>(
379            "SessionsListResponse",
380            "sessions-list-response.schema.json",
381        ),
382        document::<SessionsListResult>("SessionsListResult", "sessions-list-result.schema.json"),
383        document::<SetElementValueArguments>(
384            "SetElementValueArguments",
385            "set-element-value-arguments.schema.json",
386        ),
387        document::<SetElementValueResult>(
388            "SetElementValueResult",
389            "set-element-value-result.schema.json",
390        ),
391        document::<SystemDescribeRequestSchema>(
392            "SystemDescribeRequest",
393            "system-describe-request.schema.json",
394        ),
395        document::<SystemDescribeResponseSchema>(
396            "SystemDescribeResponse",
397            "system-describe-response.schema.json",
398        ),
399        document::<SystemDescribeResult>(
400            "SystemDescribeResult",
401            "system-describe-result.schema.json",
402        ),
403        document::<SystemHelloRequestSchema>(
404            "SystemHelloRequest",
405            "system-hello-request.schema.json",
406        ),
407        document::<SystemHelloResponseSchema>(
408            "SystemHelloResponse",
409            "system-hello-response.schema.json",
410        ),
411        document::<TapElementArguments>("TapElementArguments", "tap-element-arguments.schema.json"),
412        document::<TapElementResult>("TapElementResult", "tap-element-result.schema.json"),
413        document::<TestEventPayload>("TestEventPayload", "test-event-payload.schema.json"),
414        document::<TestEvent>("TestEvent", "test-event.schema.json"),
415        document::<TextMatchMode>("TextMatchMode", "text-match-mode.schema.json"),
416        document::<TextMatch>("TextMatch", "text-match.schema.json"),
417        document::<TransportInfo>("TransportInfo", "transport-info.schema.json"),
418        document::<UiContextKind>("UiContextKind", "ui-context-kind.schema.json"),
419        document::<UiContextRef>("UiContextRef", "ui-context-ref.schema.json"),
420        document::<UiContextSelector>("UiContextSelector", "ui-context-selector.schema.json"),
421        document::<UiNodeRef>("UiNodeRef", "ui-node-ref.schema.json"),
422        document::<UiNode>("UiNode", "ui-node.schema.json"),
423        document::<UiRect>("UiRect", "ui-rect.schema.json"),
424        document::<UiSnapshotGetParams>(
425            "UiSnapshotGetParams",
426            "ui-snapshot-get-params.schema.json",
427        ),
428        document::<UiSnapshotGetRequestSchema>(
429            "UiSnapshotGetRequest",
430            "ui-snapshot-get-request.schema.json",
431        ),
432        document::<UiSnapshotGetResponseSchema>(
433            "UiSnapshotGetResponse",
434            "ui-snapshot-get-response.schema.json",
435        ),
436        document::<UiSnapshotGetResult>(
437            "UiSnapshotGetResult",
438            "ui-snapshot-get-result.schema.json",
439        ),
440        document::<UiSnapshotOmissionReason>(
441            "UiSnapshotOmissionReason",
442            "ui-snapshot-omission-reason.schema.json",
443        ),
444        document::<UiSnapshotRef>("UiSnapshotRef", "ui-snapshot-ref.schema.json"),
445        document::<UiSnapshot>("UiSnapshot", "ui-snapshot.schema.json"),
446        document::<VerdictRecordParams>("VerdictRecordParams", "verdict-record-params.schema.json"),
447        document::<VerdictRecordRequestSchema>(
448            "VerdictRecordRequest",
449            "verdict-record-request.schema.json",
450        ),
451        document::<VerdictRecordResponseSchema>(
452            "VerdictRecordResponse",
453            "verdict-record-response.schema.json",
454        ),
455        document::<VerdictRecordResult>("VerdictRecordResult", "verdict-record-result.schema.json"),
456        document::<VerdictStatus>("VerdictStatus", "verdict-status.schema.json"),
457        document::<Verdict>("Verdict", "verdict.schema.json"),
458        document::<Viewport>("Viewport", "viewport.schema.json"),
459        document::<WaitForElementArguments>(
460            "WaitForElementArguments",
461            "wait-for-element-arguments.schema.json",
462        ),
463        document::<WaitForElementCondition>(
464            "WaitForElementCondition",
465            "wait-for-element-condition.schema.json",
466        ),
467        document::<WaitForElementResult>(
468            "WaitForElementResult",
469            "wait-for-element-result.schema.json",
470        ),
471    ]
472}
473
474fn document<T: JsonSchema>(name: &'static str, file_name: &'static str) -> SchemaDocument {
475    let schema_id = format!(
476        "urn:devicerail:schema:protocol:v{SCHEMA_SET_VERSION}:{}",
477        file_name.trim_end_matches(".schema.json")
478    );
479    let root = SchemaSettings::draft2020_12()
480        .for_deserialize()
481        .into_generator()
482        .into_root_schema_for::<T>();
483    let mut schema = serde_json::to_value(root).expect("schemars schema serializes to JSON");
484    let root = schema
485        .as_object_mut()
486        .expect("schemars root schema is an object");
487    root.insert("$id".to_owned(), Value::String(schema_id.clone()));
488    root.insert("title".to_owned(), Value::String(name.to_owned()));
489
490    SchemaDocument {
491        name,
492        file_name,
493        schema_id,
494        schema,
495    }
496}
497
498#[allow(dead_code)]
499#[derive(JsonSchema)]
500#[schemars(rename = "SystemHelloRequest")]
501#[serde(rename_all = "camelCase", deny_unknown_fields)]
502struct SystemHelloRequestSchema {
503    jsonrpc: JsonRpcVersion,
504    id: RpcId,
505    method: SystemHelloMethodSchema,
506    params: HelloParams,
507}
508
509#[allow(dead_code)]
510#[derive(JsonSchema)]
511enum SystemHelloMethodSchema {
512    #[serde(rename = "system.hello")]
513    SystemHello,
514}
515
516macro_rules! no_params_request_schema {
517    ($request:ident, $method_type:ident, $method_variant:ident, $title:literal, $method:literal) => {
518        #[allow(dead_code)]
519        #[derive(JsonSchema)]
520        #[schemars(rename = $title)]
521        #[serde(rename_all = "camelCase", deny_unknown_fields)]
522        struct $request {
523            jsonrpc: JsonRpcVersion,
524            id: RpcId,
525            method: $method_type,
526            #[serde(default, skip_serializing_if = "Option::is_none")]
527            #[schemars(with = "NoParamsSchema")]
528            params: Option<NoParamsSchema>,
529        }
530
531        #[allow(dead_code)]
532        #[derive(JsonSchema)]
533        enum $method_type {
534            #[serde(rename = $method)]
535            $method_variant,
536        }
537    };
538}
539
540macro_rules! timeout_no_params_request_schema {
541    ($request:ident, $method_type:ident, $method_variant:ident, $title:literal, $method:literal) => {
542        #[allow(dead_code)]
543        #[derive(JsonSchema)]
544        #[schemars(rename = $title)]
545        #[serde(rename_all = "camelCase", deny_unknown_fields)]
546        struct $request {
547            jsonrpc: JsonRpcVersion,
548            id: RpcId,
549            method: $method_type,
550            #[serde(default, skip_serializing_if = "Option::is_none")]
551            #[schemars(with = "RequestTimeoutMs")]
552            timeout_ms: Option<RequestTimeoutMs>,
553            #[serde(default, skip_serializing_if = "Option::is_none")]
554            #[schemars(with = "NoParamsSchema")]
555            params: Option<NoParamsSchema>,
556        }
557
558        #[allow(dead_code)]
559        #[derive(JsonSchema)]
560        enum $method_type {
561            #[serde(rename = $method)]
562            $method_variant,
563        }
564    };
565}
566
567macro_rules! response_schema {
568    ($response:ident, $success:ident, $title:literal, $result:ty) => {
569        #[allow(dead_code)]
570        #[derive(JsonSchema)]
571        #[schemars(rename = $title)]
572        #[serde(untagged)]
573        enum $response {
574            Success(Box<$success>),
575            Failure(SystemHelloFailureSchema),
576        }
577
578        #[allow(dead_code)]
579        #[derive(JsonSchema)]
580        #[serde(rename_all = "camelCase", deny_unknown_fields)]
581        struct $success {
582            jsonrpc: JsonRpcVersion,
583            id: RpcId,
584            result: $result,
585        }
586    };
587}
588
589no_params_request_schema!(
590    SystemDescribeRequestSchema,
591    SystemDescribeMethodSchema,
592    SystemDescribe,
593    "SystemDescribeRequest",
594    "system.describe"
595);
596timeout_no_params_request_schema!(
597    DeviceConnectRequestSchema,
598    DeviceConnectMethodSchema,
599    DeviceConnect,
600    "DeviceConnectRequest",
601    "device.connect"
602);
603timeout_no_params_request_schema!(
604    DeviceDisconnectRequestSchema,
605    DeviceDisconnectMethodSchema,
606    DeviceDisconnect,
607    "DeviceDisconnectRequest",
608    "device.disconnect"
609);
610timeout_no_params_request_schema!(
611    DeviceCapabilitiesRequestSchema,
612    DeviceCapabilitiesMethodSchema,
613    DeviceCapabilities,
614    "DeviceCapabilitiesRequest",
615    "device.capabilities"
616);
617timeout_no_params_request_schema!(
618    DeviceObserveRequestSchema,
619    DeviceObserveMethodSchema,
620    DeviceObserve,
621    "DeviceObserveRequest",
622    "device.observe"
623);
624no_params_request_schema!(
625    SessionStartRequestSchema,
626    SessionStartMethodSchema,
627    SessionStart,
628    "SessionStartRequest",
629    "session.start"
630);
631no_params_request_schema!(
632    SessionCurrentRequestSchema,
633    SessionCurrentMethodSchema,
634    SessionCurrent,
635    "SessionCurrentRequest",
636    "session.current"
637);
638no_params_request_schema!(
639    SessionsListRequestSchema,
640    SessionsListMethodSchema,
641    SessionsList,
642    "SessionsListRequest",
643    "sessions.list"
644);
645
646response_schema!(
647    SystemDescribeResponseSchema,
648    SystemDescribeSuccessSchema,
649    "SystemDescribeResponse",
650    SystemDescribeResult
651);
652response_schema!(
653    DeviceConnectResponseSchema,
654    DeviceConnectSuccessSchema,
655    "DeviceConnectResponse",
656    DeviceConnectResult
657);
658response_schema!(
659    DeviceDisconnectResponseSchema,
660    DeviceDisconnectSuccessSchema,
661    "DeviceDisconnectResponse",
662    DeviceDisconnectResult
663);
664response_schema!(
665    DeviceCapabilitiesResponseSchema,
666    DeviceCapabilitiesSuccessSchema,
667    "DeviceCapabilitiesResponse",
668    DeviceCapabilitiesResult
669);
670response_schema!(
671    DeviceObserveResponseSchema,
672    DeviceObserveSuccessSchema,
673    "DeviceObserveResponse",
674    DeviceObserveResult
675);
676response_schema!(
677    DeviceExecuteResponseSchema,
678    DeviceExecuteSuccessSchema,
679    "DeviceExecuteResponse",
680    DeviceExecuteResult
681);
682response_schema!(
683    UiSnapshotGetResponseSchema,
684    UiSnapshotGetSuccessSchema,
685    "UiSnapshotGetResponse",
686    UiSnapshotGetResult
687);
688response_schema!(
689    VerdictRecordResponseSchema,
690    VerdictRecordSuccessSchema,
691    "VerdictRecordResponse",
692    VerdictRecordResult
693);
694response_schema!(
695    SessionStartResponseSchema,
696    SessionStartSuccessSchema,
697    "SessionStartResponse",
698    SessionStartResult
699);
700response_schema!(
701    SessionCurrentResponseSchema,
702    SessionCurrentSuccessSchema,
703    "SessionCurrentResponse",
704    SessionCurrentResult
705);
706response_schema!(
707    SessionEndResponseSchema,
708    SessionEndSuccessSchema,
709    "SessionEndResponse",
710    SessionEndResult
711);
712response_schema!(
713    SessionsListResponseSchema,
714    SessionsListSuccessSchema,
715    "SessionsListResponse",
716    SessionsListResult
717);
718response_schema!(
719    SessionExportResponseSchema,
720    SessionExportSuccessSchema,
721    "SessionExportResponse",
722    SessionExportResult
723);
724response_schema!(
725    EventsListResponseSchema,
726    EventsListSuccessSchema,
727    "EventsListResponse",
728    EventsListResult
729);
730response_schema!(
731    EventsStreamOpenResponseSchema,
732    EventsStreamOpenSuccessSchema,
733    "EventsStreamOpenResponse",
734    EventsStreamOpenResult
735);
736response_schema!(
737    EventsSubscribeResponseSchema,
738    EventsSubscribeSuccessSchema,
739    "EventsSubscribeResponse",
740    EventsSubscribeResult
741);
742response_schema!(
743    EventsClearResponseSchema,
744    EventsClearSuccessSchema,
745    "EventsClearResponse",
746    EventsClearResult
747);
748response_schema!(
749    MediaStreamCaptureResponseSchema,
750    MediaStreamCaptureSuccessSchema,
751    "MediaStreamCaptureResponse",
752    MediaStreamCaptureResult
753);
754response_schema!(
755    MediaStreamEndResponseSchema,
756    MediaStreamEndSuccessSchema,
757    "MediaStreamEndResponse",
758    MediaStreamEndResult
759);
760response_schema!(
761    MediaStreamStartResponseSchema,
762    MediaStreamStartSuccessSchema,
763    "MediaStreamStartResponse",
764    MediaStreamStartResult
765);
766
767#[allow(dead_code)]
768#[derive(JsonSchema)]
769#[schemars(rename = "MediaStreamCaptureRequest")]
770#[serde(rename_all = "camelCase", deny_unknown_fields)]
771struct MediaStreamCaptureRequestSchema {
772    jsonrpc: JsonRpcVersion,
773    id: RpcId,
774    method: MediaStreamCaptureMethodSchema,
775    #[serde(default, skip_serializing_if = "Option::is_none")]
776    #[schemars(with = "RequestTimeoutMs")]
777    timeout_ms: Option<RequestTimeoutMs>,
778    params: MediaStreamCaptureParams,
779}
780
781#[allow(dead_code)]
782#[derive(JsonSchema)]
783enum MediaStreamCaptureMethodSchema {
784    #[serde(rename = "media.stream.capture")]
785    MediaStreamCapture,
786}
787
788#[allow(dead_code)]
789#[derive(JsonSchema)]
790#[schemars(rename = "MediaStreamEndRequest")]
791#[serde(rename_all = "camelCase", deny_unknown_fields)]
792struct MediaStreamEndRequestSchema {
793    jsonrpc: JsonRpcVersion,
794    id: RpcId,
795    method: MediaStreamEndMethodSchema,
796    params: MediaStreamEndParams,
797}
798
799#[allow(dead_code)]
800#[derive(JsonSchema)]
801enum MediaStreamEndMethodSchema {
802    #[serde(rename = "media.stream.end")]
803    MediaStreamEnd,
804}
805
806#[allow(dead_code)]
807#[derive(JsonSchema)]
808#[schemars(rename = "MediaStreamStartRequest")]
809#[serde(rename_all = "camelCase", deny_unknown_fields)]
810struct MediaStreamStartRequestSchema {
811    jsonrpc: JsonRpcVersion,
812    id: RpcId,
813    method: MediaStreamStartMethodSchema,
814    params: MediaStreamStartParams,
815}
816
817#[allow(dead_code)]
818#[derive(JsonSchema)]
819enum MediaStreamStartMethodSchema {
820    #[serde(rename = "media.stream.start")]
821    MediaStreamStart,
822}
823
824#[allow(dead_code)]
825#[derive(JsonSchema)]
826#[schemars(rename = "EventsStreamOpenRequest")]
827#[serde(rename_all = "camelCase", deny_unknown_fields)]
828struct EventsStreamOpenRequestSchema {
829    jsonrpc: JsonRpcVersion,
830    id: RpcId,
831    method: EventsStreamOpenMethodSchema,
832    params: EventsStreamOpenParams,
833}
834
835#[allow(dead_code)]
836#[derive(JsonSchema)]
837enum EventsStreamOpenMethodSchema {
838    #[serde(rename = "events.stream.open")]
839    EventsStreamOpen,
840}
841
842#[allow(dead_code)]
843#[derive(JsonSchema)]
844#[schemars(rename = "EventsSubscribeRequest")]
845#[serde(rename_all = "camelCase", deny_unknown_fields)]
846struct EventsSubscribeRequestSchema {
847    jsonrpc: JsonRpcVersion,
848    id: RpcId,
849    method: EventsSubscribeMethodSchema,
850    params: EventsSubscribeParams,
851}
852
853#[allow(dead_code)]
854#[derive(JsonSchema)]
855enum EventsSubscribeMethodSchema {
856    #[serde(rename = "events.subscribe")]
857    EventsSubscribe,
858}
859
860#[allow(dead_code)]
861#[derive(JsonSchema)]
862#[schemars(rename = "DeviceExecuteRequest")]
863#[serde(rename_all = "camelCase", deny_unknown_fields)]
864struct DeviceExecuteRequestSchema {
865    jsonrpc: JsonRpcVersion,
866    id: RpcId,
867    method: DeviceExecuteMethodSchema,
868    #[serde(default, skip_serializing_if = "Option::is_none")]
869    #[schemars(with = "RequestTimeoutMs")]
870    timeout_ms: Option<RequestTimeoutMs>,
871    params: DeviceExecuteParams,
872}
873
874#[allow(dead_code)]
875#[derive(JsonSchema)]
876enum DeviceExecuteMethodSchema {
877    #[serde(rename = "device.execute")]
878    DeviceExecute,
879}
880
881#[allow(dead_code)]
882#[derive(JsonSchema)]
883#[schemars(rename = "UiSnapshotGetRequest")]
884#[serde(rename_all = "camelCase", deny_unknown_fields)]
885struct UiSnapshotGetRequestSchema {
886    jsonrpc: JsonRpcVersion,
887    id: RpcId,
888    method: UiSnapshotGetMethodSchema,
889    params: UiSnapshotGetParams,
890}
891
892#[allow(dead_code)]
893#[derive(JsonSchema)]
894enum UiSnapshotGetMethodSchema {
895    #[serde(rename = "ui.snapshot.get")]
896    UiSnapshotGet,
897}
898
899#[allow(dead_code)]
900#[derive(JsonSchema)]
901#[schemars(rename = "VerdictRecordRequest")]
902#[serde(rename_all = "camelCase", deny_unknown_fields)]
903struct VerdictRecordRequestSchema {
904    jsonrpc: JsonRpcVersion,
905    id: RpcId,
906    method: VerdictRecordMethodSchema,
907    params: VerdictRecordParams,
908}
909
910#[allow(dead_code)]
911#[derive(JsonSchema)]
912enum VerdictRecordMethodSchema {
913    #[serde(rename = "verdict.record")]
914    VerdictRecord,
915}
916
917#[allow(dead_code)]
918#[derive(JsonSchema)]
919#[schemars(rename = "SessionEndRequest")]
920#[serde(rename_all = "camelCase", deny_unknown_fields)]
921struct SessionEndRequestSchema {
922    jsonrpc: JsonRpcVersion,
923    id: RpcId,
924    method: SessionEndMethodSchema,
925    #[serde(default, skip_serializing_if = "Option::is_none")]
926    #[schemars(with = "SessionEndParams")]
927    params: Option<SessionEndParams>,
928}
929
930#[allow(dead_code)]
931#[derive(JsonSchema)]
932enum SessionEndMethodSchema {
933    #[serde(rename = "session.end")]
934    SessionEnd,
935}
936
937#[allow(dead_code)]
938#[derive(JsonSchema)]
939#[schemars(rename = "SessionExportRequest")]
940#[serde(rename_all = "camelCase", deny_unknown_fields)]
941struct SessionExportRequestSchema {
942    jsonrpc: JsonRpcVersion,
943    id: RpcId,
944    method: SessionExportMethodSchema,
945    #[serde(default, skip_serializing_if = "Option::is_none")]
946    #[schemars(with = "SessionExportParams")]
947    params: Option<SessionExportParams>,
948}
949
950#[allow(dead_code)]
951#[derive(JsonSchema)]
952enum SessionExportMethodSchema {
953    #[serde(rename = "session.export")]
954    SessionExport,
955}
956
957#[allow(dead_code)]
958#[derive(JsonSchema)]
959#[schemars(rename = "EventsListRequest")]
960#[serde(rename_all = "camelCase", deny_unknown_fields)]
961struct EventsListRequestSchema {
962    jsonrpc: JsonRpcVersion,
963    id: RpcId,
964    method: EventsListMethodSchema,
965    #[serde(default, skip_serializing_if = "Option::is_none")]
966    #[schemars(with = "EventsListParams")]
967    params: Option<EventsListParams>,
968}
969
970#[allow(dead_code)]
971#[derive(JsonSchema)]
972enum EventsListMethodSchema {
973    #[serde(rename = "events.list")]
974    EventsList,
975}
976
977#[allow(dead_code)]
978#[derive(JsonSchema)]
979#[schemars(rename = "EventsClearRequest")]
980#[serde(rename_all = "camelCase", deny_unknown_fields)]
981struct EventsClearRequestSchema {
982    jsonrpc: JsonRpcVersion,
983    id: RpcId,
984    method: EventsClearMethodSchema,
985    #[serde(default, skip_serializing_if = "Option::is_none")]
986    #[schemars(with = "SessionTargetParams")]
987    params: Option<SessionTargetParams>,
988}
989
990#[allow(dead_code)]
991#[derive(JsonSchema)]
992enum EventsClearMethodSchema {
993    #[serde(rename = "events.clear")]
994    EventsClear,
995}
996
997#[allow(dead_code)]
998#[derive(JsonSchema)]
999#[schemars(rename = "DeviceSelectRequest")]
1000#[serde(rename_all = "camelCase", deny_unknown_fields)]
1001struct DeviceSelectRequestSchema {
1002    jsonrpc: JsonRpcVersion,
1003    id: RpcId,
1004    method: DeviceSelectMethodSchema,
1005    params: DeviceSelectParams,
1006}
1007
1008#[allow(dead_code)]
1009#[derive(JsonSchema)]
1010enum DeviceSelectMethodSchema {
1011    #[serde(rename = "device.select")]
1012    DeviceSelect,
1013}
1014
1015#[allow(dead_code)]
1016#[derive(JsonSchema)]
1017#[schemars(rename = "DeviceSelectResponse")]
1018#[serde(untagged)]
1019enum DeviceSelectResponseSchema {
1020    Success(DeviceSelectSuccessSchema),
1021    Failure(SystemHelloFailureSchema),
1022}
1023
1024#[allow(dead_code)]
1025#[derive(JsonSchema)]
1026#[serde(rename_all = "camelCase", deny_unknown_fields)]
1027struct DeviceSelectSuccessSchema {
1028    jsonrpc: JsonRpcVersion,
1029    id: RpcId,
1030    result: DeviceSelectResult,
1031}
1032
1033#[allow(dead_code)]
1034#[derive(JsonSchema)]
1035#[schemars(rename = "DevicesListRequest")]
1036#[serde(rename_all = "camelCase", deny_unknown_fields)]
1037struct DevicesListRequestSchema {
1038    jsonrpc: JsonRpcVersion,
1039    id: RpcId,
1040    method: DevicesListMethodSchema,
1041    #[serde(default, skip_serializing_if = "Option::is_none")]
1042    #[schemars(with = "NoParamsSchema")]
1043    params: Option<NoParamsSchema>,
1044}
1045
1046#[allow(dead_code)]
1047#[derive(JsonSchema)]
1048enum DevicesListMethodSchema {
1049    #[serde(rename = "devices.list")]
1050    DevicesList,
1051}
1052
1053#[allow(dead_code)]
1054#[derive(JsonSchema)]
1055#[serde(untagged)]
1056enum NoParamsSchema {
1057    Object(EmptyParamsObjectSchema),
1058    Array([Value; 0]),
1059}
1060
1061#[allow(dead_code)]
1062#[derive(JsonSchema)]
1063#[serde(deny_unknown_fields)]
1064struct EmptyParamsObjectSchema {}
1065
1066#[allow(dead_code)]
1067#[derive(JsonSchema)]
1068#[schemars(rename = "DevicesListResponse")]
1069#[serde(untagged)]
1070enum DevicesListResponseSchema {
1071    Success(DevicesListSuccessSchema),
1072    Failure(SystemHelloFailureSchema),
1073}
1074
1075#[allow(dead_code)]
1076#[derive(JsonSchema)]
1077#[serde(rename_all = "camelCase", deny_unknown_fields)]
1078struct DevicesListSuccessSchema {
1079    jsonrpc: JsonRpcVersion,
1080    id: RpcId,
1081    result: DevicesListResult,
1082}
1083
1084#[allow(dead_code)]
1085#[derive(JsonSchema)]
1086#[schemars(rename = "RequestCancelRequest")]
1087#[serde(rename_all = "camelCase", deny_unknown_fields)]
1088struct RequestCancelRequestSchema {
1089    jsonrpc: JsonRpcVersion,
1090    id: RpcId,
1091    method: RequestCancelMethodSchema,
1092    params: RequestCancelParams,
1093}
1094
1095#[allow(dead_code)]
1096#[derive(JsonSchema)]
1097enum RequestCancelMethodSchema {
1098    #[serde(rename = "request.cancel")]
1099    RequestCancel,
1100}
1101
1102#[allow(dead_code)]
1103#[derive(JsonSchema)]
1104#[schemars(rename = "RequestCancelResponse")]
1105#[serde(untagged)]
1106enum RequestCancelResponseSchema {
1107    Success(RequestCancelSuccessSchema),
1108    Failure(SystemHelloFailureSchema),
1109}
1110
1111#[allow(dead_code)]
1112#[derive(JsonSchema)]
1113#[serde(rename_all = "camelCase", deny_unknown_fields)]
1114struct RequestCancelSuccessSchema {
1115    jsonrpc: JsonRpcVersion,
1116    id: RpcId,
1117    result: RequestCancelResult,
1118}
1119
1120#[allow(dead_code)]
1121#[derive(JsonSchema)]
1122#[schemars(rename = "SystemHelloResponse")]
1123#[serde(untagged)]
1124enum SystemHelloResponseSchema {
1125    Success(SystemHelloSuccessSchema),
1126    Failure(SystemHelloFailureSchema),
1127}
1128
1129#[allow(dead_code)]
1130#[derive(JsonSchema)]
1131#[serde(rename_all = "camelCase", deny_unknown_fields)]
1132struct SystemHelloSuccessSchema {
1133    jsonrpc: JsonRpcVersion,
1134    id: RpcId,
1135    result: HelloResult,
1136}
1137
1138#[allow(dead_code)]
1139#[derive(JsonSchema)]
1140#[serde(rename_all = "camelCase", deny_unknown_fields)]
1141struct SystemHelloFailureSchema {
1142    jsonrpc: JsonRpcVersion,
1143    id: NullableRpcIdSchema,
1144    error: RpcError,
1145}
1146
1147#[allow(dead_code)]
1148#[derive(JsonSchema)]
1149#[serde(untagged)]
1150enum NullableRpcIdSchema {
1151    Id(RpcId),
1152    Null(()),
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157    use super::{JSON_SCHEMA_DRAFT, documents};
1158
1159    const PUBLIC_SCHEMA_DOCUMENT_COUNT: usize = 174;
1160
1161    const PUBLIC_METHODS: [&str; 24] = [
1162        "device.capabilities",
1163        "device.connect",
1164        "device.disconnect",
1165        "device.execute",
1166        "device.observe",
1167        "device.select",
1168        "devices.list",
1169        "events.clear",
1170        "events.list",
1171        "events.stream.open",
1172        "events.subscribe",
1173        "media.stream.capture",
1174        "media.stream.end",
1175        "media.stream.start",
1176        "request.cancel",
1177        "session.current",
1178        "session.end",
1179        "session.export",
1180        "session.start",
1181        "sessions.list",
1182        "system.describe",
1183        "system.hello",
1184        "ui.snapshot.get",
1185        "verdict.record",
1186    ];
1187
1188    #[test]
1189    fn documents_have_unique_stable_names_and_ids() {
1190        let documents = documents();
1191        assert_eq!(documents.len(), PUBLIC_SCHEMA_DOCUMENT_COUNT);
1192        let mut names = documents
1193            .iter()
1194            .map(|document| document.file_name)
1195            .collect::<Vec<_>>();
1196        let original = names.clone();
1197        names.sort_unstable();
1198        names.dedup();
1199
1200        assert_eq!(names.len(), documents.len());
1201        assert_eq!(original, names, "documents must stay sorted by file name");
1202        for document in documents {
1203            assert_eq!(document.schema["$schema"], JSON_SCHEMA_DRAFT);
1204            assert_eq!(document.schema["$id"], document.schema_id);
1205            assert_eq!(document.schema["title"], document.name);
1206        }
1207    }
1208
1209    #[test]
1210    fn protected_action_types_have_independent_root_documents() {
1211        let documents = documents();
1212        for name in [
1213            "ActionProtection",
1214            "RecordedActionCall",
1215            "ScreenshotOmissionReason",
1216        ] {
1217            assert!(
1218                documents.iter().any(|document| document.name == name),
1219                "missing root schema for {name}"
1220            );
1221        }
1222    }
1223
1224    #[test]
1225    fn action_input_schema_remains_an_unconstrained_json_value() {
1226        let document = documents()
1227            .into_iter()
1228            .find(|document| document.name == "ActionDefinition")
1229            .expect("action definition schema exists");
1230
1231        assert_eq!(document.schema["properties"]["inputSchema"], true);
1232    }
1233
1234    #[test]
1235    fn every_public_method_has_request_and_response_schemas() {
1236        let documents = documents();
1237        for method in PUBLIC_METHODS {
1238            let stem = method.replace('.', "-");
1239            let request_file = format!("{stem}-request.schema.json");
1240            let response_file = format!("{stem}-response.schema.json");
1241
1242            let request = documents
1243                .iter()
1244                .find(|document| document.file_name == request_file)
1245                .unwrap_or_else(|| panic!("missing request schema for {method}"));
1246            assert!(
1247                request.schema.to_string().contains(method),
1248                "request schema for {method} must lock its method literal"
1249            );
1250            assert!(
1251                documents
1252                    .iter()
1253                    .any(|document| document.file_name == response_file),
1254                "missing response schema for {method}"
1255            );
1256        }
1257    }
1258
1259    #[test]
1260    fn every_method_response_reuses_one_failure_envelope() {
1261        let documents = documents();
1262        let mut expected_failure = None;
1263
1264        for method in PUBLIC_METHODS {
1265            let file_name = format!("{}-response.schema.json", method.replace('.', "-"));
1266            let response = documents
1267                .iter()
1268                .find(|document| document.file_name == file_name)
1269                .unwrap_or_else(|| panic!("missing response schema for {method}"));
1270            let failure = response.schema["$defs"]["SystemHelloFailureSchema"].clone();
1271            assert!(
1272                failure.is_object(),
1273                "{method} must define the failure branch"
1274            );
1275            if let Some(expected) = &expected_failure {
1276                assert_eq!(
1277                    &failure, expected,
1278                    "{method} must reuse the common failure envelope"
1279                );
1280            } else {
1281                expected_failure = Some(failure);
1282            }
1283            assert!(
1284                response.schema["anyOf"]
1285                    .as_array()
1286                    .expect("method response is a success/failure union")
1287                    .iter()
1288                    .any(|branch| { branch["$ref"] == "#/$defs/SystemHelloFailureSchema" }),
1289                "{method} response must expose the failure branch"
1290            );
1291        }
1292    }
1293}