util 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,12 @@
1
+ require 'util/args'
2
+ require 'util/communia'
3
+ require 'util/console_logger'
4
+ require 'util/downloader'
5
+ require 'util/i18n'
6
+ require 'util/lists'
7
+ require 'util/result'
8
+ require 'util/test'
9
+ require 'util/yaml'
10
+
11
+ # A collection of simple utilities to reduce boilerplate.
12
+ module Util; end
@@ -0,0 +1,138 @@
1
+ module Util
2
+ # Functions to typecheck the arguments of a function.
3
+ class Args
4
+ private_class_method :new
5
+
6
+ # If no alternative value is provided to {check}, these will be used.
7
+ DEFAULT_VALUES = {
8
+ Array => [],
9
+ 'Boolean' => false,
10
+ Class => NilClass,
11
+ Complex => 0.to_c,
12
+ Encoding => Encoding::UTF_8,
13
+ FalseClass => true,
14
+ Float => 0.0,
15
+ Hash => {},
16
+ Integer => 0,
17
+ Module => Kernel,
18
+ NilClass => nil,
19
+ Object => Object.new,
20
+ Queue => Queue.new,
21
+ Random => Random::DEFAULT,
22
+ Range => (0..),
23
+ Rational => 0.to_r,
24
+ Regexp => /.*/,
25
+ SizedQueue => SizedQueue.new(1),
26
+ String => '',
27
+ Symbol => :nil,
28
+ Time => Time.now,
29
+ TrueClass => false,
30
+ }
31
+
32
+ # @no_doc
33
+ FUNCTIONS = {
34
+ Array => :to_a,
35
+ Complex => :to_c,
36
+ Enumerator => :to_enum,
37
+ Float => :to_f,
38
+ Hash => :to_h,
39
+ Integer => :to_i,
40
+ Rational => :to_r,
41
+ String => :to_s,
42
+ Symbol => :to_sym,
43
+ }
44
+
45
+ # Verify whether a given argument or arguments belong to a class or can
46
+ # be converted into that class. Any number of sequences of the three
47
+ # arguments below can be passed.
48
+ # @param [Object] value argument to check
49
+ # @param [Class, String] klass class to which it must belong; Boolean is
50
+ # a synonym for TrueClass; all standard classes who have #to_X will try
51
+ # to convert the value if it responds to the given conversion method
52
+ # (if not provided, +NilClass+ is used)
53
+ # @param [Object] alt value to use if the argument does not belong to the
54
+ # wanted class (if not provided, will default to +DEFAULT_VALUES[klass]+)
55
+ # @return [Object, Array<Object>] an array of the checked and converted
56
+ # values, or just the value if the array has only one element
57
+ # @example
58
+ # def create_integer_hash key, value
59
+ # key, value = Util::Args.check key, Symbol, :def, value, Integer, nil
60
+ # { key => value }
61
+ # end
62
+ #
63
+ # hash1 = create_integer_hash 'hello', 13.4 # { :hello => 13 }
64
+ # hash2 = create_integer_hash nil, nil # { :def => nil }
65
+ def self.check *args
66
+ check_internal nil, *args
67
+ end
68
+
69
+ # Typecheck the content of an options hash, while ignoring undefined
70
+ # options. Calls Args.check on the values associated with a given key,
71
+ # according to the rest of the informations given.
72
+ # @param [Hash] opts hash whose content to check
73
+ # @param [Array] args see {check} for the rest of the arguments
74
+ # @return (see check)
75
+ # @note It is not necessary to check whether +opts+ is a +Hash+, the
76
+ # method will do it.
77
+ # @example
78
+ # def initialize opts={}
79
+ # @encoding, @font_size, @line_height = Util::Args.check_opts opts, \
80
+ # :enc, String, 'UTF-8', :size, Integer, 12, :height, Float, 1.0
81
+ # end
82
+ def self.check_opts opts, *args
83
+ check_internal opts, *args
84
+ end
85
+
86
+ private
87
+
88
+ # Common parts to both {check} and {check_opts}.
89
+ def self.check_internal opts, *args
90
+ return nil if args.length == 0
91
+ opts = check opts, Hash, {} unless opts.nil?
92
+ count, modulo = args.length.divmod 3
93
+
94
+ if modulo == 1 then
95
+ args << NilClass
96
+ end
97
+
98
+ if modulo > 0 then
99
+ count += 1
100
+ args << DEFAULT_VALUES[args.last]
101
+ end
102
+
103
+ res = []
104
+ (0...count).each do |i|
105
+ value, klass, alt = args[i*3], args[i*3+1], args[i*3+2]
106
+ value = opts[value] unless opts.nil?
107
+ res << alt and next if value.nil?
108
+
109
+ if FUNCTIONS.has_key? klass then
110
+ f = FUNCTIONS[klass]
111
+ res << (value.respond_to?(f) ? value.send(f) : alt)
112
+ next
113
+ end
114
+
115
+ case klass.to_s
116
+ when 'Boolean' then res << (value ? true : alt) and next
117
+ when 'FalseClass' then res << (value ? alt : false) and next
118
+ when 'TrueClass' then res << (value ? true : alt) and next
119
+ end
120
+
121
+ res << alt and next unless klass.is_a?(Class) and value.is_a?(klass)
122
+ res << value
123
+ end
124
+
125
+ (res.length < 2) ? res.first : res
126
+ end
127
+ end
128
+
129
+ # Alias for {Args.check_opts}
130
+ class Opts
131
+ private_class_method :new
132
+
133
+ # Alias for {Args.check_opts}
134
+ def self.check opts, *args
135
+ Args.check_opts opts, *args
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,8 @@
1
+ module Util
2
+ # @no_doc
3
+ # Path to source code of the Ruby part of the gem.
4
+ LIB_PATH = File.dirname(File.dirname(File.expand_path(__FILE__)))
5
+ # @no_doc
6
+ # Path to the data files of the gem.
7
+ SHARE_PATH = File.join File.dirname(LIB_PATH), 'share'
8
+ end
@@ -0,0 +1,178 @@
1
+ module Util
2
+ # Help write formatted messages to the console, for friendlier
3
+ # command-line interface. Uses generally available ANSI codes, so
4
+ # it should work on all UNIXes and on recent Windows.
5
+ #
6
+ # @example
7
+ # cl = ConsoleLogger.new e: { :stderr => true }
8
+ # cl.warning 'Errors will be logged on STDERR.'
9
+ # # Message written in yellow
10
+ # begin
11
+ # text = File.read 'secret.msg'
12
+ # rescue Exception => e
13
+ # msg = 'Cannot go any further because of %E%, aborting.'
14
+ # cl.error msg, 'E': e.message
15
+ # # Message written in red
16
+ # end
17
+ class ConsoleLogger
18
+ require 'util/args'
19
+
20
+ # ANSI code to reset all formatting
21
+ RESET = "\x1b[0m"
22
+ # @no_doc
23
+ BASE = "\x1b[%CODE%m"
24
+ # @no_doc
25
+ COLORS = { :black => 0, :red => 1, :green => 2, :yellow => 3,
26
+ :blue => 4, :magenta => 5, :cyan => 6, :white => 7 }
27
+ # @no_doc
28
+ COLOR_TYPES = { :fg => 30, :bg => 40, :bright => 60 }
29
+ # @no_doc
30
+ DECORS = { :bold => 1, :faint => 2, :italic => 3, :underline => 4,
31
+ :blink => 5, :reverse => 7, :conceal => 8, :crossed => 9,
32
+ :dbl_underline => 21, :overline => 53 }
33
+ # @no_doc
34
+ CL = ConsoleLogger
35
+
36
+ # Generate the ANSI code to obtain a given formatting.
37
+ # @param opts [Hash] the wanted formatting
38
+ # @option opts [:black, :blue, :cyan, :green, :magenta,
39
+ # :red, :white, :yellow] :color font color
40
+ # @option opts [idem] :bgcolor background color
41
+ # @option opts [Boolean] :bright use bright font color
42
+ # @option opts [Boolean] :bgbright use bright background color
43
+ # @option opts [:blink, :bold, :conceal, :crossed,
44
+ # :dbl_underline, :faint, :italic, :overline, :reverse,
45
+ # :underline, Array<idem>] :decor text decorations
46
+ def self.escape_code opts={}
47
+ opts = Util::Args.check opts, Hash, {}
48
+ return RESET if opts.empty?
49
+ code = ''
50
+
51
+ if opts.has_key? :color then
52
+ color = Util::Args.check opts[:color], Symbol, :white
53
+ color = :white unless COLORS.has_key? color
54
+ bright = Util::Args.check opts[:bright], 'Boolean', false
55
+
56
+ cur = COLOR_TYPES[:fg] + COLORS[color]
57
+ cur += COLOR_TYPES[:bright] if bright
58
+ code += cur.to_s
59
+ end
60
+
61
+ if opts.has_key? :bgcolor then
62
+ color = Util::Args.check opts[:bgcolor], Symbol, :black
63
+ color = :black unless COLORS.has_key? color
64
+ bright = Util::Args.check opts[:bgbright], 'Boolean', false
65
+
66
+ cur = COLOR_TYPES[:bg] + COLORS[color]
67
+ cur += COLOR_TYPES[:bright] if bright
68
+ code += ';' unless code.empty?
69
+ code += cur.to_s
70
+ end
71
+
72
+ if opts.has_key? :decor then
73
+ decors = Util::Args.check opts[:decor], Array, [opts[:decor]]
74
+ cur = ''
75
+ decors.each do |d|
76
+ cur += ';' + DECORS[d].to_s if DECORS.has_key? d
77
+ end
78
+ cur = cur.sub ';', '' if code.empty?
79
+ code += cur
80
+ end
81
+
82
+ code.empty? ? RESET : BASE.sub('%CODE%', code)
83
+ end
84
+
85
+ # Create a new ConsoleLogger.
86
+ # @param config [Hash] initial configuration: for each kind of
87
+ # message, whether to use STDERR or STDOUT, and which formatting
88
+ # @option config [Hash { :stderr => Boolean, :code => String }]
89
+ # e configuration for error (defaults to red text)
90
+ # @option config [Hash { :stderr => Boolean, :code => String }]
91
+ # i configuration for information (defaults to cyan text)
92
+ # @option config [Hash { :stderr => Boolean, :code => String }]
93
+ # n configuration for normal (defaults to no formatting)
94
+ # @option config [Hash { :stderr => Boolean, :code => String }]
95
+ # o configuration for ok (defaults to green text)
96
+ # @option config [Hash { :stderr => Boolean, :code => String }]
97
+ # w configuration for warning (defaults to yellow text)
98
+ def initialize config={}
99
+ config = Util::Args.check config, Hash, {}
100
+ @config = {
101
+ :e => { :io => $stdout, :code => CL.escape_code(color: :red) },
102
+ :i => { :io => $stdout, :code => CL.escape_code(color: :cyan) },
103
+ :n => { :io => $stdout, :code => '' },
104
+ :o => { :io => $stdout, :code => CL.escape_code(color: :green) },
105
+ :w => { :io => $stdout, :code => CL.escape_code(color: :yellow) },
106
+ }
107
+
108
+ config.each_pair do |k, v|
109
+ next unless @config.has_key? k
110
+ v = Util::Args.check v, Hash, {}
111
+ @config[k][:io] = (v[:stderr] == true) ? $stderr : $stdout
112
+ @config[k][:code] = CL.escape_code v
113
+ end
114
+ end
115
+
116
+ # Print an error to the console.
117
+ # @param msg [String] message to print
118
+ # @param payload [Hash<#to_s, #to_s>] parts to replace in the base
119
+ # message: '%KEY%' will be replaced by 'VALUE'.
120
+ # @example
121
+ # msg = 'The array contains only %I% objects of type %T%.'
122
+ # cl.error msg, 'T': Float, 'I': arr.how_many?(Float)
123
+ #
124
+ # # Results in `The array contains only 42 objects of type Float.`
125
+ # @return nil
126
+ def error msg, payload={}
127
+ self.printf :e, msg, payload
128
+ end
129
+
130
+ # Print an important message to the console.
131
+ # @param (see #error)
132
+ # @example (see #error)
133
+ # @return (see #error)
134
+ def important msg, payload={}
135
+ self.printf :i, msg, payload
136
+ end
137
+
138
+ # Print a normal message to the console.
139
+ # @param (see #error)
140
+ # @example (see #error)
141
+ # @return (see #error)
142
+ def normal msg, payload={}
143
+ self.printf :n, msg, payload
144
+ end
145
+
146
+ # Print an approval to the console.
147
+ # @param (see #error)
148
+ # @example (see #error)
149
+ # @return (see #error)
150
+ def ok msg, payload={}
151
+ self.printf :o, msg, payload
152
+ end
153
+
154
+ # Print a warning to the console.
155
+ # @param (see #error)
156
+ # @example (see #error)
157
+ # @return (see #error)
158
+ def warning msg, payload={}
159
+ self.printf :w, msg, payload
160
+ end
161
+
162
+ private
163
+
164
+ # Common parts to all messaging methods.
165
+ # @param type [:e, :i, :n, :o, :w] kind of message
166
+ # @param msg (see #error)
167
+ # @param payload (see #error)
168
+ # @return nil
169
+ def printf type, msg, payload
170
+ msg = Util::Args.check msg, String, ''
171
+ payload = Util::Args.check payload, Hash, {}
172
+ payload.each_pair do |k, v|
173
+ msg = msg.gsub "%#{k}%", v.to_s
174
+ end
175
+ @config[type][:io].puts @config[type][:code] + msg + RESET
176
+ end
177
+ end
178
+ end
@@ -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