boson 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/boson/util.rb CHANGED
@@ -24,12 +24,6 @@ module Boson
24
24
  any_const_get(camelize(string))
25
25
  end
26
26
 
27
- def symbolize_keys(hash)
28
- hash.inject({}) {|options, (key, value)|
29
- options[key.to_sym] = value; options
30
- }
31
- end
32
-
33
27
  # Returns a constant like const_get() no matter what namespace it's nested in.
34
28
  # Returns nil if the constant is not found.
35
29
  def any_const_get(name)
@@ -80,16 +74,13 @@ module Boson
80
74
  all_modules
81
75
  end
82
76
 
83
- # Returns array of _all_ common instance methods between two modules/classes.
84
- def common_instance_methods(module1, module2)
85
- (module1.instance_methods + module1.private_instance_methods) & (module2.instance_methods + module2.private_instance_methods)
86
- end
87
-
88
- # Creates a module under a given base module and possible name. If the module already exists, it attempts
89
- # to create one with a number appended to the name.
77
+ # Creates a module under a given base module and possible name. If the module already exists or conflicts
78
+ # per top_level_class_conflict, it attempts to create one with a number appended to the name.
90
79
  def create_module(base_module, name)
91
80
  desired_class = camelize(name)
92
- if (suffix = ([""] + (1..10).to_a).find {|e| !base_module.const_defined?(desired_class+e)})
81
+ possible_suffixes = [''] + %w{1 2 3 4 5 6 7 8 9 10}
82
+ if (suffix = possible_suffixes.find {|e| !base_module.const_defined?(desired_class+e) &&
83
+ !top_level_class_conflict(base_module, "#{base_module}::#{desired_class}#{e}") })
93
84
  base_module.const_set(desired_class+suffix, Module.new)
94
85
  end
95
86
  end
@@ -108,5 +99,20 @@ module Boson
108
99
  def recursive_hash_merge(hash1, hash2)
109
100
  hash1.merge(hash2) {|k,o,n| (o.is_a?(Hash)) ? recursive_hash_merge(o,n) : n}
110
101
  end
102
+
103
+ # From Rubygems, determine a user's home.
104
+ def find_home
105
+ ['HOME', 'USERPROFILE'].each {|e| return ENV[e] if ENV[e] }
106
+ return "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
107
+ File.expand_path("~")
108
+ rescue
109
+ File::ALT_SEPARATOR ? "C:/" : "/"
110
+ end
111
+
112
+ # Returns name of top level class that conflicts if it exists. For example, for base module Boson::Commands,
113
+ # Boson::Commands::Alias conflicts with Alias if Alias exists.
114
+ def top_level_class_conflict(base_module, conflicting_module)
115
+ (conflicting_module =~ /^#{base_module}.*::([^:]+)/) && Object.const_defined?($1) && $1
116
+ end
111
117
  end
112
118
  end
data/lib/boson/view.rb CHANGED
@@ -11,33 +11,88 @@ module Boson
11
11
 
12
12
  # Renders any object via Hirb. Options are passed directly to
13
13
  # {Hirb::Console.render_output}[http://tagaholic.me/hirb/doc/classes/Hirb/Console.html#M000011].
14
- def render(object, options={})
14
+ def render(object, options={}, return_obj=false)
15
15
  if silent_object?(object)
16
16
  puts(object.inspect) unless options[:silence_booleans]
17
17
  else
18
- render_object(object, options)
18
+ render_object(object, options, return_obj)
19
19
  end
20
20
  end
21
21
 
22
+ # Searches and sorts an array of objects or hashes using options :query, :sort and :reverse_sort.
23
+ # The :query option is a hash of fields mapped to their search terms. Searches are OR-ed.
24
+ def search_and_sort(object, options)
25
+ if object.is_a?(Array)
26
+ object = search_object(object, options[:query]) if options[:query]
27
+ object = sort_object(object, options[:sort], options[:reverse_sort]) if object.size > 0 && options[:sort]
28
+ end
29
+ object
30
+ end
31
+
32
+ #:stopdoc:
33
+ def toggle_pager
34
+ Hirb::View.toggle_pager
35
+ end
36
+
22
37
  def silent_object?(obj)
23
38
  [nil,false,true].include?(obj)
24
39
  end
25
40
 
26
- def render_object(object, options={}) #:nodoc:
41
+ def render_object(object, options={}, return_obj=false)
27
42
  options[:class] ||= :auto_table
28
- if object.is_a?(Array) && object.size > 0 && (sort = options.delete(:sort))
29
- begin
30
- sort_lambda = object[0].is_a?(Hash) ? (object[0][sort].respond_to?(:<=>) ?
31
- lambda {|e| e[sort] } : lambda {|e| e[sort].to_s }) :
32
- (object[0].send(sort).respond_to?(:<=>) ? lambda {|e| e.send(sort)} :
33
- lambda {|e| e.send(sort).to_s })
34
- object = object.sort_by &sort_lambda
35
- object = object.reverse if options[:reverse_sort]
36
- rescue NoMethodError, ArgumentError
37
- $stderr.puts "Sort failed with nonexistant method '#{sort}'"
43
+ render_result = Hirb::Console.render_output(object, options)
44
+ return_obj ? object : render_result
45
+ end
46
+
47
+ def search_object(object, query_hash)
48
+ if object[0].is_a?(Hash)
49
+ TableCallbacks.search_callback(object, :query=>query_hash)
50
+ else
51
+ query_hash.map {|field,query| object.select {|e| e.send(field).to_s =~ /#{query}/i } }.flatten.uniq
52
+ end
53
+ rescue NoMethodError
54
+ $stderr.puts "Query failed with nonexistant method '#{$!.message[/`(.*)'/,1]}'"
55
+ end
56
+
57
+ def sort_object(object, sort, reverse_sort=false)
58
+ if object[0].is_a?(Hash)
59
+ TableCallbacks.sort_callback(object, :sort=>sort, :reverse_sort=>reverse_sort)
60
+ else
61
+ sort_lambda = object.all? {|e| e.send(sort).respond_to?(:<=>) } ? lambda {|e| e.send(sort) || ''} :
62
+ lambda {|e| e.send(sort).to_s }
63
+ object = object.sort_by &sort_lambda
64
+ object = object.reverse if reverse_sort
65
+ object
66
+ end
67
+ rescue NoMethodError, ArgumentError
68
+ $stderr.puts "Sort failed with nonexistant method '#{sort}'"
69
+ end
70
+ #:startdoc:
71
+
72
+ # Callbacks used by Hirb::Helpers::Table to search and sort arrays of hashes.
73
+ module TableCallbacks
74
+ extend self
75
+ # Case-insensitive searches an array of hashes using the option :query. Numerical string keys
76
+ # in :query are converted to actual numbers to interface with Hirb. See View.search_and_sort for more
77
+ # about :query.
78
+ def search_callback(obj, options)
79
+ !options[:query] ? obj : begin
80
+ options[:query].map {|field,query|
81
+ field = field.to_i if field.to_s[/^\d+$/]
82
+ obj.select {|e| e[field].to_s =~ /#{query}/i }
83
+ }.flatten.uniq
38
84
  end
39
85
  end
40
- Hirb::Console.render_output(object, options)
86
+
87
+ # Sorts an array of hashes using :sort option and reverses the sort with :reverse_sort option.
88
+ def sort_callback(obj, options)
89
+ sort = options[:sort].to_s[/^\d+$/] ? options[:sort].to_i : options[:sort]
90
+ sort_lambda = (obj.all? {|e| e[sort].respond_to?(:<=>) } ? lambda {|e| e[sort] } : lambda {|e| e[sort].to_s })
91
+ obj = obj.sort_by &sort_lambda
92
+ obj = obj.reverse if options[:reverse_sort]
93
+ obj
94
+ end
41
95
  end
42
96
  end
43
- end
97
+ end
98
+ Hirb::Helpers::Table.send :include, Boson::View::TableCallbacks
@@ -55,8 +55,8 @@ module Boson
55
55
  end
56
56
 
57
57
  test "global option takes value with whitespace" do
58
- View.expects(:render).with(anything, {:sort => :lib, :fields => [:name, :lib]})
59
- start('commands', '-g', 'f=name,lib s=lib')
58
+ View.expects(:render).with(anything, {:vertical=>true, :fields => [:name, :lib]}, anything)
59
+ start('commands', '-g', 'f=name,lib vertical')
60
60
  end
61
61
 
62
62
  test "execute option errors are caught" do
@@ -69,7 +69,11 @@ module Boson
69
69
 
70
70
  test "failed subcommand prints error and not command not found" do
71
71
  BinRunner.expects(:execute_command).raises("bling")
72
- capture_stderr { start("commands.blah") }.should =~ /Error: bling/
72
+ capture_stderr { start("commands.to_s") }.should =~ /Error: bling/
73
+ end
74
+
75
+ test "nonexistant subcommand prints command not found" do
76
+ capture_stderr { start("to_s.bling") }.should =~ /'to_s.bling' not found/
73
77
  end
74
78
 
75
79
  test "undiscovered command prints error" do
@@ -102,19 +106,31 @@ module Boson
102
106
  end
103
107
 
104
108
  context "load_command_by_index" do
109
+ def index(options={})
110
+ Manager.expects(:load).with {|*args| args[0][0].is_a?(Module) ? true : args[0] == options[:load]
111
+ }.at_least(1).returns(!options[:fails])
112
+ Index.expects(:write)
113
+ end
114
+
105
115
  test "with index option, no existing index and core command updates index and prints index message" do
106
- Manager.expects(:load).with {|*args| args[0][0].is_a?(Module) ? true : args[0] == Runner.all_libraries }.at_least(1)
107
- Index.expects(:exists?).returns(false)
108
- Index.expects(:write)
116
+ index :load=>Runner.all_libraries
117
+ Index.stubs(:exists?).returns(false)
109
118
  capture_stdout { start("--index", "libraries") }.should =~ /Generating index/
110
119
  end
111
120
 
112
121
  test "with index option, existing index and core command updates incremental index" do
113
- Index.expects(:changed_libraries).returns(['changed'])
114
- Manager.expects(:load).with {|*args| args[0][0].is_a?(Module) ? true : args[0] == ['changed'] }.at_least(1)
115
- Index.expects(:exists?).returns(true)
116
- Index.expects(:write)
117
- capture_stdout { start("--index", "libraries")}.should =~ /Indexing.*changed/
122
+ index :load=>['changed']
123
+ Index.stubs(:exists?).returns(true)
124
+ capture_stdout { start("--index=changed", "libraries")}.should =~ /Indexing.*changed/
125
+ end
126
+
127
+ test "with index option, failed indexing prints error" do
128
+ index :load=>['changed'], :fails=>true
129
+ Index.stubs(:exists?).returns(true)
130
+ Manager.stubs(:failed_libraries).returns(['changed'])
131
+ capture_stderr {
132
+ capture_stdout { start("--index=changed", "libraries")}.should =~ /Indexing.*changed/
133
+ }.should =~ /Error:.*failed.*changed/
118
134
  end
119
135
 
120
136
  test "with core command updates index and doesn't print index message" do
@@ -13,7 +13,20 @@ module Boson
13
13
 
14
14
  test "in a subdirectory loads" do
15
15
  load 'site/delicious', :file_string=>"module Delicious; def blah; end; end"
16
- library_has_module('site/delicious', "Boson::Commands::Delicious")
16
+ library_has_module('site/delicious', "Boson::Commands::Site::Delicious")
17
+ command_exists?('blah')
18
+ end
19
+
20
+ test "in a sub subdirectory loads" do
21
+ load 'web/site/delicious', :file_string=>"module Delicious; def blah; end; end"
22
+ library_has_module('web/site/delicious', "Boson::Commands::Web::Site::Delicious")
23
+ command_exists?('blah')
24
+ end
25
+
26
+ test "loads by basename" do
27
+ Dir.stubs(:[]).returns(['./test/commands/site/github.rb'])
28
+ load 'github', :file_string=>"module Github; def blah; end; end", :exists=>false
29
+ library_has_module('site/github', "Boson::Commands::Site::Github")
17
30
  command_exists?('blah')
18
31
  end
19
32
 
data/test/index_test.rb CHANGED
@@ -9,7 +9,8 @@ module Boson
9
9
  before(:each) { reset_boson; Index.instance_eval "@libraries = @commands = nil" }
10
10
 
11
11
  def transfers(options={})
12
- Index.instance_variable_set "@libraries", [Library.new(:name=>'blah'), Library.new(:name=>'bling')]
12
+ Index.instance_variable_set "@libraries", [Library.new(:name=>'blah', :commands=>['blurb']),
13
+ Library.new(:name=>'bling')]
13
14
  Index.instance_variable_set "@commands", [Command.new(:name=>'blurb', :lib=>'blah')]
14
15
  Index.read_and_transfer options[:ignored] || []
15
16
  Boson.libraries.map {|e| e.name}.should == options[:libraries]
data/test/loader_test.rb CHANGED
@@ -7,20 +7,62 @@ module Boson
7
7
  Manager.load([Boson::Commands::Namespace])
8
8
  end
9
9
 
10
+ context "config" do
11
+ before(:each) { reset }
12
+ test "from callback overridden by user's config" do
13
+ with_config(:libraries=>{'blih'=>{:namespace=>false}}) do
14
+ load :blih, :file_string=>"module Blah; def self.config; {:namespace=>'bling'}; end; end"
15
+ library('blih').namespace.should == false
16
+ end
17
+ end
18
+
19
+ # if this test fails, other exists? using methods fail
20
+ test "from callback recursively merges with user's config" do
21
+ with_config(:libraries=>{'blah'=>{:commands=>{'bling'=>{:description=>'bling', :options=>{:num=>3}}}}}) do
22
+ File.stubs(:exists?).returns(true)
23
+ load :blah, :file_string=> "module Blah; def self.config; {:commands=>{'blang'=>{:alias=>'ba'}, " +
24
+ "'bling'=>{:options=>{:verbose=>:boolean}}}}; end; end"
25
+ library('blah').command_object('bling').options.should == {:verbose=>:boolean, :num=>3}
26
+ library('blah').command_object('bling').description.should == 'bling'
27
+ library('blah').command_object('blang').alias.should == 'ba'
28
+ end
29
+ end
30
+
31
+ test "non-hash from inspector overridden by user's config" do
32
+ with_config(:libraries=>{'blah'=>{:commands=>{'bling'=>{:description=>'already'}}}}) do
33
+ load :blah, :file_string=>"module Blah; #from file\ndef bling; end; end"
34
+ library('blah').command_object('bling').description.should == 'already'
35
+ end
36
+ end
37
+
38
+ test "hash from inspector recursively merged with user's config" do
39
+ with_config(:libraries=>{'blah'=>{:commands=>{'blung'=>{:args=>[], :render_options=>{:sort=>'this'}}}}}) do
40
+ CommentInspector.expects(:scrape).returns({:render_options=>{:fields=>['this']}})
41
+ load :blah, :file_string=>"module Blah; def blung; end; end"
42
+ library('blah').command_object('blung').render_options.should == {:fields=>["this"], :sort=>"this"}
43
+ end
44
+ end
45
+ end
46
+
10
47
  context "load" do
11
48
  before(:each) { reset }
12
- test "calls included hook" do
49
+ test "calls included callback" do
13
50
  capture_stdout {
14
51
  load :blah, :file_string=>"module Blah; def self.included(mod); puts 'included blah'; end; def blah; end; end"
15
52
  }.should =~ /included blah/
16
53
  end
17
54
 
18
- test "calls methods in config init_methods" do
19
- with_config(:libraries=>{"blah"=>{:init_methods=>['blah']}}) do
20
- capture_stdout {
21
- load :blah, :file_string=>"module Blah; def blah; puts 'yo'; end; end"
22
- }.should == "yo\n"
23
- end
55
+ test "calls after_included callback" do
56
+ capture_stdout {
57
+ load :blah, :file_string=>"module Blah; def self.after_included; puts 'yo'; end; end"
58
+ }.should == "yo\n"
59
+ end
60
+
61
+ test "prints error if library module conflicts with top level constant/module" do
62
+ capture_stderr {
63
+ load :blah, :file_string=>"module Object; def self.blah; end; end"
64
+ }.should =~ /conflict.*'Object'/
65
+ library_loaded?('blah')
24
66
  end
25
67
 
26
68
  test "prints error and returns false for existing library" do
@@ -37,6 +79,14 @@ module Boson
37
79
  end
38
80
  end
39
81
 
82
+ test "loads a library and creates its class commands" do
83
+ with_config(:libraries=>{"blah"=>{:class_commands=>{"bling"=>"Blah.bling", "Blah"=>['hmm']}}}) do
84
+ load :blah, :file_string=>"module Blah; def self.bling; end; def self.hmm; end; end"
85
+ command_exists? 'bling'
86
+ command_exists? 'hmm'
87
+ end
88
+ end
89
+
40
90
  test "loads a library with dependencies" do
41
91
  File.stubs(:exists?).returns(true)
42
92
  File.stubs(:read).returns("module Water; def water; end; end", "module Oaks; def oaks; end; end")
@@ -60,6 +110,14 @@ module Boson
60
110
  end
61
111
  end
62
112
 
113
+ test "prints error for method conflicts with main_object method" do
114
+ with_config(:error_method_conflicts=>true) do
115
+ capture_stderr {
116
+ load('blah', :file_string=>"module Blah; def require; end; end")
117
+ }.should =~ /Unable to load library blah.*conflict.*require/
118
+ end
119
+ end
120
+
63
121
  test "prints error for method conflicts with config error_method_conflicts" do
64
122
  with_config(:error_method_conflicts=>true) do
65
123
  load('blah', :file_string=>"module Blah; def chwhat; end; end")
@@ -81,11 +139,25 @@ module Boson
81
139
  context "module library" do
82
140
  def mock_library(*args); end
83
141
 
84
- test "loads a module library" do
85
- eval %[module ::Harvey; def bird; end; end]
142
+ test "loads a module library and all its class methods by default" do
143
+ eval %[module ::Harvey; def self.bird; end; def self.eagle; end; end]
86
144
  load ::Harvey, :no_mock=>true
87
- library_has_module('harvey', "Harvey")
88
- command_exists?('bird')
145
+ library_has_command('harvey', 'bird')
146
+ library_has_command('harvey', 'eagle')
147
+ end
148
+
149
+ test "loads a module library with specified commands" do
150
+ eval %[module ::Peanut; def self.bird; end; def self.eagle; end; end]
151
+ load ::Peanut, :no_mock=>true, :commands=>%w{bird}
152
+ library('peanut').commands.size.should == 1
153
+ library_has_command('peanut', 'bird')
154
+ end
155
+
156
+ test "loads a module library as a class" do
157
+ eval %[class ::Mentok; def self.bird; end; def self.eagle; end; end]
158
+ load ::Mentok, :no_mock=>true, :commands=>%w{bird}
159
+ library('mentok').commands.size.should == 1
160
+ library_has_command('mentok', 'bird')
89
161
  end
90
162
  end
91
163
 
@@ -145,17 +217,8 @@ module Boson
145
217
  end
146
218
  end
147
219
 
148
- test "loads with config except" do
149
- with_config(:libraries=>{'blong'=>{:namespace=>true, :except=>['wrong']}}) do
150
- load 'blong', :file_string=>"module Blong; def bling; end; def wrong; end; end"
151
- library_has_command('blong', 'bling')
152
- library_has_command('blong', 'wrong', false)
153
- library('blong').commands.size.should == 1
154
- end
155
- end
156
-
157
220
  test "prints error if namespace conflicts with existing commands" do
158
- eval "module Conflict; def bleng; end; end"
221
+ eval "module Conflict; def self.bleng; end; end"
159
222
  load Conflict, :no_mock=>true
160
223
  with_config(:libraries=>{'bleng'=>{:namespace=>true}}) do
161
224
  capture_stderr {
data/test/manager_test.rb CHANGED
@@ -5,7 +5,7 @@ module Boson
5
5
  context "after_load" do
6
6
  def load_library(hash)
7
7
  new_attributes = {:name=>hash.delete(:name), :commands=>[], :created_dependencies=>[], :loaded=>true}
8
- [:module, :except, :commands].each {|e| new_attributes[e] = hash.delete(e) if hash[e] }
8
+ [:module, :commands].each {|e| new_attributes[e] = hash.delete(e) if hash[e] }
9
9
  Manager.expects(:load_once).returns(Library.new(new_attributes))
10
10
  Manager.load([hash[:name]])
11
11
  end
@@ -24,14 +24,6 @@ module Boson
24
24
  command_exists?('meatwad')
25
25
  end
26
26
 
27
- test "loads library with commands and except option" do
28
- Boson.main_object.instance_eval("class<<self;self;end").expects(:undef_method).with('frylock')
29
- load_library :name=>'blah', :commands=>['frylock','meatwad'], :except=>['frylock']
30
- library_loaded? 'blah'
31
- command_exists?('frylock', false)
32
- command_exists?('meatwad')
33
- end
34
-
35
27
  context "command aliases" do
36
28
  before(:each) { eval %[module ::Aquateen; def frylock; end; end] }
37
29
  after(:each) { Object.send(:remove_const, "Aquateen") }
@@ -1,7 +1,7 @@
1
1
  require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
3
  module Boson
4
- class InspectorTest < Test::Unit::TestCase
4
+ class MethodInspectorTest < Test::Unit::TestCase
5
5
  test "non commands module can't set anything" do
6
6
  eval "module Blah; end"
7
7
  MethodInspector.current_module = Blah
@@ -21,7 +21,7 @@ module Boson
21
21
  end
22
22
 
23
23
  before(:all) { eval "module ::Boson::Commands::Zzz; end" }
24
- before(:each) { MethodInspector.mod_store[::Boson::Commands::Zzz] = {} }
24
+ before(:each) { MethodInspector.mod_store.delete(::Boson::Commands::Zzz) }
25
25
 
26
26
  test "desc sets descriptions" do
27
27
  parsed = parse "desc 'test'; def m1; end; desc 'one'; desc 'more'; def m2; end"
@@ -90,7 +90,7 @@ module Boson
90
90
  end
91
91
 
92
92
  it "allows humanized opt name" do
93
- create 'foo' => :string, :bar => :required
93
+ create 'foo' => :string, :bar => :string
94
94
  parse("-f", "1", "-b", "2").should == {:foo => "1", :bar => "2"}
95
95
  end
96
96
 
@@ -113,6 +113,7 @@ module Boson
113
113
  it "accepts --[no-]opt variant for booleans, setting false for value" do
114
114
  create "--foo" => :boolean
115
115
  parse("--no-foo")["foo"].should == false
116
+ parse("--no-f")["foo"].should == false
116
117
  parse("--foo")["foo"].should == true
117
118
  end
118
119
 
@@ -125,7 +126,7 @@ module Boson
125
126
 
126
127
  context "option values can be set with" do
127
128
  it "a opt=<value> assignment" do
128
- create "--foo" => :required
129
+ create :foo => :string
129
130
  parse("--foo=12")["foo"].should == "12"
130
131
  parse("-f=12")["foo"].should == "12"
131
132
  parse("--foo=bar=baz")["foo"].should == "bar=baz"
@@ -133,8 +134,8 @@ module Boson
133
134
  end
134
135
 
135
136
  it "a -nXY assignment" do
136
- create "--num" => :required
137
- parse("-n12")["num"].should == "12"
137
+ create "--num" => :numeric
138
+ parse("-n12")["num"].should == 12
138
139
  end
139
140
 
140
141
  it "conjoined short options" do
@@ -146,17 +147,17 @@ module Boson
146
147
  end
147
148
 
148
149
  it "conjoined short options with argument" do
149
- create "--foo" => true, "--bar" => true, "--app" => :required
150
+ create "--foo" => true, "--bar" => true, "--app" => :numeric
150
151
  opts = parse "-fba", "12"
151
152
  opts["foo"].should == true
152
153
  opts["bar"].should == true
153
- opts["app"].should == "12"
154
+ opts["app"].should == 12
154
155
  end
155
156
  end
156
157
 
157
158
  context "parse" do
158
159
  it "extracts non-option arguments" do
159
- create "--foo" => :required, "--bar" => true
160
+ create "--foo" => :string, "--bar" => true
160
161
  parse("foo", "bar", "--baz", "--foo", "12", "--bar", "-T", "bang").should == {
161
162
  :foo => "12", :bar => true
162
163
  }
@@ -208,14 +209,55 @@ module Boson
208
209
  end
209
210
  end
210
211
 
211
- it ":required type raises an error if it isn't given" do
212
- create "--foo" => :required, "--bar" => :string
213
- assert_error(OptionParser::Error, 'no value.*required.*foo') { parse("--bar", "str") }
212
+ context ":required option attribute" do
213
+ before(:all) {
214
+ create "--foo" => {:type=>:string, :required=>true}, :bar => {:type=>:hash, :required=>true}
215
+ }
216
+
217
+ it "raises an error if string option isn't given" do
218
+ assert_error(OptionParser::Error, 'no value.*required.*foo') { parse("--bar", "str:ok") }
219
+ end
220
+
221
+ it "raises an error if non-string option isn't given" do
222
+ assert_error(OptionParser::Error, 'no value.*required.*bar') { parse("--foo", "yup") }
223
+ end
224
+
225
+ it "raises no error when given arguments" do
226
+ parse("--foo", "yup", "--bar","ok:dude").should == {:foo=>'yup', :bar=>{'ok'=>'dude'}}
227
+ end
214
228
  end
215
-
229
+
230
+ context ":bool_default option attribute" do
231
+ before(:all) {
232
+ create :foo=>{:type=>:string, :bool_default=>'whoop'}, :bar=>{:type=>:array, :bool_default=>'1'},
233
+ :verbose=>:boolean, :yep=>{:type=>:string, :bool_default=>true}
234
+ }
235
+
236
+ it "sets default boolean" do
237
+ parse('--foo', '--bar', '1')[:foo].should == 'whoop'
238
+ parse('--foo', 'ok', 'dokay')[:foo].should == 'whoop'
239
+ end
240
+
241
+ it "sets options normally" do
242
+ parse('--foo=boo', '--bar=har').should == {:foo=>'boo', :bar=>['har']}
243
+ end
244
+
245
+ it "sets default boolean for array" do
246
+ parse("--bar", '--foo', '2')[:bar].should == ['1']
247
+ end
248
+
249
+ it "sets default boolean for non-string value" do
250
+ parse('--yep', '--foo=2')[:yep].should == true
251
+ end
252
+
253
+ it "default booleans can be joined with boolean options" do
254
+ parse('-fbv').should == {:verbose=>true, :bar=>['1'], :foo=>'whoop'}
255
+ end
256
+ end
257
+
216
258
  context ":string type" do
217
259
  before :each do
218
- create "--foo" => :string, "--bar" => :string
260
+ create "--foo" => :string, "--bar" => :string, :blah=>{:type=>:string, :default=>:huh}
219
261
  end
220
262
 
221
263
  it "doesn't set nonexistant options" do
@@ -238,6 +280,10 @@ module Boson
238
280
  it "overwrites earlier values with later values" do
239
281
  parse("--foo", "12", "--foo", "13")[:foo].should == "13"
240
282
  end
283
+
284
+ it "can have symbolic default value" do
285
+ parse('--blah','ok')[:blah].should == 'ok'
286
+ end
241
287
  end
242
288
 
243
289
  context ":string type with :values attribute" do
@@ -298,8 +344,8 @@ module Boson
298
344
 
299
345
  context ":array type" do
300
346
  before(:all) {
301
- create :a=>:array, :b=>[1,2,3], :c=>{:type=>:array, :values=>%w{foo fa bar zebra}},
302
- :d=>{:type=>:array, :split=>" "}
347
+ create :a=>:array, :b=>[1,2,3], :c=>{:type=>:array, :values=>%w{foo fa bar zebra}, :enum=>false},
348
+ :d=>{:type=>:array, :split=>" ", :values=>[:ab, :bc, :cd], :enum=>false}
303
349
  }
304
350
 
305
351
  it "supports array defaults" do
@@ -318,9 +364,71 @@ module Boson
318
364
  parse("-c","f,b")[:c].should == %w{fa bar}
319
365
  end
320
366
 
321
- it "allows a configurable splitter" do
367
+ it "auto aliases symbolic :values" do
368
+ parse("-d","a c")[:d].should == [:ab,:cd]
369
+ end
370
+
371
+ it "supports a configurable splitter" do
322
372
  parse("-d", "yogi berra")[:d].should == %w{yogi berra}
323
373
  end
374
+
375
+ it "aliases * to all values" do
376
+ parse("-c", '*')[:c].sort.should == %w{bar fa foo zebra}
377
+ parse("-c", '*,ok')[:c].sort.should == %w{bar fa foo ok zebra}
378
+ end
379
+ end
380
+
381
+ context ":hash type" do
382
+ before(:all) {
383
+ create :a=>:hash, :b=>{:default=>{:a=>'b'}}, :c=>{:type=>:hash, :keys=>%w{one two three}},
384
+ :e=>{:type=>:hash, :keys=>[:one, :two, :three], :default_keys=>:three},
385
+ :d=>{:type=>:hash, :split=>" "}
386
+ }
387
+
388
+ it "converts comma delimited pairs to hash" do
389
+ parse("-a", "f:3,g:4")[:a].should == {'f'=>'3', 'g'=>'4'}
390
+ end
391
+
392
+ it "supports hash defaults" do
393
+ parse[:b].should == {:a=>'b'}
394
+ end
395
+
396
+ it "raises error when option has no value" do
397
+ assert_error(OptionParser::Error, "no value.*'a'") { parse("-a") }
398
+ end
399
+
400
+ it "raises error if invalid key-value pair given for unknown keys" do
401
+ assert_error(OptionParser::Error, "invalid.*pair.*'a'") { parse("-a", 'b') }
402
+ end
403
+
404
+ it "auto aliases :keys attribute" do
405
+ parse("-c","t:3,o:1")[:c].should == {'three'=>'3', 'one'=>'1'}
406
+ end
407
+
408
+ it "adds in explicit default keys with value only argument" do
409
+ parse('-e', 'whoop')[:e].should == {:three=>'whoop'}
410
+ end
411
+
412
+ it "adds in default keys from known :keys with value only argument" do
413
+ parse("-c","okay")[:c].should == {'one'=>'okay'}
414
+ end
415
+
416
+ it "auto aliases symbolic :keys" do
417
+ parse("-e","t:3,o:1")[:e].should == {:three=>'3', :one=>'1'}
418
+ end
419
+
420
+ it "supports a configurable splitter" do
421
+ parse("-d","a:ab b:bc")[:d].should == {'a'=>'ab', 'b'=>'bc'}
422
+ end
423
+
424
+ it "supports grouping keys" do
425
+ parse("-c", "t,tw:foo,o:bar")[:c].should == {'three'=>'foo','two'=>'foo', 'one'=>'bar'}
426
+ end
427
+
428
+ it "aliases * to all keys" do
429
+ parse("-c", "*:foo")[:c].should == {'three'=>'foo', 'two'=>'foo', 'one'=>'foo'}
430
+ parse('-a', '*:foo')[:a].should == {'*'=>'foo'}
431
+ end
324
432
  end
325
433
 
326
434
  context "option with attributes" do
@@ -341,11 +449,11 @@ module Boson
341
449
  end
342
450
  end
343
451
 
452
+ def usage
453
+ @opt.formatted_usage.split(" ").sort
454
+ end
455
+
344
456
  context "#formatted_usage" do
345
- def usage
346
- @opt.formatted_usage.split(" ").sort
347
- end
348
-
349
457
  it "outputs string args with sample values" do
350
458
  create "--repo" => :string, "--branch" => "bugfix", "-n" => 6
351
459
  usage.should == %w([--branch=bugfix] [--repo=REPO] [-n=6])
@@ -360,6 +468,54 @@ module Boson
360
468
  create "--libs" => :array
361
469
  usage.should == ["[--libs=A,B,C]"]
362
470
  end
471
+
472
+ it "outputs hash args with sample value" do
473
+ create '--paths' => :hash
474
+ usage.should == ["[--paths=A:B,C:D]"]
475
+ end
476
+ end
477
+
478
+ context "user defined option class" do
479
+ before(:all) {
480
+ ::FooBoo = Struct.new(:name)
481
+ module ::Boson::Options::FooBoo
482
+ def create_foo_boo(value)
483
+ ::FooBoo.new(value)
484
+ end
485
+ def validate_foo_boo(value); end
486
+ end
487
+ ::Boson::OptionParser.send :include, ::Boson::Options::FooBoo
488
+ create :a=>:foo_boo, :b=>::FooBoo.new('blah'), :c=>:blah_blah,
489
+ :d=>{:type=>:foo_boo, :type=>::FooBoo.new('bling')}
490
+ }
491
+
492
+ test "created from symbol" do
493
+ (obj = parse('-a', 'whoop')[:a]).class.should == ::FooBoo
494
+ obj.name.should == 'whoop'
495
+ end
496
+
497
+ test "created from default" do
498
+ (obj = parse[:b]).class.should == ::FooBoo
499
+ obj.name.should == 'blah'
500
+ end
501
+
502
+ test "created from type attribute" do
503
+ (obj = parse('-d', 'whoop')[:d]).class.should == ::FooBoo
504
+ obj.name.should == 'whoop'
505
+ end
506
+
507
+ test "has its validation called" do
508
+ @opt.expects(:validate_foo_boo)
509
+ parse("-a", 'blah')
510
+ end
511
+
512
+ test "has default usage" do
513
+ usage[0].should == "[-a=:foo_boo]"
514
+ end
515
+
516
+ test "when nonexistant raises error" do
517
+ assert_error(OptionParser::Error, "invalid.*:blah_blah") { parse("-c", 'ok') }
518
+ end
363
519
  end
364
520
  end
365
- end
521
+ end