Skip to content

Commit 26b10e0

Browse files
author
James Brundage
committed
Adding REST transpiler (#114)
1 parent d44473f commit 26b10e0

File tree

1 file changed

+390
-0
lines changed

1 file changed

+390
-0
lines changed

Transpilers/Rest.psx.ps1

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
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

Comments
 (0)