1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
use crate::entity::ConfigurationEntity;
use serde::de::DeserializeOwned;
use std::{collections::HashMap, fmt::Debug};
use url::Url;

pub mod closure;
#[cfg(feature = "env-loader")]
pub mod env;
#[cfg(feature = "fs")]
pub mod fs;

#[derive(Debug, thiserror::Error)]
pub enum ConfigurationLoadError {
    #[error(
        "{loader} configuration loader could not found configuration `{configuration_source}`"
    )]
    NotFound {
        loader: String,
        configuration_source: String,
    },
    #[error("{loader} configuration loader has no access to load configuration from `{configuration_source}`")]
    NoAccess {
        loader: String,
        configuration_source: String,
    },
    #[error("{loader} configuration loader reached timeout `{timeout_in_seconds}s` to load `{configuration_source}`")]
    Timeout {
        loader: String,
        configuration_source: String,
        timeout_in_seconds: usize,
    },
    #[error("{loader} configuration loader got invalid source `{configuration_source}`")]
    InvalidSource {
        loader: String,
        configuration_source: String,
        #[source]
        error: anyhow::Error,
    },
    #[error("Invalid URL `{url}`")]
    InvalidUrl {
        url: String,
        #[source]
        error: anyhow::Error,
    },
    #[error("Could not found configuration loader for scheme {scheme}")]
    UrlSchemeNotFound { scheme: String },
    #[error("{loader} configuration loader found duplicate configurations `{configuration_source}({extension_1}|{extension_2})`")]
    Duplicate {
        loader: String,
        configuration_source: String,
        extension_1: String,
        extension_2: String,
    },
    #[error("{loader} configuration loader could not {description} `{configuration_source}`")]
    Load {
        loader: String,
        configuration_source: String,
        description: String,
        #[source]
        error: anyhow::Error,
        retryable: bool,
    },
    #[error("Could not acquire lock for configuration loader with source ")]
    AcquireLock { configuration_source: String },
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}
pub trait ConfigurationLoader: Send + Sync + Debug {
    fn name(&self) -> &'static str;
    fn scheme_list(&self) -> Vec<String>;
    fn try_load(
        &self,
        source: Url,
        maybe_whitelist: Option<&[String]>,
    ) -> Result<HashMap<String, ConfigurationEntity>, ConfigurationLoadError>;
}

#[cfg(feature = "qs")]
pub fn parse_url<R: DeserializeOwned>(url: &mut Url) -> Result<R, ConfigurationLoadError> {
    serde_qs::from_str(url.query().unwrap_or_default())
        .map(|result| {
            url.set_query(None);
            result
        })
        .map_err(|error| ConfigurationLoadError::InvalidUrl {
            url: url.to_string(),
            error: error.into(),
        })
}

impl ConfigurationLoadError {
    pub fn is_retryable(&self) -> bool {
        if let Self::Load { retryable, .. } = self {
            *retryable
        } else {
            false
        }
    }
}