sod 0.0.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.adoc +227 -78
- data/lib/sod/action.rb +16 -4
- data/lib/sod/command.rb +20 -10
- data/lib/sod/context.rb +3 -4
- data/lib/sod/graph/node.rb +9 -8
- data/lib/sod/presenters/node.rb +6 -4
- data/sod.gemspec +2 -2
- data.tar.gz.sig +0 -0
- metadata +3 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23b08587b0e2a98f391369e82409ccbfd1186efb54741a76b48c1b011d12a3bc
|
4
|
+
data.tar.gz: eb8792a76509485900d2dac190766214fa109d5aff8e0c582229fd8dbcdc92cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 264edeb65dd9a578a7dbdf6cfbbb4d26fd3e39100c461ffde67ab20e61c4df5eb7c5cfd1b0eb088c9de7c45bf8b72939257a07e92c7ec4166c7749709648be0b
|
7
|
+
data.tar.gz: 883472c9fdb8b3392736e413e53a462be049d29bbce034195e9b6614fa9e018ace1690c685d976a30a2366a210d5cba12cc0eb2943476d5ba739fce2f3e348eb
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.adoc
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
:figure-caption!:
|
4
4
|
|
5
5
|
:cogger_link: link:https://alchemists.io/projects/cogger[Cogger]
|
6
|
-
:dry_container_link: link:https://dry-rb.org/gems/dry-container[Dry Container]
|
7
6
|
:etcher_link: link:https://alchemists.io/projects/etcher[Etcher]
|
8
7
|
:gemsmith_link: link:https://alchemists.io/projects/gemsmith[Gemsmith]
|
9
8
|
:git-lint_link: link:https://alchemists.io/projects/git-lint[Git Lint]
|
@@ -13,7 +12,6 @@
|
|
13
12
|
:option_parser_link: link:https://rubyapi.org/o/s?q=OptionParser[Option Parser]
|
14
13
|
:pennyworth_link: link:https://alchemists.io/projects/pennyworth[Pennyworth]
|
15
14
|
:pragmater_link: link:https://alchemists.io/projects/pragmater[Pragmater]
|
16
|
-
:rake_link: link:https://github.com/ruby/rake[Rake]
|
17
15
|
:rubysmith_link: link:https://alchemists.io/projects/rubysmith[Rubysmith]
|
18
16
|
:runcom_link: link:https://alchemists.io/projects/runcom[Runcom]
|
19
17
|
:spek_link: link:https://alchemists.io/projects/spek[Spek]
|
@@ -43,11 +41,11 @@ toc::[]
|
|
43
41
|
|
44
42
|
*DSL*
|
45
43
|
|
46
|
-
image::https://alchemists.io/images/projects/sod/screenshots/dsl.png[A screenshot of the DSL syntax,width=
|
44
|
+
image::https://alchemists.io/images/projects/sod/screenshots/dsl.png[A screenshot of the DSL syntax,width=539,height=479,role=focal_point]
|
47
45
|
|
48
46
|
*Output*
|
49
47
|
|
50
|
-
image::https://alchemists.io/images/projects/sod/screenshots/output.png[A screenshot of the generated help documentation,width=
|
48
|
+
image::https://alchemists.io/images/projects/sod/screenshots/output.png[A screenshot of the generated help documentation,width=472,height=498,role=focal_point]
|
51
49
|
|
52
50
|
== Requirements
|
53
51
|
|
@@ -222,7 +220,7 @@ Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
|
222
220
|
end
|
223
221
|
----
|
224
222
|
|
225
|
-
Despite the `Start`, `Stop`, and `Dump` actions not being implemented yet -- because you'll get a `NameError` if you try -- this does mean you'd have the following functionality available
|
223
|
+
Despite the `Start`, `Stop`, and `Dump` actions not being implemented yet -- because you'll get a `NameError` if you try -- this does mean you'd eventually have the following functionality available from the command line:
|
226
224
|
|
227
225
|
[source,bash]
|
228
226
|
----
|
@@ -242,7 +240,7 @@ Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
|
242
240
|
end
|
243
241
|
----
|
244
242
|
|
245
|
-
The first _positional_ argument (i.e. `DB`) is _always_ your action
|
243
|
+
The first _positional_ argument (i.e. `DB`) is _always_ your action, the second _positional_ argument is the first positional argument to the `DB.new` method followed by the `host` and `port` _keyword_ arguments. In other words, here's what's happening:
|
246
244
|
|
247
245
|
[source,ruby]
|
248
246
|
----
|
@@ -259,81 +257,201 @@ DB.new "MyDatabase", host: localhost, port: 5432
|
|
259
257
|
This also means you get the following benefits:
|
260
258
|
|
261
259
|
* Lazy initialization of your commands/actions.
|
262
|
-
*
|
260
|
+
* Quick injection of dependencies or customization of dependencies in general.
|
261
|
+
* Automatic forwarding of positional and/or keyword arguments to your command/action. Blocks are excluded since they are used by the `on` method for nesting purposes.
|
263
262
|
|
264
263
|
To further understand the DSL, commands, and actions you'll need to start with actions since they are the building blocks.
|
265
264
|
|
266
265
|
==== Actions
|
267
266
|
|
268
|
-
Actions are the lowest building blocks of the DSL which allow you to quickly implement, test, reuse, and compose more complex architectures. They provide a nice
|
267
|
+
Actions are the lowest building blocks of the DSL which allow you to quickly implement, test, reuse, and compose more complex architectures. They provide a nice layer atop native `OptionParser#on` functionality.
|
269
268
|
|
270
|
-
There are two kinds of actions: custom and prefabricated. We'll start with custom actions and explore prefabricated actions later. Custom actions allow you to define your own functionality by inheriting from `Sod::Action` and leveraging the DSL that comes with it.
|
269
|
+
There are two kinds of actions: custom and prefabricated. We'll start with custom actions and explore prefabricated actions later. Custom actions allow you to define your own functionality by inheriting from `Sod::Action` and leveraging the DSL that comes with it.
|
271
270
|
|
272
|
-
|
271
|
+
===== Macros
|
272
|
+
|
273
|
+
Here's a high level breakdown of the macros you can use:
|
274
|
+
|
275
|
+
* `description`: Optional (but strongly encouraged). Allows you to describe your action and appears within help documentation. If the description is not defined, then only your action's handle (i.e. aliases) will be shown.
|
273
276
|
* `ancillary`: Optional. Allows you to provide supplemental text in addition to your description that might be helpful to know about when displaying help documentation. This can accept single or multiple arguments. Order matters since each argument will appear on a separate line in the order listed.
|
274
|
-
* `on`: Required. Allows you to define the behavior of your action through keyword arguments. Otherwise, if not defined,
|
277
|
+
* `on`: Required. Allows you to define the behavior of your action through keyword arguments. Otherwise, if not defined, you'll get a `Sod::Error` telling you that you must, at a minimum, define some aliases. This macro mimics {option_parser_link} `#on` behavior via the following positional and keyword arguments:
|
275
278
|
** `aliases`: Required. This is a positional argument and defines the short and long form aliases of your action. Your aliases can be a single string (i.e. `on "--version"`) or an array of short and long form aliases. For example, using `on %w[-v --version]` would allow you to use `-v` or `--version` from the command line to call your action. You can also use boolean aliases such as `--build` or `--[no-]build` which the option parser will supply to your `#call` method as a boolean value.
|
276
279
|
** `argument`: Optional. Serves as documentation, must be a string value, and allows the {option_parser_link} to determine if the argument is required or optional. As per the {option_parser_link} documentation, you could use the following values for example:
|
277
280
|
*** `TEXT`: Required text.
|
278
281
|
*** `[TEXT]`: Optional text.
|
279
282
|
*** `a,b,c`: Required list.
|
280
283
|
*** `[a,b,c]`: Optional list.
|
281
|
-
** `type`: Optional. The type is inferred from your argument but, if you need to be explicit or want to use a custom type not supported by
|
284
|
+
** `type`: Optional. The type is inferred from your argument but, if you need to be explicit or want to use a custom type not supported by default by option parser, you can specify the type by providing a primitive. Example: `String`, `Array`, `Hash`, `Date`, etc. You can also use custom types, provided by this gem and explained later, or implement your own.
|
282
285
|
** `allow`: Optional. Allows you to define what values are allowed as defined via the `argument` or `type` keywords. This can be a string, array, hash, etc. as long as it's compatible with what is defined via the `argument` and/or `type` keyword. This information will also show up in the help documentation as well.
|
283
286
|
** `default`: Optional. Allows you to supply a default value and is a handy for simple values which don't require lazy evaluation via the corresponding default macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.
|
284
287
|
** `description`: Optional. Allows you to define a description. Handy for short descriptions that can fit on a single line. Otherwise, for longer descriptions, use the macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.
|
285
288
|
** `ancillary`: Optional. Allows you to define ancillary text to supplement your description. It can accept a string or an array. Handy for short, supplementary, text that can fit on a single line. Otherwise, for more verbose details, use the macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.
|
286
|
-
* `default`: Optional. Uses a block
|
289
|
+
* `default`: Optional. Uses a block which lazy evaluates and resolves your value. This is most helpful when used in combination with an _optional_ `argument` and/or `type` which can fallback to a safe default. This information shows up in the help text where the value is rendered as green text. In the case of booleans, they will be rendered as green for `true` and red for `false`.
|
290
|
+
|
291
|
+
With the above in mind, let's look at a few examples of what you can do when you put all of this together.
|
287
292
|
|
288
|
-
|
293
|
+
===== Booleans
|
294
|
+
|
295
|
+
Boolean flags are long alases only, take _no arguments_, and use `[no-]` syntax after the double dashes. Here's a minimal implementation:
|
289
296
|
|
290
297
|
[source,ruby]
|
291
298
|
----
|
292
|
-
class
|
293
|
-
|
299
|
+
class Demo < Sod::Action
|
300
|
+
on "--[no-]run"
|
301
|
+
|
302
|
+
def call(value) = puts "Got: #{value}"
|
303
|
+
end
|
294
304
|
|
305
|
+
cli = Sod.new { on Demo }
|
306
|
+
|
307
|
+
cli.call %w[--run] # "Got: true"
|
308
|
+
cli.call %w[--no-run] # "Got: false"
|
309
|
+
----
|
310
|
+
|
311
|
+
Because a value is always provided when using a boolean flag, you can make it a required positional parameter via your method definition (i.e. `call(value)`). You don't need to worry about type safety because {option_parser_link} will either pass in a `true` or `false` value as you can see from the output above.
|
312
|
+
|
313
|
+
===== Arguments
|
314
|
+
|
315
|
+
Arguments inform {option_parser_link} how to parse values as either _optional_ or _required_. Here's minimal implementation of an optional argument:
|
316
|
+
|
317
|
+
[source,ruby]
|
318
|
+
----
|
319
|
+
class Demo < Sod::Action
|
320
|
+
on %w[-e --echo], argument: "[TEXT]"
|
321
|
+
|
322
|
+
def call(text = nil) = puts "Got: #{text}"
|
323
|
+
end
|
324
|
+
|
325
|
+
cli = Sod.new { on Demo }
|
326
|
+
|
327
|
+
cli.call %w[-e] # "Got: "
|
328
|
+
cli.call %w[--echo] # "Got: "
|
329
|
+
cli.call %w[-e hi] # "Got: hi"
|
330
|
+
cli.call %w[--echo hi] # "Got: hi"
|
331
|
+
----
|
332
|
+
|
333
|
+
The method definition of `call(text = nil)` is important because if you call the action directly you'd want to have a safe default that mirrors the `on` macro. You could provide a non-nil default but we'll discuss this more later. You could also use a `call(text)` method definition since {option_parser_link} will always give you a value even if it is `nil`. You can see see how this behavior plays out in the examples above. On the flip side, when you need a _required_ argument, simply drop the brackets (i.e. `[]`). Here's an example:
|
334
|
+
|
335
|
+
[source,ruby]
|
336
|
+
----
|
337
|
+
class Demo < Sod::Action
|
295
338
|
on %w[-e --echo], argument: "TEXT"
|
296
339
|
|
297
|
-
def call(text) = puts text
|
340
|
+
def call(text) = puts "Got: #{text}"
|
298
341
|
end
|
299
342
|
|
300
|
-
cli = Sod.new
|
301
|
-
|
302
|
-
|
343
|
+
cli = Sod.new { on Demo }
|
344
|
+
|
345
|
+
cli.call %w[-e] # "🛑 Missing argument: -e"
|
346
|
+
cli.call %w[--echo] # "🛑 Missing argument: --echo"
|
347
|
+
cli.call %w[-e hi] # "Got: hi"
|
348
|
+
cli.call %w[--echo hi] # "Got: hi"
|
349
|
+
----
|
350
|
+
|
351
|
+
There are only three major differences between the earlier _optional_ example and the above _required_ example:
|
352
|
+
|
353
|
+
* The argument is required because it's not wrapped in brackets.
|
354
|
+
* The method definition requires a `text` parameter.
|
355
|
+
* You get an error when not providing an argument.
|
356
|
+
|
357
|
+
===== Types
|
358
|
+
|
359
|
+
Types are optional but worth having when you need the safety check. Here's a minimal example:
|
360
|
+
|
361
|
+
[source,ruby]
|
362
|
+
----
|
363
|
+
class Demo < Sod::Action
|
364
|
+
on %w[-e --echo], argument: "NUMBER", type: Float
|
365
|
+
|
366
|
+
def call(number) = puts "Got: #{number}"
|
303
367
|
end
|
368
|
+
|
369
|
+
cli = Sod.new { on Demo }
|
370
|
+
|
371
|
+
cli.call %w[--echo 123] # "Got: 123.0"
|
372
|
+
cli.call %w[--echo 1.5] # "Got: 1.5"
|
373
|
+
cli.call %w[--echo hi] # 🛑 Invalid argument: --echo hi
|
304
374
|
----
|
305
375
|
|
306
|
-
|
376
|
+
Notice the type is a `Float` where only the first two examples work but the last one ends in an error because {option_parser_link} can't cast the raw input to a float.
|
377
|
+
|
378
|
+
===== Allows
|
379
|
+
|
380
|
+
Allows give you the ability to define what is acceptable as input and need to match your type (if you supply one). Here's a minimal example:
|
307
381
|
|
308
382
|
[source,ruby]
|
309
383
|
----
|
310
|
-
|
384
|
+
class Demo < Sod::Action
|
385
|
+
on %w[-e --echo], argument: "TEXT", allow: %w[hi hello]
|
311
386
|
|
312
|
-
|
313
|
-
|
314
|
-
# USAGE
|
315
|
-
# demo [OPTIONS]
|
316
|
-
#
|
317
|
-
# OPTIONS
|
318
|
-
# -e, --echo TEXT Echo input as output.
|
319
|
-
# -h, --help [COMMAND] Show this message.
|
320
|
-
#
|
321
|
-
# cli.call %w[--echo hello]
|
387
|
+
def call(text) = puts "Got: #{text}"
|
388
|
+
end
|
322
389
|
|
323
|
-
cli.
|
390
|
+
cli = Sod.new { on Demo }
|
324
391
|
|
325
|
-
#
|
392
|
+
cli.call %w[--echo hi] # "Got: hi"
|
393
|
+
cli.call %w[--echo hello] # "Got: hello"
|
394
|
+
cli.call %w[--echo test] # "🛑 Invalid argument: --echo test"
|
395
|
+
----
|
326
396
|
|
327
|
-
|
397
|
+
Here you can see the first two examples pass while the last one fails because `"test"` isn't a valid value within the allowed array.
|
328
398
|
|
329
|
-
|
399
|
+
===== Defaults
|
330
400
|
|
331
|
-
|
401
|
+
Defaults are not something that {option_parser_link} supports out-of-the-box but are handy for documentation purposes and within your implementation as fallback values. Here's a minimal example:
|
332
402
|
|
333
|
-
|
403
|
+
[source,ruby]
|
334
404
|
----
|
405
|
+
class Demo < Sod::Action
|
406
|
+
on %w[-e --echo], argument: "[TEXT]", default: "fallback"
|
407
|
+
|
408
|
+
def call(text = nil) = puts "Got: #{text || default}"
|
409
|
+
end
|
335
410
|
|
336
|
-
|
411
|
+
cli = Sod.new { on Demo }
|
412
|
+
|
413
|
+
cli.call %w[--echo] # "Got: fallback"
|
414
|
+
cli.call %w[--echo hi] # "Got: hi"
|
415
|
+
----
|
416
|
+
|
417
|
+
Notice how the default is printed when no value is given but is overwritten when an actual value is supplied. This is the correct way to handle defaults but might not be what you are used to. If you're thinking that you'd rather write the implementation like this:
|
418
|
+
|
419
|
+
[source,ruby]
|
420
|
+
----
|
421
|
+
def call(text = default) = puts "Got: #{text}"
|
422
|
+
----
|
423
|
+
|
424
|
+
...you'd not be wrong. In fact, if you initialized and called the action, you'd get what you'd expect:
|
425
|
+
|
426
|
+
[source,ruby]
|
427
|
+
----
|
428
|
+
demo = Demo.new
|
429
|
+
|
430
|
+
demo.call # "Got: fallback"
|
431
|
+
demo.call "hi" # "Got: hi"
|
432
|
+
----
|
433
|
+
|
434
|
+
The reason the above is a problem is because {option_parser_link} ignores _optional_ parameters and all keywords. Here's the fully modified example:
|
435
|
+
|
436
|
+
[source,ruby]
|
437
|
+
----
|
438
|
+
class Demo < Sod::Action
|
439
|
+
on %w[-e --echo], argument: "[TEXT]", default: "fallback"
|
440
|
+
|
441
|
+
def call(text = default) = puts "Got: #{text}"
|
442
|
+
end
|
443
|
+
|
444
|
+
cli = Sod.new { on Demo }
|
445
|
+
|
446
|
+
cli.call %w[--echo] # "Got: "
|
447
|
+
cli.call %w[--echo hi] # "Got: hi"
|
448
|
+
----
|
449
|
+
|
450
|
+
Notice how there is surprising behavior with the first result (i.e. an empty string). This is because when {option_parser_link} completely ignores the value of the _optional_ parameter. I've logged an link:https://github.com/ruby/optparse/issues/55[issue] if you want to know more. For now, be aware of this quirk as it can be confusing if you are not familiar with {option_parser_link}.
|
451
|
+
|
452
|
+
===== Examples
|
453
|
+
|
454
|
+
The following are a few more examples, in case it helps, with the first leveraging all features:
|
337
455
|
|
338
456
|
[source,ruby]
|
339
457
|
----
|
@@ -408,7 +526,7 @@ cli.call ["--echo"]
|
|
408
526
|
# `Echo#call [[:rest, :*]]` must be implemented. (NotImplementedError)
|
409
527
|
----
|
410
528
|
|
411
|
-
At a minimum,
|
529
|
+
At a minimum, your `#call` method needs to allow the forwarding of positional arguments which means you can use `def call(*)` if you want to ignore arguments or define which arguments you care about and ignore the rest. Up to you. Also, _all_ of the information defined within your action is available to you within the instance. Here's an example action which inspects itself:
|
412
530
|
|
413
531
|
[source,ruby]
|
414
532
|
----
|
@@ -451,53 +569,57 @@ Commands are a step up from actions in that they allow you to organize and group
|
|
451
569
|
|
452
570
|
[source,ruby]
|
453
571
|
----
|
454
|
-
|
572
|
+
#! /usr/bin/env ruby
|
573
|
+
# frozen_string_literal: true
|
574
|
+
|
575
|
+
# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.
|
576
|
+
|
577
|
+
require "optparse"
|
578
|
+
|
579
|
+
input = {}
|
455
580
|
|
456
581
|
# Command
|
457
|
-
OptionParser.new do |
|
582
|
+
parser = OptionParser.new do |instance|
|
458
583
|
# Actions
|
459
|
-
|
460
|
-
|
461
|
-
parser.on("--three", "Three.") { |value| options[:three] = value }
|
584
|
+
instance.on("--[no-]one", "One.") { |value| input[:one] = value }
|
585
|
+
instance.on("--[no-]two", "Two.") { |value| input[:two] = value }
|
462
586
|
end
|
587
|
+
|
588
|
+
parser.parse ["--one", "--no-two"]
|
589
|
+
puts input
|
590
|
+
|
591
|
+
# {:one=>true, :two=>false}
|
463
592
|
----
|
464
593
|
|
465
594
|
The equivalent of the above, as provided by this gem, is:
|
466
595
|
|
467
596
|
[source,ruby]
|
468
597
|
----
|
469
|
-
|
470
|
-
|
471
|
-
require "refinements/structs"
|
472
|
-
require "sod"
|
598
|
+
#! /usr/bin/env ruby
|
599
|
+
# frozen_string_literal: true
|
473
600
|
|
474
|
-
|
475
|
-
extend Dry::Container::Mixin
|
601
|
+
# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.
|
476
602
|
|
477
|
-
|
478
|
-
end
|
603
|
+
require "bundler/inline"
|
479
604
|
|
480
|
-
|
605
|
+
gemfile true do
|
606
|
+
source "https://rubygems.org"
|
607
|
+
gem "sod"
|
608
|
+
end
|
481
609
|
|
482
610
|
class One < Sod::Action
|
483
|
-
include Import[:input]
|
484
|
-
|
485
611
|
on "--[no-]one", description: "One."
|
486
612
|
|
487
|
-
def call(value) = input[:one] = value
|
613
|
+
def call(value) = context.input[:one] = value
|
488
614
|
end
|
489
615
|
|
490
616
|
class Two < Sod::Action
|
491
|
-
include Import[:input]
|
492
|
-
|
493
617
|
on "--[no-]two", description: "Two."
|
494
618
|
|
495
|
-
def call(value) = input[:two] = value
|
619
|
+
def call(value) = context.input[:two] = value
|
496
620
|
end
|
497
621
|
|
498
622
|
class Demo < Sod::Command
|
499
|
-
include Import[:input]
|
500
|
-
|
501
623
|
handle "demo"
|
502
624
|
|
503
625
|
description "A demonstration command."
|
@@ -505,11 +627,13 @@ class Demo < Sod::Command
|
|
505
627
|
on One
|
506
628
|
on Two
|
507
629
|
|
508
|
-
def call = puts input
|
630
|
+
def call = puts context.input
|
509
631
|
end
|
510
632
|
|
511
|
-
|
512
|
-
|
633
|
+
context = Sod::Context[input: {}]
|
634
|
+
|
635
|
+
cli = Sod.new banner: "Demo 0.0.0: A demonstration" do
|
636
|
+
on(Demo, context:)
|
513
637
|
on Sod::Prefabs::Actions::Help, self
|
514
638
|
end
|
515
639
|
|
@@ -518,15 +642,13 @@ cli.call ["demo", "--one", "--no-two"]
|
|
518
642
|
# {:one => true, :two => false}
|
519
643
|
----
|
520
644
|
|
521
|
-
You might be thinking: "Hey, that's more lines of code!" True but -- more importantly -- you get the benefit of composible and reusable architectures -- because each command/action is encapsulated -- which you don't get with {option_parser_link}.
|
522
|
-
|
523
|
-
By the way, the above example uses the {dry_container_link} gem for defining dependencies and the {infusible_link} gem for injecting those dependencies. You'll also notice that the `input` hash is memoized within the container to allow for mutation. The fact that you have to mutate input is a bummer and you should strive to avoid mutation whenever you can. In this case, mutation is necessary because the underlining architecture of the {option_parser_link} doesn't provide any other way to share state amongst your commands and actions. So this is one example of how you can do that.
|
645
|
+
You might be thinking: "Hey, that's more lines of code!" True but -- more importantly -- you get the benefit of composible and reusable architectures -- because each command/action is encapsulated -- which you don't get with {option_parser_link}. You'll also notice that the `input` hash is mutated. The fact that you have to mutate input is a bummer and you should strive to avoid mutation whenever you can. In this case, mutation is necessary because the underlining architecture of the {option_parser_link} doesn't provide any other way to share state amongst your commands and actions. So this is one example of how you can do that.
|
524
646
|
|
525
|
-
|
647
|
+
As mentioned earlier with actions, commands share a similar DSL with a few differences in terms of macros:
|
526
648
|
|
527
|
-
* `handle`: Required. The name of your command or the _namespace_ for which you group multiple actions. Otherwise, if not defined,
|
528
|
-
* `description`: Optional (but strongly recommended). Defines what your command is about and shows up in the help documentation. Otherwise, if not provided, your command's
|
529
|
-
* `ancillary`: Optional. Allows you to provide supplemental text for your description
|
649
|
+
* `handle`: Required. The name of your command or the _namespace_ for which you group multiple actions. Must be a string. Otherwise, if not defined, you'll get a `Sod::Error`.
|
650
|
+
* `description`: Optional (but strongly recommended). Defines what your command is about and shows up in the help documentation. Otherwise, if not provided, only your command's handle will be shown.
|
651
|
+
* `ancillary`: Optional. Allows you to provide supplemental text for your description. Can accept single or multiple arguments. Order matters since each argument will appear on a separate line in the order listed below your description.
|
530
652
|
* `on`: Required. The syntax for this is identical to the CLI DSL where you define your action (constant) as the first positional argument followed by any number of positional and/or keyword arguments that you want to feed into your action when the `.new` method is called.
|
531
653
|
|
532
654
|
If we reuse the above example and print the help documentation, you'll see the following output:
|
@@ -572,7 +694,7 @@ Inline commands provide a lightweight way to namespace your actions when you don
|
|
572
694
|
|
573
695
|
[source,ruby]
|
574
696
|
----
|
575
|
-
cli = Sod.new
|
697
|
+
cli = Sod.new banner: "Demo 0.0.0: A demonstration" do
|
576
698
|
on "demo", "A demonstration command." do
|
577
699
|
on One
|
578
700
|
on Two
|
@@ -586,7 +708,7 @@ Inline commands can have ancillary text by passing in additional arguments _afte
|
|
586
708
|
|
587
709
|
[source,ruby]
|
588
710
|
----
|
589
|
-
cli = Sod.new
|
711
|
+
cli = Sod.new banner: "Demo 0.0.0: A demonstration" do
|
590
712
|
on "demo", "A demonstration command.", "Some text.", "Some more text."
|
591
713
|
end
|
592
714
|
----
|
@@ -664,13 +786,13 @@ context.defaults_path # "path/to/defaults.yml"
|
|
664
786
|
context["my/path", :defaults_path] # "my/path"
|
665
787
|
----
|
666
788
|
|
667
|
-
The override is handy for situations where you have a
|
789
|
+
The override is handy for situations where you have a value (first argument) that you would prefer to use while still being able to fallback to the `:defaults_path` if the override is `nil`. When you put all of this together, this means you can build a single context and use it within your commands and actions by injecting it:
|
668
790
|
|
669
791
|
[source,ruby]
|
670
792
|
----
|
671
793
|
context = Sod::Context[defaults_path: "path/to/defaults.yml" version_label: "Demo 0.0.0"]
|
672
794
|
|
673
|
-
Sod.new
|
795
|
+
Sod.new banner: "A demonstration." do
|
674
796
|
on(Sod::Prefabs::Commands::Config, context:)
|
675
797
|
on(Sod::Prefabs::Actions::Version, context:)
|
676
798
|
on Sod::Prefabs::Actions::Help, self
|
@@ -681,7 +803,7 @@ end
|
|
681
803
|
|
682
804
|
==== Types
|
683
805
|
|
684
|
-
Types are a way to extend default {option_parser_link} functionality.
|
806
|
+
Types are a way to extend default {option_parser_link} functionality. Here are a few types -- not provided by {option_parser_link} -- worth knowing about:
|
685
807
|
|
686
808
|
**Pathname**
|
687
809
|
|
@@ -713,6 +835,27 @@ class Demo < Sod::Action
|
|
713
835
|
end
|
714
836
|
----
|
715
837
|
|
838
|
+
**Custom**
|
839
|
+
|
840
|
+
Creating a custom type requires minimal effort and can be implemented in only a few files:
|
841
|
+
|
842
|
+
[source,ruby]
|
843
|
+
----
|
844
|
+
# lib/my_type.rb
|
845
|
+
|
846
|
+
MyType = -> value { # Implementation details go here. }
|
847
|
+
----
|
848
|
+
|
849
|
+
[source,ruby]
|
850
|
+
----
|
851
|
+
# lib/extensions/option_parser.rb
|
852
|
+
require "optparse"
|
853
|
+
|
854
|
+
OptionParser.accept(MyType) { |value| MyType.call value }
|
855
|
+
----
|
856
|
+
|
857
|
+
Once you've implemented a custom type, you are then free to require and reference it within the DSL.
|
858
|
+
|
716
859
|
==== Prefabrications
|
717
860
|
|
718
861
|
Several pre-built commands and actions are provided for you as foundational tooling to get you up and running quickly. You can use and customize them as desired.
|
@@ -755,7 +898,7 @@ This action is most useful when building customizable CLIs where you want users
|
|
755
898
|
|
756
899
|
===== Help
|
757
900
|
|
758
|
-
By now you should be familiar with the help action which allows you to print CLI documentation for users of your CLI. This action consumes the entire graph (i.e. `self`) of information in order to render documentation. You'll want to add this by default or customize with your own help action should you not like the default functionality. Anything is possible. Here's
|
901
|
+
By now you should be familiar with the help action which allows you to print CLI documentation for users of your CLI. This action consumes the entire graph (i.e. `self`) of information in order to render documentation. You'll want to add this by default or customize with your own help action should you not like the default functionality. Anything is possible. Here's some usage:
|
759
902
|
|
760
903
|
[source,ruby]
|
761
904
|
----
|
@@ -925,6 +1068,12 @@ You can also use the IRB console for direct access to all objects:
|
|
925
1068
|
bin/console
|
926
1069
|
----
|
927
1070
|
|
1071
|
+
=== Architecture
|
1072
|
+
|
1073
|
+
The architecture of this gem is built entirely around {option_parser_link} by using a graph of nodes (i.e. commands) which can be walked since each node within the graph may or may not have children (i.e. nesting).
|
1074
|
+
|
1075
|
+
image::https://alchemists.io/images/projects/sod/doc/architecture.svg[Architecture Diagram]
|
1076
|
+
|
928
1077
|
== Tests
|
929
1078
|
|
930
1079
|
To test, run:
|
data/lib/sod/action.rb
CHANGED
@@ -8,9 +8,9 @@ module Sod
|
|
8
8
|
class Action
|
9
9
|
extend Forwardable
|
10
10
|
|
11
|
-
def self.inherited
|
11
|
+
def self.inherited subclass
|
12
12
|
super
|
13
|
-
|
13
|
+
subclass.class_eval { @attributes = {} }
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.description text
|
@@ -42,11 +42,12 @@ module Sod
|
|
42
42
|
|
43
43
|
@record = model[
|
44
44
|
**klass.instance_variable_get(:@attributes),
|
45
|
-
description:
|
46
|
-
ancillary: Array(
|
45
|
+
description: load(:description),
|
46
|
+
ancillary: Array(load(:ancillary)).compact,
|
47
47
|
default: load_default
|
48
48
|
]
|
49
49
|
|
50
|
+
verify_aliases
|
50
51
|
verify_argument
|
51
52
|
end
|
52
53
|
|
@@ -68,12 +69,23 @@ module Sod
|
|
68
69
|
|
69
70
|
private
|
70
71
|
|
72
|
+
def verify_aliases
|
73
|
+
fail Error, "Aliases must be defined." unless aliases
|
74
|
+
end
|
75
|
+
|
71
76
|
def verify_argument
|
72
77
|
return unless argument && !argument.start_with?("[") && default
|
73
78
|
|
74
79
|
fail Error, "Required argument can't be used with default."
|
75
80
|
end
|
76
81
|
|
82
|
+
def load attribute
|
83
|
+
klass = self.class
|
84
|
+
fallback = klass.instance_variable_get(:@attributes)[attribute]
|
85
|
+
|
86
|
+
klass.instance_variable_get("@#{attribute}") || fallback
|
87
|
+
end
|
88
|
+
|
77
89
|
def load_default
|
78
90
|
klass = self.class
|
79
91
|
fallback = klass.instance_variable_get(:@attributes)[:default].method :itself
|
data/lib/sod/command.rb
CHANGED
@@ -10,9 +10,9 @@ module Sod
|
|
10
10
|
|
11
11
|
include Import[:logger]
|
12
12
|
|
13
|
-
def self.inherited
|
13
|
+
def self.inherited subclass
|
14
14
|
super
|
15
|
-
|
15
|
+
subclass.class_eval { @actions = Set.new }
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.handle text
|
@@ -35,16 +35,10 @@ module Sod
|
|
35
35
|
|
36
36
|
def initialize(context: Context::EMPTY, model: Models::Command, **)
|
37
37
|
super(**)
|
38
|
-
klass = self.class
|
39
38
|
@context = context
|
39
|
+
@record = build_record model
|
40
40
|
|
41
|
-
|
42
|
-
handle: klass.instance_variable_get(:@handle),
|
43
|
-
description: klass.instance_variable_get(:@description),
|
44
|
-
ancillary: Array(klass.instance_variable_get(:@ancillary)).compact,
|
45
|
-
actions: Set[*build_actions],
|
46
|
-
operation: method(:call)
|
47
|
-
]
|
41
|
+
verify_handle
|
48
42
|
end
|
49
43
|
|
50
44
|
def call
|
@@ -65,10 +59,26 @@ module Sod
|
|
65
59
|
|
66
60
|
private
|
67
61
|
|
62
|
+
def build_record model
|
63
|
+
klass = self.class
|
64
|
+
|
65
|
+
model[
|
66
|
+
handle: klass.instance_variable_get(:@handle),
|
67
|
+
description: klass.instance_variable_get(:@description),
|
68
|
+
ancillary: Array(klass.instance_variable_get(:@ancillary)).compact,
|
69
|
+
actions: Set[*build_actions],
|
70
|
+
operation: method(:call)
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
68
74
|
def build_actions
|
69
75
|
self.class.instance_variable_get(:@actions).map do |action, positionals, keywords|
|
70
76
|
action.new(*positionals, **keywords.merge!(context:))
|
71
77
|
end
|
72
78
|
end
|
79
|
+
|
80
|
+
def verify_handle
|
81
|
+
fail Error, "Invalid handle: #{handle.inspect}. Must be a string." unless handle in String
|
82
|
+
end
|
73
83
|
end
|
74
84
|
end
|
data/lib/sod/context.rb
CHANGED
@@ -11,11 +11,10 @@ module Sod
|
|
11
11
|
@attributes = attributes
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
default || public_send(fallback)
|
14
|
+
def [] override, fallback
|
15
|
+
override || public_send(fallback)
|
17
16
|
rescue NoMethodError
|
18
|
-
raise Error, "Invalid context.
|
17
|
+
raise Error, "Invalid context. Override or fallback (#{fallback.inspect}) values are missing."
|
19
18
|
end
|
20
19
|
|
21
20
|
def to_h = attributes.dup
|
data/lib/sod/graph/node.rb
CHANGED
@@ -28,14 +28,12 @@ module Sod
|
|
28
28
|
|
29
29
|
def get_child(*lineage, node: self) = lineage.empty? ? node : get(lineage, node, __method__)
|
30
30
|
|
31
|
-
def on(object, *, **, &
|
31
|
+
def on(object, *, **, &)
|
32
32
|
lineage.clear if depth.zero?
|
33
33
|
|
34
34
|
process(object, *, **)
|
35
|
-
|
36
|
-
|
37
|
-
instance_eval(&block) if block
|
38
|
-
decrement
|
35
|
+
visit(&)
|
36
|
+
self
|
39
37
|
end
|
40
38
|
|
41
39
|
def call = (operation.call if operation)
|
@@ -46,8 +44,6 @@ module Sod
|
|
46
44
|
|
47
45
|
attr_accessor :depth
|
48
46
|
|
49
|
-
# :reek:TooManyStatements
|
50
|
-
# rubocop:todo Metrics/AbcSize
|
51
47
|
def process(object, *, **)
|
52
48
|
ancestry = object.is_a?(Class) ? object.ancestors : []
|
53
49
|
|
@@ -61,7 +57,6 @@ module Sod
|
|
61
57
|
fail Error, "Invalid command or action. Unable to add: #{object.inspect}."
|
62
58
|
end
|
63
59
|
end
|
64
|
-
# rubocop:enable Metrics/AbcSize
|
65
60
|
|
66
61
|
def add_inline_command handle, *positionals
|
67
62
|
description, *ancillary = positionals
|
@@ -99,6 +94,12 @@ module Sod
|
|
99
94
|
public_send(message, *lineage, node:)
|
100
95
|
end
|
101
96
|
|
97
|
+
def visit &block
|
98
|
+
increment
|
99
|
+
instance_eval(&block) if block
|
100
|
+
decrement
|
101
|
+
end
|
102
|
+
|
102
103
|
def increment = self.depth += 1
|
103
104
|
|
104
105
|
def decrement = self.depth -= 1
|
data/lib/sod/presenters/node.rb
CHANGED
@@ -16,7 +16,7 @@ module Sod
|
|
16
16
|
using Refinements::Arrays
|
17
17
|
using Refinements::Strings
|
18
18
|
|
19
|
-
delegate
|
19
|
+
delegate %i[handle description ancillary operation children] => :node
|
20
20
|
|
21
21
|
attr_reader :actions
|
22
22
|
|
@@ -32,9 +32,9 @@ module Sod
|
|
32
32
|
# rubocop:enable Metrics/ParameterLists
|
33
33
|
|
34
34
|
def to_s
|
35
|
-
[banner, "", *usage, "", *colored_actions, "", *colored_commands].tap(&:compact!)
|
36
|
-
|
37
|
-
|
35
|
+
[banner, body, "", *usage, "", *colored_actions, "", *colored_commands].tap(&:compact!)
|
36
|
+
.join("\n")
|
37
|
+
.strip
|
38
38
|
end
|
39
39
|
|
40
40
|
private
|
@@ -43,6 +43,8 @@ module Sod
|
|
43
43
|
|
44
44
|
def banner = color[description, :bold]
|
45
45
|
|
46
|
+
def body = ancillary.empty? ? nil : ancillary.join("\n").prepend("\n")
|
47
|
+
|
46
48
|
def usage
|
47
49
|
actions = " #{colored_handle} [OPTIONS]" unless all.empty?
|
48
50
|
commands = " #{colored_handle} COMMAND [OPTIONS]" unless children.empty?
|
data/sod.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
spec.name = "sod"
|
5
|
-
spec.version = "0.
|
5
|
+
spec.version = "0.1.1"
|
6
6
|
spec.authors = ["Brooke Kuhlmann"]
|
7
7
|
spec.email = ["brooke@alchemists.io"]
|
8
8
|
spec.homepage = "https://alchemists.io/projects/sod"
|
9
|
-
spec.summary = "A
|
9
|
+
spec.summary = "A domain specific language for creating composable command line interfaces."
|
10
10
|
spec.license = "Hippocratic-2.1"
|
11
11
|
|
12
12
|
spec.metadata = {
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sod
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brooke Kuhlmann
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
3n5C8/6Zh9DYTkpcwPSuIfAga6wf4nXc9m6JAw8AuMLaiWN/r/2s4zJsUHYERJEu
|
36
36
|
gZGm4JqtuSg8pYjPeIJxS960owq+SfuC+jxqmRA54BisFCv/0VOJi7tiJVY=
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date: 2023-06-
|
38
|
+
date: 2023-06-23 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: cogger
|
@@ -186,5 +186,5 @@ requirements: []
|
|
186
186
|
rubygems_version: 3.4.14
|
187
187
|
signing_key:
|
188
188
|
specification_version: 4
|
189
|
-
summary: A
|
189
|
+
summary: A domain specific language for creating composable command line interfaces.
|
190
190
|
test_files: []
|
metadata.gz.sig
CHANGED
Binary file
|