nimbus_cli/
feature_utils.rs
1use std::{
6 path::{Path, PathBuf},
7 time::{SystemTime, UNIX_EPOCH},
8};
9
10use anyhow::Result;
11use heck::ToKebabCase;
12use serde_json::{json, Value};
13
14use crate::{value_utils, NimbusApp};
15
16pub(crate) fn create_experiment(
17 app: &NimbusApp,
18 feature_id: &str,
19 files: &Vec<PathBuf>,
20) -> Result<Value> {
21 let mut branches = Vec::new();
22 for f in files {
23 branches.push(branch(feature_id, f)?);
24 }
25
26 let control = slug(files.first().unwrap())?;
27
28 let start = SystemTime::now();
29 let now = start
30 .duration_since(UNIX_EPOCH)
31 .expect("Time went backawards");
32
33 let user = whoami::username();
34 let slug = format!(
35 "{} test {} {:x}",
36 user,
37 feature_id,
38 now.as_secs() & ((1u64 << 16) - 1)
39 )
40 .to_kebab_case();
41
42 let app_name = app
43 .app_name()
44 .expect("An app name is expected. This is a bug in nimbus-cli");
45 Ok(json!({
46 "appId": &app_name,
47 "appName": &app_name,
48 "application": &app_name,
49 "arguments": {},
50 "branches": branches,
51 "bucketConfig": {
52 "count": 10_000,
53 "namespace": format!("{}-1", &slug),
54 "randomizationUnit": "nimbus_id",
55 "start": 0,
56 "total": 10_000
57 },
58 "channel": app.channel,
59 "endDate": null,
60 "enrollmentEndDate": null,
61 "featureIds": [
62 feature_id,
63 ],
64 "featureValidationOptOut": false,
65 "id": &slug,
66 "isEnrollmentPaused": false,
67 "isRollout": false,
68 "last_modified": now.as_secs() * 1000,
69 "outcomes": [],
70 "probeSets": [],
71 "proposedDuration": 7,
72 "proposedEnrollment": 7,
73 "referenceBranch": control,
74 "schemaVersion": "1.11.0",
75 "slug": &slug,
76 "startDate": null,
77 "targeting": "true",
78 "userFacingDescription": format!("Testing the {} feature from nimbus-cli", feature_id),
79 "userFacingName": format!("[{}] Testing {}", &user, feature_id)
80 }))
81}
82
83pub(crate) fn slug(path: &Path) -> Result<String> {
84 let filename = path
85 .file_stem()
86 .ok_or_else(|| anyhow::Error::msg("File has no filename"))?;
87 Ok(filename.to_string_lossy().to_string().to_kebab_case())
88}
89
90fn branch(feature_id: &str, file: &Path) -> Result<Value> {
91 let value: Value = value_utils::read_from_file(file)?;
92
93 let config = value.as_object().ok_or_else(|| {
94 anyhow::Error::msg(format!(
95 "{} does not contain a JSON object",
96 file.to_str().unwrap()
97 ))
98 })?;
99
100 Ok(json!({
101 "feature": {
102 "enabled": true,
103 "featureId": feature_id,
104 "value": config,
105 },
106 "slug": slug(file)?,
107 }))
108}