diff --git a/.github/config-schema.json b/.github/config-schema.json index 8d43d3bec..0e062b41a 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -982,6 +982,25 @@ } ] }, + "opa": { + "default": { + "detect_extensions": [ + "rego" + ], + "detect_files": [], + "detect_folders": [], + "disabled": false, + "format": "via [$symbol($version )]($style)", + "style": "bold blue", + "symbol": "🪖 ", + "version_format": "v${raw}" + }, + "allOf": [ + { + "$ref": "#/definitions/OpaConfig" + } + ] + }, "openstack": { "default": { "disabled": false, @@ -3864,6 +3883,55 @@ }, "additionalProperties": false }, + "OpaConfig": { + "type": "object", + "properties": { + "format": { + "default": "via [$symbol($version )]($style)", + "type": "string" + }, + "version_format": { + "default": "v${raw}", + "type": "string" + }, + "symbol": { + "default": "🪖 ", + "type": "string" + }, + "style": { + "default": "bold blue", + "type": "string" + }, + "disabled": { + "default": false, + "type": "boolean" + }, + "detect_extensions": { + "default": [ + "rego" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_files": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "detect_folders": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "OspConfig": { "type": "object", "properties": { diff --git a/docs/.vuepress/public/presets/toml/bracketed-segments.toml b/docs/.vuepress/public/presets/toml/bracketed-segments.toml index 51ea664a9..b8138d79c 100644 --- a/docs/.vuepress/public/presets/toml/bracketed-segments.toml +++ b/docs/.vuepress/public/presets/toml/bracketed-segments.toml @@ -100,6 +100,9 @@ format = '\[[$symbol($version)]($style)\]' [ocaml] format = '\[[$symbol($version)(\($switch_indicator$switch_name\))]($style)\]' +[opa] +format = '\[[$symbol($version)]($style)\]' + [openstack] format = '\[[$symbol$cloud(\($project\))]($style)\]' diff --git a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml index 0cea589c8..fa617d9db 100644 --- a/docs/.vuepress/public/presets/toml/no-runtime-versions.toml +++ b/docs/.vuepress/public/presets/toml/no-runtime-versions.toml @@ -61,6 +61,9 @@ format = 'via [$symbol]($style)' [ocaml] format = 'via [$symbol(\($switch_indicator$switch_name\) )]($style)' +[opa] +format = 'via [$symbol]($style)' + [perl] format = 'via [$symbol]($style)' diff --git a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml index d152423ca..5360d7936 100644 --- a/docs/.vuepress/public/presets/toml/plain-text-symbols.toml +++ b/docs/.vuepress/public/presets/toml/plain-text-symbols.toml @@ -97,6 +97,9 @@ symbol = "nix " [ocaml] symbol = "ml " +[opa] +symbol = "opa " + [package] symbol = "pkg " diff --git a/docs/config/README.md b/docs/config/README.md index 7f345985e..8dc2f00a9 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -259,6 +259,7 @@ $lua\ $nim\ $nodejs\ $ocaml\ +$opa\ $perl\ $php\ $pulumi\ @@ -2635,6 +2636,43 @@ By default the module will be shown if any of the following conditions are met: format = "via [🐪 $version]($style) " ``` +## Open Policy Agent + +The `opa` module shows the currently installed version of the OPA tool. +By default the module will be shown if the current directory contains a `.rego` file. + +### Options + +| Option | Default | Description | +| ------------------- | ------------------------------------ | ------------------------------------------------------------------------- | +| `format` | `"via [$symbol($version )]($style)"` | The format for the module. | +| `version_format` | `"v${raw}"` | The version format. Available vars are `raw`, `major`, `minor`, & `patch` | +| `symbol` | `"🪖 "` | A format string representing the symbol of OPA. | +| `detect_extensions` | `["rego"]` | Which extensions should trigger this module. | +| `detect_files` | `[]` | Which filenames should trigger this module. | +| `detect_folders` | `[]` | Which folders should trigger this module. | +| `style` | `"bold blue"` | The style for the module. | +| `disabled` | `false` | Disables the `opa` module. | + +### Variables + +| Variable | Example | Description | +| -------- | --------- | ------------------------------------ | +| version | `v0.44.0` | The version of `opa` | +| symbol | | Mirrors the value of option `symbol` | +| style\* | | Mirrors the value of option `style` | + +*: This variable can only be used as a part of a style string + +### Example + +```toml +# ~/.config/starship.toml + +[opa] +format = "via [⛑️ $version](bold red) " +``` + ## OpenStack The `openstack` module shows the current OpenStack cloud and project. The module diff --git a/src/configs/mod.rs b/src/configs/mod.rs index e767d0d81..582d56c56 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -51,6 +51,7 @@ pub mod nim; pub mod nix_shell; pub mod nodejs; pub mod ocaml; +pub mod opa; pub mod openstack; pub mod package; pub mod perl; @@ -195,6 +196,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] ocaml: ocaml::OCamlConfig<'a>, #[serde(borrow)] + opa: opa::OpaConfig<'a>, + #[serde(borrow)] openstack: openstack::OspConfig<'a>, #[serde(borrow)] package: package::PackageConfig<'a>, diff --git a/src/configs/opa.rs b/src/configs/opa.rs new file mode 100644 index 000000000..0dc55bcef --- /dev/null +++ b/src/configs/opa.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr( + feature = "config-schema", + derive(schemars::JsonSchema), + schemars(deny_unknown_fields) +)] +#[serde(default)] +pub struct OpaConfig<'a> { + pub format: &'a str, + pub version_format: &'a str, + pub symbol: &'a str, + pub style: &'a str, + pub disabled: bool, + pub detect_extensions: Vec<&'a str>, + pub detect_files: Vec<&'a str>, + pub detect_folders: Vec<&'a str>, +} + +impl<'a> Default for OpaConfig<'a> { + fn default() -> Self { + OpaConfig { + format: "via [$symbol($version )]($style)", + version_format: "v${raw}", + symbol: "🪖 ", + style: "bold blue", + disabled: false, + detect_extensions: vec!["rego"], + detect_files: vec![], + detect_folders: vec![], + } + } +} diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 6cc4da77b..11f6627a5 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -67,6 +67,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "nim", "nodejs", "ocaml", + "opa", "perl", "php", "pulumi", diff --git a/src/module.rs b/src/module.rs index c23e9435d..31881ee7d 100644 --- a/src/module.rs +++ b/src/module.rs @@ -59,6 +59,7 @@ pub const ALL_MODULES: &[&str] = &[ "nix_shell", "nodejs", "ocaml", + "opa", "openstack", "package", "perl", diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 6626e1c77..62de0ec6c 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -48,6 +48,7 @@ mod nim; mod nix_shell; mod nodejs; mod ocaml; +mod opa; mod openstack; mod package; mod perl; @@ -143,6 +144,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "nix_shell" => nix_shell::module(context), "nodejs" => nodejs::module(context), "ocaml" => ocaml::module(context), + "opa" => opa::module(context), "openstack" => openstack::module(context), "package" => package::module(context), "perl" => perl::module(context), @@ -251,6 +253,7 @@ pub fn description(module: &str) -> &'static str { "nix_shell" => "The nix-shell environment", "nodejs" => "The currently installed version of NodeJS", "ocaml" => "The currently installed version of OCaml", + "opa" => "The currently installed version of Open Platform Agent", "openstack" => "The current OpenStack cloud and project", "package" => "The package version of the current directory's project", "perl" => "The currently installed version of Perl", diff --git a/src/modules/opa.rs b/src/modules/opa.rs new file mode 100644 index 000000000..546a13011 --- /dev/null +++ b/src/modules/opa.rs @@ -0,0 +1,110 @@ +/// Creates a module with the current Open Policy Agent version +use super::{Context, Module, ModuleConfig}; + +use crate::configs::opa::OpaConfig; +use crate::formatter::StringFormatter; +use crate::formatter::VersionFormatter; +use crate::utils::get_command_string_output; + +/// Creates a module with the current Open Policy Agent version +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("opa"); + let config = OpaConfig::try_load(module.config); + + let is_opa_project = context + .try_begin_scan()? + .set_files(&config.detect_files) + .set_extensions(&config.detect_extensions) + .set_folders(&config.detect_folders) + .is_match(); + + if !is_opa_project { + return None; + } + + let parsed = StringFormatter::new(config.format).and_then(|formatter| { + formatter + .map_meta(|var, _| match var { + "symbol" => Some(config.symbol), + _ => None, + }) + .map_style(|variable| match variable { + "style" => Some(Ok(config.style)), + _ => None, + }) + .map(|variable| match variable { + "version" => { + let opa_version = get_opa_version(context)?; + VersionFormatter::format_module_version( + module.get_name(), + &opa_version, + config.version_format, + ) + .map(Ok) + } + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `opa`:\n{}", error); + return None; + } + }); + + Some(module) +} + +fn get_opa_version(context: &Context) -> Option { + let version_output: String = context + .exec_cmd("opa", &["version"]) + .map(get_command_string_output)?; + parse_opa_version(version_output) +} + +fn parse_opa_version(version_output: String) -> Option { + Some(version_output.split_whitespace().nth(1)?.to_string()) +} + +#[cfg(test)] +mod tests { + use crate::test::ModuleRenderer; + use nu_ansi_term::Color; + use std::fs::File; + use std::io; + + #[test] + fn folder_without_opa_files() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let actual = ModuleRenderer::new("opa").path(dir.path()).collect(); + let expected = None; + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn folder_with_opa_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("test.rego"))?.sync_all()?; + let actual = ModuleRenderer::new("opa").path(dir.path()).collect(); + let expected = Some(format!("via {}", Color::Blue.bold().paint("🪖 v0.44.0 "))); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn no_opa_installed() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("test.rego"))?.sync_all()?; + let actual = ModuleRenderer::new("opa") + .path(dir.path()) + .cmd("opa version", None) + .collect(); + let expected = Some(format!("via {}", Color::Blue.bold().paint("🪖 "))); + assert_eq!(expected, actual); + dir.close() + } +} diff --git a/src/utils.rs b/src/utils.rs index 959c37067..48d5b02ea 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -270,6 +270,17 @@ active boot switches: -d:release\n", stdout: String::from("4.10.0\n"), stderr: String::default(), }), + "opa version" => Some(CommandOutput { + stdout: String::from("Version: 0.44.0 +Build Commit: e8d488f +Build Timestamp: 2022-09-07T23:50:25Z +Build Hostname: 119428673f4c +Go Version: go1.19.1 +Platform: linux/amd64 +WebAssembly: unavailable +"), + stderr: String::default(), + }), "opam switch show --safe" => Some(CommandOutput { stdout: String::from("default\n"), stderr: String::default(),