switches.rb 0.10.4 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea74840ee7d4c44d9a22e7e6c4464eb0417fd4ac0d65e0683465600188e2d903
4
- data.tar.gz: 7f68a8a35001b0ff0445d605f276b3205c0651e3ab5a582e970fe5d4a771ea0a
3
+ metadata.gz: fc49f311d672f64884c36a8b1645d17be83d83df5e630a272a4740db00a48448
4
+ data.tar.gz: 9a5b8accb318737020f7d1c03dfee96ae21529dafe357aabc62abcbd5314dc5a
5
5
  SHA512:
6
- metadata.gz: 284df213837788db6c63752a357d763e16a84ea37331eb405365a6754335e8372a70a7dc880caddd3f6220d4530ccf901a4bfff12e3e9482ebbc7b22433e0bf2
7
- data.tar.gz: 2b067b99909da06acaa0e366dae1ac41307f5347cd44383a3a036bac06e21eac521b6d670ac2ff07b6eac6f3332f9b07ddcee590c51e1466f039823569465f94
6
+ metadata.gz: 0171a8d12cacc99884b6606f08fb5fbf2ac048e5f9d8ed147e3bfe224a2724d2ecb6fc6d077edc9e533a2a7d4dfd2a0fb9c8563f63824ed0cd78506f6c708795
7
+ data.tar.gz: 7888b090acfe50edc7ded58b12b005232be881e0c9489edb07dec7fd126ec7afb8ef1a1cb926682e0eec96bd19efc4a5f26e9a555341f637e3caa60a905f04b8
data/CHANGELOG CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## 20260816
4
4
 
5
+ 0.11.0: Forward to OptionParser, which #method_missing has never actually done.
6
+
7
+ 1. ~ lib/Switches.rb: #method_missing tested (@op.methods - Switches.instance_methods).include?(method_name.to_s), which compares a String against an array of Symbols and so was never true. Every call fell through to the settings OpenStruct, which accepts anything, so s.banner = '...' — documented in the README's "With a banner" example — silently became a setting named banner while the parser kept its default of "Usage: <program_name> [options]". The comparison is now made in symbols, and the forwarding works: #banner=, #separator, #program_name= and the other 58 OptionParser methods reach the parser.
8
+ 2. ~ lib/Switches.rb: a declared switch takes precedence over an OptionParser method of the same name, + Switches#declared_switch?. Making the forwarding work otherwise shadows any switch sharing a name with the parser, and OptionParser has version, help, release, load, order, top, base, parse, warn, abort and banner among its 61, several of which are switches a program would reasonably want. The rule is that the program's own declarations beat the wrapper's interface, which also leaves any existing program's switches reading as they did.
9
+ 3. ~ lib/Switches.rb: + Switches::NEVER_FORWARDED, the names Ruby may call implicitly, which are excluded from the forwarded set. The set has always been built by subtraction — everything the parser responds to, less everything Switches defines — which admits any such name Switches does not happen to define. OptionParser#to_a is `summarize("#{banner}".split(/^/))`, so without the exclusion Array(switches) and [*switches] answer with the parser's help. That was latent for as long as the branch was dead and became reachable the moment it was not, so it is part of this fix rather than a consequence of it. to_h was never at risk, but only because Switches happens to define it; the constant makes the exclusion deliberate rather than incidental.
10
+ 4. ~ lib/Switches.rb: + Switches#option_parser_methods, memoised, since the difference was recomputed upon every settings read, and + Switches#option_parser_method?.
11
+ 5. + spec/all.rb: forwarding of a writer, of a method taking an argument, and of a reader, each checked through the parser's own help output rather than by reading back what was written; that a declared switch wins over the OptionParser method of the same name, read and written; that a name which is neither still reaches the settings; and that #to_a, Array() and the splat are answered by the switches rather than the parser.
12
+ 6. ~ README.md: the Notes entry claiming a Switches instance can't define a method whose name matches an OptionParser method now describes what happens, which is the reverse: the switch wins.
13
+
14
+ Not done: #respond_to_missing?, which ordinarily accompanies #method_missing. Note that it would not have prevented the to_a case above — Ruby's conversion check falls back to calling #method_missing directly when the ordinary dispatch fails, so Array() reaches #to_a whatever respond_to? answers. It wants deciding alongside the collision question below, both being about which names the object should own.
15
+
16
+ Still outstanding: the converse fault, where #method_missing does not fire at all because the method exists. A switch named after a method Object already has — :tap, :class, :display, :hash, :method, :type — never reaches the settings, and Kernel#tap without a block raises LocalJumpError nowhere near the declaration.
17
+
18
+ 0.10.5: Specify and document that a multi-word switch may be supplied hyphenated, and name that form when it is missing.
19
+
20
+ 1. + spec/all.rb: multi-word switches, in both their underscored and their hyphenated form, for a boolean, an optional argument (#set), a required argument (#set!) and a required switch (#required); that either form counts as supplied for a required switch; and that the setting is named for the symbol declared whichever form was supplied. This behaviour is OptionParser's rather than this library's — a long switch is filed under a lookup key with its underscores turned to hyphens, and whatever is supplied is converted the same way before the lookup, so --update_template and --update-template have always been the one switch. It went untested and undocumented here, which left it resting upon an implementation detail of a library this one only wraps. The specs now pin it, and would fail here rather than in a caller's script were it ever to change.
21
+ 2. ~ lib/Switches.rb: the missing-switch message names the hyphenated form, so a switch declared :access_token is reported as --access-token, in place of the --access_token built from the symbol verbatim. Cosmetic, and it says the form to prefer at a command line.
22
+ 3. ~ lib/Switches.rb: + Switches#switch_string, which builds the switch string for an attribute, in place of the identical construction #on_args and #check_required_switches each carried.
23
+ 4. ~ README.md: + a multi-word switch to the Usage section, and + a note recording where the behaviour comes from, that the setting is named for the symbol declared, and that declaring the attribute hyphenated buys nothing and costs the accessor.
24
+
5
25
  0.10.4: Declare ostruct as a runtime dependency, in place of installing it at require time.
6
26
 
7
27
  1. ~ lib/Switches.rb: require 'ostruct', in place of the require_gem 'ostruct' introduced in 0.10.0. require_gem rescues the LoadError and shells out to gem install, so the dependency went unannounced and was satisfied silently into whatever GEM_HOME was set — which, under a Homebrew formula's wrapper, means the program installing a gem into its own keg. A library should announce what it needs and fail loudly without it.
data/README.md CHANGED
@@ -135,6 +135,12 @@ switches = Switches.new do |s|
135
135
  s.set(:a){'This is the -a switch.'}
136
136
  end
137
137
 
138
+ # With a multi-word switch, which may be supplied in either its underscored or its hyphenated form
139
+ switches = Switches.new do |s|
140
+ s.boolean(:update_template)
141
+ end
142
+ # --update_template and --update-template both set it, and the setting remains #update_template?
143
+
138
144
  # With a banner
139
145
  switches = Switches.new do |s|
140
146
  s.banner = 'Here is a banner for the switches.'
@@ -186,9 +192,10 @@ switches.class # => Hash
186
192
  ## Notes
187
193
 
188
194
  - The switch `-?` can't be used, since there is no Ruby method `#?` for it to map to.
189
- - A `Switches` instance can't define a method whose name matches an `OptionParser` method, since such calls are forwarded to the underlying `OptionParser` instance.
195
+ - A call which is neither a switch nor a `Switches` method is forwarded to the underlying `OptionParser`, which is what makes `s.banner = '...'` and `s.separator '...'` work. A declared switch wins over an `OptionParser` method of the same name, so `s.set(:version)` still reads back through `#version`; without that rule a switch called `--version`, `--help`, `--load` or `--order` would be shadowed by the parser, all four being names `OptionParser` has. The names Ruby may call implicitly — `#to_a`, `#to_s` and the rest of `Switches::NEVER_FORWARDED` — are never forwarded, since `OptionParser#to_a` answers with the help and would otherwise be what `Array(switches)` and `[*switches]` gave you.
190
196
  - There is a clear demarcation between whether a switch's argument is required and whether the switch itself is required.
191
197
  - The bang interface methods (for example `set!` and `required!`) modify the switch in place with a required argument, rather than relying on a default or a value set elsewhere and later.
198
+ - A long switch whose name carries an underscore may be supplied in its hyphenated form as well, so `s.boolean(:update_template)` accepts both `--update_template` and `--update-template`. This comes from `OptionParser`, which files a long switch under a lookup key with its underscores turned to hyphens and applies the same conversion to whatever is supplied, so the two forms are one switch rather than two. The setting is named for the symbol declared, and so remains `#update_template?` whichever form was supplied. Declaring the attribute hyphenated, as `:'update-template'`, buys nothing and costs the accessor, which would then be reachable only through `#send`.
192
199
 
193
200
  ## Contributing
194
201
 
@@ -2,5 +2,5 @@
2
2
  # Switches::VERSION
3
3
 
4
4
  class Switches
5
- VERSION = '0.10.4'
5
+ VERSION = '0.11.0'
6
6
  end
data/lib/Switches.rb CHANGED
@@ -153,6 +153,9 @@ class RequiredSwitchMissing < RuntimeError; end
153
153
 
154
154
  class Switches
155
155
 
156
+ # Names Ruby may call implicitly, and which must reach the settings rather than OptionParser. #to_a would otherwise answer Array() and the splat with the parser's help.
157
+ NEVER_FORWARDED = [:to_a, :to_ary, :to_h, :to_hash, :to_s, :to_str, :pretty_print]
158
+
156
159
  class << self
157
160
 
158
161
  def as_h(*args)
@@ -277,19 +280,36 @@ class Switches
277
280
  end
278
281
 
279
282
  def method_missing(method_name, *args, &block)
280
- if (@op.methods - Switches.instance_methods).include?(method_name.to_s)
281
- @op.send(method_name.to_s, *args, &block)
283
+ if option_parser_method?(method_name) && !declared_switch?(method_name)
284
+ @op.send(method_name, *args, &block)
282
285
  else
283
- @settings.send(method_name.to_s, *args, &block)
286
+ @settings.send(method_name, *args, &block)
284
287
  end
285
288
  end
286
289
 
290
+ def option_parser_methods
291
+ @option_parser_methods ||= @op.methods - Switches.instance_methods - NEVER_FORWARDED
292
+ end
293
+
294
+ def option_parser_method?(method_name)
295
+ option_parser_methods.include?(method_name.to_sym)
296
+ end
297
+
298
+ def declared_switch?(method_name)
299
+ @all_switches.include?(method_name.to_s.delete_suffix('='))
300
+ end
301
+
302
+ def switch_string(attr)
303
+ attr = attr.to_s
304
+ "-#{attr.long_switch? ? '-' : ''}#{attr.delete('?')}"
305
+ end
306
+
287
307
  def on_args(requires_argument, options, *attrs, &block)
288
308
  on_args = []
289
309
  attrs.collect{|e| e.to_s}.each do |attr|
290
310
  @defaults[attr] = options[:default] if options[:default]
291
311
  @castings[attr] = (options[:cast] || options[:type] || options[:class]) if (options[:cast] || options[:type] || options[:class])
292
- on_args << "-#{attr.long_switch? ? '-' : ''}#{attr.to_s.delete('?')}"
312
+ on_args << switch_string(attr)
293
313
  end
294
314
  if requires_argument
295
315
  on_args << (on_args.pop + ' REQUIRED_ARGUMENT')
@@ -310,7 +330,7 @@ class Switches
310
330
  messages = []
311
331
  @required_switches.each do |required_switch|
312
332
  unless supplied_switches.include?(required_switch)
313
- messages << "required switch, -#{required_switch.long_switch? ? '-' : ''}#{required_switch.to_s.delete('?')}, is missing"
333
+ messages << "required switch, #{switch_string(required_switch).tr('_', '-')}, is missing"
314
334
  end
315
335
  end
316
336
  unless messages.empty?
data/spec/all.rb CHANGED
@@ -164,6 +164,140 @@ describe Switches do
164
164
  end
165
165
  end
166
166
 
167
+ describe "OptionParser methods" do
168
+ def build(&block)
169
+ ARGV.clear
170
+ Switches.new(&block)
171
+ end
172
+
173
+ it "forwards a writer to the OptionParser" do
174
+ expect(build{|s| s.banner = 'Usage: tap-audit [options]'}.help).to match(/\AUsage: tap-audit \[options\]/)
175
+ end
176
+
177
+ it "forwards a method taking an argument to the OptionParser" do
178
+ expect(build{|s| s.separator 'Options:'}.help).to match(/^Options:$/)
179
+ end
180
+
181
+ it "forwards a reader to the OptionParser" do
182
+ switches = build{|s| s.program_name = 'tap-audit'}
183
+ expect(switches.program_name).to eq('tap-audit')
184
+ expect(switches.help).to match(/\AUsage: tap-audit/)
185
+ end
186
+
187
+ it "prefers a declared switch to the OptionParser method of the same name" do
188
+ ARGV.clear
189
+ %w{--version 2.1}.each{|a| ARGV << a}
190
+ expect(Switches.new{|s| s.set(:version)}.version).to eq('2.1')
191
+ end
192
+
193
+ it "prefers a declared switch when it is written to as well" do
194
+ switches = build{|s| s.set(:banner)}
195
+ switches.banner = 'a setting, not the banner'
196
+ expect(switches.to_h[:banner]).to eq('a setting, not the banner')
197
+ end
198
+
199
+ it "falls through to the settings for a name which is neither" do
200
+ expect(build{|s| s.set(:port)}.nonesuch).to eq(nil)
201
+ end
202
+
203
+ it "does not forward #to_a, which OptionParser answers with its help" do
204
+ expect(build{|s| s.set(:port)}.to_a).to eq(nil)
205
+ end
206
+
207
+ it "leaves Array() with the switches rather than the parser's help" do
208
+ switches = build{|s| s.set(:port)}
209
+ expect(Array(switches)).to eq([switches])
210
+ end
211
+
212
+ it "leaves the splat with the switches rather than the parser's help" do
213
+ switches = build{|s| s.set(:port)}
214
+ expect([*switches]).to eq([switches])
215
+ end
216
+ end
217
+
218
+ describe "multi-word switches" do
219
+ def build(switches_string = '')
220
+ ARGV.clear
221
+ switches_string.split.each{|a| ARGV << a}
222
+ Switches.new do |s|
223
+ s.boolean :update_template
224
+ s.set :access_token
225
+ end
226
+ end
227
+
228
+ it "sets a boolean switch supplied in its underscored form" do
229
+ expect(build('--update_template').update_template?).to eq(true)
230
+ end
231
+
232
+ it "sets a boolean switch supplied in its hyphenated form" do
233
+ expect(build('--update-template').update_template?).to eq(true)
234
+ end
235
+
236
+ it "sets a switch to the argument supplied in its underscored form" do
237
+ expect(build('--access_token abc123').access_token).to eq('abc123')
238
+ end
239
+
240
+ it "sets a switch to the argument supplied in its hyphenated form" do
241
+ expect(build('--access-token abc123').access_token).to eq('abc123')
242
+ end
243
+
244
+ it "names the setting for the symbol declared, whichever form is supplied" do
245
+ expect(build('--update-template --access-token abc123').to_h).to eq({update_template?: true, access_token: 'abc123'})
246
+ end
247
+ end
248
+
249
+ describe "multi-word switches with a required argument" do
250
+ def build(switches_string = '')
251
+ ARGV.clear
252
+ switches_string.split.each{|a| ARGV << a}
253
+ Switches.new do |s|
254
+ s.set! :access_token
255
+ end
256
+ end
257
+
258
+ it "sets the switch to the argument supplied in its underscored form" do
259
+ expect(build('--access_token abc123').access_token).to eq('abc123')
260
+ end
261
+
262
+ it "sets the switch to the argument supplied in its hyphenated form" do
263
+ expect(build('--access-token abc123').access_token).to eq('abc123')
264
+ end
265
+
266
+ it "raises OptionParser::MissingArgument when no argument is supplied to the underscored form" do
267
+ expect{build('--access_token')}.to raise_error(OptionParser::MissingArgument)
268
+ end
269
+
270
+ it "raises OptionParser::MissingArgument when no argument is supplied to the hyphenated form" do
271
+ expect{build('--access-token')}.to raise_error(OptionParser::MissingArgument)
272
+ end
273
+ end
274
+
275
+ describe "multi-word required switches" do
276
+ def build(switches_string = '')
277
+ ARGV.clear
278
+ switches_string.split.each{|a| ARGV << a}
279
+ Switches.new do |s|
280
+ s.required :access_token
281
+ end
282
+ end
283
+
284
+ it "counts the underscored form as supplied" do
285
+ expect(build('--access_token abc123').access_token).to eq('abc123')
286
+ end
287
+
288
+ it "counts the hyphenated form as supplied" do
289
+ expect(build('--access-token abc123').access_token).to eq('abc123')
290
+ end
291
+
292
+ it "raises RequiredSwitchMissing when neither form is supplied" do
293
+ expect{build}.to raise_error(RequiredSwitchMissing)
294
+ end
295
+
296
+ it "names the hyphenated form in the RequiredSwitchMissing message" do
297
+ expect{build}.to raise_error(RequiredSwitchMissing, /--access-token/)
298
+ end
299
+ end
300
+
167
301
  describe "#perform" do
168
302
  def build(switches_string = '', &block)
169
303
  ARGV.clear
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switches.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.4
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran