hammer_cli 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -5,929 +5,8 @@ Hammer is a generic clamp-based CLI framework. It uses existing clamp features a
5
5
  We recommend to get familiar with the [Clamp documentation](https://github.com/mdub/clamp/#quick-start)
6
6
  before creating some hammer specific plugins.
7
7
 
8
-
9
- Writing your own Hammer plugin
10
- ------------------------------
11
-
12
- In this tutorial we will create a simple hello world plugin.
13
-
14
- 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/).
15
- 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.
16
-
17
- Create the basic gem structure in a project subdirectory of your choice:
18
- ```
19
- $ cd ./my_first_hammer_plugin/
20
- $ touch Gemfile
21
- $ touch hammer_cli_hello.gemspec
22
- $ mkdir -p lib/hammer_cli_hello
23
- $ touch lib/hammer_cli_hello.rb
24
- $ touch lib/hammer_cli_hello/version.rb
25
- ```
26
-
27
- Example `Gemfile`:
28
- ```ruby
29
- source "https://rubygems.org"
30
-
31
- gemspec
32
- ```
33
-
34
- Example `hammer_cli_hello.gemspec` file:
35
- ```ruby
36
- $:.unshift File.expand_path("../lib", __FILE__)
37
- require "hammer_cli_hello/version"
38
-
39
- Gem::Specification.new do |s|
40
-
41
- s.name = "hammer_cli_hello"
42
- s.authors = ["Me"]
43
- s.version = HammerCLIHello.version.dup
44
- s.platform = Gem::Platform::RUBY
45
- s.summary = %q{Hello world commands for Hammer}
46
-
47
- s.files = Dir['lib/**/*.rb']
48
- s.require_paths = ["lib"]
49
-
50
- s.add_dependency 'hammer_cli', '>= 0.0.6'
51
- end
52
- ```
53
- More details about the gemspec structure is again at [rubygems.org](http://guides.rubygems.org/specification-reference/).
54
-
55
- We'll have to specify the plugins version in `lib/hammer_cli_hello/version.rb`:
56
- ```ruby
57
- module HammerCLIHello
58
- def self.version
59
- @version ||= Gem::Version.new '0.0.1'
60
- end
61
- end
62
- ```
63
-
64
- This should be enough for creating a minimalist gem. Let's build and install it.
65
- ```
66
- $ gem build ./hammer_cli_hello.gemspec
67
- $ gem install hammer_cli_hello-0.0.1.gem
68
- ```
69
-
70
- Update the hammer config to enable your plugin.
71
- ```yaml
72
- :modules:
73
- - hammer_cli_hello
74
- # - hammer_cli_foreman
75
- # - hammer_cli_katello_bridge
76
- ```
77
-
78
-
79
- Verify the installation by running:
80
- ```
81
- $ hammer -v > /dev/null
82
- ```
83
-
84
- You should see a message saying that your module was loaded (second line in the sample output).
85
- ```
86
- [ INFO 2013-10-16 11:19:06 Init] Configuration from the file /etc/foreman/cli_config.yml has been loaded
87
- [ INFO 2013-10-16 11:19:06 Init] Extension module hammer_cli_hello loaded
88
- [ INFO 2013-10-16 11:19:06 HammerCLI::MainCommand] Called with options: {"verbose"=>true}
89
- ```
90
-
91
- 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.
92
-
93
- Optionally you can add a Rakefile and build and install the gem with `rake install`
94
- ```ruby
95
- # ./Rakefile
96
- require 'bundler/gem_tasks'
97
- ```
98
-
99
-
100
- Create your first command
101
- -------------------------
102
-
103
- We will create a simple command called `hello` that will print a sentence "Hello World!" to stdout.
104
-
105
- ### Declare the command
106
-
107
- ```
108
- touch ./lib/hammer_cli_hello/hello_world.rb
109
- ```
110
-
111
- ```ruby
112
- # ./lib/hammer_cli_hello/hello_world.rb
113
- require 'hammer_cli'
114
-
115
- # it's a good practise to nest commands into modules
116
- module HammerCLIHello
117
-
118
- # hammer commands must be descendants of AbstractCommand
119
- class HelloCommand < HammerCLI::AbstractCommand
120
-
121
- # execute is the heart of the command
122
- def execute
123
- # we use print_message instead of simple puts
124
- # the reason will be described later in the part called Output
125
- print_message "Hello World!"
126
- end
127
- end
128
-
129
- # now plug your command into the hammer's main command
130
- HammerCLI::MainCommand.subcommand
131
- 'hello', # command's name
132
- "Say Hello World!", # description
133
- HammerCLIHello::HelloCommand # the class
134
- end
135
- ```
136
-
137
- The last bit is to require the file with your command in `hammer_cli_hello.rb`.
138
- Hammer actually loads this file and this is how the commands from plugins get loaded
139
- into hammer.
140
- ```ruby
141
- # ./lib/hammer_cli_hello.rb
142
- require 'hammer_cli_hello/hello_world'
143
- ```
144
-
145
- Rebuild and reinstall your plugin and see the results of `hammer -h`
146
- ```
147
- gem build ./hammer_cli_hello.gemspec && gem install hammer_cli_hello-0.0.1.gem
148
- ```
149
-
150
-
151
- ```
152
- $ hammer -h
153
- Usage:
154
- hammer [OPTIONS] SUBCOMMAND [ARG] ...
155
-
156
- Parameters:
157
- SUBCOMMAND subcommand
158
- [ARG] ... subcommand arguments
159
-
160
- Subcommands:
161
- shell Interactive Shell
162
- hello Say Hello World!
163
-
164
- Options:
165
- -v, --verbose be verbose
166
- -c, --config CFG_FILE path to custom config file
167
- -u, --username USERNAME username to access the remote system
168
- -p, --password PASSWORD password to access the remote system
169
- --version show version
170
- --show-ids Show ids of associated resources
171
- --csv Output as CSV (same as --adapter=csv)
172
- --output ADAPTER Set output format. One of [csv, table, base, silent]
173
- --csv-separator SEPARATOR Character to separate the values
174
- -P, --ask-pass Ask for password
175
- --autocomplete LINE Get list of possible endings
176
- -h, --help print help
177
- ```
178
-
179
- Now try running the command.
180
-
181
- ```
182
- $ hammer hello
183
- Hello World!
184
- Error: exit code must be integer
185
- ```
186
-
187
- What's wrong here? Hammer requires integer exit codes as return values from the method `execute`.
188
- It's usually just `HammerCLI::EX_OK`. Add it as the very last line of `execute`, rebuild and the
189
- command should run fine.
190
-
191
- See [exit_codes.rb](https://github.com/theforeman/hammer-cli/blob/master/lib/hammer_cli/exit_codes.rb)
192
- for the full list of available exit codes.
193
-
194
-
195
- ### Declaring options
196
- Our new command has only one option so far. It's `-h` which is built in for every command by default.
197
- Option declaration is the same as in clamp so please read it's
198
- [documentation](https://github.com/mdub/clamp/#declaring-options)
199
- on that topic.
200
-
201
- Example option usage could go like this:
202
- ```ruby
203
- class HelloCommand < HammerCLI::AbstractCommand
204
-
205
- option '--name', "NAME", "Name of the person you want to greet"
206
-
207
- def execute
208
- print_message "Hello %s!" % (name || "World")
209
- HammerCLI::EX_OK
210
- end
211
- end
212
- ```
213
-
214
- ```
215
- $ hammer hello -h
216
- Usage:
217
- hammer hello [OPTIONS]
218
-
219
- Options:
220
- --name NAME Name of the person you want to greet
221
- -h, --help print help
222
- ```
223
-
224
- ```
225
- $ hammer hello --name 'Foreman'
226
- Hello Foreman!
227
- ```
228
-
229
-
230
- ### Option validation
231
- Hammer provides extended functionality for validating options.
232
-
233
- #### DSL
234
- First of all there is a dsl for validating combinations of options:
235
- ```ruby
236
- validate_options do
237
- all(:name, :surname).required # requires all the options
238
- option(:age).required # requires a single option,
239
- # equivalent of :required => true in option declaration
240
- any(:email, :phone).required # requires at least one of the options
241
-
242
- # Tt is possible to create more complicated constructs.
243
- # This example requires either the full address or nothing
244
- if any(:street, :city, :zip).exist?
245
- all(:street, :city, :zip).required
246
- end
247
-
248
- # Here you can reject all address related option when --no-address is passed
249
- if option(:no_address).exist?
250
- all(:street, :city, :zip).rejected
251
- end
252
- end
253
-
254
- ```
255
-
256
- #### Option normalizers
257
- Another option-related feature is a set of normalizers for specific option types. They validate and preprocess
258
- option values. Each normalizer has a description of the format it accepts. This description is printed
259
- in commands' help.
260
-
261
- ##### _List_
262
-
263
- Parses comma separated strings to a list of values.
264
-
265
- ```ruby
266
- option "--users", "USER_NAMES", "List of user names",
267
- :format => HammerCLI::Options::Normalizers::List.new
268
- ```
269
- `--users='J.R.,Gary,Bobby'` -> `['J.R.', 'Gary', 'Bobby']`
270
-
271
- ##### _File_
272
-
273
- Loads contents of a file and returns it as a value of the option.
274
-
275
- ```ruby
276
- option "--poem", "PATH_TO_POEM", "File containing the text of your poem",
277
- :format => HammerCLI::Options::Normalizers::File.new
278
- ```
279
- `--poem=~/verlaine/les_poetes_maudits.txt` -> content of the file
280
-
281
- ##### _Bool_
282
-
283
- Case insensitive true/false values. Translates _yes,y,true,t,1_ to `true` and _no,n,false,f,0_ to `false`.
284
-
285
- ```ruby
286
- option "--start", "START", "Start the action",
287
- :format => HammerCLI::Options::Normalizers::Bool.new
288
- ```
289
- `--start=yes` -> `true`
290
-
291
- ##### _KeyValueList_
292
-
293
- Parses a comma separated list of key=value pairs. Can be used for naming attributes with vague structure.
294
-
295
- ```ruby
296
- option "--attributes", "ATTRIBUTES", "Values of various attributes",
297
- :format => HammerCLI::Options::Normalizers::KeyValueList.new
298
- ```
299
- `--attributes="material=unoptanium,thickness=3"` -> `{'material' => 'unoptanium', 'thickness' => '3'}`
300
-
301
- ### Adding subcommands
302
- Commands in the cli can be structured into a tree of parent commands (nodes) and subcommands (leaves).
303
- Neither the number of subcommands nor the nesting is limited. Please note that no parent command
304
- can perform any action and therefore it's useless to define `execute` method for them. This limit
305
- comes from Clamp's implementation of the command hierarchy.
306
-
307
- We've already used command nesting for plugging the `HelloCommand` command into the main command.
308
- But let's create a new command `say` and show how to connect it with others to be more demonstrative.
309
-
310
- ```ruby
311
- module HammerCLIHello
312
-
313
- # a new parent command 'say'
314
- class SayCommand < HammerCLI::AbstractCommand
315
-
316
- # subcommand 'hello' remains the same
317
- class HelloCommand < HammerCLI::AbstractCommand
318
-
319
- option '--name', "NAME", "Name of the person you want to greet"
320
-
321
- def execute
322
- print_message "Hello %s!" % (name || "World")
323
- HammerCLI::EX_OK
324
- end
325
- end
326
-
327
- # plug the original command into 'say'
328
- subcommand 'hello', "Say Hello World!", HammerCLIHello::SayCommand::HelloCommand
329
- end
330
-
331
- # plug the 'say' command into the main command
332
- HammerCLI::MainCommand.subcommand 'say', "Say something", HammerCLIHello::SayCommand
333
- end
334
- ```
335
-
336
- The result will be:
337
- ```
338
- $ hammer say hello
339
- Hello World!
340
- ```
341
-
342
- This is very typical usage of subcommands. When you create more of them it may feel a bit
343
- duplicit to always define the subcommand structure at the end of the class definition.
344
- Hammer provides utility methods for subcommand autoloading. This is handy especially
345
- when you have growing number of subcommands. See how it works in the following example:
346
-
347
- ```ruby
348
- module HammerCLIHello
349
-
350
- class SayCommand < HammerCLI::AbstractCommand
351
-
352
- class HelloCommand < HammerCLI::AbstractCommand
353
- command_name 'hello' # name and description moves to the command's class
354
- desc 'Say Hello World!'
355
- # ...
356
- end
357
-
358
- class HiCommand < HammerCLI::AbstractCommand
359
- command_name 'hi'
360
- desc 'Say Hi World!'
361
- # ...
362
- end
363
-
364
- class ByeCommand < HammerCLI::AbstractCommand
365
- command_name 'bye'
366
- desc 'Say Bye World!'
367
- # ...
368
- end
369
-
370
- autoload_subcommands
371
- end
372
-
373
- HammerCLI::MainCommand.subcommand 'say', "Say something", HammerCLIHello::SayCommand
374
- end
375
- ```
376
-
377
- ```
378
- $ hammer say
379
- Usage:
380
- hammer say [OPTIONS] SUBCOMMAND [ARG] ...
381
-
382
- Parameters:
383
- SUBCOMMAND subcommand
384
- [ARG] ... subcommand arguments
385
-
386
- Subcommands:
387
- hi Say Hi World!
388
- hello Say Hello World!
389
- bye Say Bye World!
390
-
391
- Options:
392
- -h, --help print help
393
- ```
394
-
395
-
396
- ### Conflicting subcommands
397
- It can happen that two different plugins define subcommands with the same name by accident.
398
- In such situations `subcommand` will throw an exception. If this is intentional and you
399
- want to redefine the existing command, use `subcommand!`.
400
- This method does not throw exceptions, replaces the original subcommand, and leaves
401
- a message in a log for debugging purposes.
402
-
403
-
404
- ### Printing some output
405
- We've mentioned above that it's not recommended practice to print output
406
- directly with `puts` in Hammer. The reason is we separate definition
407
- of the output from its interpretation. Hammer uses so called _output adapters_
408
- that can modify the output format.
409
-
410
- Hammer comes with four basic output adapters:
411
- * __base__ - simple output, structured records
412
- * __table__ - records printed in tables, ideal for printing lists of records
413
- * __csv__ - comma separated output, ideal for scripting and grepping
414
- * __silent__ - no output, used for testing
415
-
416
- The detailed documentation on creating adapters is coming soon.
417
-
418
- #### Printing messages
419
- Very simple, just call
420
- ```ruby
421
- print_message(msg)
422
- ```
423
-
424
- #### Printing hash records
425
- Typical usage of a cli is interaction with some api. In many cases it's listing
426
- some records returned by the api.
427
-
428
- Hammer comes with support for selecting and formatting of hash record fields.
429
- You first create so called _output definition_ that you apply on your data. The result
430
- is a collection of fields each having its type. The collection is then passed to some
431
- _output adapter_ which handles the actuall formatting and printing.
432
-
433
- Hammer provides a DSL for defining the output. Next rather complex example will
434
- explain how to use it in action.
435
-
436
- Imagine there's an API of some service that returns list of users:
437
- ```ruby
438
- [{
439
- :id => 1,
440
- :email => 'tom@email.com',
441
- :phone => '123456111',
442
- :first_name => 'Tom',
443
- :last_name => 'Sawyer',
444
- :roles => ['Admin', 'Editor'],
445
- :timestamps => {
446
- :created => '2012-12-18T15:24:42Z',
447
- :updated => '2012-12-18T15:24:42Z'
448
- }
449
- },{
450
- :id => 2,
451
- :email => 'huckleberry@email.com',
452
- :phone => '123456222',
453
- :first_name => 'Huckleberry',
454
- :last_name => 'Finn',
455
- :roles => ['Admin'],
456
- :timestamps => {
457
- :created => '2012-12-18T15:25:00Z',
458
- :updated => '2012-12-20T14:00:15Z'
459
- }
460
- }]
461
- ```
462
-
463
- We can create an output definition that selects and formats some of the fields:
464
- ```ruby
465
- class Command < HammerCLI::AbstractCommand
466
-
467
- output do
468
- # Simple field with a label. The first parameter is key in the printed hash.
469
- field :id, 'ID'
470
-
471
- # Fields can have types. The type determines how the field is printed.
472
- # All available types are listed below.
473
- # Here we want the roles to act as list.
474
- field :roles, 'System Roles', Fields::List
475
-
476
- # Label is used for grouping fields.
477
- label 'Contacts' do
478
- field :email, 'Email'
479
- field :phone, 'Phone No.'
480
- end
481
-
482
- # From is used for accessing nested fields.
483
- from :timestamps do
484
- # See how date gets formatted in the output
485
- field :created, 'Created At', Fields::Date
486
- end
487
- end
488
-
489
- def execute
490
- records = retrieve_data
491
- print_records( # <- printing utility of AbstractCommand
492
- output_definition, # <- method for accessing fields defined in the block 'output'
493
- records # <- the data to print
494
- )
495
- return HammerCLI::EX_OK
496
- end
497
-
498
- end
499
- ```
500
-
501
- Using the base adapter the output will look like:
502
- ```
503
- ID: 1
504
- System Roles: Admin, Editor
505
- Name: Tom Sawyer
506
- Contacts:
507
- Email: tom@email.com
508
- Phone No.: 123456111
509
- Created At: 2012/12/18 15:24:42
510
-
511
- ID: 2
512
- System Roles: Admin
513
- Name: Huckleberry Finn
514
- Contacts:
515
- Email: huckleberry@email.com
516
- Phone No.: 123456222
517
- Created At: 2012/12/18 15:25:00
518
- ```
519
-
520
- You can optionally use output definition from another command as a base and extend it with
521
- additional fields. This is helpful when there are two commands, one listing brief data and
522
- another one showing details. Typically it's list and show.
523
- ```ruby
524
- class ShowCommand < HammerCLI::AbstractCommand
525
-
526
- output ListCommand.output_definition do
527
- # additional fields
528
- end
529
-
530
- # ...
531
- end
532
- ```
533
-
534
-
535
- All Hammer field types are:
536
- * __Date__
537
- * __Id__ - Used to mark ID values, current print adapters have support for turning id printing on/off.
538
- See hammer's parameter `--show-ids`.
539
- * __List__
540
- * __KeyValue__ - Formats hashes containing `:name` and `:value`
541
- * __Collection__ - Enables to render subcollections. Takes a block with another output definition.
542
-
543
- The default adapter for every command is Base adapter. It is possible to override
544
- the default one by redefining command's method `adapter`.
545
-
546
- ```ruby
547
- def adapter
548
- # return :base, :table, :csv or name of your own adapter here
549
- :table
550
- end
551
- ```
552
-
553
-
554
- Other useful command features
555
- -----------------------------
556
-
557
- #### Logging
558
- Hammer provides integrated [logger](https://github.com/TwP/logging)
559
- with broad setting options (use hammer's config file):
560
-
561
- ```yaml
562
- :log_dir: '<path>' # - directory where the logs are stored.
563
- # The default is /var/log/foreman/ and the log file is named hammer.log
564
- :log_level: '<level>' # - logging level. One of debug, info, warning, error, fatal
565
- :log_owner: '<owner>' # - logfile owner
566
- :log_group: '<group>' # - logfile group
567
- :log_size: 1048576 # - size in bytes, when exceeded the log rotates. Default is 1MB
568
- :watch_plain: false # - turn on/off syntax highlighting of data being logged in debug mode
569
- ```
570
-
571
- Example usage in commands:
572
- ```ruby
573
- # Get a logger instance
574
- logger('Logger name')
575
-
576
- # It uses a command class name as the logger's name by default
577
- logger
578
-
579
- # Log a message at corresponding log level
580
- logger.debug("...")
581
- logger.error("...")
582
- logger.info("...")
583
- logger.fatal("...")
584
- logger.warn("...")
585
-
586
- # Writes an awesome print dump of a value to the log
587
- logger.watch('Some label', value)
588
- ```
589
-
590
- #### Exception handling
591
- Exception handling in Hammer is centralized by
592
- [ExceptionHandler](https://github.com/theforeman/hammer-cli/blob/master/lib/hammer_cli/exception_handler.rb).
593
- Each plugin, module or even a command can have a separate exception handler. The exception handler class
594
- is looked up in the module structure from a command to the top level.
595
-
596
- Define method `self.exception_handler_class` in your plugin's module to use a custom exception handler:
597
- ```ruby
598
- # ./lib/hammer_cli_hello.rb
599
-
600
- module HammerCLIHello
601
-
602
- def self.exception_handler_class
603
- HammerCLIHello::CustomExceptionHandler
604
- end
605
- end
606
-
607
- require 'hammer_cli_hello/hello_world'
608
- ```
609
-
610
- Centralized exception handling implies that you should raise exceptions on error states in your command
611
- rather than handle it and return error codes. This approach guarrantees that error messages are logged and
612
- printed consistently and correct exit codes are returned.
613
-
614
-
615
- #### Configuration
616
- Values form config files are accesible via class `HammerCLI::Settings`.
617
- It's method `get` returns either the value or nil when it's not found.
618
-
619
- Config values belonging to a specific plugin must be nested under
620
- the plugin's name in config files.
621
-
622
- ```yaml
623
- #cli_config.yml
624
- :log_dir: /var/log/hammer/
625
- :hello_world:
626
- :name: John
627
- ```
628
-
629
- ```ruby
630
- HammerCLI::Settings.get(:log_dir) # get a value
631
- HammerCLI::Settings.get(:hello_world, :name) # get a nested value
632
- ```
633
-
634
- There's more ways where to place your config file for hammer.
635
- Read more in [the settings howto](https://github.com/theforeman/hammer-cli#configuration).
636
-
637
-
638
- Creating commands for RESTful API with ApiPie
639
- ---------------------------------------------
640
-
641
- CLIs binded to a rest api do simillar things for most of the resources. Typically it's
642
- CRUD actions that appear for nearly every resource. Actions differ with parameters
643
- accross resources but the operations remain the same.
644
-
645
- Hammer is optimised for usage with [ApiPie](https://github.com/Pajk/apipie-rails)
646
- and generated api bindings and tries to reduce the effort neccessary for a command creation.
647
-
648
-
649
- ### ApiPie and bindings
650
-
651
- [ApiPie](https://github.com/Pajk/apipie-rails) is a documentation library for RESTful APIs.
652
- Unlike traditional tools ApiPie uses DSL for api description. This brings many advantages. See its
653
- documentation for details.
654
-
655
- Foreman comes with [ruby bindings](https://github.com/theforeman/foreman_api) automatically generated
656
- from the information provided by ApiPie. Every resource (eg. Architecture, User) has it's own
657
- class with methods for each available action (eg. create, show, index, destroy).
658
- Apart from that it contains also full api documentation with parameters for the actions.
659
- This enables to reuse the documentation on client side for automatic option definition
660
- and reduce the amount of custom code per CLI action.
661
-
662
-
663
- ### ApiPie commands in Hammer
664
-
665
- Hammer identifies two basic types of ApiPie commands:
666
-
667
- - __ReadCommand__
668
- - should be used for actions that print records
669
- - retrieves the data and prints them in given format (uses output definition)
670
- - typical actions in rails terminology: _index, show_
671
-
672
- - __WriteCommand__
673
- - should used for actions that modify records
674
- - sends modifying request and prints the result
675
- - typical actions in rails terminology: _create, update, destroy_
676
-
677
- Both command classes are single resource related and expect the resource and an action to be defined.
678
- There's a simple DSL for that:
679
-
680
- ```ruby
681
- class ListCommand < HammerCLI::Apipie::ReadCommand
682
- # define resource and the action together
683
- resource ForemanApi::Resources::Architecture, :index
684
- end
685
-
686
- # or
687
-
688
- class ListCommand2 < HammerCLI::Apipie::ReadCommand
689
- # define them separately
690
- resource ForemanApi::Resources::Architecture
691
- action :index
692
- end
693
- ```
694
-
695
- #### Options definition
696
-
697
- When the resource-action pair is defined we can take the advantage of automatic option definition.
698
- There's a class method `apipie_options` for this purpose.
699
-
700
- ```ruby
701
- class ListCommand < HammerCLI::Apipie::ReadCommand
702
- resource ForemanApi::Resources::Architecture, :index
703
-
704
- apipie_options
705
- end
706
- ```
707
-
708
- If we plug the command into an existing command tree and check the help we will see there
709
- are four parameters defined from the ApiPie docs. Compare the result with
710
- [online api documentation](http://www.theforeman.org/api/apidoc/architectures/index.html).
711
- ```
712
- $ hammer architecture list -h
713
- Usage:
714
- hammer architecture list [OPTIONS]
715
-
716
- Options:
717
- --search SEARCH filter results
718
- --order ORDER sort results
719
- --page PAGE paginate results
720
- --per-page PER_PAGE number of entries per request
721
- -h, --help print help
722
- ```
723
-
724
- It is possible to combine apipie options with custom ones. If the generated options
725
- doesn't suit your needs for any reason, you can always skip and redefine them by hand.
726
- See following example.
727
- ```ruby
728
- class ListCommand < HammerCLI::Apipie::ReadCommand
729
- resource ForemanApi::Resources::Architecture, :index
730
-
731
- apipie_options :without => [:search, :order]
732
- option '--search', 'QUERY', "search query"
733
- end
734
- ```
735
-
736
- ```
737
- hammer architecture list -h
738
- Usage:
739
- hammer architecture list [OPTIONS]
740
-
741
- Options:
742
- --page PAGE paginate results
743
- --per-page PER_PAGE number of entries per request
744
- --search QUERY search query
745
- -h, --help print help
746
- ```
747
- Note that the `--search` description has changed and `--order` disappeared.
748
-
749
- Automatic options reflect:
750
- - parameter names and descriptions
751
- - required parameters
752
- - parameter types - the only supported type is array, which is translated to option normalizer `List`
753
-
754
- #### Write commands
755
-
756
- Write commands are expected to print result of the api action. There are
757
- two class methods for setting success and failure messages. Messages are
758
- printed according to the http status code the api returned.
759
-
760
- ```ruby
761
- success_message "The user has been created"
762
- failure_message "Could not create the user"
763
- ```
764
-
765
-
766
- #### Example 1: Create an architecture
767
-
768
- ```ruby
769
- class CreateCommand < HammerCLI::Apipie::WriteCommand
770
- command_name "create"
771
- resource ForemanApi::Resources::Architecture, :create
772
-
773
- success_message "Architecture created"
774
- failure_message "Could not create the architecture"
775
-
776
- apipie_options
777
- end
778
- ```
779
-
780
- ```
781
- $ hammer architecture create -h
782
- Usage:
783
- hammer architecture create [OPTIONS]
784
-
785
- Options:
786
- --name NAME
787
- --operatingsystem-ids OPERATINGSYSTEM_IDS Operatingsystem ID’s
788
- Comma separated list of values.
789
- -h, --help print help
790
- ```
791
-
792
- ```
793
- $ hammer architecture create
794
- ERROR: option '--name' is required
795
-
796
- See: 'hammer architecture create --help'
797
- ```
798
-
799
- ```
800
- $ hammer architecture create --name test --operatingsystem-ids=1,2
801
- Architecture created
802
- ```
803
-
804
- ```
805
- $ hammer architecture create --name test
806
- Could not create the architecture:
807
- Name has already been taken
808
- ```
809
-
810
-
811
- #### Example 2: Show an architecture
812
-
813
- ```ruby
814
- class InfoCommand < HammerCLI::Apipie::ReadCommand
815
- command_name "info"
816
- resource ForemanApi::Resources::Architecture, :show
817
-
818
- # It's a good practice to reuse output definition from list commands
819
- # and add more details. It helps avoiding duplicities.
820
- output ListCommand.output_definition do
821
- from "architecture" do
822
- field :operatingsystem_ids, "OS ids", Fields::List
823
- field :created_at, "Created at", Fields::Date
824
- field :updated_at, "Updated at", Fields::Date
825
- end
826
- end
827
-
828
- apipie_options
829
- end
830
- ```
831
-
832
- ```
833
- $ hammer architecture info -h
834
- Usage:
835
- hammer architecture info [OPTIONS]
836
-
837
- Options:
838
- --id ID
839
- -h, --help print help
840
- ```
841
-
842
- ```
843
- $ hammer architecture info --id 1
844
- Id: 1
845
- Name: x86_64
846
- OS ids: 1, 3
847
- Created at: 2013/06/08 18:53:56
848
- Updated at: 2013/06/08 19:17:43
849
- ```
850
-
851
-
852
- #### Tips
853
-
854
- When you define more command like we've shown above you find yourself repeating
855
- `resource ...` in every one of them. As the commands are usually grouped by
856
- the resource it is handy to extract the resource definition one level up to
857
- the encapsulating command.
858
-
859
- ```ruby
860
- class Architecture < HammerCLI::Apipie::Command
861
-
862
- resource ForemanApi::Resources::Architecture
863
-
864
- class ListCommand < HammerCLI::Apipie::ReadCommand
865
- action :index
866
- # ...
867
- end
868
-
869
-
870
- class InfoCommand < HammerCLI::Apipie::ReadCommand
871
- action :show
872
- # ...
873
- end
874
-
875
- # ...
876
- end
877
- ```
878
-
879
- ApiPie resources are being looked up in the encapsulating classes and modules
880
- when the definition is missing in the command class. If they are not found even there
881
- the resource of the parent command is used at runtime. This is useful for context-aware
882
- shared commands.
883
-
884
- The following example shows a common subcommand that can be attached to
885
- any parent of which resource implements method `add_tag`. Please note that this example
886
- is fictitious. There's no tags in Foreman's architectures and users.
887
- ```ruby
888
- module Tags
889
- class AddTag < HammerCLI::Apipie::WriteCommand
890
- option '--id', 'ID', 'ID of the resource'
891
- option '--tag', 'TAG', 'Name of the tag to add'
892
- action :add_tag
893
- command_name 'add_tag'
894
- end
895
- end
896
-
897
- class Architecture < HammerCLI::Apipie::Command
898
- resource ForemanApi::Resources::Architecture
899
- # ...
900
- include Tags
901
- autoload_subcommands
902
- end
903
-
904
- class User < HammerCLI::Apipie::Command
905
- resource ForemanApi::Resources::User
906
- # ...
907
- include Tags
908
- autoload_subcommands
909
- end
910
- ```
911
-
912
- ```
913
- $ hammer architecture add_tag -h
914
- Usage:
915
- hammer architecture add_tag [OPTIONS]
916
-
917
- Options:
918
- --id ID ID of the resource
919
- --tag TAG Name of the tag to add
920
- -h, --help print help
921
- ```
922
-
923
- ```
924
- $ hammer user add_tag -h
925
- Usage:
926
- hammer user add_tag [OPTIONS]
927
-
928
- Options:
929
- --id ID ID of the resource
930
- --tag TAG Name of the tag to add
931
- -h, --help print help
932
- ```
933
-
8
+ Contents:
9
+ - [Writing a plugin](writing_a_plugin.md#writing-your-own-hammer-plugin)
10
+ - [Creating commands](creating_commands.md#create-your-first-command)
11
+ - [Creating ApiPie commands](creating_apipie_commands.md#creating-commands-for-restful-api-with-apipie)
12
+ - [Development tips](development_tips.md#development-tips)