projvar/sources/
mod.rs

1// SPDX-FileCopyrightText: 2021 - 2024 Robin Vobruba <hoijui.quaero@gmail.com>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5pub mod bitbucket_ci;
6pub mod deriver;
7pub mod env;
8pub mod fs;
9pub mod git;
10pub mod github_ci;
11pub mod gitlab_ci;
12pub mod jenkins_ci;
13pub mod selector;
14pub mod travis_ci;
15
16use std::path::Path;
17use std::sync::LazyLock;
18
19use cli_utils::{BoxError, BoxResult};
20use thiserror::Error;
21
22use crate::environment::Environment;
23use crate::var::{Confidence, Key, C_HIGH};
24use crate::{cleanup, std_error, tools, validator, value_conversions};
25
26#[derive(PartialEq, Eq, PartialOrd, Ord)]
27pub enum Hierarchy {
28    Low,
29    Middle,
30    High,
31    Higher,
32    EvenHigher,
33    Top,
34}
35
36static NO_PROPS: LazyLock<Vec<String>> = LazyLock::new(Vec::<String>::new);
37
38/// This enumerates all possible errors returned by this module.
39#[derive(Error, Debug)]
40pub enum Error {
41    #[error("The value '{low_level_value}' - fetched from the underlying source - was bad: {msg}")]
42    BadLowLevelValue {
43        msg: String,
44        low_level_value: String,
45    },
46
47    /// Represents all other cases of `std::io::Error`.
48    #[error(transparent)]
49    ConversionError(#[from] value_conversions::Error),
50
51    /// Represents all other cases of `std::io::Error`.
52    #[error(transparent)]
53    IO(#[from] std::io::Error),
54
55    /// Represents all other cases of `std::io::Error`.
56    #[error(transparent)]
57    Git(#[from] tools::git::Error),
58
59    /// Represents all other cases of `std_error::Error`.
60    #[error(transparent)]
61    Std(#[from] std_error::Error),
62
63    /// Represents all other cases of `std::error::Error`.
64    #[error(transparent)]
65    Other(#[from] BoxError),
66}
67
68pub type ConfVal = (Confidence, String);
69pub type RetrieveRes = BoxResult<Option<ConfVal>>;
70
71pub trait VarSource {
72    /// Indicates whether this source of variables is usable.
73    /// It might not be usable if the underlying data-source (e.g. a file) does not exist,
74    /// or is not reachable (e.g. a web URL).
75    fn is_usable(&self, environment: &mut Environment) -> bool;
76
77    /// Used to evaluate whether we preffer this sources values
78    /// over the ones of an other.
79    /// This is used for sorting.
80    fn hierarchy(&self) -> Hierarchy;
81
82    /// The name of this type.
83    /// This is used for display and sorting.
84    fn type_name(&self) -> &'static str;
85
86    /// The properties (usually parameters to `Self::new`)
87    /// of the particular instance of an object of this trait.
88    /// This is used for display and sorting.
89    fn properties(&self) -> &Vec<String>;
90
91    /// As I failed to implement `fmt::Display` for all implementing structs
92    /// in one impl, I took this road, which works for our case.
93    fn display(&self) -> String {
94        format!("{}{:?}", self.type_name(), self.properties())
95    }
96
97    /// Tries to retrieve the value of a single `key`.
98    ///
99    /// # Errors
100    ///
101    /// If the underlying data-source (e.g. a file) does not exist,
102    /// or is not reachable (e.g. a web URL),
103    /// or innumerable other kinds of problems,
104    /// depending on the kind of the source.
105    fn retrieve(&self, environment: &mut Environment, key: Key) -> RetrieveRes;
106
107    /// Uses an already found build-tag as the version field,
108    /// if available.
109    ///
110    /// # Errors
111    ///
112    /// See [`Self::retrieve`].
113    fn version_from_build_tag(&self, environment: &mut Environment, key: Key) -> RetrieveRes {
114        assert!(matches!(key, Key::Version));
115        Ok(self
116            .retrieve(environment, Key::BuildTag)?
117            .map(|conf_val| cleanup::conf_version(environment, conf_val))
118            .filter(|conf_val| {
119                if let Ok(validity) = validator::get(key)(environment, &conf_val.1) {
120                    validity.is_good()
121                } else {
122                    false
123                }
124            }))
125    }
126}
127
128#[must_use]
129pub fn var(
130    environment: &Environment,
131    key: &str,
132    confidence: Confidence,
133) -> Option<(Confidence, String)> {
134    environment
135        .vars
136        .get(key)
137        .map(|val| (confidence, val.clone()))
138}
139
140fn ref_ok_or_err<'t>(refr: &str, part: Option<&'t str>) -> Result<&'t str, Error> {
141    part.ok_or_else(|| Error::BadLowLevelValue {
142        msg: "Invalid git reference, should be 'refs/<TYPE>/<NAME>'".to_owned(),
143        low_level_value: refr.to_owned(),
144    })
145}
146
147fn ref_extract_name_if_type_matches(refr: &str, required_ref_type: &str) -> RetrieveRes {
148    let mut parts = refr.split('/');
149    let extracted_ref_type = ref_ok_or_err(refr, parts.nth(1))?;
150    Ok(if extracted_ref_type == required_ref_type {
151        // it *is* a branch
152        let branch_name = ref_ok_or_err(refr, parts.next())?;
153        Some((C_HIGH, branch_name.to_owned()))
154    } else {
155        None
156    })
157}
158
159/// Given a git reference, returns the branch name,
160/// if `refr` reffers to a branch; None otherwise.
161/// `refr` references should look like:
162/// * "refs/tags/v1.2.3"
163/// * "refs/heads/master"
164/// * "refs/pull/:prNumber/merge"
165///
166/// # Errors
167///
168/// If the given ref is ill-formatted, meaning it does not split
169/// into at least 3 parts with the '/' separator)
170pub fn ref_extract_branch(refr: &str) -> RetrieveRes {
171    ref_extract_name_if_type_matches(refr, "heads")
172}
173
174/// Given a git reference, returns the tag name,
175/// if it reffers to a tag; None otherwise.
176/// `refr` references should look like:
177/// * "refs/tags/v1.2.3"
178/// * "refs/heads/master"
179/// * "refs/pull/:prNumber/merge"
180///
181/// # Errors
182///
183/// If the given ref is ill-formatted, meaning it does not split
184/// into at least 3 parts with the '/' separator)
185pub fn ref_extract_tag(refr: &str) -> RetrieveRes {
186    ref_extract_name_if_type_matches(refr, "tags")
187}
188
189fn is_git_repo_root(repo_path: Option<&Path>) -> bool {
190    tools::git::Repo::try_from(repo_path).is_ok()
191}
192
193#[must_use]
194pub fn default_list(repo_path: &Path) -> Vec<Box<dyn VarSource>> {
195    let mut sources: Vec<Box<dyn VarSource>> = vec![];
196    if is_git_repo_root(Some(repo_path)) {
197        sources.push(Box::new(git::VarSource {}));
198    }
199    sources.push(Box::new(fs::VarSource {}));
200    sources.push(Box::new(bitbucket_ci::VarSource {}));
201    sources.push(Box::new(github_ci::VarSource {}));
202    sources.push(Box::new(gitlab_ci::VarSource {}));
203    sources.push(Box::new(jenkins_ci::VarSource {}));
204    sources.push(Box::new(travis_ci::VarSource {}));
205    sources.push(Box::new(env::VarSource {}));
206    sources.push(Box::new(selector::VarSource {}));
207    sources.push(Box::new(deriver::VarSource {}));
208    // NOTE We add the deriver a second time,
209    //      so it may derive from values created in the first run.
210    sources.push(Box::new(deriver::VarSource {}));
211    if log::log_enabled!(log::Level::Trace) {
212        for source in &sources {
213            log::trace!("Registered source {}.", source.display());
214        }
215    }
216    sources
217}