escort 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +8 -0
  3. data/README.md +365 -14
  4. data/TODO.md +157 -44
  5. data/escort.gemspec +1 -0
  6. data/examples/{1_1_basic.rb → attic/1_1_basic.rb} +0 -0
  7. data/examples/{1_2_basic_requires_arguments.rb → attic/1_2_basic_requires_arguments.rb} +0 -0
  8. data/examples/{2_2_command.rb → attic/2_2_command.rb} +0 -0
  9. data/examples/{2_2_command_requires_arguments.rb → attic/2_2_command_requires_arguments.rb} +0 -0
  10. data/examples/{2_3_nested_commands.rb → attic/2_3_nested_commands.rb} +0 -0
  11. data/examples/{3_validations.rb → attic/3_validations.rb} +0 -0
  12. data/examples/{4_1_config_file.rb → attic/4_1_config_file.rb} +0 -0
  13. data/examples/{argument_handling → attic/argument_handling}/basic.rb +0 -0
  14. data/examples/{argument_handling → attic/argument_handling}/basic_command.rb +0 -0
  15. data/examples/{argument_handling → attic/argument_handling}/no_arguments.rb +0 -0
  16. data/examples/{argument_handling → attic/argument_handling}/no_arguments_command.rb +0 -0
  17. data/examples/{command_aliases → attic/command_aliases}/app.rb +0 -0
  18. data/examples/{config_file → attic/config_file}/.apprc2 +0 -0
  19. data/examples/{config_file → attic/config_file}/app.rb +0 -0
  20. data/examples/{config_file → attic/config_file}/sub_commands.rb +0 -0
  21. data/examples/{default_command → attic/default_command}/app.rb +0 -0
  22. data/examples/{sub_commands → attic/sub_commands}/app.rb +0 -0
  23. data/examples/{validation_basic → attic/validation_basic}/app.rb +0 -0
  24. data/examples/basic +10 -0
  25. data/examples/basic_conflicts +17 -0
  26. data/examples/basic_depends_on +25 -0
  27. data/examples/basic_flags +15 -0
  28. data/examples/basic_options +14 -0
  29. data/examples/basic_options_multi +15 -0
  30. data/examples/basic_require_arguments +17 -0
  31. data/examples/basic_texts +21 -0
  32. data/examples/basic_validations +21 -0
  33. data/examples/command +19 -0
  34. data/examples/commands/example_command.rb +13 -0
  35. data/lib/escort/action_command/base.rb +4 -0
  36. data/lib/escort/app.rb +33 -11
  37. data/lib/escort/formatter/borderless_table.rb +4 -0
  38. data/lib/escort/formatter/command.rb +87 -0
  39. data/lib/escort/formatter/commands.rb +36 -0
  40. data/lib/escort/formatter/default_help_formatter.rb +68 -73
  41. data/lib/escort/formatter/global_command.rb +17 -0
  42. data/lib/escort/formatter/option.rb +138 -0
  43. data/lib/escort/formatter/options.rb +17 -3
  44. data/lib/escort/formatter/shell_command_executor.rb +49 -0
  45. data/lib/escort/formatter/terminal.rb +17 -9
  46. data/lib/escort/formatter/terminal_formatter.rb +6 -0
  47. data/lib/escort/logger.rb +4 -4
  48. data/lib/escort/option_dependency_validator.rb +83 -0
  49. data/lib/escort/option_parser.rb +11 -1
  50. data/lib/escort/setup/configuration/reader.rb +0 -2
  51. data/lib/escort/setup/configuration/writer.rb +0 -2
  52. data/lib/escort/setup/dsl/command.rb +2 -7
  53. data/lib/escort/setup/dsl/global.rb +2 -9
  54. data/lib/escort/setup/dsl/options.rb +56 -0
  55. data/lib/escort/setup_accessor.rb +23 -6
  56. data/lib/escort/trollop.rb +4 -3
  57. data/lib/escort/validator.rb +4 -1
  58. data/lib/escort/version.rb +1 -1
  59. data/lib/escort.rb +8 -1
  60. data/spec/integration/basic_conflicts_spec.rb +47 -0
  61. data/spec/integration/basic_depends_on_spec.rb +275 -0
  62. data/spec/integration/basic_options_spec.rb +9 -21
  63. data/spec/integration/basic_options_with_multi_spec.rb +30 -0
  64. data/spec/integration/basic_spec.rb +5 -6
  65. data/spec/integration/basic_validations_spec.rb +77 -0
  66. data/spec/integration/basic_with_arguments_spec.rb +33 -0
  67. data/spec/integration/basic_with_text_fields_spec.rb +21 -0
  68. data/spec/lib/escort/formatter/command_spec.rb +238 -0
  69. data/spec/lib/escort/formatter/global_command_spec.rb +50 -0
  70. data/spec/lib/escort/formatter/option_spec.rb +300 -0
  71. data/spec/lib/escort/formatter/shell_command_executor_spec.rb +59 -0
  72. data/spec/lib/escort/formatter/string_splitter_spec.rb +12 -0
  73. data/spec/lib/escort/formatter/terminal_spec.rb +19 -0
  74. data/spec/lib/escort/setup_accessor_spec.rb +1 -0
  75. data/spec/spec_helper.rb +9 -3
  76. data/spec/support/integration_helpers.rb +2 -0
  77. data/spec/{helpers/execute_action_matcher.rb → support/matchers/execute_action_for_command_matcher.rb} +3 -3
  78. data/spec/support/matchers/execute_action_with_arguments_matcher.rb +25 -0
  79. data/spec/support/matchers/execute_action_with_options_matcher.rb +29 -0
  80. data/spec/support/matchers/exit_with_code_matcher.rb +29 -0
  81. data/spec/support/shared_contexts/integration_setup.rb +34 -0
  82. metadata +86 -28
  83. data/examples/basic/app.rb +0 -16
  84. data/lib/escort/formatter/common.rb +0 -58
  85. data/spec/helpers/exit_with_code_matcher.rb +0 -21
  86. data/spec/helpers/give_option_to_action_with_value_matcher.rb +0 -22
@@ -0,0 +1,83 @@
1
+ module Escort
2
+ class OptionDependencyValidator
3
+ class << self
4
+ def for(parser)
5
+ self.new(parser)
6
+ end
7
+ end
8
+
9
+ attr_reader :parser
10
+
11
+ def initialize(parser)
12
+ @parser = parser
13
+ end
14
+
15
+ def validate(options, dependencies)
16
+ dependencies.each_pair do |option_name, dependency_rules|
17
+ ensure_dependencies_satisfied_for(option_name, dependency_rules, options)
18
+ end
19
+ options
20
+ end
21
+
22
+ private
23
+
24
+ def option_exists?(option)
25
+ parser.specs.keys.include?(option)
26
+ end
27
+
28
+ def ensure_dependencies_satisfied_for(option_name, dependency_rules, options)
29
+ ensure_dependency_for_valid_option(option_name)
30
+ dependency_rules.each do |rule|
31
+ case rule
32
+ when Hash
33
+ handle_all_option_value_dependency_rules(option_name, rule, options)
34
+ else
35
+ ensure_option_depends_on_valid_option(option_name, rule)
36
+ handle_possible_presence_dependency_issue(option_name, rule, options)
37
+ end
38
+ end
39
+ end
40
+
41
+ def handle_all_option_value_dependency_rules(option_name, rule, options)
42
+ if option_was_specified?(option_name, options)
43
+ rule.each_pair do |rule_option, rule_option_value|
44
+ ensure_option_depends_on_valid_option(option_name, rule_option)
45
+ handle_possible_option_value_dependency_issue(option_name, rule_option, rule_option_value, options)
46
+ end
47
+ end
48
+ end
49
+
50
+ def handle_possible_option_value_dependency_issue(option_name, rule_option, rule_option_value, options)
51
+ unless options[rule_option] == rule_option_value
52
+ raise Escort::UserError.new("Option dependency unsatisfied, '#{option_name}' depends on '#{rule_option}' having value '#{rule_option_value}', '#{option_name}' specified with value '#{options[option_name]}', but '#{rule_option}' is '#{options[rule_option]}'")
53
+ end
54
+ end
55
+
56
+ def handle_possible_presence_dependency_issue(option_name, rule, options)
57
+ if option_was_specified?(option_name, options) && option_was_unspecified?(rule, options)
58
+ raise Escort::UserError.new("Option dependency unsatisfied, '#{option_name}' depends on '#{rule}', '#{option_name}' specified with value '#{options[option_name]}', but '#{rule}' is unspecified")
59
+ end
60
+ end
61
+
62
+ def option_was_unspecified?(option_name, options)
63
+ !option_was_specified?(option_name, options)
64
+ end
65
+
66
+
67
+ def option_was_specified?(option_name, options)
68
+ !options[option_name].nil? && !(options[option_name] == []) && !(options[option_name] == false)
69
+ end
70
+
71
+ def ensure_dependency_for_valid_option(option_name)
72
+ unless option_exists?(option_name)
73
+ raise Escort::ClientError.new("Dependency specified for option '#{option_name}', but no such option was defined, perhaps you misspelled it")
74
+ end
75
+ end
76
+
77
+ def ensure_option_depends_on_valid_option(option_name, rule)
78
+ unless option_exists?(rule)
79
+ raise Escort::ClientError.new("'#{option_name}' is set up to depend on '#{rule}', but '#{rule}' does not appear to be a valid option, perhaps it is a spelling error")
80
+ end
81
+ end
82
+ end
83
+ end
@@ -54,6 +54,7 @@ module Escort
54
54
  parser.version(setup.version) #set the version if it was provided
55
55
  parser.help_formatter(Escort::Formatter::DefaultHelpFormatter.new(setup, context))
56
56
  parsed_options = parse_options_string(parser, cli_options)
57
+ Escort::OptionDependencyValidator.for(parser).validate(parsed_options, setup.dependencies_for(context))
57
58
  Escort::Validator.for(parser).validate(parsed_options, setup.validations_for(context))
58
59
  end
59
60
 
@@ -66,12 +67,21 @@ module Escort
66
67
  end
67
68
 
68
69
  def add_option_conflicts_to(parser, context = [])
69
- setup.conflicting_options_for(context).each do |conflict_list|
70
+ conflicting_options_for_context = setup.conflicting_options_for(context)
71
+ conflicting_options_for_context.keys.each do |option_name|
72
+ conflict_list = [option_name] + conflicting_options_for_context[option_name]
73
+ conflict_list.each do |option|
74
+ raise Escort::ClientError.new("Conflict defined with option '#{option}', but this option does not exist, perhaps it is a spelling error.") unless option_exists?(parser, option)
75
+ end
70
76
  parser.conflicts *conflict_list
71
77
  end
72
78
  parser
73
79
  end
74
80
 
81
+ def option_exists?(parser, option)
82
+ parser.specs.keys.include?(option)
83
+ end
84
+
75
85
  def default_option_values_from_config_for(parser, context)
76
86
  unless configuration.empty?
77
87
  parser.specs.each do |sym, opts|
@@ -1,5 +1,3 @@
1
- require 'json'
2
-
3
1
  module Escort
4
2
  module Setup
5
3
  module Configuration
@@ -1,5 +1,3 @@
1
- require 'json'
2
-
3
1
  module Escort
4
2
  module Setup
5
3
  module Configuration
@@ -21,11 +21,8 @@ module Escort
21
21
  Action.action(@name, @action, &block)
22
22
  end
23
23
 
24
- def validations(&block)
25
- Validations.validations(@name, @validations, &block)
26
- end
27
-
28
24
  def command(name, options = {}, &block)
25
+ raise Escort::ClientError.new("Parameter to 'requires_arguments' must be a boolean") unless [true, false].include?(boolean)
29
26
  options[:requires_arguments] = @requires_arguments
30
27
  command = Command.new(name.to_sym, options, &block)
31
28
  aliases = [options[:aliases] || []].flatten + [name]
@@ -43,7 +40,7 @@ module Escort
43
40
  end
44
41
 
45
42
  def conflicting_options(*command_names)
46
- @conflicts << command_names
43
+ raise Escort::ClientError.new("This interface for specifying conflicting options is no longer supported, please use 'opts.conflict' in the options block")
47
44
  end
48
45
 
49
46
  def summary(summary)
@@ -61,11 +58,9 @@ module Escort
61
58
  @commands = {}
62
59
  @options = Options.new(name)
63
60
  @action = Action.new(name)
64
- @validations = Validations.new(name)
65
61
  @name = nil
66
62
  @description = nil
67
63
  @aliases = []
68
- @conflicts = []
69
64
  @summary = nil
70
65
  end
71
66
  end
@@ -17,10 +17,6 @@ module Escort
17
17
  Action.action(:global, @action, &block)
18
18
  end
19
19
 
20
- def validations(&block)
21
- Validations.validations(:global, @validations, &block)
22
- end
23
-
24
20
  def command(name, options = {}, &block)
25
21
  options[:requires_arguments] = @requires_arguments
26
22
  command = Command.new(name.to_sym, options, &block)
@@ -31,7 +27,7 @@ module Escort
31
27
  end
32
28
 
33
29
  def requires_arguments(boolean = true)
34
- #TODO raise a client error if the value is anything besides true or false
30
+ raise Escort::ClientError.new("Parameter to 'requires_arguments' must be a boolean") unless [true, false].include?(boolean)
35
31
  @requires_arguments = boolean
36
32
  @commands.each do |command|
37
33
  command.requires_arguments(boolean)
@@ -54,9 +50,8 @@ module Escort
54
50
  @description = description
55
51
  end
56
52
 
57
-
58
53
  def conflicting_options(*command_names)
59
- @conflicts << command_names
54
+ raise Escort::ClientError.new("This interface for specifying conflicting options is no longer supported, please use 'opts.conflict' in the options block")
60
55
  end
61
56
 
62
57
  private
@@ -69,9 +64,7 @@ module Escort
69
64
  @requires_arguments = false
70
65
  @options = Options.new
71
66
  @action = Action.new
72
- @validations = Validations.new
73
67
  @config_file = nil
74
- @conflicts = []
75
68
  end
76
69
 
77
70
  def set_instance_variable_on(instance, instance_variable, value)
@@ -13,11 +13,67 @@ module Escort
13
13
  def initialize(command_name = :global)
14
14
  @command_name = command_name
15
15
  @options = {}
16
+ @dependencies = {}
17
+ @conflicts = {}
18
+ @validations = {}
16
19
  end
17
20
 
18
21
  def opt(name, desc="", opts={})
19
22
  opts[:desc] ||= desc
20
23
  @options[name] ||= opts
24
+ dependency(name, :on => opts[:depends_on]) if opts[:depends_on]
25
+ conflict(*[name, opts[:conflicts_with]].flatten) if opts[:conflicts_with]
26
+ end
27
+
28
+ def validate(name, description, &block)
29
+ @validations[name] ||= []
30
+ @validations[name] << {:desc => description, :block => block}
31
+ end
32
+
33
+ def dependency(option_name, opts = {})
34
+ ensure_dependency_specification_syntax(opts)
35
+ @dependencies[option_name] ||= []
36
+ rules_as_array(opts).each do |rule|
37
+ ensure_no_self_dependency(option_name, rule)
38
+ @dependencies[option_name] << rule
39
+ end
40
+ end
41
+
42
+ def conflict(*opts)
43
+ opts.each do |opt|
44
+ conflicts_for_opt = opts.reject{|value| value == opt}
45
+ @conflicts[opt] ||= []
46
+ @conflicts[opt] += conflicts_for_opt
47
+ @conflicts[opt].uniq!
48
+ end
49
+ end
50
+
51
+ private
52
+ def ensure_no_self_dependency(option_name, rule)
53
+ case rule
54
+ when Hash
55
+ rule.each_pair do |rule_option, rule_option_value|
56
+ handle_possible_self_dependency(option_name, rule_option)
57
+ end
58
+ else
59
+ handle_possible_self_dependency(option_name, rule)
60
+ end
61
+ end
62
+
63
+ def ensure_dependency_specification_syntax(opts)
64
+ unless opts[:on]
65
+ raise Escort::ClientError.new("Problem with syntax of dependency specification in #{@command_name} options block, #{option_name} missing ':on' condition")
66
+ end
67
+ end
68
+
69
+ def rules_as_array(opts)
70
+ [opts[:on]].flatten
71
+ end
72
+
73
+ def handle_possible_self_dependency(option_name, rule)
74
+ if option_name == rule
75
+ raise Escort::ClientError.new("Problem with syntax of dependency specification in #{@command_name} options block, #{option_name} is set to depend on itself")
76
+ end
21
77
  end
22
78
  end
23
79
  end
@@ -14,17 +14,22 @@ module Escort
14
14
 
15
15
  def conflicting_options_for(context = [])
16
16
  with_context(context) do |current_context|
17
- conflict_lists_for(current_context)
17
+ conflicts_hash_for(current_context)
18
18
  end
19
19
  end
20
20
 
21
-
22
21
  def validations_for(context = [])
23
22
  with_context(context) do |current_context|
24
23
  validations_hash_from(current_context)
25
24
  end
26
25
  end
27
26
 
27
+ def dependencies_for(context = [])
28
+ with_context(context) do |current_context|
29
+ dependencies_hash_from(current_context)
30
+ end
31
+ end
32
+
28
33
  def command_names_for(context = [])
29
34
  with_context(context) do |current_context|
30
35
  command_names_from(current_context)
@@ -169,13 +174,25 @@ module Escort
169
174
  end
170
175
  end
171
176
 
172
- def conflict_lists_for(context_object)
173
- fetch_instance_variable_from(context_object, :conflicts)
177
+ def conflicts_hash_for(context_object)
178
+ ensure_context_object(context_object, {}) do
179
+ options_object = options_object_from(context_object)
180
+ fetch_instance_variable_from(options_object, :conflicts)
181
+ end
174
182
  end
175
183
 
176
184
  def validations_hash_from(context_object)
177
- validations_object = fetch_instance_variable_from(context_object, :validations)
178
- fetch_instance_variable_from(validations_object, :validations)
185
+ ensure_context_object(context_object, {}) do
186
+ validations_object = fetch_instance_variable_from(context_object, :options)
187
+ fetch_instance_variable_from(validations_object, :validations)
188
+ end
189
+ end
190
+
191
+ def dependencies_hash_from(context_object)
192
+ ensure_context_object(context_object, {}) do
193
+ options_object = options_object_from(context_object)
194
+ fetch_instance_variable_from(options_object, :dependencies)
195
+ end
179
196
  end
180
197
 
181
198
  def fetch_instance_variable_from_setup(instance_variable)
@@ -750,9 +750,10 @@ def with_standard_exception_handling(parser)
750
750
  begin
751
751
  yield
752
752
  rescue CommandlineError => e
753
- $stderr.puts "Error: #{e.message}."
754
- $stderr.puts "Try --help for help."
755
- exit(1)
753
+ raise Escort::UserError.new(e.message, e)
754
+ #$stderr.puts "Error: #{e.message}."
755
+ #$stderr.puts "Try --help for help."
756
+ #exit(1)
756
757
  rescue HelpNeeded
757
758
  parser.current_help_formatter ? parser.current_help_formatter.print(parser) : parser.educate
758
759
  exit
@@ -31,7 +31,10 @@ module Escort
31
31
 
32
32
  def validate_option(option, options, validations_array)
33
33
  validations_array.each do |validation_data|
34
- parser.die(option, validation_data[:desc]) if invalid?(option, options, validation_data)
34
+ if invalid?(option, options, validation_data)
35
+ raise Escort::UserError.new("#{option} #{validation_data[:desc]}")
36
+ end
37
+ #parser.die(option, validation_data[:desc]) if invalid?(option, options, validation_data)
35
38
  end
36
39
  end
37
40
 
@@ -1,3 +1,3 @@
1
1
  module Escort
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/escort.rb CHANGED
@@ -6,11 +6,17 @@ require 'escort/logger'
6
6
 
7
7
  require 'escort/error/error'
8
8
 
9
+ require 'escort/formatter/option'
10
+ require 'escort/formatter/options'
11
+ require 'escort/formatter/command'
12
+ require 'escort/formatter/commands'
13
+ require 'escort/formatter/global_command'
14
+ require 'escort/formatter/shell_command_executor'
9
15
  require 'escort/formatter/terminal'
10
16
  require 'escort/formatter/string_splitter'
11
17
  require 'escort/formatter/terminal_formatter'
12
18
  require 'escort/formatter/borderless_table'
13
- require 'escort/formatter/common'
19
+ #require 'escort/formatter/common'
14
20
  require 'escort/formatter/default_help_formatter'
15
21
 
16
22
  require 'escort/setup/dsl/options'
@@ -31,6 +37,7 @@ require 'escort/setup/configuration/loader'
31
37
 
32
38
  require 'escort/setup_accessor'
33
39
 
40
+ require 'escort/option_dependency_validator'
34
41
  require 'escort/validator'
35
42
  require 'escort/auto_options'
36
43
  require 'escort/global_pre_parser'
@@ -0,0 +1,47 @@
1
+ describe "Escort basic app with conflicting options", :integration => true do
2
+ subject { Escort::App.create(option_string, &app_configuration) }
3
+
4
+ let(:app_configuration) do
5
+ lambda do |app|
6
+ app.options do |opts|
7
+ opts.opt :flag1, "Flag 1", :short => '-f', :long => '--flag1', :type => :boolean
8
+ opts.opt :flag2, "Flag 2", :short => :none, :long => '--flag2', :type => :boolean
9
+
10
+ opts.conflict :flag1, :flag2
11
+ end
12
+
13
+ app.action do |options, arguments|
14
+ Escort::IntegrationTestCommand.new(options, arguments).execute(result)
15
+ end
16
+ end
17
+ end
18
+
19
+ context "when conflicting options supplied" do
20
+ let(:option_string) { "--flag1 --flag2" }
21
+ it("should exit with code 2 or 3") { expect{ subject }.to exit_with_code(2, 3) }
22
+ end
23
+
24
+ context "when no conflicting options supplied" do
25
+ let(:option_string) { "--flag1" }
26
+ it("should exit with code 0") { expect{ subject }.to exit_with_code(0) }
27
+ end
28
+
29
+ context "when non-existant option in conflict list" do
30
+ let(:app_configuration) do
31
+ lambda do |app|
32
+ app.options do |opts|
33
+ opts.opt :flag1, "Flag 1", :short => '-f', :long => '--flag1', :type => :boolean
34
+ opts.opt :flag2, "Flag 2", :short => :none, :long => '--flag2', :type => :boolean
35
+
36
+ opts.conflict :flag1, :flag2, :flag3
37
+ end
38
+
39
+ app.action do |options, arguments|
40
+ Escort::IntegrationTestCommand.new(options, arguments).execute(result)
41
+ end
42
+ end
43
+ end
44
+ let(:option_string) { "--flag1" }
45
+ it("should exit with code 2") { expect{ subject }.to exit_with_code(2) }
46
+ end
47
+ end