Skip to main content

dre_core/config/
schedule.rs

1//! The shapes of `sets.yml` (Sets shared by reports), `timings.yml` (named timings) and
2//! `schedules.yml` (named schedules).
3//!
4//! A timing block (`cron`, `every`, `rrule` and their `starting`, `at`, `except`, `also`) is
5//! checked by [`crate::schedule::validate_block`]; here it's read as written.
6
7use schemars::JsonSchema;
8use serde::Deserialize;
9
10use super::de::{Located, Loose, Map, UnknownKeys};
11
12type JsonMap = serde_json::Map<String, serde_json::Value>;
13/// A timing block's key, as written, with where (to keep the file's order).
14type Raw = Option<Located<serde_json::Value>>;
15
16/// Set names mapped to their definitions.
17#[derive(Deserialize, JsonSchema)]
18#[serde(transparent)]
19#[schemars(title = "DRE Sets")]
20pub struct SetsFile(pub Map<Loose<Option<SetConfig>>>);
21
22/// One Set.
23#[derive(Deserialize, JsonSchema)]
24#[schemars(rename = "set", deny_unknown_fields)]
25pub struct SetConfig {
26    /// The connection a Binding with this Set runs on. May use Jinja with `var()`, `env_var()`, `run.*` and `target.name`.
27    pub profile: Option<Loose<String>>,
28    /// Variables, read in SQL and YAML with `var('name')`. Values can be strings, numbers, booleans, lists or maps.
29    #[serde(default)]
30    #[schemars(schema_with = "any_map")]
31    pub vars: Option<Loose<JsonMap>>,
32    /// The locale for this Set's number filters (`fr-FR`), above the report's.
33    pub locale: Option<Loose<String>>,
34}
35
36/// The timings file, `timings.yml`: timing names mapped to timings that any schedule can use with `timing: <name>`.
37#[derive(Deserialize, JsonSchema)]
38#[serde(transparent)]
39#[schemars(title = "DRE timings", extend("propertyNames" = {"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"}))]
40pub struct TimingsFile(pub Map<Loose<TimingConfig>>);
41
42/// One named timing: when it fires, in which timezone, and the dates it skips or adds.
43#[derive(Deserialize, JsonSchema)]
44#[schemars(rename = "timing", deny_unknown_fields)]
45pub struct TimingConfig {
46    /// A cron expression (5 fields, or a macro such as `@daily`). A timing needs exactly one of `cron`, `every` or `rrule`.
47    #[serde(default)]
48    #[schemars(with = "String")]
49    pub cron: Raw,
50    /// Every N days, weeks or months, with exactly one unit, e.g. `{days: 3}`.
51    #[serde(default)]
52    #[schemars(with = "Every")]
53    pub every: Raw,
54    /// An iCalendar recurrence rule, e.g. `FREQ=MONTHLY;BYDAY=2TU`. Use `starting` for a rule with `INTERVAL` above 1 or `COUNT`, and `at` for its time of day.
55    #[serde(default)]
56    #[schemars(with = "String")]
57    pub rrule: Raw,
58    /// With `every` or `rrule`: the first date, `YYYY-MM-DD`. `every` needs it, and so does a rule with `INTERVAL` above 1, a `COUNT`, or a day it takes from its start.
59    #[serde(default)]
60    #[schemars(with = "String", regex(pattern = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"))]
61    pub starting: Raw,
62    /// With `every` or `rrule`: the time of day, `HH:MM` (24-hour), unless the rule sets `BYHOUR`/`BYMINUTE`. Default: 00:00.
63    #[serde(default)]
64    #[schemars(with = "String", regex(pattern = "^[0-9]{2}:[0-9]{2}$"))]
65    pub at: Raw,
66    /// Dates (`YYYY-MM-DD`, in its timezone) it doesn't fire on, e.g. holidays.
67    #[serde(default)]
68    #[schemars(with = "Vec<Date>")]
69    pub except: Raw,
70    /// Extra dates (`YYYY-MM-DD`) it fires on, at its time of day in its timezone. Needs a timing that fires at one time of day.
71    #[serde(default)]
72    #[schemars(with = "Vec<Date>")]
73    pub also: Raw,
74    /// The timezone it fires in, an IANA name such as `Australia/Sydney`; schedules using the timing also run in it. Default: the project's `timezone:`, then UTC.
75    pub timezone: Option<Loose<String>>,
76    #[serde(rename = "$unknown", default)]
77    #[schemars(skip)]
78    pub unknown: UnknownKeys,
79}
80
81impl TimingConfig {
82    /// The timing block as written, for [`crate::schedule::validate_block`].
83    pub fn block(&self) -> JsonMap {
84        block([
85            ("cron", &self.cron),
86            ("every", &self.every),
87            ("rrule", &self.rrule),
88            ("starting", &self.starting),
89            ("at", &self.at),
90            ("except", &self.except),
91            ("also", &self.also),
92        ])
93    }
94}
95
96/// The schedules file, `schedules.yml`: a list of named schedules.
97#[derive(Deserialize, JsonSchema)]
98#[serde(transparent)]
99#[schemars(title = "DRE schedules")]
100pub struct SchedulesFile(pub Vec<Located<Loose<ScheduleConfig>>>);
101
102/// One named schedule. DRE doesn't fire it; your orchestrator runs `dre run --schedule <name>`.
103#[derive(Deserialize, JsonSchema)]
104#[schemars(rename = "schedule", deny_unknown_fields)]
105pub struct ScheduleConfig {
106    /// The schedule's name: letters, digits and `_`, not starting with a digit. Unique across the project.
107    #[schemars(required, regex(pattern = "^[A-Za-z_][A-Za-z0-9_]*$"))]
108    pub name: Option<Loose<String>>,
109    /// The report to run. Use `report` or `select`, not both.
110    pub report: Option<Loose<String>>,
111    /// The Set of `report` to run. Only with `report`.
112    pub set: Option<Loose<String>>,
113    /// A selector for the reports to run, e.g. `tag:regulatory`. Use `report` or `select`, not both.
114    pub select: Option<Loose<String>>,
115    /// Variables for the run: above the report's own and below `--var`.
116    #[serde(default)]
117    #[schemars(schema_with = "any_map")]
118    pub vars: Option<Loose<JsonMap>>,
119    /// The name of a timing in `timings.yml` to fire on. Use `timing` or one of `cron`, `every`, `rrule`; with `timing`, the schedule sets none of the timing's keys (`timezone`, `starting`, `at`, `except`, `also`).
120    #[schemars(regex(pattern = "^[A-Za-z_][A-Za-z0-9_]*$"))]
121    pub timing: Option<Loose<String>>,
122    /// The timezone it fires in, and the one `run.date` and `run.now` use, an IANA name such as `Australia/Sydney`. Default: the project's `timezone:` for firing (the report's for the run), then UTC.
123    pub timezone: Option<Located<Loose<String>>>,
124    /// `false` pauses the schedule: it keeps its name and settings but `dre schedule ls` lists no occurrences for it. Default: `true`.
125    pub enabled: Option<Loose<bool>>,
126    /// A cron expression (5 fields, or a macro such as `@daily`). A schedule needs exactly one of `timing`, `cron`, `every` or `rrule`.
127    #[serde(default)]
128    #[schemars(with = "String")]
129    pub cron: Raw,
130    /// Every N days, weeks or months, with exactly one unit, e.g. `{days: 3}`.
131    #[serde(default)]
132    #[schemars(with = "Every")]
133    pub every: Raw,
134    /// An iCalendar recurrence rule, e.g. `FREQ=MONTHLY;BYDAY=2TU`. Use `starting` for a rule with `INTERVAL` above 1 or `COUNT`, and `at` for its time of day.
135    #[serde(default)]
136    #[schemars(with = "String")]
137    pub rrule: Raw,
138    /// With `every` or `rrule`: the first date, `YYYY-MM-DD`. `every` needs it, and so does a rule with `INTERVAL` above 1, a `COUNT`, or a day it takes from its start.
139    #[serde(default)]
140    #[schemars(with = "String", regex(pattern = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"))]
141    pub starting: Raw,
142    /// With `every` or `rrule`: the time of day, `HH:MM` (24-hour), unless the rule sets `BYHOUR`/`BYMINUTE`. Default: 00:00.
143    #[serde(default)]
144    #[schemars(with = "String", regex(pattern = "^[0-9]{2}:[0-9]{2}$"))]
145    pub at: Raw,
146    /// Dates (`YYYY-MM-DD`, in its timezone) it doesn't fire on, e.g. holidays.
147    #[serde(default)]
148    #[schemars(with = "Vec<Date>")]
149    pub except: Raw,
150    /// Extra dates (`YYYY-MM-DD`) it fires on, at its time of day in its timezone. Needs a timing that fires at one time of day.
151    #[serde(default)]
152    #[schemars(with = "Vec<Date>")]
153    pub also: Raw,
154    #[serde(rename = "$unknown", default)]
155    #[schemars(skip)]
156    pub unknown: UnknownKeys,
157}
158
159impl ScheduleConfig {
160    /// The keys it sets that a timing would set (its block and `timezone`), in the file's order.
161    pub fn timing_keys(&self) -> Vec<&'static str> {
162        let mut keys: Vec<(&'static str, (usize, usize))> = [
163            ("cron", &self.cron),
164            ("every", &self.every),
165            ("rrule", &self.rrule),
166            ("starting", &self.starting),
167            ("at", &self.at),
168            ("except", &self.except),
169            ("also", &self.also),
170        ]
171        .into_iter()
172        .filter_map(|(k, v)| v.as_ref().map(|v| (k, (v.line, v.column))))
173        .collect();
174        if let Some(tz) = &self.timezone {
175            keys.push(("timezone", (tz.line, tz.column)));
176        }
177        keys.sort_by_key(|(_, at)| *at);
178        keys.into_iter().map(|(k, _)| k).collect()
179    }
180
181    /// The schedule's own timing block, for [`crate::schedule::validate_block`].
182    pub fn block(&self) -> JsonMap {
183        block([
184            ("cron", &self.cron),
185            ("every", &self.every),
186            ("rrule", &self.rrule),
187            ("starting", &self.starting),
188            ("at", &self.at),
189            ("except", &self.except),
190            ("also", &self.also),
191        ])
192    }
193}
194
195/// The keys that are written, in the file's order.
196fn block<const N: usize>(keys: [(&str, &Raw); N]) -> JsonMap {
197    let mut written: Vec<(&str, &Located<serde_json::Value>)> = keys
198        .into_iter()
199        .filter_map(|(k, v)| Some((k, v.as_ref()?)))
200        .collect();
201    written.sort_by_key(|(_, v)| (v.line, v.column));
202    written
203        .into_iter()
204        .map(|(k, v)| (k.to_string(), v.value.clone()))
205        .collect()
206}
207
208/// Every N days, weeks or months, with exactly one unit.
209#[derive(JsonSchema)]
210#[schemars(deny_unknown_fields, inline, extend("minProperties" = 1, "maxProperties" = 1))]
211#[allow(dead_code)]
212struct Every {
213    /// Every this many days.
214    #[schemars(range(min = 1))]
215    days: Option<u32>,
216    /// Every this many weeks.
217    #[schemars(range(min = 1))]
218    weeks: Option<u32>,
219    /// Every this many months.
220    #[schemars(range(min = 1))]
221    months: Option<u32>,
222}
223
224// A date, `YYYY-MM-DD` (no description of its own in the schema).
225#[derive(JsonSchema)]
226#[schemars(inline)]
227#[allow(dead_code)]
228struct Date(#[schemars(regex(pattern = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"))] String);
229
230fn any_map(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
231    schemars::json_schema!({"type": "object", "additionalProperties": true})
232}