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,95 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/config/named'
4
+ require 'cliqr/command/argument_operator'
5
+
6
+ module Cliqr
7
+ module Config
8
+ # Config attributes for a command's option
9
+ #
10
+ # @api private
11
+ class Option < Cliqr::Config::Named
12
+ # Optional short name for the option
13
+ #
14
+ # @return [String]
15
+ attr_accessor :short
16
+ validates :short,
17
+ non_empty_nil_ok_format: /^[a-z0-9A-Z]$/
18
+
19
+ # Optional field that restricts values of this option to a certain type
20
+ #
21
+ # @return [Symbol] Type of the option
22
+ attr_accessor :type
23
+ validates :type,
24
+ inclusion: [:any, Config::NUMERIC_ARGUMENT_TYPE, Config::BOOLEAN_ARGUMENT_TYPE]
25
+
26
+ # Operation to be applied to the option value after validation
27
+ #
28
+ # @return [Class<Cliqr::Command::ArgumentOperator>]
29
+ attr_accessor :operator
30
+ validates :operator,
31
+ one_of: [
32
+ { extend: Cliqr::Command::ArgumentOperator },
33
+ { type_of: Proc }
34
+ ]
35
+
36
+ # Default value for this option
37
+ #
38
+ # @return [Object]
39
+ attr_accessor :default
40
+
41
+ # Initialize a new config instance for an option with UNSET attribute values
42
+ def initialize
43
+ super
44
+
45
+ @short = UNSET
46
+ @type = UNSET
47
+ @operator = UNSET
48
+ @default = UNSET
49
+ end
50
+
51
+ # Finalize option's config by adding default values for unset values
52
+ #
53
+ # @return [Cliqr::Config::Option]
54
+ def finalize
55
+ super
56
+
57
+ @short = Config.get_if_unset(@short, nil)
58
+ @type = Config.get_if_unset(@type, ANY_ARGUMENT_TYPE)
59
+ @operator = Util.ensure_instance(
60
+ Config.get_if_unset(@operator, Cliqr::Command::ArgumentOperator.for_type(@type)))
61
+ @default = Config.get_if_unset(@default, ARGUMENT_DEFAULTS[@type])
62
+
63
+ self
64
+ end
65
+
66
+ # Check if a option's short name is defined
67
+ #
68
+ # @return [Boolean] <tt>true</tt> if options' short name is not null neither empty
69
+ def short?
70
+ !(@short.nil? || @short.empty?)
71
+ end
72
+
73
+ # Check if a option's type is defined
74
+ #
75
+ # @return [Boolean] <tt>true</tt> if options' type is not nil and not equal to <tt>:any</tt>
76
+ def type?
77
+ !@type.nil? && @type != :any
78
+ end
79
+
80
+ # Check if a option is of boolean type
81
+ #
82
+ # @return [Boolean] <tt>true</tt> is the option is of type <tt>:boolean</tt>
83
+ def boolean?
84
+ @type == :boolean
85
+ end
86
+
87
+ # Check if a default value setting is defined
88
+ #
89
+ # @return [Boolean]
90
+ def default?
91
+ !@default.nil?
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,130 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/util'
4
+ require 'cliqr/command/base_command'
5
+ require 'cliqr/config/base'
6
+ require 'cliqr/config/option'
7
+
8
+ module Cliqr
9
+ module Config
10
+ # Configuration setting for an option based setting
11
+ #
12
+ # @api private
13
+ class OptionBased < Cliqr::Config::Named
14
+ # Array of options applied to the base command
15
+ #
16
+ # @return [Array<OptionConfig>]
17
+ attr_accessor :options
18
+ validates :options,
19
+ hash: true
20
+
21
+ # New config instance with all attributes set as UNSET
22
+ def initialize
23
+ super
24
+
25
+ @options = {}
26
+ @short_option_index = {}
27
+ end
28
+
29
+ # Finalize config by adding default values for unset values
30
+ #
31
+ # @return [Cliqr::Config::OptionBased]
32
+ def finalize
33
+ super
34
+
35
+ if options? && @short_option_index.empty?
36
+ @options.values.each do |option|
37
+ @short_option_index[option.short.to_s] = option if option.short?
38
+ end
39
+ end
40
+
41
+ self
42
+ end
43
+
44
+ # Set value for a config option
45
+ #
46
+ # @param [Symbol] name Name of the config parameter
47
+ # @param [Object] value Value for the config parameter
48
+ # @param [Proc] block Function which populates configuration for a sub-attribute
49
+ #
50
+ # @return [Cliqr::Config::Option] Newly added option config
51
+ def set_config(name, value, *args, &block)
52
+ case name
53
+ when :option
54
+ handle_option(value, &block) # value is the long name for the option
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ # Check if options are set
61
+ def options?
62
+ return false if @options.nil?
63
+ !@options.empty?
64
+ end
65
+
66
+ # Check if particular option is set
67
+ #
68
+ # @param [String] name Name of the option to check
69
+ def option?(name)
70
+ @options.key?(name.to_s) || @short_option_index.key?(name.to_s)
71
+ end
72
+
73
+ # Get value of a option
74
+ #
75
+ # @param [String] name Name of the option
76
+ #
77
+ # @return [String] value for the option
78
+ def option(name)
79
+ if @options.key?(name.to_s)
80
+ @options[name.to_s]
81
+ else
82
+ @short_option_index[name.to_s]
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # Handle configuration for a new option
89
+ #
90
+ # @param [Symbol] name Long name of the option
91
+ # @param [Proc] block Populate the option's config in this function block
92
+ #
93
+ # @return [Cliqr::Config::Option] Newly created option's config
94
+ def handle_option(name, &block)
95
+ option_config = Option.build(&block)
96
+ option_config.name = name
97
+ add_option(option_config)
98
+ end
99
+
100
+ # Add a new option for the command
101
+ #
102
+ # @return [Cliqr::Config::Option] Newly added option's config
103
+ def add_option(option_config)
104
+ validate_option_name(option_config)
105
+
106
+ @options[option_config.name.to_s] = option_config
107
+ @short_option_index[option_config.short.to_s] = option_config if option_config.short?
108
+
109
+ option_config
110
+ end
111
+
112
+ # Make sure that the option's name is unique
113
+ #
114
+ # @param [Cliqr::Config::Option] option_config Config for this particular option
115
+ #
116
+ # @return [Cliqr::Config::Option] Validated OptionConfig instance
117
+ def validate_option_name(option_config)
118
+ fail Cliqr::Error::DuplicateOptions,
119
+ "multiple options with long name \"#{option_config.name}\"" \
120
+ if option?(option_config.name)
121
+
122
+ fail Cliqr::Error::DuplicateOptions,
123
+ "multiple options with short name \"#{option_config.short}\"" \
124
+ if option?(option_config.short)
125
+
126
+ option_config
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/command/shell_prompt_builder'
4
+ require 'cliqr/command/shell_banner_builder'
5
+
6
+ module Cliqr
7
+ module Config
8
+ # Config attributes for shell
9
+ #
10
+ # @api private
11
+ class Shell < Cliqr::Config::OptionBased
12
+ # Enable or disable the shell action
13
+ #
14
+ # @return [Symbol]
15
+ attr_accessor :enabled
16
+ validates :enabled,
17
+ inclusion: [true, false]
18
+
19
+ # Prompt for the shell
20
+ #
21
+ # @return [String]
22
+ # @return [Proc]
23
+ attr_accessor :prompt
24
+ validates :prompt,
25
+ one_of: [
26
+ { extend: Command::ShellPromptBuilder },
27
+ { type_of: Proc },
28
+ { type_of: String }
29
+ ]
30
+
31
+ # Banner that is displayed when shell starts
32
+ #
33
+ # @return [String]
34
+ attr_accessor :banner
35
+ validates :banner,
36
+ one_of: [
37
+ { extend: Command::ShellBannerBuilder },
38
+ { type_of: Proc },
39
+ { type_of: String }
40
+ ]
41
+
42
+ # Initialize a new config instance for an option with UNSET attribute values
43
+ def initialize
44
+ super
45
+
46
+ @enabled = UNSET
47
+ @prompt = UNSET
48
+ @banner = UNSET
49
+ end
50
+
51
+ # Finalize shell's config by adding default values for unset values
52
+ #
53
+ # @return [Cliqr::Config::Option]
54
+ def finalize
55
+ super
56
+
57
+ case @enabled
58
+ when Cliqr::Config::ENABLE_CONFIG
59
+ @enabled = true
60
+ when Cliqr::Config::DISABLE_CONFIG
61
+ @enabled = false
62
+ when UNSET
63
+ @enabled = true
64
+ end
65
+ @prompt = Config.get_if_unset(@prompt, Cliqr::Command::ShellPromptBuilder.new)
66
+ @banner = Config.get_if_unset(@banner, Cliqr::Command::ShellBannerBuilder.new)
67
+
68
+ # set default name for the shell action
69
+ @name = 'shell' if @name.is_a?(String) && @name.empty?
70
+
71
+ self
72
+ end
73
+
74
+ # Check if shell is enabled
75
+ def enabled?
76
+ @enabled
77
+ end
78
+
79
+ # Disable colors in shell
80
+ #
81
+ # @return [Cliqr::Command::Color]
82
+ def disable_color
83
+ @prompt.disable_color if @prompt.respond_to?(:disable_color)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/config/validation/validator_factory'
4
+
5
+ module Cliqr
6
+ module Config
7
+ module Validation
8
+ # A collection of configured validators
9
+ #
10
+ # @api private
11
+ class ValidationSet
12
+ # Initialize a new instance of validation set
13
+ def initialize
14
+ @set = {}
15
+ end
16
+
17
+ # Add a new validator
18
+ #
19
+ # @param [Symbol] name Name of the validator
20
+ # @param [Object] options Configuration option to initialize the validator
21
+ #
22
+ # @return [Hash] A map of all validators
23
+ def add(name, options)
24
+ @set[name] = \
25
+ Hash[options.map { |type, config| [type, ValidatorFactory.get(type, config)] }]
26
+ end
27
+
28
+ # Merge validations form another set
29
+ #
30
+ # @param [Cliqr::Config::Validation::ValidationSet] other
31
+ #
32
+ # @return [Cliqr::Config::Validation::ValidationSet]
33
+ def merge(other)
34
+ other.each { |name, validations| @set[name] = validations }
35
+ end
36
+
37
+ # Iterate over each type of validators
38
+ #
39
+ # @return [Object]
40
+ def each_key(&block)
41
+ @set.each_key(&block)
42
+ end
43
+
44
+ # Iterate over each validators
45
+ #
46
+ # @return [Cliqr::Config::Validation::ValidatorFactory::Validator]
47
+ def each(&block)
48
+ @set.each(&block)
49
+ end
50
+
51
+ # Run the validators for a attribute against its value
52
+ #
53
+ # @param [Symbol] attribute Name of the attribute
54
+ # @param [Object] value Value of the attribute
55
+ # @param [Cliqr::ValidationErrors] errors A collection wrapper for all validation errors
56
+ #
57
+ # @return [Array] All validators that ran for the attribute against the value
58
+ def validate(attribute, value, errors)
59
+ @set[attribute].values.each do |validator|
60
+ validator.validate(attribute, value, errors)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,403 @@
1
+ # encoding: utf-8
2
+
3
+ module Cliqr
4
+ module Config
5
+ module Validation
6
+ # A factory class to retrieve a attribute validator based on the configuration type
7
+ #
8
+ # @api private
9
+ module ValidatorFactory
10
+ # Does not validates anything, used by default if a unknown validator type is used
11
+ class Validator
12
+ # Run this validator against an attribute value
13
+ #
14
+ # @param [String] name Name of the attribute
15
+ # @param [Object] value Value of the attribute
16
+ # @param [Cliqr::ValidationErrors] errors Errors after validation finished
17
+ #
18
+ # @return [Boolean] <tt>true</tt> if validation passed
19
+ def validate(name, value, errors)
20
+ validation_sequence(name, value, errors)
21
+ end
22
+
23
+ private
24
+
25
+ # Recursively validate using parent validator then apply itself
26
+ #
27
+ # @return [Boolean] <tt>true</tt> if validation passed
28
+ def validation_sequence(name, value, errors)
29
+ return false unless validate_parent(name, value, errors)
30
+
31
+ local_errors = ValidationErrors.new
32
+ @class_stack.last.instance_method(:do_validate).bind(self)
33
+ .call(name, value, local_errors)
34
+ errors.merge(local_errors)
35
+ local_errors.empty?
36
+ end
37
+
38
+ # Validate attribute using the parent validator's logic
39
+ #
40
+ # @return [Boolean] <tt>true</tt> if parent class' validation passed
41
+ def validate_parent(name, value, errors)
42
+ @class_stack = (@class_stack || [self.class])
43
+ parent_class = (@class_stack.last || self.class).superclass
44
+ @class_stack.push(parent_class)
45
+ begin
46
+ return parent_class.instance_method(:validate).bind(self) \
47
+ .call(name, value, errors) if parent_class < Validator
48
+ true
49
+ ensure
50
+ @class_stack.pop
51
+ end
52
+ end
53
+ end
54
+
55
+ # This is used in case a unknown validation is used
56
+ class NOOPValidator < Validator
57
+ # Initialize a new no-op validator
58
+ def initialize(type)
59
+ @type = type
60
+ end
61
+
62
+ protected
63
+
64
+ # Fails if invoked
65
+ #
66
+ # @return [Object]
67
+ def do_validate(_name, _value, _errors)
68
+ fail Cliqr::Error::UnknownValidatorType, "unknown validation type: '#{@type}'"
69
+ end
70
+ end
71
+
72
+ # Verifies that an attribute's value is non-nil
73
+ class NonNilValidator < Validator
74
+ # Initialize a new non-nil validator
75
+ def initialize(enabled)
76
+ @enabled = enabled
77
+ end
78
+
79
+ protected
80
+
81
+ # Validate presence of an attribute's value
82
+ #
83
+ # @return [Cliqr::ValidationErrors] Errors after the validation has finished
84
+ def do_validate(name, value, errors)
85
+ errors.add("'#{name}' cannot be nil") if @enabled && value.nil?
86
+ errors
87
+ end
88
+ end
89
+
90
+ # Verifies that an attribute's value is non-empty
91
+ class NonEmptyValidator < NonNilValidator
92
+ # Create a new non-empty validator
93
+ def initialize(enabled)
94
+ super(enabled)
95
+ @enabled = enabled
96
+ end
97
+
98
+ protected
99
+
100
+ # Validate that a attribute's value is not empty
101
+ #
102
+ # @return [Cliqr::ValidationErrors] Errors after the validation has finished
103
+ def do_validate(name, value, errors)
104
+ errors.add("'#{name}' cannot be empty") \
105
+ if @enabled && value.respond_to?(:empty?) && value.empty?
106
+ errors
107
+ end
108
+ end
109
+
110
+ # Validates the value of an attribute against a regex pattern
111
+ class FormatValidator < NonNilValidator
112
+ # Initialize a new format validator
113
+ #
114
+ # @param [Regex] format Format of the value to validate required
115
+ def initialize(format)
116
+ super(true)
117
+ @format = format
118
+ end
119
+
120
+ protected
121
+
122
+ # Run the format validator to check attribute value's format
123
+ #
124
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
125
+ def do_validate(name, value, errors)
126
+ errors.add("value for '#{name}' must match /#{@format.source}/; " \
127
+ "actual: #{value.inspect}") \
128
+ if !value.nil? && !match?(value)
129
+ errors
130
+ end
131
+
132
+ # Check if the value matches a particular format
133
+ def match?(value)
134
+ return false unless value.is_a?(String) || value.is_a?(Symbol)
135
+ !@format.match(value).nil?
136
+ end
137
+ end
138
+
139
+ # Validates that a value matches a pattern and it is not empty
140
+ class NonEmptyFormatValidator < NonEmptyValidator
141
+ # Initialize a new non-empty format validator
142
+ #
143
+ # @param [Regex] format Format of the value to validate required
144
+ def initialize(format)
145
+ super(true)
146
+ @format = format
147
+ end
148
+
149
+ protected
150
+
151
+ # Run the format validator along with non-empty check
152
+ #
153
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
154
+ def do_validate(name, value, errors)
155
+ FormatValidator.new(@format).validate(name, value, errors)
156
+ end
157
+ end
158
+
159
+ # Validates that a value matches a pattern and it is not empty; nil value allowed
160
+ class NonEmptyNilOkFormatValidator < Validator
161
+ # Initialize a new non-empty-nil-ok format validator
162
+ #
163
+ # @param [Regex] format Format of the value to validate required
164
+ def initialize(format)
165
+ @format = format
166
+ end
167
+
168
+ protected
169
+
170
+ # Run the validator
171
+ #
172
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
173
+ def do_validate(name, value, errors)
174
+ unless value.nil?
175
+ local_errors = ValidationErrors.new
176
+ local_errors.add("'#{name}' cannot be empty") \
177
+ if value.respond_to?(:empty?) && value.empty?
178
+ FormatValidator.new(@format).validate(name, value, local_errors) \
179
+ if local_errors.empty?
180
+ errors.merge(local_errors)
181
+ end
182
+ errors
183
+ end
184
+ end
185
+
186
+ # Validates that the value of an attribute is of a type that extends from another
187
+ class TypeHierarchyValidator < NonNilValidator
188
+ # Create a new instance of type hierarchy validator
189
+ #
190
+ # @param [Class] super_type Class reference that the validated variable must extend from
191
+ def initialize(super_type)
192
+ super(true)
193
+ @super_type = super_type
194
+ end
195
+
196
+ protected
197
+
198
+ # Check if the type of <tt>value</tt> is extensible from a <tt>super_type</tt>
199
+ #
200
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
201
+ def do_validate(name, value, errors)
202
+ begin
203
+ return if value.is_a?(@super_type) || \
204
+ (value.respond_to?(:<) ? value < @super_type : value.is_a?(@super_type))
205
+ rescue ArgumentError
206
+ # the comparison failed which indicates that the validation also failed
207
+ end
208
+ errors.add("#{name} of type '#{value.class.name}' " \
209
+ "does not extend from '#{@super_type}'")
210
+ end
211
+ end
212
+
213
+ # Validates each element inside a collection
214
+ class CollectionValidator < TypeHierarchyValidator
215
+ # Create a new collection validator
216
+ def initialize(_config, type = nil)
217
+ super(type || Array)
218
+ end
219
+
220
+ protected
221
+
222
+ # Validate each element inside a collection and prepend index to error
223
+ #
224
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
225
+ def do_validate(name, values, errors)
226
+ valid = true
227
+ iterator(values) do |value, key|
228
+ valid = false unless value.valid?
229
+ value.errors.each do |error|
230
+ if value.name.nil? || value.name.empty?
231
+ errors.add("#{name}[#{key}] - #{error}")
232
+ else
233
+ errors.add("#{name.to_s.gsub(/s$/, '')} \"#{value.name}\" - #{error}")
234
+ end
235
+ end
236
+ end
237
+ valid
238
+ end
239
+
240
+ # Iterator for each item in the array
241
+ #
242
+ # @return [Array]
243
+ def iterator(array, &block)
244
+ array.each_with_index do |value, index|
245
+ block.call(value, index + 1) unless value.skip_validation?
246
+ end
247
+ end
248
+ end
249
+
250
+ # Validates each element inside a hash map
251
+ class HashValidator < CollectionValidator
252
+ # Create a new hash validator
253
+ def initialize(config)
254
+ super(config, Hash)
255
+ end
256
+
257
+ protected
258
+
259
+ # Validate each element inside a hash and prepend key to error
260
+ #
261
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
262
+ def do_validate(_name, _values, _errors)
263
+ true
264
+ end
265
+
266
+ # Iterate over each key value pair in the hash
267
+ #
268
+ # @return [Hash]
269
+ def iterator(hash, &block)
270
+ hash.each_with_index do |(key, value), index|
271
+ block.call(value, key.empty? ? (index + 1) : key)
272
+ end
273
+ end
274
+ end
275
+
276
+ # Validate that a attribute value is included in a predefined set
277
+ class InclusionValidator < NonNilValidator
278
+ # Create a new inclusion validator
279
+ #
280
+ # @param [Array<Symbol>] allowed_values A set of allowed values
281
+ def initialize(allowed_values)
282
+ @allowed_values = allowed_values
283
+ end
284
+
285
+ protected
286
+
287
+ # Validate that a value is included in <tt>allowed_values</tt>
288
+ #
289
+ # @return [Nothing]
290
+ def do_validate(_name, value, errors)
291
+ errors.add("invalid type '#{value}'") unless @allowed_values.include?(value)
292
+ end
293
+ end
294
+
295
+ # Validate the type of a attribute's value
296
+ class TypeOfValidator < NonNilValidator
297
+ # Create a new <tt>:type_of</tt> validator
298
+ def initialize(type)
299
+ super(true)
300
+ @type = type
301
+ end
302
+
303
+ protected
304
+
305
+ # Run the <tt>:type_of</tt> validation check
306
+ #
307
+ # @return [Nothing]
308
+ def do_validate(name, value, errors)
309
+ errors.add("#{name} should be a '#{@type}' not '#{value.class}'") \
310
+ unless matches?(value)
311
+ end
312
+
313
+ # Check if a value matches a type
314
+ def matches?(value)
315
+ begin
316
+ return true if value.class == @type
317
+ return value < @type if value.respond_to?(:<)
318
+ rescue ArgumentError
319
+ # nothing to do here just return false below
320
+ end
321
+ false
322
+ end
323
+ end
324
+
325
+ # Run multiple validators on a value to assert at-least one of them passes
326
+ class OneOfValidator < Validator
327
+ # Create a new <tt>:one_of</tt> validator
328
+ def initialize(validator_configs)
329
+ @validators = []
330
+ validator_configs.each do |config|
331
+ @validators.push(ValidatorFactory.get(config.first[0], config.first[1]))
332
+ end
333
+ end
334
+
335
+ protected
336
+
337
+ # Run each validator one by one until one passes
338
+ #
339
+ # @return [Nothing]
340
+ def do_validate(name, value, errors)
341
+ local_errors = ValidationErrors.new
342
+ passing_validator = @validators.find do |validator|
343
+ validator_errors = ValidationErrors.new.tap do |temp_errors|
344
+ validator.validate(name, value, temp_errors)
345
+ local_errors.merge(temp_errors)
346
+ end
347
+ validator_errors.empty?
348
+ end
349
+ errors.add("invalid value for #{name}; fix one of - [#{local_errors}]") \
350
+ if passing_validator.nil?
351
+ end
352
+ end
353
+
354
+ # Validates child element
355
+ class ChildValidator < Validator
356
+ # Create a new child validator
357
+ def initialize(_config)
358
+ end
359
+
360
+ protected
361
+
362
+ # Validate the child element
363
+ #
364
+ # @return [Boolean] <tt>true</tt> if there were any errors during validation
365
+ def do_validate(name, value, errors)
366
+ return true if value.valid?
367
+ value.errors.each do |error|
368
+ errors.add("#{name} - #{error}")
369
+ end
370
+ false
371
+ end
372
+ end
373
+
374
+ # A hash of validator type id to validator class
375
+ VALIDATORS = {
376
+ :non_empty => NonEmptyValidator,
377
+ :non_empty_format => NonEmptyFormatValidator,
378
+ :non_empty_nil_ok_format => NonEmptyNilOkFormatValidator,
379
+ :format => FormatValidator,
380
+ :extend => TypeHierarchyValidator,
381
+ :collection => CollectionValidator,
382
+ :hash => HashValidator,
383
+ :inclusion => InclusionValidator,
384
+ :one_of => OneOfValidator,
385
+ :type_of => TypeOfValidator,
386
+ :child => ChildValidator
387
+ }
388
+
389
+ # Get a new validator based on the type and config param
390
+ #
391
+ # @return [Cliqr::Validation::ValidatorFactory::Validator]
392
+ def self.get(validator_type, config)
393
+ validator_class = VALIDATORS[validator_type]
394
+ if validator_class.nil?
395
+ NOOPValidator.new(validator_type)
396
+ else
397
+ validator_class.new(config)
398
+ end
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end