cliqr 1.2.0 → 2.0.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +9 -71
  4. data/examples/numbers +1 -2
  5. data/examples/vagrant +0 -3
  6. data/lib/cliqr.rb +52 -11
  7. data/lib/cliqr/argument_validation/argument_type_validator.rb +2 -2
  8. data/lib/cliqr/argument_validation/validator.rb +3 -3
  9. data/lib/cliqr/{cli → command}/argument_operator.rb +2 -2
  10. data/lib/cliqr/{cli → command}/argument_operator_context.rb +1 -1
  11. data/lib/cliqr/{cli/command.rb → command/base_command.rb} +2 -2
  12. data/lib/cliqr/command/color.rb +174 -0
  13. data/lib/cliqr/{cli → command}/command_context.rb +68 -20
  14. data/lib/cliqr/command/shell_banner_builder.rb +17 -0
  15. data/lib/cliqr/command/shell_command.rb +125 -0
  16. data/lib/cliqr/command/shell_prompt_builder.rb +26 -0
  17. data/lib/cliqr/config/action.rb +226 -0
  18. data/lib/cliqr/config/base.rb +84 -0
  19. data/lib/cliqr/config/command.rb +137 -0
  20. data/lib/cliqr/config/dsl.rb +81 -0
  21. data/lib/cliqr/config/event.rb +43 -0
  22. data/lib/cliqr/config/event_based.rb +78 -0
  23. data/lib/cliqr/config/named.rb +55 -0
  24. data/lib/cliqr/config/option.rb +95 -0
  25. data/lib/cliqr/config/option_based.rb +130 -0
  26. data/lib/cliqr/config/shell.rb +87 -0
  27. data/lib/cliqr/config/validation/validation_set.rb +66 -0
  28. data/lib/cliqr/config/validation/validator_factory.rb +403 -0
  29. data/lib/cliqr/config/validation/verifiable.rb +91 -0
  30. data/lib/cliqr/error.rb +20 -4
  31. data/lib/cliqr/events/event.rb +56 -0
  32. data/lib/cliqr/events/event_context.rb +31 -0
  33. data/lib/cliqr/events/handler.rb +32 -0
  34. data/lib/cliqr/events/invoker.rb +70 -0
  35. data/lib/cliqr/{cli → executor}/command_runner_factory.rb +3 -3
  36. data/lib/cliqr/{cli → executor}/router.rb +4 -4
  37. data/lib/cliqr/{cli/executor.rb → executor/runner.rb} +25 -10
  38. data/lib/cliqr/interface.rb +98 -0
  39. data/lib/cliqr/parser/token_factory.rb +1 -1
  40. data/lib/cliqr/usage/command_usage_context.rb +94 -0
  41. data/lib/cliqr/usage/option_usage_context.rb +86 -0
  42. data/lib/cliqr/usage/templates/partial/action_list.erb +10 -0
  43. data/lib/cliqr/usage/templates/partial/command_name.erb +3 -0
  44. data/lib/cliqr/usage/templates/partial/option_list.erb +18 -0
  45. data/lib/cliqr/usage/templates/partial/usage_info.erb +5 -0
  46. data/lib/cliqr/usage/templates/usage/cli.erb +4 -0
  47. data/lib/cliqr/usage/templates/usage/shell.erb +2 -0
  48. data/lib/cliqr/usage/usage_builder.rb +59 -0
  49. data/lib/cliqr/util.rb +81 -34
  50. data/lib/cliqr/version.rb +1 -1
  51. data/spec/config/action_config_validator_spec.rb +127 -5
  52. data/spec/config/config_finalize_spec.rb +3 -3
  53. data/spec/config/config_validator_spec.rb +120 -17
  54. data/spec/config/option_config_validator_spec.rb +1 -1
  55. data/spec/dsl/interface_spec.rb +2 -2
  56. data/spec/dsl/usage_spec.rb +461 -465
  57. data/spec/executor/action_executor_spec.rb +1 -1
  58. data/spec/executor/color_executor_spec.rb +125 -0
  59. data/spec/executor/command_runner_spec.rb +6 -8
  60. data/spec/executor/event_executor_spec.rb +365 -0
  61. data/spec/executor/executor_spec.rb +49 -11
  62. data/spec/executor/help_executor_spec.rb +107 -103
  63. data/spec/fixtures/action_reader_command.rb +1 -1
  64. data/spec/fixtures/test_arg_printer_event_handler.rb +9 -0
  65. data/spec/fixtures/test_color_shell_prompt.rb +13 -0
  66. data/spec/fixtures/test_empty_event_handler.rb +5 -0
  67. data/spec/fixtures/test_invoker_event_handler.rb +9 -0
  68. data/spec/fixtures/test_shell_banner.rb +8 -0
  69. data/spec/fixtures/test_shell_prompt.rb +13 -0
  70. data/spec/shell/shell_executor_spec.rb +700 -0
  71. data/spec/validation/validation_spec.rb +2 -2
  72. metadata +65 -27
  73. data/lib/cliqr/cli/config.rb +0 -554
  74. data/lib/cliqr/cli/interface.rb +0 -107
  75. data/lib/cliqr/cli/shell_command.rb +0 -69
  76. data/lib/cliqr/cli/usage_builder.rb +0 -185
  77. data/lib/cliqr/config_validation/validation_set.rb +0 -48
  78. data/lib/cliqr/config_validation/validator_factory.rb +0 -319
  79. data/lib/cliqr/config_validation/verifiable.rb +0 -89
  80. data/lib/cliqr/dsl.rb +0 -59
  81. data/spec/executor/shell_executor_spec.rb +0 -233
  82. data/templates/usage.erb +0 -39
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/validation_errors'
4
+ require 'cliqr/config/validation/validator_factory'
5
+ require 'cliqr/config/validation/validation_set'
6
+
7
+ module Cliqr
8
+ module Config
9
+ # Validation framework for the command line interface config definition adopted from
10
+ # lotus/validations by @jodosha
11
+ #
12
+ # @api private
13
+ #
14
+ # @see https://github.com/lotus/validations
15
+ module Validation
16
+ # If a class includes this module, we add a few useful methods to that class
17
+ #
18
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
19
+ #
20
+ # @return [Object]
21
+ def self.included(base)
22
+ base.class_eval do
23
+ extend Verifiable
24
+ end
25
+ end
26
+
27
+ # Check if the class is valid based on the configured attribute validations
28
+ #
29
+ # @return [Boolean] <tt>true</tt> if there are no validation errors
30
+ def valid?
31
+ validate
32
+
33
+ errors.empty?
34
+ end
35
+
36
+ # Run the validation against all attribute values
37
+ #
38
+ # @return [Hash] All validated attributed attributes and their values
39
+ def validate
40
+ read_attributes.each do |name, value|
41
+ validations.validate(name, value, errors)
42
+ end
43
+ end
44
+
45
+ # Get the list of validations to be performed
46
+ #
47
+ # @return [Hash] A hash of attribute name to its validator
48
+ def validations
49
+ self.class.__send__(:validations)
50
+ end
51
+
52
+ # Read current values for all attributes that must be validated
53
+ #
54
+ # @return [Hash] All attributes that must be validated along with their current values
55
+ def read_attributes
56
+ {}.tap do |attributes|
57
+ validations.each_key do |attribute|
58
+ attributes[attribute] = public_send(attribute)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Get a list of errors after validation finishes
64
+ #
65
+ # @return [Cliqr::ValidationErrors] A wrapper of all errors
66
+ def errors
67
+ @errors ||= ValidationErrors.new
68
+ end
69
+
70
+ # Validations DSL
71
+ module Verifiable
72
+ # Add a new validation for a attribute
73
+ #
74
+ # @param [Symbol] name Name of the attribute to validate
75
+ # @param [Object] options Configuration to initialize a attribute validator with
76
+ #
77
+ # @return [Cliqr::Config::Validation::ValidationSet]
78
+ def validates(name, options)
79
+ validations.add(name, options)
80
+ end
81
+
82
+ # Get or create a new <tt>Cliqr::Config::Validation::ValidationSet</tt>
83
+ #
84
+ # @return [Cliqr::Config::Validation::ValidationSet]
85
+ def validations
86
+ @validations ||= ValidationSet.new
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -6,6 +6,15 @@ module Cliqr
6
6
  #
7
7
  # @api private
8
8
  class CliqrError < StandardError
9
+ class << self
10
+ # The error code based on the error condition
11
+ #
12
+ # @return [Integer]
13
+ attr_accessor :error_code
14
+ end
15
+
16
+ @error_code = 100
17
+
9
18
  # Set up the error to wrap another error's trace
10
19
  #
11
20
  # @param [String] error_message A short error description
@@ -55,7 +64,14 @@ module Cliqr
55
64
  class ValidationError < CliqrError; end
56
65
 
57
66
  # Encapsulates the error that gets thrown during a command execution
58
- class CommandRuntimeError < CliqrError; end
67
+ class CommandRuntimeError < CliqrError
68
+ @error_code = 101
69
+ end
70
+
71
+ # Raised if an argument does not conform to the option's type
72
+ class IllegalArgumentError < CliqrError
73
+ @error_code = 102
74
+ end
59
75
 
60
76
  # Error to signify that a command's runner is not available
61
77
  class UnknownCommandRunnerException < CliqrError; end
@@ -75,9 +91,6 @@ module Cliqr
75
91
  # Raised if multiple actions are defined with same name at the same nesting level
76
92
  class DuplicateActions < CliqrError; end
77
93
 
78
- # Raised if an argument does not conform to the option's type
79
- class IllegalArgumentError < CliqrError; end
80
-
81
94
  # Indicates that a unknown validator type is being used in a class
82
95
  class UnknownValidatorType < CliqrError; end
83
96
 
@@ -86,5 +99,8 @@ module Cliqr
86
99
 
87
100
  # Raised when a command is executed that is not supposed to run
88
101
  class IllegalCommandError < CliqrError; end
102
+
103
+ # Raised if event invocation is in invalid state
104
+ class InvocationError < CliqrError; end
89
105
  end
90
106
  end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ module Cliqr
4
+ module Events
5
+ # Defines a event and its properties
6
+ #
7
+ # @api private
8
+ class Event
9
+ # Event's name
10
+ #
11
+ # @return [String]
12
+ attr_reader :name
13
+
14
+ # Command that was first invoked which resulted in this event
15
+ #
16
+ # @return [String]
17
+ attr_reader :command
18
+
19
+ # Time when the event was invoked
20
+ #
21
+ # @return [Time]
22
+ attr_reader :timestamp
23
+
24
+ # If this event was invoked from another event handler, this will ne non-nil
25
+ #
26
+ # @return [Cliqr::Events::Event]
27
+ attr_reader :parent
28
+
29
+ # Create a new event
30
+ def initialize(name, command, parent)
31
+ @name = name
32
+ @command = command
33
+ @parent = parent
34
+ @timestamp = Time.now
35
+ @propagate = true
36
+ end
37
+
38
+ # Check if this event has a parent event
39
+ def parent?
40
+ !parent.nil?
41
+ end
42
+
43
+ # Control event propagation
44
+ def propagate?
45
+ @propagate
46
+ end
47
+
48
+ # Unset propagation bit
49
+ #
50
+ # @return [Boolean]
51
+ def stop_propagation
52
+ @propagate = false
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module Cliqr
4
+ module Events
5
+ # The context in which event handlers are invoked
6
+ #
7
+ # @api private
8
+ class EventContext
9
+ # Create a new event context to execute events
10
+ def initialize(invoker, context, event)
11
+ @invoker = invoker
12
+ @context = context
13
+ @event = event
14
+ end
15
+
16
+ # Invoke a event handler chain by name
17
+ #
18
+ # @return [Boolean]
19
+ def invoke(event_name, *args)
20
+ @invoker.invoke(event_name, @event, *args)
21
+ end
22
+
23
+ # Handle the case when a method is invoked to get an option value
24
+ #
25
+ # @return [Object] Option's value
26
+ def method_missing(name, *_args, &_block)
27
+ @context.get_or_check_option(name)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module Cliqr
4
+ # Set of classes to manage and facilitate event handling
5
+ #
6
+ # @api private
7
+ module Events
8
+ # Event handler that all event handlers come from
9
+ class Handler
10
+ # Create a instance of event handler
11
+ def initialize(context)
12
+ @context = context
13
+ end
14
+
15
+ # Handle a incoming event needs to be implemented in a subclass
16
+ #
17
+ # @throws [Cliqr::Error::InvocationError]
18
+ #
19
+ # @return [Nothing]
20
+ def handle(*_args)
21
+ fail Cliqr::Error::InvocationError, 'handle method not implemented by handler class'
22
+ end
23
+
24
+ # Invoker another event
25
+ #
26
+ # @return [Boolean]
27
+ def invoke(*args)
28
+ @context.invoke(*args)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/events/event'
4
+ require 'cliqr/events/event_context'
5
+
6
+ module Cliqr
7
+ module Events
8
+ # Invokes a event and associated event-chain by propagating the event
9
+ #
10
+ # @api private
11
+ class Invoker
12
+ # Create a new event invoker instance
13
+ #
14
+ # @param [Cliqr::Config::Action] config
15
+ def initialize(config, context)
16
+ @config = config
17
+ @context = context
18
+ end
19
+
20
+ # Invoke a event in the context of the configuration set in this invoker
21
+ #
22
+ # @return [Boolean] <tt>true</tt> if the event was handled by an associated handler
23
+ def invoke(event_name, parent_event, *args)
24
+ handled = false
25
+ current_config = @config
26
+ event = build_event(event_name, parent_event)
27
+ loop do
28
+ if current_config.handle?(event_name)
29
+ handle(event, current_config.event(event_name), *args)
30
+ handled = true
31
+ break unless event.propagate?
32
+ end
33
+ break if current_config.root?
34
+ current_config = current_config.parent
35
+ end
36
+ handled
37
+ end
38
+
39
+ private
40
+
41
+ # Build a event by name
42
+ #
43
+ # @return [Cliqr::Events::Event]
44
+ def build_event(name, parent_event)
45
+ Events::Event.new(name, @config.command, parent_event)
46
+ end
47
+
48
+ # Handle invocation of a event
49
+ #
50
+ # @return [Nothing]
51
+ def handle(event, event_config, *args)
52
+ context = Events::EventContext.new(self, @context, event)
53
+ context.instance_exec(event, *args, &wrap(event_config.handler, context))
54
+ rescue StandardError => e
55
+ raise Cliqr::Error::InvocationError.new("failed invocation for #{event.name}", e)
56
+ end
57
+
58
+ # Wrap the event invocation handler in a proc
59
+ #
60
+ # @return [Proc]
61
+ def wrap(event_handler, context)
62
+ return event_handler if event_handler.is_a?(Proc)
63
+ handler = event_handler.new(context)
64
+ proc do |*args|
65
+ handler.handle(*args)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -3,7 +3,7 @@
3
3
  require 'stringio'
4
4
 
5
5
  module Cliqr
6
- module CLI
6
+ module Executor
7
7
  # Factory class to get instance of CommandRunner based on input options
8
8
  #
9
9
  # @api private
@@ -12,8 +12,8 @@ module Cliqr
12
12
  #
13
13
  # @param [Hash] options Used to build a command runner instance
14
14
  #
15
- # @return [Cliqr::CLI::StandardCommandRunner] If default output is require from command
16
- # @return [Cliqr::CLI::BufferedCommandRunner] If command's output needs to be buffered
15
+ # @return [Cliqr::Executor::StandardCommandRunner] If default output is require from command
16
+ # @return [Cliqr::Executor::BufferedCommandRunner] If command's output needs to be buffered
17
17
  def self.get(**options)
18
18
  case options[:output]
19
19
  when :default
@@ -1,18 +1,18 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'cliqr/cli/command_runner_factory'
3
+ require 'cliqr/executor/command_runner_factory'
4
4
 
5
5
  module Cliqr
6
- module CLI
6
+ module Executor
7
7
  # Used for routing the command to the appropriate command handler based on the interface config
8
8
  #
9
9
  # @api private
10
10
  class Router
11
11
  # Create a new Router instance
12
12
  #
13
- # @param [Cliqr::CLI::Config] config Command line configuration
13
+ # @param [Cliqr::Config::Command] config Command line configuration
14
14
  #
15
- # @return [Cliqr::CLI::Router]
15
+ # @return [Cliqr::Interface::Router]
16
16
  def initialize(config)
17
17
  @config = config
18
18
  end
@@ -1,17 +1,18 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'cliqr/cli/router'
4
- require 'cliqr/cli/command_context'
5
3
  require 'cliqr/argument_validation/validator'
6
4
  require 'cliqr/parser/argument_parser'
5
+ require 'cliqr/command/command_context'
6
+ require 'cliqr/executor/router'
7
7
 
8
8
  module Cliqr
9
- module CLI
10
- # Handles command execution with error handling
11
- #
12
- # @api private
13
- class Executor
14
- # Create a new command executor
9
+ # Handles command execution with error handling
10
+ #
11
+ # @api private
12
+ module Executor
13
+ # The command runner that handles errors as well
14
+ class Runner
15
+ # Create a new command runner
15
16
  def initialize(config)
16
17
  @config = config
17
18
  @validator = Cliqr::ArgumentValidation::Validator.new
@@ -27,11 +28,11 @@ module Cliqr
27
28
  args = Cliqr::Util.sanitize_args(args, @config)
28
29
  action_config, parsed_input = parse(args)
29
30
  begin
30
- command_context = CommandContext.build(action_config, parsed_input, options) \
31
+ command_context = Command::CommandContext.build(action_config, parsed_input, options) \
31
32
  do |forwarded_args, forwarded_options|
32
33
  execute(forwarded_args, options.merge(forwarded_options))
33
34
  end
34
- Router.new(action_config).handle(command_context, **options)
35
+ Executor::Router.new(action_config).handle(command_context, **options)
35
36
  rescue StandardError => e
36
37
  raise Cliqr::Error::CommandRuntimeError.new(
37
38
  "command '#{action_config.command}' failed", e)
@@ -55,5 +56,19 @@ module Cliqr
55
56
  [action_config, parsed_input]
56
57
  end
57
58
  end
59
+
60
+ # Exit code mapper based on error type
61
+ #
62
+ # @api private
63
+ class ExitCode
64
+ # Get exit code based on type
65
+ #
66
+ # @return [Integer]
67
+ def self.code(type)
68
+ return 0 if type == :success
69
+ return type.class.error_code if type.class.respond_to?(:error_code)
70
+ 99
71
+ end
72
+ end
58
73
  end
59
74
  end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/error'
4
+ require 'cliqr/executor/runner'
5
+ require 'cliqr/usage/usage_builder'
6
+
7
+ # The top level module
8
+ module Cliqr
9
+ # The execution interface to a command line application built using Cliqr
10
+ #
11
+ # @api private
12
+ class Interface
13
+ # Command line interface configuration
14
+ #
15
+ # @return [Cliqr::CLI::Config]
16
+ attr_accessor :config
17
+
18
+ # Create a new interface instance with a config
19
+ #
20
+ # @param [Cliqr::Config::Command] config Config used to create this interface
21
+ def initialize(config)
22
+ @config = config
23
+ @runner = Executor::Runner.new(config)
24
+ end
25
+
26
+ # Get usage information of this command line interface instance
27
+ #
28
+ # @return [String] Defines usage of this interface
29
+ def usage
30
+ Usage::UsageBuilder.new(:cli).build(config)
31
+ end
32
+
33
+ # Execute a command
34
+ #
35
+ # @param [Array<String>] args Arguments that will be used to execute the command
36
+ # @param [Hash] options Options for command execution
37
+ #
38
+ # @return [Integer] Exit code of the command execution
39
+ def execute(args = [], **options)
40
+ execute_internal(args, options)
41
+ Executor::ExitCode.code(:success)
42
+ rescue Cliqr::Error::CliqrError => e
43
+ puts e.message
44
+ Executor::ExitCode.code(e)
45
+ end
46
+
47
+ # Executes a command without handling error conditions
48
+ #
49
+ # @return [Integer] Exit code
50
+ def execute_internal(args = [], **options)
51
+ options = {
52
+ :output => :default,
53
+ :environment => :cli
54
+ }.merge(options)
55
+ @runner.execute(args, options)
56
+ end
57
+
58
+ # Invoke the builder method for [Cliqr::CLI::Interface]
59
+ #
60
+ # @param [Cliqr::CLI::Config] config Instance of the command line config
61
+ #
62
+ # @return [Cliqr::CLI::Interface]
63
+ def self.build(config)
64
+ InterfaceBuilder.new(config).build
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Builder for [Cliqr::CLI::Interface]
71
+ #
72
+ # @api private
73
+ class InterfaceBuilder
74
+ # Start building a command line interface
75
+ #
76
+ # @param [Cliqr::CLI::Config] config the configuration options for the
77
+ # interface (validated using CLI::Validator)
78
+ #
79
+ # @return [Cliqr::CLI::ConfigBuilder]
80
+ def initialize(config)
81
+ @config = config
82
+ end
83
+
84
+ # Validate and build a cli interface based on the configuration options
85
+ #
86
+ # @return [Cliqr::CLI::Interface]
87
+ #
88
+ # @throws [Cliqr::Error::ConfigNotFound] if a config is <tt>nil</tt>
89
+ # @throws [Cliqr::Error::ValidationError] if the validation for config fails
90
+ def build
91
+ fail Cliqr::Error::ConfigNotFound, 'a valid config should be defined' if @config.nil?
92
+ fail Cliqr::Error::ValidationError, \
93
+ "invalid Cliqr interface configuration - [#{@config.errors}]" unless @config.valid?
94
+
95
+ Interface.new(@config)
96
+ end
97
+ end
98
+ end