-
Notifications
You must be signed in to change notification settings - Fork 0
/
rest.vim
593 lines (519 loc) · 17 KB
/
rest.vim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
setlocal commentstring=#%s
let s:vrc_auto_format_response_patterns = {
\ 'json': 'python -m json.tool',
\ 'xml': 'xmllint --format -',
\}
let s:vrc_glob_delim = '\v^--\s*$'
let s:vrc_comment_delim = '\c\v^\s*(#|//)'
let s:vrc_block_delimiter = '\c\v^\s*HTTPS?://|^--'
function! s:StrTrim(txt)
return substitute(a:txt, '\v^\s*([^[:space:]].*[^[:space:]])\s*$', '\1', 'g')
endfunction
function! s:GetOptValue(opt, defVal)
if exists('b:' . a:opt)
return eval('b:' . a:opt)
endif
if exists('g:' . a:opt)
return eval('g:' . a:opt)
endif
return a:defVal
endfunction
function! s:GetDictValue(dictName, key, defVal)
for prefix in ['b', 'g', 's']
let varName = prefix . ':' . a:dictName
if exists(varName) && has_key(eval(varName), a:key)
return get(eval(varName), a:key)
endif
endfor
return a:defVal
endfunction
"""
" @return [int, int] First and last line of the enclosing request block.
"
function! s:LineNumsRequestBlock()
let curPos = getpos('.')
let blockStart = 0
let blockEnd = 0
let lineNumGlobDelim = s:LineNumGlobSectionDelim()
""" Find the start of the enclosing request block.
normal! $
let blockStart = search(s:vrc_block_delimiter, 'bn')
if !blockStart || blockStart > curPos[1] || blockStart <= lineNumGlobDelim
call cursor(curPos[1:])
return [0, 0]
endif
""" Find the start of the next request block.
let blockEnd = search(s:vrc_block_delimiter, 'n') - 1
if blockEnd <= blockStart
let blockEnd = line('$')
endif
call cursor(curPos[1:])
return [blockStart, blockEnd]
endfunction
"""
" @return int The line number of the global section delimiter.
"
function! s:LineNumGlobSectionDelim()
let curPos = getpos('.')
normal! gg
let lineNum = search(s:vrc_glob_delim, 'cn')
call cursor(curPos[1:])
return lineNum
endfunction
"""
" Parse host between the given line numbers (inclusive end).
"
" @return [line num or 0, string]
"
function! s:ParseHost(start, end)
if a:end < a:start
return [0, '']
endif
let curPos = getpos('.')
call cursor(a:start, 1)
let lineNum = search('\v\c^\s*HTTPS?://', 'cn', a:end)
call cursor(curPos[1:])
if !lineNum
return [lineNum, '']
endif
return [lineNum, s:StrTrim(getline(lineNum))]
endfunction
"""
" @return [int, string]
"
function! s:ParseVerbQuery(start, end)
let curPos = getpos('.')
call cursor(a:start, 1)
let lineNum = search(
\ '\c\v^(GET|POST|PUT|DELETE|HEAD|PATCH|OPTIONS|TRACE)\s+',
\ 'cn',
\ a:end
\)
call cursor(curPos[1:])
if !lineNum
return [lineNum, '']
endif
return [lineNum, s:StrTrim(getline(lineNum))]
endfunction
"""
" Parse header options between the given line numbers (inclusive end).
"
" @return dict
"
function! s:ParseHeaders(start, end)
let contentTypeOpt = s:GetOptValue('vrc_header_content_type', 'application/json')
let headers = {'Content-Type': contentTypeOpt}
if (a:end < a:start)
return headers
endif
let lineBuf = getline(a:start, a:end)
let hasContentType = 0
for line in lineBuf
let line = s:StrTrim(line)
if line ==? '' || line =~? s:vrc_comment_delim
continue
endif
let sepIdx = stridx(line, ':')
if sepIdx > -1
let key = s:StrTrim(line[0:sepIdx - 1])
let headers[key] = s:StrTrim(line[sepIdx + 1:])
endif
endfor
return headers
endfunction
"""
" Parse values in global section
"
" @return dict
"
function! s:ParseVals(start, end)
let vals = {}
if (a:end < a:start)
return vals
endif
let lineBuf = getline(a:start, a:end)
for line in lineBuf
let line = s:StrTrim(line)
if line ==? '' || line =~? s:vrc_comment_delim
continue
endif
let sepIdx = stridx(line, '=')
if sepIdx > -1
let key = s:StrTrim(line[0:sepIdx - 1])
let vals[key] = s:StrTrim(line[sepIdx + 1:])
endif
endfor
return vals
endfunction
"""
" @return dict { 'host': String, 'headers': {}, 'vals': {} }
"
function! s:ParseGlobSection()
let globSection = {
\ 'host': '',
\ 'headers': {},
\ 'vals': {},
\}
""" Search for the line of the global section delimiter.
let lastLine = s:LineNumGlobSectionDelim()
if !lastLine
return globSection
endif
""" Parse global host.
let [hostLine, host] = s:ParseHost(1, lastLine - 1)
""" Parse global headers.
let headers = s:ParseHeaders(hostLine + 1, lastLine - 1)
""" Parse global vals.
let vals = s:ParseVals(hostLine + 1, lastLine - 1)
let globSection = {
\ 'host': host,
\ 'headers': headers,
\ 'vals': vals,
\}
return globSection
endfunction
"""
" @param int start
" @param int resumeFrom (inclusive)
" @param int end (inclusive)
" @param dict globSection
" @return dict
"
function! s:ParseRequest(start, resumeFrom, end, globSection)
""" Parse host.
let [lineNumHost, host] = s:ParseHost(a:start, a:end)
if !lineNumHost
let host = get(a:globSection, 'host', '')
let lineNumHost = a:start
endif
if empty(host)
return {
\ 'success': 0,
\ 'msg': 'Missing host',
\}
endif
""" Parse the HTTP verb query.
let [lineNumVerb, restQuery] = s:ParseVerbQuery(a:resumeFrom, a:end)
if !lineNumVerb
return {
\ 'success': 0,
\ 'msg': 'Missing query',
\}
endif
""" Parse the next HTTP verb query.
let resumeFrom = lineNumVerb + 1
let [lineNumNextVerb, nextRestQuery] = s:ParseVerbQuery(lineNumVerb + 1, a:end)
if !lineNumNextVerb
let resumeFrom = a:end + 1
let lineNumNextVerb = a:end + 1
endif
""" Parse headers if any and merge with global headers.
let localHeaders = s:ParseHeaders(lineNumHost + 1, lineNumVerb - 1)
let headers = get(a:globSection, 'headers', {})
call extend(headers, localHeaders)
let vals = get(a:globSection, 'vals', {})
""" Parse http verb, query path, and data body.
let [httpVerb; queryPathList] = split(restQuery)
let dataBody = getline(lineNumVerb + 1, lineNumNextVerb - 1)
""" Search and replace values in queryPath
let queryPath = join(queryPathList, '')
for key in keys(vals)
let queryPath = substitute(queryPath, ":" . key, vals[key], "")
endfor
""" Filter out comment and blank lines.
call filter(dataBody, 'v:val !~ ''\v^\s*(#|//).*$|\v^\s*$''')
""" Some might need leading/trailing spaces in body rows.
"call map(dataBody, 's:StrTrim(v:val)')
return {
\ 'success': 1,
\ 'resumeFrom': resumeFrom,
\ 'msg': '',
\ 'host': host,
\ 'headers': headers,
\ 'httpVerb': httpVerb,
\ 'requestPath': queryPath,
\ 'dataBody': dataBody
\}
endfunction
"""
" Construct the cUrl command given the request.
"
function! s:GetCurlCommand(request)
""" Construct curl args.
let curlArgs = ['-sgS']
let vrcIncludeHeader = s:GetOptValue('vrc_include_response_header', 1)
if vrcIncludeHeader
call add(curlArgs, '-i')
endif
let vrcDebug = s:GetOptValue('vrc_debug', 0)
if vrcDebug
call add(curlArgs, '-v')
endif
let secureSsl = s:GetOptValue('vrc_ssl_secure', 0)
if a:request.host =~? '\v^\s*HTTPS://' && !secureSsl
call add(curlArgs, '-k')
endif
""" Add --ipv4
let resolveToIpv4 = s:GetOptValue('vrc_resolve_to_ipv4', 0)
if resolveToIpv4
call add(curlArgs, '--ipv4')
endif
""" Add --cookie-jar
let cookieJar = s:GetOptValue('vrc_cookie_jar', 0)
if !empty(cookieJar)
call add(curlArgs, '-b ' . shellescape(cookieJar))
call add(curlArgs, '-c ' . shellescape(cookieJar))
endif
""" Add -L option to enable redirects
let locationEnabled = s:GetOptValue('vrc_follow_redirects', 0)
if locationEnabled
call add(curlArgs, '-L')
endif
""" Add headers.
for key in keys(a:request.headers)
call add(curlArgs, '-H ' . shellescape(key . ': ' . a:request.headers[key]))
endfor
""" Timeout options.
call add(curlArgs, '--connect-timeout ' . s:GetOptValue('vrc_connect_timeout', 10))
call add(curlArgs, '--max-time ' . s:GetOptValue('vrc_max_time', 60))
""" Response times
"call add(curlArgs, '-w %{time_connect}:%{time_starttransfer}:%{time_total}')
""" Add http verb.
let httpVerb = a:request.httpVerb
call add(curlArgs, s:GetCurlRequestOpt(httpVerb))
""" Add data body.
let dataBody = a:request.dataBody
if !empty(dataBody)
call add(
\ curlArgs,
\ s:GetCurlDataArgs(httpVerb, dataBody)
\)
endif
return 'curl ' . join(curlArgs) . ' ' . shellescape(a:request.host . a:request.requestPath)
"return 'curl ' . join(curlArgs) . ' ' . shellescape(a:request.host . a:request.requestPath) . ' -w \'---- Connect Time: %{time_connect} - Start Transfer : %{time_starttransfer} - Total Time: %{time_total}---- \''
endfunction
"""
" Get the cUrl option for request method (--get, --head, -X <verb>...)
"
function! s:GetCurlRequestOpt(httpVerb)
if a:httpVerb ==? 'GET'
if s:GetOptValue('vrc_allow_get_request_body', 0)
return '-X GET'
endif
return '--get'
elseif a:httpVerb ==? 'HEAD'
return '--head'
endif
return '-X ' . a:httpVerb
endfunction
"""
" Get the cUrl option to include data body (--data, --data-urlencode...)
"
" @param dict httpVerb
" @param list dataLines
" @return string
"
function! s:GetCurlDataArgs(httpVerb, dataLines)
""" These verbs should have request body passed as POST params.
if a:httpVerb ==? 'POST'
\ || a:httpVerb ==? 'PUT'
\ || a:httpVerb ==? 'PATCH'
\ || a:httpVerb ==? 'OPTIONS'
""" If data is loaded from file.
if stridx(get(a:dataLines, 0, ''), '@') == 0
return '--data-binary ' . shellescape(a:dataLines[0])
endif
""" If request body is split line by line.
if s:GetOptValue('vrc_split_request_body', 0)
call map(a:dataLines, '"--data " . shellescape(v:val)')
return join(a:dataLines)
endif
""" Otherwise, send the request body as a whole.
return '--data ' . shellescape(join(a:dataLines, ''))
endif
""" If verb is GET and GET request body is allowed.
if a:httpVerb ==? 'GET' && s:GetOptValue('vrc_allow_get_request_body', 0)
return '--data ' . shellescape(join(a:dataLines, ''))
endif
""" For other cases, request body is passed as GET params.
if s:GetOptValue('vrc_split_request_body', 0)
""" If request body is split, url-encode each line.
call map(a:dataLines, '"--data-urlencode " . shellescape(v:val)')
return join(a:dataLines)
endif
""" Otherwise, url-encode and send the request body as a whole.
return '--data-urlencode ' . shellescape(join(a:dataLines, ''))
endfunction
"""
" @param string tmpBufName
" @param dict outputInfo
" {
" 'outputChunks': list[string],
" 'commands': list[string],
" }
"
function! s:DisplayOutput(tmpBufName, outputInfo)
""" Get view options before working in the view buffer.
let autoFormatResponse = s:GetOptValue('vrc_auto_format_response_enabled', 1)
let syntaxHighlightResponse = s:GetOptValue('vrc_syntax_highlight_response', 1)
let includeResponseHeader = s:GetOptValue('vrc_include_response_header', 1)
let contentType = s:GetOptValue('vrc_response_default_content_type', '')
""" Setup view.
let origWin = winnr()
let outputWin = bufwinnr(bufnr(a:tmpBufName))
if outputWin == -1
let cmdSplit = 'vsplit'
if s:GetOptValue('vrc_horizontal_split', 0)
let cmdSplit = 'split'
endif
""" Create view if not loadded or hidden.
execute 'rightbelow ' . cmdSplit . ' ' . a:tmpBufName
setlocal buftype=nofile
else
""" View already shown, switch to it.
execute outputWin . 'wincmd w'
endif
""" Display output in view.
setlocal modifiable
silent! normal! ggdG
let output = join(a:outputInfo['outputChunks'], "\n\n")
call setline('.', split(substitute(output, '[[:return:]]', '', 'g'), '\v\n'))
""" Display commands in quickfix window if any.
if (!empty(a:outputInfo['commands']))
execute 'cgetexpr' string(a:outputInfo['commands'])
copen
execute outputWin 'wincmd w'
endif
""" Detect content-type based on the returned header.
let emptyLineNum = 0
if includeResponseHeader
call cursor(1, 0)
let emptyLineNum = search('\v^\s*$', 'n')
let contentTypeLineNum = search('\v\c^Content-Type:', 'n', emptyLineNum)
if contentTypeLineNum > 0
let contentType = substitute(
\ getline(contentTypeLineNum),
\ '\v\c^Content-Type:\s*([^;[:blank:]]*).*$',
\ '\1',
\ 'g'
\)
endif
endif
""" Continue with options depending content-type.
if !empty(contentType)
let fileType = substitute(contentType, '\v^.*/(.*\+)?(.*)$', '\2', 'g')
""" Auto-format the response.
if autoFormatResponse
let formatCmd = s:GetDictValue('vrc_auto_format_response_patterns', fileType, '')
if !empty(formatCmd)
""" Auto-format response body
let formattedBody = system(
\ formatCmd,
\ getline(emptyLineNum, '$')
\)
if v:shell_error == 0
silent! execute (emptyLineNum + 1) . ',$delete _'
if s:GetOptValue('vrc_auto_format_uhex', 0)
let formattedBody = substitute(
\ formattedBody,
\ '\v\\u(\x{4})',
\ '\=nr2char("0x" . submatch(1), 1)',
\ 'g'
\)
endif
call append('$', split(formattedBody, '\v\n'))
elseif s:GetOptValue('vrc_debug', 0)
echom "VRC: auto-format error: " . v:shell_error
echom formattedBody
endif
endif
endif
""" Syntax-highlight response.
if syntaxHighlightResponse
syntax clear
try
execute "syntax include @vrc_" . fileType . " syntax/" . fileType . ".vim"
execute "syntax region body start=/^$/ end=/\%$/ contains=@vrc_" . fileType
catch
endtry
endif
endif
""" Finalize view.
setlocal nomodifiable
execute origWin . 'wincmd w'
endfunction
function! s:RunQuery(start, end)
let globSection = s:ParseGlobSection()
let outputInfo = {
\ 'outputChunks': [],
\ 'commands': [],
\}
" The `while loop` is to support multiple
" requests using consecutive verbs.
let resumeFrom = a:start
let shouldShowCommand = s:GetOptValue('vrc_show_command', 0)
let shouldDebug = s:GetOptValue('vrc_debug', 0)
while resumeFrom < a:end
let request = s:ParseRequest(a:start, resumeFrom, a:end, globSection)
if !request.success
echom request.msg
return
endif
let curlCmd = s:GetCurlCommand(request)
if shouldDebug
echom curlCmd
endif
silent !clear
redraw!
call add(outputInfo['outputChunks'], system(curlCmd))
if shouldShowCommand
call add(outputInfo['commands'], curlCmd)
endif
let resumeFrom = request.resumeFrom
endwhile
call s:DisplayOutput(
\ s:GetOptValue('vrc_output_buffer_name', '__REST_response__'),
\ outputInfo
\)
endfunction
"""
" Restore the win line to the given previous line.
"
function! s:RestoreWinLine(prevLine)
let offset = winline() - a:prevLine
if !offset
return
elseif offset > 0
exec "normal! " . offset . "\<C-e>"
else
exec "normal! " . -offset . "\<C-y>"
endif
endfunction
function! VrcQuery()
""" We'll jump pretty much. Save the current win line to set the view as before.
let curWinLine = winline()
let curPos = getpos('.')
if curPos[1] <= s:LineNumGlobSectionDelim()
echom 'Cannot execute global section'
return
endif
""" Determine the REST request block to process.
let [blockStart, blockEnd] = s:LineNumsRequestBlock()
if !blockStart
call s:RestoreWinLine(curWinLine)
echom 'Missing host/block start'
return
endif
""" Parse and execute the query
call s:RunQuery(blockStart, blockEnd)
call s:RestoreWinLine(curWinLine)
endfunction
function! VrcMap()
let triggerKey = s:GetOptValue('vrc_trigger', '<C-j>')
execute 'vnoremap <buffer> ' . triggerKey . ' :call VrcQuery()<CR>'
execute 'nnoremap <buffer> ' . triggerKey . ' :call VrcQuery()<CR>'
execute 'inoremap <buffer> ' . triggerKey . ' <Esc>:call VrcQuery()<CR>'
endfunction
if s:GetOptValue('vrc_set_default_mapping', 1)
call VrcMap()
endif