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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19d01c484b19fd633a6e73b3710d38dad056bd066e7c2dc9c613ddd1eae4c28f
4
- data.tar.gz: 7140effe816c143e7ff9e336dadd9b1bd7b424ee6ca0c1b4ff75a12021fe0979
3
+ metadata.gz: 177c9b330862eac8b588d0bffe1d4819ffe20b5f03f13e8617291e1c890bc979
4
+ data.tar.gz: c0819b68fa611fa4aa38f088816984ae6f1d444f710a425135ead32d60f71f9f
5
5
  SHA512:
6
- metadata.gz: aa8feda707d1e1c79bcaf9205e768465e21d2c39b8cba9143387816bd17d5df34f7e871d43ece9a3780a1a3577b701017633823e5e695635eb73d4f8ec954f0d
7
- data.tar.gz: af08cb829652c2ba30600dd7163bba31dd9bc1e5bdbfc8d5f9d289ac285fcde5f878de541f4523e07906cdbb41bb25ebca632bae28df4113e741aa4b9e160512
6
+ metadata.gz: 7b3fdc8068632f7184fd77f6f7a34e2126ca54d7b7a09d4d1d5f92686dcfb16dc6af0c0a6470f188af82f9274388cc6aad0774c97a4e1aa312508976a328d24e
7
+ data.tar.gz: 74ea0e68e127db132d859f32806dc1f07fa86aa1b08beeeb949fc37aa53a405473825481dbe9bc2ffea1b5ea0d9d16555022f4ab956a9fc9df457d9d35da3401
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 0.0.1 / 2020-10-03
2
-
3
- * Everything is new. First release.
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) universal network client interface for world wide web (www) requests via HTTP
4
-
5
-
6
- * home :: [github.com/rubycoco/webclient](https://github.com/rubycoco/webclient)
7
- * bugs :: [github.com/rubycoco/webclient/issues](https://github.com/rubycoco/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
- * forum :: [groups.google.com/group/wwwmake](https://groups.google.com/group/wwwmake)
11
-
12
-
13
-
14
- ## Usage
15
-
16
- TBD
17
-
18
-
19
- ## License
20
-
21
- The `webclient` scripts are dedicated to the public domain.
22
- Use it as you please with no restrictions whatsoever.
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/rubycoco/webclient' }
12
-
13
- self.author = 'Gerald Bauer'
14
- self.email = 'ruby-talk@ruby-lang.org'
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
@@ -1,20 +1,19 @@
1
-
2
- class Webclient
3
- MAJOR = 0 ## todo: namespace inside version or something - why? why not??
4
- MINOR = 2
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
- require 'pp'
2
- require 'time'
3
- require 'date'
4
- require 'fileutils'
5
-
6
- require 'uri'
7
- require 'net/http'
8
- require 'net/https'
9
-
10
- require 'json'
11
- require 'yaml'
12
-
13
-
14
- # our own code
15
- require 'webclient/version' # note: let version always go first
16
- require 'webclient/webclient'
17
-
18
-
19
- ############
20
- ## add convenience alias for camel case / alternate different spelling
21
- WebClient = Webclient
22
-
23
-
24
- # say hello
25
- puts Webclient.banner ## if defined?( $RUBYLIBS_DEBUG )
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.2.1
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: 2021-04-05 00:00:00.000000000 Z
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: '3.22'
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: '3.22'
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: ruby-talk@ruby-lang.org
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
- homepage: https://github.com/rubycoco/webclient
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.1.4
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
@@ -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
-