Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ getAll = "Get all instances of the resource"
resource = "The name of the resource to invoke"
functionAbout = "Operations on DSC functions"
listFunctionAbout = "List or find functions"
functionCategory = "Category to filter functions on; specify multiple times to require all categories"
functionDescription = "Description to search for in the function description, accepts wildcards"
version = "The version of the resource to invoke in semver format"
serverAbout = "Use DSC as a server over JSON-RPC (useful as MCP server)"
bicepAbout = "Use DSC as a Bicep server over gRPC"
Expand Down Expand Up @@ -152,6 +154,7 @@ tableHeader_functionName = "Function"
tableHeader_functionCategory = "Category"
tableHeader_syntax = "Syntax"
invalidFunctionFilter = "Invalid function filter"
invalidFunctionDescriptionFilter = "Invalid function description filter"
maxInt = "maxInt"
invalidManifest = "Error in manifest for"
jsonArrayNotSupported = "JSON array output format is only supported for `--all'"
Expand Down
5 changes: 5 additions & 0 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use clap::{Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use dsc_lib::dscresources::command_resource::TraceLevel;
use dsc_lib::functions::FunctionCategory;
use dsc_lib::progress::ProgressFormat;
use dsc_lib::types::{FullyQualifiedTypeName, ResourceVersionReq, TypeNameFilter};
use rust_i18n::t;
Expand Down Expand Up @@ -193,6 +194,10 @@ pub enum FunctionSubCommand {
List {
/// Optional function name to filter the list
function_name: Option<String>,
#[clap(short = 'c', long = "category", help = t!("args.functionCategory").to_string())]
category: Vec<FunctionCategory>,
#[clap(short, long, help = t!("args.functionDescription").to_string())]
description: Option<String>,
#[clap(short = 'o', long, help = t!("args.outputFormat").to_string())]
output_format: Option<ListOutputFormat>,
},
Expand Down
31 changes: 27 additions & 4 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use dsc_lib::{
},
dscresources::dscresource::{Capability, ImplementedAs, validate_json, validate_properties},
extensions::dscextension::Capability as ExtensionCapability,
functions::FunctionDispatcher,
functions::{FunctionCategory, FunctionDispatcher},
progress::ProgressFormat,
util::convert_wildcard_to_regex,
};
Expand Down Expand Up @@ -541,8 +541,8 @@ pub fn extension(subcommand: &ExtensionSubCommand, progress_format: ProgressForm
pub fn function(subcommand: &FunctionSubCommand) {
let functions = FunctionDispatcher::new();
match subcommand {
FunctionSubCommand::List { function_name, output_format } => {
list_functions(&functions, function_name.as_ref(), output_format.as_ref());
FunctionSubCommand::List { function_name, category, description, output_format } => {
list_functions(&functions, function_name.as_ref(), category, description.as_ref(), output_format.as_ref());
},
}
}
Expand Down Expand Up @@ -689,7 +689,7 @@ fn list_extensions(dsc: &mut DscManager, extension_name: &TypeNameFilter, format
}
}

fn list_functions(functions: &FunctionDispatcher, function_name: Option<&String>, output_format: Option<&ListOutputFormat>) {
fn list_functions(functions: &FunctionDispatcher, function_name: Option<&String>, category: &[FunctionCategory], description: Option<&String>, output_format: Option<&ListOutputFormat>) {
let write_table = should_write_table(output_format);
let mut table = Table::new(&[
t!("subcommand.tableHeader_functionCategory").to_string().as_ref(),
Expand All @@ -709,13 +709,36 @@ fn list_functions(functions: &FunctionDispatcher, function_name: Option<&String>
exit(EXIT_INVALID_ARGS);
};

let description_regex = description.map(|description| {
let regex_str = convert_wildcard_to_regex(description);
// strip the `^` and `$` anchors so the filter searches within the description text
let regex_str = &regex_str[1..regex_str.len() - 1];
let mut regex_builder = RegexBuilder::new(regex_str);
regex_builder.case_insensitive(true);
let Ok(regex) = regex_builder.build() else {
error!("{}: {}", t!("subcommand.invalidFunctionDescriptionFilter"), regex_str);
exit(EXIT_INVALID_ARGS);
};
regex
});

let mut functions_list = functions.list();
functions_list.sort();
for function in functions_list {
if !regex.is_match(&function.name) {
continue;
}

// all specified categories must be present on the function (AND filter)
if !category.iter().all(|c| function.category.contains(c)) {
continue;
}

if let Some(ref description_regex) = description_regex
&& !description_regex.is_match(&function.description) {
continue;
}

if write_table {
table.add_row(vec![
function.category.iter().map(std::string::ToString::to_string).collect::<Vec<String>>().join(", "),
Expand Down
71 changes: 71 additions & 0 deletions dsc/tests/dsc_function_list.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,75 @@ Describe 'Tests for function list subcommand' {
}
$foundWideLine | Should -BeTrue
}

Context 'Category filter' {
It 'Should filter by a single category' {
$out = dsc function list --category resource | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out | Should -Not -BeNullOrEmpty
foreach ($function in $out) {
$function.category | Should -Contain 'resource'
}
$out.name | Should -Contain 'resourceId'
}

It 'Should treat multiple categories as an AND filter' {
$out = dsc function list --category array --category object | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out | Should -Not -BeNullOrEmpty
foreach ($function in $out) {
$function.category | Should -Contain 'array'
$function.category | Should -Contain 'object'
}
$out.name | Should -Contain 'tryGet'
}

It 'Should match category case-insensitively' {
$out = dsc function list --category Array | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out | Should -Not -BeNullOrEmpty
foreach ($function in $out) {
$function.category | Should -Contain 'array'
}
}

It 'Should error on an invalid category' {
$out = dsc function list --category notARealCategory 2>&1
$LASTEXITCODE | Should -Not -Be 0
($out | Out-String) | Should -Match 'Invalid function category'
}
}

Context 'Description filter' {
It 'Should filter on description text' {
$out = dsc function list --description base64 | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out | Should -Not -BeNullOrEmpty
foreach ($function in $out) {
$function.description | Should -Match 'base64'
}
$out.name | Should -Contain 'base64'
$out.name | Should -Contain 'base64ToString'
}

It 'Should support wildcards in the description filter' {
$out = dsc function list --description 'encodes*base64' | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.name | Should -Contain 'base64'
}

It 'Should return nothing when the description does not match' {
$out = dsc function list --description 'zzzNoSuchDescriptionzzz' | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out | Should -BeNullOrEmpty
}
}

Context 'Combined filters' {
It 'Should combine name, category, and description filters' {
$out = dsc function list 'try*' --category array --category object --description 'retrieve' | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.name | Should -BeExactly 'tryGet'
}
}
}
1 change: 1 addition & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ noObjectArgs = "Function '%{name}' does not accept object arguments, accepted ty
noStringArgs = "Function '%{name}' does not accept string arguments, accepted types are: %{accepted_args_string}"
lambdaNotFound = "Function '%{name}' could not find lambda with ID '%{id}'"
lambdaTooManyParams = "Function '%{name}' requires lambda with 1 or 2 parameters (element and optional index)"
invalidCategory = "Invalid function category '%{category}', valid categories are: %{valid_categories}"

[functions.add]
description = "Adds two or more numbers together"
Expand Down
43 changes: 43 additions & 0 deletions lib/dsc-lib/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,46 @@ impl Display for FunctionCategory {
}
}
}

impl FunctionCategory {
/// All defined function categories.
pub const ALL: [FunctionCategory; 12] = [
FunctionCategory::Array,
FunctionCategory::Cidr,
FunctionCategory::Comparison,
FunctionCategory::Date,
FunctionCategory::Deployment,
FunctionCategory::Lambda,
FunctionCategory::Logical,
FunctionCategory::Numeric,
FunctionCategory::Object,
FunctionCategory::Resource,
FunctionCategory::String,
FunctionCategory::System,
];
}

impl std::str::FromStr for FunctionCategory {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"array" => Ok(FunctionCategory::Array),
"cidr" => Ok(FunctionCategory::Cidr),
"comparison" => Ok(FunctionCategory::Comparison),
"date" => Ok(FunctionCategory::Date),
"deployment" => Ok(FunctionCategory::Deployment),
"lambda" => Ok(FunctionCategory::Lambda),
"logical" => Ok(FunctionCategory::Logical),
"numeric" => Ok(FunctionCategory::Numeric),
"object" => Ok(FunctionCategory::Object),
"resource" => Ok(FunctionCategory::Resource),
"string" => Ok(FunctionCategory::String),
"system" => Ok(FunctionCategory::System),
_ => {
let valid = Self::ALL.iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join(", ");
Err(t!("functions.invalidCategory", category = s, valid_categories = valid).to_string())
},
}
}
}
15 changes: 13 additions & 2 deletions lib/dsc-lib/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,15 @@ pub fn parse_input_to_json(value: &str) -> Result<String, DscError> {
/// A string that holds the regex pattern.
#[must_use]
pub fn convert_wildcard_to_regex(wildcard: &str) -> String {
let mut regex = wildcard.to_string().replace('.', "\\.").replace('?', ".").replace('*', ".*?");
regex.insert(0, '^');
let mut regex = String::with_capacity(wildcard.len() + 2);
regex.push('^');
for c in wildcard.chars() {
match c {
'*' => regex.push_str(".*?"),
'?' => regex.push('.'),
_ => regex.push_str(&regex::escape(&c.to_string())),
}
}
regex.push('$');
regex
}
Expand All @@ -95,6 +102,10 @@ mod tests {
let wildcard = "r*";
let regex = convert_wildcard_to_regex(wildcard);
assert_eq!(regex, "^r.*?$");

let wildcard = "a+b(c)[d]^e$f|g\\h";
let regex = convert_wildcard_to_regex(wildcard);
assert_eq!(regex, r"^a\+b\(c\)\[d\]\^e\$f\|g\\h$");
}
}

Expand Down
Loading