util 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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