/* 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 http://mozilla.org/MPL/2.0/. */ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Display; use serde::{Deserialize, Serialize}; use crate::{ command_line::commands::GenerateExperimenterManifestCmd, error::{FMLError, Result}, intermediate_representation::{FeatureDef, FeatureManifest, PropDef, TargetLanguage, TypeRef}, }; pub(crate) type ExperimenterManifest = BTreeMap; #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct ExperimenterFeature { description: String, has_exposure: bool, #[serde(skip_serializing_if = "Option::is_none")] exposure_description: Option, #[serde(skip_serializing_if = "Option::is_none")] is_early_startup: Option, variables: BTreeMap, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct ExperimenterFeatureProperty { #[serde(rename = "type")] property_type: String, description: String, #[serde(rename = "enum")] #[serde(skip_serializing_if = "Option::is_none")] variants: Option>, } impl TryFrom for ExperimenterManifest { type Error = crate::error::FMLError; fn try_from(fm: FeatureManifest) -> Result { fm.iter_all_feature_defs() .map(|(fm, f)| Ok((f.name(), fm.create_experimenter_feature(f)?))) .collect() } } impl FeatureManifest { fn create_experimenter_feature(&self, feature: &FeatureDef) -> Result { Ok(ExperimenterFeature { description: feature.doc(), has_exposure: true, is_early_startup: None, // TODO: Add exposure description to the IR so // we can use it here if it's needed exposure_description: Some("".into()), variables: self.props_to_variables(&feature.props)?, }) } fn props_to_variables( &self, props: &[PropDef], ) -> Result> { // Ideally this would be implemented as a `TryFrom>` // however, we need a reference to the `FeatureManifest` to get the valid // variants of an enum let mut map = BTreeMap::new(); props.iter().try_for_each(|prop| -> Result<()> { let typ = ExperimentManifestPropType::from(prop.typ()).to_string(); let yaml_prop = if let TypeRef::Enum(e) = prop.typ() { let enum_def = self .find_enum(&e) .ok_or(FMLError::InternalError("Found enum with no definition"))?; let variants = enum_def .variants .iter() .map(|variant| variant.name()) .collect::>(); ExperimenterFeatureProperty { variants: Some(variants), description: prop.doc(), property_type: typ, } } else { ExperimenterFeatureProperty { variants: None, description: prop.doc(), property_type: typ, } }; map.insert(prop.name(), yaml_prop); Ok(()) })?; Ok(map) } } enum ExperimentManifestPropType { Json, Boolean, Int, String, } impl Display for ExperimentManifestPropType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { ExperimentManifestPropType::Boolean => "boolean", ExperimentManifestPropType::Int => "int", ExperimentManifestPropType::Json => "json", ExperimentManifestPropType::String => "string", }; write!(f, "{}", s) } } impl From for ExperimentManifestPropType { fn from(typ: TypeRef) -> Self { match typ { TypeRef::Object(_) | TypeRef::EnumMap(_, _) | TypeRef::StringMap(_) | TypeRef::List(_) => Self::Json, TypeRef::Boolean => Self::Boolean, TypeRef::Int => Self::Int, TypeRef::String | TypeRef::BundleImage | TypeRef::BundleText | TypeRef::StringAlias(_) | TypeRef::Enum(_) => Self::String, TypeRef::Option(inner) => Self::from(inner), } } } impl From> for ExperimentManifestPropType { fn from(typ: Box) -> Self { (*typ).into() } } pub(crate) fn generate_manifest( ir: FeatureManifest, cmd: &GenerateExperimenterManifestCmd, ) -> Result<()> { let experiment_manifest: ExperimenterManifest = ir.try_into()?; let output_str = match cmd.language { TargetLanguage::ExperimenterJSON => serde_json::to_string_pretty(&experiment_manifest)?, // This is currently just a re-render of the JSON in YAML. // However, the YAML format will diverge in time, so experimenter can support // a richer manifest format (probably involving generating schema that can validate // JSON patches in the FeatureConfig.) TargetLanguage::ExperimenterYAML => serde_yaml::to_string(&experiment_manifest)?, // If in doubt, output the previously generated default. _ => serde_json::to_string(&experiment_manifest)?, }; std::fs::write(&cmd.output, output_str)?; Ok(()) }