From 6e15c00238a06e92cf411a669590002eb22324e7 Mon Sep 17 00:00:00 2001 From: Ryan Sabatini <11415980+rjsab@users.noreply.github.com> Date: Sun, 27 Nov 2022 08:02:23 -0600 Subject: [PATCH] feat(azure): add username to azure module config (#4323) * add username to azure module config * add username to azure module config * formatting with cargo fmt * Handle parse failure on azureProfile.json allow program to procede if unable to parse azure profile due to missing keys from the JSON structure. remove unused keys from struct Code cleanup with suggestions from PR maintainer Cargo clippy fixes --- docs/config/README.md | 18 +- src/modules/azure.rs | 567 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 524 insertions(+), 61 deletions(-) diff --git a/docs/config/README.md b/docs/config/README.md index de60a2b56..0715a4c27 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -439,7 +439,7 @@ Enterprise_Naming_Scheme-voidstars = 'void**' ## Azure -The `azure` module shows the current Azure Subscription. This is based on showing the name of the default subscription, as defined in the `~/.azure/azureProfile.json` file. +The `azure` module shows the current Azure Subscription. This is based on showing the name of the default subscription or the username, as defined in the `~/.azure/azureProfile.json` file. ### Options @@ -450,7 +450,9 @@ The `azure` module shows the current Azure Subscription. This is based on showin | `style` | `'blue bold'` | The style used in the format. | | `disabled` | `true` | Disables the `azure` module. | -### Example +### Examples + +#### Display Subscription Name ```toml # ~/.config/starship.toml @@ -462,6 +464,18 @@ symbol = 'ﴃ ' style = 'blue bold' ``` +#### Display Username + +```toml +# ~/.config/starship.toml + +[azure] +disabled = false +format = "on [$symbol($username)]($style) " +symbol = "ﴃ " +style = "blue bold" +``` + ## Battery The `battery` module shows how charged the device's battery is and its current charging status. diff --git a/src/modules/azure.rs b/src/modules/azure.rs index 31a7ccb64..de99a48e4 100644 --- a/src/modules/azure.rs +++ b/src/modules/azure.rs @@ -1,15 +1,32 @@ -use std::fs::File; -use std::io::{BufReader, Read}; -use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; use super::{Context, Module, ModuleConfig}; -type JValue = serde_json::Value; - use crate::configs::azure::AzureConfig; use crate::formatter::StringFormatter; -type SubscriptionName = String; +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct AzureProfile { + installation_id: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + subscriptions: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +struct User { + name: String, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct Subscription { + name: String, + user: User, + is_default: bool, +} pub fn module<'a>(context: &'a Context) -> Option> { let mut module = context.new_module("azure"); @@ -19,11 +36,14 @@ pub fn module<'a>(context: &'a Context) -> Option> { return None; }; - let subscription_name: Option = get_azure_subscription_name(context); - if subscription_name.is_none() { - log::info!("Could not find Azure subscription name"); + let subscription: Option = get_azure_profile_info(context); + + if subscription.is_none() { + log::info!("Could not find Subscriptions in azureProfile.json"); return None; - }; + } + + let subscription = subscription.unwrap(); let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter @@ -36,7 +56,8 @@ pub fn module<'a>(context: &'a Context) -> Option> { _ => None, }) .map(|variable| match variable { - "subscription" => Some(Ok(subscription_name.as_ref().unwrap())), + "subscription" => Some(Ok(&subscription.name)), + "username" => Some(Ok(&subscription.user.name)), _ => None, }) .parse(None, Some(context)) @@ -53,24 +74,24 @@ pub fn module<'a>(context: &'a Context) -> Option> { Some(module) } -fn get_azure_subscription_name(context: &Context) -> Option { +fn get_azure_profile_info(context: &Context) -> Option { let mut config_path = get_config_file_location(context)?; config_path.push("azureProfile.json"); - let parsed_json = parse_json(&config_path)?; + let azure_profile = load_azure_profile(&config_path)?; + azure_profile + .subscriptions + .into_iter() + .find(|s| s.is_default) +} - let subscriptions = parsed_json.get("subscriptions")?.as_array()?; - let subscription_name = subscriptions.iter().find_map(|s| { - if s.get("isDefault")? == true { - Some(s.get("name")?.as_str()?.to_string()) - } else { - None - } - }); - if subscription_name.is_some() { - subscription_name +fn load_azure_profile(config_path: &PathBuf) -> Option { + let json_data = fs::read_to_string(config_path).ok()?; + let sanitized_json_data = json_data.strip_prefix('\u{feff}').unwrap_or(&json_data); + if let Ok(azure_profile) = serde_json::from_str::(sanitized_json_data) { + Some(azure_profile) } else { - log::info!("Could not find subscription name"); + log::info!("Failed to parse azure profile."); None } } @@ -86,27 +107,9 @@ fn get_config_file_location(context: &Context) -> Option { }) } -fn parse_json(json_file_path: &Path) -> Option { - let mut buffer: Vec = Vec::new(); - - let json_file = File::open(json_file_path).ok()?; - let mut reader = BufReader::new(json_file); - reader.read_to_end(&mut buffer).ok()?; - - let bytes = buffer.as_mut_slice(); - let decodedbuffer = bytes.strip_prefix(&[239, 187, 191]).unwrap_or(bytes); - - if let Ok(parsed_json) = serde_json::from_slice(decodedbuffer) { - Some(parsed_json) - } else { - log::info!("Failed to parse json"); - None - } -} - #[cfg(test)] mod tests { - use crate::modules::azure::parse_json; + use crate::modules::azure::load_azure_profile; use crate::test::ModuleRenderer; use ini::Ini; use nu_ansi_term::Color; @@ -154,6 +157,7 @@ mod tests { "name": "user@domain.com", "type": "user" }, + "isDefault": false, "tenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", "environmentName": "AzureCloud", "homeTenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", @@ -194,6 +198,424 @@ mod tests { dir.close() } + #[test] + fn user_name_set_correctly() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let azure_profile_contents = r#"{ + "installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", + "subscriptions": [ + { + "id": "f568c543-d12e-de0b-3d85-69843598b565", + "name": "Subscription 2", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "environmentName": "AzureCloud", + "homeTenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "managedByTenants": [] + }, + { + "id": "d4442d26-ea6d-46c4-07cb-4f70b8ae5465", + "name": "Subscription 3", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "environmentName": "AzureCloud", + "homeTenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "managedByTenants": [] + }, + { + "id": "f3935dc9-92b5-9a93-da7b-42c325d86939", + "name": "Subscription 1", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": true, + "tenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "environmentName": "AzureCloud", + "homeTenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "managedByTenants": [] + } + ] + } + "#; + + generate_test_config(&dir, azure_profile_contents)?; + let dir_path = &dir.path().to_string_lossy(); + let actual = ModuleRenderer::new("azure") + .config(toml::toml! { + [azure] + format = "on [$symbol($username)]($style)" + disabled = false + }) + .env("AZURE_CONFIG_DIR", dir_path.as_ref()) + .collect(); + let expected = Some(format!( + "on {}", + Color::Blue.bold().paint("ﴃ user@domain.com") + )); + assert_eq!(actual, expected); + dir.close() + } + + #[test] + fn subscription_name_empty() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let azure_profile_contents = r#"{ + "installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", + "subscriptions": [ + { + "id": "f568c543-d12e-de0b-3d85-69843598b565", + "name": "Subscription 2", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "environmentName": "AzureCloud", + "homeTenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "managedByTenants": [] + }, + { + "id": "d4442d26-ea6d-46c4-07cb-4f70b8ae5465", + "name": "Subscription 3", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "environmentName": "AzureCloud", + "homeTenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "managedByTenants": [] + }, + { + "id": "f3935dc9-92b5-9a93-da7b-42c325d86939", + "name": "", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": true, + "tenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "environmentName": "AzureCloud", + "homeTenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "managedByTenants": [] + } + ] + } + "#; + + generate_test_config(&dir, azure_profile_contents)?; + let dir_path = &dir.path().to_string_lossy(); + let actual = ModuleRenderer::new("azure") + .config(toml::toml! { + [azure] + format = "on [$symbol($subscription:$username)]($style)" + disabled = false + }) + .env("AZURE_CONFIG_DIR", dir_path.as_ref()) + .collect(); + let expected = Some(format!( + "on {}", + Color::Blue.bold().paint("ﴃ :user@domain.com") + )); + assert_eq!(actual, expected); + dir.close() + } + + #[test] + fn user_name_empty() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let azure_profile_contents = r#"{ + "installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", + "subscriptions": [ + { + "id": "f568c543-d12e-de0b-3d85-69843598b565", + "name": "Subscription 2", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "environmentName": "AzureCloud", + "homeTenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "managedByTenants": [] + }, + { + "id": "d4442d26-ea6d-46c4-07cb-4f70b8ae5465", + "name": "Subscription 3", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "environmentName": "AzureCloud", + "homeTenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "managedByTenants": [] + }, + { + "id": "f3935dc9-92b5-9a93-da7b-42c325d86939", + "name": "Subscription 1", + "state": "Enabled", + "user": { + "name": "", + "type": "user" + }, + "isDefault": true, + "tenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "environmentName": "AzureCloud", + "homeTenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "managedByTenants": [] + } + ] + } + "#; + + generate_test_config(&dir, azure_profile_contents)?; + let dir_path = &dir.path().to_string_lossy(); + let actual = ModuleRenderer::new("azure") + .config(toml::toml! { + [azure] + format = "on [$symbol($subscription:$username)]($style)" + disabled = false + }) + .env("AZURE_CONFIG_DIR", dir_path.as_ref()) + .collect(); + let expected = Some(format!( + "on {}", + Color::Blue.bold().paint("ﴃ Subscription 1:") + )); + assert_eq!(actual, expected); + dir.close() + } + + #[test] + fn user_name_missing_from_profile() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let azure_profile_contents = r#"{ + "installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", + "subscriptions": [ + { + "id": "f568c543-d12e-de0b-3d85-69843598b565", + "name": "Subscription 2", + "state": "Enabled", + "user": { + "type": "user" + }, + "isDefault": false, + "tenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "environmentName": "AzureCloud", + "homeTenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "managedByTenants": [] + }, + { + "id": "d4442d26-ea6d-46c4-07cb-4f70b8ae5465", + "name": "Subscription 3", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "environmentName": "AzureCloud", + "homeTenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "managedByTenants": [] + }, + { + "id": "f3935dc9-92b5-9a93-da7b-42c325d86939", + "name": "Subscription 1", + "state": "Enabled", + "user": { + "name": "", + "type": "user" + }, + "isDefault": true, + "tenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "environmentName": "AzureCloud", + "homeTenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "managedByTenants": [] + } + ] + } + "#; + + generate_test_config(&dir, azure_profile_contents)?; + let dir_path = &dir.path().to_string_lossy(); + let actual = ModuleRenderer::new("azure") + .config(toml::toml! { + [azure] + format = "on [$symbol($subscription:$username)]($style)" + disabled = false + }) + .env("AZURE_CONFIG_DIR", dir_path.as_ref()) + .collect(); + let expected = None; + assert_eq!(actual, expected); + dir.close() + } + + #[test] + fn subscription_name_missing_from_profile() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let azure_profile_contents = r#"{ + "installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", + "subscriptions": [ + { + "id": "f568c543-d12e-de0b-3d85-69843598b565", + "name": "Subscription 2", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "environmentName": "AzureCloud", + "homeTenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "managedByTenants": [] + }, + { + "id": "d4442d26-ea6d-46c4-07cb-4f70b8ae5465", + "name": "Subscription 3", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "environmentName": "AzureCloud", + "homeTenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "managedByTenants": [] + }, + { + "id": "f3935dc9-92b5-9a93-da7b-42c325d86939", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": true, + "tenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "environmentName": "AzureCloud", + "homeTenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "managedByTenants": [] + } + ] + } + "#; + + generate_test_config(&dir, azure_profile_contents)?; + let dir_path = &dir.path().to_string_lossy(); + let actual = ModuleRenderer::new("azure") + .config(toml::toml! { + [azure] + format = "on [$symbol($subscription:$username)]($style)" + disabled = false + }) + .env("AZURE_CONFIG_DIR", dir_path.as_ref()) + .collect(); + let expected = None; + assert_eq!(actual, expected); + dir.close() + } + + #[test] + fn subscription_name_and_username_found() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let azure_profile_contents = r#"{ + "installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", + "subscriptions": [ + { + "id": "f568c543-d12e-de0b-3d85-69843598b565", + "name": "Subscription 2", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "environmentName": "AzureCloud", + "homeTenantId": "0e8a15ec-b0f5-d355-7062-8ece54c59aee", + "managedByTenants": [] + }, + { + "id": "d4442d26-ea6d-46c4-07cb-4f70b8ae5465", + "name": "Subscription 3", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": false, + "tenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "environmentName": "AzureCloud", + "homeTenantId": "a4e1bb4b-5330-2d50-339d-b9674d3a87bc", + "managedByTenants": [] + }, + { + "id": "f3935dc9-92b5-9a93-da7b-42c325d86939", + "name": "Subscription 1", + "state": "Enabled", + "user": { + "name": "user@domain.com", + "type": "user" + }, + "isDefault": true, + "tenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "environmentName": "AzureCloud", + "homeTenantId": "f0273a19-7779-e40a-00a1-53b8331b3bb6", + "managedByTenants": [] + } + ] + } + "#; + + generate_test_config(&dir, azure_profile_contents)?; + let dir_path = &dir.path().to_string_lossy(); + let actual = ModuleRenderer::new("azure") + .config(toml::toml! { + [azure] + format = "on [$symbol($subscription:$username)]($style)" + disabled = false + }) + .env("AZURE_CONFIG_DIR", dir_path.as_ref()) + .collect(); + let expected = Some(format!( + "on {}", + Color::Blue.bold().paint("ﴃ Subscription 1:user@domain.com") + )); + assert_eq!(actual, expected); + dir.close() + } + #[test] fn subscription_azure_profile_empty() -> io::Result<()> { let dir = tempfile::tempdir()?; @@ -203,6 +625,7 @@ mod tests { .with_section(Some("AzureCloud")) .set("subscription", "f3935dc9-92b5-9a93-da7b-42c325d86939"); + //let azure_profile_contents = "\u{feff}{\"installationId\": \"2652263e-40f8-11ed-ae3b-367ddada549c\", \"subscriptions\": []}"; let azure_profile_contents = r#"{ "installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", "subscriptions": [] @@ -223,6 +646,48 @@ mod tests { dir.close() } + #[test] + fn azure_profile_with_leading_char() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let bom = vec![239, 187, 191]; + let mut bom_str = String::from_utf8(bom).unwrap(); + + let json_str = + r#"{"installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", "subscriptions": []}"#; + + bom_str.push_str(json_str); + + let dir_path_no_bom = save_string_to_file(&dir, bom_str, String::from("bom.json"))?; + let sanitized_json = load_azure_profile(&dir_path_no_bom).unwrap(); + + assert_eq!( + sanitized_json.installation_id, + "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3" + ); + assert!(sanitized_json.subscriptions.is_empty()); + dir.close() + } + + #[test] + fn azure_profile_without_leading_char() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let json_str = + r#"{"installationId": "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3", "subscriptions": []}"#; + + let dir_path_no_bom = + save_string_to_file(&dir, json_str.to_string(), String::from("bom.json"))?; + let sanitized_json = load_azure_profile(&dir_path_no_bom).unwrap(); + + assert_eq!( + sanitized_json.installation_id, + "3deacd2a-b9db-77e1-aa42-23e2f8dfffc3" + ); + assert!(sanitized_json.subscriptions.is_empty()); + dir.close() + } + #[test] fn files_missing() -> io::Result<()> { let dir = tempfile::tempdir()?; @@ -237,22 +702,6 @@ mod tests { dir.close() } - #[test] - fn json_parsing() -> io::Result<()> { - let dir = tempfile::tempdir()?; - - let bom = vec![239, 187, 191]; - let mut bom_str = String::from_utf8(bom).unwrap(); - let json_str = r#"{"testKey": "testValue"}"#; - bom_str.push_str(json_str); - - let dir_path_no_bom = save_string_to_file(&dir, bom_str, String::from("bom.json"))?; - let parsed_json = parse_json(&dir_path_no_bom).unwrap(); - - assert_eq!(parsed_json.get("testKey").unwrap(), "testValue"); - dir.close() - } - fn save_string_to_file( dir: &TempDir, contents: String,