escort 0.1.0 → 0.2.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 (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