nimbus_cli/output/
features.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::path::Path;
6
7use anyhow::Result;
8use nimbus_fml::intermediate_representation::FeatureManifest;
9use serde_json::Value;
10
11use crate::{
12    sources::{ExperimentSource, ManifestSource},
13    value_utils::{self, CliUtils},
14};
15
16impl ManifestSource {
17    pub(crate) fn print_defaults<P>(
18        &self,
19        feature_id: Option<&String>,
20        output: Option<P>,
21    ) -> Result<bool>
22    where
23        P: AsRef<Path>,
24    {
25        let manifest: FeatureManifest = self.try_into()?;
26        let json = self.get_defaults_json(&manifest, feature_id)?;
27        value_utils::write_to_file_or_print(output, &json)?;
28        Ok(true)
29    }
30
31    fn get_defaults_json(
32        &self,
33        fm: &FeatureManifest,
34        feature_id: Option<&String>,
35    ) -> Result<Value> {
36        Ok(match feature_id {
37            Some(id) => {
38                let (_, feature) = fm.find_feature(id).ok_or_else(|| {
39                    anyhow::Error::msg(format!("Feature '{id}' does not exist in this manifest"))
40                })?;
41                feature.default_json()
42            }
43            _ => fm.default_json(),
44        })
45    }
46}
47
48impl ExperimentSource {
49    #[allow(clippy::too_many_arguments)]
50    pub(crate) fn print_features<P>(
51        &self,
52        branch: &String,
53        manifest_source: &ManifestSource,
54        feature_id: Option<&String>,
55        validate: bool,
56        multi: bool,
57        output: Option<P>,
58    ) -> Result<bool>
59    where
60        P: AsRef<Path>,
61    {
62        let json = self.get_features_json(manifest_source, feature_id, branch, validate, multi)?;
63        value_utils::write_to_file_or_print(output, &json)?;
64        Ok(true)
65    }
66
67    fn get_features_json(
68        &self,
69        manifest_source: &ManifestSource,
70        feature_id: Option<&String>,
71        branch: &String,
72        validate: bool,
73        multi: bool,
74    ) -> Result<Value> {
75        let value = self.try_into()?;
76
77        // Find the named branch.
78        let branches = value_utils::try_find_branches_from_experiment(&value)?;
79        let b = branches
80            .iter()
81            .find(|b| b.get_str("slug").unwrap() == branch)
82            .ok_or_else(|| anyhow::format_err!("Branch '{branch}' does not exist"))?;
83
84        // Find the features for this branch: there may be more than one.
85        let feature_values = value_utils::try_find_features_from_branch(b)?;
86
87        // Now extract the relevant features out of the branches.
88        let mut result = serde_json::value::Map::new();
89        for f in feature_values {
90            let id = f.get_str("featureId")?;
91            let value = f
92                .get("value")
93                .ok_or_else(|| anyhow::format_err!("Branch {branch} feature {id} has no value"))?;
94            match feature_id {
95                None => {
96                    // If the user hasn't specified a feature, then just add it.
97                    result.insert(id.to_string(), value.clone());
98                }
99                Some(feature_id) if feature_id == id => {
100                    // If the user has specified a feature, and this is it, then also add it.
101                    result.insert(id.to_string(), value.clone());
102                }
103                // Otherwise, the user has specified a feature, and this wasn't it.
104                _ => continue,
105            }
106        }
107
108        // By now: we have all the features that we need, and no more.
109
110        // If validating, then we should merge with the defaults from the manifest.
111        // If not, then nothing more is needed to be done: we're delivering the partial feature configuration.
112        if validate {
113            let fm: FeatureManifest = manifest_source.try_into()?;
114            let mut new = serde_json::value::Map::new();
115            for (id, value) in result {
116                let def = fm.validate_feature_config(&id, value)?;
117                new.insert(id.to_owned(), def.default_json());
118            }
119            result = new;
120        }
121
122        Ok(if !multi && result.len() == 1 {
123            // By default, if only a single feature is being displayed,
124            // we can output just the feature config.
125            match (result.values().find(|_| true), feature_id) {
126                (Some(v), _) => v.to_owned(),
127                (_, Some(id)) => anyhow::bail!(
128                    "The '{id}' feature is not involved in '{branch}' branch of '{self}'"
129                ),
130                (_, _) => {
131                    anyhow::bail!("No features available in '{branch}' branch of '{self}'")
132                }
133            }
134        } else {
135            // Otherwise, we can output the `{ featureId: featureValue }` in its entirety.
136            Value::Object(result)
137        })
138    }
139}