choosy 0.1.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 (42) hide show
  1. data/Gemfile +7 -0
  2. data/Gemfile.lock +25 -0
  3. data/LICENSE +23 -0
  4. data/README.markdown +393 -0
  5. data/Rakefile +57 -0
  6. data/lib/VERSION +1 -0
  7. data/lib/choosy/base_command.rb +59 -0
  8. data/lib/choosy/command.rb +32 -0
  9. data/lib/choosy/converter.rb +115 -0
  10. data/lib/choosy/dsl/base_command_builder.rb +172 -0
  11. data/lib/choosy/dsl/command_builder.rb +43 -0
  12. data/lib/choosy/dsl/option_builder.rb +155 -0
  13. data/lib/choosy/dsl/super_command_builder.rb +41 -0
  14. data/lib/choosy/errors.rb +11 -0
  15. data/lib/choosy/option.rb +22 -0
  16. data/lib/choosy/parse_result.rb +64 -0
  17. data/lib/choosy/parser.rb +184 -0
  18. data/lib/choosy/printing/color.rb +101 -0
  19. data/lib/choosy/printing/erb_printer.rb +23 -0
  20. data/lib/choosy/printing/help_printer.rb +174 -0
  21. data/lib/choosy/super_command.rb +77 -0
  22. data/lib/choosy/super_parser.rb +81 -0
  23. data/lib/choosy/verifier.rb +62 -0
  24. data/lib/choosy/version.rb +12 -0
  25. data/lib/choosy.rb +16 -0
  26. data/spec/choosy/base_command_spec.rb +11 -0
  27. data/spec/choosy/command_spec.rb +51 -0
  28. data/spec/choosy/converter_spec.rb +145 -0
  29. data/spec/choosy/dsl/base_command_builder_spec.rb +328 -0
  30. data/spec/choosy/dsl/commmand_builder_spec.rb +80 -0
  31. data/spec/choosy/dsl/option_builder_spec.rb +386 -0
  32. data/spec/choosy/dsl/super_command_builder_spec.rb +83 -0
  33. data/spec/choosy/parser_spec.rb +275 -0
  34. data/spec/choosy/printing/color_spec.rb +74 -0
  35. data/spec/choosy/printing/help_printer_spec.rb +117 -0
  36. data/spec/choosy/super_command_spec.rb +80 -0
  37. data/spec/choosy/super_parser_spec.rb +106 -0
  38. data/spec/choosy/verifier_spec.rb +180 -0
  39. data/spec/integration/command-A_spec.rb +37 -0
  40. data/spec/integration/supercommand-A_spec.rb +61 -0
  41. data/spec/spec_helpers.rb +30 -0
  42. metadata +150 -0
@@ -0,0 +1,115 @@
1
+ require 'choosy/errors'
2
+ require 'time'
3
+ require 'date'
4
+ require 'yaml'
5
+
6
+ module Choosy
7
+ class Converter
8
+ CONVERSIONS = {
9
+ :integer => nil,
10
+ :int => :integer,
11
+ :float => nil,
12
+ :symbol => nil,
13
+ :file => nil, # Succeeds only if a file is present
14
+ :yaml => nil, # Loads a YAML file, if present
15
+ :date => nil,
16
+ :time => nil,
17
+ :datetime => nil,
18
+ :string => nil,
19
+ :boolean => nil,
20
+ :bool => :boolean
21
+ }
22
+
23
+ def self.for(ty)
24
+ if ty.is_a?(Symbol) && CONVERSIONS.has_key?(ty)
25
+ val = CONVERSIONS[ty]
26
+ if val
27
+ return val
28
+ else
29
+ return ty
30
+ end
31
+ elsif ty.respond_to?(:convert)
32
+ return ty
33
+ end
34
+
35
+ nil
36
+ end
37
+
38
+ def self.convert(ty, value)
39
+ if CONVERSIONS.has_key?(ty)
40
+ send(ty, value)
41
+ else
42
+ ty.convert(value)
43
+ end
44
+ end
45
+
46
+ def self.boolean(value)
47
+ value # already set
48
+ end
49
+
50
+ def self.string(value)
51
+ value # already set
52
+ end
53
+
54
+ def self.integer(value)
55
+ begin
56
+ return Integer(value)
57
+ rescue ArgumentError
58
+ raise Choosy::ConversionError.new("Unable to convert '#{value}' into an integer")
59
+ end
60
+ end
61
+
62
+ def self.float(value)
63
+ begin
64
+ return Float(value)
65
+ rescue ArgumentError
66
+ raise Choosy::ConversionError.new("Unable to convert '#{value}' into a float")
67
+ end
68
+ end
69
+
70
+ def self.symbol(value)
71
+ value.to_sym
72
+ end
73
+
74
+ def self.file(value)
75
+ if File.exist?(value)
76
+ File.new(value)
77
+ else
78
+ raise Choosy::ValidationError.new("Unable to locate file: '#{value}'")
79
+ end
80
+ end
81
+
82
+ def self.yaml(value)
83
+ fd = file(value)
84
+ begin
85
+ return YAML::load_file(fd.path)
86
+ rescue Error
87
+ raise Choosy::ConversionError.new("Unable to load YAML from file: '#{value}'")
88
+ end
89
+ end
90
+
91
+ def self.date(value)
92
+ begin
93
+ return Date.parse(value)
94
+ rescue ArgumentError
95
+ raise Choosy::ConversionError.new("Unable to convert '#{value}' into a date")
96
+ end
97
+ end
98
+
99
+ def self.time(value)
100
+ begin
101
+ return Time.parse(value)
102
+ rescue ArgumentError
103
+ raise Choosy::ConversionError.new("Unable to convert '#{value}' into a time")
104
+ end
105
+ end
106
+
107
+ def self.datetime(value)
108
+ begin
109
+ return DateTime.parse(value)
110
+ rescue ArgumentError
111
+ raise Choosy::ConversionError.new("Unable to convert '#{value}' into datetime")
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,172 @@
1
+ require 'choosy/errors'
2
+ require 'choosy/dsl/option_builder'
3
+ require 'choosy/printing/erb_printer'
4
+
5
+ module Choosy::DSL
6
+ class BaseCommandBuilder
7
+ attr_reader :command
8
+
9
+ def initialize(command)
10
+ @command = command
11
+ end
12
+
13
+ def summary(msg)
14
+ @command.summary = msg
15
+ end
16
+
17
+ def printer(kind, options=nil)
18
+ return if kind.nil?
19
+
20
+ p = nil
21
+ if kind == :standard
22
+ p = Choosy::Printing::HelpPrinter.new
23
+ elsif kind == :erb
24
+ p = Choosy::Printing::ERBPrinter.new
25
+ if options.nil? || options[:template].nil?
26
+ raise Choosy::ConfigurationError.new("no template file given to ERBPrinter")
27
+ elsif !File.exist?(options[:template])
28
+ raise Choosy::ConfigurationError.new("the template file doesn't exist: #{options[:template]}")
29
+ end
30
+ p.template = options[:template]
31
+ elsif kind.respond_to?(:print!)
32
+ p = kind
33
+ else
34
+ raise Choosy::ConfigurationError.new("Unknown printing method for help: #{kind}")
35
+ end
36
+
37
+ if p.respond_to?(:color) && options && options.has_key?(:color)
38
+ p.color.disable! if !options[:color]
39
+ end
40
+
41
+ @command.printer = p
42
+ end
43
+
44
+ def desc(msg)
45
+ @command.description = msg
46
+ end
47
+
48
+ def separator(msg=nil)
49
+ @command.listing << (msg.nil? ? "" : msg)
50
+ end
51
+
52
+ def option(arg)
53
+ raise Choosy::ConfigurationError.new("The option name was nil") if arg.nil?
54
+
55
+ builder = nil
56
+
57
+ if arg.is_a?(Hash)
58
+ raise Choosy::ConfigurationError.new("Malformed option hash") if arg.count != 1
59
+ name = arg.keys[0]
60
+ builder = OptionBuilder.new(name)
61
+
62
+ to_process = arg[name]
63
+ if to_process.is_a?(Array)
64
+ builder.dependencies to_process
65
+ elsif to_process.is_a?(Hash)
66
+ builder.from_hash to_process
67
+ else
68
+ raise Choosy::ConfigurationError.new("Unable to process option hash")
69
+ end
70
+ else
71
+ builder = OptionBuilder.new(arg)
72
+ raise Choosy::ConfigurationError.new("No configuration block was given") if !block_given?
73
+ end
74
+
75
+ yield builder if block_given?
76
+ finalize_option_builder builder
77
+ end
78
+
79
+ # Option types
80
+ def self.create_conversions
81
+ Choosy::Converter::CONVERSIONS.keys.each do |method|
82
+ next if method == :boolean || method == :bool
83
+
84
+ define_method method do |sym, desc, config=nil, &block|
85
+ simple_option(sym, desc, true, :one, method, config, &block)
86
+ end
87
+
88
+ plural = "#{method}s".to_sym
89
+ define_method plural do |sym, desc, config=nil, &block|
90
+ simple_option(sym, desc, true, :many, method, config, &block)
91
+ end
92
+
93
+ underscore = "#{method}_"
94
+ define_method underscore do |sym, desc, config=nil, &block|
95
+ simple_option(sym, desc, false, :one, method, config, &block)
96
+ end
97
+
98
+ plural_underscore = "#{plural}_".to_sym
99
+ define_method plural_underscore do |sym, desc, config=nil, &block|
100
+ simple_option(sym, desc, false, :many, method, config, &block)
101
+ end
102
+ end
103
+ end
104
+
105
+ create_conversions
106
+ alias :single :string
107
+ alias :single_ :string_
108
+
109
+ alias :multiple :strings
110
+ alias :multiple_ :strings_
111
+
112
+ def boolean(sym, desc, config=nil, &block)
113
+ simple_option(sym, desc, true, :zero, :boolean, config, &block)
114
+ end
115
+ def boolean_(sym, desc, config=nil, &block)
116
+ simple_option(sym, desc, false, :zero, :boolean, config, &block)
117
+ end
118
+ alias :bool :boolean
119
+ alias :bool_ :boolean_
120
+
121
+ def version(msg)
122
+ v = OptionBuilder.new(OptionBuilder::VERSION)
123
+ v.long '--version'
124
+ v.desc "The version number"
125
+
126
+ v.validate do
127
+ raise Choosy::VersionCalled.new(msg)
128
+ end
129
+
130
+ yield v if block_given?
131
+ finalize_option_builder v
132
+ end
133
+
134
+ def finalize!
135
+ if @command.printer.nil?
136
+ printer :standard
137
+ end
138
+ end
139
+
140
+ protected
141
+ def finalize_option_builder(option_builder)
142
+ option_builder.finalize!
143
+ @command.option_builders[option_builder.option.name] = option_builder
144
+ @command.listing << option_builder.option
145
+
146
+ option_builder.option
147
+ end
148
+
149
+ private
150
+ def simple_option(sym, desc, allow_short, param, cast, config, &block)
151
+ name = sym.to_s
152
+ builder = OptionBuilder.new sym
153
+ builder.desc desc
154
+ builder.short "-#{name[0]}" if allow_short
155
+ builder.long "--#{name.downcase.gsub(/_/, '-')}"
156
+ builder.param format_param(name, param)
157
+ builder.cast cast
158
+ builder.from_hash config if config
159
+
160
+ yield builder if block_given?
161
+ finalize_option_builder builder
162
+ end
163
+
164
+ def format_param(name, count)
165
+ case count
166
+ when :zero then nil
167
+ when :one then name.upcase
168
+ when :many then "#{name.upcase}+"
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,43 @@
1
+ require 'choosy/errors'
2
+ require 'choosy/dsl/base_command_builder'
3
+ require 'choosy/dsl/option_builder'
4
+ require 'choosy/printing/help_printer'
5
+
6
+ module Choosy::DSL
7
+ class CommandBuilder < BaseCommandBuilder
8
+ def executor(exec=nil, &block)
9
+ if exec.nil?
10
+ if block_given?
11
+ @command.executor = block
12
+ else
13
+ raise Choosy::ConfigurationError.new("The executor was nil")
14
+ end
15
+ else
16
+ if !exec.respond_to?(:execute!)
17
+ raise Choosy::ConfigurationError.new("Execution class doesn't implement 'execute!'")
18
+ end
19
+ @command.executor = exec
20
+ end
21
+ end
22
+
23
+ def help(msg=nil)
24
+ h = OptionBuilder.new(OptionBuilder::HELP)
25
+ h.short '-h'
26
+ h.long '--help'
27
+ msg ||= "Show this help message"
28
+ h.desc msg
29
+
30
+ h.validate do
31
+ raise Choosy::HelpCalled.new(@name)
32
+ end
33
+
34
+ finalize_option_builder h
35
+ end
36
+
37
+ def arguments(&block)
38
+ raise Choosy::ConfigurationError.new("No block to arguments call") if !block_given?
39
+
40
+ command.argument_validation = block
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,155 @@
1
+ require 'choosy/option'
2
+ require 'choosy/errors'
3
+ require 'choosy/converter'
4
+
5
+ module Choosy::DSL
6
+ class OptionBuilder
7
+ HELP = :__help__
8
+ VERSION = :__version__
9
+
10
+ ZERO_ARITY = (0 .. 0)
11
+ ONE_ARITY = (1 .. 1)
12
+ MANY_ARITY = (1 .. 1000)
13
+
14
+ attr_reader :option
15
+
16
+ def initialize(name)
17
+ @option = Choosy::Option.new(name)
18
+ @count_called = false
19
+ end
20
+
21
+ def short(flag, param=nil)
22
+ option.short_flag = flag
23
+ param(param)
24
+ end
25
+
26
+ def long(flag, param=nil)
27
+ option.long_flag = flag
28
+ param(param)
29
+ end
30
+
31
+ def flags(shorter, longer=nil, parameter=nil)
32
+ short(shorter)
33
+ long(longer) if longer
34
+ param(parameter) if parameter
35
+ end
36
+
37
+ def desc(description)
38
+ option.description = description
39
+ end
40
+
41
+ def default(value)
42
+ option.default_value = value
43
+ end
44
+
45
+ def required(value=nil)
46
+ option.required = if value.nil? || value == true
47
+ true
48
+ else
49
+ false
50
+ end
51
+ end
52
+
53
+ def param(param)
54
+ return if param.nil?
55
+ option.flag_parameter = param
56
+ return if @count_called
57
+
58
+ if param =~ /\+$/
59
+ option.arity = MANY_ARITY
60
+ else
61
+ option.arity = ONE_ARITY
62
+ end
63
+ end
64
+
65
+ def count(restriction)
66
+ @count_called = true
67
+ if restriction.is_a?(Hash)
68
+ lower_bound = restriction[:at_least] || restriction[:exactly] || 1
69
+ upper_bound = restriction[:at_most] || restriction[:exactly] || 1000
70
+
71
+ check_count(lower_bound)
72
+ check_count(upper_bound)
73
+ if lower_bound > upper_bound
74
+ raise Choosy::ConfigurationError.new("The upper bound (#{upper_bound}) is less than the lower bound (#{lower_bound}).")
75
+ end
76
+
77
+ option.arity = (lower_bound .. upper_bound)
78
+ elsif restriction == :zero || restriction == :none
79
+ option.arity = ZERO_ARITY
80
+ elsif restriction == :once
81
+ option.arity = ONE_ARITY
82
+ else
83
+ check_count(restriction)
84
+ option.arity = (restriction .. restriction)
85
+ end
86
+ end
87
+
88
+ def cast(ty)
89
+ option.cast_to = Choosy::Converter.for(ty)
90
+ if option.cast_to.nil?
91
+ raise Choosy::ConfigurationError.new("Unknown conversion cast: #{ty}")
92
+ end
93
+ end
94
+
95
+ def validate(&block)
96
+ option.validation_step = block
97
+ end
98
+
99
+ def fail(msg)
100
+ flag_fmt = if option.short_flag && option.long_flag
101
+ "#{option.short_flag}/#{option.long_flag}"
102
+ end
103
+ flag_fmt ||= option.short_flag || option.long_flag
104
+ flag_param = if option.flag_parameter
105
+ " #{option.flag_parameter}"
106
+ end
107
+ raise Choosy::ValidationError.new("#{flag_fmt}#{flag_param}: #{msg}")
108
+ end
109
+
110
+ def dependencies(*args)
111
+ if args.count == 1 && args[0].is_a?(Array)
112
+ option.dependent_options = args[0]
113
+ else
114
+ option.dependent_options = args
115
+ end
116
+ end
117
+
118
+ def from_hash(hash)
119
+ raise Choosy::ConfigurationError.new("Only hash arguments allowed") if !hash.is_a?(Hash)
120
+
121
+ hash.each do |k, v|
122
+ if respond_to?(k)
123
+ if v.is_a?(Array)
124
+ self.send(k, *v)
125
+ else
126
+ self.send(k, v)
127
+ end
128
+ else
129
+ raise Choosy::ConfigurationError.new("Not a recognized option: #{k}")
130
+ end
131
+ end
132
+ end
133
+
134
+ def finalize!
135
+ if option.arity.nil?
136
+ option.arity = ZERO_ARITY
137
+ end
138
+
139
+ if option.cast_to.nil?
140
+ if option.arity == ZERO_ARITY
141
+ option.cast_to = :boolean
142
+ else
143
+ option.cast_to = :string
144
+ end
145
+ end
146
+ end
147
+
148
+ private
149
+ def check_count(count)
150
+ if !count.is_a?(Integer)
151
+ raise Choosy::ConfigurationError.new("Expected a number to count, got '#{count}'")
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,41 @@
1
+ require 'choosy/errors'
2
+ require 'choosy/dsl/base_command_builder'
3
+ require 'choosy/command'
4
+
5
+ module Choosy::DSL
6
+ class SuperCommandBuilder < BaseCommandBuilder
7
+ def command(cmd)
8
+ subcommand = if cmd.is_a?(Choosy::Command)
9
+ cmd
10
+ else
11
+ Choosy::Command.new(cmd)
12
+ end
13
+ yield subcommand.builder if block_given?
14
+ finalize_subcommand(subcommand)
15
+ end
16
+
17
+ def help(msg=nil)
18
+ msg ||= "Show the info for a command, or this message"
19
+ help = Choosy::Command.new :help do |help|
20
+ help.summary msg
21
+
22
+ help.arguments do |args|
23
+ if args.nil? || args.length == 0
24
+ raise Choosy::HelpCalled.new(@command.name)
25
+ else
26
+ raise Choosy::HelpCalled.new(args[0].to_sym)
27
+ end
28
+ end
29
+ end
30
+ finalize_subcommand(help)
31
+ end
32
+
33
+ private
34
+ def finalize_subcommand(subcommand)
35
+ subcommand.builder.finalize!
36
+ @command.command_builders[subcommand.name] = subcommand.builder
37
+ @command.listing << subcommand
38
+ subcommand
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module Choosy
2
+ Error = Class.new(RuntimeError)
3
+
4
+ ConfigurationError = Class.new(Choosy::Error)
5
+ ValidationError = Class.new(Choosy::Error)
6
+ HelpCalled = Class.new(Choosy::Error)
7
+ VersionCalled = Class.new(Choosy::Error)
8
+ ConversionError = Class.new(Choosy::Error)
9
+ ParseError = Class.new(Choosy::Error)
10
+ SuperParseError = Class.new(Choosy::Error)
11
+ end
@@ -0,0 +1,22 @@
1
+ module Choosy
2
+ class Option
3
+ attr_accessor :name, :description
4
+ attr_accessor :short_flag, :long_flag, :flag_parameter
5
+ attr_accessor :cast_to, :default_value
6
+ attr_accessor :validation_step
7
+ attr_accessor :arity
8
+ attr_accessor :dependent_options
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ @required = false
13
+ end
14
+
15
+ def required=(req)
16
+ @required = req
17
+ end
18
+ def required?
19
+ @required == true
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,64 @@
1
+ require 'choosy/verifier'
2
+
3
+ module Choosy
4
+ class BaseParseResult
5
+ attr_reader :command, :options, :unparsed
6
+
7
+ def initialize(command)
8
+ @command = command
9
+ @options = {}
10
+ @unparsed = []
11
+ @verified = false
12
+ end
13
+
14
+ def [](opt)
15
+ @options[opt]
16
+ end
17
+
18
+ def []=(opt, val)
19
+ @options[opt] = val
20
+ end
21
+
22
+ def verified?
23
+ @verified
24
+ end
25
+
26
+ def verify!
27
+ basic_verification
28
+ end
29
+
30
+ protected
31
+ def basic_verification(&block)
32
+ verifier = Verifier.new
33
+ verifier.verify_options!(self)
34
+ yield verifier if block_given?
35
+ @verified = true
36
+ self
37
+ end
38
+ end
39
+
40
+ class ParseResult < BaseParseResult
41
+ attr_reader :args
42
+
43
+ def initialize(command)
44
+ super(command)
45
+ @args = []
46
+ end
47
+
48
+ def verify!
49
+ return self if verified?
50
+ basic_verification do |verifier|
51
+ verifier.verify_arguments!(self)
52
+ end
53
+ end
54
+ end
55
+
56
+ class SuperParseResult < BaseParseResult
57
+ attr_reader :subresults
58
+
59
+ def initialize(command)
60
+ super(command)
61
+ @subresults = []
62
+ end
63
+ end
64
+ end