Skip to main content

dre_protocol/
msg.rs

1//! Control messages. Every JSON frame is an object with a `type` field.
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6pub use crate::options::OptionField;
7use crate::{Kind, PluginId};
8
9/// Core → plugin.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum Request {
13    /// Always first. The plugin picks a version in both ranges or replies `version_mismatch`.
14    Hello {
15        min_version: u32,
16        max_version: u32,
17        core_version: String,
18        /// The plugin core wants served, for an executable that provides several. A package
19        /// that doesn't provide it replies `error`; without it, the package serves its first.
20        #[serde(default, skip_serializing_if = "Option::is_none")]
21        plugin: Option<PluginId>,
22    },
23    /// Describe the connection fields a profile output for this plugin takes (used by `dre init`)
24    /// and the options a report's config block for it takes.
25    Describe {},
26    /// Check one config block of options (a format's `output:` keys, a destination entry's keys)
27    /// without doing anything. Replies `validated`.
28    Validate {
29        options: Map<String, Value>,
30    },
31    /// Source: open a session. All later `execute`/`check` requests run on it.
32    Open {
33        connection: Map<String, Value>,
34        read_only: bool,
35    },
36    /// Source: run one statement. Replies `result` (then Arrow frames, then `result_end`) or
37    /// `no_result`.
38    Execute {
39        sql: String,
40        #[serde(default, skip_serializing_if = "Option::is_none")]
41        row_limit: Option<u64>,
42    },
43    /// Source: verify one statement without executing it. Replies `ok` or `error`.
44    Check {
45        sql: String,
46    },
47    /// Format: write result sets to `path`. Followed, per result set, by Arrow frames (at least one,
48    /// carrying the schema) and a `result_set_end`, then `finish`. Replies `written`.
49    Write {
50        path: String,
51        format: String,
52        options: Map<String, Value>,
53        result_sets: Vec<ResultSetMeta>,
54        #[serde(default, skip_serializing_if = "Option::is_none")]
55        template: Option<Value>,
56    },
57    ResultSetEnd {},
58    Finish {},
59    /// Destination: deliver a local file, or with `files` (only to a plugin advertising
60    /// `multi_file`) every file of one output at once. Exactly one of `local_path`/`files` is set,
61    /// unless `message` is: then `files` (possibly empty) holds the files to attach to it, and is
62    /// sent only to a plugin advertising `message`. `options` are the destination entry's plugin
63    /// options, rendered by core. Replies `delivered`.
64    Deliver {
65        #[serde(default, skip_serializing_if = "Option::is_none")]
66        local_path: Option<String>,
67        #[serde(default, skip_serializing_if = "Option::is_none")]
68        remote_path: Option<String>,
69        #[serde(default, skip_serializing_if = "Vec::is_empty")]
70        files: Vec<DeliveryFile>,
71        connection: Map<String, Value>,
72        #[serde(default)]
73        options: Map<String, Value>,
74        #[serde(default, skip_serializing_if = "Option::is_none")]
75        message: Option<Message>,
76    },
77    /// Source: load rows into a temporary table on the session, named after `name`. Followed by
78    /// Arrow frames (at least one, carrying the schema) and a `result_set_end`. Replies `loaded`.
79    /// Only sent to plugins advertising `load`.
80    Load {
81        name: String,
82    },
83    /// End the conversation; the plugin exits 0.
84    Close {},
85}
86
87/// Plugin → core.
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89#[serde(tag = "type", rename_all = "snake_case")]
90pub enum Response {
91    Hello {
92        protocol_version: u32,
93        kind: Kind,
94        name: String,
95        version: String,
96        #[serde(default)]
97        capabilities: Vec<String>,
98        /// Every plugin the executable serves; empty for one that serves only `kind`/`name`.
99        #[serde(default, skip_serializing_if = "Vec::is_empty")]
100        provides: Vec<PluginId>,
101    },
102    VersionMismatch {
103        min_version: u32,
104        max_version: u32,
105    },
106    Describe {
107        connection_fields: Vec<ConnectionField>,
108        #[serde(default, skip_serializing_if = "Vec::is_empty")]
109        option_fields: Vec<OptionField>,
110        /// Source: the character the database quotes identifiers with (`"` for DuckDB and
111        /// Postgres, a backtick for Databricks). Core uses it to quote names a project asks to
112        /// quote (a source's `quoting:`). Every source sets it; other kinds leave it out.
113        #[serde(default, skip_serializing_if = "Option::is_none")]
114        identifier_quote: Option<String>,
115        /// Destination advertising `message`: the most characters a message may have in the
116        /// service, after translation. Core shows it next to a message's length in a preview.
117        #[serde(default, skip_serializing_if = "Option::is_none")]
118        message_limit: Option<u64>,
119    },
120    /// Every problem with the options, each a sentence naming the key; empty when they're fine.
121    Validated {
122        errors: Vec<String>,
123    },
124    Ok {},
125    /// A result set follows as Arrow frames, ended by `result_end`.
126    Result {
127        columns: Vec<String>,
128    },
129    ResultEnd {
130        rows: u64,
131    },
132    NoResult {
133        #[serde(default, skip_serializing_if = "Option::is_none")]
134        rows_affected: Option<u64>,
135    },
136    Written {
137        files: Vec<String>,
138        /// Things the person should know about the files (a value written as text, say).
139        #[serde(default, skip_serializing_if = "Vec::is_empty")]
140        warnings: Vec<String>,
141    },
142    Delivered {
143        location: String,
144    },
145    /// `relation` is what SQL uses to read the loaded rows. `warning`, when set, is shown to the
146    /// user (e.g. the database has no bulk path, so a load this size is slow).
147    Loaded {
148        relation: String,
149        rows: u64,
150        #[serde(default, skip_serializing_if = "Option::is_none")]
151        warning: Option<String>,
152    },
153    Error {
154        message: String,
155    },
156}
157
158/// A rendered message, as `deliver` carries it.
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct Message {
161    /// One line, plain text.
162    pub title: String,
163    /// The portable Markdown subset ([`crate::markdown`]).
164    pub text: String,
165    /// An HTML body, when one was made; core leaves it out today. A plugin that can show HTML
166    /// (email) uses it when present, else converts `text`.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub html: Option<String>,
169    /// The `.md` file core wrote: `# title`, then the text.
170    pub path: String,
171}
172
173/// One file of a multi-file `deliver`.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct DeliveryFile {
176    pub local_path: String,
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub remote_path: Option<String>,
179}
180
181/// Per result set, what a format plugin needs to lay it out.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct ResultSetMeta {
184    /// Sheet name (xlsx) or file suffix (single-table formats).
185    pub name: String,
186    /// The query (`.sql` basename) that produced it.
187    pub query: String,
188    /// 1-based index among that query's result sets.
189    pub result_index: usize,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub anchor: Option<String>,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub header: Option<bool>,
194    /// The query entry's `columns:` map: per result column, how to show it.
195    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
196    pub columns: std::collections::BTreeMap<String, ColumnOptions>,
197}
198
199/// One entry of a `columns:` map.
200#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
201pub struct ColumnOptions {
202    /// An Excel number format code.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub format: Option<String>,
205    /// A row formula (`={qty}*{price}`), written in place of the column's value, which becomes
206    /// its cached result.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub formula: Option<String>,
209    /// A totals row entry under the data: `sum`, `average`, `count`, `min`, `max`, or a formula
210    /// over whole columns (`=SUM({amount:*})/COUNT({qty:*})`).
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub total: Option<String>,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216pub struct ConnectionField {
217    pub name: String,
218    #[serde(default)]
219    pub description: String,
220    #[serde(default)]
221    pub required: bool,
222    /// Offered as an `env_var()` reference by default in `dre init`.
223    #[serde(default)]
224    pub secret: bool,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub default: Option<Value>,
227    /// A source type whose profile has the same field: `dre init` offers the value entered for
228    /// that source as this field's default (e.g. one Databricks host for source and destination).
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub same_as_source: Option<String>,
231    /// Set by hand in `profiles.yml`: `dre init` doesn't ask for it. For alternatives to a
232    /// prompted field (a key given as text instead of a file) and nested blocks (`ssh:`).
233    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
234    pub manual: bool,
235}
236
237impl ConnectionField {
238    pub fn new(name: &str, description: &str) -> Self {
239        ConnectionField {
240            name: name.into(),
241            description: description.into(),
242            required: false,
243            secret: false,
244            default: None,
245            same_as_source: None,
246            manual: false,
247        }
248    }
249    pub fn required(mut self) -> Self {
250        self.required = true;
251        self
252    }
253    pub fn secret(mut self) -> Self {
254        self.secret = true;
255        self
256    }
257    pub fn default(mut self, v: impl Into<Value>) -> Self {
258        self.default = Some(v.into());
259        self
260    }
261    pub fn same_as_source(mut self, source_type: &str) -> Self {
262        self.same_as_source = Some(source_type.into());
263        self
264    }
265    pub fn manual(mut self) -> Self {
266        self.manual = true;
267        self
268    }
269}