Skip to main content

alembic_engine/
external.rs

1//! helpers for implementing external adapters.
2
3use crate::{ApplyReport, BackendId, Op, ProvisionReport, StateData};
4use alembic_core::{JsonMap, Key, Schema, TypeName};
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::io::{self, Read, Write};
8
9/// current external adapter protocol version.
10pub const EXTERNAL_PROTOCOL_VERSION: u8 = 1;
11
12/// request envelope sent to external adapters.
13#[derive(Debug, Serialize, Deserialize)]
14pub struct ExternalEnvelope {
15    /// protocol version.
16    pub version: u8,
17    /// request payload.
18    #[serde(flatten)]
19    pub request: ExternalRequest,
20}
21
22/// external adapter request variants.
23#[derive(Debug, Serialize, Deserialize)]
24#[serde(tag = "method", rename_all = "snake_case")]
25pub enum ExternalRequest {
26    /// read inventory for the requested types.
27    Read {
28        schema: Schema,
29        types: Vec<TypeName>,
30        state: StateData,
31    },
32    /// apply a set of operations.
33    Write {
34        schema: Schema,
35        ops: Vec<Op>,
36        state: StateData,
37    },
38    /// ensure the backend schema exists.
39    EnsureSchema { schema: Schema },
40}
41
42/// observed object representation for external adapters.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ExternalObject {
45    /// object type.
46    pub type_name: TypeName,
47    /// natural key for matching.
48    pub key: Key,
49    /// observed attributes.
50    pub attrs: JsonMap,
51    /// backend id when known.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub backend_id: Option<BackendId>,
54}
55
56/// response wrapper for external adapters.
57#[derive(Debug, Serialize, Deserialize)]
58pub struct ExternalResponse<T> {
59    /// whether the request succeeded.
60    pub ok: bool,
61    /// payload on success.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub result: Option<T>,
64    /// error message on failure.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub error: Option<String>,
67}
68
69impl<T> ExternalResponse<T> {
70    /// build a success response.
71    pub fn ok(result: T) -> Self {
72        Self {
73            ok: true,
74            result: Some(result),
75            error: None,
76        }
77    }
78
79    /// build an error response.
80    pub fn error(message: impl Into<String>) -> Self {
81        Self {
82            ok: false,
83            result: None,
84            error: Some(message.into()),
85        }
86    }
87
88    /// convert a result into a response.
89    pub fn from_result(result: Result<T>) -> Self {
90        match result {
91            Ok(value) => Self::ok(value),
92            Err(err) => Self::error(err.to_string()),
93        }
94    }
95}
96
97/// external adapter helper trait.
98pub trait ExternalAdapter {
99    /// read objects from the backend.
100    fn read(
101        &mut self,
102        schema: &Schema,
103        types: &[TypeName],
104        state: &StateData,
105    ) -> Result<Vec<ExternalObject>>;
106
107    /// apply operations to the backend.
108    fn write(&mut self, schema: &Schema, ops: &[Op], state: &StateData) -> Result<ApplyReport>;
109
110    /// provision backend schema elements.
111    fn ensure_schema(&mut self, schema: &Schema) -> Result<ProvisionReport> {
112        let _ = schema;
113        Ok(ProvisionReport::default())
114    }
115}
116
117/// run an external adapter using stdin/stdout for a single request.
118pub fn run_external_adapter<A: ExternalAdapter>(mut adapter: A) -> io::Result<()> {
119    let mut input = String::new();
120    io::stdin().read_to_string(&mut input)?;
121    if input.trim().is_empty() {
122        return Ok(());
123    }
124
125    let envelope: ExternalEnvelope = match serde_json::from_str(&input) {
126        Ok(envelope) => envelope,
127        Err(err) => return write_error(format!("invalid request: {err}")),
128    };
129
130    if envelope.version != EXTERNAL_PROTOCOL_VERSION {
131        return write_error(format!(
132            "unsupported protocol version {} (expected {})",
133            envelope.version, EXTERNAL_PROTOCOL_VERSION
134        ));
135    }
136
137    let mut stdout = io::BufWriter::new(io::stdout());
138    match envelope.request {
139        ExternalRequest::Read {
140            schema,
141            types,
142            state,
143        } => {
144            let response = ExternalResponse::from_result(adapter.read(&schema, &types, &state));
145            write_response(&mut stdout, response)
146        }
147        ExternalRequest::Write { schema, ops, state } => {
148            let response = ExternalResponse::from_result(adapter.write(&schema, &ops, &state));
149            write_response(&mut stdout, response)
150        }
151        ExternalRequest::EnsureSchema { schema } => {
152            let response = ExternalResponse::from_result(adapter.ensure_schema(&schema));
153            write_response(&mut stdout, response)
154        }
155    }
156}
157
158fn write_error(message: String) -> io::Result<()> {
159    let mut stdout = io::BufWriter::new(io::stdout());
160    let response = ExternalResponse::<serde_json::Value>::error(message);
161    write_response(&mut stdout, response)
162}
163
164fn write_response<T: Serialize>(
165    out: &mut impl Write,
166    response: ExternalResponse<T>,
167) -> io::Result<()> {
168    serde_json::to_writer(&mut *out, &response).map_err(io::Error::other)?;
169    out.write_all(b"\n")?;
170    out.flush()
171}
172
173/// convenience macro to define an external adapter main.
174#[macro_export]
175macro_rules! alembic_external_main {
176    ($adapter:expr) => {
177        fn main() -> std::io::Result<()> {
178            $crate::external::run_external_adapter($adapter)
179        }
180    };
181}
182
183#[cfg(test)]
184mod tests {
185    use super::ExternalResponse;
186    use serde_json::json;
187
188    #[test]
189    fn external_response_ok_serializes() {
190        let response = ExternalResponse::ok(vec!["one".to_string()]);
191        let value = serde_json::to_value(&response).unwrap();
192        assert_eq!(value, json!({"ok": true, "result": ["one"]}));
193    }
194
195    #[test]
196    fn external_response_error_serializes() {
197        let response: ExternalResponse<Vec<String>> = ExternalResponse::error("boom");
198        let value = serde_json::to_value(&response).unwrap();
199        assert_eq!(value, json!({"ok": false, "error": "boom"}));
200    }
201}