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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ed0e633c091b67d809287f755ef22f95c8625bb62a381033d2460fcc674a0cd
4
- data.tar.gz: 0556220a0042ef1ee187c5b6ef042de2c948ed360350e6100cd4d4efcd47863f
3
+ metadata.gz: 23b08587b0e2a98f391369e82409ccbfd1186efb54741a76b48c1b011d12a3bc
4
+ data.tar.gz: eb8792a76509485900d2dac190766214fa109d5aff8e0c582229fd8dbcdc92cd
5
5
  SHA512:
6
- metadata.gz: c6a68109cf04ecbc9c030061b04bc7dac9974464a3756b7be2ee3a6af792a7001078367d889e50f914572af940c255467969602308287bf4e8deff68939ec4b4
7
- data.tar.gz: 9f9e4015d492451a15b350bd5ee343c9e8976f067798ff0c0363f9e6c28785c69e683cb932a4bbc54006f570aa0996af522ca53af3dd650619aa7a951fcfa08f
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=597,height=512,role=focal_point]
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=473,height=500,role=focal_point]
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 to you from the command line:
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 (don't worry, this'll be explained shortly), 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:
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
- * Any positional and/or keyword arguments will be forwarded to your command/action. Blocks are excluded since they are used by the `on` method for nesting purposes.
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 wrapper around native {option_parser_link} functionality so if you are familiar with how `OptionParser#on` works, then you'll feel at home with actions.
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. Here's a high level breakdown of the macros you can use:
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
- * `description`: Optional (but strongly encouraged). Allows you to describe your action and appears within help documentation. If the description is not defined, then your action will be runnable while hidden from help documentation (this is similar to how {rake_link} task descriptions work).
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, the action will be ignored. This macro mimics {option_parser_link} `#on` behavior via the following positional and keyword arguments:
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 the option parser by default, you can specify the type by providing a primitive. Example: `String`, `Array`, `Hash`, `Date`, etc. You can also use custom types, as provided by this gem, or your own custom type. See {option_parser_link} documentation for details.
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, which is lazy evaluated, so you can define a default 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 too. If your default value is a boolean then it will be color coded green for `true` and red for `false`.
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
- At a minimum, you want to define the `description` and `on` macros while implementing the `#call` message. Here's an example of implementing an action that echoes input as output:
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 Echo < Sod::Action
293
- description "Echo input as output."
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 :demo, banner: "Demo 0.0.0: A demonstration" do
301
- on Echo
302
- on Sod::Prefabs::Actions::Help, self
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
- If we run the above implementation with different inputs, we'll get multiple outputs:
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
- cli.call
384
+ class Demo < Sod::Action
385
+ on %w[-e --echo], argument: "TEXT", allow: %w[hi hello]
311
386
 
312
- # Demo 0.0.0: A demonstration
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.call %w[--echo hello]
390
+ cli = Sod.new { on Demo }
324
391
 
325
- # hello
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
- cli.call %s[--e hello]
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
- # hello
399
+ ===== Defaults
330
400
 
331
- cli.call ["--echo"]
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
- # 🛑 Missing argument for: --echo.
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
- As you can see, the description shows up in the help text while the `-e` and `--echo` aliases can be used interchangeably. We only get the missing argument error when the argument (i.e. `"TEXT"`) isn't supplied. Finally, when the action is called, we see that the `"hello"` text is outputted to the console for use. Here's an updated version of the above implementation which leverages all features:
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, as shown from the error above, 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:
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
- options = {}
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 |parser|
582
+ parser = OptionParser.new do |instance|
458
583
  # Actions
459
- parser.on("--one", "One.") { |value| options[:one] = value }
460
- parser.on("--two", "Two.") { |value| options[:two] = value }
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
- require "dry/container"
470
- require "infusible"
471
- require "refinements/structs"
472
- require "sod"
598
+ #! /usr/bin/env ruby
599
+ # frozen_string_literal: true
473
600
 
474
- module Container
475
- extend Dry::Container::Mixin
601
+ # Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.
476
602
 
477
- register(:input, memoize: true) { Hash.new }
478
- end
603
+ require "bundler/inline"
479
604
 
480
- Import = Infusible.with Container
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
- cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
512
- on Demo
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
- You'll also notice, as mentioned with actions earlier, that commands share, roughly, the same DSL as actions with a few differences in terms of macros:
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, then your command won't be runnable.
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 description will be blank.
529
- * `ancillary`: Optional. Allows you to provide supplemental text for 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.
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 :demo, banner: "Demo 0.0.0: A demonstration" do
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 :demo, banner: "Demo 0.0.0: A demonstration" do
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 default value that you would prefer to use (i.e. first argument) but want 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:
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 :demo, banner: "A demonstration." do
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. Two types, not provided by {option_parser_link}, that are worth being aware of are:
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 how to use:
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 base
11
+ def self.inherited subclass
12
12
  super
13
- base.class_eval { @attributes = {} }
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: klass.instance_variable_get(:@description),
46
- ancillary: Array(klass.instance_variable_get(:@ancillary)).compact,
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 base
13
+ def self.inherited subclass
14
14
  super
15
- base.class_eval { @actions = Set.new }
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
- @record = model[
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
- # :reek:ControlParameter
15
- def [] default, fallback
16
- default || public_send(fallback)
14
+ def [] override, fallback
15
+ override || public_send(fallback)
17
16
  rescue NoMethodError
18
- raise Error, "Invalid context. Default or fallback (#{fallback.inspect}) values are missing."
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
@@ -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, *, **, &block)
31
+ def on(object, *, **, &)
32
32
  lineage.clear if depth.zero?
33
33
 
34
34
  process(object, *, **)
35
-
36
- increment
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
@@ -16,7 +16,7 @@ module Sod
16
16
  using Refinements::Arrays
17
17
  using Refinements::Strings
18
18
 
19
- delegate Graph::Node.members => :node
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
- .join("\n")
37
- .strip
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.0.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 Domain Specific Language for creating composable Command Line Interfaces."
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.0.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-15 00:00:00.000000000 Z
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 Domain Specific Language for creating composable Command Line Interfaces.
189
+ summary: A domain specific language for creating composable command line interfaces.
190
190
  test_files: []
metadata.gz.sig CHANGED
Binary file