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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use std::path::Path;

use anyhow::Result;
use nimbus_fml::intermediate_representation::FeatureManifest;
use serde_json::Value;

use crate::{
    sources::{ExperimentSource, ManifestSource},
    value_utils::{self, CliUtils},
};

impl ManifestSource {
    pub(crate) fn print_defaults<P>(
        &self,
        feature_id: Option<&String>,
        output: Option<P>,
    ) -> Result<bool>
    where
        P: AsRef<Path>,
    {
        let manifest: FeatureManifest = self.try_into()?;
        let json = self.get_defaults_json(&manifest, feature_id)?;
        value_utils::write_to_file_or_print(output, &json)?;
        Ok(true)
    }

    fn get_defaults_json(
        &self,
        fm: &FeatureManifest,
        feature_id: Option<&String>,
    ) -> Result<Value> {
        Ok(match feature_id {
            Some(id) => {
                let (_, feature) = fm.find_feature(id).ok_or_else(|| {
                    anyhow::Error::msg(format!("Feature '{id}' does not exist in this manifest"))
                })?;
                feature.default_json()
            }
            _ => fm.default_json(),
        })
    }
}

impl ExperimentSource {
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn print_features<P>(
        &self,
        branch: &String,
        manifest_source: &ManifestSource,
        feature_id: Option<&String>,
        validate: bool,
        multi: bool,
        output: Option<P>,
    ) -> Result<bool>
    where
        P: AsRef<Path>,
    {
        let json = self.get_features_json(manifest_source, feature_id, branch, validate, multi)?;
        value_utils::write_to_file_or_print(output, &json)?;
        Ok(true)
    }

    fn get_features_json(
        &self,
        manifest_source: &ManifestSource,
        feature_id: Option<&String>,
        branch: &String,
        validate: bool,
        multi: bool,
    ) -> Result<Value> {
        let value = self.try_into()?;

        // Find the named branch.
        let branches = value_utils::try_find_branches_from_experiment(&value)?;
        let b = branches
            .iter()
            .find(|b| b.get_str("slug").unwrap() == branch)
            .ok_or_else(|| anyhow::format_err!("Branch '{branch}' does not exist"))?;

        // Find the features for this branch: there may be more than one.
        let feature_values = value_utils::try_find_features_from_branch(b)?;

        // Now extract the relevant features out of the branches.
        let mut result = serde_json::value::Map::new();
        for f in feature_values {
            let id = f.get_str("featureId")?;
            let value = f
                .get("value")
                .ok_or_else(|| anyhow::format_err!("Branch {branch} feature {id} has no value"))?;
            match feature_id {
                None => {
                    // If the user hasn't specified a feature, then just add it.
                    result.insert(id.to_string(), value.clone());
                }
                Some(feature_id) if feature_id == id => {
                    // If the user has specified a feature, and this is it, then also add it.
                    result.insert(id.to_string(), value.clone());
                }
                // Otherwise, the user has specified a feature, and this wasn't it.
                _ => continue,
            }
        }

        // By now: we have all the features that we need, and no more.

        // If validating, then we should merge with the defaults from the manifest.
        // If not, then nothing more is needed to be done: we're delivering the partial feature configuration.
        if validate {
            let fm: FeatureManifest = manifest_source.try_into()?;
            let mut new = serde_json::value::Map::new();
            for (id, value) in result {
                let def = fm.validate_feature_config(&id, value)?;
                new.insert(id.to_owned(), def.default_json());
            }
            result = new;
        }

        Ok(if !multi && result.len() == 1 {
            // By default, if only a single feature is being displayed,
            // we can output just the feature config.
            match (result.values().find(|_| true), feature_id) {
                (Some(v), _) => v.to_owned(),
                (_, Some(id)) => anyhow::bail!(
                    "The '{id}' feature is not involved in '{branch}' branch of '{self}'"
                ),
                (_, _) => {
                    anyhow::bail!("No features available in '{branch}' branch of '{self}'")
                }
            }
        } else {
            // Otherwise, we can output the `{ featureId: featureValue }` in its entirety.
            Value::Object(result)
        })
    }
}