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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +46 -17
- data/lib/cliqr/argument_validation/argument_type_validator.rb +31 -0
- data/lib/cliqr/argument_validation/option_validator.rb +27 -0
- data/lib/cliqr/argument_validation/validator.rb +67 -0
- data/lib/cliqr/cli/command_context.rb +21 -12
- data/lib/cliqr/cli/config.rb +57 -9
- data/lib/cliqr/cli/executor.rb +17 -11
- data/lib/cliqr/cli/interface.rb +7 -3
- data/lib/cliqr/error.rb +17 -19
- data/lib/cliqr/parser/argument_parser.rb +21 -0
- data/lib/cliqr/parser/argument_tree_walker.rb +51 -0
- data/lib/cliqr/parser/boolean_option_token.rb +26 -0
- data/lib/cliqr/parser/parsed_input.rb +53 -0
- data/lib/cliqr/parser/parsed_input_builder.rb +61 -0
- data/lib/cliqr/parser/single_valued_option_token.rb +53 -0
- data/lib/cliqr/parser/token.rb +45 -0
- data/lib/cliqr/parser/token_factory.rb +69 -0
- data/lib/cliqr/validation/validation_set.rb +48 -0
- data/lib/cliqr/validation/validator_factory.rb +265 -0
- data/lib/cliqr/validation/verifiable.rb +89 -0
- data/lib/cliqr/validation_errors.rb +61 -0
- data/lib/cliqr/version.rb +1 -1
- data/spec/config/config_validator_spec.rb +51 -30
- data/spec/config/option_config_validator_spec.rb +143 -0
- data/spec/dsl/interface_spec.rb +48 -114
- data/spec/executor/executor_spec.rb +19 -1
- data/spec/fixtures/test_option_checker_command.rb +8 -0
- data/spec/parser/argument_parser_spec.rb +33 -39
- data/spec/validation/argument_validation_spec.rb +141 -0
- data/spec/validation/error_spec.rb +22 -0
- data/spec/validation/validation_spec.rb +11 -0
- metadata +27 -10
- data/lib/cliqr/cli/argument_validator.rb +0 -19
- data/lib/cliqr/cli/config_validator.rb +0 -104
- data/lib/cliqr/cli/parser/argument_parser.rb +0 -23
- data/lib/cliqr/cli/parser/argument_tree_walker.rb +0 -56
- data/lib/cliqr/cli/parser/option_token.rb +0 -72
- data/lib/cliqr/cli/parser/parsed_argument_builder.rb +0 -66
- data/lib/cliqr/cli/parser/token.rb +0 -38
- 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
|