Skip to main content

superdoc_sdk/
invoke.rs

1//! Invocation boundary used by generated methods and recording transports.
2
3use serde_json::Value;
4use std::{future::Future, time::Duration};
5
6/// A structured error from the client, host, or document operation.
7#[derive(Clone)]
8pub struct Error {
9    pub code: String,
10    pub message: String,
11    /// Original host text is retained for explicit diagnostics. It is omitted
12    /// from Debug/Display because it can contain document data or credentials.
13    pub host_message: Option<Box<str>>,
14    pub details: Option<Box<Value>>,
15    pub operation_id: Option<Box<str>>,
16    pub rpc_code: Option<i64>,
17    pub exit_code: Option<Box<i64>>,
18    /// A sent mutation may have completed even when its response was lost.
19    pub outcome_unknown: bool,
20}
21
22impl Error {
23    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
24        Self {
25            code: code.into(),
26            message: message.into(),
27            host_message: None,
28            details: None,
29            operation_id: None,
30            rpc_code: None,
31            exit_code: None,
32            outcome_unknown: false,
33        }
34    }
35
36    pub fn decode(cause: serde_json::Error) -> Self {
37        let mut error = Self::new(
38            "HOST_PROTOCOL_ERROR",
39            "Host result does not match its declared response type",
40        );
41        error.details = Some(Box::new(
42            serde_json::json!({"decodeError":cause.to_string()}),
43        ));
44        error
45    }
46}
47
48impl std::fmt::Display for Error {
49    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(formatter, "{}: {}", self.code, self.message)
51    }
52}
53impl std::error::Error for Error {}
54
55impl std::fmt::Debug for Error {
56    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        formatter
58            .debug_struct("Error")
59            .field("code", &self.code)
60            .field("operation_id", &self.operation_id)
61            .field("rpc_code", &self.rpc_code)
62            .field("exit_code", &self.exit_code)
63            .field("outcome_unknown", &self.outcome_unknown)
64            .finish_non_exhaustive()
65    }
66}
67
68pub type Result<T> = std::result::Result<T, Error>;
69
70/// Invocation options are separate from operation parameters and do not become
71/// extra document fields when sent through structured document RPC.
72#[derive(Clone, Default)]
73pub struct InvokeOptions {
74    pub timeout: Option<Duration>,
75    pub expected_revision: Option<String>,
76    pub change_mode: Option<String>,
77    pub dry_run: Option<bool>,
78    pub stdin_bytes: Option<Vec<u8>>,
79}
80
81impl std::fmt::Debug for InvokeOptions {
82    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        formatter
84            .debug_struct("InvokeOptions")
85            .field("timeout", &self.timeout)
86            .field("has_stdin", &self.stdin_bytes.is_some())
87            .finish_non_exhaustive()
88    }
89}
90
91/// A generated method calls exactly one canonical operation through this seam.
92/// Implementations own routing, authentication, session binding and I/O.
93pub trait Invoker: Sync {
94    fn handle_decode_error(&self, error: Error) -> Error {
95        error
96    }
97
98    fn invoke(
99        &self,
100        operation_id: &str,
101        params: Value,
102        options: InvokeOptions,
103    ) -> impl Future<Output = Result<Value>> + Send;
104}