cliqr 0.1.0 → 1.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +46 -17
  4. data/lib/cliqr/argument_validation/argument_type_validator.rb +31 -0
  5. data/lib/cliqr/argument_validation/option_validator.rb +27 -0
  6. data/lib/cliqr/argument_validation/validator.rb +67 -0
  7. data/lib/cliqr/cli/command_context.rb +21 -12
  8. data/lib/cliqr/cli/config.rb +57 -9
  9. data/lib/cliqr/cli/executor.rb +17 -11
  10. data/lib/cliqr/cli/interface.rb +7 -3
  11. data/lib/cliqr/error.rb +17 -19
  12. data/lib/cliqr/parser/argument_parser.rb +21 -0
  13. data/lib/cliqr/parser/argument_tree_walker.rb +51 -0
  14. data/lib/cliqr/parser/boolean_option_token.rb +26 -0
  15. data/lib/cliqr/parser/parsed_input.rb +53 -0
  16. data/lib/cliqr/parser/parsed_input_builder.rb +61 -0
  17. data/lib/cliqr/parser/single_valued_option_token.rb +53 -0
  18. data/lib/cliqr/parser/token.rb +45 -0
  19. data/lib/cliqr/parser/token_factory.rb +69 -0
  20. data/lib/cliqr/validation/validation_set.rb +48 -0
  21. data/lib/cliqr/validation/validator_factory.rb +265 -0
  22. data/lib/cliqr/validation/verifiable.rb +89 -0
  23. data/lib/cliqr/validation_errors.rb +61 -0
  24. data/lib/cliqr/version.rb +1 -1
  25. data/spec/config/config_validator_spec.rb +51 -30
  26. data/spec/config/option_config_validator_spec.rb +143 -0
  27. data/spec/dsl/interface_spec.rb +48 -114
  28. data/spec/executor/executor_spec.rb +19 -1
  29. data/spec/fixtures/test_option_checker_command.rb +8 -0
  30. data/spec/parser/argument_parser_spec.rb +33 -39
  31. data/spec/validation/argument_validation_spec.rb +141 -0
  32. data/spec/validation/error_spec.rb +22 -0
  33. data/spec/validation/validation_spec.rb +11 -0
  34. metadata +27 -10
  35. data/lib/cliqr/cli/argument_validator.rb +0 -19
  36. data/lib/cliqr/cli/config_validator.rb +0 -104
  37. data/lib/cliqr/cli/parser/argument_parser.rb +0 -23
  38. data/lib/cliqr/cli/parser/argument_tree_walker.rb +0 -56
  39. data/lib/cliqr/cli/parser/option_token.rb +0 -72
  40. data/lib/cliqr/cli/parser/parsed_argument_builder.rb +0 -66
  41. data/lib/cliqr/cli/parser/token.rb +0 -38
  42. data/lib/cliqr/cli/parser/token_factory.rb +0 -58
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/parser/token'
4
+ require 'cliqr/parser/single_valued_option_token'
5
+ require 'cliqr/parser/boolean_option_token'
6
+
7
+ module Cliqr
8
+ module Parser
9
+ # A factory class to get a instance of {Cliqr::CLI::Parser::Token}
10
+ # based on the argument
11
+ #
12
+ # @api private
13
+ class TokenFactory
14
+ # Create a new token factory instance
15
+ #
16
+ # @param [Cliqr::CLI::Config] config Command line interface configuration
17
+ #
18
+ # @return [Cliqr::CLI::Parser::TokenFactory]
19
+ def initialize(config)
20
+ @config = config
21
+ end
22
+
23
+ # Get a new instance of {Cliqr::CLI::Parser::Token} based on the argument
24
+ #
25
+ # @param [String] arg The argument used to get a token instance (default nil)
26
+ #
27
+ # @return [Cliqr::CLI::Parser::Token]
28
+ def get_token(arg = nil)
29
+ if arg.nil?
30
+ Token.new
31
+ else
32
+ case arg
33
+ when /^--(no-)?([a-zA-Z][a-zA-Z0-9\-_]*)$/, /^(-)([a-zA-Z])$/
34
+ option_config = get_option_config(Regexp.last_match(2), arg)
35
+ build_token(option_config, arg)
36
+ else
37
+ fail Cliqr::Error::InvalidArgumentError, "invalid command argument \"#{arg}\""
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Build a option token handler based on the option's config
45
+ #
46
+ # @return [Cliqr::CLI::Parser::Token]
47
+ def build_token(option_config, arg)
48
+ case option_config.type
49
+ when :boolean
50
+ BooleanOptionToken.new(option_config.name, arg)
51
+ else
52
+ SingleValuedOptionToken.new(option_config.name, arg)
53
+ end
54
+ end
55
+
56
+ # Check if a option is defined with the requested name then return it
57
+ #
58
+ # @param [String] name Long name of the option
59
+ # @param [String] arg THe argument that was parsed to get the option name
60
+ #
61
+ # @return [Cliqr::CLI::OptionConfig] Requested option configuration
62
+ def get_option_config(name, arg)
63
+ fail Cliqr::Error::UnknownCommandOption,
64
+ "unknown option \"#{arg}\"" unless @config.option?(name)
65
+ @config.option(name)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/validation/validator_factory'
4
+
5
+ module Cliqr
6
+ module Validation
7
+ # A collection of configured validators
8
+ #
9
+ # @api private
10
+ class ValidationSet
11
+ # Initialize a new instance of validation set
12
+ def initialize
13
+ @validations = {}
14
+ end
15
+
16
+ # Add a new validator
17
+ #
18
+ # @param [Symbol] name Name of the validator
19
+ # @param [Object] options Configuration option to initialize the validator
20
+ #
21
+ # @return [Hash] A map of all validators
22
+ def add(name, options)
23
+ @validations[name] = \
24
+ Hash[options.map { |type, config| [type, ValidatorFactory.get(type, config)] }]
25
+ end
26
+
27
+ # Iterate over each type of validators
28
+ #
29
+ # @return [Object]
30
+ def each_key(&block)
31
+ @validations.each_key(&block)
32
+ end
33
+
34
+ # Run the validators for a attribute against its value
35
+ #
36
+ # @param [Symbol] attribute Name of the attribute
37
+ # @param [Object] value Value of the attribute
38
+ # @param [Cliqr::Validation::Errors] errors A collection wrapper for all validation errors
39
+ #
40
+ # @return [Array] All validators that ran for the attribute against the value
41
+ def validate(attribute, value, errors)
42
+ @validations[attribute].values.each do |validator|
43
+ validator.validate(attribute, value, errors)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,265 @@
1
+ # encoding: utf-8
2
+
3
+ module Cliqr
4
+ module Validation
5
+ # A factory class to retrieve a attribute validator based on the configuration type
6
+ #
7
+ # @api private
8
+ module ValidatorFactory
9
+ # Does not validates anything, used by default if a unknown validator type is used
10
+ class Validator
11
+ # Run this validator against an attribute value
12
+ #
13
+ # @param [String] name Name of the attribute
14
+ # @param [Object] value Value of the attribute
15
+ # @param [Cliqr::ValidationErrors] errors Errors after validation finished
16
+ #
17
+ # @return [Boolean] <tt>true</tt> if validation passed
18
+ def validate(name, value, errors)
19
+ validation_sequence(name, value, errors)
20
+ end
21
+
22
+ private
23
+
24
+ # Recursively validate using parent validator then apply itself
25
+ #
26
+ # @return [Boolean] <tt>true</tt> if validation passed
27
+ def validation_sequence(name, value, errors)
28
+ return false unless validate_parent(name, value, errors)
29
+
30
+ local_errors = ValidationErrors.new
31
+ @class_stack.last.instance_method(:do_validate).bind(self).call(name, value, local_errors)
32
+ errors.merge(local_errors)
33
+ local_errors.empty?
34
+ end
35
+
36
+ # Validate attribute using the parent validator's logic
37
+ #
38
+ # @return [Boolean] <tt>true</tt> if parent class' validation passed
39
+ def validate_parent(name, value, errors)
40
+ @class_stack = (@class_stack || [self.class])
41
+ parent_class = (@class_stack.last || self.class).superclass
42
+ @class_stack.push(parent_class)
43
+ begin
44
+ return parent_class.instance_method(:validate).bind(self) \
45
+ .call(name, value, errors) if parent_class < Validator
46
+ true
47
+ ensure
48
+ @class_stack.pop
49
+ end
50
+ end
51
+ end
52
+
53
+ # This is used in case a unknown validation is used
54
+ class NOOPValidator < Validator
55
+ # Initialize a new no-op validator
56
+ def initialize(type)
57
+ @type = type
58
+ end
59
+
60
+ protected
61
+
62
+ # Fails if invoked
63
+ #
64
+ # @return [Object]
65
+ def do_validate(_name, _value, _errors)
66
+ fail Cliqr::Error::UnknownValidatorType, "unknown validation type: '#{@type}'"
67
+ end
68
+ end
69
+
70
+ # Verifies that an attribute's value is non-nil
71
+ class NonNilValidator < Validator
72
+ # Initialize a new non-nil validator
73
+ def initialize(enabled)
74
+ @enabled = enabled
75
+ end
76
+
77
+ protected
78
+
79
+ # Validate presence of an attribute's value
80
+ #
81
+ # @return [Cliqr::ValidationErrors] Errors after the validation has finished
82
+ def do_validate(name, value, errors)
83
+ errors.add("'#{name}' cannot be nil") if @enabled && value.nil?
84
+ errors
85
+ end
86
+ end
87
+
88
+ # Verifies that an attribute's value is non-empty
89
+ class NonEmptyValidator < NonNilValidator
90
+ # Create a new non-empty validator
91
+ def initialize(enabled)
92
+ super(enabled)
93
+ @enabled = enabled
94
+ end
95
+
96
+ protected
97
+
98
+ # Validate that a attribute's value is not empty
99
+ #
100
+ # @return [Cliqr::ValidationErrors] Errors after the validation has finished
101
+ def do_validate(name, value, errors)
102
+ errors.add("'#{name}' cannot be empty") \
103
+ if @enabled && value.respond_to?(:empty?) && value.empty?
104
+ errors
105
+ end
106
+ end
107
+
108
+ # Validates the value of an attribute against a regex pattern
109
+ class FormatValidator < NonNilValidator
110
+ # Initialize a new format validator
111
+ #
112
+ # @param [Regex] format Format of the value to validate required
113
+ def initialize(format)
114
+ super(true)
115
+ @format = format
116
+ end
117
+
118
+ protected
119
+
120
+ # Run the format validator to check attribute value's format
121
+ #
122
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
123
+ def do_validate(name, value, errors)
124
+ errors.add("value for '#{name}' must match /#{@format.source}/; " \
125
+ "actual: #{value.inspect}") \
126
+ if !value.nil? && @format.match(value).nil?
127
+ errors
128
+ end
129
+ end
130
+
131
+ # Validates that a value matches a pattern and it is not empty
132
+ class NonEmptyFormatValidator < NonEmptyValidator
133
+ # Initialize a new non-empty format validator
134
+ #
135
+ # @param [Regex] format Format of the value to validate required
136
+ def initialize(format)
137
+ super(true)
138
+ @format = format
139
+ end
140
+
141
+ protected
142
+
143
+ # Run the format validator along with non-empty check
144
+ #
145
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
146
+ def do_validate(name, value, errors)
147
+ FormatValidator.new(@format).validate(name, value, errors)
148
+ end
149
+ end
150
+
151
+ # Validates that a value matches a pattern and it is not empty; nil value allowed
152
+ class NonEmptyNilOkFormatValidator < Validator
153
+ # Initialize a new non-empty-nil-ok format validator
154
+ #
155
+ # @param [Regex] format Format of the value to validate required
156
+ def initialize(format)
157
+ @format = format
158
+ end
159
+
160
+ protected
161
+
162
+ # Run the validator
163
+ #
164
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
165
+ def do_validate(name, value, errors)
166
+ unless value.nil?
167
+ local_errors = ValidationErrors.new
168
+ local_errors.add("'#{name}' cannot be empty") \
169
+ if value.respond_to?(:empty?) && value.empty?
170
+ FormatValidator.new(@format).validate(name, value, local_errors) \
171
+ if local_errors.empty?
172
+ errors.merge(local_errors)
173
+ end
174
+ errors
175
+ end
176
+ end
177
+
178
+ # Validates that the value of an attribute is of a type that extends from another
179
+ class TypeHierarchyValidator < NonNilValidator
180
+ # Create a new instance of type hierarchy validator
181
+ #
182
+ # @param [Class] super_type Class reference that the validated variable must extend from
183
+ def initialize(super_type)
184
+ super(true)
185
+ @super_type = super_type
186
+ end
187
+
188
+ protected
189
+
190
+ # Check if the type of <tt>value</tt> is extensible from a <tt>super_type</tt>
191
+ #
192
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
193
+ def do_validate(name, value, errors)
194
+ errors.add("value '#{value}' of type '#{value.class.name}' for '#{name}' " \
195
+ "does not extend from '#{@super_type}'") \
196
+ unless value.is_a?(@super_type) || value < @super_type
197
+ end
198
+ end
199
+
200
+ # Validates each element inside a collection
201
+ class CollectionValidator < TypeHierarchyValidator
202
+ # Create a new collection validator
203
+ def initialize(_config)
204
+ super(Array)
205
+ end
206
+
207
+ protected
208
+
209
+ # Validate each element inside a collection and prepend index to error
210
+ #
211
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
212
+ def do_validate(name, values, errors)
213
+ valid = true
214
+ values.each_with_index do |value, index|
215
+ valid = false unless value.valid?
216
+ value.errors.each { |error| errors.add("#{name}[#{index + 1}] - #{error}") }
217
+ end
218
+ valid
219
+ end
220
+ end
221
+
222
+ # Validate that a attribute value is included in a predefined set
223
+ class InclusionValidator < NonNilValidator
224
+ # Create a new inclusion validator
225
+ #
226
+ # @param [Array<Symbol>] allowed_values A set of allowed values
227
+ def initialize(allowed_values)
228
+ @allowed_values = allowed_values
229
+ end
230
+
231
+ protected
232
+
233
+ # Validate that a value is included in <tt>allowed_values</tt>
234
+ #
235
+ # @return [Nothing]
236
+ def do_validate(_name, value, errors)
237
+ errors.add("invalid type '#{value}'") unless @allowed_values.include?(value)
238
+ end
239
+ end
240
+
241
+ # A hash of validator type id to validator class
242
+ VALIDATORS = {
243
+ :non_empty => NonEmptyValidator,
244
+ :non_empty_format => NonEmptyFormatValidator,
245
+ :non_empty_nil_ok_format => NonEmptyNilOkFormatValidator,
246
+ :format => FormatValidator,
247
+ :extend => TypeHierarchyValidator,
248
+ :collection => CollectionValidator,
249
+ :inclusion => InclusionValidator
250
+ }
251
+
252
+ # Get a new validator based on the type and config param
253
+ #
254
+ # @return [Cliqr::Validation::ValidatorFactory::Validator]
255
+ def self.get(validator_type, config)
256
+ validator_class = VALIDATORS[validator_type]
257
+ if validator_class.nil?
258
+ NOOPValidator.new(validator_type)
259
+ else
260
+ validator_class.new(config)
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,89 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/validation/validator_factory'
4
+ require 'cliqr/validation/validation_set'
5
+ require 'cliqr/validation_errors'
6
+
7
+ module Cliqr
8
+ # Validation framework for the command line interface config definition adopted from
9
+ # lotus/validations by @jodosha
10
+ #
11
+ # @api private
12
+ #
13
+ # @see https://github.com/lotus/validations
14
+ module Validation
15
+ # If a class includes this module, we add a few useful methods to that class
16
+ #
17
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
18
+ #
19
+ # @return [Object]
20
+ def self.included(base)
21
+ base.class_eval do
22
+ extend Verifiable
23
+ end
24
+ end
25
+
26
+ # Check if the class is valid based on the configured attribute validations
27
+ #
28
+ # @return [Boolean] <tt>true</tt> if there are no validation errors
29
+ def valid?
30
+ validate
31
+
32
+ errors.empty?
33
+ end
34
+
35
+ # Run the validation against all attribute values
36
+ #
37
+ # @return [Hash] All validated attributed attributes and their values
38
+ def validate
39
+ read_attributes.each do |name, value|
40
+ validations.validate(name, value, errors)
41
+ end
42
+ end
43
+
44
+ # Get the list of validations to be performed
45
+ #
46
+ # @return [Hash] A hash of attribute name to its validator
47
+ def validations
48
+ self.class.__send__(:validations)
49
+ end
50
+
51
+ # Read current values for all attributes that must be validated
52
+ #
53
+ # @return [Hash] All attributes that must be validated along with their current values
54
+ def read_attributes
55
+ {}.tap do |attributes|
56
+ validations.each_key do |attribute|
57
+ attributes[attribute] = public_send(attribute)
58
+ end
59
+ end
60
+ end
61
+
62
+ # Get a list of errors after validation finishes
63
+ #
64
+ # @return [Cliqr::ValidationErrors] A wrapper of all errors
65
+ def errors
66
+ @errors ||= ValidationErrors.new
67
+ end
68
+
69
+ # Validations DSL
70
+ module Verifiable
71
+ # Add a new validation for a attribute
72
+ #
73
+ # @param [Symbol] name Name of the attribute to validate
74
+ # @param [Object] options Configuration to initialize a attribute validator with
75
+ #
76
+ # @return [Cliqr::Validation::ValidationSet] A wrapper of all validations configured so far
77
+ def validates(name, options)
78
+ validations.add(name, options)
79
+ end
80
+
81
+ # Get or create a new <tt>Cliqr::Validation::ValidationSet</tt>
82
+ #
83
+ # @return [Cliqr::Validation::ValidationSet]
84
+ def validations
85
+ @validations ||= ValidationSet.new
86
+ end
87
+ end
88
+ end
89
+ end