webclient 0.2.1 → 0.3.1
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/CHANGELOG.md +4 -4
- data/Manifest.txt +5 -1
- data/README.md +23 -22
- data/Rakefile +28 -28
- data/lib/webclient/version.rb +19 -20
- data/lib/webclient/webclient-get.rb +51 -0
- data/lib/webclient/webclient-post.rb +93 -0
- data/lib/webclient/webclient_response-status.rb +50 -0
- data/lib/webclient/webclient_response-text.rb +306 -0
- data/lib/webclient/webclient_response.rb +112 -0
- data/lib/webclient.rb +38 -25
- metadata +12 -8
- data/lib/webclient/webclient.rb +0 -170
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 177c9b330862eac8b588d0bffe1d4819ffe20b5f03f13e8617291e1c890bc979
|
|
4
|
+
data.tar.gz: c0819b68fa611fa4aa38f088816984ae6f1d444f710a425135ead32d60f71f9f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b3fdc8068632f7184fd77f6f7a34e2126ca54d7b7a09d4d1d5f92686dcfb16dc6af0c0a6470f188af82f9274388cc6aad0774c97a4e1aa312508976a328d24e
|
|
7
|
+
data.tar.gz: 74ea0e68e127db132d859f32806dc1f07fa86aa1b08beeeb949fc37aa53a405473825481dbe9bc2ffea1b5ea0d9d16555022f4ab956a9fc9df457d9d35da3401
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
### 0.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
### 0.3.1
|
|
2
|
+
### 0.0.1 / 2020-10-03
|
|
3
|
+
|
|
4
|
+
* Everything is new. First release.
|
data/Manifest.txt
CHANGED
|
@@ -4,4 +4,8 @@ README.md
|
|
|
4
4
|
Rakefile
|
|
5
5
|
lib/webclient.rb
|
|
6
6
|
lib/webclient/version.rb
|
|
7
|
-
lib/webclient/webclient.rb
|
|
7
|
+
lib/webclient/webclient-get.rb
|
|
8
|
+
lib/webclient/webclient-post.rb
|
|
9
|
+
lib/webclient/webclient_response-status.rb
|
|
10
|
+
lib/webclient/webclient_response-text.rb
|
|
11
|
+
lib/webclient/webclient_response.rb
|
data/README.md
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
# webclient
|
|
2
|
-
|
|
3
|
-
webclient gem - yet (another)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* home :: [github.com/
|
|
7
|
-
* bugs :: [github.com/
|
|
8
|
-
* gem :: [rubygems.org/gems/webclient](https://rubygems.org/gems/webclient)
|
|
9
|
-
* rdoc :: [rubydoc.info/gems/webclient](http://rubydoc.info/gems/webclient)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
# webclient
|
|
2
|
+
|
|
3
|
+
webclient gem - yet (another) universal network client interface for world wide web (www) requests via HTTP
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
* home :: [github.com/rubycocos/webclient](https://github.com/rubycocos/webclient)
|
|
7
|
+
* bugs :: [github.com/rubycocos/webclient/issues](https://github.com/rubycocos/webclient/issues)
|
|
8
|
+
* gem :: [rubygems.org/gems/webclient](https://rubygems.org/gems/webclient)
|
|
9
|
+
* rdoc :: [rubydoc.info/gems/webclient](http://rubydoc.info/gems/webclient)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
TBD
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## License
|
|
21
|
+
|
|
22
|
+
The `webclient` scripts are dedicated to the public domain.
|
|
23
|
+
Use as you please with no restrictions whatsoever.
|
data/Rakefile
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
require 'hoe'
|
|
2
|
-
require './lib/webclient/version.rb'
|
|
3
|
-
|
|
4
|
-
Hoe.spec 'webclient' do
|
|
5
|
-
|
|
6
|
-
self.version = Webclient::VERSION
|
|
7
|
-
|
|
8
|
-
self.summary = 'webclient gem - yet (another) universal network client interface for world wide web (www) requests via HTTP'
|
|
9
|
-
self.description = summary
|
|
10
|
-
|
|
11
|
-
self.urls = { home: 'https://github.com/
|
|
12
|
-
|
|
13
|
-
self.author = 'Gerald Bauer'
|
|
14
|
-
self.email = '
|
|
15
|
-
|
|
16
|
-
# switch extension to .markdown for gihub formatting
|
|
17
|
-
self.readme_file = 'README.md'
|
|
18
|
-
self.history_file = 'CHANGELOG.md'
|
|
19
|
-
|
|
20
|
-
self.extra_deps = []
|
|
21
|
-
|
|
22
|
-
self.licenses = ['Public Domain']
|
|
23
|
-
|
|
24
|
-
self.spec_extras = {
|
|
25
|
-
required_ruby_version: '>= 2.2.2'
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
end
|
|
1
|
+
require 'hoe'
|
|
2
|
+
require './lib/webclient/version.rb'
|
|
3
|
+
|
|
4
|
+
Hoe.spec 'webclient' do
|
|
5
|
+
|
|
6
|
+
self.version = Webclient::VERSION
|
|
7
|
+
|
|
8
|
+
self.summary = 'webclient gem - yet (another) universal network client interface for world wide web (www) requests via HTTP'
|
|
9
|
+
self.description = summary
|
|
10
|
+
|
|
11
|
+
self.urls = { home: 'https://github.com/rubycocos/webclient' }
|
|
12
|
+
|
|
13
|
+
self.author = 'Gerald Bauer'
|
|
14
|
+
self.email = 'gerald.bauer@gmail.com'
|
|
15
|
+
|
|
16
|
+
# switch extension to .markdown for gihub formatting
|
|
17
|
+
self.readme_file = 'README.md'
|
|
18
|
+
self.history_file = 'CHANGELOG.md'
|
|
19
|
+
|
|
20
|
+
self.extra_deps = []
|
|
21
|
+
|
|
22
|
+
self.licenses = ['Public Domain']
|
|
23
|
+
|
|
24
|
+
self.spec_extras = {
|
|
25
|
+
required_ruby_version: '>= 2.2.2'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
end
|
data/lib/webclient/version.rb
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
class Webclient
|
|
3
|
-
MAJOR = 0 ## todo: namespace inside version or something - why? why not??
|
|
4
|
-
MINOR =
|
|
5
|
-
PATCH = 1
|
|
6
|
-
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
|
7
|
-
|
|
8
|
-
def self.version
|
|
9
|
-
VERSION
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.banner
|
|
13
|
-
"webclient/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.root
|
|
17
|
-
File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
|
|
18
|
-
end
|
|
19
|
-
end # module Webclient
|
|
20
|
-
|
|
1
|
+
|
|
2
|
+
class Webclient
|
|
3
|
+
MAJOR = 0 ## todo: namespace inside version or something - why? why not??
|
|
4
|
+
MINOR = 3
|
|
5
|
+
PATCH = 1
|
|
6
|
+
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
|
7
|
+
|
|
8
|
+
def self.version
|
|
9
|
+
VERSION
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.banner
|
|
13
|
+
"webclient/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.root
|
|
17
|
+
File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
|
|
18
|
+
end
|
|
19
|
+
end # module Webclient
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
class Webclient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def self.get( url, headers: {}, auth: [] )
|
|
6
|
+
|
|
7
|
+
uri = URI.parse( url )
|
|
8
|
+
http = Net::HTTP.new( uri.host, uri.port )
|
|
9
|
+
|
|
10
|
+
if uri.instance_of? URI::HTTPS
|
|
11
|
+
http.use_ssl = true
|
|
12
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
request = Net::HTTP::Get.new( uri.request_uri )
|
|
16
|
+
|
|
17
|
+
### add (custom) headers if any
|
|
18
|
+
## check/todo: is there are more idiomatic way for Net::HTTP ???
|
|
19
|
+
## use
|
|
20
|
+
## request = Net::HTTP::Get.new( uri.request_uri, headers )
|
|
21
|
+
## why? why not?
|
|
22
|
+
## instead of e.g.
|
|
23
|
+
## request['X-Auth-Token'] = 'xxxxxxx'
|
|
24
|
+
## request['User-Agent'] = 'ruby'
|
|
25
|
+
## request['Accept'] = '*/*'
|
|
26
|
+
if headers && headers.size > 0
|
|
27
|
+
headers.each do |key,value|
|
|
28
|
+
request[ key ] = value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if auth.size == 2 ## e.g. ['user', 'password']
|
|
34
|
+
## always assume basic auth for now
|
|
35
|
+
## auth[0] => user
|
|
36
|
+
## auth[1] => password
|
|
37
|
+
request.basic_auth( auth[0], auth[1] )
|
|
38
|
+
puts " using basic auth - user: #{auth[0]}, password: ***"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
puts "GET #{uri}..."
|
|
43
|
+
|
|
44
|
+
response = http.request( request )
|
|
45
|
+
|
|
46
|
+
## note: return "unified" wrapped response
|
|
47
|
+
Response.new( response )
|
|
48
|
+
end # method self.get
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
end # class Webclient
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
|
|
2
|
+
class Webclient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
## todo/check
|
|
8
|
+
## maybe add a
|
|
9
|
+
### self.post_form( url, form/params, **kwargs) - why? why not?
|
|
10
|
+
###
|
|
11
|
+
|
|
12
|
+
def self.post( url, headers: {},
|
|
13
|
+
auth: [],
|
|
14
|
+
body: nil,
|
|
15
|
+
form: nil,
|
|
16
|
+
json: nil ## json - convenience shortcut (for body & encoding)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
uri = URI.parse( url )
|
|
20
|
+
http = Net::HTTP.new( uri.host, uri.port )
|
|
21
|
+
|
|
22
|
+
if uri.instance_of? URI::HTTPS
|
|
23
|
+
http.use_ssl = true
|
|
24
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
request = Net::HTTP::Post.new( uri.request_uri )
|
|
28
|
+
|
|
29
|
+
### add (custom) headers if any
|
|
30
|
+
## check/todo: is there are more idiomatic way for Net::HTTP ???
|
|
31
|
+
## use
|
|
32
|
+
## request = Net::HTTP::Get.new( uri.request_uri, headers )
|
|
33
|
+
## why? why not?
|
|
34
|
+
## instead of e.g.
|
|
35
|
+
## request['X-Auth-Token'] = 'xxxxxxx'
|
|
36
|
+
## request['User-Agent'] = 'ruby'
|
|
37
|
+
## request['Accept'] = '*/*'
|
|
38
|
+
if headers && headers.size > 0
|
|
39
|
+
headers.each do |key,value|
|
|
40
|
+
request[ key ] = value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if auth.size == 2 ## e.g. ['user', 'password']
|
|
45
|
+
## always assume basic auth for now
|
|
46
|
+
## auth[0] => user
|
|
47
|
+
## auth[1] => password
|
|
48
|
+
request.basic_auth( auth[0], auth[1] )
|
|
49
|
+
puts " using basic auth - user: #{auth[0]}, password: ***"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if body
|
|
54
|
+
request.body = body.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if form
|
|
58
|
+
## fix-fix-fix: urlencode key/values!!!!!
|
|
59
|
+
|
|
60
|
+
###
|
|
61
|
+
## maybe use ??
|
|
62
|
+
## uri = URI "http://localhost:4567/greet"
|
|
63
|
+
## params = { :name => 'Peter' }
|
|
64
|
+
## uri.query = URI.encode_www_form params
|
|
65
|
+
|
|
66
|
+
form_urlencoded = form.map do |k,v|
|
|
67
|
+
"#{k}=#{v}"
|
|
68
|
+
end.join( '&' )
|
|
69
|
+
|
|
70
|
+
request.body = form_urlencoded
|
|
71
|
+
|
|
72
|
+
request['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if json
|
|
76
|
+
# note: the body needs to be a JSON string - use pretty generate and NOT "compact" style - why? why not?
|
|
77
|
+
request.body = JSON.pretty_generate( json )
|
|
78
|
+
|
|
79
|
+
## move (auto-set) header content-type up (before custom headers) - why? why not?
|
|
80
|
+
request['Content-Type'] = 'application/json'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
puts "POST #{uri}..."
|
|
85
|
+
|
|
86
|
+
response = http.request( request )
|
|
87
|
+
|
|
88
|
+
## note: return "unified" wrapped response
|
|
89
|
+
Response.new( response )
|
|
90
|
+
end # method self.post
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
end # class Webclient
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class Webclient
|
|
2
|
+
class Response
|
|
3
|
+
|
|
4
|
+
#####################
|
|
5
|
+
# nested class Response::Status
|
|
6
|
+
class Status
|
|
7
|
+
### fix-fix-fix
|
|
8
|
+
## maybe fold back
|
|
9
|
+
## into response.status | status_code
|
|
10
|
+
## response.status_message | status_msg
|
|
11
|
+
## keep it simple?
|
|
12
|
+
##
|
|
13
|
+
## keep status.ok? => response.ok?
|
|
14
|
+
## keep status.nok? => response.nok?
|
|
15
|
+
|
|
16
|
+
attr_reader :code
|
|
17
|
+
|
|
18
|
+
def initialize( code, message: nil )
|
|
19
|
+
## note - upstream Net::HTTP::Response::code is a string e.g. "200"!!!
|
|
20
|
+
## convert to integer number
|
|
21
|
+
@code = code.to_i(10)
|
|
22
|
+
@message = message
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_i() @code; end ## use alias_method :to_id, :code - why? why not?
|
|
27
|
+
def to_int() @code; end
|
|
28
|
+
|
|
29
|
+
## note - allow compare with integer e.g.
|
|
30
|
+
## response.status == 200
|
|
31
|
+
def ==(other)
|
|
32
|
+
other.is_a?(Status) ? code == other.code : code == other.to_i
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ok?() code == 200; end
|
|
37
|
+
def nok?() code != 200; end
|
|
38
|
+
|
|
39
|
+
def success?() (200..299).include?(code); end
|
|
40
|
+
def redirect?() (300..399).include?(code); end
|
|
41
|
+
def error?() code >= 400; end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def message() @message; end
|
|
45
|
+
alias_method :msg, :message ## add/keep shorter alias too - why? why not?
|
|
46
|
+
end # (nested) class Status
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
end # class Response
|
|
50
|
+
end # class Webclient
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
|
|
2
|
+
class Webclient
|
|
3
|
+
class Response
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# regex to capture the charset from both HTML5 and HTML4 meta tags
|
|
8
|
+
# -- the modern HTML5 <meta charset="..."> tag, or
|
|
9
|
+
# -- the older HTML4 <meta http-equiv="Content-Type" ...> tag
|
|
10
|
+
## <meta http-equiv="Content-Type" content="text/html;
|
|
11
|
+
## charset=windows-1252"
|
|
12
|
+
## support multi-line (m) - why? why not???
|
|
13
|
+
##
|
|
14
|
+
## note - add the n (NOENCODING) flag
|
|
15
|
+
## The n flag forces Ruby to compile and process the regex as a raw sequence of bytes
|
|
16
|
+
## (ASCII-8BIT). This allows it to safely match against US-ASCII, ASCII-8BIT,
|
|
17
|
+
## or UTF-8 strings without throwing compatibility errors
|
|
18
|
+
## charset
|
|
19
|
+
### note - charset class was [^"' >]+ changed to more strict/simple [a-z0-9-_]+
|
|
20
|
+
## check if other "weirdo" encoding name exist?
|
|
21
|
+
HTML_CHARSET_RE = %r{ <meta [^>]+
|
|
22
|
+
charset [ ]* = [ ]*
|
|
23
|
+
["']? (?<charset> [a-z0-9_-]+)
|
|
24
|
+
}ixn
|
|
25
|
+
|
|
26
|
+
HTML_CHARSET_ALIASES = {
|
|
27
|
+
'utf8' => 'UTF-8',
|
|
28
|
+
'utfs-8' => 'UTF-8', ## typo in rsssf (fix otherwise or here??)
|
|
29
|
+
'cp1252' => 'Windows-1252',
|
|
30
|
+
'latin1' => 'ISO-8859-1',
|
|
31
|
+
'ascii' => 'US-ASCII',
|
|
32
|
+
'binary' => 'ASCII-8BIT'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### internal helper
|
|
38
|
+
### to get "upstream" encoding
|
|
39
|
+
### note - unicode bom will override user encoding !!!
|
|
40
|
+
## -- use _text_encoding_upstream or such - why? why not?
|
|
41
|
+
## change/rename _8bit to chars_8bit - why? why not?
|
|
42
|
+
|
|
43
|
+
def _text_encoding() defined?( @_text_encoding ) ? @_text_encoding : nil; end
|
|
44
|
+
def _text_encoding_source() defined?( @_text_encoding_source ) ? @_text_encoding_source : nil; end
|
|
45
|
+
|
|
46
|
+
def _text_encoding_valid() defined?( @_text_encoding_valid) ? @_text_encoding_valid : nil; end
|
|
47
|
+
def _text_ascii_only() defined?( @_text_ascii_only ) ? @_text_ascii_only : nil; end
|
|
48
|
+
def _text_8bit() defined?( @_text_8bit ) ? @_text_8bit : nil; end
|
|
49
|
+
def _text_utf8_replace() defined?( @_text_utf8_replace ) ? @_text_utf8_replace : nil; end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## use encoding: nil (with fallback 'UTF-8')
|
|
54
|
+
## lets us check if user encoding passed in or
|
|
55
|
+
## if default fallback used !!!!
|
|
56
|
+
## use for encoding_source (hierarchy) !!
|
|
57
|
+
## e.g. bom|html|http| user or fallback
|
|
58
|
+
##
|
|
59
|
+
## or use a new force_encoding property/option for user
|
|
60
|
+
## e.g. bom| force| html|http|...
|
|
61
|
+
|
|
62
|
+
## todo/check: rename encoding to html/http-like charset - why? why not?
|
|
63
|
+
## or keep encoding as used for ruby's strings
|
|
64
|
+
def _decode_text( encoding: _encoding_user )
|
|
65
|
+
|
|
66
|
+
if encoding.nil?
|
|
67
|
+
encoding = 'UTF-8' ### use UTF-8 as fallback (default encoding)
|
|
68
|
+
encoding_source = 'fallback'
|
|
69
|
+
else
|
|
70
|
+
encoding_source = 'user'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# note: Net::HTTP will NOT set encoding UTF-8 etc.
|
|
75
|
+
# will be set to ASCII-8BIT == BINARY == Encoding Unknown; Raw Bytes Here
|
|
76
|
+
##
|
|
77
|
+
## todo/assert
|
|
78
|
+
## make sure encoding is ASCII-8BIT == BINARY !!!
|
|
79
|
+
##
|
|
80
|
+
## note !!!! - make sure text is always a copy (thus, use dup(licate)!!)
|
|
81
|
+
## NOT a reference to @response.body.to_s
|
|
82
|
+
## otherwise force_encoding
|
|
83
|
+
## will change the encoding "upstream"
|
|
84
|
+
text = @response.body.b.dup
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## note - record 7bit ascii code range (ENC_CODERANGE_7BIT) check (on "raw" blob before changing encoding)
|
|
89
|
+
## see https://shopify.engineering/code-ranges-ruby-strings
|
|
90
|
+
##
|
|
91
|
+
## String#ascii_only?
|
|
92
|
+
## returns true if every character in the string has a byte value between 0 and 127.
|
|
93
|
+
##
|
|
94
|
+
### ENC_CODERANGE_7BIT:
|
|
95
|
+
## Every single byte in the string is between 0 and 127.
|
|
96
|
+
## If this flag is already set, ascii_only?
|
|
97
|
+
## immediately returns true.
|
|
98
|
+
##
|
|
99
|
+
## ENC_CODERANGE_VALID:
|
|
100
|
+
## The string contains valid characters for its encoding (like UTF-8),
|
|
101
|
+
## but at least one character is outside the 0–127 range
|
|
102
|
+
## (e.g., it contains a 128+ byte).
|
|
103
|
+
## If this flag is set, it immediately returns false.
|
|
104
|
+
##
|
|
105
|
+
##
|
|
106
|
+
## check before optional bom-removal
|
|
107
|
+
@_text_ascii_only = text.ascii_only?
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
###
|
|
111
|
+
## note
|
|
112
|
+
## auto-check for unicode byte-order marks (BOM)s!!
|
|
113
|
+
## and auto-strip bom!!
|
|
114
|
+
##
|
|
115
|
+
## common BOMs to check
|
|
116
|
+
## UTF-8: EF BB BF
|
|
117
|
+
## UTF-16 BE: FE FF
|
|
118
|
+
## UTF-16 LE: FF FE
|
|
119
|
+
## UTF-32 BE: 00 00 FE FF
|
|
120
|
+
## UTF-32 LE: FF FE 00 00
|
|
121
|
+
|
|
122
|
+
encoding_bom =
|
|
123
|
+
if text.start_with?("\x00\x00\xFE\xFF".b)
|
|
124
|
+
text = text.byteslice(4..)
|
|
125
|
+
'UTF-32BE'
|
|
126
|
+
elsif text.start_with?("\xFF\xFE\x00\x00".b)
|
|
127
|
+
text = text.byteslice(4..)
|
|
128
|
+
'UTF-32LE'
|
|
129
|
+
elsif text.start_with?("\xFE\xFF".b)
|
|
130
|
+
text = text.byteslice(2..)
|
|
131
|
+
'UTF-16BE'
|
|
132
|
+
elsif text.start_with?("\xFF\xFE".b)
|
|
133
|
+
text = text.byteslice(2..)
|
|
134
|
+
'UTF-16LE'
|
|
135
|
+
elsif text.start_with?("\xEF\xBB\xBF".b)
|
|
136
|
+
text = text.byteslice(3..)
|
|
137
|
+
'UTF-8'
|
|
138
|
+
else
|
|
139
|
+
nil # no bom found
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if encoding_bom
|
|
146
|
+
puts " [debug] auto-removing unicode >#{encoding_bom}< encoding bom (magic bytes) in response.text"
|
|
147
|
+
|
|
148
|
+
if encoding_bom.downcase != encoding.downcase
|
|
149
|
+
puts " [debug] !!! WARN - auto-fixing response.text encoding; >#{encoding}< overridden by >#{encoding_bom}< unicode encoding bom"
|
|
150
|
+
encoding = encoding_bom
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
encoding_source = 'bom'
|
|
154
|
+
else
|
|
155
|
+
## fix-fix-fix check/add http content type check with charset!!
|
|
156
|
+
##
|
|
157
|
+
## check if html content type
|
|
158
|
+
## text/html
|
|
159
|
+
## application/xhtml+xml
|
|
160
|
+
## && check html meta charset in page in first 1028 bytes
|
|
161
|
+
##
|
|
162
|
+
## note - content_type might return nil (guard with to_s!!)
|
|
163
|
+
## maybe use/make into html? helper like gif? pdf? or such
|
|
164
|
+
|
|
165
|
+
if content_type.to_s.match?( %r{text/html}i ) ||
|
|
166
|
+
content_type.to_s.match?( %r{application/xhtml}i )
|
|
167
|
+
|
|
168
|
+
if (m = HTML_CHARSET_RE.match( text[0, 1028] ))
|
|
169
|
+
encoding_html = m[:charset]
|
|
170
|
+
## note - normalize encoding_html
|
|
171
|
+
## plus fix known type errors!!!
|
|
172
|
+
encoding_html = HTML_CHARSET_ALIASES[ encoding_html.downcase ] || encoding_html
|
|
173
|
+
|
|
174
|
+
## fix-fix-fix
|
|
175
|
+
## validate with ruby's builtin in encoding registry!!!
|
|
176
|
+
# 3. Validate against Ruby's internal encoding registry
|
|
177
|
+
## begin
|
|
178
|
+
## Encoding.find(standard_name).name
|
|
179
|
+
## rescue ArgumentError
|
|
180
|
+
## unknown encoding!!!
|
|
181
|
+
## end
|
|
182
|
+
|
|
183
|
+
if encoding_html.downcase != encoding.downcase
|
|
184
|
+
## note - change WARN to INFO
|
|
185
|
+
puts " [debug] !!! WARN - overwrite response.text encoding; >#{encoding}< overridden by >#{encoding_html}< html meta charset"
|
|
186
|
+
encoding = encoding_html
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
encoding_source = 'html'
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
##
|
|
196
|
+
## note - allow "hack-y" access to "upstream" encoding used before conversion to utf-8
|
|
197
|
+
## e.g. use response._text_encoding or
|
|
198
|
+
## response._text_encoding_source (e.g. bom|html|http|user)
|
|
199
|
+
@_text_encoding = encoding
|
|
200
|
+
@_text_encoding_source = encoding_source
|
|
201
|
+
|
|
202
|
+
###
|
|
203
|
+
### if encoding.start_with? utf
|
|
204
|
+
## or has encoding_bom
|
|
205
|
+
### do nothing
|
|
206
|
+
## otherwise
|
|
207
|
+
## tally all 8-bit ascii chars (above > 127)
|
|
208
|
+
|
|
209
|
+
if encoding_bom || encoding.downcase.start_with?( 'utf' )
|
|
210
|
+
@_text_8bit = nil
|
|
211
|
+
else
|
|
212
|
+
## get/track 8-bit bytes (1xxxxxxx), that is, > 127 (128-255)
|
|
213
|
+
bytes = text.bytes.select { |byte| byte > 127 }
|
|
214
|
+
|
|
215
|
+
if bytes.empty?
|
|
216
|
+
@_text_8bit = nil
|
|
217
|
+
else
|
|
218
|
+
@_text_8bit = "#{bytes.count} - "
|
|
219
|
+
## bytes.tally
|
|
220
|
+
## e.g. {195=>1, 169=>1, 240=>1, 159=>1, 152=>1, 138=>1}
|
|
221
|
+
## note - use sort (turns in array e.g. [[138,1],...])
|
|
222
|
+
@_text_8bit += bytes.tally.sort.map {|ord,count| "#{ord}=>#{count}"}.join(', ')
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if encoding.downcase == 'utf-8'
|
|
232
|
+
text = text.force_encoding( Encoding::UTF_8 )
|
|
233
|
+
|
|
234
|
+
## track/check code range if valid/broken
|
|
235
|
+
@_text_encoding_valid = text.valid_encoding?
|
|
236
|
+
else
|
|
237
|
+
## [debug] GET=http://www.football-data.co.uk/mmz4281/0405/SC0.csv
|
|
238
|
+
## Encoding::UndefinedConversionError: "\xA0" from ASCII-8BIT to UTF-8
|
|
239
|
+
## note: 0xA0 (160) is NBSP (non-breaking space) in Windows-1252
|
|
240
|
+
|
|
241
|
+
## note: assume windows encoding (for football-data.uk)
|
|
242
|
+
## use "Windows-1252" for input and convert to utf-8
|
|
243
|
+
##
|
|
244
|
+
## see https://www.justinweiss.com/articles/3-steps-to-fix-encoding-problems-in-ruby/
|
|
245
|
+
## see https://en.wikipedia.org/wiki/Windows-1252
|
|
246
|
+
## txt = txt.force_encoding( 'Windows-1252' )
|
|
247
|
+
## txt = txt.encode( 'UTF-8' )
|
|
248
|
+
## Encoding::UTF_8 => 'UTF-8'
|
|
249
|
+
puts " [debug] try converting response.text encoding from >#{encoding}< to >UTF-8<"
|
|
250
|
+
text = text.force_encoding( encoding )
|
|
251
|
+
|
|
252
|
+
## track/check code range if valid/broken
|
|
253
|
+
## note - check BEFORE conversion to utf-8 - why? why not?
|
|
254
|
+
@_text_encoding_valid = text.valid_encoding?
|
|
255
|
+
|
|
256
|
+
## note be more tolerant when converting - use replace for now - why? why not?
|
|
257
|
+
## maybe add a strict (no replace) version later
|
|
258
|
+
text = text.encode(
|
|
259
|
+
Encoding::UTF_8,
|
|
260
|
+
invalid: :replace,
|
|
261
|
+
undef: :replace,
|
|
262
|
+
replace: "�"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
errors = text.scan( "�" )
|
|
266
|
+
if errors.size > 0
|
|
267
|
+
puts " [debug] !!! WARN - #{errors.size} invalid/undef character encoding error(s) replaced w/ �"
|
|
268
|
+
@_text_utf8_replace = errors.size
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Normalize unicode (utf-8) string to Composed (NFC)
|
|
274
|
+
# NFC (Normalization Form Canonical Composition)
|
|
275
|
+
|
|
276
|
+
=begin
|
|
277
|
+
use nfkc ??
|
|
278
|
+
or delegate to userland??
|
|
279
|
+
|
|
280
|
+
Pro-Tip: Watch out for Ligatures and Compatibility Issues
|
|
281
|
+
While NFC handles standard accents beautifully,
|
|
282
|
+
you might occasionally want NFKC (Normalization Form Compatibility Composition)
|
|
283
|
+
instead.
|
|
284
|
+
pages sometimes contain legacy typographical quirks like:
|
|
285
|
+
Ligatures: The characters fi or fl typed as a single glyph.
|
|
286
|
+
Roman Numerals / Fractions: Characters like Ⅳ or ½.
|
|
287
|
+
|
|
288
|
+
If you use standard NFC, those symbols remain as complex single characters.
|
|
289
|
+
If you use NFKC, Ruby will break them down into standard,
|
|
290
|
+
easily searchable text (fi becomes fi, Ⅳ becomes IV, and ½ becomes 1/2).
|
|
291
|
+
=end
|
|
292
|
+
|
|
293
|
+
###
|
|
294
|
+
## todo/check - add nfc: true|false
|
|
295
|
+
## to text() as option (if unicode - utf8) - why? why not?
|
|
296
|
+
## or text_unicode( nfc: true|false )
|
|
297
|
+
|
|
298
|
+
## comment out for now - get
|
|
299
|
+
## unicode_normalize/normalize.rb:126:in `gsub': invalid byte sequence in UTF-8
|
|
300
|
+
text = text.unicode_normalize(:nfc)
|
|
301
|
+
|
|
302
|
+
text
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
end # class Response
|
|
306
|
+
end # class Webclient
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
class Webclient
|
|
3
|
+
# wrap Net::HTTP::Response or
|
|
4
|
+
# maybe HTTPX or such in the future
|
|
5
|
+
|
|
6
|
+
## check - rename to HttpResponse?
|
|
7
|
+
## and use HttpErrorResponse or such - why? why not?
|
|
8
|
+
class Response
|
|
9
|
+
def initialize( response )
|
|
10
|
+
@response = response
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
## todo - find a better name for underlying object - instead of raw use ??
|
|
14
|
+
## note - raw used by python requests too
|
|
15
|
+
## use for streaming and such - why? why not?
|
|
16
|
+
def raw() @response; end
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
###############
|
|
20
|
+
## response status methods
|
|
21
|
+
|
|
22
|
+
def status
|
|
23
|
+
@status ||= Status.new( @response.code, message: @response.message )
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
## add "flat" shortcuts - keep - why? why not?
|
|
27
|
+
def status_code() status.to_i; end
|
|
28
|
+
def ok?() status.ok?; end
|
|
29
|
+
def nok?() status.nok?; end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
###
|
|
34
|
+
## keep http_version on Response - why? why not?
|
|
35
|
+
## only really 1.0 and 1.1
|
|
36
|
+
## check if value is a string?
|
|
37
|
+
def version() @response.http_version; end
|
|
38
|
+
alias_method :http_version, :version ## add/keep longer alias too - why? why not?
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
###
|
|
45
|
+
# note - add a writeable encoding_user attribute
|
|
46
|
+
## on default (if not set by user) returns nil
|
|
47
|
+
def _encoding_user=( value ) @_encoding_user = value; end
|
|
48
|
+
def _encoding_user() defined?( @_encoding_user ) ? @_encoding_user : nil; end
|
|
49
|
+
|
|
50
|
+
## cache (returned) decoded text - why? why not?
|
|
51
|
+
def text( encoding: _encoding_user )
|
|
52
|
+
@text ||= _decode_text( encoding: encoding )
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
## convenience helper; returns parsed json data; note: always assume utf-8 (text) encoding
|
|
56
|
+
## cache returned (parsed) json value - why? why not?
|
|
57
|
+
## add :symbolize_keys option - why? why not?
|
|
58
|
+
def json
|
|
59
|
+
@json ||= JSON.parse( text )
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## always use t raw binary data
|
|
65
|
+
## and always use @response.body.b
|
|
66
|
+
## or body.b (binary ascii-7bit) string/buffer here !!!!
|
|
67
|
+
##
|
|
68
|
+
|
|
69
|
+
def body() @response.body.b; end
|
|
70
|
+
alias_method :blob, :body
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
################
|
|
75
|
+
# nested class Response::Headers
|
|
76
|
+
class Headers
|
|
77
|
+
def initialize( response )
|
|
78
|
+
@response = response
|
|
79
|
+
end
|
|
80
|
+
def each( &blk )
|
|
81
|
+
@response.each_header do |key, value| # iterate all response headers
|
|
82
|
+
blk.call( key, value )
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end # nested class Response::Headers
|
|
86
|
+
|
|
87
|
+
def headers
|
|
88
|
+
@headers ||= Headers.new( @response )
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## add some predefined/built-in header(s) convenience shortcuts
|
|
95
|
+
## check: change to headers['content-type'] or such - why? why not?
|
|
96
|
+
def content_type() @response.content_type; end
|
|
97
|
+
def content_length() @response.content_length; end
|
|
98
|
+
|
|
99
|
+
###
|
|
100
|
+
## note - content_type might return nil, thus, use to_s (gets converted to "")
|
|
101
|
+
def image_jpg?() content_type.to_s.match?( %r{image/jpeg}i ); end
|
|
102
|
+
def image_png?() content_type.to_s.match?( %r{image/png}i ); end
|
|
103
|
+
def image_gif?() content_type.to_s.match?( %r{image/gif}i ); end
|
|
104
|
+
|
|
105
|
+
alias_method :image_jpeg?, :image_jpg?
|
|
106
|
+
alias_method :jpeg?, :image_jpg?
|
|
107
|
+
alias_method :jpg?, :image_jpg?
|
|
108
|
+
alias_method :png?, :image_png?
|
|
109
|
+
alias_method :gif?, :image_gif?
|
|
110
|
+
|
|
111
|
+
end ## class Response
|
|
112
|
+
end # class Webclient
|
data/lib/webclient.rb
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require '
|
|
7
|
-
require '
|
|
8
|
-
require '
|
|
9
|
-
|
|
10
|
-
require '
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
require '
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
###
|
|
2
|
+
## note - requires basicially a mirror/clone of cococs "prelude/prolog"
|
|
3
|
+
## see <https://github.com/rubycocos/cocos/blob/master/lib/cocos.rb>
|
|
4
|
+
|
|
5
|
+
require 'pp'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'date'
|
|
8
|
+
require 'fileutils'
|
|
9
|
+
require 'pathname' ### auto-add for use of relative_path construction
|
|
10
|
+
require 'base64' ### ## e.g. Base64.decode64,Base64.encode64,...
|
|
11
|
+
|
|
12
|
+
require 'uri'
|
|
13
|
+
require 'net/http'
|
|
14
|
+
require 'net/https'
|
|
15
|
+
require 'cgi' ## auto-add for use of params encoding
|
|
16
|
+
|
|
17
|
+
require 'json'
|
|
18
|
+
require 'yaml'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# our own code
|
|
23
|
+
require_relative 'webclient/version' # note: let version always go first
|
|
24
|
+
|
|
25
|
+
require_relative 'webclient/webclient-get'
|
|
26
|
+
require_relative 'webclient/webclient-post'
|
|
27
|
+
require_relative 'webclient/webclient_response'
|
|
28
|
+
require_relative 'webclient/webclient_response-status'
|
|
29
|
+
require_relative 'webclient/webclient_response-text'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
############
|
|
33
|
+
## add convenience alias for camel case / alternate different spelling
|
|
34
|
+
WebClient = Webclient
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# say hello
|
|
38
|
+
puts Webclient.banner ## if defined?( $RUBYLIBS_DEBUG )
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: webclient
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gerald Bauer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rdoc
|
|
@@ -36,17 +36,17 @@ dependencies:
|
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '4.2'
|
|
40
40
|
type: :development
|
|
41
41
|
prerelease: false
|
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
46
|
+
version: '4.2'
|
|
47
47
|
description: webclient gem - yet (another) universal network client interface for
|
|
48
48
|
world wide web (www) requests via HTTP
|
|
49
|
-
email:
|
|
49
|
+
email: gerald.bauer@gmail.com
|
|
50
50
|
executables: []
|
|
51
51
|
extensions: []
|
|
52
52
|
extra_rdoc_files:
|
|
@@ -60,8 +60,12 @@ files:
|
|
|
60
60
|
- Rakefile
|
|
61
61
|
- lib/webclient.rb
|
|
62
62
|
- lib/webclient/version.rb
|
|
63
|
-
- lib/webclient/webclient.rb
|
|
64
|
-
|
|
63
|
+
- lib/webclient/webclient-get.rb
|
|
64
|
+
- lib/webclient/webclient-post.rb
|
|
65
|
+
- lib/webclient/webclient_response-status.rb
|
|
66
|
+
- lib/webclient/webclient_response-text.rb
|
|
67
|
+
- lib/webclient/webclient_response.rb
|
|
68
|
+
homepage: https://github.com/rubycocos/webclient
|
|
65
69
|
licenses:
|
|
66
70
|
- Public Domain
|
|
67
71
|
metadata: {}
|
|
@@ -82,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
82
86
|
- !ruby/object:Gem::Version
|
|
83
87
|
version: '0'
|
|
84
88
|
requirements: []
|
|
85
|
-
rubygems_version: 3.
|
|
89
|
+
rubygems_version: 3.5.22
|
|
86
90
|
signing_key:
|
|
87
91
|
specification_version: 4
|
|
88
92
|
summary: webclient gem - yet (another) universal network client interface for world
|
data/lib/webclient/webclient.rb
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
class Webclient
|
|
3
|
-
|
|
4
|
-
class Response # nested class - wrap Net::HTTP::Response
|
|
5
|
-
def initialize( response )
|
|
6
|
-
@response = response
|
|
7
|
-
end
|
|
8
|
-
def raw() @response; end
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## todo/check: rename encoding to html/http-like charset - why? why not?
|
|
12
|
-
def text( encoding: 'UTF-8' )
|
|
13
|
-
# note: Net::HTTP will NOT set encoding UTF-8 etc.
|
|
14
|
-
# will be set to ASCII-8BIT == BINARY == Encoding Unknown; Raw Bytes Here
|
|
15
|
-
# thus, set/force encoding to utf-8
|
|
16
|
-
text = @response.body.to_s
|
|
17
|
-
if encoding.downcase == 'utf-8'
|
|
18
|
-
text = text.force_encoding( Encoding::UTF_8 )
|
|
19
|
-
else
|
|
20
|
-
## [debug] GET=http://www.football-data.co.uk/mmz4281/0405/SC0.csv
|
|
21
|
-
## Encoding::UndefinedConversionError: "\xA0" from ASCII-8BIT to UTF-8
|
|
22
|
-
## note: 0xA0 (160) is NBSP (non-breaking space) in Windows-1252
|
|
23
|
-
|
|
24
|
-
## note: assume windows encoding (for football-data.uk)
|
|
25
|
-
## use "Windows-1252" for input and convert to utf-8
|
|
26
|
-
##
|
|
27
|
-
## see https://www.justinweiss.com/articles/3-steps-to-fix-encoding-problems-in-ruby/
|
|
28
|
-
## see https://en.wikipedia.org/wiki/Windows-1252
|
|
29
|
-
## txt = txt.force_encoding( 'Windows-1252' )
|
|
30
|
-
## txt = txt.encode( 'UTF-8' )
|
|
31
|
-
## Encoding::UTF_8 => 'UTF-8'
|
|
32
|
-
puts " [debug] converting response.text encoding from >#{encoding}< to >UTF-8<"
|
|
33
|
-
|
|
34
|
-
text = text.force_encoding( encoding )
|
|
35
|
-
text = text.encode( Encoding::UTF_8 )
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
text
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
## convenience helper; returns parsed json data; note: always assume utf-8 (text) encoding
|
|
42
|
-
def json() JSON.parse( text ); end
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class Headers # nested (nested) class
|
|
47
|
-
def initialize( response )
|
|
48
|
-
@response = response
|
|
49
|
-
end
|
|
50
|
-
def each( &blk )
|
|
51
|
-
@response.each_header do |key, value| # Iterate all response headers
|
|
52
|
-
blk.call( key, value )
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
def headers() @headers ||= Headers.new( @response ); end
|
|
57
|
-
|
|
58
|
-
class Status # nested (nested) class
|
|
59
|
-
def initialize( response )
|
|
60
|
-
@response = response
|
|
61
|
-
end
|
|
62
|
-
def code() @response.code.to_i; end
|
|
63
|
-
def ok?() code == 200; end
|
|
64
|
-
def nok?() code != 200; end
|
|
65
|
-
def message() @response.message; end
|
|
66
|
-
end
|
|
67
|
-
def status() @status ||= Status.new( @response ); end
|
|
68
|
-
end # (nested) class Response
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def self.get( url, headers: {}, auth: [] )
|
|
72
|
-
|
|
73
|
-
uri = URI.parse( url )
|
|
74
|
-
http = Net::HTTP.new( uri.host, uri.port )
|
|
75
|
-
|
|
76
|
-
if uri.instance_of? URI::HTTPS
|
|
77
|
-
http.use_ssl = true
|
|
78
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
request = Net::HTTP::Get.new( uri.request_uri )
|
|
82
|
-
|
|
83
|
-
### add (custom) headers if any
|
|
84
|
-
## check/todo: is there are more idiomatic way for Net::HTTP ???
|
|
85
|
-
## use
|
|
86
|
-
## request = Net::HTTP::Get.new( uri.request_uri, headers )
|
|
87
|
-
## why? why not?
|
|
88
|
-
## instead of e.g.
|
|
89
|
-
## request['X-Auth-Token'] = 'xxxxxxx'
|
|
90
|
-
## request['User-Agent'] = 'ruby'
|
|
91
|
-
## request['Accept'] = '*/*'
|
|
92
|
-
if headers && headers.size > 0
|
|
93
|
-
headers.each do |key,value|
|
|
94
|
-
request[ key ] = value
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if auth.size == 2 ## e.g. ['user', 'password']
|
|
100
|
-
## always assume basic auth for now
|
|
101
|
-
## auth[0] => user
|
|
102
|
-
## auth[1] => password
|
|
103
|
-
request.basic_auth( auth[0], auth[1] )
|
|
104
|
-
puts " using basic auth - user: #{auth[0]}, password: ***"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
puts "GET #{uri}..."
|
|
109
|
-
|
|
110
|
-
response = http.request( request )
|
|
111
|
-
|
|
112
|
-
## note: return "unified" wrapped response
|
|
113
|
-
Response.new( response )
|
|
114
|
-
end # method self.get
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def self.post( url, headers: {},
|
|
118
|
-
body: nil,
|
|
119
|
-
json: nil ## json - convenience shortcut (for body & encoding)
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
uri = URI.parse( url )
|
|
123
|
-
http = Net::HTTP.new( uri.host, uri.port )
|
|
124
|
-
|
|
125
|
-
if uri.instance_of? URI::HTTPS
|
|
126
|
-
http.use_ssl = true
|
|
127
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
request = Net::HTTP::Post.new( uri.request_uri )
|
|
131
|
-
|
|
132
|
-
### add (custom) headers if any
|
|
133
|
-
## check/todo: is there are more idiomatic way for Net::HTTP ???
|
|
134
|
-
## use
|
|
135
|
-
## request = Net::HTTP::Get.new( uri.request_uri, headers )
|
|
136
|
-
## why? why not?
|
|
137
|
-
## instead of e.g.
|
|
138
|
-
## request['X-Auth-Token'] = 'xxxxxxx'
|
|
139
|
-
## request['User-Agent'] = 'ruby'
|
|
140
|
-
## request['Accept'] = '*/*'
|
|
141
|
-
if headers && headers.size > 0
|
|
142
|
-
headers.each do |key,value|
|
|
143
|
-
request[ key ] = value
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
if body
|
|
148
|
-
request.body = body.to_s
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
if json
|
|
152
|
-
# note: the body needs to be a JSON string - use pretty generate and NOT "compact" style - why? why not?
|
|
153
|
-
request.body = JSON.pretty_generate( json )
|
|
154
|
-
|
|
155
|
-
## move (auto-set) header content-type up (before custom headers) - why? why not?
|
|
156
|
-
request['Content-Type'] = 'application/json'
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
puts "POST #{uri}..."
|
|
161
|
-
|
|
162
|
-
response = http.request( request )
|
|
163
|
-
|
|
164
|
-
## note: return "unified" wrapped response
|
|
165
|
-
Response.new( response )
|
|
166
|
-
end # method self.post
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
end # class Webclient
|
|
170
|
-
|