optify/
provider.rs

1use config::{self};
2use std::collections::HashMap;
3
4use crate::schema::metadata::OptionsMetadata;
5
6// Replicating https://github.com/juharris/dotnet-OptionsProvider/blob/main/src/OptionsProvider/OptionsProvider/IOptionsProvider.cs
7// and https://github.com/juharris/dotnet-OptionsProvider/blob/main/src/OptionsProvider/OptionsProvider/OptionsProviderWithDefaults.cs
8
9// We won't truly use files at runtime, we're just using fake files that are backed by strings because that's easy to use with the `config` library.
10pub(crate) type SourceValue = config::File<config::FileSourceString, config::FileFormat>;
11
12pub(crate) type Aliases = HashMap<unicase::UniCase<String>, String>;
13pub(crate) type Features = HashMap<String, OptionsMetadata>;
14pub(crate) type Sources = HashMap<String, SourceValue>;
15
16pub struct GetOptionsPreferences {
17    pub skip_feature_name_conversion: bool,
18}
19
20pub struct CacheOptions {}
21
22/// ⚠️ Development in progress ⚠️\
23/// Not truly considered public and mainly available to support bindings for other languages.
24pub struct OptionsProvider {
25    aliases: Aliases,
26    features: Features,
27    sources: Sources,
28}
29
30impl OptionsProvider {
31    pub(crate) fn new(aliases: &Aliases, features: &Features, sources: &Sources) -> Self {
32        OptionsProvider {
33            aliases: aliases.clone(),
34            features: features.clone(),
35            sources: sources.clone(),
36        }
37    }
38
39    pub fn get_all_options(
40        &self,
41        feature_names: &Vec<String>,
42        cache_options: &Option<CacheOptions>,
43        preferences: &Option<GetOptionsPreferences>,
44    ) -> Result<serde_json::Value, String> {
45        let config = self._get_entire_config(feature_names, cache_options, preferences)?;
46
47        match config {
48            Ok(cfg) => match cfg.try_deserialize() {
49                Ok(value) => Ok(value),
50                Err(e) => Err(e.to_string()),
51            },
52            Err(e) => Err(e.to_string()),
53        }
54    }
55
56    // Map an alias or canonical feature name (perhaps derived from a file name) to a canonical feature name.
57    // Canonical feature names map to themselves.
58    //
59    // @param feature_name The name of an alias or a feature.
60    // @return The canonical feature name.
61    pub fn get_canonical_feature_name(&self, feature_name: &str) -> Result<&String, String> {
62        // Canonical feature names are also included as keys in the aliases map.
63        let feature_name = unicase::UniCase::new(feature_name.to_owned());
64        match self.aliases.get(&feature_name) {
65            Some(canonical_name) => Ok(canonical_name),
66            None => Err(format!(
67                "The given feature {:?} was not found.",
68                feature_name
69            )),
70        }
71    }
72
73    pub fn get_feature_metadata(&self, canonical_feature_name: &str) -> Option<&OptionsMetadata> {
74        self.features.get(canonical_feature_name)
75    }
76
77    pub fn get_features(&self) -> Vec<String> {
78        self.sources.keys().map(|s| s.to_owned()).collect()
79    }
80
81    pub fn get_features_with_metadata(&self) -> &Features {
82        &self.features
83    }
84
85    pub fn get_options(
86        &self,
87        key: &str,
88        feature_names: &Vec<String>,
89    ) -> Result<serde_json::Value, String> {
90        self.get_options_with_preferences(key, feature_names, &None, &None)
91    }
92
93    pub fn get_options_with_preferences(
94        &self,
95        key: &str,
96        feature_names: &Vec<String>,
97        cache_options: &Option<CacheOptions>,
98        preferences: &Option<GetOptionsPreferences>,
99    ) -> Result<serde_json::Value, String> {
100        let config = self._get_entire_config(feature_names, cache_options, preferences)?;
101
102        match config {
103            Ok(cfg) => match cfg.get(key) {
104                Ok(value) => Ok(value),
105                Err(e) => Err(e.to_string()),
106            },
107            Err(e) => Err(e.to_string()),
108        }
109    }
110
111    fn _get_entire_config(
112        &self,
113        feature_names: &Vec<String>,
114        cache_options: &Option<CacheOptions>,
115        preferences: &Option<GetOptionsPreferences>,
116    ) -> Result<Result<config::Config, config::ConfigError>, String> {
117        if let Some(_cache_options) = cache_options {
118            return Err("Caching is not supported yet.".to_owned());
119        };
120        let mut config_builder = config::Config::builder();
121        let mut skip_feature_name_conversion = false;
122        if let Some(_preferences) = preferences {
123            skip_feature_name_conversion = _preferences.skip_feature_name_conversion;
124        }
125        for feature_name in feature_names {
126            // Check for an alias.
127            // Canonical feature names are also included as keys in the aliases map.
128            let mut canonical_feature_name = feature_name;
129            if !skip_feature_name_conversion {
130                canonical_feature_name = self.get_canonical_feature_name(feature_name)?;
131            }
132
133            let source = match self.sources.get(canonical_feature_name) {
134                Some(src) => src,
135                // Should not happen.
136                // All canonical feature names are included as keys in the sources map.
137                // It could happen in the future if we allow aliases to be added directly, but we should try to validate them when the provider is built.
138                None => {
139                    return Err(format!(
140                        "Feature name {:?} was not found.",
141                        canonical_feature_name
142                    ))
143                }
144            };
145            config_builder = config_builder.add_source(source.clone());
146        }
147        Ok(config_builder.build())
148    }
149}