caty 0.0.1
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.
- data/HISTORY.markdown +7 -0
- data/LICENSE.txt +22 -0
- data/README.markdown +92 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/caty.gemspec +73 -0
- data/lib/caty.rb +341 -0
- data/lib/caty/converters.rb +101 -0
- data/lib/caty/errors.rb +17 -0
- data/lib/caty/global_option.rb +40 -0
- data/lib/caty/has_description.rb +26 -0
- data/lib/caty/help_system.rb +107 -0
- data/lib/caty/helpers.rb +19 -0
- data/lib/caty/indirection.rb +26 -0
- data/lib/caty/option.rb +100 -0
- data/lib/caty/option_array.rb +30 -0
- data/lib/caty/option_constructor.rb +89 -0
- data/lib/caty/task.rb +74 -0
- data/lib/caty/task_hash.rb +75 -0
- data/test/kitty.rb +87 -0
- data/test/test_caty.rb +100 -0
- data/test/test_option.rb +81 -0
- data/test/test_option_array.rb +25 -0
- data/test/test_structure.rb +72 -0
- data/test/test_task.rb +28 -0
- metadata +94 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
#
|
2
|
+
# Contains the OptionArray class.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'delegate'
|
6
|
+
require 'ohash'
|
7
|
+
|
8
|
+
#
|
9
|
+
# Represents an array of options and adds some
|
10
|
+
# useful methods.
|
11
|
+
#
|
12
|
+
class Caty::OptionArray < Array
|
13
|
+
|
14
|
+
include Caty::Helpers
|
15
|
+
|
16
|
+
#
|
17
|
+
# Forwards the ::grep!() to the different Options
|
18
|
+
# and collects the results into an OpenHash.
|
19
|
+
#
|
20
|
+
def grep!( args )
|
21
|
+
returning(OpenHash.new) do |hash|
|
22
|
+
self.each do |option|
|
23
|
+
grab = option.grep!(args)
|
24
|
+
hash[option.name.to_sym] = grab unless grab.nil?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
#
|
2
|
+
# Contains the OptionConstructor class.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Constructs options using a DSL, e.g:
|
7
|
+
#
|
8
|
+
# constructor.construct do
|
9
|
+
# desc('description')
|
10
|
+
# string :option, 'default'
|
11
|
+
#
|
12
|
+
# desc('desc2')
|
13
|
+
# integer :nother_one
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Used by Caty when Caty::global_options() is
|
17
|
+
# called.
|
18
|
+
#
|
19
|
+
class Caty::OptionConstructor
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
#
|
24
|
+
# Returns an array of all the option types
|
25
|
+
# registered.
|
26
|
+
#
|
27
|
+
def types
|
28
|
+
@types ||= Array.new
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Registers an option type (symbol) with the
|
33
|
+
# constructor.
|
34
|
+
#
|
35
|
+
def register( type )
|
36
|
+
@types ||= Array.new
|
37
|
+
@types << type
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Creates a new OptionConstructor that constructs
|
44
|
+
# the given options of the given class.
|
45
|
+
#
|
46
|
+
def initialize( option_klass )
|
47
|
+
@klass = option_klass
|
48
|
+
@options = Caty::OptionArray.new
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Constructs the actual options according to the
|
53
|
+
# given block.
|
54
|
+
#
|
55
|
+
def construct( &block )
|
56
|
+
self.instance_eval(&block)
|
57
|
+
@options
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
#
|
63
|
+
# Emulates the type methods, e.g:
|
64
|
+
#
|
65
|
+
# string :name, 'default'
|
66
|
+
#
|
67
|
+
def method_missing( meth, *args, &block )
|
68
|
+
if self.class.types.include?(meth) and args.length.between?(1,2)
|
69
|
+
name = args[0]
|
70
|
+
default = args.length == 2 ?
|
71
|
+
args[1] : meth
|
72
|
+
option = @klass.new(name, default)
|
73
|
+
option.description, @description =
|
74
|
+
@description, nil
|
75
|
+
@options << option
|
76
|
+
else
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Allows you to describe the constructed options.
|
83
|
+
#
|
84
|
+
def desc( description )
|
85
|
+
@description = description
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
data/lib/caty/task.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#
|
2
|
+
# Contains the Task class.
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Represents a single task.
|
7
|
+
#
|
8
|
+
# A Task object is created for every public method created
|
9
|
+
# in the Caty subclass.
|
10
|
+
#
|
11
|
+
class Caty::Task
|
12
|
+
|
13
|
+
include Caty::Helpers
|
14
|
+
include Caty::HasDescription
|
15
|
+
|
16
|
+
attr_accessor :name, :usage
|
17
|
+
|
18
|
+
#
|
19
|
+
# Creates a new task with the given name for the
|
20
|
+
# given instance_method.
|
21
|
+
#
|
22
|
+
def initialize( name, instance_method, options )
|
23
|
+
@name, @instance_method, @options =
|
24
|
+
name, instance_method, options
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Tries to remove the options defined for this task
|
29
|
+
# from the args array.
|
30
|
+
#
|
31
|
+
# Returns an OpenHash containing the retrieved
|
32
|
+
# options.
|
33
|
+
#
|
34
|
+
def parse!( args )
|
35
|
+
@args = args
|
36
|
+
@options.grep!(args)
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Executes the associated instance_method by binding it
|
41
|
+
# to the given _context_.
|
42
|
+
#
|
43
|
+
def execute( context )
|
44
|
+
@instance_method.bind(context).call(*@args)
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Resolving end point.
|
49
|
+
# See Caty::Indirection#resolve() for more information.
|
50
|
+
#
|
51
|
+
def resolve( task_hash )
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Returns a string representation of the task and its
|
57
|
+
# options to be used by the help system.
|
58
|
+
#
|
59
|
+
def to_help
|
60
|
+
[ self.to_s , self.short_description, self.description ]
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
returning(@name.to_s) do |output|
|
65
|
+
output << " #{@usage}" unless @usage.nil?
|
66
|
+
|
67
|
+
@options.each do |option|
|
68
|
+
output << " #{option}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
#
|
2
|
+
# Contains the TaskHash class.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'delegate'
|
6
|
+
|
7
|
+
#
|
8
|
+
# A hash that preserves the order of task insertion.
|
9
|
+
#
|
10
|
+
class Caty::TaskHash < DelegateClass(Hash)
|
11
|
+
|
12
|
+
#
|
13
|
+
# Works like Hash#initialize.
|
14
|
+
#
|
15
|
+
def initialize( *args )
|
16
|
+
@ary = Array.new
|
17
|
+
@hash = Hash.new(*args)
|
18
|
+
super(@hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Works like Hash#[].
|
23
|
+
#
|
24
|
+
def []( num_or_key )
|
25
|
+
case num_or_key
|
26
|
+
when Numeric, Range then @ary[num_or_key]
|
27
|
+
else @hash[num_or_key]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Works like Hash#[]=.
|
33
|
+
#
|
34
|
+
def []=( key, value )
|
35
|
+
index = @ary.find { |item| item == @hash[key] }
|
36
|
+
|
37
|
+
if @hash[key].nil? or index.nil?
|
38
|
+
@ary << value
|
39
|
+
else
|
40
|
+
@ary[index] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
@hash[key] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Follows indirections until the task behind
|
48
|
+
# the given task name is discovered.
|
49
|
+
#
|
50
|
+
def resolve( task_name )
|
51
|
+
task = self[task_name]
|
52
|
+
|
53
|
+
if task.nil? then nil
|
54
|
+
else task.resolve(self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Returns a copy of the task hash as an array.
|
60
|
+
# The order of the items in the array is the same as in the
|
61
|
+
# task hash.
|
62
|
+
#
|
63
|
+
def to_a
|
64
|
+
@ary.dup
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Returns a copy of the task hash as a plain hash.
|
69
|
+
#
|
70
|
+
def to_h
|
71
|
+
@hash.dup
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
data/test/kitty.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'caty'
|
4
|
+
|
5
|
+
#
|
6
|
+
# A Kitty.
|
7
|
+
#
|
8
|
+
class Kitty < Caty
|
9
|
+
|
10
|
+
global_options do
|
11
|
+
|
12
|
+
#
|
13
|
+
# It's trained not to meow if you tell it to be quiet...
|
14
|
+
#
|
15
|
+
desc('No meowing.')
|
16
|
+
boolean :quiet, false
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# It needs to eat sometimes.
|
22
|
+
#
|
23
|
+
desc('SOMETHING', cut("
|
24
|
+
Eats SOMETHING.
|
25
|
+
You can give the kitty any -amount of food.
|
26
|
+
"))
|
27
|
+
task_options :amount => 1
|
28
|
+
|
29
|
+
def eat( something )
|
30
|
+
puts "The kitty eats #{task_options.amount} #{something}!"
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# By default the kitty just meows at you.
|
35
|
+
#
|
36
|
+
def meow
|
37
|
+
puts 'meow!'
|
38
|
+
end
|
39
|
+
|
40
|
+
default :meow
|
41
|
+
|
42
|
+
#
|
43
|
+
# It likes to meow a lot.
|
44
|
+
#
|
45
|
+
before do |task|
|
46
|
+
puts 'meow!' unless global_options.quiet or task == :help
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# And it tells you when it's done.
|
51
|
+
#
|
52
|
+
after do |task|
|
53
|
+
puts 'purrr...' unless task == :help
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# If you ask it politely, it will tell you what it can do.
|
58
|
+
#
|
59
|
+
help_task
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# You could put this in a separate file and require it here
|
65
|
+
# instead, thus separating any tasks from the others and
|
66
|
+
# reducing interdependencies
|
67
|
+
#
|
68
|
+
Kitty.append do
|
69
|
+
|
70
|
+
#
|
71
|
+
# It can be sleepy...
|
72
|
+
# And doze away...
|
73
|
+
#
|
74
|
+
desc("Sleeps a bit.")
|
75
|
+
map :doze => :sleep
|
76
|
+
|
77
|
+
def sleep
|
78
|
+
puts 'The kitty sleeps...'
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Pushes the kitty's on/off switch ;-)
|
85
|
+
#
|
86
|
+
Kitty.start!
|
87
|
+
|
data/test/test_caty.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bacon'
|
3
|
+
require 'facon'
|
4
|
+
require 'caty'
|
5
|
+
|
6
|
+
describe 'Caty' do
|
7
|
+
|
8
|
+
DEFAULT_TESTER = mock('default')
|
9
|
+
|
10
|
+
class CatyTest < Caty
|
11
|
+
|
12
|
+
default :beer
|
13
|
+
map 'lager' => :beer
|
14
|
+
|
15
|
+
def beer arg = DEFAULT_TESTER
|
16
|
+
arg.beer
|
17
|
+
end
|
18
|
+
|
19
|
+
def energy_drink
|
20
|
+
party(42) # raises an argument error
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def punch arg
|
26
|
+
arg.punch
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def wine arg
|
32
|
+
arg.wine
|
33
|
+
end
|
34
|
+
|
35
|
+
def party
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
def suppress_output
|
41
|
+
output = $stdout = StringIO.new
|
42
|
+
yield
|
43
|
+
return output.string
|
44
|
+
ensure
|
45
|
+
$stdout = STDOUT
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should pass arguments' do
|
49
|
+
tester = mock('beer')
|
50
|
+
tester.should.receive(:beer)
|
51
|
+
CatyTest.start!(['beer', tester])
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should handle mappings' do
|
55
|
+
tester = mock('lager')
|
56
|
+
tester.should.receive(:beer)
|
57
|
+
CatyTest.start!(['lager', tester])
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should handle default task invocation' do
|
61
|
+
DEFAULT_TESTER.should.receive(:beer)
|
62
|
+
CatyTest.start!([])
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should not invoke private or protected methods' do
|
66
|
+
suppress_output do
|
67
|
+
tester = mock('punch')
|
68
|
+
tester.should.not.receive(:punch)
|
69
|
+
CatyTest.start!(['punch', tester])
|
70
|
+
|
71
|
+
tester = mock('wine')
|
72
|
+
tester.should.not.receive(:wine)
|
73
|
+
CatyTest.start!(['wine', tester])
|
74
|
+
end.should.not.be.empty
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should catch task argument errors' do
|
78
|
+
suppress_output do
|
79
|
+
lambda {
|
80
|
+
CatyTest.start!(%w{beer and lemonade})
|
81
|
+
}.should.not.raise ArgumentError
|
82
|
+
end.should.not.be.empty
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should not catch other argument errors' do
|
86
|
+
suppress_output do
|
87
|
+
lambda {
|
88
|
+
CatyTest.start!(%w{energy_drink})
|
89
|
+
}.should.raise ArgumentError
|
90
|
+
end.should.be.empty
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should handle unknown tasks' do
|
94
|
+
suppress_output do
|
95
|
+
CatyTest.start!(%w{salad})
|
96
|
+
end.should.not.be.empty
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|