hammer_cli 0.0.9 → 0.0.10

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/doc/creating_apipie_commands.md +296 -0
  4. data/doc/creating_commands.md +547 -0
  5. data/doc/developer_docs.md +5 -926
  6. data/doc/development_tips.md +30 -0
  7. data/doc/writing_a_plugin.md +90 -0
  8. data/lib/hammer_cli/abstract.rb +31 -11
  9. data/lib/hammer_cli/apipie/resource.rb +14 -6
  10. data/lib/hammer_cli/apipie/write_command.rb +14 -5
  11. data/lib/hammer_cli/exception_handler.rb +7 -4
  12. data/lib/hammer_cli/options/normalizers.rb +27 -0
  13. data/lib/hammer_cli/output/adapter/abstract.rb +8 -8
  14. data/lib/hammer_cli/output/adapter/csv.rb +37 -4
  15. data/lib/hammer_cli/output/adapter/silent.rb +2 -2
  16. data/lib/hammer_cli/output/dsl.rb +3 -1
  17. data/lib/hammer_cli/output/output.rb +24 -19
  18. data/lib/hammer_cli/utils.rb +18 -0
  19. data/lib/hammer_cli/version.rb +1 -1
  20. data/lib/hammer_cli.rb +1 -0
  21. data/test/unit/abstract_test.rb +296 -0
  22. data/test/unit/apipie/command_test.rb +270 -0
  23. data/test/unit/apipie/fake_api.rb +101 -0
  24. data/test/unit/apipie/read_command_test.rb +34 -0
  25. data/test/unit/apipie/write_command_test.rb +38 -0
  26. data/test/unit/exception_handler_test.rb +45 -0
  27. data/test/unit/main_test.rb +47 -0
  28. data/test/unit/options/normalizers_test.rb +148 -0
  29. data/test/unit/options/option_definition_test.rb +43 -0
  30. data/test/unit/output/adapter/abstract_test.rb +96 -0
  31. data/test/unit/output/adapter/base_test.rb +27 -0
  32. data/test/unit/output/adapter/csv_test.rb +75 -0
  33. data/test/unit/output/adapter/table_test.rb +58 -0
  34. data/test/unit/output/definition_test.rb +27 -0
  35. data/test/unit/output/dsl_test.rb +119 -0
  36. data/test/unit/output/fields_test.rb +97 -0
  37. data/test/unit/output/formatters_test.rb +83 -0
  38. data/test/unit/output/output_test.rb +104 -0
  39. data/test/unit/settings_test.rb +106 -0
  40. data/test/unit/test_helper.rb +20 -0
  41. data/test/unit/utils_test.rb +35 -0
  42. data/test/unit/validator_test.rb +142 -0
  43. metadata +112 -35
  44. data/LICENSE +0 -5
  45. data/hammer_cli_complete +0 -13
@@ -0,0 +1,30 @@
1
+ Development Tips
2
+ ----------------
3
+
4
+ ### Local gem modifications
5
+ If you want to modify gems setup for development needs, create a file `Gemfile.local` in the root of your hammer-cli checkout. You can override the setup from `Gemfile` there. This file is git-ignored so you can easily keep your custom tuning.
6
+
7
+ Typical usage is for linking plugins from local checkouts:
8
+ ```ruby
9
+ gem 'hammer_cli_foreman', :path => '../hammer-cli-foreman'
10
+ ```
11
+
12
+ ### Debugging with Pry
13
+ [Pry](https://github.com/pry/pry) is a runtime developer console for ruby.
14
+ It allows for debugging when [Pry Debugger](https://github.com/nixme/pry-debugger) is installed alongside.
15
+
16
+ For basic usage, add following lines to your `Gemfile.local`:
17
+
18
+ ```ruby
19
+ gem 'pry'
20
+ gem 'pry-debugger', :platforms => [:ruby_19]
21
+ ```
22
+
23
+ Then add this line at the place where you want the script to break:
24
+
25
+ ```ruby
26
+ require 'pry'; binding.pry
27
+ ```
28
+
29
+ Pry Debugger supports all expected debugging features.
30
+ See [its documentation](https://github.com/nixme/pry-debugger#pry-debugger-) for details.
@@ -0,0 +1,90 @@
1
+ Writing your own Hammer plugin
2
+ ------------------------------
3
+
4
+ In this tutorial we will create a simple hello world plugin.
5
+
6
+ Hammer plugins are nothing but gems. Details on how to build a gem can be found for example at [rubygems.org](http://guides.rubygems.org/make-your-own-gem/).
7
+ In the first part of this tutorial we will briefly guide you through the process of creating a very simple gem. First of all you will need rubygems package installed on your system.
8
+
9
+ Create the basic gem structure in a project subdirectory of your choice:
10
+ ```
11
+ $ cd ./my_first_hammer_plugin/
12
+ $ touch Gemfile
13
+ $ touch hammer_cli_hello.gemspec
14
+ $ mkdir -p lib/hammer_cli_hello
15
+ $ touch lib/hammer_cli_hello.rb
16
+ $ touch lib/hammer_cli_hello/version.rb
17
+ ```
18
+
19
+ Example `Gemfile`:
20
+ ```ruby
21
+ source "https://rubygems.org"
22
+
23
+ gemspec
24
+ ```
25
+
26
+ Example `hammer_cli_hello.gemspec` file:
27
+ ```ruby
28
+ $:.unshift File.expand_path("../lib", __FILE__)
29
+ require "hammer_cli_hello/version"
30
+
31
+ Gem::Specification.new do |s|
32
+
33
+ s.name = "hammer_cli_hello"
34
+ s.authors = ["Me"]
35
+ s.version = HammerCLIHello.version.dup
36
+ s.platform = Gem::Platform::RUBY
37
+ s.summary = %q{Hello world commands for Hammer}
38
+
39
+ s.files = Dir['lib/**/*.rb']
40
+ s.require_paths = ["lib"]
41
+
42
+ s.add_dependency 'hammer_cli', '>= 0.0.6'
43
+ end
44
+ ```
45
+ More details about the gemspec structure is again at [rubygems.org](http://guides.rubygems.org/specification-reference/).
46
+
47
+ We'll have to specify the plugins version in `lib/hammer_cli_hello/version.rb`:
48
+ ```ruby
49
+ module HammerCLIHello
50
+ def self.version
51
+ @version ||= Gem::Version.new '0.0.1'
52
+ end
53
+ end
54
+ ```
55
+
56
+ This should be enough for creating a minimalist gem. Let's build and install it.
57
+ ```
58
+ $ gem build ./hammer_cli_hello.gemspec
59
+ $ gem install hammer_cli_hello-0.0.1.gem
60
+ ```
61
+
62
+ Update the hammer config to enable your plugin.
63
+ ```yaml
64
+ :modules:
65
+ - hammer_cli_hello
66
+ # - hammer_cli_foreman
67
+ # - hammer_cli_katello_bridge
68
+ ```
69
+
70
+
71
+ Verify the installation by running:
72
+ ```
73
+ $ hammer -v > /dev/null
74
+ ```
75
+
76
+ You should see a message saying that your module was loaded (second line in the sample output).
77
+ ```
78
+ [ INFO 2013-10-16 11:19:06 Init] Configuration from the file /etc/foreman/cli_config.yml has been loaded
79
+ [ INFO 2013-10-16 11:19:06 Init] Extension module hammer_cli_hello loaded
80
+ [ INFO 2013-10-16 11:19:06 HammerCLI::MainCommand] Called with options: {"verbose"=>true}
81
+ ```
82
+
83
+ Done. Your first hammer plugin is installed. Unfortunatelly it does not contain any commands yet. So let's start adding some to finally enjoy real results.
84
+
85
+ Optionally you can add a Rakefile and build and install the gem with `rake install`
86
+ ```ruby
87
+ # ./Rakefile
88
+ require 'bundler/gem_tasks'
89
+ ```
90
+
@@ -53,7 +53,7 @@ module HammerCLI
53
53
  end
54
54
 
55
55
  def exception_handler
56
- @exception_handler ||= exception_handler_class.new(:context=>context, :adapter=>adapter)
56
+ @exception_handler ||= exception_handler_class.new(:output => output)
57
57
  end
58
58
 
59
59
  def initialize(*args)
@@ -66,16 +66,21 @@ module HammerCLI
66
66
  context[:path][-2]
67
67
  end
68
68
 
69
- def self.subcommand!(name, description, subcommand_class = self, &block)
69
+ def self.remove_subcommand(name)
70
70
  self.recognised_subcommands.delete_if do |sc|
71
71
  if sc.is_called?(name)
72
- Logging.logger[self].info "subcommand #{name} (#{sc.subcommand_class}) replaced with #{name} (#{subcommand_class})"
72
+ logger.info "subcommand #{name} (#{sc.subcommand_class}) was removed."
73
73
  true
74
74
  else
75
75
  false
76
76
  end
77
77
  end
78
+ end
79
+
80
+ def self.subcommand!(name, description, subcommand_class = self, &block)
81
+ remove_subcommand(name)
78
82
  self.subcommand(name, description, subcommand_class, &block)
83
+ logger.info "subcommand #{name} (#{subcommand_class}) was created."
79
84
  end
80
85
 
81
86
  def self.subcommand(name, description, subcommand_class = self, &block)
@@ -89,37 +94,52 @@ module HammerCLI
89
94
  def self.output(definition=nil, &block)
90
95
  dsl = HammerCLI::Output::Dsl.new
91
96
  dsl.build &block if block_given?
92
-
93
97
  output_definition.append definition.fields unless definition.nil?
94
98
  output_definition.append dsl.fields
95
99
  end
96
100
 
101
+ def output
102
+ @output ||= HammerCLI::Output::Output.new(context, :default_adapter => adapter)
103
+ end
104
+
97
105
  def output_definition
98
106
  self.class.output_definition
99
107
  end
100
108
 
109
+ def self.inherited_output_definition
110
+ od = nil
111
+ if superclass.respond_to? :output_definition
112
+ od_super = superclass.output_definition
113
+ od = od_super.dup unless od_super.nil?
114
+ end
115
+ od
116
+ end
117
+
101
118
  def self.output_definition
102
- @output_definition ||= HammerCLI::Output::Definition.new
119
+ @output_definition = @output_definition || inherited_output_definition || HammerCLI::Output::Definition.new
103
120
  @output_definition
104
121
  end
105
122
 
106
123
  protected
107
124
 
108
125
  def print_records(definition, records)
109
- HammerCLI::Output::Output.print_records(definition, records, context,
110
- :adapter => adapter)
126
+ output.print_records(definition, records)
111
127
  end
112
128
 
113
- def print_message(msg)
114
- HammerCLI::Output::Output.print_message(msg, context, :adapter=>adapter)
129
+ def print_message(msg, msg_params={})
130
+ output.print_message(msg, msg_params)
115
131
  end
116
132
 
117
- def logger(name=self.class)
133
+ def self.logger(name=self)
118
134
  logger = Logging.logger[name]
119
135
  logger.extend(HammerCLI::Logger::Watch) if not logger.respond_to? :watch
120
136
  logger
121
137
  end
122
138
 
139
+ def logger(name=self.class)
140
+ self.class.logger(name)
141
+ end
142
+
123
143
  def validator
124
144
  options = self.class.recognised_options.collect{|opt| opt.of(self)}
125
145
  @validator ||= HammerCLI::Validator.new(options)
@@ -147,7 +167,7 @@ module HammerCLI
147
167
 
148
168
  def self.command_name(name=nil)
149
169
  @name = name if name
150
- @name
170
+ @name || (superclass.respond_to?(:command_name) ? superclass.command_name : nil)
151
171
  end
152
172
 
153
173
  def self.autoload_subcommands
@@ -52,7 +52,7 @@ module HammerCLI::Apipie
52
52
 
53
53
  def resource
54
54
  # if the resource definition is not available in this command's class
55
- # try to look it up in parent command's class
55
+ # or its superclass try to look it up in parent command's class
56
56
  if self.class.resource
57
57
  return ResourceInstance.from_definition(self.class.resource, resource_config)
58
58
  else
@@ -74,13 +74,12 @@ module HammerCLI::Apipie
74
74
 
75
75
  module ClassMethods
76
76
 
77
- def resource(resource_class=nil, action=nil)
78
- @api_resource = ResourceDefinition.new(resource_class) unless resource_class.nil?
79
- @api_action = action unless action.nil?
77
+ def class_resource
80
78
  return @api_resource if @api_resource
79
+ return superclass.class_resource if superclass.respond_to? :class_resource
80
+ end
81
81
 
82
- # if the resource definition is not available in this class
83
- # try to look it up in it's enclosing module/class
82
+ def module_resource
84
83
  enclosing_module = self.name.split("::")[0..-2].inject(Object) { |mod, cls| mod.const_get cls }
85
84
 
86
85
  if enclosing_module.respond_to? :resource
@@ -88,6 +87,15 @@ module HammerCLI::Apipie
88
87
  end
89
88
  end
90
89
 
90
+ def resource(resource_class=nil, action=nil)
91
+ @api_resource = ResourceDefinition.new(resource_class) unless resource_class.nil?
92
+ @api_action = action unless action.nil?
93
+
94
+ # if the resource definition is not available in this class
95
+ # try to look it up in it's enclosing module/class
96
+ return class_resource || module_resource
97
+ end
98
+
91
99
  def action(action=nil)
92
100
  @api_action = action unless action.nil?
93
101
  return @api_action if @api_action
@@ -7,16 +7,25 @@ module HammerCLI::Apipie
7
7
  include HammerCLI::Messages
8
8
 
9
9
  def execute
10
- send_request
11
- print_success_message
10
+ response = send_request
11
+ logger.watch "Retrieved data: ", response
12
+ print_success_message(response)
12
13
  return HammerCLI::EX_OK
13
14
  end
14
15
 
15
16
  protected
16
17
 
17
- def print_success_message
18
- msg = success_message
19
- print_message(msg) unless msg.nil?
18
+ def success_message_params(response)
19
+ response
20
+ end
21
+
22
+ def print_success_message(response)
23
+ if success_message
24
+ print_message(
25
+ success_message,
26
+ success_message_params(response)
27
+ )
28
+ end
20
29
  end
21
30
 
22
31
  def send_request
@@ -6,8 +6,7 @@ module HammerCLI
6
6
 
7
7
  def initialize(options={})
8
8
  @logger = Logging.logger['Exception']
9
- @context = options[:context] || {}
10
- @adapter = options[:adapter] || :base
9
+ @output = options[:output]
11
10
  end
12
11
 
13
12
  def mappings
@@ -25,6 +24,10 @@ module HammerCLI
25
24
  raise e
26
25
  end
27
26
 
27
+ def output
28
+ @output || HammerCLI::Output::Output.new
29
+ end
30
+
28
31
  protected
29
32
 
30
33
  def print_error(error)
@@ -32,9 +35,9 @@ module HammerCLI
32
35
  @logger.error error
33
36
 
34
37
  if @options[:heading]
35
- HammerCLI::Output::Output.print_error @options[:heading], error, @context, :adapter => @adapter
38
+ output.print_error(@options[:heading], error)
36
39
  else
37
- HammerCLI::Output::Output.print_error error, nil, @context, :adapter => @adapter
40
+ output.print_error(error)
38
41
  end
39
42
  end
40
43
 
@@ -53,6 +53,7 @@ module HammerCLI
53
53
  end
54
54
 
55
55
  def format(bool)
56
+ bool = bool.to_s
56
57
  if bool.downcase.match(/^(true|t|yes|y|1)$/i)
57
58
  return true
58
59
  elsif bool.downcase.match(/^(false|f|no|n|0)$/i)
@@ -71,6 +72,32 @@ module HammerCLI
71
72
  end
72
73
  end
73
74
 
75
+
76
+ class Enum < AbstractNormalizer
77
+
78
+ def initialize(allowed_values)
79
+ @allowed_values = allowed_values
80
+ end
81
+
82
+ def description
83
+ "One of %s" % quoted_values
84
+ end
85
+
86
+ def format(value)
87
+ if @allowed_values.include? value
88
+ value
89
+ else
90
+ raise ArgumentError, "value must be one of '%s'" % quoted_values
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def quoted_values
97
+ @allowed_values.map { |v| "'#{v}'" }.join(', ')
98
+ end
99
+ end
100
+
74
101
  end
75
102
  end
76
103
  end
@@ -11,27 +11,27 @@ module HammerCLI::Output::Adapter
11
11
  @formatters = HammerCLI::Output::Formatters::FormatterLibrary.new(filter_formatters(formatters))
12
12
  end
13
13
 
14
- def print_message(msg)
15
- puts msg
14
+ def print_message(msg, msg_params={})
15
+ puts msg.format(msg_params)
16
16
  end
17
17
 
18
- def print_error(msg, details=nil)
18
+ def print_error(msg, details=nil, msg_params={})
19
19
  details = details.split("\n") if details.kind_of? String
20
20
 
21
21
  if details
22
22
  indent = " "
23
- $stderr.puts msg+":"
24
- $stderr.puts indent + details.join("\n"+indent)
25
- else
26
- $stderr.puts msg
23
+ msg += ":\n"
24
+ msg += indent + details.join("\n"+indent)
27
25
  end
26
+
27
+ $stderr.puts msg.format(msg_params)
28
28
  end
29
29
 
30
30
  def print_records(fields, data)
31
31
  raise NotImplementedError
32
32
  end
33
33
 
34
- private
34
+ private
35
35
 
36
36
  def filter_formatters(formatters_map)
37
37
  formatters_map ||= {}
@@ -17,14 +17,12 @@ module HammerCLI::Output::Adapter
17
17
  end
18
18
 
19
19
  def print_records(fields, data)
20
- csv_string = CSV.generate(
21
- :col_sep => @context[:csv_separator] || ',',
22
- :encoding => 'utf-8') do |csv|
20
+ csv_string = generate do |csv|
23
21
  # labels
24
22
  csv << fields.select{ |f| !(f.class <= Fields::Id) || @context[:show_ids] }.map { |f| f.label }
25
23
  # data
26
24
  data.each do |d|
27
- csv << fields.inject([]) do |row, f|
25
+ csv << fields.inject([]) do |row, f|
28
26
  unless f.class <= Fields::Id && !@context[:show_ids]
29
27
  value = (f.get_value(d) || '')
30
28
  formatter = @formatters.formatter_for_type(f.class)
@@ -36,6 +34,41 @@ module HammerCLI::Output::Adapter
36
34
  end
37
35
  puts csv_string
38
36
  end
37
+
38
+ def print_message(msg, msg_params={})
39
+ csv_string = generate do |csv|
40
+ id = msg_params["id"] || msg_params[:id]
41
+ name = msg_params["name"] || msg_params[:name]
42
+
43
+ labels = ["Message"]
44
+ data = [msg.format(msg_params)]
45
+
46
+ if id
47
+ labels << "Id"
48
+ data << id
49
+ end
50
+
51
+ if name
52
+ labels << "Name"
53
+ data << name
54
+ end
55
+
56
+ csv << labels
57
+ csv << data
58
+ end
59
+ puts csv_string
60
+ end
61
+
62
+ private
63
+
64
+ def generate(&block)
65
+ CSV.generate(
66
+ :col_sep => @context[:csv_separator] || ',',
67
+ :encoding => 'utf-8',
68
+ &block
69
+ )
70
+ end
71
+
39
72
  end
40
73
 
41
74
  HammerCLI::Output::Output.register_adapter(:csv, CSValues)
@@ -3,10 +3,10 @@ module HammerCLI::Output::Adapter
3
3
 
4
4
  class Silent < Abstract
5
5
 
6
- def print_message(msg)
6
+ def print_message(msg, msg_params={})
7
7
  end
8
8
 
9
- def print_error(msg, details=[])
9
+ def print_error(msg, details=[], msg_params={})
10
10
  end
11
11
 
12
12
  def print_records(fields, data)
@@ -18,7 +18,9 @@ module HammerCLI::Output
18
18
  protected
19
19
 
20
20
  def field(key, label, type=nil, options={}, &block)
21
- options[:path] = current_path.clone << key
21
+ options[:path] = current_path.clone
22
+ options[:path] << key if !key.nil?
23
+
22
24
  options[:label] = label
23
25
  type ||= Fields::DataField
24
26
  custom_field type, options, &block
@@ -1,30 +1,33 @@
1
1
  module HammerCLI::Output
2
2
  class Output
3
3
 
4
- def self.print_message(msg, context, options={})
5
- adapter(options[:adapter], context).print_message(msg.to_s)
4
+ def initialize(context={}, options={})
5
+ self.context = context
6
+ self.default_adapter = options[:default_adapter]
6
7
  end
7
8
 
8
- def self.print_error(msg, details=nil, context={}, options={})
9
- adapter(options[:adapter], context).print_error(msg.to_s, details)
9
+ attr_accessor :default_adapter
10
+
11
+ def print_message(msg, msg_params={})
12
+ adapter.print_message(msg.to_s, msg_params)
10
13
  end
11
14
 
12
- def self.print_records(definition, records, context, options={})
13
- adapter(options[:adapter], context).print_records(definition.fields, [records].flatten(1))
15
+ def print_error(msg, details=nil, msg_params={})
16
+ adapter.print_error(msg.to_s, details, msg_params)
14
17
  end
15
18
 
16
- def self.adapter(req_adapter, context)
17
- if context[:adapter]
18
- adapter_name = context[:adapter].to_sym
19
- else
20
- adapter_name = req_adapter
21
- end
19
+ def print_records(definition, records)
20
+ adapter.print_records(definition.fields, [records].flatten(1))
21
+ end
22
+
23
+ def adapter
24
+ adapter_name = context[:adapter] || default_adapter
22
25
 
23
26
  begin
24
- init_adapter(adapter_name, context)
27
+ init_adapter(adapter_name.to_sym)
25
28
  rescue NameError
26
- Logging.logger[self.name].warn("Required adapter '#{adapter_name}' was not found, using 'base' instead")
27
- init_adapter(:base, context)
29
+ Logging.logger[self.class.name].warn("Required adapter '#{adapter_name}' was not found, using 'base' instead")
30
+ init_adapter(:base)
28
31
  end
29
32
  end
30
33
 
@@ -51,10 +54,12 @@ module HammerCLI::Output
51
54
  end
52
55
 
53
56
  private
54
-
55
- def self.init_adapter(adapter_name, context)
56
- raise NameError unless adapters.has_key? adapter_name
57
- adapters[adapter_name].new(context, formatters)
57
+
58
+ attr_accessor :context
59
+
60
+ def init_adapter(adapter_name)
61
+ raise NameError unless self.class.adapters.has_key? adapter_name
62
+ self.class.adapters[adapter_name].new(context, self.class.formatters)
58
63
  end
59
64
 
60
65
  end
@@ -0,0 +1,18 @@
1
+
2
+ class String
3
+
4
+ # string formatting for ruby 1.8
5
+ def format(params)
6
+ if params.is_a? Hash
7
+ array_params = self.scan(/%[<{]([^>}]*)[>}]/).collect do |name|
8
+ name = name[0]
9
+ params[name.to_s] || params[name.to_sym]
10
+ end
11
+
12
+ self.gsub(/%[<{]([^>}]*)[>}]/, '%') % array_params
13
+ else
14
+ self % params
15
+ end
16
+ end
17
+
18
+ end
@@ -1,5 +1,5 @@
1
1
  module HammerCLI
2
2
  def self.version
3
- @version ||= Gem::Version.new '0.0.9'
3
+ @version ||= Gem::Version.new '0.0.10'
4
4
  end
5
5
  end
data/lib/hammer_cli.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'hammer_cli/utils'
1
2
  require 'hammer_cli/version'
2
3
  require 'hammer_cli/exit_codes'
3
4
  require 'hammer_cli/settings'