util 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ module Util
2
+ # A helper to safely dowload a file. Can be dowloaded to an actual file,
3
+ # or directly parsed by another gem (e. g. +Nokogiri+).
4
+ # @example
5
+ # require 'oga' # Must be required by the user, `Util` will not do it
6
+ # url = 'https://www.perdu.com/'
7
+ # html = Util::Downloader.new(url).set_dest(Oga)
8
+ # html.download # => Util::Result.ok
9
+ # html.data.at_css('h1').text # 'Perdu sur l\'Internet ?'
10
+ #
11
+ # url = 'https://gitlab.com/uploads/-/system/user/avatar/5582173/avatar.png'
12
+ # Util::Downloader.new(url).set_name('guillel.png').set_force.download
13
+ class Downloader
14
+ attr_reader :data
15
+ attr_reader :dest
16
+ attr_reader :force
17
+ attr_reader :name
18
+ attr_reader :ref
19
+ attr_reader :url
20
+
21
+ # @no_doc
22
+ ALLOWED = [:data, :force, :url]
23
+ # @no_doc
24
+ DEFAULT_OPTIONS = {
25
+ :dest => '.',
26
+ :name => '',
27
+ :ref => '',
28
+ }
29
+
30
+ # Create a new helper to safely download a file.
31
+ # @param url [String] URL of the page to download
32
+ # @param opts [#to_h] download options
33
+ # @option opts [String, Class] :dest the directory in which to download,
34
+ # or the class with which to parse it
35
+ # @option opts [Boolean] :force whether to replace an existing file
36
+ # @option opts [String] :name name under which to save the file
37
+ # @option opts [String] :ref referer to use while downloading
38
+ # @return [self]
39
+ def initialize url, opts={}
40
+ require 'util/args'
41
+ @url, opts = Util::Args.check url, String, '', opts, Hash, {}
42
+ opts[:force] === true ? set_force : unset_force
43
+
44
+ DEFAULT_OPTIONS.each_pair do |k, v|
45
+ self.method('set_' + k.to_s).call (opts[k].nil? ? v : opts[k])
46
+ end
47
+ end
48
+
49
+ # Actually download the file, according to the given options.
50
+ # @return [Util::Result]
51
+ def download
52
+ require 'open-uri'
53
+ require 'util/result'
54
+
55
+ return Util::Result.err :no_url, self if @url.empty?
56
+ @name = File.basename URI(@url).path if @name.empty?
57
+ @name = 'index.html' if @name.empty? or @name == '/'
58
+
59
+ if @dest.is_a?(String) then
60
+ return Util::Result.err :no_dir, self unless File.directory? @dest
61
+ return Util::Result.err :file_exists, self if not @force \
62
+ and File.exists?(File.join @dest, @name)
63
+ end
64
+
65
+ begin
66
+ io = @ref.empty? ? URI.open(@url) : URI.open(@url, 'Referer' => @ref)
67
+ case @dest.to_s
68
+ when 'Magick' then @data = Magick::Image.from_blob io.read
69
+ when 'Nokogiri' then @data = Nokogiri::HTML io
70
+ when 'Nokogiri::HTML' then @data = Nokogiri::HTML io
71
+ when 'Nokogiri::XML' then @data = Nokogiri::XML io
72
+ when 'Oga' then @data = Oga.parse_html io
73
+ when 'Oga::HTML' then @data = Oga.parse_html io
74
+ when 'Oga::XML' then @data = Oga.parse_xml io
75
+ when 'REXML' then @data = REXML::Document.new io
76
+ else
77
+ if @dest.respond_to? :parse then
78
+ @data = @dest.parse io
79
+ elsif @dest.respond_to? :read then
80
+ @data = @dest.read io
81
+ else
82
+ IO.copy_stream(io, File.join(@dest, @name))
83
+ end
84
+ end
85
+ rescue Exception => e
86
+ return Util::Result.err e, self
87
+ end
88
+
89
+ Util::Result.ok
90
+ end
91
+
92
+ # Return the value of one of the options, the URL, or the parsed web page.
93
+ # @param key [#to_sym] the attribute to read
94
+ # @return [Object] if succesfull
95
+ # @return [nil] if +key+ cannot be converted to a symbol, or is not part
96
+ # of the allowed attributes to read
97
+ def [] key
98
+ require 'util/args'
99
+ key = Util::Args.check key, Symbol, nil
100
+ return nil if key.nil?
101
+ return nil unless (DEFAULT_OPTIONS.keys + ALLOWED).include? key
102
+ instance_variable_get('@' + key.to_s)
103
+ end
104
+
105
+ # Set the directory in which to download the file, or the class with
106
+ # which to parse it. Currently accepted class are the following.
107
+ # - +Magick+ (will try to parse a +Magick::Image+).
108
+ # - +Nokogiri::HTML+ or its alias +Nokogiri+.
109
+ # - +Nokogiri::XML+.
110
+ # - +Oga::HTML+ or its alias +Oga+.
111
+ # - +Oga::XML+.
112
+ # - +REXML+.
113
+ # - Any class that responds to +parse+.
114
+ # - Any class that responds to +read+.
115
+ # Please note that with the last two, the methods are tried in this
116
+ # order, and the result might not be what would be expected, especially
117
+ # if the given method does not accept an +IO+ object.
118
+ # @param dir [Module] path to the directory, or if the file shall not
119
+ # be saved, the class that shall parse it
120
+ # @return [self]
121
+ def set_dest dir=DEFAULT_OPTIONS[:dest]
122
+ @dest = dir.is_a?(Module) ? dir : dir.to_s
123
+ self
124
+ end
125
+
126
+ # Set {download} to replace the destination file if it already exists.
127
+ # @return [self]
128
+ def set_force
129
+ @force = true
130
+ self
131
+ end
132
+
133
+ # Set {download} to not replace the destination file if it already exists.
134
+ # @return [self]
135
+ def unset_force
136
+ @force = false
137
+ self
138
+ end
139
+
140
+ # Set the name under which to save the file. If the given name is
141
+ # empty, defaults to the same name as in the remote location.
142
+ # @param n [String] the file name
143
+ # @return [self]
144
+ def set_name n=DEFAULT_OPTIONS[:name]
145
+ @name = n.to_s
146
+ self
147
+ end
148
+
149
+ # Set the referer to use while downloading.
150
+ # @param r [String] the referer
151
+ # @return [self]
152
+ def set_ref r=DEFAULT_OPTIONS[:ref]
153
+ @ref = r.to_s
154
+ self
155
+ end
156
+ end
157
+ end
@@ -53,10 +53,10 @@ module Util
53
53
  return ''
54
54
  end
55
55
 
56
- require 'util/arg'
57
- id, opts = Arg.check_a [[id, String, ''], [opts, Hash, {}]]
58
- o_lang, o_mod = Arg.check_h opts, [[:lang, Symbol, @default_lang],
59
- [:mod, String, @last_used_mod]]
56
+ require 'util/args'
57
+ id = Util::Args.check id, String, ''
58
+ o_lang, o_mod = Util::Opts.check opts, :lang, Symbol, @default_lang, \
59
+ :mod, String, @last_used_mod
60
60
 
61
61
  mod = o_mod.empty? ? @last_used_mod : o_mod
62
62
  mod = @messages.keys[0] if mod.empty?
@@ -194,12 +194,11 @@ module Util
194
194
  # @return [String, String, Boolean, Boolean] success
195
195
  # @return [false] failure
196
196
  def self.register_check_args o_name, path, relative, recursive
197
- require 'util/arg'
197
+ require 'util/args'
198
198
  init unless initialized?
199
- name, path, relative, recursive = Arg.check_a [
200
- [o_name, String, ''], [path, String, ''],
201
- [relative, FalseClass, true], [recursive, TrueClass, false]
202
- ]
199
+ name, path, relative, recursive = Util::Args.check \
200
+ o_name, String, '', path, String, '', \
201
+ relative, FalseClass, true, recursive, TrueClass, false
203
202
  return false if check_t name.empty?, :reg_no_name, o_name
204
203
 
205
204
  path = relative ? './temp.rb' : '.' if path.empty?
@@ -79,8 +79,8 @@ module Util
79
79
  # @return [:dep, :priv, :val, nil]
80
80
  def self.category code
81
81
  init if @complete.nil?
82
- require 'util/arg'
83
- code = Arg.check code, Symbol, false, true
82
+ require 'util/args'
83
+ code = Util::Args.check code, Symbol, false
84
84
  return nil unless code
85
85
 
86
86
  info = @complete[code]
@@ -129,8 +129,8 @@ module Util
129
129
  # @return [Symbol]
130
130
  def self.from1 code
131
131
  init if @complete.nil?
132
- require 'util/arg'
133
- code = Arg.check code, Symbol, false, true
132
+ require 'util/args'
133
+ code = Util::Args.check code, Symbol, false
134
134
  @from1[code]
135
135
  end
136
136
 
@@ -139,8 +139,8 @@ module Util
139
139
  # @return [Symbol]
140
140
  def self.from2 code
141
141
  init if @complete.nil?
142
- require 'util/arg'
143
- code = Arg.check code, Symbol, false, true
142
+ require 'util/args'
143
+ code = Util::Args.check code, Symbol, false
144
144
  @from2[code]
145
145
  end
146
146
  end
@@ -0,0 +1,175 @@
1
+ module Util
2
+ # An object representing the result of a computation, whether if be a success
3
+ # or a failure. Similar to +Result+ in Rust and OCaml and +Maybe+ in
4
+ # Haskell.
5
+ # @example
6
+ # def divide num, denom
7
+ # return Util::Result.err 'Division by zero.', self if denom == 0
8
+ # Util::Result.ok num / denom
9
+ # end
10
+ # No exception is ever raised, except when the value is retrieved
11
+ class Result
12
+ private_class_method :new
13
+ # The function in which the error happened, if defined.
14
+ # @return [String, nil]
15
+ attr_reader :where
16
+
17
+ # Create an error +Result+.
18
+ # @param [Object] content return value
19
+ # @param [Object] caller must be set to +self+ so that the +Result+
20
+ # can store the function in which the error happened.
21
+ # @return [Result]
22
+ def self.err content=nil, caller=nil
23
+ self.send :new, :err, content, caller
24
+ end
25
+
26
+ # Create a success +Result+.
27
+ # @param [Object] content return value
28
+ # @return [Result]
29
+ def self.ok content=nil
30
+ self.send :new, :ok, content
31
+ end
32
+
33
+ # Create a +Result+.
34
+ # @param [:err, :ok] type type of +Result+
35
+ # @param [Object] content return value
36
+ # @param [Object] caller must be set to +self+ so that the +Result+
37
+ # can store the function in which the error happened.
38
+ # @return [Result]
39
+ def initialize type, content, caller=nil
40
+ @type, @content = type, content
41
+ return if caller.nil?
42
+
43
+ fn = caller_locations(3).first.base_label
44
+ @where = (fn == '<main>') ? fn : (caller.is_a?(Class) \
45
+ ? "#{caller}.#{fn}" : "#{caller.class}##{fn}")
46
+ end
47
+
48
+ # Compare two +Result+s. Will return true if both are of the same
49
+ # type, and if their contents are equal.
50
+ # @param [Result] other other +Result+
51
+ # @return [Boolean]
52
+ def == other
53
+ return false if other.instance_variable_get('@type') != @type
54
+ other_content = other.instance_variable_get('@content')
55
+ return false if @content.class != other_content.class
56
+ return true unless @content.respond_to? '=='
57
+ @content == other_content
58
+ end
59
+
60
+ # Call a method from the toplevel namespace on the value of
61
+ # the result, if it is a success. Otherwise, does nothing.
62
+ #
63
+ # If the method being called does not return a +Result+, the return
64
+ # value will be wrapped in a success +Result+. Any raised exception
65
+ # will be wrapped in an error +Result+.
66
+ # @param name [Symbol, String] Name of the method to call. If a String
67
+ # is provided, will convert it to a symbol.
68
+ # @return [Result]
69
+ def bind name, *args
70
+ bind_common Object, name, *args
71
+ end
72
+
73
+ # Call a class method on the value of the result if it is a success.
74
+ # Otherwise, does nothing.
75
+ #
76
+ # If the method being called does not return a +Result+, the return
77
+ # value will be wrapped in a success +Result+. Any raised exception
78
+ # will be wrapped in an error +Result+.
79
+ # @param (see #bind)
80
+ # @return [Result]
81
+ def bindcm name, *args
82
+ bind_common @content.class, name, *args
83
+ end
84
+
85
+ # Call an instance method on the value of the result if it is a success.
86
+ # Otherwise, does nothing.
87
+ #
88
+ # If the method being called does not return a +Result+, the return
89
+ # value will be wrapped in a success +Result+. Any raised exception
90
+ # will be wrapped in an error +Result+.
91
+ # @param (see #bind)
92
+ # @return [Result]
93
+ def bindm name, *args
94
+ bind_common @content, name, *args
95
+ end
96
+
97
+ # Indicate whether the result is an error.
98
+ # @return [Boolean]
99
+ def err?
100
+ @type == :err
101
+ end
102
+
103
+ # Retrieve the error value.
104
+ # @return [Object]
105
+ # @raise [ArgumentError] if the result is not an error.
106
+ def error
107
+ return @content if @type == :err
108
+ raise ArgumentError
109
+ end
110
+
111
+ # Retrieve the error value or a placeholder.
112
+ # @param [Object] alt placeholder
113
+ # @return [Object]
114
+ def error_or alt
115
+ @type == :err ? @content : alt
116
+ end
117
+
118
+ # Indicate whether the result is a success.
119
+ # @return [Boolean]
120
+ def ok?
121
+ @type == :ok
122
+ end
123
+
124
+ # Respresent the value as a string.
125
+ # @return [String]
126
+ # @note This functions “prettyfies” the +Result+. To actually
127
+ # see its state, use +inspect+.
128
+ def to_s
129
+ return '[+] ' + @content.to_s if @type == :ok
130
+ (@where.nil? ? '[-] ' : "[- #{@where}] ") + @content.to_s
131
+ end
132
+
133
+ # Retrieve the success value.
134
+ # @return [Object]
135
+ # @raise [ArgumentError] if the result is not a success.
136
+ def value
137
+ return @content if @type == :ok
138
+ raise ArgumentError
139
+ end
140
+
141
+ # Retrieve the success value or a placeholder.
142
+ # @param [Object] alt placeholder
143
+ # @return [Object]
144
+ def value_or alt
145
+ @type == :ok ? @content : alt
146
+ end
147
+
148
+ private
149
+
150
+ # Call a method of a given base on the value of the result,
151
+ # if it is a success. Otherwise, does nothing.
152
+ #
153
+ # If the method being called does not return a +Result+, the return
154
+ # value will be wrapped in a success +Result+. Any raised exception
155
+ # will be wrapped in an error +Result+.
156
+ # @param base [Object] The base from which to call the method. Can be
157
+ # a class, an instance or the main object.
158
+ # @param name [Symbol, String] Name of the method to call. If a String
159
+ # is provided, will convert it to a symbol.
160
+ # @return [Result]
161
+ def bind_common base, name, *args
162
+ return self if @type == :err
163
+ name = name.to_s.to_sym unless name.is_a? Symbol
164
+ begin
165
+ method = base.method name
166
+ res = base.is_a?(Class) \
167
+ ? method.call(@content, *args)
168
+ : method.call(*args)
169
+ return res.is_a?(Result) ? res : Result.ok(res)
170
+ rescue Exception => e
171
+ Result.err e, self
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,22 @@
1
+ results = [nil, '', true, 42, 42.0, :nil, true, true, Util, NilClass, 'tata']
2
+ $test.register 'args-check', results do ||
3
+ require 'util/args'
4
+ [ Util::Args.check,
5
+ Util::Args.check(nil, String, '', nil, FalseClass),
6
+ Util::Args.check(42, Integer, 0, 42, Float, 0.0, 42, Symbol, :nil),
7
+ Util::Args.check(true, 'Boolean', false, 42, 'Boolean'),
8
+ Util::Args.check(Util, Module, Kernel, Util, Class, NilClass),
9
+ Util::Args.check('toto', 'NotAClass', 'tata')
10
+ ].flatten
11
+ end
12
+
13
+ results = [nil, 'toto', 'tata', 42.0, NilClass]
14
+ $test.register 'opts-check', results do ||
15
+ require 'util/args'
16
+ opts = { :toto => 'tata', :titi => 42, 'tutu' => TrueClass }
17
+ [ Util::Opts.check(nil),
18
+ Util::Opts.check(nil, :toto, String),
19
+ Util::Opts.check(opts, :toto, String, '', :titi, Float, 0.0, \
20
+ :tutu, Class, NilClass),
21
+ ].flatten
22
+ end
@@ -0,0 +1,145 @@
1
+ require 'open-uri'
2
+ require 'yaml'
3
+
4
+ results = [nil, __dir__, false, 'fake.html', 'http://www.qwant.com']
5
+ results += ['http://www.fake.info', nil, __dir__, false, 'fake.html']
6
+ results += ['http://www.qwant.com', 'http://www.fake.info', nil, nil, __dir__]
7
+ results += ['.', true, 'drop.htm', '42']
8
+ $test.register 'downloader-setters-getters', results do ||
9
+ require 'util/downloader'
10
+ dl = Util::Downloader.new 'http://www.fake.info', dest: __dir__, \
11
+ name: 'fake.html', ref: 'http://www.qwant.com', force: 'true'
12
+ res = [dl.data, dl.dest, dl.force, dl.name, dl.ref, dl.url,
13
+ dl[:data], dl[:dest], dl[:force], dl[:name], dl[:ref], dl[:url],
14
+ dl[42], dl[:io], dl['dest']
15
+ ]
16
+ dl.set_force.set_ref(42).unset_force.set_dest.set_name('drop.htm').set_force
17
+ res += [dl.dest, dl.force, dl.name, dl.ref]
18
+ res
19
+ end
20
+
21
+ results = [:no_url, :no_dir, :no_dir, :file_exists]
22
+ $test.register 'downloader-normal-errors', results do ||
23
+ require 'util/downloader'
24
+ path = File.join __dir__, 'downloader'
25
+ fake_path = File.join path, 'fake'
26
+ true_path = File.join path, 'orig'
27
+ not_a_dir = File.join path, 'orig', 'simple.html'
28
+ url = 'https://gitlab.com/uploads/-/system/user/avatar/5582173/avatar.png'
29
+ [ Util::Downloader.new('').download.error,
30
+ Util::Downloader.new(url).set_dest(fake_path).download.error,
31
+ Util::Downloader.new(url).set_dest(not_a_dir).download.error,
32
+ Util::Downloader.new(url).set_dest(true_path).set_name('simple.html')\
33
+ .download.error
34
+ ]
35
+ end
36
+
37
+ $test.register 'downloader-404', OpenURI::HTTPError, '404' do ||
38
+ require 'util/downloader'
39
+ path = File.join __dir__, 'downloader'
40
+ url = 'https://www.perdu.com/404'
41
+ Util::Downloader.new(url).set_dest(path).download.error
42
+ end
43
+
44
+ $test.register 'downloader-success' do ||
45
+ require 'util/downloader'
46
+ path = File.join __dir__, 'downloader'
47
+ url = 'https://www.perdu.com/'
48
+ Util::Downloader.new(url).set_dest(path).download.value.nil?
49
+ end
50
+
51
+ $test.register 'downloader-second-download', :file_exists do ||
52
+ require 'util/downloader'
53
+ path = File.join __dir__, 'downloader'
54
+ url = 'https://www.perdu.com/'
55
+ Util::Downloader.new(url).set_dest(path).download.error
56
+ end
57
+
58
+ $test.register 'downloader-third-download' do ||
59
+ require 'util/downloader'
60
+ path = File.join __dir__, 'downloader'
61
+ url = 'https://www.perdu.com/'
62
+ dl = Util::Downloader.new(url).set_dest(path).set_force.download.value.nil?
63
+ end
64
+
65
+ results = [204, '<html>', 'Perdu sur l\'Internet ?']
66
+ $test.register 'downloader-right-download', results do ||
67
+ path = File.join __dir__, 'downloader', 'index.html'
68
+ content = File.read path
69
+ File.delete path
70
+ [content.length, content[0..5], content[61..82]]
71
+ end
72
+
73
+ begin
74
+ require 'rmagick'
75
+
76
+ results = [nil, 5798, 'PNG', 200, 200, '#D4D400001C1C']
77
+ $test.register 'downloader-rmagick', results do ||
78
+ require 'util/downloader'
79
+ url = 'https://gitlab.com/uploads/-/system/user/avatar/5582173/avatar.png'
80
+ dl = Util::Downloader.new(url).set_dest(Magick)
81
+ [ dl.download.value,
82
+ dl.data[0].filesize,
83
+ dl.data[0].format,
84
+ dl.data[0].rows,
85
+ dl.data[0].columns,
86
+ dl.data[0].pixel_color(79, 142).to_color,
87
+ ]
88
+ end
89
+ rescue LoadError
90
+ $test.register 'downloader-RMAGICK-NOT-INSTALLED' do || true end
91
+ end
92
+
93
+ begin
94
+ require 'nokogiri'
95
+
96
+ results = [nil, 'Vous Etes Perdu ?', 'Perdu sur l\'Internet ?']
97
+ $test.register 'downloader-nokogiri', results do ||
98
+ require 'util/downloader'
99
+ url = 'https://www.perdu.com/'
100
+ dl = Util::Downloader.new(url).set_dest(Nokogiri::HTML)
101
+ [ dl.download.value,
102
+ dl.data.title,
103
+ dl.data.at_css('h1').text
104
+ ]
105
+ end
106
+ rescue LoadError
107
+ $test.register 'downloader-NOKOGIRI-NOT-INSTALLED' do || true end
108
+ end
109
+
110
+ begin
111
+ require 'oga'
112
+
113
+ results = [nil, :html, 'Perdu sur l\'Internet ?']
114
+ $test.register 'downloader-oga', results do ||
115
+ require 'util/downloader'
116
+ url = 'https://www.perdu.com/'
117
+ dl = Util::Downloader.new(url).set_dest(Oga::HTML)
118
+ [ dl.download.value,
119
+ dl.data.type,
120
+ dl.data.at_css('h1').text
121
+ ]
122
+ end
123
+ rescue LoadError
124
+ $test.register 'downloader-OGA-NOT-INSTALLED', nil do || end
125
+ end
126
+
127
+ results = [nil, 'Browse the first website']
128
+ results += ['http://info.cern.ch/hypertext/WWW/TheProject.html']
129
+ $test.register 'downloader-rexml', results do ||
130
+ require 'util/downloader'
131
+ require 'rexml/document'
132
+ url = 'http://info.cern.ch'
133
+ dl = Util::Downloader.new(url).set_dest(REXML)
134
+ [ dl.download.value,
135
+ dl.data.root.get_elements('//ul/li/a')[0].text,
136
+ dl.data.root.get_elements('//ul/li/a')[0]['href'],
137
+ ]
138
+ end
139
+
140
+ $test.register 'dowloader-yaml', YAML::SyntaxError, 'mapping values' do ||
141
+ require 'util/downloader'
142
+ url = 'https://gitlab.com/guillel/util-gem/-/blob/master' \
143
+ '/share/lists/iso639-3.yml'
144
+ Util::Downloader.new(url).set_dest(YAML).download.error
145
+ end