webclient 0.2.2 → 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: 8db40d5a48638657362e50374418f3f911079afbdf9852a6e99038a6dd46f549
4
- data.tar.gz: 334f393fc27e197a685200c5f9a85f34c7940ac7c37bce7a80dcef971d642daf
3
+ metadata.gz: 177c9b330862eac8b588d0bffe1d4819ffe20b5f03f13e8617291e1c890bc979
4
+ data.tar.gz: c0819b68fa611fa4aa38f088816984ae6f1d444f710a425135ead32d60f71f9f
5
5
  SHA512:
6
- metadata.gz: 81d41206af0b7c517318f908f4ce7482bb11849470ef1631c949a6479509d177ac2027c79f7cf18eb7b007cb0150f45d73e267f270b7e0a717482f002c4076fa
7
- data.tar.gz: 2312bfe78c5c621a5b58e4498c9f5c0adbc2ecca929bbf73ccaf0f3c79f07d5ec823f234536bcfd22773506dfd1b891a29ac202081a05551b39b4b3f4ac7aabe
6
+ metadata.gz: 7b3fdc8068632f7184fd77f6f7a34e2126ca54d7b7a09d4d1d5f92686dcfb16dc6af0c0a6470f188af82f9274388cc6aad0774c97a4e1aa312508976a328d24e
7
+ data.tar.gz: 74ea0e68e127db132d859f32806dc1f07fa86aa1b08beeeb949fc37aa53a405473825481dbe9bc2ffea1b5ea0d9d16555022f4ab956a9fc9df457d9d35da3401
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
+ ### 0.3.1
1
2
  ### 0.0.1 / 2020-10-03
2
3
 
3
4
  * Everything is new. First release.
4
-
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,13 +1,12 @@
1
1
  # webclient
2
2
 
3
- webclient gem - yet (another) universal network client interface for world wide web (www) requests via HTTP
3
+ webclient gem - yet (another) universal network client interface for world wide web (www) requests via HTTP
4
4
 
5
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)
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
8
  * gem :: [rubygems.org/gems/webclient](https://rubygems.org/gems/webclient)
9
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
10
 
12
11
 
13
12
 
@@ -16,7 +15,9 @@ webclient gem - yet (another) universal network client interface for world wide
16
15
  TBD
17
16
 
18
17
 
18
+
19
+
19
20
  ## License
20
21
 
21
22
  The `webclient` scripts are dedicated to the public domain.
22
- Use it as you please with no restrictions whatsoever.
23
+ Use as you please with no restrictions whatsoever.
data/Rakefile CHANGED
@@ -8,10 +8,10 @@ Hoe.spec 'webclient' do
8
8
  self.summary = 'webclient gem - yet (another) universal network client interface for world wide web (www) requests via HTTP'
9
9
  self.description = summary
10
10
 
11
- self.urls = { home: 'https://github.com/rubycoco/webclient' }
11
+ self.urls = { home: 'https://github.com/rubycocos/webclient' }
12
12
 
13
13
  self.author = 'Gerald Bauer'
14
- self.email = 'ruby-talk@ruby-lang.org'
14
+ self.email = 'gerald.bauer@gmail.com'
15
15
 
16
16
  # switch extension to .markdown for gihub formatting
17
17
  self.readme_file = 'README.md'
@@ -1,8 +1,8 @@
1
1
 
2
2
  class Webclient
3
3
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
4
- MINOR = 2
5
- PATCH = 2
4
+ MINOR = 3
5
+ PATCH = 1
6
6
  VERSION = [MAJOR,MINOR,PATCH].join('.')
7
7
 
8
8
  def self.version
@@ -17,4 +17,3 @@ class Webclient
17
17
  File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
18
18
  end
19
19
  end # module Webclient
20
-
@@ -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,19 +1,32 @@
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
+
1
5
  require 'pp'
2
6
  require 'time'
3
7
  require 'date'
4
8
  require 'fileutils'
9
+ require 'pathname' ### auto-add for use of relative_path construction
10
+ require 'base64' ### ## e.g. Base64.decode64,Base64.encode64,...
5
11
 
6
12
  require 'uri'
7
13
  require 'net/http'
8
14
  require 'net/https'
15
+ require 'cgi' ## auto-add for use of params encoding
9
16
 
10
17
  require 'json'
11
18
  require 'yaml'
12
19
 
13
20
 
21
+
14
22
  # our own code
15
- require 'webclient/version' # note: let version always go first
16
- require 'webclient/webclient'
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'
17
30
 
18
31
 
19
32
  ############
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.2
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: 2022-04-28 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.23'
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.23'
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.3.7
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,202 +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
- def body() @response.body.to_s; end
46
- alias_method :blob, :body
47
-
48
-
49
-
50
- class Headers # nested (nested) class
51
- def initialize( response )
52
- @response = response
53
- end
54
- def each( &blk )
55
- @response.each_header do |key, value| # Iterate all response headers
56
- blk.call( key, value )
57
- end
58
- end
59
- end
60
- def headers() @headers ||= Headers.new( @response ); end
61
-
62
-
63
- ## add some predefined/built-in header(s) convenience shortcuts
64
- def content_type
65
- ## check: change to headers['content-type'] or such - why? why not?
66
- @response.content_type
67
- end
68
- def content_length
69
- @response.content_length
70
- end
71
-
72
- def image_jpg?
73
- content_type =~ %r{image/jpeg}i
74
- end
75
- def image_png?
76
- content_type =~ %r{image/png}i
77
- end
78
- def image_gif?
79
- content_type =~ %r{image/gif}i
80
- end
81
-
82
- alias_method :image_jpeg?, :image_jpg?
83
- alias_method :jpeg?, :image_jpg?
84
- alias_method :jpg?, :image_jpg?
85
- alias_method :png?, :image_png?
86
- alias_method :gif?, :image_gif?
87
-
88
-
89
-
90
- class Status # nested (nested) class
91
- def initialize( response )
92
- @response = response
93
- end
94
- def code() @response.code.to_i; end
95
- def ok?() code == 200; end
96
- def nok?() code != 200; end
97
- def message() @response.message; end
98
- end
99
- def status() @status ||= Status.new( @response ); end
100
- end # (nested) class Response
101
-
102
-
103
- def self.get( url, headers: {}, auth: [] )
104
-
105
- uri = URI.parse( url )
106
- http = Net::HTTP.new( uri.host, uri.port )
107
-
108
- if uri.instance_of? URI::HTTPS
109
- http.use_ssl = true
110
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
111
- end
112
-
113
- request = Net::HTTP::Get.new( uri.request_uri )
114
-
115
- ### add (custom) headers if any
116
- ## check/todo: is there are more idiomatic way for Net::HTTP ???
117
- ## use
118
- ## request = Net::HTTP::Get.new( uri.request_uri, headers )
119
- ## why? why not?
120
- ## instead of e.g.
121
- ## request['X-Auth-Token'] = 'xxxxxxx'
122
- ## request['User-Agent'] = 'ruby'
123
- ## request['Accept'] = '*/*'
124
- if headers && headers.size > 0
125
- headers.each do |key,value|
126
- request[ key ] = value
127
- end
128
- end
129
-
130
-
131
- if auth.size == 2 ## e.g. ['user', 'password']
132
- ## always assume basic auth for now
133
- ## auth[0] => user
134
- ## auth[1] => password
135
- request.basic_auth( auth[0], auth[1] )
136
- puts " using basic auth - user: #{auth[0]}, password: ***"
137
- end
138
-
139
-
140
- puts "GET #{uri}..."
141
-
142
- response = http.request( request )
143
-
144
- ## note: return "unified" wrapped response
145
- Response.new( response )
146
- end # method self.get
147
-
148
-
149
- def self.post( url, headers: {},
150
- body: nil,
151
- json: nil ## json - convenience shortcut (for body & encoding)
152
- )
153
-
154
- uri = URI.parse( url )
155
- http = Net::HTTP.new( uri.host, uri.port )
156
-
157
- if uri.instance_of? URI::HTTPS
158
- http.use_ssl = true
159
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
160
- end
161
-
162
- request = Net::HTTP::Post.new( uri.request_uri )
163
-
164
- ### add (custom) headers if any
165
- ## check/todo: is there are more idiomatic way for Net::HTTP ???
166
- ## use
167
- ## request = Net::HTTP::Get.new( uri.request_uri, headers )
168
- ## why? why not?
169
- ## instead of e.g.
170
- ## request['X-Auth-Token'] = 'xxxxxxx'
171
- ## request['User-Agent'] = 'ruby'
172
- ## request['Accept'] = '*/*'
173
- if headers && headers.size > 0
174
- headers.each do |key,value|
175
- request[ key ] = value
176
- end
177
- end
178
-
179
- if body
180
- request.body = body.to_s
181
- end
182
-
183
- if json
184
- # note: the body needs to be a JSON string - use pretty generate and NOT "compact" style - why? why not?
185
- request.body = JSON.pretty_generate( json )
186
-
187
- ## move (auto-set) header content-type up (before custom headers) - why? why not?
188
- request['Content-Type'] = 'application/json'
189
- end
190
-
191
-
192
- puts "POST #{uri}..."
193
-
194
- response = http.request( request )
195
-
196
- ## note: return "unified" wrapped response
197
- Response.new( response )
198
- end # method self.post
199
-
200
-
201
- end # class Webclient
202
-