hammer_cli 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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