convoy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.irbrc +3 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +22 -0
  9. data/README.md +705 -0
  10. data/Rakefile +1 -0
  11. data/convoy.gemspec +24 -0
  12. data/examples/.my_apprc +24 -0
  13. data/examples/basic +10 -0
  14. data/examples/basic_config_file +16 -0
  15. data/examples/basic_conflicts +17 -0
  16. data/examples/basic_depends_on +25 -0
  17. data/examples/basic_flags +15 -0
  18. data/examples/basic_options +14 -0
  19. data/examples/basic_options_multi +15 -0
  20. data/examples/basic_require_arguments +17 -0
  21. data/examples/basic_texts +21 -0
  22. data/examples/basic_validations +21 -0
  23. data/examples/basic_with_everything +30 -0
  24. data/examples/commands/example_command.rb +13 -0
  25. data/examples/suite_complex +65 -0
  26. data/examples/suite_simple +19 -0
  27. data/examples/suite_with_sub_commands +94 -0
  28. data/lib/convoy.rb +83 -0
  29. data/lib/convoy/action_command/base.rb +85 -0
  30. data/lib/convoy/action_command/escort_utility_command.rb +53 -0
  31. data/lib/convoy/app.rb +127 -0
  32. data/lib/convoy/arguments.rb +20 -0
  33. data/lib/convoy/auto_options.rb +71 -0
  34. data/lib/convoy/error/error.rb +33 -0
  35. data/lib/convoy/formatter/command.rb +87 -0
  36. data/lib/convoy/formatter/commands.rb +37 -0
  37. data/lib/convoy/formatter/cursor_position.rb +29 -0
  38. data/lib/convoy/formatter/default_help_formatter.rb +117 -0
  39. data/lib/convoy/formatter/global_command.rb +17 -0
  40. data/lib/convoy/formatter/option.rb +152 -0
  41. data/lib/convoy/formatter/options.rb +28 -0
  42. data/lib/convoy/formatter/shell_command_executor.rb +49 -0
  43. data/lib/convoy/formatter/stream_output_formatter.rb +88 -0
  44. data/lib/convoy/formatter/string_grid.rb +108 -0
  45. data/lib/convoy/formatter/string_splitter.rb +50 -0
  46. data/lib/convoy/formatter/terminal.rb +30 -0
  47. data/lib/convoy/global_pre_parser.rb +43 -0
  48. data/lib/convoy/logger.rb +75 -0
  49. data/lib/convoy/option_dependency_validator.rb +82 -0
  50. data/lib/convoy/option_parser.rb +155 -0
  51. data/lib/convoy/setup/configuration/generator.rb +75 -0
  52. data/lib/convoy/setup/configuration/instance.rb +34 -0
  53. data/lib/convoy/setup/configuration/loader.rb +43 -0
  54. data/lib/convoy/setup/configuration/locator/base.rb +19 -0
  55. data/lib/convoy/setup/configuration/locator/chaining.rb +29 -0
  56. data/lib/convoy/setup/configuration/locator/descending_to_home.rb +23 -0
  57. data/lib/convoy/setup/configuration/locator/executing_script_directory.rb +15 -0
  58. data/lib/convoy/setup/configuration/locator/specified_directory.rb +21 -0
  59. data/lib/convoy/setup/configuration/merge_tool.rb +38 -0
  60. data/lib/convoy/setup/configuration/reader.rb +36 -0
  61. data/lib/convoy/setup/configuration/writer.rb +46 -0
  62. data/lib/convoy/setup/dsl/action.rb +17 -0
  63. data/lib/convoy/setup/dsl/command.rb +67 -0
  64. data/lib/convoy/setup/dsl/config_file.rb +13 -0
  65. data/lib/convoy/setup/dsl/global.rb +29 -0
  66. data/lib/convoy/setup/dsl/options.rb +81 -0
  67. data/lib/convoy/setup_accessor.rb +206 -0
  68. data/lib/convoy/trollop.rb +861 -0
  69. data/lib/convoy/utils.rb +21 -0
  70. data/lib/convoy/validator.rb +45 -0
  71. data/spec/integration/basic_config_file_spec.rb +126 -0
  72. data/spec/integration/basic_conflicts_spec.rb +47 -0
  73. data/spec/integration/basic_depends_on_spec.rb +275 -0
  74. data/spec/integration/basic_options_spec.rb +41 -0
  75. data/spec/integration/basic_options_with_multi_spec.rb +30 -0
  76. data/spec/integration/basic_spec.rb +38 -0
  77. data/spec/integration/basic_validations_spec.rb +77 -0
  78. data/spec/integration/basic_with_arguments_spec.rb +35 -0
  79. data/spec/integration/basic_with_text_fields_spec.rb +21 -0
  80. data/spec/integration/suite_simple_spec.rb +45 -0
  81. data/spec/integration/suite_sub_command_spec.rb +51 -0
  82. data/spec/lib/convoy/action_command/base_spec.rb +200 -0
  83. data/spec/lib/convoy/formatter/command_spec.rb +238 -0
  84. data/spec/lib/convoy/formatter/global_command_spec.rb +50 -0
  85. data/spec/lib/convoy/formatter/option_spec.rb +300 -0
  86. data/spec/lib/convoy/formatter/shell_command_executor_spec.rb +59 -0
  87. data/spec/lib/convoy/formatter/stream_output_formatter_spec.rb +214 -0
  88. data/spec/lib/convoy/formatter/string_grid_spec.rb +59 -0
  89. data/spec/lib/convoy/formatter/string_splitter_spec.rb +50 -0
  90. data/spec/lib/convoy/formatter/terminal_spec.rb +19 -0
  91. data/spec/lib/convoy/setup/configuration/generator_spec.rb +101 -0
  92. data/spec/lib/convoy/setup/configuration/loader_spec.rb +79 -0
  93. data/spec/lib/convoy/setup/configuration/locator/chaining_spec.rb +81 -0
  94. data/spec/lib/convoy/setup/configuration/locator/descending_to_home_spec.rb +57 -0
  95. data/spec/lib/convoy/setup/configuration/locator/executing_script_directory_spec.rb +29 -0
  96. data/spec/lib/convoy/setup/configuration/locator/specified_directory_spec.rb +33 -0
  97. data/spec/lib/convoy/setup/configuration/merge_tool_spec.rb +41 -0
  98. data/spec/lib/convoy/setup/configuration/reader_spec.rb +41 -0
  99. data/spec/lib/convoy/setup/configuration/writer_spec.rb +75 -0
  100. data/spec/lib/convoy/setup_accessor_spec.rb +226 -0
  101. data/spec/lib/convoy/utils_spec.rb +30 -0
  102. data/spec/spec_helper.rb +29 -0
  103. data/spec/support/integration_helpers.rb +2 -0
  104. data/spec/support/matchers/execute_action_for_command_matcher.rb +21 -0
  105. data/spec/support/matchers/execute_action_with_arguments_matcher.rb +25 -0
  106. data/spec/support/matchers/execute_action_with_options_matcher.rb +29 -0
  107. data/spec/support/matchers/exit_with_code_matcher.rb +29 -0
  108. data/spec/support/shared_contexts/integration_setup.rb +34 -0
  109. metadata +292 -0
@@ -0,0 +1,82 @@
1
+ module Convoy
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 Convoy::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 Convoy::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
+ def option_was_specified?(option_name, options)
67
+ !options[option_name].nil? && !(options[option_name] == []) && !(options[option_name] == false)
68
+ end
69
+
70
+ def ensure_dependency_for_valid_option(option_name)
71
+ unless option_exists?(option_name)
72
+ raise Convoy::ClientError.new("Dependency specified for option '#{option_name}', but no such option was defined, perhaps you misspelled it")
73
+ end
74
+ end
75
+
76
+ def ensure_option_depends_on_valid_option(option_name, rule)
77
+ unless option_exists?(rule)
78
+ raise Convoy::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")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,155 @@
1
+ module Convoy
2
+ class OptionParser
3
+ attr_reader :setup, :configuration
4
+
5
+ def initialize(configuration, setup)
6
+ @configuration = configuration
7
+ @setup = setup
8
+ end
9
+
10
+ def parse(cli_options)
11
+ options = init_invoked_options_hash
12
+ parse_global_options(cli_options, options[:global][:options])
13
+ parse_command_options(cli_options, [], options[:global][:commands])
14
+
15
+ [options, arguments_from(cli_options)]
16
+ end
17
+
18
+ private
19
+
20
+ def init_invoked_options_hash
21
+ {
22
+ :global => {
23
+ :options => {},
24
+ :commands => {}
25
+ }
26
+ }
27
+ end
28
+
29
+ def parse_global_options(cli_options, options)
30
+ context = []
31
+ options.merge!(parse_options(cli_options, context))
32
+ end
33
+
34
+ def parse_command_options(cli_options, context, options)
35
+ unless cli_option_is_a_command?(cli_options, context)
36
+ options
37
+ else
38
+ command = command_name_from(cli_options)
39
+ context << command
40
+ current_options = parse_options(cli_options, context)
41
+ options[command] = {}
42
+ options[command][:options] = current_options
43
+ options[command][:commands] = {}
44
+ parse_command_options(cli_options, context, options[command][:commands])
45
+ end
46
+ end
47
+
48
+ def parse_options(cli_options, context = [])
49
+ stop_words = setup.command_names_for(context).map(&:to_s)
50
+ parser = init_parser(stop_words)
51
+ parser = add_setup_options_to(parser, context)
52
+ parser = add_option_conflicts_to(parser, context)
53
+ parser = default_option_values_from_config_for(parser, context)
54
+ parser.version(setup.version) #set the version if it was provided
55
+ parser.help_formatter(Convoy::Formatter::DefaultHelpFormatter.new(setup, context))
56
+ parsed_options = parse_options_string(parser, cli_options)
57
+ Convoy::OptionDependencyValidator.for(parser).validate(parsed_options, setup.dependencies_for(context))
58
+ Convoy::Validator.for(parser).validate(parsed_options, setup.validations_for(context))
59
+ end
60
+
61
+ def add_setup_options_to(parser, context = [])
62
+ setup.options_for(context).each do |name, opts|
63
+ parser.opt name, opts[:desc] || "", opts.dup #have to make sure to dup here, otherwise opts might get stuff added to it and it
64
+ #may cause problems later, e.g. adds default value and when parsed again trollop barfs
65
+ end
66
+ parser
67
+ end
68
+
69
+ def add_option_conflicts_to(parser, context = [])
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 Convoy::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
76
+ parser.conflicts *conflict_list
77
+ end
78
+ parser
79
+ end
80
+
81
+ def option_exists?(parser, option)
82
+ parser.specs.keys.include?(option)
83
+ end
84
+
85
+ def default_option_values_from_config_for(parser, context)
86
+ unless configuration.empty?
87
+ parser.specs.each do |sym, opts|
88
+ if config_has_value_for_context?(sym, context)
89
+ default = config_value_for_context(sym, context)
90
+ opts[:default] = default
91
+ if opts[:multi] && default.nil?
92
+ opts[:default] = [] # multi arguments default to [], not nil
93
+ elsif opts[:multi] && !default.kind_of?(Array)
94
+ opts[:default] = [default]
95
+ else
96
+ opts[:default] = default
97
+ end
98
+ end
99
+ end
100
+ end
101
+ parser
102
+ end
103
+
104
+ def config_has_value_for_context?(option, context)
105
+ relevant_config_hash = config_hash_for_context(context)
106
+ relevant_config_hash[:options].include?(option)
107
+ end
108
+
109
+ def config_value_for_context(option, context)
110
+ relevant_config_hash = config_hash_for_context(context)
111
+ relevant_config_hash[:options][option]
112
+ end
113
+
114
+ def config_hash_for_context(context)
115
+ relevant_config_hash = configuration.global
116
+ context.each do |command_name|
117
+ command_name = command_name.to_sym
118
+ relevant_config_hash = relevant_config_hash[:commands][command_name]
119
+ relevant_config_hash = ensure_config_hash_has_options_and_commands(relevant_config_hash)
120
+ end
121
+ relevant_config_hash
122
+ end
123
+
124
+ def ensure_config_hash_has_options_and_commands(relevant_config_hash)
125
+ relevant_config_hash ||= {}
126
+ relevant_config_hash[:commands] ||= {}
127
+ relevant_config_hash[:options] ||= {}
128
+ relevant_config_hash
129
+ end
130
+
131
+ def cli_option_is_a_command?(cli_options, context)
132
+ cli_options.size > 0 && setup.command_names_for(context).include?(cli_options[0].to_sym)
133
+ end
134
+
135
+ def init_parser(stop_words)
136
+ Trollop::Parser.new.tap do |parser|
137
+ parser.stop_on(stop_words) # make sure we halt parsing if we see a command
138
+ end
139
+ end
140
+
141
+ def parse_options_string(parser, cli_options)
142
+ Trollop::with_standard_exception_handling(parser) do
143
+ parser.parse(cli_options)
144
+ end
145
+ end
146
+
147
+ def command_name_from(cli_options)
148
+ cli_options.shift.to_sym
149
+ end
150
+
151
+ def arguments_from(cli_options)
152
+ cli_options
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,75 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ class Generator
5
+ attr_reader :setup
6
+
7
+ def initialize(setup)
8
+ @setup = setup
9
+ end
10
+
11
+ def default_data
12
+ config_hash = init_config_hash
13
+ options([], config_hash[:global][:options]) #global options
14
+ options_for_commands(setup.canonical_command_names_for([]), [], config_hash[:global][:commands])
15
+ config_hash
16
+ end
17
+
18
+ private
19
+
20
+ def options_for_commands(commands, context, options = {})
21
+ commands.each do |command_name|
22
+ command_name = command_name.to_sym
23
+ #next if command_name == :convoy
24
+ options[command_name] = {}
25
+ options[command_name][:options] = {}
26
+ options[command_name][:commands] = {}
27
+ current_context = context.dup
28
+ current_context << command_name
29
+ options(current_context, options[command_name][:options]) #command_options
30
+ options_for_commands(setup.canonical_command_names_for(current_context), current_context, options[command_name][:commands])
31
+ end
32
+ end
33
+
34
+ def init_config_hash
35
+ {
36
+ :global => {
37
+ :options => {},
38
+ :commands => {}
39
+ },
40
+ :user => {}
41
+ }
42
+ end
43
+
44
+ def options(context = [], options = {})
45
+ command_names = setup.command_names_for(context)
46
+ parser = init_parser(command_names)
47
+ parser = add_setup_options_to(parser, context)
48
+ options.merge!(default_option_values(parser))
49
+ end
50
+
51
+ def init_parser(stop_words)
52
+ Trollop::Parser.new.tap do |parser|
53
+ parser.stop_on(stop_words) # make sure we halt parsing if we see a command
54
+ end
55
+ end
56
+
57
+ def add_setup_options_to(parser, context = [])
58
+ setup.options_for(context).each do |name, opts|
59
+ parser.opt name, opts[:desc] || "", opts.dup #have to make sure to dup here, otherwise opts might get stuff added to it and it
60
+ #may cause problems later, e.g. adds default value and when parsed again trollop barfs
61
+ end
62
+ parser
63
+ end
64
+
65
+ def default_option_values(parser)
66
+ hash = {}
67
+ parser.specs.each_pair do |key, data|
68
+ hash[key] = data[:default] || nil
69
+ end
70
+ hash
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,34 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ class Instance
5
+ class << self
6
+ def blank
7
+ self.new(nil, {})
8
+ end
9
+ end
10
+
11
+ attr_reader :data, :path
12
+
13
+ def initialize(path, hash)
14
+ @data = hash
15
+ @path = path
16
+ end
17
+
18
+ def blank?
19
+ data.empty?
20
+ end
21
+
22
+ alias empty? blank?
23
+
24
+ def global
25
+ data[:global] || {}
26
+ end
27
+
28
+ def user
29
+ data[:user] || {}
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ class Loader
5
+ attr_reader :setup, :auto_options
6
+
7
+ def initialize(setup, auto_options)
8
+ @setup = setup
9
+ @auto_options = auto_options
10
+ end
11
+
12
+ def configuration
13
+ if setup.has_config_file?
14
+ Writer.new(config_path, Generator.new(setup).default_data).write if setup.config_file_autocreatable?
15
+ Reader.new(config_path).read
16
+ else
17
+ Instance.blank
18
+ end
19
+ end
20
+
21
+ def default_config_path
22
+ @default_config_path ||= (config_filename ? File.join(File.expand_path(ENV["HOME"]), config_filename) : nil)
23
+ end
24
+
25
+ private
26
+
27
+ def config_filename
28
+ @config_filename ||= setup.config_file
29
+ end
30
+
31
+ def config_path
32
+ @config_path ||= (auto_options.non_default_config_path || locator.locate || default_config_path)
33
+ end
34
+
35
+ def locator
36
+ Locator::Chaining.new(config_filename).
37
+ add_locator(Locator::ExecutingScriptDirectory.new(config_filename)).
38
+ add_locator(Locator::DescendingToHome.new(config_filename))
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ module Locator
5
+ class Base
6
+ attr_reader :filename
7
+
8
+ def initialize(filename)
9
+ @filename = filename
10
+ end
11
+
12
+ def locate
13
+ raise "Must be defined in child class"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Convoy
2
+ module Setup
3
+ module Configuration
4
+ module Locator
5
+ class Chaining < Base
6
+ attr_reader :locators
7
+
8
+ def initialize(filename, locators = [])
9
+ super(filename)
10
+ @locators = locators || []
11
+ end
12
+
13
+ def locate
14
+ locators.each do |locator|
15
+ filepath = locator.locate
16
+ return filepath if filepath
17
+ end
18
+ nil
19
+ end
20
+
21
+ def add_locator(locator)
22
+ @locators << locator
23
+ self
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ require 'pathname'
2
+
3
+ module Convoy
4
+ module Setup
5
+ module Configuration
6
+ module Locator
7
+ class DescendingToHome < Base
8
+ def locate
9
+ return nil unless filename
10
+ possible_configs = []
11
+ Pathname.new(Dir.pwd).descend do |path|
12
+ filepath = File.join(path, filename)
13
+ if File.exists?(filepath)
14
+ possible_configs << filepath
15
+ end
16
+ end
17
+ possible_configs.empty? ? nil : possible_configs.last
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end