diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ba5054e..09c8673ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.0.12: +* Adding assert keyword (#143) +* Fixing new keyword for blank constructors (#142 ) +* Rest Transpiler: + * Handling multiple QueryString values (#139) + * Only passing ContentType to invoker if invoker supports it (#141) + * Defaulting to JSON body when ContentType is unspecified (#140) +--- + ## 0.0.11: * Source Generators Now Support Parameters / Arguments (#75) * Invoke-PipeScript Terminating Build Errors (#135) diff --git a/PipeScript.psd1 b/PipeScript.psd1 index 82499b1b5..778a0e6d1 100644 --- a/PipeScript.psd1 +++ b/PipeScript.psd1 @@ -1,22 +1,33 @@ @{ - ModuleVersion = '0.0.11' + ModuleVersion = '0.0.12' Description = 'An Extensible Transpiler for PowerShell (and anything else)' RootModule = 'PipeScript.psm1' PowerShellVersion = '4.0' AliasesToExport = '*' FormatsToProcess = 'PipeScript.format.ps1xml' TypesToProcess = 'PipeScript.types.ps1xml' - Guid = 'fc054786-b1ce-4ed8-a90f-7cc9c27edb06' - CompanyName='Start-Automating' - Copyright='2022 Start-Automating' - Author='James Brundage' + Guid = 'fc054786-b1ce-4ed8-a90f-7cc9c27edb06' + CompanyName = 'Start-Automating' + Copyright = '2022 Start-Automating' + Author = 'James Brundage' PrivateData = @{ PSData = @{ ProjectURI = 'https://github.com/StartAutomating/PipeScript' LicenseURI = 'https://github.com/StartAutomating/PipeScript/blob/main/LICENSE' - - Tags = 'PipeScript','PowerShell', 'Transpilation', 'Compiler' + RecommendModule = @('PSMinifier') + RelatedModule = @() + BuildModule = @('EZOut','Piecemeal','PipeScript','HelpOut', 'PSDevOps') + Tags = 'PipeScript','PowerShell', 'Transpilation', 'Compiler' ReleaseNotes = @' +## 0.0.12: +* Adding assert keyword (#143) +* Fixing new keyword for blank constructors (#142 ) +* Rest Transpiler: + * Handling multiple QueryString values (#139) + * Only passing ContentType to invoker if invoker supports it (#141) + * Defaulting to JSON body when ContentType is unspecified (#140) +--- + ## 0.0.11: * Source Generators Now Support Parameters / Arguments (#75) * Invoke-PipeScript Terminating Build Errors (#135) diff --git a/README.md b/README.md index 942cb661c..e5b90fd42 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,16 @@ # What Is PipeScript? -> PipeScript is a transpiled scripting language built atop of PowerShell. +PipeScript is a scripting language built atop PowerShell. -> PipeScript can be run interactively -> PipeScript can embedded within many languages to dynamically generate source code +PipeScript is transpiled into PowerShell. + + +PipeScript can be run interactively, or used to build more PowerShell with less code. + + +PipeScript can also be embedded in many other languages to dynamically generate source code. ## What's a Transpiler? diff --git a/Transpilers/Keywords/Assert.psx.ps1 b/Transpilers/Keywords/Assert.psx.ps1 new file mode 100644 index 000000000..398819733 --- /dev/null +++ b/Transpilers/Keywords/Assert.psx.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + Assert keyword +.DESCRIPTION + Assert is a common keyword in many programming languages. + + In PipeScript, Asset will take a condition and an optional action. + + The condtion may be contained in either parenthesis or a [ScriptBlock]. + + If there is no action, the assertion will throw an exception containing the condition. + + If the action is a string, the assertion will throw that error as a string. + + If the action is a ScriptBlock, it will be run if the assertion is false. + + Assertions will not be transpiled or included if -Verbose or -Debug has not been set. + + Additionally, while running, Assertions will be ignored if -Verbose or -Debug has not been set. +.EXAMPLE + # With no second argument, assert will throw an error with the condition of the assertion. + Invoke-PipeScript { + assert (1 -eq 1) + } -Debug +.EXAMPLE + # With a second argument of a string, assert will throw an error + Invoke-PipeScript { + assert ($true) "It's true" + } -Debug +.EXAMPLE + # Conditions can also be written as a ScriptBlock + Invoke-PipeScript { + assert {$true} "Process id '$pid' Asserted" + } -Verbose +.EXAMPLE + # If the assertion action was a ScriptBlock, no exception is automatically thrown + Invoke-PipeScript { + assert ($true) { Write-Information "Assertion was true"} + } -Verbose +#> +[ValidateScript({ + # This transpiler should run if the command is literally 'assert' + $commandAst = $_ + return ($commandAst -and $CommandAst.CommandElements[0].Value -eq 'assert') +})] +param( +# The CommandAst +[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='CommandAst')] +[Management.Automation.Language.CommandAst] +$CommandAst +) + +process { + $CommandName, $CommandArgs = $commandAst.CommandElements + $firstArg, $secondArg = $CommandArgs + + # If the first arg can be a condition in simple or complex form + if (-not $firstArg -or $firstArg.GetType().Name -notin + 'ParenExpressionAst', + 'ScriptBlockExpressionAst', + 'VariableExpressionAst', + 'MemberExpressionAst', + 'StringConstantExpressionAst', + 'ExpandableStringExpressionAst') { + # If it was the wrong type, let them know. + Write-Error "Assert must be followed by one of the following expressions: +* Variable +* Member +* String +* Parenthesis +* ScriptBlock +" + return + } + + # If there was a second argument, it must be a string or ScriptBlock. + if ($secondArg -and $secondArg.GetType().Name -notin + 'ScriptBlockExpressionAst', + 'StringConstantExpressionAst', + 'ExpandableStringExpressionAst') { + Write-Error "Assert must be followed by a ScriptBlock or string" + return + } + + # We need to create a [ScriptBlock] for the condition so we can transpile it. + $firstArgTypeName = $firstArg.GetType().Name + # The condition will always check for -DebugPreference or -VerbosePreference. + $checkDebugPreference = '($debugPreference,$verbosePreference -ne ''silentlyContinue'')' + + $condition = + [ScriptBlock]::Create("($checkDebugPreference -and $( + # If the condition is already in parenthesis, + if ($firstArgTypeName -eq 'ParenExpressionAst') { + "$FirstArg" # leave it alone. + } + # If the condition is a ScriptBlockExpression, + elseif ($firstArgTypeName -eq 'ScriptBlockExpressionAst') + { + # put it in parenthesis. + "($($FirstArg -replace '^\{' -replace '\}$'))" + } + # Otherwise + else + { + "($FirstArg)" # embed the condition in parenthesis. + } + ))") + + # Transpile the condition. + $condition = $condition | .>Pipescript + + # Now we create the entire assertion script + $newScript = + # If there was no second argument + if (-not $secondArg) { + # Rethrow the condition + "if $condition { throw '{$($firstArg -replace "'", "''")}' } " + } elseif ($secondArg.GetType().Name -eq 'ScriptBlockExpressionAst') { + # If the second argument was a script, transpile and embed it. + "if $condition {$([ScriptBlock]::Create( + ($secondArg -replace '^\{' -replace '\}$') + ) | .>Pipescript)}" + } else { + # Otherwise, throw the second argument. + "if $condition { throw $secondArg } " + } + + if ($DebugPreference, $VerbosePreference -ne 'silentlyContinue') { + [scriptblock]::Create($newScript) + } else { + {} + } +} diff --git a/Transpilers/Keywords/New.psx.ps1 b/Transpilers/Keywords/New.psx.ps1 index fb6fc075c..b28d80fc5 100644 --- a/Transpilers/Keywords/New.psx.ps1 +++ b/Transpilers/Keywords/New.psx.ps1 @@ -19,6 +19,8 @@ .> { new byte 1 } .EXAMPLE .> { new int[] 5 } +.EXAMPLE + .> { new Timespan } .EXAMPLE .> { new datetime 12/31/1999 } .EXAMPLE @@ -97,7 +99,13 @@ process { $constructorArguments[0] -is [string]) { "[$newTypeName]::parse(" + ($constructorArguments -join ',') + ")" } elseif ($realNewType::new) { - "[$newTypeName]::new(" + ($constructorArguments -join ',') + ")" + if ($constructorArguments) { + "[$newTypeName]::new(" + ($constructorArguments -join ',') + ")" + } elseif ($realNewType::new.overloadDefinitions -notmatch '\(\)$') { + "[$newTypeName]::new(`$null)" + } else { + "[$newTypeName]::new()" + } } elseif ($realNewType.IsPrimitive) { if ($constructorArguments) { if ($constructorArguments.Length -eq 1) { diff --git a/Transpilers/Keywords/README.md b/Transpilers/Keywords/README.md index 5bdeedee1..40ae82ec5 100644 --- a/Transpilers/Keywords/README.md +++ b/Transpilers/Keywords/README.md @@ -3,13 +3,54 @@ This directory and it's subdirectories contain additional language keywords with Most keywords will be implemented as a Transpiler that tranforms a CommandAST. -|DisplayName |Synopsis | -|------------------|----------------------------| -|[New](New.psx.ps1)|['new' keyword](New.psx.ps1)| +|DisplayName |Synopsis | +|------------------------|--------------------------------| +|[Assert](Assert.psx.ps1)|[Assert keyword](Assert.psx.ps1)| +|[New](New.psx.ps1) |['new' keyword](New.psx.ps1) | +## Assert Example 1 + + +~~~PowerShell + # With no second argument, assert will throw an error with the condition of the assertion. + Invoke-PipeScript { + assert (1 -eq 1) + } -Debug +~~~ + +## Assert Example 2 + + +~~~PowerShell + # With a second argument of a string, assert will throw an error + Invoke-PipeScript { + assert ($true) "It's true" + } -Debug +~~~ + +## Assert Example 3 + + +~~~PowerShell + # Conditions can also be written as a ScriptBlock + Invoke-PipeScript { + assert {$true} "Process id '$pid' Asserted" + } -Verbose +~~~ + +## Assert Example 4 + + +~~~PowerShell + # If the assertion action was a ScriptBlock, no exception is automatically thrown + Invoke-PipeScript { + assert ($true) { Write-Information "Assertion was true"} + } -Verbose +~~~ + ## New Example 1 @@ -35,19 +76,26 @@ Most keywords will be implemented as a Transpiler that tranforms a CommandAST. ~~~PowerShell - .> { new datetime 12/31/1999 } + .> { new Timespan } ~~~ ## New Example 5 ~~~PowerShell - .> { new @{RandomNumber = Get-Random; A ='b'}} + .> { new datetime 12/31/1999 } ~~~ ## New Example 6 +~~~PowerShell + .> { new @{RandomNumber = Get-Random; A ='b'}} +~~~ + +## New Example 7 + + ~~~PowerShell .> { new Diagnostics.ProcessStartInfo @{FileName='f'} } ~~~ diff --git a/Transpilers/Rest.psx.ps1 b/Transpilers/Rest.psx.ps1 index 163d452a1..9dd1a2517 100644 --- a/Transpilers/Rest.psx.ps1 +++ b/Transpilers/Rest.psx.ps1 @@ -137,6 +137,11 @@ If a parameter is a [switch], it will be turned into a [bool]. [PSObject] $QueryParameter, +# If provided, will join multiple values of a query by this string. +# If the string is '&', each value will be provided as a key-value pair. +[string] +$JoinQueryValue, + # A script block to be run on each output. [ScriptBlock] $ForEachOutput @@ -274,7 +279,7 @@ process { begin { $myCmd = $MyInvocation.MyCommand function ConvertRestInput { - param([Collections.IDictionary]$RestInput = @{}) + param([Collections.IDictionary]$RestInput = @{}, [switch]$ToQueryString) foreach ($ri in @($RestInput.GetEnumerator())) { $RestParameterAttributes = @($myCmd.Parameters[$ri.Key].Attributes) $restParameterName = $ri.Key @@ -297,7 +302,14 @@ process { $restParameterValue -as [bool] } else { - $restParameterValue + if ($ToQueryString -and + $restParameterValue -is [Array] -and + $JoinQueryValue) { + $restParameterValue -join $JoinQueryValue + } else { + $restParameterValue + } + } if ($restParameterValue -is [Collections.IDictionary]) { @@ -320,7 +332,7 @@ process { { begin { function ConvertRestInput { - param([Collections.IDictionary]$RestInput = @{}) + param([Collections.IDictionary]$RestInput = @{}, [switch]$ToQueryString) foreach ($ri in @($RestInput.GetEnumerator())) { $restParameterValue = $ri.Value @@ -332,7 +344,13 @@ process { $restParameterValue -as [bool] } else { - $restParameterValue + if ($ToQueryString -and + $restParameterValue -is [Array] -and + $JoinQueryValue) { + $restParameterValue -join $JoinQueryValue + } else { + $restParameterValue + } } $RestInput[$ri.Key] = $restParameterValue @@ -373,6 +391,7 @@ process { `$contentType = '$contentType' `$bodyParameterNames = @('$($bodyParameterNames -join "','")') `$queryParameterNames = @('$($queryParameterNames -join "','")') + `$joinQueryValue = '$joinQueryValue' `$uriParameterNames = @('$($uriParameterNames -join "','")') `$endpoints = @("$($endpoint -join "','")") `$ForEachOutput = { @@ -430,7 +449,7 @@ process { if ($method) { $invokeSplat.Method = $method } - if ($ContentType) { + if ($ContentType -and $invokerCommandInfo.Parameters.ContentType) { $invokeSplat.ContentType = $ContentType } } @@ -472,7 +491,7 @@ process { } { process { - $queryParams = ConvertRestInput $queryParams + $queryParams = ConvertRestInput $queryParams -ToQueryString if ($invokerCommandinfo.Parameters['QueryParameter'] -and $invokerCommandinfo.Parameters['QueryParameter'].ParameterType -eq [Collections.IDictionary]) { @@ -480,7 +499,14 @@ process { } else { $queryParamStr = @(foreach ($qp in $QueryParams.GetEnumerator()) { - "$($qp.Key)=$([Web.HttpUtility]::UrlEncode($qpValue).Replace('+', '%20'))" + $qpValue = $qp.value + if ($JoinQueryValue -eq '&') { + foreach ($qVal in $qpValue -split '&') { + "$($qp.Key)=$([Web.HttpUtility]::UrlEncode($qValue).Replace('+', '%20'))" + } + } else { + "$($qp.Key)=$([Web.HttpUtility]::UrlEncode($qpValue).Replace('+', '%20'))" + } }) -join '&' if ($invokeSplat.Uri.Contains('?')) { $invokeSplat.Uri = "$($invokeSplat.Uri)" + '&' + $queryParamStr @@ -520,7 +546,7 @@ process { @(foreach ($bodyPart in $completeBody.GetEnumerator()) { "$($bodyPart.Key.ToString().ToLower())=$([Web.HttpUtility]::UrlEncode($bodyPart.Value))" }) -join '&' - } elseif ($ContentType -match 'json') { + } elseif ($ContentType -match 'json' -or -not $ContentType) { ConvertTo-Json $completeBody } diff --git a/docs/Assert.md b/docs/Assert.md new file mode 100644 index 000000000..4c9cc73a3 --- /dev/null +++ b/docs/Assert.md @@ -0,0 +1,78 @@ + +Assert +------ +### Synopsis +Assert keyword + +--- +### Description + +Assert is a common keyword in many programming languages. + +In PipeScript, Asset will take a condition and an optional action. + +The condtion may be contained in either parenthesis or a [ScriptBlock]. + +If there is no action, the assertion will throw an exception containing the condition. + +If the action is a string, the assertion will throw that error as a string. + +If the action is a ScriptBlock, it will be run if the assertion is false. + +Assertions will not be transpiled or included if -Verbose or -Debug has not been set. + +Additionally, while running, Assertions will be ignored if -Verbose or -Debug has not been set. + +--- +### Examples +#### EXAMPLE 1 +```PowerShell +# With no second argument, assert will throw an error with the condition of the assertion. +Invoke-PipeScript { + assert (1 -eq 1) +} -Debug +``` + +#### EXAMPLE 2 +```PowerShell +# With a second argument of a string, assert will throw an error +Invoke-PipeScript { + assert ($true) "It's true" +} -Debug +``` + +#### EXAMPLE 3 +```PowerShell +# Conditions can also be written as a ScriptBlock +Invoke-PipeScript { + assert {$true} "Process id '$pid' Asserted" +} -Verbose +``` + +#### EXAMPLE 4 +```PowerShell +# If the assertion action was a ScriptBlock, no exception is automatically thrown +Invoke-PipeScript { + assert ($true) { Write-Information "Assertion was true"} +} -Verbose +``` + +--- +### Parameters +#### **CommandAst** + +The CommandAst + + + +|Type |Requried|Postion|PipelineInput | +|------------------|--------|-------|--------------| +|```[CommandAst]```|true |named |true (ByValue)| +--- +### Syntax +```PowerShell +Assert -CommandAst [] +``` +--- + + diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b8ba5054e..09c8673ef 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.0.12: +* Adding assert keyword (#143) +* Fixing new keyword for blank constructors (#142 ) +* Rest Transpiler: + * Handling multiple QueryString values (#139) + * Only passing ContentType to invoker if invoker supports it (#141) + * Defaulting to JSON body when ContentType is unspecified (#140) +--- + ## 0.0.11: * Source Generators Now Support Parameters / Arguments (#75) * Invoke-PipeScript Terminating Build Errors (#135) diff --git a/docs/New.md b/docs/New.md index 09228d8e9..8df886cdb 100644 --- a/docs/New.md +++ b/docs/New.md @@ -38,16 +38,21 @@ If 'new' #### EXAMPLE 4 ```PowerShell -{ new datetime 12/31/1999 } +{ new Timespan } ``` #### EXAMPLE 5 ```PowerShell -{ new @{RandomNumber = Get-Random; A ='b'}} +{ new datetime 12/31/1999 } ``` #### EXAMPLE 6 ```PowerShell +{ new @{RandomNumber = Get-Random; A ='b'}} +``` + +#### EXAMPLE 7 +```PowerShell { new Diagnostics.ProcessStartInfo @{FileName='f'} } ``` diff --git a/docs/README.md b/docs/README.md index 942cb661c..e5b90fd42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,11 +4,16 @@ # What Is PipeScript? -> PipeScript is a transpiled scripting language built atop of PowerShell. +PipeScript is a scripting language built atop PowerShell. -> PipeScript can be run interactively -> PipeScript can embedded within many languages to dynamically generate source code +PipeScript is transpiled into PowerShell. + + +PipeScript can be run interactively, or used to build more PowerShell with less code. + + +PipeScript can also be embedded in many other languages to dynamically generate source code. ## What's a Transpiler? diff --git a/docs/Rest.md b/docs/Rest.md index ed19eabf5..8ab81610c 100644 --- a/docs/Rest.md +++ b/docs/Rest.md @@ -210,6 +210,17 @@ If a parameter is a [switch], it will be turned into a [bool]. |----------------|--------|-------|-------------| |```[PSObject]```|false |named |false | --- +#### **JoinQueryValue** + +If provided, will join multiple values of a query by this string. +If the string is '&', each value will be provided as a key-value pair. + + + +|Type |Requried|Postion|PipelineInput| +|--------------|--------|-------|-------------| +|```[String]```|false |named |false | +--- #### **ForEachOutput** A script block to be run on each output. @@ -222,7 +233,7 @@ A script block to be run on each output. --- ### Syntax ```PowerShell -Rest [-ScriptBlock ] [-RESTEndpoint] [-ContentType ] [-Method ] [-InvokeCommand ] [-InvokeParameterVariable ] [-UriParameterHelp ] [-UriParameterType ] [-BodyParameter ] [-QueryParameter ] [-ForEachOutput ] [] +Rest [-ScriptBlock ] [-RESTEndpoint] [-ContentType ] [-Method ] [-InvokeCommand ] [-InvokeParameterVariable ] [-UriParameterHelp ] [-UriParameterType ] [-BodyParameter ] [-QueryParameter ] [-JoinQueryValue ] [-ForEachOutput ] [] ``` ---