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
@@ -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)