ugh 1.0.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.
Files changed (9) hide show
  1. checksums.yaml +7 -0
  2. data/GPL-3 +674 -0
  3. data/Manifest.txt +7 -0
  4. data/build.sh +9 -0
  5. data/lib/ugh.rb +89 -0
  6. data/test-ugh.rb +190 -0
  7. data/ugh.fab +287 -0
  8. data/ugh.gemspec +18 -0
  9. metadata +67 -0
@@ -0,0 +1,7 @@
1
+ GPL-3
2
+ Manifest.txt
3
+ build.sh
4
+ lib/ugh.rb
5
+ test-ugh.rb
6
+ ugh.fab
7
+ ugh.gemspec
@@ -0,0 +1,9 @@
1
+ #! /bin/bash
2
+
3
+ set -e
4
+
5
+ echo maui ugh.fab
6
+ maui ugh.fab
7
+
8
+ echo gem build ugh.gemspec
9
+ gem build ugh.gemspec
@@ -0,0 +1,89 @@
1
+ class StandardError
2
+ def initialize short_message = 'unspecified ugh', **attributes
3
+ super short_message
4
+ @short_message = short_message
5
+ @attributes = attributes
6
+ return
7
+ end
8
+
9
+ attr_accessor :short_message
10
+ attr_accessor :attributes
11
+
12
+ def to_s attributes_in_parentheses: true
13
+ s = '' << @short_message
14
+ firstp = true
15
+ @attributes.each_pair do |name, value|
16
+ if firstp then
17
+ s << (attributes_in_parentheses ? ' (' : ', ')
18
+ firstp = false
19
+ else
20
+ s << ', '
21
+ end
22
+ firstp = false
23
+ s << name.to_s << ': ' << value.inspect
24
+ end
25
+ s << ')' if attributes_in_parentheses and !firstp
26
+ return s
27
+ end
28
+
29
+ def inspect
30
+ return '#<' + to_s(attributes_in_parentheses: false) + '>'
31
+ end
32
+
33
+ def [] name
34
+ return @attributes[name.to_sym]
35
+ end
36
+
37
+ def []= name, value
38
+ return @attributes[name.to_sym] = value
39
+ end
40
+ end
41
+
42
+ def ugh short_message = 'unspecified ugh', **attr
43
+ if short_message.is_a? Class then
44
+ # Uh-oh, it's not a short message at all but an exception
45
+ # class (or so we should hope). Let's instantiate it.
46
+ raise short_message.new(**attr)
47
+ else
48
+ raise Ugh.new(short_message, **attr)
49
+ end
50
+ end
51
+
52
+ class Ugh < RuntimeError
53
+ end
54
+
55
+ def ugh? klass = Ugh, **attributes
56
+ begin
57
+ return yield
58
+ rescue klass => exception
59
+ evaluated_attributes = {}
60
+ attributes.each_pair do |name, value|
61
+ if value.is_a? Proc then
62
+ unless exception.attributes.has_key? name then
63
+ value = value.call
64
+ else
65
+ value = nil
66
+ end
67
+ end
68
+ evaluated_attributes[name] = value
69
+ end
70
+ exception.attributes =
71
+ evaluated_attributes.merge exception.attributes
72
+ raise exception
73
+ end
74
+ end
75
+
76
+ class SystemCallError
77
+ def strerror
78
+ # Remove in-sentence bits of context such as filename(s)
79
+ # by looking up the error message without context:
80
+ m = SystemCallError.new(errno).message
81
+
82
+ # Fix message case by downcasing the initial uppercase
83
+ # letter except if it's immediately followed by another
84
+ # uppercase letter, as in [["RPC version wrong"]]:
85
+ m = m.sub(/\A[[:upper:]](?![[:upper:]])/){$&.downcase}
86
+
87
+ return m
88
+ end
89
+ end
@@ -0,0 +1,190 @@
1
+ #! /usr/bin/ruby -Ilib
2
+
3
+ require 'ugh'
4
+
5
+ require 'test/unit'
6
+
7
+ class Ughish < Ugh
8
+ end
9
+
10
+ class UghTests < Test::Unit::TestCase
11
+ def test_everything
12
+ e = Ugh.new 'harumph'
13
+ assert e.is_a? Ugh
14
+ assert e.is_a? Exception
15
+ assert e.is_a? StandardError
16
+ assert e.is_a? RuntimeError
17
+
18
+ assert e.short_message.is_a? String
19
+ assert_equal e.short_message, 'harumph'
20
+ assert e.attributes.is_a? Hash
21
+ assert e.attributes.empty?
22
+
23
+ e = Ugh.new 'harrumph', to_wit: 'arrgh'
24
+ assert_equal e.short_message, 'harrumph'
25
+ assert_equal e.attributes.keys, [:to_wit]
26
+ assert e[:to_wit].is_a? String
27
+ assert_equal e[:to_wit], 'arrgh'
28
+ assert e['to_wit'].is_a? String
29
+ assert_equal e['to_wit'], 'arrgh'
30
+
31
+ e[:during] = :mumbling
32
+ assert_equal e.attributes.keys, [:to_wit, :during]
33
+ assert_equal e[:during], :mumbling
34
+
35
+ assert_equal e.to_s,
36
+ 'harrumph (to_wit: "arrgh", during: :mumbling)'
37
+ assert_equal e.to_s(attributes_in_parentheses: false),
38
+ 'harrumph, to_wit: "arrgh", during: :mumbling'
39
+ assert_equal e.inspect,
40
+ '#<harrumph, to_wit: "arrgh", during: :mumbling>'
41
+
42
+ e['loudness'] = :slight
43
+ assert_equal e.attributes.keys,
44
+ [:to_wit, :during, :loudness]
45
+ assert_equal e[:loudness], :slight
46
+
47
+ e.attributes = {:something_else => 'entirely'}
48
+ assert_equal e.to_s, 'harrumph (something_else: "entirely")'
49
+
50
+ e.short_message = 'rah-rah'
51
+ assert_equal e.to_s, 'rah-rah (something_else: "entirely")'
52
+
53
+ e.attributes.delete :something_else
54
+ assert_equal e.to_s, 'rah-rah'
55
+
56
+ assert_raise Ugh do
57
+ ugh
58
+ end
59
+
60
+ assert_raise Ughish do
61
+ ugh Ughish
62
+ end
63
+
64
+ begin
65
+ ugh colour: 'yellow'
66
+ rescue Ugh => e
67
+ assert e.is_a? Ugh
68
+ assert_equal e.attributes.keys, [:colour]
69
+ assert_equal e[:colour], 'yellow'
70
+ end
71
+
72
+ begin
73
+ ugh? bird: 'crow' do
74
+ ugh colour: 'yellow'
75
+ end
76
+ rescue Ugh => e
77
+ assert_equal e.attributes.keys, [:bird, :colour]
78
+ assert_equal e[:bird], 'crow'
79
+ assert_equal e[:colour], 'yellow'
80
+ end
81
+
82
+ begin
83
+ ugh? bird: 'crow', colour: 'yellow' do
84
+ ugh colour: 'purple'
85
+ end
86
+ rescue Ugh => e
87
+ assert_equal e.attributes.keys, [:bird, :colour]
88
+ assert_equal e[:bird], 'crow'
89
+ assert_equal e[:colour], 'purple'
90
+ end
91
+
92
+ begin
93
+ ugh? Ughish, bird: 'crow', colour: 'yellow' do
94
+ ugh colour: 'purple'
95
+ end
96
+ rescue Ugh => e
97
+ assert_equal e.attributes.keys, [:colour]
98
+ assert_equal e[:bird], nil
99
+ assert_equal e[:colour], 'purple'
100
+ end
101
+
102
+ assert_equal ugh?(bird: 'crow'){3}, 3
103
+
104
+ begin
105
+ i = 1
106
+ counter_evaluated = false
107
+ ugh? counter: proc{counter_evaluated = true; i} do
108
+ i += 1
109
+ ugh
110
+ i += 1
111
+ end
112
+ rescue Ugh => e
113
+ assert_equal counter_evaluated, true
114
+ assert_equal e.attributes.keys, [:counter]
115
+ assert_equal e[:counter], 2
116
+ end
117
+
118
+ begin
119
+ i = 1
120
+ counter_evaluated = false
121
+ ugh? counter: proc{counter_evaluated = true; i} do
122
+ i += 1
123
+ ugh counter: 7
124
+ i += 1
125
+ end
126
+ rescue Ugh => e
127
+ assert_equal counter_evaluated, false
128
+ assert_equal e.attributes.keys, [:counter]
129
+ assert_equal e[:counter], 7
130
+ end
131
+
132
+ begin
133
+ ugh? foo: 1 do
134
+ ugh? bar: 2 do
135
+ ugh? foo: 3 do
136
+ ugh
137
+ end
138
+ end
139
+ end
140
+ rescue Ugh => e
141
+ assert_equal e.attributes.keys, [:foo, :bar]
142
+ end
143
+
144
+ begin
145
+ ugh? bar: 2 do
146
+ ugh? foo: 3 do
147
+ ugh
148
+ end
149
+ end
150
+ rescue Ugh => e
151
+ assert_equal e.attributes.keys, [:bar, :foo]
152
+ end
153
+
154
+ assert_raise Errno::ENOTDIR do
155
+ File.read 'test-ugh.rb/file-that-can-not-exist'
156
+ end
157
+
158
+ begin
159
+ File.read 'test-ugh.rb/file-that-can-not-exist'
160
+ rescue StandardError => e
161
+ assert e.respond_to? :strerror
162
+ assert e.strerror.is_a? String
163
+ assert_equal e.strerror, 'not a directory'
164
+ end
165
+
166
+ begin
167
+ raise Errno::EBADRPC
168
+ rescue StandardError => e
169
+ assert_equal e.strerror, 'RPC struct is bad'
170
+ end
171
+
172
+ begin
173
+ ugh? SystemCallError, filename: 'something/something' do
174
+ File.read 'test-ugh.rb/file-that-can-not-exist'
175
+ end
176
+ rescue StandardError => e
177
+ assert_equal e[:filename], 'something/something'
178
+ end
179
+
180
+ begin
181
+ ugh? filename: 'something/something' do
182
+ File.read 'test-ugh.rb/file-that-can-not-exist'
183
+ end
184
+ rescue StandardError => e
185
+ assert_equal e[:filename], nil
186
+ end
187
+
188
+ return
189
+ end
190
+ end
data/ugh.fab ADDED
@@ -0,0 +1,287 @@
1
+ This is the [[ugh]] Rubygem. It provides facilities for
2
+ attaching attributes to Ruby exceptions (to be exact, those
3
+ exceptions that inherit from [[StandardError]]) at creation time
4
+ and via dynamic scoping. It also defines
5
+ [[SystemCallError#strerror]] to produce a description of a
6
+ low-level I/O operation in a manner suitable for embedding into
7
+ a traditional Unix command line interface style error message.
8
+
9
+
10
+ * BEWARE!
11
+
12
+ In order to achieve these goals, [[ugh.rb]] defines or redefines
13
+ certain methods in builtin Ruby classes. Under certain
14
+ hypothetical conditions, this may theoretically lead to
15
+ conflicts with other libraries also meddling in these areas,
16
+ perhaps for overlapping purposes, perhaps for incompatible
17
+ purposes. It is unlikely but possible.
18
+
19
+
20
+ == Attributes for [[StandardError]]
21
+
22
+ We'll start by augmenting [[StandardError]].
23
+
24
+ << .file lib/ugh.rb >>:
25
+ class StandardError
26
+ << @ [[StandardError]] >>
27
+ end
28
+
29
+
30
+ Its constructor shall accept an optional and arbitrary set of
31
+ attributes. It shall store the message argument passed to it
32
+ (if any) in [[@short_message]] (as contrary to the 'long
33
+ message' returned by [[#message]] that will also represent the
34
+ attributes) and these attributes in [[@attributes]] as a
35
+ [[Hash]] keyed by [[Symbol]]:s. (Note that Ruby's runtime
36
+ engine does not directly reveal the message passed to
37
+ [[Exception#initialize]]; instead, one has to call either
38
+ [[#to_s]], [[#inspect]], or [[#message]].)
39
+
40
+ We'll implement this by defining [[StandardError#initialize]].
41
+ Unfortunately, it's unreasonably tricky to 'properly' get the
42
+ replaced constructor to chain to the original [[StandardError]]
43
+ constructor. Fortunately, it turns out that [[StandardError]]
44
+ does not /have/ a built-in constructor; it just inherits one
45
+ from [[Exception]]. (See [[error.cc]]'s [[rb_eStandardError]].)
46
+ Thus, our new method can just call the inherited constructor via
47
+ [[super]], and as long as no other piece of code attempts to
48
+ hijack [[StandardError]]'s constructor in the same Ruby engine,
49
+ things should work out fine.
50
+
51
+ << @ [[StandardError]] >>:
52
+
53
+ def initialize short_message = 'unspecified ugh', **attributes
54
+ super short_message
55
+ @short_message = short_message
56
+ @attributes = attributes
57
+ return
58
+ end
59
+
60
+
61
+ Both [[@short_message]] and [[@attributes]] shall be revealed to
62
+ the user as appropriate methods, in read/write mode. We do this
63
+ by [[attr_accessor]].
64
+
65
+ attr_accessor :short_message
66
+ attr_accessor :attributes
67
+
68
+
69
+ [[StandardError#to_s]] shall provide a reasonable representation
70
+ of both the short message and the attributes.
71
+
72
+ We'll generate a single-line string. (Multiple lines is
73
+ tempting if there are many attributes, but it would violate
74
+ several implicit assumptions about [[Object#to_s]].) At the
75
+ beginning of the string will be the short message. If there are
76
+ any attributes, we'll append these to the string, by default
77
+ wrapped in parentheses. The attributes will be separated from
78
+ each other by commas, and each name-value pair will be joined
79
+ together with a colon. We'll use [[#inspect]] to convert the
80
+ attributes' values into string form but embed the attributes'
81
+ names as they are. We'll permit the caller to turn off the
82
+ parentheses via a named parameter, which we'll call
83
+ [[attributes_in_parentheses]]. When it's turned off, the short
84
+ message will be separated from the first attribute by a
85
+ comma instead of an opening paren.
86
+
87
+ def to_s attributes_in_parentheses: true
88
+ s = '' << @short_message
89
+ firstp = true
90
+ @attributes.each_pair do |name, value|
91
+ if firstp then
92
+ s << (attributes_in_parentheses ? ' (' : ', ')
93
+ firstp = false
94
+ else
95
+ s << ', '
96
+ end
97
+ firstp = false
98
+ s << name.to_s << ': ' << value.inspect
99
+ end
100
+ s << ')' if attributes_in_parentheses and !firstp
101
+ return s
102
+ end
103
+
104
+
105
+ [[StandardError#inspect]] shall provide a reasonable
106
+ representation of both the short message and the attributes; one
107
+ that won't be easily confused with external representation of an
108
+ object.
109
+
110
+ We'll implement this by calling [[#to_s]] and wrapping its
111
+ result into [[#<...>]]. Because this wrapper already provides a
112
+ suitable enclosure, we'll turn [[#to_s]]'s
113
+ [[attributes_in_parentheses]] feature off in [[#inspect]].
114
+
115
+ def inspect
116
+ return '#<' + to_s(attributes_in_parentheses: false) + '>'
117
+ end
118
+
119
+
120
+ There's no need to define [[StandardError#message]]. The way
121
+ the runtime defines [[Exception#message]], it just calls
122
+ [[#to_s]], which does the right thing.
123
+
124
+
125
+ We'll also provide syntactic sugar, in the form of [[#[]]] and
126
+ [[#[]=]], for accessing and replacing the attributes. If the
127
+ user needs more complex operations, the method [[attributes]]
128
+ should be used explicitly. In these methods, which for the main
129
+ part are just delegation wrappers, we'll convert the [[name]]
130
+ argument into a [[Symbol]] by calling its [[#to_sym]]. This
131
+ way, the user can pass either strings rather than symbols as
132
+ names, and the right thing will still happen.
133
+
134
+ def [] name
135
+ return @attributes[name.to_sym]
136
+ end
137
+
138
+
139
+ def []= name, value
140
+ return @attributes[name.to_sym] = value
141
+ end
142
+
143
+
144
+ == Raising attributeful exceptions
145
+
146
+ Next, we'll provide an attribute-friendly way to raise run-time
147
+ errors. A tempting approach would be to replace the builtin
148
+ [[raise]]; unfortunately, given its nontrivial signature _and_
149
+ the relation to [[Thread#raise]], the KISS principle would
150
+ strongly suggest another way. Thus, we'll define a method with
151
+ a simple, easy to pronounce and intuitively obvious name.
152
+
153
+ << .file lib/ugh.rb >>:
154
+
155
+ def ugh short_message = 'unspecified ugh', **attr
156
+ if short_message.is_a? Class then
157
+ # Uh-oh, it's not a short message at all but an exception
158
+ # class (or so we should hope). Let's instantiate it.
159
+ raise short_message.new(**attr)
160
+ else
161
+ raise Ugh.new(short_message, **attr)
162
+ end
163
+ end
164
+
165
+
166
+ The exception class [[ugh]] uses inherits from [[RuntimeError]].
167
+ However, note that since we attached the attribute mechanism to
168
+ its parent class, [[StandardError]], it is quite possible, if it
169
+ would be more appropriate, to define new exception classes that
170
+ do not inherit from [[RuntimeError]] and still benefit from the
171
+ attribution facility. The user will just have to manually
172
+ construct an instance and [[raise]] it.
173
+
174
+
175
+ class Ugh < RuntimeError
176
+ end
177
+
178
+
179
+ == Defining exception attributes via dynamic scope
180
+
181
+ Next, we'll define a method that will take arbitrary attributes
182
+ and a block, and attach these attributes to any exception that
183
+ might get raised from this block. We'll call it [[ugh?]]. The
184
+ name violates conventions somewhat, in that the trailing ques
185
+ does not indicate it's a predicate, rather it implies an
186
+ interrogation in the form of "So, ugh happened? Well, about
187
+ this: ...".
188
+
189
+ There are a few nuances about attributing and re-attributing
190
+ exceptions. For one, we'll want to give precedence to the
191
+ attributes that the exception already has -- since they were set
192
+ up closer to the exception happening, perhaps as it was raised,
193
+ perhaps in an inner [[ugh?]] block, they're presumedly more
194
+ specific and more useful than what we would have at the current
195
+ [[ugh?]] block.
196
+
197
+ For another, it sometimes happens that a block knows that it
198
+ wants to attach an attribute to exceptions raised from it, but
199
+ doesn't know the attribute's value at the start of the block.
200
+ Such a conundrum might be solved by nesting several [[ugh?]]
201
+ blocks, in accordance with the information becoming available,
202
+ and this is often the recommended approach. However, there are
203
+ some patterns -- such as the standard line-by-line parser -- in
204
+ which case it might be more appropriate to define an attribute
205
+ in an [[ugh?]] block not by a constant value but by an
206
+ expression, to be evaluated when the exception gets actually
207
+ thrown. In order to support this pattern, we'll check, when
208
+ handling an exception, whether any of the attributes passed to
209
+ [[ugh?]] have a value that is an instance of [[Proc]], and if
210
+ so, execute it and use the returned value rather than the
211
+ [[Proc]] itself as the attribute to be attached to the
212
+ exception.
213
+
214
+ Note that this means that it won't be possible to attach
215
+ [[Proc]] instances to an exception as attributes deliberately
216
+ via [[ugh?]] blocks. It's a bit of a wart, but considering that
217
+ attributes of exceptions are primarily meant for a user's eyes,
218
+ to help with diagnosing the issue, and automated handling of
219
+ formal attributes should in general not do anything more than
220
+ present the attributes to the user in a suitable form, the cost
221
+ is probably acceptable. In the rare cases when this won't hold,
222
+ the user can easily resort to a [[begin]] ... [[rescue]] ...
223
+ [[end]] block instead of [[ugh?]].
224
+
225
+ For third, not all attributions apply to all exceptions alike.
226
+ To that end, while [[ugh?]] by default handles instances of
227
+ [[Ugh]], it shall accept an optional exception class to narrow
228
+ (or possibly widen) the set of attributes subject to relabelling
229
+ by this [[ugh?]] block.
230
+
231
+ << .file lib/ugh.rb >>:
232
+ def ugh? klass = Ugh, **attributes
233
+ begin
234
+ return yield
235
+ rescue klass => exception
236
+ evaluated_attributes = {}
237
+ attributes.each_pair do |name, value|
238
+ << ? Evaluate [[value]] of this attribute >>
239
+ evaluated_attributes[name] = value
240
+ end
241
+ exception.attributes =
242
+ evaluated_attributes.merge exception.attributes
243
+ raise exception
244
+ end
245
+ end
246
+
247
+
248
+ We won't execute [[value.call]] even if [[value]] is a [[Proc]]
249
+ if we would be immediately discarding its result afterwards for
250
+ the reason that such an attribute has already been attached to
251
+ the exception. However, we don't want to let this affect the
252
+ order of keys in this [[Hash]] -- recall that modern Ruby
253
+ versions retain a hash's key order --, so in such a case we'll
254
+ use [[nil]] as the placeholder.
255
+
256
+ << ? Evaluate [[value]] of this attribute >>:
257
+ if value.is_a? Proc then
258
+ unless exception.attributes.has_key? name then
259
+ value = value.call
260
+ else
261
+ value = nil
262
+ end
263
+ end
264
+
265
+
266
+ == [[strerror]]-like error messages for system calls
267
+
268
+ Finally, on a perihperally related topic, we'll define
269
+ [[SystemCallError#strerror]] to return an error message somewhat
270
+ more conforming to Unix's command line interface error reporting
271
+ customs than what Ruby provides by default.
272
+
273
+ << .file lib/ugh.rb >>:
274
+ class SystemCallError
275
+ def strerror
276
+ # Remove in-sentence bits of context such as filename(s)
277
+ # by looking up the error message without context:
278
+ m = SystemCallError.new(errno).message
279
+
280
+ # Fix message case by downcasing the initial uppercase
281
+ # letter except if it's immediately followed by another
282
+ # uppercase letter, as in [["RPC version wrong"]]:
283
+ m = m.sub(/\A[[:upper:]](?![[:upper:]])/){$&.downcase}
284
+
285
+ return m
286
+ end
287
+ end