mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 08:07:15 +00:00
feat: add support for switch and attributes in pwsh params (#7143)
This commit is contained in:
@@ -46,8 +46,6 @@ pub fn parse_powershell_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE_BASH: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+)\}|\{(\d+):-(.*)\})"(?:[\t ]*)?(?:#.*)?\r?$"#).unwrap();
|
||||
|
||||
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[([\w\[\]]+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?\r?"#).unwrap();
|
||||
}
|
||||
|
||||
fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
@@ -98,10 +96,12 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
|
||||
let lower_code = code.to_lowercase();
|
||||
let param_start = lower_code.find("param")?;
|
||||
|
||||
// Verify that only comments and whitespace appear before "param"
|
||||
// Verify that only comments, whitespace, and [CmdletBinding()] appear before "param"
|
||||
let before_param = &code[..param_start];
|
||||
let mut chars = before_param.chars().peekable();
|
||||
let mut in_block_comment = false;
|
||||
let mut in_attribute_bracket = false;
|
||||
let mut bracket_depth = 0;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if in_block_comment {
|
||||
@@ -110,6 +110,19 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
|
||||
chars.next(); // consume '>'
|
||||
in_block_comment = false;
|
||||
}
|
||||
} else if in_attribute_bracket {
|
||||
// Track bracket depth to handle nested brackets/parens in attributes
|
||||
match ch {
|
||||
'[' => bracket_depth += 1,
|
||||
']' => {
|
||||
bracket_depth -= 1;
|
||||
if bracket_depth == 0 {
|
||||
in_attribute_bracket = false;
|
||||
}
|
||||
}
|
||||
// Allow parentheses inside attributes (e.g., [CmdletBinding()])
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match ch {
|
||||
// Start of block comment: <#
|
||||
@@ -126,6 +139,11 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
// Start of attribute bracket (e.g., [CmdletBinding()])
|
||||
'[' => {
|
||||
in_attribute_bracket = true;
|
||||
bracket_depth = 1;
|
||||
}
|
||||
// Whitespace is allowed
|
||||
c if c.is_whitespace() => {}
|
||||
// Any other character means there's code before param
|
||||
@@ -134,8 +152,8 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
|
||||
}
|
||||
}
|
||||
|
||||
// If we're still in a block comment at the end, it's unclosed - invalid
|
||||
if in_block_comment {
|
||||
// If we're still in a block comment or unclosed attribute bracket, it's invalid
|
||||
if in_block_comment || in_attribute_bracket {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -214,108 +232,269 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
|
||||
None
|
||||
}
|
||||
|
||||
enum ParserState {
|
||||
Normal,
|
||||
InSingleQuote,
|
||||
InDoubleQuote,
|
||||
}
|
||||
fn split_pwsh_args(code: &str) -> Vec<&str> {
|
||||
let mut chars = code.char_indices().peekable();
|
||||
let mut state = ParserState::Normal;
|
||||
let mut splits = vec![];
|
||||
let mut last_idx = 0;
|
||||
while let Some((idx, char)) = chars.next() {
|
||||
match (&state, char) {
|
||||
(ParserState::Normal, '\'') => {
|
||||
state = ParserState::InSingleQuote;
|
||||
}
|
||||
(ParserState::Normal, '"') => {
|
||||
state = ParserState::InDoubleQuote;
|
||||
}
|
||||
(ParserState::InSingleQuote, '\'') => {
|
||||
state = ParserState::Normal;
|
||||
}
|
||||
(ParserState::InDoubleQuote, '"') => {
|
||||
state = ParserState::Normal;
|
||||
}
|
||||
(ParserState::Normal, ',') => {
|
||||
splits.push(&code[last_idx..idx]);
|
||||
last_idx = idx + 1; // skip the comma
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if last_idx < code.len() {
|
||||
splits.push(&code[last_idx..]);
|
||||
}
|
||||
|
||||
splits
|
||||
}
|
||||
|
||||
fn parse_powershell_single_typ(typ: &str) -> Typ {
|
||||
match typ.to_lowercase().as_str() {
|
||||
"string" => Typ::Str(None),
|
||||
"int" | "long" => Typ::Int,
|
||||
"decimal" | "double" | "single" => Typ::Float,
|
||||
"datetime" => Typ::Datetime,
|
||||
"bool" => Typ::Bool,
|
||||
"bool" | "switch" => Typ::Bool,
|
||||
"pscustomobject" => Typ::Object(ObjectType::new(None, None)),
|
||||
_ => Typ::Str(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let param_wrapper = extract_powershell_param_block(code, false);
|
||||
let mut args = vec![];
|
||||
if let Some(param_wrapper) = param_wrapper {
|
||||
let params = split_pwsh_args(param_wrapper);
|
||||
for param in params {
|
||||
if let Some(cap) = RE_POWERSHELL_ARGS.captures(param) {
|
||||
let typ = cap.get(1).map(|x| x.as_str().to_string());
|
||||
let name = cap.get(2).unwrap().as_str().to_string();
|
||||
/// Single-pass PowerShell parameter parser.
|
||||
/// Parses the content of a param() block and extracts all parameter information.
|
||||
///
|
||||
/// This function processes PowerShell parameter declarations in a single pass, handling:
|
||||
/// - Parameter attributes: [Parameter(Mandatory)], [Parameter(Mandatory=$true)], [ValidateSet(...)], etc.
|
||||
/// - Type annotations: [string], [int[]], [PSCustomObject], etc.
|
||||
/// - Variable names: $Name, $Value, etc.
|
||||
/// - Default values: = 'text', = 25, = $env:VAR, etc.
|
||||
/// - Mandatory detection: Parameters with Mandatory attribute are marked as required
|
||||
fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum State {
|
||||
Normal,
|
||||
InSingleQuote,
|
||||
InDoubleQuote,
|
||||
InBracket,
|
||||
}
|
||||
|
||||
let mut parsed_typ = if let Some(typ) = typ {
|
||||
if typ.as_str().ends_with("[]") {
|
||||
Some(Typ::List(Box::new(parse_powershell_single_typ(
|
||||
typ.as_str().strip_suffix("[]").unwrap(),
|
||||
))))
|
||||
} else {
|
||||
Some(parse_powershell_single_typ(typ.as_str()))
|
||||
let mut args = Vec::new();
|
||||
let mut chars = content.char_indices().peekable();
|
||||
let mut state = State::Normal;
|
||||
let mut bracket_depth: i32 = 0;
|
||||
let mut paren_depth: i32 = 0;
|
||||
|
||||
// Current parameter being built
|
||||
let mut type_annotation: Option<String> = None;
|
||||
let mut var_name: Option<String> = None;
|
||||
let mut default_value: Option<String> = None;
|
||||
let mut is_mandatory = false;
|
||||
|
||||
// Track position for extracting text
|
||||
let mut last_bracket_start = None;
|
||||
let mut found_dollar = false;
|
||||
|
||||
while let Some((idx, ch)) = chars.next() {
|
||||
match state {
|
||||
State::InSingleQuote => {
|
||||
if ch == '\'' {
|
||||
state = State::Normal;
|
||||
}
|
||||
}
|
||||
State::InDoubleQuote => {
|
||||
if ch == '"' {
|
||||
// Check for escape character
|
||||
if idx > 0 && content.chars().nth(idx - 1) != Some('`') {
|
||||
state = State::Normal;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
State::InBracket => {
|
||||
match ch {
|
||||
'[' => bracket_depth += 1,
|
||||
']' => {
|
||||
bracket_depth -= 1;
|
||||
if bracket_depth == 0 {
|
||||
// Extract the bracket content
|
||||
if let Some(start) = last_bracket_start {
|
||||
let bracket_content = &content[start + 1..idx];
|
||||
|
||||
let default = if let Some(x) = cap.get(3) {
|
||||
Some(json!(x.as_str().to_string()))
|
||||
} else if let Some(x) = cap.get(4) {
|
||||
if parsed_typ.is_none() {
|
||||
if x.as_str().parse::<i64>().is_ok() {
|
||||
parsed_typ = Some(Typ::Int);
|
||||
} else if x.as_str().parse::<f64>().is_ok() {
|
||||
parsed_typ = Some(Typ::Float);
|
||||
// Check if this is a Parameter attribute with Mandatory (case-insensitive)
|
||||
let lower = bracket_content.to_lowercase();
|
||||
if lower.starts_with("parameter(") || lower.starts_with("parameter ") {
|
||||
// Check for Mandatory (case-insensitive)
|
||||
if lower.contains("mandatory") {
|
||||
// Check if it's explicitly set to false
|
||||
if !lower.contains("mandatory=$false") && !lower.contains("mandatory = $false") {
|
||||
is_mandatory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this looks like a type (simple word, possibly with [])
|
||||
let is_type = !bracket_content.contains('(')
|
||||
&& !bracket_content.contains('=')
|
||||
&& (bracket_content.chars().next().unwrap_or(' ').is_alphabetic()
|
||||
|| bracket_content.starts_with('['));
|
||||
|
||||
if is_type && !found_dollar {
|
||||
type_annotation = Some(bracket_content.to_string());
|
||||
}
|
||||
}
|
||||
state = State::Normal;
|
||||
last_bracket_start = None;
|
||||
}
|
||||
}
|
||||
serde_json::Number::from_str(x.as_str())
|
||||
.ok()
|
||||
.map(serde_json::Value::Number)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
'(' => paren_depth += 1,
|
||||
')' => paren_depth = paren_depth.saturating_sub(1),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
State::Normal => {
|
||||
match ch {
|
||||
'\'' => state = State::InSingleQuote,
|
||||
'"' => state = State::InDoubleQuote,
|
||||
'[' => {
|
||||
state = State::InBracket;
|
||||
bracket_depth = 1;
|
||||
last_bracket_start = Some(idx);
|
||||
}
|
||||
'$' => {
|
||||
found_dollar = true;
|
||||
// Extract variable name
|
||||
let name_start = idx + 1;
|
||||
let mut name_end = name_start;
|
||||
while let Some(&(_, next_ch)) = chars.peek() {
|
||||
if next_ch.is_alphanumeric() || next_ch == '_' {
|
||||
name_end += 1;
|
||||
chars.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
var_name = Some(content[name_start..name_end].to_string());
|
||||
}
|
||||
'=' if found_dollar => {
|
||||
// Extract default value
|
||||
// Skip whitespace after =
|
||||
while let Some(&(_, next_ch)) = chars.peek() {
|
||||
if next_ch.is_whitespace() {
|
||||
chars.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
args.push(Arg {
|
||||
name: name,
|
||||
typ: parsed_typ.unwrap_or(Typ::Str(None)),
|
||||
default: default.clone(),
|
||||
otyp: None,
|
||||
has_default: default.is_some(),
|
||||
oidx: None,
|
||||
});
|
||||
let default_start = chars.peek().map(|(i, _)| *i).unwrap_or(content.len());
|
||||
let mut default_end = default_start;
|
||||
let mut in_string = false;
|
||||
let mut string_char = ' ';
|
||||
|
||||
while let Some((i, ch)) = chars.peek().copied() {
|
||||
if in_string {
|
||||
if ch == string_char && content.chars().nth(i.saturating_sub(1)) != Some('`') {
|
||||
in_string = false;
|
||||
default_end = i + 1;
|
||||
chars.next();
|
||||
} else {
|
||||
default_end = i + 1;
|
||||
chars.next();
|
||||
}
|
||||
} else if ch == '\'' || ch == '"' {
|
||||
in_string = true;
|
||||
string_char = ch;
|
||||
default_end = i + 1;
|
||||
chars.next();
|
||||
} else if ch == ',' {
|
||||
break;
|
||||
} else if ch.is_whitespace() && chars.clone().skip(1).next().map(|(_, c)| c) == Some(',') {
|
||||
break;
|
||||
} else {
|
||||
default_end = i + 1;
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
|
||||
default_value = Some(content[default_start..default_end].trim().to_string());
|
||||
}
|
||||
',' => {
|
||||
// End of parameter, finalize it
|
||||
if let Some(name) = var_name.take() {
|
||||
args.push(finalize_parameter(name, type_annotation.take(), default_value.take(), is_mandatory)?);
|
||||
}
|
||||
|
||||
// Reset for next parameter
|
||||
type_annotation = None;
|
||||
var_name = None;
|
||||
default_value = None;
|
||||
is_mandatory = false;
|
||||
found_dollar = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(args))
|
||||
|
||||
// Finalize last parameter
|
||||
if let Some(name) = var_name {
|
||||
args.push(finalize_parameter(name, type_annotation, default_value, is_mandatory)?);
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn finalize_parameter(
|
||||
name: String,
|
||||
type_annotation: Option<String>,
|
||||
default_value: Option<String>,
|
||||
is_mandatory: bool,
|
||||
) -> anyhow::Result<Arg> {
|
||||
// Store the original PowerShell type for use in the executor
|
||||
let otyp = type_annotation.clone();
|
||||
|
||||
let mut parsed_typ = if let Some(typ) = type_annotation {
|
||||
if typ.ends_with("[]") {
|
||||
Some(Typ::List(Box::new(parse_powershell_single_typ(
|
||||
typ.strip_suffix("[]").unwrap(),
|
||||
))))
|
||||
} else {
|
||||
Some(parse_powershell_single_typ(&typ))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let default = if let Some(default_str) = default_value {
|
||||
// Try to parse as string (quoted)
|
||||
if (default_str.starts_with('"') && default_str.ends_with('"'))
|
||||
|| (default_str.starts_with('\'') && default_str.ends_with('\''))
|
||||
{
|
||||
Some(json!(default_str[1..default_str.len() - 1].to_string()))
|
||||
} else {
|
||||
// Try to parse as number
|
||||
if parsed_typ.is_none() {
|
||||
if default_str.parse::<i64>().is_ok() {
|
||||
parsed_typ = Some(Typ::Int);
|
||||
} else if default_str.parse::<f64>().is_ok() {
|
||||
parsed_typ = Some(Typ::Float);
|
||||
}
|
||||
}
|
||||
serde_json::Number::from_str(&default_str)
|
||||
.ok()
|
||||
.map(serde_json::Value::Number)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// has_default semantics:
|
||||
// - true: parameter is optional (has a default value OR is not mandatory)
|
||||
// - false: parameter is required (marked as Mandatory AND no default value)
|
||||
// Simplified: A parameter is optional unless it's mandatory without a default
|
||||
let has_default = default.is_some() || !is_mandatory;
|
||||
|
||||
Ok(Arg {
|
||||
name,
|
||||
typ: parsed_typ.unwrap_or(Typ::Str(None)),
|
||||
default: default.clone(),
|
||||
otyp,
|
||||
has_default,
|
||||
oidx: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let param_wrapper = extract_powershell_param_block(code, false);
|
||||
if let Some(param_wrapper) = param_wrapper {
|
||||
Ok(Some(parse_powershell_parameters(param_wrapper)?))
|
||||
} else {
|
||||
Ok(Some(vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -402,23 +581,23 @@ non_required="${5:-}"
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: None, // No type annotation
|
||||
name: "Msg".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
has_default: true, // Optional (not mandatory)
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: Some("string".to_string()), // [string]
|
||||
name: "Msg2".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
has_default: true, // Optional (not mandatory)
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: None, // No type annotation
|
||||
name: "Dflt".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: Some(json!("default value, with comma")),
|
||||
@@ -426,7 +605,7 @@ non_required="${5:-}"
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: Some("int".to_string()), // [int]
|
||||
name: "Nb".to_string(),
|
||||
typ: Typ::Int,
|
||||
default: Some(json!(3)),
|
||||
@@ -434,7 +613,7 @@ non_required="${5:-}"
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: None, // Type inferred from default value
|
||||
name: "Nb2".to_string(),
|
||||
typ: Typ::Float,
|
||||
default: Some(json!(5.0)),
|
||||
@@ -442,7 +621,7 @@ non_required="${5:-}"
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: None, // Type inferred from default value
|
||||
name: "Nb3".to_string(),
|
||||
typ: Typ::Int,
|
||||
default: Some(json!(5)),
|
||||
@@ -450,35 +629,35 @@ non_required="${5:-}"
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: None, // No type annotation
|
||||
name: "Wahoo".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
has_default: true, // Optional (not mandatory)
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: Some("PSCustomObject".to_string()), // [PSCustomObject]
|
||||
name: "Obj".to_string(),
|
||||
typ: Typ::Object(ObjectType::new(None, None)),
|
||||
default: None,
|
||||
has_default: false,
|
||||
has_default: true, // Optional (not mandatory)
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: Some("string[]".to_string()), // [string[]]
|
||||
name: "Arr".to_string(),
|
||||
typ: Typ::List(Box::new(Typ::Str(None))),
|
||||
default: None,
|
||||
has_default: false,
|
||||
has_default: true, // Optional (not mandatory)
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
otyp: Some("string".to_string()), // [string] (last type bracket with Mandatory)
|
||||
name: "Message".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
has_default: false, // Required (Mandatory attribute)
|
||||
oidx: None
|
||||
}
|
||||
],
|
||||
@@ -602,6 +781,242 @@ non_required="${5:-}"
|
||||
extract_powershell_param_block("function test-x{ param($Name)\n}", false),
|
||||
None
|
||||
);
|
||||
|
||||
// Valid: [CmdletBinding()] before param
|
||||
assert_eq!(
|
||||
extract_powershell_param_block("[CmdletBinding()]\nparam($Name)", false),
|
||||
Some("$Name")
|
||||
);
|
||||
assert_eq!(
|
||||
extract_powershell_param_block("[CmdletBinding()]\nparam($Name, $Age)", true),
|
||||
Some("param($Name, $Age)")
|
||||
);
|
||||
|
||||
// Valid: [CmdletBinding()] with options before param
|
||||
assert_eq!(
|
||||
extract_powershell_param_block(
|
||||
"[CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)",
|
||||
false
|
||||
),
|
||||
Some("$Path")
|
||||
);
|
||||
|
||||
// Valid: Multiple attributes before param
|
||||
assert_eq!(
|
||||
extract_powershell_param_block(
|
||||
"[CmdletBinding()]\n[OutputType([string])]\nparam($Value)",
|
||||
false
|
||||
),
|
||||
Some("$Value")
|
||||
);
|
||||
|
||||
// Valid: CmdletBinding with comments
|
||||
assert_eq!(
|
||||
extract_powershell_param_block(
|
||||
"# My function\n[CmdletBinding()]\nparam($Name)",
|
||||
false
|
||||
),
|
||||
Some("$Name")
|
||||
);
|
||||
|
||||
// Valid: CmdletBinding with whitespace variations
|
||||
assert_eq!(
|
||||
extract_powershell_param_block(
|
||||
"[CmdletBinding()] \n param($Name)",
|
||||
false
|
||||
),
|
||||
Some("$Name")
|
||||
);
|
||||
|
||||
// Invalid: Unclosed attribute bracket
|
||||
assert_eq!(
|
||||
extract_powershell_param_block("[CmdletBinding(\nparam($Name)", false),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_powershell_sig_with_parameter_attributes() -> anyhow::Result<()> {
|
||||
// Test with [Parameter(Mandatory=$true)] attribute
|
||||
let code = r#"[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Name,
|
||||
[Parameter(Mandatory=$false)]
|
||||
[int]$Age = 25
|
||||
)"#;
|
||||
let result = parse_powershell_sig(code)?;
|
||||
assert_eq!(result.args.len(), 2);
|
||||
assert_eq!(result.args[0].name, "Name");
|
||||
assert_eq!(result.args[0].typ, Typ::Str(None));
|
||||
assert_eq!(result.args[0].has_default, false);
|
||||
assert_eq!(result.args[1].name, "Age");
|
||||
assert_eq!(result.args[1].typ, Typ::Int);
|
||||
assert_eq!(result.args[1].has_default, true);
|
||||
assert_eq!(result.args[1].default, Some(json!(25)));
|
||||
|
||||
// Test with complex attributes
|
||||
let code2 = r#"param(
|
||||
[Parameter(Mandatory=$true, Position=0)]
|
||||
[ValidateSet('Red', 'Green', 'Blue')]
|
||||
[string]$Color,
|
||||
[Parameter(ValueFromPipeline=$true)]
|
||||
[string[]]$Items
|
||||
)"#;
|
||||
let result2 = parse_powershell_sig(code2)?;
|
||||
assert_eq!(result2.args.len(), 2);
|
||||
assert_eq!(result2.args[0].name, "Color");
|
||||
assert_eq!(result2.args[0].typ, Typ::Str(None));
|
||||
assert_eq!(result2.args[1].name, "Items");
|
||||
assert_eq!(result2.args[1].typ, Typ::List(Box::new(Typ::Str(None))));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_powershell_single_pass_parser() -> anyhow::Result<()> {
|
||||
// Test the single-pass parser with a complex real-world example
|
||||
let code = r#"[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0, HelpMessage="Enter the server name")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$ServerName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateRange(1, 65535)]
|
||||
[int]$Port = 8080,
|
||||
|
||||
[Parameter(ValueFromPipeline=$true)]
|
||||
[string[]]$LogFiles,
|
||||
|
||||
[ValidateSet('Debug', 'Info', 'Warning', 'Error')]
|
||||
[string]$LogLevel = 'Info',
|
||||
|
||||
[PSCustomObject]$Config
|
||||
)"#;
|
||||
let result = parse_powershell_sig(code)?;
|
||||
|
||||
assert_eq!(result.args.len(), 5);
|
||||
|
||||
// ServerName: mandatory string with no default
|
||||
assert_eq!(result.args[0].name, "ServerName");
|
||||
assert_eq!(result.args[0].typ, Typ::Str(None));
|
||||
assert_eq!(result.args[0].has_default, false);
|
||||
|
||||
// Port: optional int with default
|
||||
assert_eq!(result.args[1].name, "Port");
|
||||
assert_eq!(result.args[1].typ, Typ::Int);
|
||||
assert_eq!(result.args[1].default, Some(json!(8080)));
|
||||
assert_eq!(result.args[1].has_default, true);
|
||||
|
||||
// LogFiles: string array (no mandatory, so optional)
|
||||
assert_eq!(result.args[2].name, "LogFiles");
|
||||
assert_eq!(result.args[2].typ, Typ::List(Box::new(Typ::Str(None))));
|
||||
assert_eq!(result.args[2].has_default, true); // Optional (not mandatory)
|
||||
|
||||
// LogLevel: string with default (ValidateSet is ignored but doesn't break parsing)
|
||||
assert_eq!(result.args[3].name, "LogLevel");
|
||||
assert_eq!(result.args[3].typ, Typ::Str(None));
|
||||
assert_eq!(result.args[3].default, Some(json!("Info")));
|
||||
assert_eq!(result.args[3].has_default, true);
|
||||
|
||||
// Config: PSCustomObject (no mandatory, so optional)
|
||||
assert_eq!(result.args[4].name, "Config");
|
||||
assert_eq!(result.args[4].typ, Typ::Object(ObjectType::new(None, None)));
|
||||
assert_eq!(result.args[4].has_default, true); // Optional (not mandatory)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_powershell_mandatory_attribute() -> anyhow::Result<()> {
|
||||
// Test various forms of the Mandatory attribute
|
||||
let code = r#"param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RequiredNoEquals,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$RequiredWithTrue,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RequiredWithSpaces,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$NotRequired,
|
||||
|
||||
[Parameter(Position=0)]
|
||||
[string]$NoMandatory,
|
||||
|
||||
[string]$PlainRequired = "default",
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[int]$RequiredInt
|
||||
)"#;
|
||||
let result = parse_powershell_sig(code)?;
|
||||
|
||||
assert_eq!(result.args.len(), 7);
|
||||
|
||||
// RequiredNoEquals: mandatory without =$true
|
||||
assert_eq!(result.args[0].name, "RequiredNoEquals");
|
||||
assert_eq!(result.args[0].has_default, false); // Required (mandatory, no default)
|
||||
|
||||
// RequiredWithTrue: mandatory with =$true
|
||||
assert_eq!(result.args[1].name, "RequiredWithTrue");
|
||||
assert_eq!(result.args[1].has_default, false); // Required
|
||||
|
||||
// RequiredWithSpaces: mandatory with spaces
|
||||
assert_eq!(result.args[2].name, "RequiredWithSpaces");
|
||||
assert_eq!(result.args[2].has_default, false); // Required
|
||||
|
||||
// NotRequired: explicitly Mandatory=$false
|
||||
assert_eq!(result.args[3].name, "NotRequired");
|
||||
assert_eq!(result.args[3].has_default, true); // Optional (not mandatory)
|
||||
|
||||
// NoMandatory: no Mandatory attribute
|
||||
assert_eq!(result.args[4].name, "NoMandatory");
|
||||
assert_eq!(result.args[4].has_default, true); // Optional (not mandatory)
|
||||
|
||||
// PlainRequired: has default value (always optional)
|
||||
assert_eq!(result.args[5].name, "PlainRequired");
|
||||
assert_eq!(result.args[5].has_default, true); // Optional (has default)
|
||||
assert_eq!(result.args[5].default, Some(json!("default")));
|
||||
|
||||
// RequiredInt: mandatory int
|
||||
assert_eq!(result.args[6].name, "RequiredInt");
|
||||
assert_eq!(result.args[6].typ, Typ::Int);
|
||||
assert_eq!(result.args[6].has_default, false); // Required
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_powershell_case_insensitive_parameter() -> anyhow::Result<()> {
|
||||
// Test that [parameter(...)] is case-insensitive
|
||||
let code = r#"param(
|
||||
[parameter(Mandatory)]
|
||||
[string]$LowerCase,
|
||||
|
||||
[PARAMETER(MANDATORY=$TRUE)]
|
||||
[string]$UpperCase,
|
||||
|
||||
[Parameter(mandatory=$true)]
|
||||
[string]$MixedCase
|
||||
)"#;
|
||||
let result = parse_powershell_sig(code)?;
|
||||
|
||||
assert_eq!(result.args.len(), 3);
|
||||
|
||||
// All should be detected as mandatory
|
||||
assert_eq!(result.args[0].name, "LowerCase");
|
||||
assert_eq!(result.args[0].has_default, false);
|
||||
|
||||
assert_eq!(result.args[1].name, "UpperCase");
|
||||
assert_eq!(result.args[1].has_default, false);
|
||||
|
||||
assert_eq!(result.args[2].name, "MixedCase");
|
||||
assert_eq!(result.args[2].has_default, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -255,20 +255,42 @@ pub async fn handle_powershell_job(
|
||||
job.args.as_ref()
|
||||
};
|
||||
|
||||
let args_owned = windmill_parser_bash::parse_powershell_sig(&content)?
|
||||
let parsed_sig = windmill_parser_bash::parse_powershell_sig(&content)?;
|
||||
|
||||
parsed_sig
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
(
|
||||
arg.name.clone(),
|
||||
job_args.and_then(|x| x.get(&arg.name).map(|x| raw_to_pwsh_param(x.get()))),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(String, Option<String>)>>();
|
||||
.filter_map(|arg| {
|
||||
let value_opt = job_args.and_then(|x| x.get(&arg.name));
|
||||
|
||||
args_owned
|
||||
.into_iter()
|
||||
.filter_map(|(n, v)| v.map(|v| format!("-{n} {v}")))
|
||||
// Check if this is a switch parameter (only [switch], not [bool])
|
||||
let is_switch = arg.otyp.as_ref().map(|t| {
|
||||
t.to_lowercase() == "switch"
|
||||
}).unwrap_or(false);
|
||||
|
||||
if is_switch {
|
||||
// Handle switch parameters: -SwitchName or omit
|
||||
if let Some(value) = value_opt {
|
||||
match serde_json::from_str::<serde_json::Value>(value.get()) {
|
||||
Ok(serde_json::Value::Bool(true)) => {
|
||||
// Switch is enabled: just pass -SwitchName
|
||||
Some(format!("-{}", arg.name))
|
||||
}
|
||||
Ok(serde_json::Value::Bool(false)) | _ => {
|
||||
// Switch is disabled or invalid: omit the parameter
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No value provided, omit the switch (defaults to false)
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// Regular parameter (including [bool]): format as -ParamName Value
|
||||
// For [bool] parameters, this will be -ParamName $true or -ParamName $false
|
||||
value_opt.map(|v| format!("-{} {}", arg.name, raw_to_pwsh_param(v.get())))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
};
|
||||
|
||||
+8
-8
@@ -1,14 +1,14 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export function parse_assets_sql(code: string): string;
|
||||
export function parse_db_resource(code: string): string | undefined;
|
||||
export function parse_bash(code: string): string;
|
||||
export function parse_mssql(code: string): string;
|
||||
export function parse_oracledb(code: string): string;
|
||||
export function parse_powershell(code: string): string;
|
||||
export function parse_bigquery(code: string): string;
|
||||
export function parse_oracledb(code: string): string;
|
||||
export function parse_graphql(code: string): string;
|
||||
export function parse_snowflake(code: string): string;
|
||||
export function parse_duckdb(code: string): string;
|
||||
export function parse_sql(code: string): string;
|
||||
export function parse_assets_sql(code: string): string;
|
||||
export function parse_mysql(code: string): string;
|
||||
export function parse_bash(code: string): string;
|
||||
export function parse_duckdb(code: string): string;
|
||||
export function parse_db_resource(code: string): string | undefined;
|
||||
export function parse_bigquery(code: string): string;
|
||||
export function parse_snowflake(code: string): string;
|
||||
export function parse_sql(code: string): string;
|
||||
|
||||
@@ -68,60 +68,6 @@ function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return decodeText(ptr, len);
|
||||
}
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_assets_sql(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_assets_sql(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function parse_db_resource(code) {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_db_resource(ptr0, len0);
|
||||
let v2;
|
||||
if (ret[0] !== 0) {
|
||||
v2 = getStringFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
}
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_bash(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_bash(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
@@ -141,25 +87,6 @@ export function parse_mssql(code) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_oracledb(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_oracledb(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
@@ -183,13 +110,13 @@ export function parse_powershell(code) {
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_bigquery(code) {
|
||||
export function parse_oracledb(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_bigquery(ptr0, len0);
|
||||
const ret = wasm.parse_oracledb(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
@@ -221,13 +148,51 @@ export function parse_graphql(code) {
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_snowflake(code) {
|
||||
export function parse_assets_sql(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_snowflake(ptr0, len0);
|
||||
const ret = wasm.parse_assets_sql(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_mysql(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_mysql(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_bash(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_bash(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
@@ -255,17 +220,33 @@ export function parse_duckdb(code) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function parse_db_resource(code) {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_db_resource(ptr0, len0);
|
||||
let v2;
|
||||
if (ret[0] !== 0) {
|
||||
v2 = getStringFromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
}
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_sql(code) {
|
||||
export function parse_bigquery(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_sql(ptr0, len0);
|
||||
const ret = wasm.parse_bigquery(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
@@ -278,13 +259,32 @@ export function parse_sql(code) {
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_mysql(code) {
|
||||
export function parse_snowflake(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_mysql(ptr0, len0);
|
||||
const ret = wasm.parse_snowflake(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_sql(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.parse_sql(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
|
||||
Binary file not shown.
Generated
+4
-4
@@ -79,7 +79,7 @@
|
||||
"windmill-parser-wasm-nu": "1.510.1",
|
||||
"windmill-parser-wasm-php": "1.574.1",
|
||||
"windmill-parser-wasm-py": "1.538.0",
|
||||
"windmill-parser-wasm-regex": "1.574.1",
|
||||
"windmill-parser-wasm-regex": "1.575.4",
|
||||
"windmill-parser-wasm-ruby": "1.526.1",
|
||||
"windmill-parser-wasm-rust": "1.558.1",
|
||||
"windmill-parser-wasm-ts": "1.565.0",
|
||||
@@ -13759,9 +13759,9 @@
|
||||
"integrity": "sha512-s+bdIgT/fA5em3zYUwF8D14uA/dZh7iu0krZYZQqZUO7txN37hwSCVfovbMkIwm4zPbsJ50mU8DRLt7UpAPZIw=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-regex": {
|
||||
"version": "1.574.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.574.1.tgz",
|
||||
"integrity": "sha512-KnNBnpTGcnBPSzTQHdsjTPJbRQ81iem98eUnMGO8Zt1qgBFJ/eFZPwmUg6TZMbMITzTi0QqRCJj08eQID1FcPg=="
|
||||
"version": "1.575.4",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.575.4.tgz",
|
||||
"integrity": "sha512-G7Ni8XxdSKLNew50B1KRmwn/z9qq3obpEDaaDFS7Z74MeUdaBB/TD69SY6RxDL8SZrh0Tl7SCbS/GGLnt1AzkQ=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-ruby": {
|
||||
"version": "1.526.1",
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
"windmill-parser-wasm-nu": "1.510.1",
|
||||
"windmill-parser-wasm-php": "1.574.1",
|
||||
"windmill-parser-wasm-py": "1.538.0",
|
||||
"windmill-parser-wasm-regex": "1.574.1",
|
||||
"windmill-parser-wasm-regex": "1.575.4",
|
||||
"windmill-parser-wasm-ruby": "1.526.1",
|
||||
"windmill-parser-wasm-rust": "1.558.1",
|
||||
"windmill-parser-wasm-ts": "1.565.0",
|
||||
|
||||
Reference in New Issue
Block a user