Skip to main content

gluex_ccdb/
context.rs

1use chrono::{DateTime, Utc};
2use gluex_core::{
3    constants::{MAX_RUN_NUMBER, MIN_RUN_NUMBER},
4    parsers::parse_timestamp,
5    run_periods::{RESTVersionSelection, RunPeriod},
6    RunNumber,
7};
8use std::{ops::Bound, str::FromStr};
9
10use crate::{CCDBError, CCDBResult};
11
12/// Absolute CCDB path wrapper that enforces formatting rules.
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct NamePath(pub String);
15impl FromStr for NamePath {
16    type Err = CCDBError;
17
18    fn from_str(s: &str) -> Result<Self, Self::Err> {
19        if !s.starts_with('/') {
20            return Err(CCDBError::NotAbsolutePath(s.to_string()));
21        }
22        if !s
23            .chars()
24            .all(|c| c.is_ascii_alphanumeric() || c == '/' || c == '_' || c == '-')
25        {
26            return Err(CCDBError::IllegalCharacter(s.to_string()));
27        }
28        Ok(Self(s.to_string()))
29    }
30}
31impl NamePath {
32    /// Returns the absolute path string (always begins with `/`).
33    #[must_use]
34    pub fn full_path(&self) -> &str {
35        &self.0
36    }
37    /// Returns the final component of the path (table or directory name).
38    #[must_use]
39    pub fn name(&self) -> &str {
40        self.0.rsplit('/').next().unwrap_or("")
41    }
42    /// Returns the parent path, or [`None`] when this path is root.
43    #[must_use]
44    pub fn parent(&self) -> Option<NamePath> {
45        if self.is_root() {
46            return None;
47        }
48        let mut parts: Vec<&str> = self.0.split('/').collect();
49        parts.pop();
50        Some(NamePath(format!("/{}", parts.join("/"))))
51    }
52    /// True when the path corresponds to the root directory.
53    #[must_use]
54    pub fn is_root(&self) -> bool {
55        self.0 == "/"
56    }
57}
58const DEFAULT_VARIATION: &str = "default";
59const DEFAULT_RUN_NUMBER: RunNumber = 0;
60
61/// Query context describing run selection, variation, and timestamp.
62#[derive(Debug, Clone)]
63pub struct CCDBContext {
64    /// [`RunNumber`] values to consider when resolving assignments.
65    pub runs: Vec<RunNumber>,
66    /// Variation (branch) to resolve within CCDB.
67    pub variation: String,
68    /// [`DateTime`] in the [`Utc`] timezone used to select the newest constants not newer than this time.
69    pub timestamp: DateTime<Utc>,
70}
71impl Default for CCDBContext {
72    fn default() -> Self {
73        Self {
74            runs: vec![DEFAULT_RUN_NUMBER],
75            variation: DEFAULT_VARIATION.to_string(),
76            timestamp: Utc::now(),
77        }
78    }
79}
80impl CCDBContext {
81    /// Builds a new context with optional run, variation, and timestamp overrides.
82    #[must_use]
83    pub fn new(
84        runs: Option<Vec<RunNumber>>,
85        variation: Option<String>,
86        timestamp: Option<DateTime<Utc>>,
87    ) -> Self {
88        let mut context = Self::default();
89        if let Some(runs) = runs {
90            context.runs = runs;
91        }
92        if let Some(variation) = variation {
93            context.variation = variation;
94        }
95        if let Some(timestamp) = timestamp {
96            context.timestamp = timestamp;
97        }
98        context
99    }
100    /// Returns a context scoped to all runs associated with the given [`RunPeriod`]. Additionally,
101    /// if a REST version is provided, the timestamp will be resolved for that version.
102    ///
103    /// # Errors
104    ///
105    /// This method will return an error if the run period is not found in the [`REST_VERSION_TIMESTAMPS`] map
106    /// or if the requested REST version is not defined for the run period.
107    pub fn with_run_period(
108        mut self,
109        run_period: RunPeriod,
110        rest_version: RESTVersionSelection,
111    ) -> CCDBResult<Self> {
112        self.runs = run_period.run_range().collect();
113        self.timestamp = rest_version.resolve_timestamp(run_period)?;
114        Ok(self)
115    }
116    /// Returns a context scoped to a single run number.
117    #[must_use]
118    pub fn with_run(mut self, run: RunNumber) -> Self {
119        self.runs = vec![run.clamp(MIN_RUN_NUMBER, MAX_RUN_NUMBER)];
120        self
121    }
122    /// Replaces the run list with the provided runs.
123    #[must_use]
124    pub fn with_runs(mut self, iter: impl IntoIterator<Item = RunNumber>) -> Self {
125        self.runs = iter
126            .into_iter()
127            .map(|r| r.clamp(MIN_RUN_NUMBER, MAX_RUN_NUMBER))
128            .collect();
129        self
130    }
131    /// Replaces the run list with all runs inside the supplied range.
132    #[must_use]
133    pub fn with_run_range(mut self, run_range: impl std::ops::RangeBounds<RunNumber>) -> Self {
134        let start = match run_range.start_bound() {
135            Bound::Included(&s) => s,
136            Bound::Excluded(&s) => s.saturating_add(1),
137            Bound::Unbounded => MIN_RUN_NUMBER,
138        }
139        .max(MIN_RUN_NUMBER);
140        let end = match run_range.end_bound() {
141            Bound::Included(&e) => e,
142            Bound::Excluded(&e) => e.saturating_sub(1),
143            Bound::Unbounded => MAX_RUN_NUMBER,
144        }
145        .min(MAX_RUN_NUMBER);
146        self.runs = if start > end {
147            Vec::new()
148        } else {
149            (start..=end).collect()
150        };
151        self
152    }
153    /// Sets the variation branch for subsequent queries.
154    #[must_use]
155    pub fn with_variation(mut self, variation: &str) -> Self {
156        self.variation = variation.to_string();
157        self
158    }
159    /// Sets the timestamp for selecting assignments (query will give the most recent assignment not newer than this).
160    #[must_use]
161    pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
162        self.timestamp = timestamp;
163        self
164    }
165    /// Sets the timestamp for selecting assignments from a formatted timestamp string (query will give the most recent assignment not newer than this).
166    ///
167    /// # Errors
168    ///
169    /// This method returns a [`CCDBError`] if the timestamp is not in the format allowed by CCDB.
170    pub fn with_timestamp_string(mut self, timestamp: &str) -> CCDBResult<Self> {
171        self.timestamp = parse_timestamp(timestamp)?;
172        Ok(self)
173    }
174}
175
176/// Parsed representation of a CCDB request string, containing both the [`NamePath`] and [`CCDBContext`].
177#[derive(Debug, Clone)]
178pub struct Request {
179    /// Absolute path to the requested table.
180    pub path: NamePath,
181    /// Context describing run/variation/timestamp selection.
182    pub context: CCDBContext,
183}
184impl FromStr for Request {
185    type Err = CCDBError;
186
187    fn from_str(s: &str) -> Result<Self, Self::Err> {
188        let (path_str, rest) = s.split_once(':').map_or((s, None), |(p, r)| (p, Some(r)));
189        let path = NamePath::from_str(path_str)?;
190        let mut run: Option<RunNumber> = None;
191        let mut variation: Option<String> = None;
192        let mut timestamp: Option<DateTime<Utc>> = None;
193        if let Some(rest) = rest {
194            let mut parts: Vec<&str> = rest.splitn(3, ':').collect();
195            while parts.len() < 3 {
196                parts.push("");
197            }
198            let (run_s, var_s, time_s) = (parts[0], parts[1], parts[2]);
199            if !run_s.is_empty() {
200                run = Some(
201                    run_s
202                        .parse::<RunNumber>()
203                        .map_err(|_| CCDBError::InvalidRunNumberError(run_s.to_string()))?,
204                );
205            }
206            if !var_s.is_empty() {
207                variation = Some(var_s.to_string());
208            }
209            if !time_s.is_empty() {
210                timestamp = Some(parse_timestamp(time_s)?);
211            }
212        }
213        Ok(Request {
214            path,
215            context: CCDBContext::new(run.map(|r| vec![r]), variation, timestamp),
216        })
217    }
218}