nimbus_fml/backends/
experimenter_manifest.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 http://mozilla.org/MPL/2.0/. */
4
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt::Display;
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    command_line::commands::GenerateExperimenterManifestCmd,
12    error::{FMLError, Result},
13    intermediate_representation::{FeatureDef, FeatureManifest, PropDef, TargetLanguage, TypeRef},
14};
15
16pub(crate) type ExperimenterManifest = BTreeMap<String, ExperimenterFeature>;
17
18#[derive(Debug, Default, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub(crate) struct ExperimenterFeature {
21    description: String,
22    has_exposure: bool,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    exposure_description: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    is_early_startup: Option<bool>,
27    variables: BTreeMap<String, ExperimenterFeatureProperty>,
28}
29
30#[derive(Debug, Default, Clone, Serialize, Deserialize)]
31pub(crate) struct ExperimenterFeatureProperty {
32    #[serde(rename = "type")]
33    property_type: String,
34    description: String,
35
36    #[serde(rename = "enum")]
37    #[serde(skip_serializing_if = "Option::is_none")]
38    variants: Option<BTreeSet<String>>,
39}
40
41impl TryFrom<FeatureManifest> for ExperimenterManifest {
42    type Error = crate::error::FMLError;
43    fn try_from(fm: FeatureManifest) -> Result<Self> {
44        fm.iter_all_feature_defs()
45            .map(|(fm, f)| Ok((f.name(), fm.create_experimenter_feature(f)?)))
46            .collect()
47    }
48}
49
50impl FeatureManifest {
51    fn create_experimenter_feature(&self, feature: &FeatureDef) -> Result<ExperimenterFeature> {
52        Ok(ExperimenterFeature {
53            description: feature.doc(),
54            has_exposure: true,
55            is_early_startup: None,
56            // TODO: Add exposure description to the IR so
57            // we can use it here if it's needed
58            exposure_description: Some("".into()),
59            variables: self.props_to_variables(&feature.props)?,
60        })
61    }
62
63    fn props_to_variables(
64        &self,
65        props: &[PropDef],
66    ) -> Result<BTreeMap<String, ExperimenterFeatureProperty>> {
67        // Ideally this would be implemented as a `TryFrom<Vec<PropDef>>`
68        // however, we need a reference to the `FeatureManifest` to get the valid
69        // variants of an enum
70        let mut map = BTreeMap::new();
71        props.iter().try_for_each(|prop| -> Result<()> {
72            let typ = ExperimentManifestPropType::from(prop.typ()).to_string();
73
74            let yaml_prop = if let TypeRef::Enum(e) = prop.typ() {
75                let enum_def = self
76                    .find_enum(&e)
77                    .ok_or(FMLError::InternalError("Found enum with no definition"))?;
78
79                let variants = enum_def
80                    .variants
81                    .iter()
82                    .map(|variant| variant.name())
83                    .collect::<BTreeSet<String>>();
84
85                ExperimenterFeatureProperty {
86                    variants: Some(variants),
87                    description: prop.doc(),
88                    property_type: typ,
89                }
90            } else {
91                ExperimenterFeatureProperty {
92                    variants: None,
93                    description: prop.doc(),
94                    property_type: typ,
95                }
96            };
97            map.insert(prop.name(), yaml_prop);
98            Ok(())
99        })?;
100        Ok(map)
101    }
102}
103
104enum ExperimentManifestPropType {
105    Json,
106    Boolean,
107    Int,
108    String,
109}
110
111impl Display for ExperimentManifestPropType {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        let s = match self {
114            ExperimentManifestPropType::Boolean => "boolean",
115            ExperimentManifestPropType::Int => "int",
116            ExperimentManifestPropType::Json => "json",
117            ExperimentManifestPropType::String => "string",
118        };
119        write!(f, "{}", s)
120    }
121}
122
123impl From<TypeRef> for ExperimentManifestPropType {
124    fn from(typ: TypeRef) -> Self {
125        match typ {
126            TypeRef::Object(_)
127            | TypeRef::EnumMap(_, _)
128            | TypeRef::StringMap(_)
129            | TypeRef::List(_) => Self::Json,
130            TypeRef::Boolean => Self::Boolean,
131            TypeRef::Int => Self::Int,
132            TypeRef::String
133            | TypeRef::BundleImage
134            | TypeRef::BundleText
135            | TypeRef::StringAlias(_)
136            | TypeRef::Enum(_) => Self::String,
137            TypeRef::Option(inner) => Self::from(inner),
138        }
139    }
140}
141
142impl From<Box<TypeRef>> for ExperimentManifestPropType {
143    fn from(typ: Box<TypeRef>) -> Self {
144        (*typ).into()
145    }
146}
147
148pub(crate) fn generate_manifest(
149    ir: FeatureManifest,
150    cmd: &GenerateExperimenterManifestCmd,
151) -> Result<()> {
152    let experiment_manifest: ExperimenterManifest = ir.try_into()?;
153    let output_str = match cmd.language {
154        TargetLanguage::ExperimenterJSON => serde_json::to_string_pretty(&experiment_manifest)?,
155        // This is currently just a re-render of the JSON in YAML.
156        // However, the YAML format will diverge in time, so experimenter can support
157        // a richer manifest format (probably involving generating schema that can validate
158        // JSON patches in the FeatureConfig.)
159        TargetLanguage::ExperimenterYAML => serde_yaml::to_string(&experiment_manifest)?,
160
161        // If in doubt, output the previously generated default.
162        _ => serde_json::to_string(&experiment_manifest)?,
163    };
164
165    std::fs::write(&cmd.output, output_str)?;
166    Ok(())
167}