Skip to main content

dre_core/config/
report.rs

1//! The shape of a report YAML file under `reports/` (one fragment of a report: a report may be
2//! split over several files, merged by name).
3//!
4//! Values the loader checks itself, to say what's wrong in DRE's words, are [`Loose`]. Outputs and
5//! Sets are read where they're used.
6
7use schemars::JsonSchema;
8use serde::Deserialize;
9use serde::de::IgnoredAny;
10
11use super::de::{Located, Loose, Map, Maybe, OneOf, UnknownKeys};
12
13type JsonMap = serde_json::Map<String, serde_json::Value>;
14
15/// One `queries:` item: a query's name, or a query with settings.
16pub type QueryItem = Located<Loose<OneOf<String, QueryEntry>>>;
17
18/// A report: SQL queries plus an output, in a YAML file under `reports/`.
19#[derive(Default, Deserialize, JsonSchema)]
20#[schemars(title = "DRE report", deny_unknown_fields)]
21pub struct ReportFile {
22    /// The report's name. Default: the name of the folder the YAML file is in. Report names are unique across the project.
23    #[schemars(length(min = 1))]
24    pub name: Option<Located<Loose<String>>>,
25    /// Tags to select the report with, `-s tag:<tag>`.
26    pub tags: Option<Located<Loose<Vec<String>>>>,
27    /// The `.sql` files whose results make the report's tabs, in the order they run, on one database session.
28    #[serde(default)]
29    #[schemars(schema_with = "queries")]
30    pub queries: Option<Located<Loose<Vec<QueryItem>>>>,
31    /// One output, or a list of outputs formatted from the same run of the queries (file outputs are delivered before messages). A list replaces the inherited output; a map changes it.
32    #[serde(default)]
33    #[schemars(schema_with = "output_or_list")]
34    pub output: Option<IgnoredAny>,
35    /// The connection (in `profiles.yml`) the queries run on, unless a query's own `profile:` or a source's says otherwise. Default: the folder's `+profile`, then `default_profile`. Can't be combined with `sets`. May use Jinja with `var()`, `env_var()`, `run.*` and `target.name`.
36    pub profile: Option<Located<Loose<String>>>,
37    /// The Sets the report can run as, by name (declared in `sets.yml`) or declared here.
38    pub sets: Option<Located<Loose<Vec<SetItem>>>>,
39    /// The Set a plain `dre run` uses. Must be one of `sets`.
40    pub default_set: Option<Located<Loose<String>>>,
41    /// Variables, read in SQL and YAML with `var('name')`. Values can be strings, numbers, booleans, lists or maps.
42    pub vars: Option<Located<Loose<JsonMap>>>,
43    /// The timezone `run.date` and `run.now` use, an IANA name such as `Australia/Sydney`. Default: UTC.
44    pub timezone: Option<Located<Loose<String>>>,
45    /// The locale this report's number filters format for (`de-DE`). Default: the folder's `+locale`, then the project's `locale`, then `en`.
46    pub locale: Option<Located<Loose<String>>>,
47    /// The plugin packages this project uses. DRE installs them on demand into `dre_deps/` and pins them in `dre.lock`. May be written in any project YAML file; `dependencies.yml` is the usual place.
48    #[serde(default)]
49    #[schemars(schema_with = "super::dependencies::plugins")]
50    pub plugins: Option<IgnoredAny>,
51    /// dbt-style source declarations (see the sources schema). May be written in any project YAML file.
52    #[serde(default)]
53    #[schemars(schema_with = "super::project::sources_ref")]
54    pub sources: Option<IgnoredAny>,
55    /// Removed: schedules live in schedules.yml. Read only to say so.
56    #[schemars(skip)]
57    pub schedule: Option<Located<IgnoredAny>>,
58    #[serde(rename = "$unknown", default)]
59    #[schemars(skip)]
60    pub unknown: UnknownKeys,
61}
62
63/// A query with settings, instead of just its name.
64#[derive(Deserialize, JsonSchema)]
65#[schemars(rename = "queryEntry", deny_unknown_fields)]
66pub struct QueryEntry {
67    /// The name of a `.sql` file under `reports/`, without folder or extension.
68    #[schemars(required)]
69    pub query: Option<Loose<String>>,
70    /// The connection this query runs on, over the report's, Set's, folder's and project's. Must agree with the `profile` of any source it uses. May use Jinja with `var()`, `env_var()`, `run.*` and `target.name`.
71    pub profile: Option<Loose<String>>,
72    /// `false` runs the file only for what it does (temp tables, `SET`s) and discards any result, so it gets no tab.
73    #[schemars(extend("default" = true))]
74    pub tab: Option<Loose<bool>>,
75    /// The tab (sheet) name. Default: the file's name. Not allowed with `tab: false`.
76    pub tab_name: Option<Loose<String>>,
77    /// Where the data starts on the sheet (xlsx only). Default: `A1`.
78    #[schemars(regex(pattern = "^[A-Za-z]{1,3}[0-9]+$"))]
79    pub anchor: Option<Loose<String>>,
80    /// Whether to write the column names as the first row (xlsx only). Default: the output's `header`.
81    pub header: Option<Loose<bool>>,
82    /// Per-column settings for this tab, by column name (xlsx only).
83    #[serde(default)]
84    #[schemars(schema_with = "columns")]
85    pub columns: Option<serde_json::Value>,
86    /// Size this tab's columns from their content (xlsx only), over the output's `autofit`. Default: the output's, which is on.
87    pub autofit: Option<Loose<bool>>,
88    /// How this tab looks (xlsx only), over the output's `style`: `font`, `header`, `totals`, `banded_rows`, `borders`, and cell keys (`bold`, `fill`, ...) for every data cell. See the xlsx plugin page.
89    #[serde(default)]
90    #[schemars(schema_with = "sheet_style")]
91    pub style: Option<serde_json::Value>,
92    #[serde(rename = "$unknown", default)]
93    #[schemars(skip)]
94    pub unknown: UnknownKeys,
95}
96
97fn sheet_style(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
98    schemars::json_schema!({
99        "type": "object",
100        "description": "How an xlsx tab looks. Cell keys apply to every data cell; `header` and `totals` take cell keys for those rows.",
101        "properties": {
102            "header": {"type": "object", "description": "Cell keys for the header row (default: bold)."},
103            "totals": {"type": "object", "description": "Cell keys for the totals row (default: bold, a thin top border)."},
104            "banded_rows": {"oneOf": [{"type": "string", "pattern": "^#?[0-9A-Fa-f]{6}$"}, {"const": false}], "description": "The fill of every other data row, `\"#RRGGBB\"`, or `false`."},
105            "borders": {"enum": ["none", "thin", "medium"], "description": "A border around every cell of the table."},
106            "bold": {"type": "boolean", "description": "Bold text in every data cell."},
107            "italic": {"type": "boolean", "description": "Italic text in every data cell."},
108            "underline": {"type": "boolean", "description": "Underlined text in every data cell."},
109            "font": {"type": "object", "description": "The font: `{name: Calibri, size: 11}`.", "properties": {"name": {"type": "string", "description": "The font's name."}, "size": {"type": "number", "description": "The size in points."}}, "additionalProperties": false},
110            "font_color": {"type": "string", "pattern": "^#?[0-9A-Fa-f]{6}$", "description": "The text colour, `\"#RRGGBB\"`."},
111            "fill": {"type": "string", "pattern": "^#?[0-9A-Fa-f]{6}$", "description": "The background colour, `\"#RRGGBB\"`."},
112            "align": {"enum": ["left", "center", "right"], "description": "Horizontal alignment."},
113            "border": {"enum": ["none", "thin", "medium"], "description": "A border around each data cell."}
114        },
115        "additionalProperties": false
116    })
117}
118
119fn columns(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
120    schemars::json_schema!({
121        "type": "object",
122        "additionalProperties": {
123            "type": "object",
124            "description": "Settings for one column of an xlsx tab.",
125            "properties": {
126                "format": {"type": "string", "description": "The Excel number format of the column, e.g. `#,##0.00` or `dd/mm/yyyy`. See the xlsx column formats in the plugins reference."},
127                "formula": {"type": "string", "description": "An Excel formula for each row of this column; `{name}` stands for that column's cell on the same row, e.g. `=ROUND({qty}*{unit_price},2)`. The SQL selects a placeholder column where the formula goes."},
128                "total": {"type": "string", "description": "Puts a total under the column: one of `sum`, `count`, `average`, `min`, `max`, or a formula such as `=SUM({net:*})`."},
129                "style": {"type": "object", "description": "How the column's data cells look: `bold`, `italic`, `underline`, `font` (`{name, size}`), `font_color`, `fill` (`\"#RRGGBB\"`), `align` (`left`, `center`, `right`), `border` (`none`, `thin`, `medium`). Over the tab's and output's `style`."},
130                "width": {"oneOf": [{"const": "auto"}, {"type": "number", "exclusiveMinimum": 0, "maximum": 255}], "description": "The column's width: `auto` (sized from its content, at most 60 characters) or a number of characters. Over the tab's and output's `autofit`."}
131            },
132            "additionalProperties": false
133        },
134        "propertyNames": {"minLength": 1}
135    })
136}
137
138/// One `sets:` item: a Set's name (declared in `sets.yml`), or a Set declared here.
139pub type SetItem = Located<Loose<OneOf<String, SetEntry>>>;
140
141/// A Set declared in the report: a named variant of it.
142#[derive(Deserialize, JsonSchema)]
143#[schemars(rename = "setEntry", deny_unknown_fields)]
144pub struct SetEntry {
145    /// The Set's name. A report can also name Sets declared in `sets.yml`.
146    #[schemars(required)]
147    pub name: Option<Loose<String>>,
148    /// The connection this Set runs on. May use Jinja with `var()`, `env_var()`, `run.*` and `target.name`.
149    pub profile: Option<Located<Loose<String>>>,
150    /// Variables, read in SQL and YAML with `var('name')`. Values can be strings, numbers, booleans, lists or maps.
151    #[serde(default)]
152    #[schemars(schema_with = "any_map")]
153    pub vars: Option<Loose<JsonMap>>,
154    /// Queries to leave out of this Set, by name.
155    pub exclude: Option<Located<Loose<Vec<String>>>>,
156    /// Replaces the report's queries for this Set.
157    #[serde(default)]
158    #[schemars(schema_with = "set_queries")]
159    pub queries: Option<Located<Loose<Vec<SetQueryItem>>>>,
160    /// Renames tabs for this Set: query name to tab name.
161    #[serde(default)]
162    #[schemars(schema_with = "tab_names")]
163    pub tab_names: Option<Loose<JsonMap>>,
164    /// Output settings for this Set: a map changes the inherited output (with several, the one its `name:` names); a list replaces them all.
165    #[serde(default)]
166    #[schemars(schema_with = "output_or_list")]
167    pub output: Option<serde_json::Value>,
168    /// The locale for this Set's number filters (`fr-FR`), above the report's.
169    pub locale: Option<Loose<String>>,
170    /// Removed: schedules live in schedules.yml. Read only to say so.
171    #[schemars(skip)]
172    pub schedule: Option<IgnoredAny>,
173    #[serde(rename = "$unknown", default)]
174    #[schemars(skip)]
175    pub unknown: UnknownKeys,
176}
177
178/// One item of a Set's `queries:`: a query's name, or a query with settings.
179pub type SetQueryItem = Loose<OneOf<String, SetQuery>>;
180
181/// A query of a Set with its own settings.
182#[derive(Deserialize)]
183pub struct SetQuery {
184    pub query: Option<Loose<String>>,
185    pub tab: Option<Loose<bool>>,
186    pub tab_name: Option<Loose<String>>,
187}
188
189fn any_map(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
190    schemars::json_schema!({"type": "object", "additionalProperties": true})
191}
192
193fn tab_names(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
194    schemars::json_schema!({"type": "object", "additionalProperties": {"type": "string"}})
195}
196
197fn output_or_list(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
198    let output = g.subschema_for::<Output>();
199    schemars::json_schema!({
200        "oneOf": [output, {"type": "array", "minItems": 1, "items": output}]
201    })
202}
203
204fn set_queries(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
205    schemars::json_schema!({
206        "type": "array",
207        "items": {
208            "description": "A query of this Set, by name, or with its own settings.",
209            "oneOf": [
210                {"type": "string"},
211                {
212                    "type": "object",
213                    "description": "A query with settings.",
214                    "properties": {
215                        "query": {"type": "string", "description": "The query's name."},
216                        "tab": {"type": "boolean", "description": "Whether the query makes a tab."},
217                        "tab_name": {"type": "string", "description": "The tab name."}
218                    },
219                    "required": ["query"],
220                    "additionalProperties": true
221                }
222            ]
223        }
224    })
225}
226
227/// How a report's result is written and where it goes. Besides the keys below, each format takes its own options (for example `delimiter` for `delimited`, `columns` for `fixed_width`, `text` for `message`); they are documented with the formats.
228// Outputs are layered (the built-in default, `default_output`, each folder's `+output`, the
229// report's and a Set's `output`) and read once merged.
230#[derive(Deserialize, JsonSchema)]
231#[schemars(rename = "output", extend("additionalProperties" = true))]
232pub struct Output {
233    /// Names the output so other outputs and Set overrides can refer to it (`outputs.<name>` in a message, `attach:`). Also the default file name (`<name>.<ext>`). Unique within the report.
234    #[schemars(regex(pattern = "^[A-Za-z_][A-Za-z0-9_]*$"))]
235    pub name: Option<Loose<String>>,
236    /// The output format: `message` (built in: a short headline rendered from the results), or `csv`, `delimited`, `fixed_width`, `parquet` or `xlsx` (each a plugin). Default: `csv`, or the project's `default_output`.
237    pub format: Option<Loose<String>>,
238    /// Which of the report's queries this output formats. Default: all of them. Each query runs once, whatever the number of outputs.
239    #[schemars(length(min = 1))]
240    pub queries: Option<Loose<Vec<String>>>,
241    /// A Jinja expression over the results (`results.<query>.value < 0`); when it's false, the output is skipped and recorded as `skipped`.
242    #[serde(default)]
243    #[schemars(schema_with = "string_or_bool")]
244    pub when: Option<Loose<OneOf<String, bool>>>,
245    /// Where to deliver the file: one destination, or a list to deliver to several in one run. Default: the file stays in the target path.
246    #[serde(default)]
247    #[schemars(schema_with = "destination_or_list")]
248    pub destination: Option<Loose<OneOf<Destination, Vec<Loose<Destination>>>>>,
249    pub template: Option<Loose<Template>>,
250    /// File extension for text formats (e.g. `aba`), instead of the format's own. `""` or `false` means none. Not for xlsx.
251    #[serde(default)]
252    #[schemars(schema_with = "extension")]
253    pub extension: Maybe<Loose<Option<OneOf<String, bool>>>>,
254    /// The format's own options.
255    #[serde(rename = "$unknown", default)]
256    #[schemars(skip)]
257    pub options: Map<serde_json::Value>,
258}
259
260/// Where a file is delivered: the name of a destination profile in `profiles.yml`, an optional `path`, and the options of that destination's plugin (e.g. `to`, `subject` and `body` for email). The built-in profile `local` copies the file to a local path.
261#[derive(Deserialize, JsonSchema)]
262#[schemars(rename = "destination", extend("additionalProperties" = true))]
263pub struct Destination {
264    /// The destination profile in `profiles.yml` (under `destinations:`) to deliver with. `local` is built in. It uses its entry for the run (`--target`, `DRE_TARGET`, else the profile's own `target:`, else `dev`); a missing entry is an error, and an entry `{deliver: false}` delivers nowhere. May use Jinja with `var()`, `env_var()`, `run.*` and `target.name`.
265    #[schemars(required)]
266    pub profile: Option<Loose<String>>,
267    /// Where to put the file: a path, or a URL such as `s3://bucket/key`, depending on the destination. Rendered with Jinja, so it can use `var()`, `run.*`, macros and `destination.*` (this destination's settings).
268    pub path: Option<Loose<String>>,
269    /// On a message output's entry, for a destination that takes messages and files (`slack`, `email`): other outputs of the report whose files go with the message.
270    #[serde(default)]
271    #[schemars(schema_with = "attach")]
272    pub attach: Option<Loose<OneOf<String, Vec<String>>>>,
273    /// The destination plugin's options.
274    #[serde(rename = "$unknown", default)]
275    #[schemars(skip)]
276    pub options: Map<serde_json::Value>,
277}
278
279/// Fills a branded Excel workbook instead of creating a new one (xlsx only).
280#[derive(Deserialize, JsonSchema)]
281#[schemars(rename = "template", deny_unknown_fields)]
282pub struct Template {
283    /// Path of the `.xlsx` template, relative to the project.
284    #[schemars(required)]
285    pub file: Option<Loose<String>>,
286    /// Where each query's data goes in the template. Default: none, so the template is copied as it is.
287    pub bindings: Option<Loose<Vec<Loose<TemplateBinding>>>>,
288}
289
290/// One block of data in an xlsx template: a table of a query's result, or a single cell.
291#[derive(Deserialize, JsonSchema)]
292#[schemars(deny_unknown_fields, inline)]
293pub struct TemplateBinding {
294    /// The template sheet to write into.
295    #[schemars(required)]
296    pub sheet: Option<Loose<String>>,
297    /// The query whose result goes here. Must be one of the Binding's queries.
298    pub query: Option<Loose<String>>,
299    /// Which result set of the query to use, counting from 1. Default: the last.
300    #[schemars(range(min = 1))]
301    pub result_index: Option<Loose<u64>>,
302    /// The top-left cell of a table block. A table block needs a `query`.
303    #[schemars(regex(pattern = "^[A-Za-z]{1,3}[0-9]+$"))]
304    pub anchor: Option<Loose<String>>,
305    /// Whether to write the column names above the data in a table block.
306    pub header: Option<Loose<bool>>,
307    /// The columns of the query to write in a table block, in order. Default: all.
308    pub columns: Option<Loose<Vec<String>>>,
309    /// Makes this a single-cell binding: the cell to write. Needs exactly one of `value`, or `query` with `column`.
310    #[schemars(regex(pattern = "^[A-Za-z]{1,3}[0-9]+$"))]
311    pub cell: Option<Loose<String>>,
312    /// A fixed value (rendered with Jinja) for a single-cell binding.
313    pub value: Option<Loose<String>>,
314    /// The column of the query's first row to write into a single-cell binding.
315    pub column: Option<Loose<String>>,
316    #[serde(rename = "$unknown", default)]
317    #[schemars(skip)]
318    pub unknown: UnknownKeys,
319}
320
321fn string_or_bool(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
322    schemars::json_schema!({"type": ["string", "boolean"]})
323}
324
325fn extension(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
326    // `null` is a value here (no extension), not schemars' "optional".
327    schemars::json_schema!({"type": ["string", "boolean", "null"], "x-dre-null": true})
328}
329
330fn destination_or_list(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
331    let destination = g.subschema_for::<Destination>();
332    schemars::json_schema!({
333        "oneOf": [destination, {"type": "array", "minItems": 1, "items": destination}]
334    })
335}
336
337fn attach(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
338    schemars::json_schema!({
339        "oneOf": [
340            {"type": "string"},
341            {"type": "array", "items": {"type": "string"}, "minItems": 1}
342        ]
343    })
344}
345
346fn queries(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
347    let entry = g.subschema_for::<QueryEntry>();
348    schemars::json_schema!({
349        "type": "array",
350        "items": {
351            "oneOf": [
352                {"type": "string", "description": "The name of a `.sql` file under `reports/`, without extension."},
353                entry
354            ]
355        }
356    })
357}