code-ruby 3.1.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/bin/code +97 -20
- data/lib/code/concerns/shared.rb +331 -15
- data/lib/code/format.rb +22 -2
- data/lib/code/network.rb +87 -0
- data/lib/code/node/call.rb +79 -2
- data/lib/code/node/call_argument.rb +14 -0
- data/lib/code/node/code.rb +5 -4
- data/lib/code/node/function_parameter.rb +7 -4
- data/lib/code/node/list.rb +29 -1
- data/lib/code/node/while.rb +21 -0
- data/lib/code/object/base_64.rb +132 -6
- data/lib/code/object/boolean.rb +60 -0
- data/lib/code/object/class.rb +138 -2
- data/lib/code/object/code.rb +111 -3
- data/lib/code/object/context.rb +57 -1
- data/lib/code/object/cryptography.rb +63 -0
- data/lib/code/object/date.rb +13339 -462
- data/lib/code/object/decimal.rb +1725 -0
- data/lib/code/object/dictionary.rb +1835 -12
- data/lib/code/object/duration.rb +28 -0
- data/lib/code/object/function.rb +261 -23
- data/lib/code/object/global.rb +534 -1
- data/lib/code/object/html.rb +227 -16
- data/lib/code/object/http.rb +244 -14
- data/lib/code/object/ics.rb +75 -13
- data/lib/code/object/identifier_list.rb +17 -2
- data/lib/code/object/integer.rb +1941 -2
- data/lib/code/object/json.rb +75 -1
- data/lib/code/object/list.rb +3417 -10
- data/lib/code/object/nothing.rb +53 -0
- data/lib/code/object/number.rb +110 -0
- data/lib/code/object/parameter.rb +140 -0
- data/lib/code/object/range.rb +596 -14
- data/lib/code/object/smtp.rb +95 -12
- data/lib/code/object/string.rb +944 -3
- data/lib/code/object/super.rb +10 -1
- data/lib/code/object/time.rb +13358 -498
- data/lib/code/object/url.rb +65 -0
- data/lib/code/object.rb +543 -0
- data/lib/code/parser.rb +177 -26
- data/lib/code-ruby.rb +3 -0
- data/lib/code.rb +30 -3
- metadata +135 -84
- data/.github/dependabot.yml +0 -15
- data/.github/workflows/ci.yml +0 -38
- data/.gitignore +0 -30
- data/.node-version +0 -1
- data/.npm-version +0 -1
- data/.prettierignore +0 -2
- data/.rspec +0 -1
- data/.rubocop.yml +0 -140
- data/.ruby-version +0 -1
- data/.tool-versions +0 -3
- data/AGENTS.md +0 -43
- data/Gemfile +0 -22
- data/Gemfile.lock +0 -292
- data/Rakefile +0 -5
- data/bin/bundle +0 -123
- data/bin/bundle-audit +0 -31
- data/bin/bundler-audit +0 -31
- data/bin/dorian +0 -31
- data/bin/rspec +0 -31
- data/bin/rubocop +0 -31
- data/bin/test +0 -5
- data/code-ruby.gemspec +0 -34
- data/docs/precedence.txt +0 -36
- data/package-lock.json +0 -14
- data/package.json +0 -7
- data/spec/bin/code_spec.rb +0 -48
- data/spec/code/format_spec.rb +0 -153
- data/spec/code/node/call_spec.rb +0 -11
- data/spec/code/object/boolean_spec.rb +0 -18
- data/spec/code/object/cryptography_spec.rb +0 -25
- data/spec/code/object/decimal_spec.rb +0 -50
- data/spec/code/object/dictionary_spec.rb +0 -98
- data/spec/code/object/function_spec.rb +0 -268
- data/spec/code/object/http_spec.rb +0 -33
- data/spec/code/object/ics_spec.rb +0 -50
- data/spec/code/object/integer_spec.rb +0 -42
- data/spec/code/object/list_spec.rb +0 -22
- data/spec/code/object/nothing_spec.rb +0 -14
- data/spec/code/object/range_spec.rb +0 -23
- data/spec/code/object/string_spec.rb +0 -26
- data/spec/code/parser/boolean_spec.rb +0 -11
- data/spec/code/parser/chained_call_spec.rb +0 -16
- data/spec/code/parser/dictionary_spec.rb +0 -18
- data/spec/code/parser/function_spec.rb +0 -16
- data/spec/code/parser/group_spec.rb +0 -11
- data/spec/code/parser/if_modifier_spec.rb +0 -18
- data/spec/code/parser/list_spec.rb +0 -17
- data/spec/code/parser/number_spec.rb +0 -11
- data/spec/code/parser/string_spec.rb +0 -20
- data/spec/code/parser_spec.rb +0 -52
- data/spec/code/type_spec.rb +0 -21
- data/spec/code_spec.rb +0 -642
- data/spec/spec_helper.rb +0 -21
- data/spec/zeitwerk/loader_spec.rb +0 -7
data/lib/code/object/html.rb
CHANGED
|
@@ -3,6 +3,144 @@
|
|
|
3
3
|
class Code
|
|
4
4
|
class Object
|
|
5
5
|
class Html < Object
|
|
6
|
+
CLASS_DOCUMENTATION = {
|
|
7
|
+
name: "Html",
|
|
8
|
+
description: "builds, escapes, parses, and queries html fragments.",
|
|
9
|
+
examples: [
|
|
10
|
+
"Html.p { :hello }",
|
|
11
|
+
"Html.escape(\"<p>a</p>\")",
|
|
12
|
+
"Html.raw(\"<p>a</p>\").css(\"p\")"
|
|
13
|
+
]
|
|
14
|
+
}.freeze
|
|
15
|
+
CLASS_FUNCTIONS = {
|
|
16
|
+
"escape" => {
|
|
17
|
+
name: "escape",
|
|
18
|
+
description: "escapes a value for html text.",
|
|
19
|
+
examples: [
|
|
20
|
+
"Html.escape(\"<p>\")",
|
|
21
|
+
"Html.escape(:hello)",
|
|
22
|
+
"Html.escape(() => { \"<strong>hello</strong>\" })"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"unescape" => {
|
|
26
|
+
name: "unescape",
|
|
27
|
+
description: "converts html entities and tags to text.",
|
|
28
|
+
examples: [
|
|
29
|
+
"Html.unescape(\"<p>\")",
|
|
30
|
+
"Html.unescape(\"<p>hello</p>\")",
|
|
31
|
+
"Html.unescape(() => { \"&\" })"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"join" => {
|
|
35
|
+
name: "join",
|
|
36
|
+
description: "joins html or text values with an optional separator.",
|
|
37
|
+
examples: [
|
|
38
|
+
"Html.join([:a, :b], \", \")",
|
|
39
|
+
"Html.join([Html.text(\"a\"), Html.text(\"b\")])",
|
|
40
|
+
"Html.join(\"<br>\", () => { [\"a\", \"b\"] })"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"text" => {
|
|
44
|
+
name: "text",
|
|
45
|
+
description: "builds an escaped html text fragment.",
|
|
46
|
+
examples: [
|
|
47
|
+
"Html.text(:hello)",
|
|
48
|
+
"Html.text(\"<strong>hello</strong>\")",
|
|
49
|
+
"Html.text(() => { :hello })"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"raw" => {
|
|
53
|
+
name: "raw",
|
|
54
|
+
description: "parses markup into an html fragment without escaping text.",
|
|
55
|
+
examples: [
|
|
56
|
+
"Html.raw(\"<strong>hello</strong>\")",
|
|
57
|
+
"Html.raw(Html.text(:hello))",
|
|
58
|
+
"Html.raw(() => { \"<em>hello</em>\" })"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}.freeze
|
|
62
|
+
INSTANCE_FUNCTIONS = {
|
|
63
|
+
"css" => {
|
|
64
|
+
name: "css",
|
|
65
|
+
description: "returns all html nodes matching a css selector.",
|
|
66
|
+
examples: [
|
|
67
|
+
"Html.raw(\"<p>a</p><p>b</p>\").css(\"p\")",
|
|
68
|
+
"Html.raw(\"<main><p>a</p></main>\").css(\"main p\")",
|
|
69
|
+
"Html.raw(\"<p class='x'>a</p>\").css(\".x\")"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"at_css" => {
|
|
73
|
+
name: "at_css",
|
|
74
|
+
description: "returns the first html node matching a css selector.",
|
|
75
|
+
examples: [
|
|
76
|
+
"Html.raw(\"<p>a</p><p>b</p>\").at_css(\"p\")",
|
|
77
|
+
"Html.raw(\"<main><p>a</p></main>\").at_css(\"main p\")",
|
|
78
|
+
"Html.raw(\"<p class='x'>a</p>\").at_css(\".x\")"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"map" => {
|
|
82
|
+
name: "map",
|
|
83
|
+
description: "returns a list by calling a function for each html node.",
|
|
84
|
+
examples: [
|
|
85
|
+
"Html.raw(\"<p>a</p><p>b</p>\").css(\"p\").map((node) => { node.to_string })",
|
|
86
|
+
"Html.raw(\"<p>a</p>\").css(:p).map((node, index) => { index })",
|
|
87
|
+
"Html.raw(\"<p>a</p>\").css(:p).map((node, index, nodes) => { nodes.to_string })"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"to_string" => {
|
|
91
|
+
name: "to_string",
|
|
92
|
+
description: "returns the html fragment's text content.",
|
|
93
|
+
examples: [
|
|
94
|
+
"Html.raw(\"<p>hello</p>\").to_string",
|
|
95
|
+
"Html.text(:hello).to_string",
|
|
96
|
+
"Html.raw(\"<p><strong>hello</strong></p>\").to_string"
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"to_html" => {
|
|
100
|
+
name: "to_html",
|
|
101
|
+
description: "returns the html fragment as html markup.",
|
|
102
|
+
examples: [
|
|
103
|
+
"Html.raw(\"<p>hello</p>\").to_html",
|
|
104
|
+
"Html.text(:p).to_html",
|
|
105
|
+
"Html.raw(\"<strong>hello</strong>\").to_html"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"inner_text" => {
|
|
109
|
+
name: "inner_text",
|
|
110
|
+
description: "returns the html fragment's text content.",
|
|
111
|
+
examples: [
|
|
112
|
+
"Html.raw(\"<p>hello</p>\").inner_text",
|
|
113
|
+
"Html.text(:hello).inner_text",
|
|
114
|
+
"Html.raw(\"<p><strong>hello</strong></p>\").inner_text"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"attribute" => {
|
|
118
|
+
name: "attribute",
|
|
119
|
+
description: "returns an html node attribute by name.",
|
|
120
|
+
examples: [
|
|
121
|
+
"Html.raw(\"<a href='/'>home</a>\").at_css(:a).attribute(:href)",
|
|
122
|
+
"Html.raw(\"<p class='x'>a</p>\").at_css(:p).attribute(:class)",
|
|
123
|
+
"Html.raw(\"<img alt='logo'>\").at_css(:img).attribute(:alt)"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"attributes" => {
|
|
127
|
+
name: "attributes",
|
|
128
|
+
description: "returns an html node's attributes as a dictionary.",
|
|
129
|
+
examples: [
|
|
130
|
+
"Html.raw(\"<a href='/'>home</a>\").at_css(:a).attributes",
|
|
131
|
+
"Html.raw(\"<p class='x'>a</p>\").at_css(:p).attributes",
|
|
132
|
+
"Html.raw(\"<img alt='logo'>\").at_css(:img).attributes"
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
}.freeze
|
|
136
|
+
|
|
137
|
+
def self.function_documentation(scope)
|
|
138
|
+
return INSTANCE_FUNCTIONS if scope == :instance
|
|
139
|
+
return CLASS_FUNCTIONS if scope == :class
|
|
140
|
+
|
|
141
|
+
{}
|
|
142
|
+
end
|
|
143
|
+
|
|
6
144
|
TAGS = %w[
|
|
7
145
|
a
|
|
8
146
|
abbr
|
|
@@ -124,7 +262,9 @@ class Code
|
|
|
124
262
|
args.first.is_a?(Nokogiri::XML::Node)
|
|
125
263
|
args.first
|
|
126
264
|
else
|
|
127
|
-
|
|
265
|
+
source = args.first.to_s
|
|
266
|
+
::Code.ensure_input_size!(source, label: "html")
|
|
267
|
+
Nokogiri.HTML(source)
|
|
128
268
|
end
|
|
129
269
|
end
|
|
130
270
|
|
|
@@ -188,14 +328,18 @@ class Code
|
|
|
188
328
|
|
|
189
329
|
if code_function.something?
|
|
190
330
|
code_content =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
331
|
+
begin
|
|
332
|
+
code_function.call(
|
|
333
|
+
arguments: List.new([code_name, code_attributes]),
|
|
334
|
+
**globals
|
|
335
|
+
)
|
|
336
|
+
rescue Error::Next => e
|
|
337
|
+
e.code_value
|
|
338
|
+
end
|
|
195
339
|
|
|
196
340
|
content =
|
|
197
341
|
if code_content.is_an?(Html)
|
|
198
|
-
|
|
342
|
+
fragment_from_html(code_content)
|
|
199
343
|
else
|
|
200
344
|
Nokogiri::XML::Text.new(code_content.to_s, fragment.document)
|
|
201
345
|
end
|
|
@@ -206,33 +350,55 @@ class Code
|
|
|
206
350
|
fragment.add_child(node)
|
|
207
351
|
|
|
208
352
|
Html.new(fragment)
|
|
353
|
+
rescue Error::Break => e
|
|
354
|
+
e.code_value
|
|
209
355
|
end
|
|
210
356
|
|
|
211
357
|
def self.code_escape(value_or_function = nil, **globals)
|
|
212
358
|
code_value =
|
|
213
359
|
if value_or_function.is_a?(Function)
|
|
214
|
-
|
|
360
|
+
begin
|
|
361
|
+
value_or_function.to_code.call(**globals)
|
|
362
|
+
rescue Error::Next => e
|
|
363
|
+
e.code_value
|
|
364
|
+
end
|
|
215
365
|
else
|
|
216
366
|
value_or_function.to_code
|
|
217
367
|
end
|
|
218
368
|
|
|
219
369
|
String.new(CGI.escapeHTML(code_value.to_s))
|
|
370
|
+
rescue Error::Break => e
|
|
371
|
+
e.code_value
|
|
220
372
|
end
|
|
221
373
|
|
|
222
374
|
def self.code_unescape(value_or_function = nil, **globals)
|
|
223
375
|
code_value =
|
|
224
376
|
if value_or_function.is_a?(Function)
|
|
225
|
-
|
|
377
|
+
begin
|
|
378
|
+
value_or_function.to_code.call(**globals)
|
|
379
|
+
rescue Error::Next => e
|
|
380
|
+
e.code_value
|
|
381
|
+
end
|
|
226
382
|
else
|
|
227
383
|
value_or_function.to_code
|
|
228
384
|
end
|
|
229
385
|
|
|
230
|
-
|
|
386
|
+
source = code_value.to_s
|
|
387
|
+
::Code.ensure_input_size!(source, label: "html")
|
|
388
|
+
|
|
389
|
+
String.new(Nokogiri::HTML.fragment(source).text)
|
|
390
|
+
rescue Error::Break => e
|
|
391
|
+
e.code_value
|
|
231
392
|
end
|
|
232
393
|
|
|
233
394
|
def self.code_join(first = nil, second = nil, **globals)
|
|
234
395
|
if second.is_a?(Function)
|
|
235
|
-
code_contents =
|
|
396
|
+
code_contents =
|
|
397
|
+
begin
|
|
398
|
+
second.to_code.call(**globals)
|
|
399
|
+
rescue Error::Next => e
|
|
400
|
+
e.code_value
|
|
401
|
+
end
|
|
236
402
|
code_separator = first.to_code
|
|
237
403
|
else
|
|
238
404
|
code_contents = first.to_code
|
|
@@ -247,14 +413,14 @@ class Code
|
|
|
247
413
|
code_contents.raw.each.with_index do |code_content, index|
|
|
248
414
|
content =
|
|
249
415
|
if code_content.is_an?(Html)
|
|
250
|
-
|
|
416
|
+
fragment_from_html(code_content)
|
|
251
417
|
else
|
|
252
418
|
Nokogiri::XML::Text.new(code_content.to_s, fragment.document)
|
|
253
419
|
end
|
|
254
420
|
|
|
255
421
|
separator =
|
|
256
422
|
if code_separator.is_an?(Html)
|
|
257
|
-
|
|
423
|
+
fragment_from_html(code_separator)
|
|
258
424
|
else
|
|
259
425
|
Nokogiri::XML::Text.new(code_separator.to_s, fragment.document)
|
|
260
426
|
end
|
|
@@ -264,12 +430,18 @@ class Code
|
|
|
264
430
|
end
|
|
265
431
|
|
|
266
432
|
Html.new(fragment)
|
|
433
|
+
rescue Error::Break => e
|
|
434
|
+
e.code_value
|
|
267
435
|
end
|
|
268
436
|
|
|
269
437
|
def self.code_text(value_or_function = nil, **globals)
|
|
270
438
|
code_value =
|
|
271
439
|
if value_or_function.is_a?(Function)
|
|
272
|
-
|
|
440
|
+
begin
|
|
441
|
+
value_or_function.to_code.call(**globals)
|
|
442
|
+
rescue Error::Next => e
|
|
443
|
+
e.code_value
|
|
444
|
+
end
|
|
273
445
|
else
|
|
274
446
|
value_or_function.to_code
|
|
275
447
|
end
|
|
@@ -280,21 +452,37 @@ class Code
|
|
|
280
452
|
)
|
|
281
453
|
|
|
282
454
|
Html.new(fragment)
|
|
455
|
+
rescue Error::Break => e
|
|
456
|
+
e.code_value
|
|
283
457
|
end
|
|
284
458
|
|
|
285
459
|
def self.code_raw(value_or_function = nil, **globals)
|
|
286
460
|
code_value =
|
|
287
461
|
if value_or_function.is_a?(Function)
|
|
288
|
-
|
|
462
|
+
begin
|
|
463
|
+
value_or_function.to_code.call(**globals)
|
|
464
|
+
rescue Error::Next => e
|
|
465
|
+
e.code_value
|
|
466
|
+
end
|
|
289
467
|
else
|
|
290
468
|
value_or_function.to_code
|
|
291
469
|
end
|
|
292
470
|
|
|
293
471
|
if code_value.is_an?(Html)
|
|
294
|
-
Html.new(
|
|
472
|
+
Html.new(fragment_from_html(code_value))
|
|
295
473
|
else
|
|
296
|
-
|
|
474
|
+
source = code_value.to_s
|
|
475
|
+
::Code.ensure_input_size!(source, label: "html")
|
|
476
|
+
Html.new(Nokogiri::HTML::DocumentFragment.parse(source))
|
|
297
477
|
end
|
|
478
|
+
rescue Error::Break => e
|
|
479
|
+
e.code_value
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def self.fragment_from_html(html)
|
|
483
|
+
source = html.to_html
|
|
484
|
+
::Code.ensure_input_size!(source, label: "html")
|
|
485
|
+
Nokogiri::HTML::DocumentFragment.parse(source)
|
|
298
486
|
end
|
|
299
487
|
|
|
300
488
|
def call(**args)
|
|
@@ -319,9 +507,15 @@ class Code
|
|
|
319
507
|
when "to_html"
|
|
320
508
|
sig(args)
|
|
321
509
|
code_to_html
|
|
510
|
+
when "inner_text"
|
|
511
|
+
sig(args)
|
|
512
|
+
code_inner_text
|
|
322
513
|
when "attribute"
|
|
323
514
|
sig(args) { String }
|
|
324
515
|
code_attribute(code_value)
|
|
516
|
+
when "attributes"
|
|
517
|
+
sig(args)
|
|
518
|
+
code_attributes
|
|
325
519
|
else
|
|
326
520
|
super
|
|
327
521
|
end
|
|
@@ -352,6 +546,8 @@ class Code
|
|
|
352
546
|
e.code_value
|
|
353
547
|
end
|
|
354
548
|
)
|
|
549
|
+
rescue Error::Break => e
|
|
550
|
+
e.code_value
|
|
355
551
|
end
|
|
356
552
|
|
|
357
553
|
def to_s
|
|
@@ -370,10 +566,25 @@ class Code
|
|
|
370
566
|
String.new(raw.text)
|
|
371
567
|
end
|
|
372
568
|
|
|
569
|
+
def code_inner_text
|
|
570
|
+
code_to_string
|
|
571
|
+
end
|
|
572
|
+
|
|
373
573
|
def code_attribute(value = nil)
|
|
374
574
|
code_value = value.to_code
|
|
375
575
|
String.new(raw.attr(code_value.to_s))
|
|
376
576
|
end
|
|
577
|
+
|
|
578
|
+
def code_attributes
|
|
579
|
+
node = raw.is_a?(::Nokogiri::XML::NodeSet) ? raw.first : raw
|
|
580
|
+
return Dictionary.new if node.blank?
|
|
581
|
+
|
|
582
|
+
Dictionary.new(
|
|
583
|
+
node.attribute_nodes.to_h do |attribute|
|
|
584
|
+
[attribute.name.to_code, attribute.value.to_code]
|
|
585
|
+
end
|
|
586
|
+
)
|
|
587
|
+
end
|
|
377
588
|
end
|
|
378
589
|
end
|
|
379
590
|
end
|
data/lib/code/object/http.rb
CHANGED
|
@@ -3,6 +3,105 @@
|
|
|
3
3
|
class Code
|
|
4
4
|
class Object
|
|
5
5
|
class Http < Object
|
|
6
|
+
CLASS_DOCUMENTATION = {
|
|
7
|
+
name: "Http",
|
|
8
|
+
description: "sends http requests and returns dictionaries with request and response details.",
|
|
9
|
+
examples: [
|
|
10
|
+
"Http.get(\"http://httpbin.org/status/200\").success?",
|
|
11
|
+
"Http.post(\"http://httpbin.org/status/200\", body: :hello).request.body",
|
|
12
|
+
"Http.fetch(:get, \"http://httpbin.org/status/204\").code"
|
|
13
|
+
]
|
|
14
|
+
}.freeze
|
|
15
|
+
CLASS_FUNCTIONS = {
|
|
16
|
+
"get" => {
|
|
17
|
+
name: "get",
|
|
18
|
+
description: "sends a get request and returns the response dictionary.",
|
|
19
|
+
examples: [
|
|
20
|
+
"Http.get(\"http://httpbin.org/status/200\").code",
|
|
21
|
+
"Http.get(\"http://httpbin.org/status/200\", query: { q: :ruby }).url",
|
|
22
|
+
"Http.get(\"http://httpbin.org/status/200\", headers: { accept: \"application/json\" }).request.headers"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"head" => {
|
|
26
|
+
name: "head",
|
|
27
|
+
description: "sends a head request and returns the response dictionary.",
|
|
28
|
+
examples: [
|
|
29
|
+
"Http.head(\"http://httpbin.org/status/200\").code",
|
|
30
|
+
"Http.head(\"http://httpbin.org/status/204\").status",
|
|
31
|
+
"Http.head(\"http://httpbin.org/status/200\", headers: { accept: \"text/html\" }).request.headers"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"post" => {
|
|
35
|
+
name: "post",
|
|
36
|
+
description: "sends a post request with optional body, form data, and headers.",
|
|
37
|
+
examples: [
|
|
38
|
+
"Http.post(\"http://httpbin.org/status/200\", body: :hello).request.body",
|
|
39
|
+
"Http.post(\"http://httpbin.org/status/200\", data: { a: 1 }).success?",
|
|
40
|
+
"Http.post(\"http://httpbin.org/status/200\", headers: { content_type: \"text/plain\" }).request.headers"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"put" => {
|
|
44
|
+
name: "put",
|
|
45
|
+
description: "sends a put request with optional body or form data.",
|
|
46
|
+
examples: [
|
|
47
|
+
"Http.put(\"http://httpbin.org/status/200\", body: :hello).request.body",
|
|
48
|
+
"Http.put(\"http://httpbin.org/status/200\", data: { a: 1 }).method",
|
|
49
|
+
"Http.put(\"http://httpbin.org/status/204\").code"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"delete" => {
|
|
53
|
+
name: "delete",
|
|
54
|
+
description: "sends a delete request and returns the response dictionary.",
|
|
55
|
+
examples: [
|
|
56
|
+
"Http.delete(\"http://httpbin.org/status/200\").success?",
|
|
57
|
+
"Http.delete(\"http://httpbin.org/status/200\", body: :hello).request.body",
|
|
58
|
+
"Http.delete(\"http://httpbin.org/status/204\").status"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"options" => {
|
|
62
|
+
name: "options",
|
|
63
|
+
description: "sends an options request and returns the response dictionary.",
|
|
64
|
+
examples: [
|
|
65
|
+
"Http.options(\"http://httpbin.org/status/200\").method",
|
|
66
|
+
"Http.options(\"http://httpbin.org/status/204\").code",
|
|
67
|
+
"Http.options(\"http://httpbin.org/status/200\", headers: { accept: \"*/*\" }).request.headers"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"trace" => {
|
|
71
|
+
name: "trace",
|
|
72
|
+
description: "sends a trace request and returns the response dictionary.",
|
|
73
|
+
examples: [
|
|
74
|
+
"Http.trace(\"http://httpbin.org/status/200\").method",
|
|
75
|
+
"Http.trace(\"http://httpbin.org/status/204\").code",
|
|
76
|
+
"Http.trace(\"http://httpbin.org/status/200\", headers: { accept: \"message/http\" }).request.headers"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"patch" => {
|
|
80
|
+
name: "patch",
|
|
81
|
+
description: "sends a patch request with optional body or form data.",
|
|
82
|
+
examples: [
|
|
83
|
+
"Http.patch(\"http://httpbin.org/status/200\", body: :hello).request.body",
|
|
84
|
+
"Http.patch(\"http://httpbin.org/status/200\", data: { a: 1 }).method",
|
|
85
|
+
"Http.patch(\"http://httpbin.org/status/204\").code"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"fetch" => {
|
|
89
|
+
name: "fetch",
|
|
90
|
+
description: "sends an http request using the given method name.",
|
|
91
|
+
examples: [
|
|
92
|
+
"Http.fetch(:get, \"http://httpbin.org/status/200\").success?",
|
|
93
|
+
"Http.fetch(:post, \"http://httpbin.org/status/200\", body: :hello).request.body",
|
|
94
|
+
"Http.fetch(:patch, \"http://httpbin.org/status/200\", data: { a: 1 }).method"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
def self.function_documentation(scope)
|
|
100
|
+
return CLASS_FUNCTIONS if scope == :class
|
|
101
|
+
|
|
102
|
+
{}
|
|
103
|
+
end
|
|
104
|
+
|
|
6
105
|
SIG = [
|
|
7
106
|
String,
|
|
8
107
|
{
|
|
@@ -85,6 +184,20 @@ class Code
|
|
|
85
184
|
network_authentication_required: 511
|
|
86
185
|
}.freeze
|
|
87
186
|
DEFAULT_TIMEOUT = 1.hour.to_f
|
|
187
|
+
MAX_REQUEST_BYTES = ::Code::MAX_INPUT_BYTES
|
|
188
|
+
MAX_RESPONSE_BYTES = ::Code::MAX_INPUT_BYTES
|
|
189
|
+
MAX_HEADER_BYTES = 32.kilobytes
|
|
190
|
+
HEADER_NAME = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
|
|
191
|
+
RESTRICTED_HEADERS = %w[
|
|
192
|
+
connection
|
|
193
|
+
content-length
|
|
194
|
+
host
|
|
195
|
+
proxy-authorization
|
|
196
|
+
te
|
|
197
|
+
trailer
|
|
198
|
+
transfer-encoding
|
|
199
|
+
upgrade
|
|
200
|
+
].freeze
|
|
88
201
|
|
|
89
202
|
def self.call(**args)
|
|
90
203
|
code_operator = args.fetch(:operator, nil).to_code
|
|
@@ -163,7 +276,7 @@ class Code
|
|
|
163
276
|
username = options.code_get("username").to_s
|
|
164
277
|
password = options.code_get("password").to_s
|
|
165
278
|
body = options.code_get("body").to_s
|
|
166
|
-
headers = options.code_get("headers").raw || {}
|
|
279
|
+
headers = sanitized_headers(options.code_get("headers").raw || {})
|
|
167
280
|
data = options.code_get("data").raw || {}
|
|
168
281
|
timeout = options.code_get("timeout")
|
|
169
282
|
open_timeout = options.code_get("open_timeout")
|
|
@@ -171,31 +284,39 @@ class Code
|
|
|
171
284
|
write_timeout = options.code_get("write_timeout")
|
|
172
285
|
query = options.code_get("query").raw || {}
|
|
173
286
|
query = query.to_a.flatten.map(&:to_s).each_slice(2).to_h.to_query
|
|
287
|
+
validate_payload_size!(username, label: "http username")
|
|
288
|
+
validate_payload_size!(password, label: "http password")
|
|
289
|
+
validate_payload_size!(query, label: "http query")
|
|
290
|
+
validate_payload_size!(body, label: "http request body")
|
|
291
|
+
validate_payload_size!(data.as_json.to_query, label: "http form data") if data.present?
|
|
174
292
|
|
|
175
293
|
url = original_url
|
|
176
294
|
url = "#{url}?#{query}" if query.present?
|
|
295
|
+
validate_payload_size!(url, label: "http url")
|
|
177
296
|
|
|
178
297
|
if username.present? || password.present?
|
|
179
298
|
authorization = ::Base64.strict_encode64("#{username}:#{password}")
|
|
180
299
|
headers["Authorization"] = "Basic #{authorization}"
|
|
300
|
+
validate_header_size!("Authorization", headers["Authorization"])
|
|
181
301
|
end
|
|
182
302
|
|
|
183
|
-
uri =
|
|
184
|
-
|
|
303
|
+
uri = parse_uri(url)
|
|
304
|
+
resolved_ip = ::Code::Network.validate_public_uri!(uri, service: "http")
|
|
305
|
+
|
|
306
|
+
http = ::Net::HTTP.new(uri.hostname, uri.port, nil)
|
|
307
|
+
http.ipaddr = resolved_ip
|
|
185
308
|
http.use_ssl = true if uri.scheme == "https"
|
|
186
|
-
default_timeout = timeout
|
|
309
|
+
default_timeout = http_timeout(timeout, DEFAULT_TIMEOUT)
|
|
187
310
|
open_timeout_value =
|
|
188
|
-
open_timeout
|
|
311
|
+
http_timeout(open_timeout, default_timeout)
|
|
189
312
|
read_timeout_value =
|
|
190
|
-
read_timeout
|
|
313
|
+
http_timeout(read_timeout, default_timeout)
|
|
191
314
|
write_timeout_value =
|
|
192
|
-
write_timeout
|
|
315
|
+
http_timeout(write_timeout, default_timeout)
|
|
193
316
|
|
|
194
317
|
http.open_timeout = open_timeout_value if open_timeout_value
|
|
195
318
|
http.read_timeout = read_timeout_value if read_timeout_value
|
|
196
|
-
|
|
197
|
-
http.write_timeout = write_timeout_value
|
|
198
|
-
end
|
|
319
|
+
http.write_timeout = write_timeout_value if write_timeout_value
|
|
199
320
|
|
|
200
321
|
request_class =
|
|
201
322
|
case verb
|
|
@@ -223,7 +344,19 @@ class Code
|
|
|
223
344
|
request.set_form_data(**data.as_json) if data.present?
|
|
224
345
|
|
|
225
346
|
begin
|
|
226
|
-
|
|
347
|
+
response_body = +""
|
|
348
|
+
response =
|
|
349
|
+
http.request(request) do |http_response|
|
|
350
|
+
validate_response_headers!(http_response)
|
|
351
|
+
|
|
352
|
+
http_response.read_body do |chunk|
|
|
353
|
+
if response_body.bytesize + chunk.bytesize > MAX_RESPONSE_BYTES
|
|
354
|
+
raise ::Code::Error, "http response is too large"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
response_body << chunk
|
|
358
|
+
end
|
|
359
|
+
end
|
|
227
360
|
rescue ::Timeout::Error, ::Errno::ETIMEDOUT
|
|
228
361
|
raise ::Code::Error, "http timeout"
|
|
229
362
|
rescue OpenSSL::SSL::SSLError, IOError, SystemCallError, SocketError
|
|
@@ -234,9 +367,10 @@ class Code
|
|
|
234
367
|
location = response["location"].to_s
|
|
235
368
|
|
|
236
369
|
if (300..399).cover?(code) && location.present? && redirects.positive?
|
|
237
|
-
new_uri = ::URI.join(uri, location)
|
|
370
|
+
new_uri = parse_uri(::URI.join(uri, location).to_s)
|
|
371
|
+
::Code::Network.validate_public_uri!(new_uri, service: "http")
|
|
238
372
|
|
|
239
|
-
if new_uri
|
|
373
|
+
if same_origin?(new_uri, uri)
|
|
240
374
|
code_fetch(
|
|
241
375
|
"get",
|
|
242
376
|
new_uri.to_s,
|
|
@@ -248,8 +382,104 @@ class Code
|
|
|
248
382
|
end
|
|
249
383
|
else
|
|
250
384
|
status = STATUS_CODES.key(code) || :ok
|
|
251
|
-
|
|
385
|
+
response_headers = response.each_header.to_h
|
|
386
|
+
request_headers = request.to_hash.transform_values do |values|
|
|
387
|
+
List.new(values)
|
|
388
|
+
end
|
|
389
|
+
body = response_body.to_s
|
|
390
|
+
|
|
391
|
+
Dictionary.new(
|
|
392
|
+
code: code,
|
|
393
|
+
status: status,
|
|
394
|
+
body: body,
|
|
395
|
+
headers: response_headers,
|
|
396
|
+
method: verb,
|
|
397
|
+
url: url,
|
|
398
|
+
"success?" => code.between?(200, 299),
|
|
399
|
+
"redirect?" => code.between?(300, 399),
|
|
400
|
+
request: {
|
|
401
|
+
method: verb,
|
|
402
|
+
url: url,
|
|
403
|
+
headers: request_headers,
|
|
404
|
+
body: request.body.to_s
|
|
405
|
+
},
|
|
406
|
+
response: {
|
|
407
|
+
code: code,
|
|
408
|
+
status: status,
|
|
409
|
+
headers: response_headers,
|
|
410
|
+
body: body
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def self.parse_uri(url)
|
|
417
|
+
::URI.parse(url)
|
|
418
|
+
rescue ::URI::InvalidURIError
|
|
419
|
+
raise ::Code::Error, "http: invalid url"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def self.same_origin?(new_uri, original_uri)
|
|
423
|
+
new_uri.scheme == original_uri.scheme &&
|
|
424
|
+
new_uri.hostname.to_s.downcase == original_uri.hostname.to_s.downcase &&
|
|
425
|
+
new_uri.port == original_uri.port
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def self.sanitized_headers(headers)
|
|
429
|
+
headers.to_h.to_h do |key, value|
|
|
430
|
+
name = key.to_s
|
|
431
|
+
header_value = value.to_s
|
|
432
|
+
|
|
433
|
+
validate_header_name!(name)
|
|
434
|
+
validate_header_value!(header_value)
|
|
435
|
+
validate_header_size!(name, header_value)
|
|
436
|
+
|
|
437
|
+
[name, header_value]
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def self.validate_header_name!(name)
|
|
442
|
+
normalized_name = name.downcase
|
|
443
|
+
|
|
444
|
+
unless HEADER_NAME.match?(name)
|
|
445
|
+
raise ::Code::Error, "http: invalid header name"
|
|
252
446
|
end
|
|
447
|
+
|
|
448
|
+
return unless RESTRICTED_HEADERS.include?(normalized_name)
|
|
449
|
+
|
|
450
|
+
raise ::Code::Error, "http: restricted header #{name.inspect}"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def self.validate_header_value!(value)
|
|
454
|
+
return unless /[\r\n\0]/.match?(value)
|
|
455
|
+
|
|
456
|
+
raise ::Code::Error, "http: invalid header value"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def self.validate_header_size!(name, value)
|
|
460
|
+
return if name.bytesize + value.bytesize <= MAX_HEADER_BYTES
|
|
461
|
+
|
|
462
|
+
raise ::Code::Error, "http: request headers are too large"
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def self.validate_response_headers!(response)
|
|
466
|
+
size =
|
|
467
|
+
response.each_header.sum do |name, value|
|
|
468
|
+
name.to_s.bytesize + value.to_s.bytesize
|
|
469
|
+
end
|
|
470
|
+
return if size <= MAX_HEADER_BYTES
|
|
471
|
+
|
|
472
|
+
raise ::Code::Error, "http response headers are too large"
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def self.validate_payload_size!(value, label:)
|
|
476
|
+
return if value.to_s.bytesize <= MAX_REQUEST_BYTES
|
|
477
|
+
|
|
478
|
+
raise ::Code::Error, "#{label} is too large"
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def self.http_timeout(value, default)
|
|
482
|
+
::Code.normalize_timeout!(value.nothing? ? default : value)
|
|
253
483
|
end
|
|
254
484
|
end
|
|
255
485
|
end
|