|
| 1 | +<# |
| 2 | +.SYNOPSIS |
| 3 | + Generates PowerShell to talk to a REST api. |
| 4 | +.DESCRIPTION |
| 5 | + Generates PowerShell that communicates with a REST api. |
| 6 | +.EXAMPLE |
| 7 | + { |
| 8 | + function Get-Sentiment { |
| 9 | + [Rest("http://text-processing.com/api/sentiment/", |
| 10 | + ContentType="application/x-www-form-urlencoded", |
| 11 | + Method = "POST", |
| 12 | + BodyParameter="Text", |
| 13 | + ForeachOutput = { |
| 14 | + $_ | Select-Object -ExpandProperty Probability -Property Label |
| 15 | + } |
| 16 | + )] |
| 17 | + param() |
| 18 | + } |
| 19 | + } | .>PipeScript | Set-Content .\Get-Sentiment.ps1 |
| 20 | +.EXAMPLE |
| 21 | + Invoke-PipeScript { |
| 22 | + [Rest("http://text-processing.com/api/sentiment/", |
| 23 | + ContentType="application/x-www-form-urlencoded", |
| 24 | + Method = "POST", |
| 25 | + BodyParameter="Text", |
| 26 | + ForeachOutput = { |
| 27 | + $_ | Select-Object -ExpandProperty Probability -Property Label |
| 28 | + } |
| 29 | + )] |
| 30 | + param() |
| 31 | + } -Parameter @{Text='wow!'} |
| 32 | +.EXAMPLE |
| 33 | + { |
| 34 | + [Rest("https://api.github.com/users/{username}/repos", |
| 35 | + QueryParameter={"type", "sort", "direction", "page", "per_page"} |
| 36 | + )] |
| 37 | + param() |
| 38 | + } | .>PipeScript |
| 39 | +.EXAMPLE |
| 40 | + Invoke-PipeScript { |
| 41 | + [Rest("https://api.github.com/users/{username}/repos", |
| 42 | + QueryParameter={"type", "sort", "direction", "page", "per_page"} |
| 43 | + )] |
| 44 | + param() |
| 45 | + } -UserName StartAutomating |
| 46 | +.EXAMPLE |
| 47 | + { |
| 48 | + [Rest("http://text-processing.com/api/sentiment/", |
| 49 | + ContentType="application/x-www-form-urlencoded", |
| 50 | + Method = "POST", |
| 51 | + BodyParameter={@{ |
| 52 | + Text = ' |
| 53 | + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] |
| 54 | + [string] |
| 55 | + $Text |
| 56 | + ' |
| 57 | + }})] |
| 58 | + param() |
| 59 | + } | .>PipeScript |
| 60 | +#> |
| 61 | +param( |
| 62 | +# The ScriptBlock. |
| 63 | +# If not empty, the contents of this ScriptBlock will preceed the REST api call. |
| 64 | +[Parameter(ValueFromPipeline)] |
| 65 | +[scriptblock] |
| 66 | +$ScriptBlock = {}, |
| 67 | + |
| 68 | +# One or more REST endpoints. This endpoint will be parsed for REST variables. |
| 69 | +[Parameter(Mandatory,Position=0)] |
| 70 | +[string[]] |
| 71 | +$RESTEndpoint, |
| 72 | + |
| 73 | +# The content type. If provided, this parameter will be passed to the -InvokeCommand. |
| 74 | +[string] |
| 75 | +$ContentType, |
| 76 | + |
| 77 | +# The method. If provided, this parameter will be passed to the -InvokeCommand. |
| 78 | +[string] |
| 79 | +$Method, |
| 80 | + |
| 81 | +# The invoke command. This command _must_ have a parameter -URI. |
| 82 | +[Alias('Invoker')] |
| 83 | +[string] |
| 84 | +$InvokeCommand = 'Invoke-RestMethod', |
| 85 | + |
| 86 | +# The name of a variable containing additional invoke parameters. |
| 87 | +# By default, this is 'InvokeParams' |
| 88 | +[Alias('InvokerParameters','InvokerParameter')] |
| 89 | +[string] |
| 90 | +$InvokeParameterVariable = 'InvokeParams', |
| 91 | + |
| 92 | +# A dictionary or list of parameters for the body. |
| 93 | +[PSObject] |
| 94 | +$BodyParameter, |
| 95 | + |
| 96 | +# A dictionary or list of query parameters. |
| 97 | +[PSObject] |
| 98 | +$QueryParameter, |
| 99 | + |
| 100 | +# A script block to be run on each output. |
| 101 | +[ScriptBlock] |
| 102 | +$ForEachOutput |
| 103 | +) |
| 104 | + |
| 105 | +begin { |
| 106 | + # Declare a Regular Expression to match URL variables. |
| 107 | + $RestVariable = [Regex]::new(@' |
| 108 | +# Matches URL segments and query strings containing variables. |
| 109 | +# Variables can be enclosed in brackets or curly braces, or preceeded by a $ or : |
| 110 | +(?> # A variable can be in a URL segment or subdomain |
| 111 | + (?<Start>[/\.]) # Match the <Start>ing slash|dot ... |
| 112 | + (?<IsOptional>\?)? # ... an optional ? (to indicate optional) ... |
| 113 | + (?: |
| 114 | + \{(?<Variable>\w+)\}| # ... A <Variable> name in {} OR |
| 115 | + \[(?<Variable>\w+)\]| # A <Variable> name in [] OR |
| 116 | + \<(?<Variable>\w+)\>| # A <Variable> name in <> OR |
| 117 | + \:(?<Variable>\w+) # A : followed by a <Variable> |
| 118 | + ) |
| 119 | +| |
| 120 | + (?<IsOptional> # If it's optional it can also be |
| 121 | + [{\[](?<Start>/) # a bracket or brace, followed by a slash |
| 122 | + ) |
| 123 | + (?<Variable>\w+)[}\]] # then a <Variable> name followed by } or ] |
| 124 | +| # OR it can be in a query parameter: |
| 125 | + (?<Start>[?&]) # Match The <Start>ing ? or & ... |
| 126 | + (?<Query>[\w\-]+) # ... the <Query> parameter name ... |
| 127 | + = # ... an equals ... |
| 128 | + (?<IsOptional>\?)? # ... an optional ? (to indicate optional) ... |
| 129 | + (?: |
| 130 | + \{(?<Variable>\w+)\}| # ... A <Variable> name in {} OR |
| 131 | + \[(?<Variable>\w+)\]| # A <Variable> name in [] OR |
| 132 | + \<(?<Variable>\w+)\>| # A <Variable> name in <> OR |
| 133 | + \:(?<Variable>\w+) # A : followed by a <Variable> |
| 134 | + ) |
| 135 | +) |
| 136 | +'@, 'IgnoreCase,IgnorePatternWhitespace') |
| 137 | + |
| 138 | + |
| 139 | + # Next declare a script block that will replace the rest variable. |
| 140 | + $ReplaceRestVariable = { |
| 141 | + param($match) |
| 142 | + |
| 143 | + if ($uriParameter -and $uriParameter[$match.Groups["Variable"].Value]) { |
| 144 | + return $match.Groups["Start"].Value + $( |
| 145 | + if ($match.Groups["Query"].Success) { $match.Groups["Query"].Value + '=' } |
| 146 | + ) + |
| 147 | + ([Web.HttpUtility]::UrlEncode( |
| 148 | + $uriParameter[$match.Groups["Variable"].Value] |
| 149 | + )) |
| 150 | + } else { |
| 151 | + return '' |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + $myCmd = $MyInvocation.MyCommand |
| 156 | +} |
| 157 | + |
| 158 | +process { |
| 159 | + # First, create a collection of URI parameters. |
| 160 | + $uriParameters = [Ordered]@{} |
| 161 | + # Then, walk over each potential endpoint |
| 162 | + foreach ($endpoint in $RESTEndpoint) { |
| 163 | + # and each match of a $RestVariable |
| 164 | + foreach ($match in $RestVariable.Matches($endpoint)) { |
| 165 | + # The name of the parameter will be in the named capture ${Variable}. |
| 166 | + $parameterName = $match.Groups["Variable"].Value |
| 167 | + # The parameter type will be a string |
| 168 | + $parameterType = '[string]' |
| 169 | + # and we'll need to put it in the proper parameter set. |
| 170 | + $parameterAttribute = "[Parameter($( |
| 171 | + if (-not $match.Groups["IsOptional"].Value) {'Mandatory'} |
| 172 | + ),ValueFromPipelineByPropertyName,ParameterSetName='$endpoint')]" |
| 173 | + # Combine these three pieces to create the parameter attribute. |
| 174 | + $uriParameters[$parameterName] = @( |
| 175 | + $parameterAttribute |
| 176 | + $parameterType |
| 177 | + '$' + $parameterName |
| 178 | + ) -join [Environment]::Newline |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + # Create a parameter block out of the uri parameters. |
| 183 | + $uriParamBlock = |
| 184 | + New-PipeScript -Parameter $uriParameters |
| 185 | + |
| 186 | + # Next, create a parameter block out of any of the body parameters. |
| 187 | + $bodyParamBlock = |
| 188 | + if ($BodyParameter) { |
| 189 | + New-PipeScript -Parameter $BodyParameter |
| 190 | + } else { {} } |
| 191 | + |
| 192 | + # And one for each of the query parameters. |
| 193 | + $QueryParamblock = |
| 194 | + if ($QueryParameter) { |
| 195 | + New-PipeScript -Parameter $QueryParameter |
| 196 | + } else { {} } |
| 197 | + |
| 198 | + $myBeginBlock = |
| 199 | + # If we used any URI parameters |
| 200 | + if ($uriParamBlock.Ast.ParamBlock.Parameters) { |
| 201 | + # Carry on the begin block from this command (this is a neat trick) |
| 202 | + [scriptblock]::Create($myCmd.ScriptBlock.Ast.BeginBlock.Extent.ToString()) |
| 203 | + } else { {} } |
| 204 | + |
| 205 | + # Next, collect the names of bodyParameters, queryParameters, and uriParameters. |
| 206 | + $bodyParameterNames = |
| 207 | + foreach ($param in $bodyParamBlock.Ast.ParamBlock.Parameters) { $param.Name -replace '^\$' } |
| 208 | + $queryParameterNames = |
| 209 | + foreach ($param in $QueryParamblock.Ast.ParamBlock.Parameters) { $param.Name -replace '^\$' } |
| 210 | + $uriParameterNames = |
| 211 | + foreach ($param in $uriParamBlock.Ast.ParamBlock.Parameters) { $param.Name -replace '^\$' } |
| 212 | + |
| 213 | + |
| 214 | + # Collect all of the parts of the script |
| 215 | + $RestScript = @( |
| 216 | + # Start with the underlying script block |
| 217 | + $ScriptBlock |
| 218 | + # Then declare the initial variables. |
| 219 | + [scriptblock]::Create((@" |
| 220 | +process { |
| 221 | + `$InvokeCommand = '$InvokeCommand' |
| 222 | + `$invokerCommandinfo = |
| 223 | + `$ExecutionContext.SessionState.InvokeCommand.GetCommand('$InvokeCommand', 'All') |
| 224 | + `$method = '$Method' |
| 225 | + `$contentType = '$contentType' |
| 226 | + `$bodyParameterNames = @('$($bodyParameterNames -join "','")') |
| 227 | + `$queryParameterNames = @('$($queryParameterNames -join "','")') |
| 228 | + `$uriParameterNames = @('$($uriParameterNames -join "','")') |
| 229 | + `$endpoints = @("$($endpoint -join "','")") |
| 230 | + `$ForEachOutput = { |
| 231 | + $(if ($foreachOutput) { $ForEachOutput | .>Pipescript }) |
| 232 | + } |
| 233 | + if (`$ForEachOutput -match '^\s{0,}$') { |
| 234 | + `$ForEachOutput = `$null |
| 235 | + } |
| 236 | +} |
| 237 | +"@)) |
| 238 | + # Next, add some boilerplate code for error handling and setting defaults |
| 239 | +{ |
| 240 | +process { |
| 241 | + if (-not $invokerCommandinfo) { |
| 242 | + Write-Error "Unable to find invoker '$InvokeCommand'" |
| 243 | + return |
| 244 | + } |
| 245 | + if (-not $psParameterSet) { $psParameterSet = $psCmdlet.ParameterSetName} |
| 246 | + if ($psParameterSet -eq '__AllParameterSets') { $psParameterSet = $endpoints[0]} |
| 247 | +} |
| 248 | +} |
| 249 | + # If we had any uri parameters |
| 250 | + if ($uriParameters.Count) { |
| 251 | + # Add the uri parameter block |
| 252 | + $uriParamBlock |
| 253 | + # And add the begin block from this script |
| 254 | + $myBeginBlock |
| 255 | + # Then add a bit to process {} to extract out the URL |
| 256 | +{ |
| 257 | +process { |
| 258 | + $originalUri = "$psParameterSet" |
| 259 | + if (-not $PSBoundParameters.ContainsKey('UriParameter')) { |
| 260 | + $uriParameter = [Ordered]@{} |
| 261 | + } |
| 262 | + foreach ($uriParameterName in $uriParameterNames) { |
| 263 | + if ($psBoundParameters.ContainsKey($uriParameterName)) { |
| 264 | + $uriParameter[$uriParameterName] = $psBoundParameters[$uriParameterName] |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + $uri = $RestVariable.Replace($originalUri, $ReplaceRestVariable) |
| 269 | +} |
| 270 | +} |
| 271 | + } else { |
| 272 | + # If uri parameters were not supplied, default to the first endpoint. |
| 273 | +{ |
| 274 | + process { |
| 275 | + $uri = $endpoints[0] |
| 276 | + } |
| 277 | +} |
| 278 | + } |
| 279 | + # Now create the invoke splat and populate it. |
| 280 | +{ |
| 281 | +process { |
| 282 | + $invokeSplat = @{} |
| 283 | + $invokeSplat.Uri = $uri |
| 284 | + if ($method) { |
| 285 | + $invokeSplat.Method = $method |
| 286 | + } |
| 287 | + if ($ContentType) { |
| 288 | + $invokeSplat.ContentType = $ContentType |
| 289 | + } |
| 290 | +} |
| 291 | +} |
| 292 | + |
| 293 | + # If we have an InvokeParameterVariable |
| 294 | + if ($InvokeParameterVariable) { |
| 295 | + # Create the code that looks for it and joins it with the splat. |
| 296 | + $InvokeParameterVariable = $InvokeParameterVariable -replace '^\$' |
| 297 | +[scriptblock]::Create(" |
| 298 | +process { |
| 299 | + if (`$$InvokeParameterVariable -and `$$InvokeParameterVariable -is [Collections.IDictionary]) { |
| 300 | + `$invokeSplat += `$$InvokeParameterVariable |
| 301 | + } |
| 302 | +} |
| 303 | +") |
| 304 | + |
| 305 | + } |
| 306 | + |
| 307 | + # If QueryParameter Names were provided |
| 308 | + if ($queryParameterNames) { |
| 309 | + # Include the query parameter block |
| 310 | + $QueryParamblock |
| 311 | + # And a section of process to handle query parameters. |
| 312 | +{ |
| 313 | +process { |
| 314 | + $QueryParams = [Ordered]@{} |
| 315 | + foreach ($QueryParameterName in $QueryParameterNames) { |
| 316 | + if ($PSBoundParameters.ContainsKey($QueryParameterName)) { |
| 317 | + $QueryParams[$QueryParameterName] = $PSBoundParameters[$QueryParameterName] |
| 318 | + } |
| 319 | + } |
| 320 | + if ($invokerCommandinfo.Parameters['QueryParameter'] -and |
| 321 | + $invokerCommandinfo.Parameters['QueryParameter'].ParameterType -eq [Collections.IDictionary]) { |
| 322 | + $invokerCommandinfo.QueryParameter = $QueryParams |
| 323 | + } else { |
| 324 | + $queryParamStr = |
| 325 | + @(foreach ($qp in $QueryParams.GetEnumerator()) { |
| 326 | + "$($qp.Key)=$([Web.HttpUtility]::UrlEncode($qp.Value).Replace('+', '%20'))" |
| 327 | + }) -join '&' |
| 328 | + if ($invokeSplat.Uri.Contains('?')) { |
| 329 | + $invokeSplat.Uri = "$($invokeSplat.Uri)" + '&' + $queryParamStr |
| 330 | + } else { |
| 331 | + $invokeSplat.Uri = "$($invokeSplat.Uri)" + '?' + $queryParamStr |
| 332 | + } |
| 333 | + } |
| 334 | +} |
| 335 | +} |
| 336 | + } |
| 337 | + |
| 338 | + # If any body parameters exist |
| 339 | + if ($bodyParameterNames) { |
| 340 | + # Include the body parameter block |
| 341 | + $bodyParamBlock |
| 342 | + # and a process section to handle the body |
| 343 | +{ |
| 344 | +process { |
| 345 | + $completeBody = [Ordered]@{} |
| 346 | + foreach ($bodyParameterName in $bodyParameterNames) { |
| 347 | + if ($bodyParameterName) { |
| 348 | + if ($PSBoundParameters.ContainsKey($bodyParameterName)) { |
| 349 | + $completeBody[$bodyParameterName] = $PSBoundParameters[$bodyParameterName] |
| 350 | + } |
| 351 | + } |
| 352 | + } |
| 353 | + |
| 354 | + $bodyContent = |
| 355 | + if ($ContentType -match 'x-www-form-urlencoded') { |
| 356 | + @(foreach ($bodyPart in $completeBody.GetEnumerator()) { |
| 357 | + "$($bodyPart.Key.ToString().ToLower())=$([Web.HttpUtility]::UrlEncode($bodyPart.Value))" |
| 358 | + }) -join '&' |
| 359 | + } elseif ($ContentType -match 'json') { |
| 360 | + ConvertTo-Json $completeBody |
| 361 | + } |
| 362 | + |
| 363 | + if ($bodyContent -and $method -ne 'get') { |
| 364 | + $invokeSplat.Body = $bodyContent |
| 365 | + } |
| 366 | +} |
| 367 | +} |
| 368 | + } |
| 369 | + |
| 370 | + # Last but not least, include the part of process that calls the REST api. |
| 371 | + { |
| 372 | +process { |
| 373 | + Write-Verbose "$($invokeSplat.Uri)" |
| 374 | + if ($ForEachOutput) { |
| 375 | + if ($ForEachOutput.Ast.ProcessBlock) { |
| 376 | + & $invokerCommandinfo @invokeSplat | & $ForEachOutput |
| 377 | + } else { |
| 378 | + & $invokerCommandinfo @invokeSplat | ForEach-Object -Process $ForEachOutput |
| 379 | + } |
| 380 | + } else { |
| 381 | + & $invokerCommandinfo @invokeSplat |
| 382 | + } |
| 383 | +} |
| 384 | + } |
| 385 | + ) |
| 386 | + |
| 387 | + # Join all of the parts together and you've got yourself a RESTful function. |
| 388 | + $RestScript | |
| 389 | + Join-PipeScript |
| 390 | +} |
0 commit comments