1use 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
15pub type QueryItem = Located<Loose<OneOf<String, QueryEntry>>>;
17
18#[derive(Default, Deserialize, JsonSchema)]
20#[schemars(title = "DRE report", deny_unknown_fields)]
21pub struct ReportFile {
22 #[schemars(length(min = 1))]
24 pub name: Option<Located<Loose<String>>>,
25 pub description: Option<Located<Loose<String>>>,
27 pub meta: Option<Located<Loose<JsonMap>>>,
29 pub tags: Option<Located<Loose<Vec<String>>>>,
31 #[serde(default)]
33 #[schemars(schema_with = "queries")]
34 pub queries: Option<Located<Loose<Vec<QueryItem>>>>,
35 #[serde(default)]
37 #[schemars(schema_with = "output_or_list")]
38 pub output: Option<IgnoredAny>,
39 pub profile: Option<Located<Loose<String>>>,
41 pub sets: Option<Located<Loose<Vec<SetItem>>>>,
43 pub default_set: Option<Located<Loose<String>>>,
45 pub vars: Option<Located<Loose<JsonMap>>>,
47 pub var_schema: Option<Located<Loose<crate::variables::Definitions>>>,
50 pub timezone: Option<Located<Loose<String>>>,
52 pub locale: Option<Located<Loose<String>>>,
54 #[serde(default)]
56 #[schemars(schema_with = "super::dependencies::plugins")]
57 pub plugins: Option<IgnoredAny>,
58 #[serde(default)]
60 #[schemars(schema_with = "super::project::sources_ref")]
61 pub sources: Option<IgnoredAny>,
62 #[schemars(skip)]
64 pub schedule: Option<Located<IgnoredAny>>,
65 #[serde(rename = "$unknown", default)]
66 #[schemars(skip)]
67 pub unknown: UnknownKeys,
68}
69
70#[derive(Deserialize, JsonSchema)]
72#[schemars(rename = "queryEntry", deny_unknown_fields)]
73pub struct QueryEntry {
74 #[schemars(required)]
76 pub query: Option<Loose<String>>,
77 pub profile: Option<Loose<String>>,
79 #[schemars(extend("default" = true))]
81 pub tab: Option<Loose<bool>>,
82 pub tab_name: Option<Loose<String>>,
84 #[schemars(regex(pattern = "^[A-Za-z]{1,3}[0-9]+$"))]
86 pub anchor: Option<Loose<String>>,
87 pub header: Option<Loose<bool>>,
89 #[serde(default)]
91 #[schemars(schema_with = "columns")]
92 pub columns: Option<serde_json::Value>,
93 pub on_empty: Option<Loose<crate::no_data::OnEmpty>>,
95 pub secret_columns: Option<Loose<Vec<String>>>,
97 pub autofit: Option<Loose<bool>>,
99 #[serde(default)]
101 #[schemars(schema_with = "sheet_style")]
102 pub style: Option<serde_json::Value>,
103 #[serde(rename = "$unknown", default)]
104 #[schemars(skip)]
105 pub unknown: UnknownKeys,
106}
107
108fn sheet_style(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
109 schemars::json_schema!({
110 "type": "object",
111 "description": "How an xlsx tab looks. Cell keys apply to every data cell; `header` and `totals` take cell keys for those rows.",
112 "properties": {
113 "header": {"type": "object", "description": "Cell keys for the header row (default: bold)."},
114 "totals": {"type": "object", "description": "Cell keys for the totals row (default: bold, a thin top border)."},
115 "banded_rows": {"oneOf": [{"type": "string", "pattern": "^#?[0-9A-Fa-f]{6}$"}, {"const": false}], "description": "The fill of every other data row, `\"#RRGGBB\"`, or `false`."},
116 "borders": {"enum": ["none", "thin", "medium"], "description": "A border around every cell of the table."},
117 "bold": {"type": "boolean", "description": "Bold text in every data cell."},
118 "italic": {"type": "boolean", "description": "Italic text in every data cell."},
119 "underline": {"type": "boolean", "description": "Underlined text in every data cell."},
120 "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},
121 "font_color": {"type": "string", "pattern": "^#?[0-9A-Fa-f]{6}$", "description": "The text colour, `\"#RRGGBB\"`."},
122 "fill": {"type": "string", "pattern": "^#?[0-9A-Fa-f]{6}$", "description": "The background colour, `\"#RRGGBB\"`."},
123 "align": {"enum": ["left", "center", "right"], "description": "Horizontal alignment."},
124 "border": {"enum": ["none", "thin", "medium"], "description": "A border around each data cell."}
125 },
126 "additionalProperties": false
127 })
128}
129
130fn columns(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
131 schemars::json_schema!({
132 "type": "object",
133 "additionalProperties": {
134 "type": "object",
135 "description": "Settings for one column of an xlsx tab.",
136 "properties": {
137 "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."},
138 "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."},
139 "total": {"type": "string", "description": "Puts a total under the column: one of `sum`, `count`, `average`, `min`, `max`, or a formula such as `=SUM({net:*})`."},
140 "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`."},
141 "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`."}
142 },
143 "additionalProperties": false
144 },
145 "propertyNames": {"minLength": 1}
146 })
147}
148
149pub type SetItem = Located<Loose<OneOf<String, SetEntry>>>;
151
152#[derive(Deserialize, JsonSchema)]
154#[schemars(rename = "setEntry", deny_unknown_fields)]
155pub struct SetEntry {
156 #[schemars(required)]
158 pub name: Option<Loose<String>>,
159 pub profile: Option<Located<Loose<String>>>,
161 #[serde(default)]
163 #[schemars(schema_with = "any_map")]
164 pub vars: Option<Loose<JsonMap>>,
165 pub exclude: Option<Located<Loose<Vec<String>>>>,
167 #[serde(default)]
169 #[schemars(schema_with = "set_queries")]
170 pub queries: Option<Located<Loose<Vec<SetQueryItem>>>>,
171 #[serde(default)]
173 #[schemars(schema_with = "tab_names")]
174 pub tab_names: Option<Loose<JsonMap>>,
175 #[serde(default)]
177 #[schemars(schema_with = "output_or_list")]
178 pub output: Option<serde_json::Value>,
179 pub locale: Option<Loose<String>>,
181 #[schemars(skip)]
183 pub schedule: Option<IgnoredAny>,
184 #[serde(rename = "$unknown", default)]
185 #[schemars(skip)]
186 pub unknown: UnknownKeys,
187}
188
189pub type SetQueryItem = Loose<OneOf<String, SetQuery>>;
191
192#[derive(Deserialize)]
194pub struct SetQuery {
195 pub query: Option<Loose<String>>,
196 pub tab: Option<Loose<bool>>,
197 pub tab_name: Option<Loose<String>>,
198 pub on_empty: Option<Loose<crate::no_data::OnEmpty>>,
199 pub secret_columns: Option<Loose<Vec<String>>>,
200}
201
202fn any_map(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
203 schemars::json_schema!({"type": "object", "additionalProperties": true})
204}
205
206fn tab_names(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
207 schemars::json_schema!({"type": "object", "additionalProperties": {"type": "string"}})
208}
209
210fn output_or_list(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
211 let output = g.subschema_for::<Output>();
212 schemars::json_schema!({
213 "oneOf": [output, {"type": "array", "minItems": 1, "items": output}]
214 })
215}
216
217fn set_queries(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
218 schemars::json_schema!({
219 "type": "array",
220 "items": {
221 "description": "A query of this Set, by name, or with its own settings.",
222 "oneOf": [
223 {"type": "string"},
224 {
225 "type": "object",
226 "description": "A query with settings.",
227 "properties": {
228 "query": {"type": "string", "description": "The query's name."},
229 "tab": {"type": "boolean", "description": "Whether the query makes a tab."},
230 "tab_name": {"type": "string", "description": "The tab name."},
231 "on_empty": {"enum": ["continue", "skip", "fail"], "description": "When this query returns no rows: keep its empty tab, leave it out, or fail the run."},
232 "secret_columns": {"type": "array", "items": {"type": "string"}, "description": "Columns whose values are masked in DRE's logs, results and previews, added to the query's own."}
233 },
234 "required": ["query"],
235 "additionalProperties": true
236 }
237 ]
238 }
239 })
240}
241
242#[derive(Deserialize, JsonSchema)]
246#[schemars(rename = "output", extend("additionalProperties" = true))]
247pub struct Output {
248 #[schemars(regex(pattern = "^[A-Za-z_][A-Za-z0-9_]*$"))]
250 pub name: Option<Loose<String>>,
251 pub format: Option<Loose<String>>,
253 #[schemars(length(min = 1))]
255 pub queries: Option<Loose<Vec<String>>>,
256 #[serde(default)]
258 #[schemars(schema_with = "string_or_bool")]
259 pub when: Option<Loose<OneOf<String, bool>>>,
260 #[serde(default)]
262 #[schemars(schema_with = "destination_or_list")]
263 pub destination: Option<Loose<OneOf<Destination, Vec<Loose<Destination>>>>>,
264 pub template: Option<Loose<Template>>,
265 #[serde(default)]
267 #[schemars(schema_with = "for_each_or_false")]
268 pub for_each: Option<Loose<OneOf<ForEach, bool>>>,
269 pub file_name: Option<Loose<String>>,
271 pub on_empty: Option<Loose<crate::no_data::OnEmpty>>,
273 #[serde(default)]
275 #[schemars(schema_with = "extension")]
276 pub extension: Maybe<Loose<Option<OneOf<String, bool>>>>,
277 #[serde(rename = "$unknown", default)]
279 #[schemars(skip)]
280 pub options: Map<serde_json::Value>,
281}
282
283#[derive(Deserialize, JsonSchema)]
285#[schemars(rename = "destination", extend("additionalProperties" = true))]
286pub struct Destination {
287 #[schemars(required)]
289 pub profile: Option<Loose<String>>,
290 pub path: Option<Loose<String>>,
292 #[serde(default)]
294 #[schemars(schema_with = "attach")]
295 pub attach: Option<Loose<OneOf<String, Vec<String>>>>,
296 pub hold: Option<Loose<bool>>,
298 #[schemars(regex(pattern = "^\\s*[0-9_]+\\s*/\\s*[A-Za-z]+\\s*$"))]
300 pub rate: Option<Loose<String>>,
301 #[schemars(range(min = 1))]
303 pub burst: Option<Loose<u64>>,
304 #[serde(default)]
306 #[schemars(schema_with = "zip_schema")]
307 pub zip: Option<Loose<OneOf<bool, ZipConfig>>>,
308 #[serde(rename = "$unknown", default)]
310 #[schemars(skip)]
311 pub options: Map<serde_json::Value>,
312}
313
314#[derive(Deserialize)]
316pub struct ZipConfig {
317 pub password: Option<Loose<String>>,
318 pub encryption: Option<Loose<crate::zip_pack::Encryption>>,
319 #[serde(rename = "$unknown", default)]
320 pub unknown: UnknownKeys,
321}
322
323fn zip_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
324 schemars::json_schema!({
325 "oneOf": [
326 {"type": "boolean", "description": "`true`: zip the files, with no password."},
327 {
328 "type": "object",
329 "description": "Zip the files with a password.",
330 "properties": {
331 "password": {"type": "string", "description": "The zip's password. Jinja: `env_var()`, `var()`, `each.*`. An empty password is an error."},
332 "encryption": {"enum": ["aes256", "zipcrypto"], "description": "`aes256` (default): strong; opens in 7-Zip, WinRAR, Keka. `zipcrypto`: weak, but opens in Windows Explorer and macOS Archive Utility."}
333 },
334 "additionalProperties": false
335 }
336 ]
337 })
338}
339
340#[derive(Deserialize, JsonSchema)]
342#[schemars(rename = "for_each", deny_unknown_fields)]
343pub struct ForEach {
344 #[schemars(required)]
346 pub query: Option<Loose<String>>,
347 #[schemars(required)]
349 #[serde(default)]
350 #[schemars(schema_with = "string_or_strings")]
351 pub key: Option<Loose<OneOf<String, Vec<String>>>>,
352 #[schemars(range(min = 1))]
354 pub concurrency: Option<Loose<u64>>,
355 pub on_row_error: Option<Loose<crate::project::RowErrors>>,
357 pub on_orphan_rows: Option<Loose<crate::project::OrphanRows>>,
359 #[schemars(range(min = 1))]
361 pub max_population_rows: Option<Loose<u64>>,
362 pub keep_results: Option<Loose<bool>>,
364 pub mask_row_values: Option<Loose<bool>>,
366}
367
368fn string_or_strings(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
369 schemars::json_schema!({
370 "oneOf": [
371 {"type": "string"},
372 {"type": "array", "items": {"type": "string"}, "minItems": 1}
373 ]
374 })
375}
376
377fn for_each_or_false(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
378 let f = g.subschema_for::<ForEach>();
379 schemars::json_schema!({"oneOf": [f, {"const": false}]})
380}
381
382#[derive(Deserialize, JsonSchema)]
384#[schemars(rename = "template", deny_unknown_fields)]
385pub struct Template {
386 #[schemars(required)]
388 pub file: Option<Loose<String>>,
389 pub bindings: Option<Loose<Vec<Loose<TemplateBinding>>>>,
391}
392
393#[derive(Deserialize, JsonSchema)]
395#[schemars(deny_unknown_fields, inline)]
396pub struct TemplateBinding {
397 #[schemars(required)]
399 pub sheet: Option<Loose<String>>,
400 pub query: Option<Loose<String>>,
402 #[schemars(range(min = 1))]
404 pub result_index: Option<Loose<u64>>,
405 #[schemars(regex(pattern = "^[A-Za-z]{1,3}[0-9]+$"))]
407 pub anchor: Option<Loose<String>>,
408 pub header: Option<Loose<bool>>,
410 pub columns: Option<Loose<Vec<String>>>,
412 #[schemars(regex(pattern = "^[A-Za-z]{1,3}[0-9]+$"))]
414 pub cell: Option<Loose<String>>,
415 pub value: Option<Loose<String>>,
417 pub column: Option<Loose<String>>,
419 #[serde(rename = "$unknown", default)]
420 #[schemars(skip)]
421 pub unknown: UnknownKeys,
422}
423
424fn string_or_bool(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
425 schemars::json_schema!({"type": ["string", "boolean"]})
426}
427
428fn extension(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
429 schemars::json_schema!({"type": ["string", "boolean", "null"], "x-dre-null": true})
431}
432
433fn destination_or_list(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
434 let destination = g.subschema_for::<Destination>();
435 schemars::json_schema!({
436 "oneOf": [destination, {"type": "array", "minItems": 1, "items": destination}]
437 })
438}
439
440fn attach(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
441 schemars::json_schema!({
442 "oneOf": [
443 {"type": "string"},
444 {"type": "array", "items": {"type": "string"}, "minItems": 1}
445 ]
446 })
447}
448
449fn queries(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
450 let entry = g.subschema_for::<QueryEntry>();
451 schemars::json_schema!({
452 "type": "array",
453 "items": {
454 "oneOf": [
455 {"type": "string", "description": "The name of a `.sql` file under `reports/`, without extension."},
456 entry
457 ]
458 }
459 })
460}