hammer_cli 0.16.0 → 0.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 836795c65ef362f852b3eed29126508d0bf23927
4
- data.tar.gz: 8300578ae0b731724817506ee20bda1ea458b71b
2
+ SHA256:
3
+ metadata.gz: 025b01a30fd003ed027068563ad6971163cdfaea6a65749d775837df3552ea5e
4
+ data.tar.gz: 99177e6377a8fecf639cfe80fd6c79868a2ef24b6638c692d54da3ed8297c9d6
5
5
  SHA512:
6
- metadata.gz: 848a8e9636cb5c8e8dea817de554d943f73db10399d897d770d71a7df5cfc66d5c843b351c64587781dd93ae5be81ecd17afe0e2ffa4e0ce7192d55e4d5fd732
7
- data.tar.gz: aa6bbeb277ae28f0d1c1626f78773dcfbd64a4b81de89609a5fa64eff4a7a607e582f464fb2b58bc7df30b3770c877b2188b62ed14030992630a21932e0b9cf8
6
+ metadata.gz: b9d259322165498e492bed10e91e8a73b200b8ca78a72fd3011af3cb7fec4d73861274f9a6c23d597c3b4294a25facc7cec1bb4e02c719856a6ecc1de4c15387
7
+ data.tar.gz: 8e09c9a846262861b6a86f9ea4dc4dbaeb9bde089f35fe6f8f30b2066ab4e611ed9a8557b25d063bd2158a9811d5f73f4541f65b57e4104072551055015928b4
@@ -0,0 +1,115 @@
1
+ Extend an existing command
2
+ -------------------------
3
+
4
+ Each command can be easily extended with one ore more `HammerCLI::CommandExtensions`:
5
+ - Define the extension
6
+ ```ruby
7
+ class Extensions < HammerCLI::CommandExtensions
8
+ # Tells if those extensions are inherited by subcommands (false by default)
9
+ # Can be changed for specific object, e.g. MyExtensions.new(inheritable: false)
10
+ inheritable true
11
+ # Simply add a new option to a command is being extended
12
+ option(option_params)
13
+ # Extend hash with data returned from server before it is printed
14
+ before_print do |data|
15
+ # data modifications
16
+ end
17
+ # Extend command's output definition
18
+ output do |definition|
19
+ # output definition modifications
20
+ end
21
+ # Extend command's help definition
22
+ help do |h|
23
+ # help modifications
24
+ end
25
+ # Extend hash with headers before request is sent
26
+ request_headers do |headers|
27
+ # headers modifications
28
+ end
29
+ # Extend hash with options before request is sent
30
+ request_options do |options|
31
+ # options modifications
32
+ end
33
+ # Extend hash with params before request is sent
34
+ request_params do |params|
35
+ # params modifications
36
+ end
37
+ # Extend option sources
38
+ option_sources do |sources, command|
39
+ # no need to call super method
40
+ # simply add your sources to sources variable
41
+ end
42
+ end
43
+ ```
44
+ - Extend the command
45
+ ```ruby
46
+ MyCommand.extend_with(Extensions.new)
47
+ # Also it is possible to specify exact extensions you want to apply
48
+ # This can be useful when you want to use several extensions
49
+ MyCommand.extend_with(
50
+ # Apply only the output extensions from Extensions
51
+ Extensions.new(only: :output),
52
+ # Apply all except output and help extensions from OtherExtensions
53
+ OtherExtensions.new(except: [:output, :help])
54
+ )
55
+ ```
56
+
57
+ __NOTE:__
58
+ - `request_*` extensions are applied before sending a request to the server
59
+ - `option`, `output`, `help` extensions are applied right away after the command is extended with `extend_with`
60
+ - `before_print` extensions are applied right away after the server returns the data
61
+
62
+ #### Example
63
+ ```ruby
64
+ class MyCommandExtensions < HammerCLI::CommandExtensions
65
+
66
+ option ['--new-option'], 'TYPE', _('Option description')
67
+
68
+ before_print do |data|
69
+ data['results'].each do |result|
70
+ result['status'] = process_errors(result['errors'])
71
+ end
72
+ end
73
+ # To use your custom helpers define them as class methods
74
+ def self.process_errors(errors)
75
+ errors.empty? ? 'ok' : 'fail'
76
+ end
77
+
78
+ output do |definition|
79
+ definition.append do
80
+ field nil, 'Statuses', Fields::Label do
81
+ from 'results' do
82
+ field 'status', _('Status')
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ help do |h|
89
+ h.text('Something useful')
90
+ end
91
+
92
+ request_headers do |headers|
93
+ headers[:ssl] = true
94
+ end
95
+
96
+ request_options do |options|
97
+ options[:with_authentication] = true
98
+ end
99
+
100
+ request_params do |params|
101
+ params[:thin] = false
102
+ end
103
+
104
+ option_sources do |sources, command|
105
+ sources.find_by_name('IdResolution').insert_relative(
106
+ :after,
107
+ 'IdParams',
108
+ HammerCLIForeman::OptionSources::PuppetEnvironmentParams.new(command)
109
+ )
110
+ sources
111
+ end
112
+ end
113
+
114
+ MyCommand.extend_with(MyCommandExtensions.new)
115
+ ```
@@ -435,7 +435,34 @@ HammerCLI::MainCommand.lazy_subcommand(
435
435
  )
436
436
  ```
437
437
 
438
+ ### Deprecated commands
439
+ To mark a command as deprecated use the `:warning` option as follows:
438
440
 
441
+ ```ruby
442
+ HammerCLI::MainCommand.lazy_subcommand(
443
+ 'say', # command's name
444
+ 'Say something', # description
445
+ 'HammerCLIHello::SayCommand', # command's class in a string
446
+ 'hammer_cli_hello/say', # require path of the file
447
+ warning: _('This command is deprecated and will be removed.')
448
+ )
449
+ ```
450
+ Or you can mark a command in its definition:
451
+
452
+ ```ruby
453
+ class SayCommand < HammerCLI::AbstractCommand
454
+
455
+ class HelloCommand < HammerCLI::AbstractCommand
456
+ warning 'This command is deprecated and will be removed.'
457
+ command_name 'hello'
458
+ desc 'Say Hello World!'
459
+ # ...
460
+ end
461
+ # ...
462
+
463
+ autoload_subcommands
464
+ end
465
+ ```
439
466
  ### Printing some output
440
467
  We've mentioned above that it's not recommended practice to print output
441
468
  directly with `puts` in Hammer. The reason is we separate definition
@@ -10,6 +10,7 @@ Contents:
10
10
  - [Creating commands](creating_commands.md#create-your-first-command)
11
11
  - [Help modification](help_modification.md#modify-an-existing-help)
12
12
  - [Commands modification](commands_modification.md#modify-an-existing-command)
13
+ - [Command extensions](commands_extension.md#extend-an-existing-command)
13
14
  - [Option builders](option_builders.md#option-builders)
14
15
  - [Creating ApiPie commands](creating_apipie_commands.md#creating-commands-for-restful-api-with-apipie)
15
16
  - [Development tips](development_tips.md#development-tips)
data/doc/release_notes.md CHANGED
@@ -1,5 +1,11 @@
1
1
  Release notes
2
2
  =============
3
+ ### 0.17.0 (2019-04-24)
4
+ * Extended capabilities for list type options ([#26120](http://projects.theforeman.org/issues/26120))
5
+ * Document how to deprecate commands ([PR #301](https://github.com/theforeman/hammer-cli/pull/301)) ([#24964](http://projects.theforeman.org/issues/24964))
6
+ * Extensible commands in hammer ([#21635](http://projects.theforeman.org/issues/21635))
7
+ * Table output allows to hide fields with no error ([#26453](http://projects.theforeman.org/issues/26453))
8
+
3
9
  ### 0.16.0 (2019-01-16)
4
10
  * Fixed instructions in i18n docs ([#25724](http://projects.theforeman.org/issues/25724))
5
11
  * Option validators can be mixed with sources ([#22253](http://projects.theforeman.org/issues/22253))
@@ -11,6 +11,7 @@ require 'hammer_cli/subcommand'
11
11
  require 'hammer_cli/options/matcher'
12
12
  require 'hammer_cli/help/builder'
13
13
  require 'hammer_cli/help/text_builder'
14
+ require 'hammer_cli/command_extensions'
14
15
  require 'logging'
15
16
 
16
17
  module HammerCLI
@@ -24,6 +25,38 @@ module HammerCLI
24
25
  def help_extension_blocks
25
26
  @help_extension_blocks ||= []
26
27
  end
28
+
29
+ def command_extensions
30
+ @command_extensions = @command_extensions || inherited_command_extensions || []
31
+ @command_extensions
32
+ end
33
+
34
+ def inherited_command_extensions
35
+ extensions = nil
36
+ if superclass.respond_to?(:command_extensions)
37
+ parent_extensions = superclass.command_extensions.select(&:inheritable?)
38
+ extensions = parent_extensions.dup unless parent_extensions.empty?
39
+ end
40
+ extensions
41
+ end
42
+
43
+ def extend_options_help(option)
44
+ extend_help do |h|
45
+ begin
46
+ h.find_item(:s_option_details)
47
+ rescue ArgumentError
48
+ option_details = HammerCLI::Help::Section.new(_('Option details'), nil, id: :s_option_details, richtext: true)
49
+ option_details.definition << HammerCLI::Help::Text.new(
50
+ _('Following parameters accept format defined by its schema (bold are required):')
51
+ )
52
+ h.definition.unshift(option_details)
53
+ ensure
54
+ h.find_item(:s_option_details).definition << HammerCLI::Help::List.new([
55
+ [option.switches.last, option.value_formatter.schema.description]
56
+ ])
57
+ end
58
+ end
59
+ end
27
60
  end
28
61
 
29
62
  def adapter
@@ -154,6 +187,21 @@ module HammerCLI
154
187
  declared_options << option
155
188
  block ||= option.default_conversion_block
156
189
  define_accessors_for(option, &block)
190
+ extend_options_help(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
191
+ end
192
+ end
193
+
194
+ def self.extend_with(*extensions)
195
+ extensions.each do |extension|
196
+ unless extension.is_a?(HammerCLI::CommandExtensions)
197
+ raise ArgumentError, _('Command extensions should be inherited from %s.') % HammerCLI::CommandExtensions
198
+ end
199
+ extension.delegatee(self)
200
+ extension.extend_options(self)
201
+ extension.extend_output(self)
202
+ extension.extend_help(self)
203
+ logger('Extensions').info "Applied #{extension.details} on #{self}."
204
+ command_extensions << extension
157
205
  end
158
206
  end
159
207
 
@@ -226,10 +274,15 @@ module HammerCLI
226
274
  @name || (superclass.respond_to?(:command_name) ? superclass.command_name : nil)
227
275
  end
228
276
 
277
+ def self.warning(message = nil)
278
+ @warning_msg = message if message
279
+ @warning_msg
280
+ end
281
+
229
282
  def self.autoload_subcommands
230
283
  commands = constants.map { |c| const_get(c) }.select { |c| c <= HammerCLI::AbstractCommand }
231
284
  commands.each do |cls|
232
- subcommand cls.command_name, cls.desc, cls
285
+ subcommand(cls.command_name, cls.desc, cls, warning: cls.warning)
233
286
  end
234
287
  end
235
288
 
@@ -269,7 +322,11 @@ module HammerCLI
269
322
  sources << HammerCLI::Options::Sources::CommandLine.new(self)
270
323
  sources << HammerCLI::Options::Sources::SavedDefaults.new(context[:defaults], logger) if context[:use_defaults]
271
324
 
272
- HammerCLI::Options::ProcessorList.new([sources])
325
+ sources = HammerCLI::Options::ProcessorList.new([sources])
326
+ self.class.command_extensions.each do |extension|
327
+ extension.extend_option_sources(sources, self)
328
+ end
329
+ sources
273
330
  end
274
331
 
275
332
  def add_validators(sources)
@@ -47,11 +47,10 @@ module HammerCLI::Apipie
47
47
  protected
48
48
 
49
49
  def send_request
50
- if resource && resource.has_action?(action)
51
- resource.call(action, request_params, request_headers, request_options)
52
- else
53
- raise HammerCLI::OperationNotSupportedError, "The server does not support such operation."
50
+ unless resource && resource.has_action?(action)
51
+ raise HammerCLI::OperationNotSupportedError, _('The server does not support such operation.')
54
52
  end
53
+ extended_data(resource.call(action, *extended_request))
55
54
  end
56
55
 
57
56
  def request_headers
@@ -90,5 +89,25 @@ module HammerCLI::Apipie
90
89
  end
91
90
  end
92
91
 
92
+ private
93
+
94
+ def extended_request
95
+ params = request_params
96
+ headers = request_headers
97
+ options = request_options
98
+ self.class.command_extensions.each do |extension|
99
+ extension.extend_request_headers(headers)
100
+ extension.extend_request_options(options)
101
+ extension.extend_request_params(params)
102
+ end
103
+ [params, headers, options]
104
+ end
105
+
106
+ def extended_data(data)
107
+ self.class.command_extensions.each do |extension|
108
+ extension.extend_before_print(data)
109
+ end
110
+ data
111
+ end
93
112
  end
94
113
  end
@@ -65,7 +65,11 @@ module HammerCLI::Apipie
65
65
  opts = {}
66
66
  opts[:required] = true if (param.required? and require_options?)
67
67
  if param.expected_type.to_s == 'array'
68
- opts[:format] = HammerCLI::Options::Normalizers::List.new
68
+ if param.params.empty?
69
+ opts[:format] = HammerCLI::Options::Normalizers::List.new
70
+ else
71
+ opts[:format] = HammerCLI::Options::Normalizers::ListNested.new(param.params)
72
+ end
69
73
  elsif param.expected_type.to_s == 'boolean' || param.validator.to_s == 'boolean'
70
74
  opts[:format] = HammerCLI::Options::Normalizers::Bool.new
71
75
  elsif param.expected_type.to_s == 'string' && param.validator =~ /Must be one of: (.*)\./
@@ -0,0 +1,222 @@
1
+ module HammerCLI
2
+ class CommandExtensions
3
+ class << self
4
+ attr_accessor :delegatee
5
+
6
+ def logger
7
+ Logging.logger[to_s]
8
+ end
9
+
10
+ def inheritable?
11
+ @inheritable
12
+ end
13
+ end
14
+
15
+ ALLOWED_EXTENSIONS = %i[
16
+ option command_options before_print data output help request
17
+ request_headers headers request_options options request_params params
18
+ option_sources
19
+ ].freeze
20
+
21
+ def initialize(options = {})
22
+ @only = options[:only] || ALLOWED_EXTENSIONS
23
+ @only = [@only] unless @only.is_a?(Array)
24
+ @except = options[:except] || []
25
+ @except = [@except] unless @except.is_a?(Array)
26
+ @inheritable = options[:inheritable]
27
+ end
28
+
29
+ def inheritable?
30
+ return @inheritable unless @inheritable.nil?
31
+
32
+ self.class.inheritable? || false
33
+ end
34
+
35
+ def self.method_missing(message, *args, &block)
36
+ if @delegatee
37
+ @delegatee.send(message, *args, &block)
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ # DSL
44
+
45
+ def self.inheritable(boolean)
46
+ @inheritable = boolean
47
+ end
48
+
49
+ def self.option(switches, type, description, opts = {}, &block)
50
+ @options ||= []
51
+ @options << { switches: switches,
52
+ type: type,
53
+ description: description,
54
+ opts: opts, block: block }
55
+ end
56
+
57
+ def self.before_print(&block)
58
+ @before_print_block = block
59
+ end
60
+
61
+ def self.output(&block)
62
+ @output_extension_block = block
63
+ end
64
+
65
+ def self.help(&block)
66
+ @help_extension_block = block
67
+ end
68
+
69
+ def self.request_headers(&block)
70
+ @request_headers_block = block
71
+ end
72
+
73
+ def self.request_options(&block)
74
+ @request_options_block = block
75
+ end
76
+
77
+ def self.request_params(&block)
78
+ @request_params_block = block
79
+ end
80
+
81
+ def self.option_sources(&block)
82
+ @option_sources_block = block
83
+ end
84
+
85
+ # Object
86
+
87
+ def extend_options(command_class)
88
+ allowed = @only & %i[command_options option]
89
+ return if allowed.empty? || (allowed & @except).any?
90
+
91
+ self.class.extend_options(command_class)
92
+ end
93
+
94
+ def extend_before_print(data)
95
+ allowed = @only & %i[before_print data]
96
+ return if allowed.empty? || (allowed & @except).any?
97
+
98
+ self.class.extend_before_print(data)
99
+ end
100
+
101
+ def extend_output(command_class)
102
+ allowed = @only & %i[output]
103
+ return if allowed.empty? || (allowed & @except).any?
104
+
105
+ self.class.extend_output(command_class)
106
+ end
107
+
108
+ def extend_help(command_class)
109
+ allowed = @only & %i[help]
110
+ return if allowed.empty? || (allowed & @except).any?
111
+
112
+ self.class.extend_help(command_class)
113
+ end
114
+
115
+ def extend_request_headers(headers)
116
+ allowed = @only & %i[request_headers headers request]
117
+ return if allowed.empty? || (allowed & @except).any?
118
+
119
+ self.class.extend_request_headers(headers)
120
+ end
121
+
122
+ def extend_request_options(options)
123
+ allowed = @only & %i[request_options options request]
124
+ return if allowed.empty? || (allowed & @except).any?
125
+
126
+ self.class.extend_request_options(options)
127
+ end
128
+
129
+ def extend_request_params(params)
130
+ allowed = @only & %i[request_params params request]
131
+ return if allowed.empty? || (allowed & @except).any?
132
+
133
+ self.class.extend_request_params(params)
134
+ end
135
+
136
+ def extend_option_sources(sources, command = nil)
137
+ allowed = @only & %i[option_sources]
138
+ return if allowed.empty? || (allowed & @except).any?
139
+
140
+ self.class.extend_option_sources(sources, command)
141
+ end
142
+
143
+ def delegatee(command_class)
144
+ self.class.delegatee = command_class
145
+ end
146
+
147
+ def details
148
+ except = @except.empty? ? '*nothing*' : @except
149
+ details = if @only == ALLOWED_EXTENSIONS
150
+ "*all* except #{except}"
151
+ else
152
+ "#{@only} only"
153
+ end
154
+ "#{self.class} for #{details}"
155
+ end
156
+
157
+ # Class
158
+
159
+ def self.extend_options(command_class)
160
+ return if @options.nil?
161
+
162
+ @options.each do |option|
163
+ command_class.send(:option,
164
+ option[:switches],
165
+ option[:type],
166
+ option[:description],
167
+ option[:opts],
168
+ &option[:block])
169
+ logger.debug("Added option for #{command_class}: #{option}")
170
+ end
171
+ end
172
+
173
+ def self.extend_before_print(data)
174
+ return if @before_print_block.nil?
175
+
176
+ @before_print_block.call(data)
177
+ logger.debug("Called block for #{@delegatee} data:\n\t#{@before_print_block}")
178
+ end
179
+
180
+ def self.extend_output(command_class)
181
+ return if @output_extension_block.nil?
182
+
183
+ @output_extension_block.call(command_class.output_definition)
184
+ logger.debug("Called block for #{@delegatee} output definition:\n\t#{@output_extension_block}")
185
+ end
186
+
187
+ def self.extend_help(command_class)
188
+ return if @help_extension_block.nil?
189
+
190
+ command_class.help_extension_blocks << @help_extension_block
191
+ logger.debug("Saved block for #{@delegatee} help definition:\n\t#{@help_extension_block}")
192
+ end
193
+
194
+ def self.extend_request_headers(headers)
195
+ return if @request_headers_block.nil?
196
+
197
+ @request_headers_block.call(headers)
198
+ logger.debug("Called block for #{@delegatee} request headers:\n\t#{@request_headers_block}")
199
+ end
200
+
201
+ def self.extend_request_options(options)
202
+ return if @request_options_block.nil?
203
+
204
+ @request_options_block.call(options)
205
+ logger.debug("Called block for #{@delegatee} request options:\n\t#{@request_options_block}")
206
+ end
207
+
208
+ def self.extend_request_params(params)
209
+ return if @request_params_block.nil?
210
+
211
+ @request_params_block.call(params)
212
+ logger.debug("Called block for #{@delegatee} request params:\n\t#{@request_params_block}")
213
+ end
214
+
215
+ def self.extend_option_sources(sources, command = nil)
216
+ return if @option_sources_block.nil?
217
+
218
+ @option_sources_block.call(sources, command)
219
+ logger.debug("Called block for #{@delegatee} option sources:\n\t#{@option_sources_block}")
220
+ end
221
+ end
222
+ end
@@ -27,11 +27,12 @@ module HammerCLI
27
27
 
28
28
  class KeyValueList < AbstractNormalizer
29
29
 
30
- PAIR_RE = '([^,=]+)=([^,\[]+|\[[^\[\]]*\])'
30
+ PAIR_RE = '([^,=]+)=([^,\{\[]+|[\{\[][^\{\}\[\]]*[\}\]])'
31
31
  FULL_RE = "^((%s)[,]?)+$" % PAIR_RE
32
32
 
33
33
  def description
34
- _("Comma-separated list of key=value")
34
+ _("Comma-separated list of key=value.") + "\n" +
35
+ _("JSON is acceptable and preferred way for complex parameters")
35
36
  end
36
37
 
37
38
  def format(val)
@@ -60,7 +61,11 @@ module HammerCLI
60
61
  result = {}
61
62
  val.scan(Regexp.new(PAIR_RE)) do |key, value|
62
63
  value = value.strip
63
- value = value.scan(/[^,\[\]]+/) if value.start_with?('[')
64
+ if value.start_with?('[')
65
+ value = value.scan(/[^,\[\]]+/)
66
+ elsif value.start_with?('{')
67
+ value = parse_key_value(value[1...-1])
68
+ end
64
69
 
65
70
  result[key.strip] = strip_value(value)
66
71
  end
@@ -72,6 +77,10 @@ module HammerCLI
72
77
  value.map do |item|
73
78
  strip_chars(item.strip, '"\'')
74
79
  end
80
+ elsif value.is_a? Hash
81
+ value.map do |key, val|
82
+ [strip_chars(key.strip, '"\''), strip_chars(val.strip, '"\'')]
83
+ end.to_h
75
84
  else
76
85
  strip_chars(value.strip, '"\'')
77
86
  end
@@ -86,14 +95,55 @@ module HammerCLI
86
95
 
87
96
  class List < AbstractNormalizer
88
97
  def description
89
- _("Comma separated list of values. Values containing comma should be quoted or escaped with backslash")
98
+ _("Comma separated list of values. Values containing comma should be quoted or escaped with backslash.") + "\n" +
99
+ _("JSON is acceptable and preferred way for complex parameters")
90
100
  end
91
101
 
92
102
  def format(val)
93
- (val.is_a?(String) && !val.empty?) ? HammerCLI::CSVParser.new.parse(val) : []
103
+ return [] unless val.is_a?(String) && !val.empty?
104
+ begin
105
+ JSON.parse(val)
106
+ rescue JSON::ParserError
107
+ HammerCLI::CSVParser.new.parse(val)
108
+ end
94
109
  end
95
110
  end
96
111
 
112
+ class ListNested < AbstractNormalizer
113
+ class Schema < Array
114
+ def description
115
+ '"' + reduce([]) do |schema, nested_param|
116
+ name = nested_param.name
117
+ name = HighLine.color(name, :bold) if nested_param.required?
118
+ schema << "#{name}=#{nested_param.expected_type}"
119
+ end.join('\,').concat(', ... "')
120
+ end
121
+ end
122
+
123
+ attr_reader :schema
124
+
125
+ def initialize(schema)
126
+ @schema = Schema.new(schema)
127
+ end
128
+
129
+ def description
130
+ _("Comma separated list of values defined by a schema. See Option details section below.") + "\n" +
131
+ _("JSON is acceptable and preferred way for complex parameters")
132
+ end
133
+
134
+ def format(val)
135
+ return [] unless val.is_a?(String) && !val.empty?
136
+ begin
137
+ JSON.parse(val)
138
+ rescue JSON::ParserError
139
+ HammerCLI::CSVParser.new.parse(val).inject([]) do |results, item|
140
+ next if item.empty?
141
+
142
+ results << KeyValueList.new.format(item)
143
+ end
144
+ end
145
+ end
146
+ end
97
147
 
98
148
  class Number < AbstractNormalizer
99
149
 
@@ -27,9 +27,9 @@ module HammerCLI
27
27
  attr_accessor :deprecated_switches
28
28
 
29
29
  def initialize(switches, type, description, options = {})
30
- self.value_formatter = options.delete(:format) || HammerCLI::Options::Normalizers::Default.new
31
- self.context_target = options.delete(:context_target)
32
- self.deprecated_switches = options.delete(:deprecated)
30
+ self.value_formatter = options[:format] || HammerCLI::Options::Normalizers::Default.new
31
+ self.context_target = options[:context_target]
32
+ self.deprecated_switches = options[:deprecated]
33
33
  super
34
34
  end
35
35
 
@@ -53,8 +53,8 @@ module HammerCLI::Output::Adapter
53
53
  # and there is no --no-headers option
54
54
  output_stream.puts line unless formatted_collection.empty? || @context[:no_headers]
55
55
 
56
- if @context[:verbosity] >= collection.meta.pagination_verbosity &&
57
- collection.respond_to?(:meta) && collection.meta.pagination_set? &&
56
+ if collection.respond_to?(:meta) && collection.meta.pagination_set? &&
57
+ @context[:verbosity] >= collection.meta.pagination_verbosity &&
58
58
  collection.count < collection.meta.subtotal
59
59
  pages = (collection.meta.subtotal.to_f / collection.meta.per_page).ceil
60
60
  puts _("Page %{page} of %{total} (use --page and --per-page for navigation).") % {:page => collection.meta.page, :total => pages}
@@ -18,6 +18,7 @@ module HammerCLI
18
18
  end
19
19
 
20
20
  def subcommand_class
21
+ @warning ||= @subcommand_class.warning
21
22
  warn(@warning) if @warning
22
23
  @subcommand_class
23
24
  end
@@ -38,12 +39,13 @@ module HammerCLI
38
39
  end
39
40
 
40
41
  def subcommand_class
41
- warn(@warning) if @warning
42
- if !@loaded
42
+ unless @loaded
43
43
  require @path
44
44
  @loaded = true
45
45
  @constantized_class = @subcommand_class.constantize
46
46
  end
47
+ @warning ||= @constantized_class.warning
48
+ warn(@warning) if @warning
47
49
  @constantized_class
48
50
  end
49
51
  end
@@ -1,5 +1,5 @@
1
1
  module HammerCLI
2
2
  def self.version
3
- @version ||= Gem::Version.new '0.16.0'
3
+ @version ||= Gem::Version.new '0.17.0'
4
4
  end
5
5
  end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -499,5 +499,33 @@ describe HammerCLI::AbstractCommand do
499
499
  assert_equal ['Validator1'], result.map(&:name)
500
500
  end
501
501
  end
502
- end
503
502
 
503
+ describe '#extend_with' do
504
+ class Extensions < HammerCLI::CommandExtensions
505
+ option '--flag', 'FLAG', 'flag'
506
+ output { |definition| definition.append(Fields::Field.new) }
507
+ help { |h| h.text('text') }
508
+ end
509
+ class Cmd < HammerCLI::AbstractCommand
510
+ def execute
511
+ HammerCLI::EX_OK
512
+ end
513
+ end
514
+ let(:extension) { Extensions.new }
515
+ let(:output_extension) { Extensions.new(only: :output) }
516
+ let(:cmd) { Class.new(Cmd) }
517
+
518
+ it 'should extend command with option, output, help right away' do
519
+ cmd.extend_with(extension)
520
+ opt = cmd.find_option('--flag')
521
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
522
+ cmd.output_definition.empty?.must_equal false
523
+ cmd.new({}).help.must_match(/.*text.*/)
524
+ end
525
+
526
+ it 'should store more than one extension' do
527
+ cmd.extend_with(extension, output_extension)
528
+ cmd.command_extensions.size.must_equal 2
529
+ end
530
+ end
531
+ end
@@ -0,0 +1,189 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ describe HammerCLI::CommandExtensions do
4
+ class CustomCmd < HammerCLI::Apipie::Command
5
+ def execute
6
+ HammerCLI::EX_OK
7
+ end
8
+
9
+ def request_headers
10
+ {}
11
+ end
12
+
13
+ def request_options
14
+ {}
15
+ end
16
+
17
+ def request_params
18
+ {}
19
+ end
20
+
21
+ public :extended_request, :extended_data
22
+ end
23
+
24
+ class CmdExtensions < HammerCLI::CommandExtensions
25
+ option '--ext', 'EXT', 'ext'
26
+ before_print do |data|
27
+ data['key'] = 'value'
28
+ end
29
+ output do |definition|
30
+ definition.append(Fields::Field.new)
31
+ definition.append(Fields::Field.new)
32
+ end
33
+ help do |h|
34
+ h.section('Section')
35
+ h.text('text')
36
+ end
37
+ request_headers do |headers|
38
+ headers[:ssl] = true
39
+ end
40
+ request_options do |options|
41
+ options[:with_authentication] = true
42
+ end
43
+ request_params do |params|
44
+ params[:thin] = true
45
+ end
46
+ end
47
+
48
+ let(:cmd) { Class.new(CustomCmd) }
49
+
50
+ context 'only' do
51
+ it 'should extend options only' do
52
+ cmd.extend_with(CmdExtensions.new(only: :option))
53
+ opt = cmd.find_option('--ext')
54
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
55
+ cmd.output_definition.empty?.must_equal true
56
+ end
57
+
58
+ it 'should extend output only' do
59
+ cmd.extend_with(CmdExtensions.new(only: :output))
60
+ cmd.output_definition.empty?.must_equal false
61
+ opt = cmd.find_option('--ext')
62
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal false
63
+ end
64
+
65
+ it 'should extend help only' do
66
+ cmd.extend_with(CmdExtensions.new(only: :help))
67
+ cmd.new({}).help.must_match(/.*Section.*/)
68
+ cmd.new({}).help.must_match(/.*text.*/)
69
+ end
70
+
71
+ it 'should extend params only' do
72
+ cmd.extend_with(CmdExtensions.new(only: :request_params))
73
+ cmd.new({}).extended_request[0].must_equal(thin: true)
74
+ cmd.new({}).extended_request[1].must_equal({})
75
+ cmd.new({}).extended_request[2].must_equal({})
76
+ end
77
+
78
+ it 'should extend headers only' do
79
+ cmd.extend_with(CmdExtensions.new(only: :request_headers))
80
+ cmd.new({}).extended_request[0].must_equal({})
81
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
82
+ cmd.new({}).extended_request[2].must_equal({})
83
+ end
84
+
85
+ it 'should extend options only' do
86
+ cmd.extend_with(CmdExtensions.new(only: :request_options))
87
+ cmd.new({}).extended_request[0].must_equal({})
88
+ cmd.new({}).extended_request[1].must_equal({})
89
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
90
+ end
91
+
92
+ it 'should extend params and options and headers' do
93
+ cmd.extend_with(CmdExtensions.new(only: :request))
94
+ cmd.new({}).extended_request[0].must_equal(thin: true)
95
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
96
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
97
+ end
98
+
99
+ it 'should extend data only' do
100
+ cmd.extend_with(CmdExtensions.new(only: :data))
101
+ cmd.new({}).help.wont_match(/.*Section.*/)
102
+ cmd.new({}).help.wont_match(/.*text.*/)
103
+ cmd.output_definition.empty?.must_equal true
104
+ opt = cmd.find_option('--ext')
105
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal false
106
+ cmd.new({}).extended_request[0].must_equal({})
107
+ cmd.new({}).extended_request[1].must_equal({})
108
+ cmd.new({}).extended_request[2].must_equal({})
109
+ cmd.new({}).extended_data({}).must_equal('key' => 'value')
110
+ end
111
+ end
112
+
113
+ context 'except' do
114
+ it 'should extend all except options' do
115
+ cmd.extend_with(CmdExtensions.new(except: :option))
116
+ opt = cmd.find_option('--ext')
117
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal false
118
+ cmd.output_definition.empty?.must_equal false
119
+ cmd.new({}).extended_request[0].must_equal(thin: true)
120
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
121
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
122
+ end
123
+
124
+ it 'should extend all except output' do
125
+ cmd.extend_with(CmdExtensions.new(except: :output))
126
+ cmd.output_definition.empty?.must_equal true
127
+ opt = cmd.find_option('--ext')
128
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
129
+ cmd.new({}).extended_request[0].must_equal(thin: true)
130
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
131
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
132
+ end
133
+
134
+ it 'should extend all except help' do
135
+ cmd.extend_with(CmdExtensions.new(except: :help))
136
+ cmd.new({}).help.wont_match(/.*Section.*/)
137
+ cmd.new({}).help.wont_match(/.*text.*/)
138
+ cmd.output_definition.empty?.must_equal false
139
+ opt = cmd.find_option('--ext')
140
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
141
+ cmd.new({}).extended_request[0].must_equal(thin: true)
142
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
143
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
144
+ end
145
+
146
+ it 'should extend all except params' do
147
+ cmd.extend_with(CmdExtensions.new(except: :request_params))
148
+ cmd.new({}).extended_request[0].must_equal({})
149
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
150
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
151
+ end
152
+
153
+ it 'should extend all except headers' do
154
+ cmd.extend_with(CmdExtensions.new(except: :request_headers))
155
+ cmd.new({}).extended_request[0].must_equal(thin: true)
156
+ cmd.new({}).extended_request[1].must_equal({})
157
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
158
+ end
159
+
160
+ it 'should extend all except options' do
161
+ cmd.extend_with(CmdExtensions.new(except: :request_options))
162
+ cmd.new({}).extended_request[0].must_equal(thin: true)
163
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
164
+ cmd.new({}).extended_request[2].must_equal({})
165
+ end
166
+
167
+ it 'should extend all except params and options and headers' do
168
+ cmd.extend_with(CmdExtensions.new(except: :request))
169
+ cmd.new({}).extended_request[0].must_equal({})
170
+ cmd.new({}).extended_request[1].must_equal({})
171
+ cmd.new({}).extended_request[2].must_equal({})
172
+ end
173
+
174
+ it 'should extend all except data' do
175
+ cmd.extend_with(CmdExtensions.new(except: :data))
176
+ cmd.new({}).help.must_match(/.*Section.*/)
177
+ cmd.new({}).help.must_match(/.*text.*/)
178
+ cmd.output_definition.empty?.must_equal false
179
+ opt = cmd.find_option('--ext')
180
+ opt.is_a?(HammerCLI::Options::OptionDefinition).must_equal true
181
+ cmd.new({}).extended_request[0].must_equal(thin: true)
182
+ cmd.new({}).extended_request[1].must_equal(ssl: true)
183
+ cmd.new({}).extended_request[2].must_equal(with_authentication: true)
184
+ cmd.new({}).extended_data({}).must_equal({})
185
+ end
186
+ end
187
+
188
+
189
+ end
@@ -14,21 +14,21 @@ describe HammerCLI::Options::Normalizers do
14
14
  end
15
15
 
16
16
  describe 'default' do
17
-
17
+
18
18
  let(:formatter) { HammerCLI::Options::Normalizers::Default.new }
19
-
19
+
20
20
  it "should not change any value" do
21
21
  formatter.format('value').must_equal 'value'
22
22
  end
23
-
23
+
24
24
  it "should not change nil value" do
25
25
  formatter.format(nil).must_be_nil
26
26
  end
27
-
27
+
28
28
  it "has empty description" do
29
29
  formatter.description.must_equal ''
30
30
  end
31
-
31
+
32
32
  it "has empty completion" do
33
33
  formatter.complete('test').must_equal []
34
34
  end
@@ -69,8 +69,85 @@ describe HammerCLI::Options::Normalizers do
69
69
  it "should catch quoting errors" do
70
70
  proc { formatter.format('1,"3,4""s') }.must_raise ArgumentError
71
71
  end
72
+
73
+ it "should accept and parse JSON" do
74
+ formatter.format("{\"name\":\"bla\", \"value\":1}").must_equal(
75
+ JSON.parse("{\"name\":\"bla\", \"value\":1}")
76
+ )
77
+ end
72
78
  end
73
79
 
80
+ describe 'list_nested' do
81
+ let(:params_raw) do
82
+ [
83
+ {name: 'name', expected_type: :string, validator: 'string', description: ''},
84
+ {name: 'value', expected_type: :string, validator: 'string', description: ''}
85
+ ]
86
+ end
87
+ let(:params) do
88
+ [
89
+ ApipieBindings::Param.new(params_raw.first),
90
+ ApipieBindings::Param.new(params_raw.last)
91
+ ]
92
+ end
93
+ let(:param) do
94
+ ApipieBindings::Param.new({
95
+ name: 'array', expected_type: :array, validator: 'nested', description: '',
96
+ params: params_raw
97
+ })
98
+ end
99
+ let(:formatter) { HammerCLI::Options::Normalizers::ListNested.new(param.params) }
100
+
101
+ it "should accept and parse JSON" do
102
+ formatter.format("{\"name\":\"bla\", \"value\":1}").must_equal(
103
+ JSON.parse("{\"name\":\"bla\", \"value\":1}")
104
+ )
105
+ end
106
+
107
+ it "should parse simple input" do
108
+ formatter.format("name=test\\,value=1,name=other\\,value=2").must_equal(
109
+ [{'name' => 'test', 'value' => '1'}, {'name' => 'other', 'value' => '2'}]
110
+ )
111
+ end
112
+
113
+ it "should parse unexpected input" do
114
+ formatter.format("name=test\\,value=1,name=other\\,value=2,unexp=doe").must_equal(
115
+ [
116
+ {'name' => 'test', 'value' => '1'}, {'name' => 'other', 'value' => '2'},
117
+ {'unexp' => 'doe'}
118
+ ]
119
+ )
120
+ end
121
+
122
+ it "should accept arrays" do
123
+ formatter.format("name=test\\,value=1,name=other\\,value=[1\\,2\\,3]").must_equal(
124
+ [{'name' => 'test', 'value' => '1'}, {'name' => 'other', 'value' => ['1', '2', '3']}]
125
+ )
126
+ end
127
+
128
+ it "should accept hashes" do
129
+ formatter.format(
130
+ "name=test\\,value={key=key1\\,value=1},name=other\\,value={key=key2\\,value=2}"
131
+ ).must_equal(
132
+ [
133
+ {'name' => 'test', 'value' => {'key' => 'key1', 'value' => '1'}},
134
+ {'name' => 'other', 'value' => {'key' => 'key2', 'value' => '2'}},
135
+ ]
136
+ )
137
+ end
138
+
139
+ it "should accept combined input" do
140
+ formatter.format(
141
+ "name=foo\\,value=1\\,adds=[1\\,2\\,3]\\,cpu={name=ddd\\,type=abc}," \
142
+ "name=bar\\,value=2\\,adds=[2\\,2\\,2]\\,cpu={name=ccc\\,type=cba}"
143
+ ).must_equal(
144
+ [
145
+ {'name' => 'foo', 'value' => '1', 'adds' => ['1','2','3'], 'cpu' => {'name' => 'ddd', 'type' => 'abc'}},
146
+ {'name' => 'bar', 'value' => '2', 'adds' => ['2','2','2'], 'cpu' => {'name' => 'ccc', 'type' => 'cba'}}
147
+ ]
148
+ )
149
+ end
150
+ end
74
151
 
75
152
  describe 'key_value_list' do
76
153
 
@@ -133,6 +210,16 @@ describe HammerCLI::Options::Normalizers do
133
210
  formatter.format("a=1,b=[],c=3").must_equal({'a' => '1', 'b' => [], 'c' => '3'})
134
211
  end
135
212
 
213
+ it "should parse hash with one item" do
214
+ formatter.format("a=1,b={key=abc,value=abc},c=3").must_equal(
215
+ {'a' => '1', 'b' => {'key' => 'abc', 'value' => 'abc'}, 'c' => '3'}
216
+ )
217
+ end
218
+
219
+ it "should parse empty hash" do
220
+ formatter.format("a=1,b={},c=3").must_equal({'a' => '1', 'b' => {}, 'c' => '3'})
221
+ end
222
+
136
223
  it "should parse a comma separated string 2" do
137
224
  proc { formatter.format("a=1,b,c=3") }.must_raise ArgumentError
138
225
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hammer_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Bačovský
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-01-16 00:00:00.000000000 Z
12
+ date: 2019-04-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: clamp
@@ -156,6 +156,7 @@ extra_rdoc_files:
156
156
  - doc/release_notes.md
157
157
  - doc/commands_modification.md
158
158
  - doc/creating_commands.md
159
+ - doc/commands_extension.md
159
160
  - doc/development_tips.md
160
161
  - doc/developer_docs.md
161
162
  - doc/writing_a_plugin.md
@@ -178,6 +179,7 @@ files:
178
179
  - bin/hammer
179
180
  - config/cli.modules.d/module_config_template.yml
180
181
  - config/cli_config.template.yml
182
+ - doc/commands_extension.md
181
183
  - doc/commands_modification.md
182
184
  - doc/creating_apipie_commands.md
183
185
  - doc/creating_commands.md
@@ -209,6 +211,7 @@ files:
209
211
  - lib/hammer_cli/ca_cert_fetcher.rb
210
212
  - lib/hammer_cli/ca_cert_manager.rb
211
213
  - lib/hammer_cli/clamp.rb
214
+ - lib/hammer_cli/command_extensions.rb
212
215
  - lib/hammer_cli/completer.rb
213
216
  - lib/hammer_cli/connection.rb
214
217
  - lib/hammer_cli/context.rb
@@ -439,6 +442,7 @@ files:
439
442
  - test/unit/apipie/option_definition_test.rb
440
443
  - test/unit/apipie/test_helper.rb
441
444
  - test/unit/ca_cert_manager_test.rb
445
+ - test/unit/command_extensions_test.rb
442
446
  - test/unit/completer_test.rb
443
447
  - test/unit/connection_test.rb
444
448
  - test/unit/csv_parser_test.rb
@@ -510,7 +514,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
510
514
  version: '0'
511
515
  requirements: []
512
516
  rubyforge_project:
513
- rubygems_version: 2.6.14.1
517
+ rubygems_version: 2.7.9
514
518
  signing_key:
515
519
  specification_version: 4
516
520
  summary: Universal command-line interface
@@ -708,6 +712,7 @@ test_files:
708
712
  - test/unit/fixtures/defaults/defaults_dashed.yml
709
713
  - test/unit/defaults_test.rb
710
714
  - test/unit/messages_test.rb
715
+ - test/unit/command_extensions_test.rb
711
716
  - test/unit/ca_cert_manager_test.rb
712
717
  - test/unit/test_helper.rb
713
718
  - test/functional/help_test.rb