cli-dispatcher 1.1.12 → 1.2.1

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 (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/structured.rb +170 -56
  3. metadata +7 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66c29617c61e15163c0659136e753b0b0951b51c68a0a4dc4980d414b5ba249e
4
- data.tar.gz: 727155a4945b77fccef856fa796403d61441f9f0361e66cdf961b109088dc523
3
+ metadata.gz: e946454a75a033c55b82ca4814654e27e75f8635ea02b42ad5fbd95a6cc7b4c8
4
+ data.tar.gz: f964e67077a8e2077a44db8b033d5a82be7fcbe10e9f2cad11504b6116e8397e
5
5
  SHA512:
6
- metadata.gz: b06116458ead3b2f14799c97d9095621074679bfab4483b9de3465cb69dfa8aae50671949534e4e03638a1d1666f46762f92bb4fade081e983aaa71ce108679e
7
- data.tar.gz: 5625718e2281259a478dea7efa64cdd75d11b4c28e792644ffe01dac8beea2f869fdc170ad9d698ae6a85f37401b520cb22d7efe3f420f1104263b06dca7fce8
6
+ metadata.gz: d93f6c815a777f40501094b84bc386178f8a45b20b6dccb4809667d3c7f855b2899a1aa48df04e9cc71eee43353b4105dd911ad1c422f187e276193aa75e8446
7
+ data.tar.gz: 6d81ee52b9cac75a9446b221b7b8c8edf767b5998146a87a14bfe0e9353e80ebfbb4b57ce639c3d2d7c1b7f5fb29c1bc809197b3d07aa71a7cfd7c355295eb99
data/lib/structured.rb CHANGED
@@ -36,6 +36,8 @@ require 'yaml'
36
36
  # As a result, at the end of the initialization of a Structured object, it will
37
37
  # have instance variables set corresponding to all the defined elements.
38
38
  #
39
+ # == Customization of Structured Classes
40
+ #
39
41
  # The above explanation is default behavior, and several customizations are
40
42
  # available.
41
43
  #
@@ -58,6 +60,57 @@ require 'yaml'
58
60
  # Please read the documentation for Structured::ClassMethods for more on
59
61
  # defining expected elements, type checking, and so on.
60
62
  #
63
+ # == Subfiles as Input
64
+ #
65
+ # The Structured class provides automatic support for separating inputs into
66
+ # YAML subfiles. This is useful for including complex objects in a file. Two
67
+ # types of subfile inputs are supported: those for object hashes, and those for
68
+ # arrays.
69
+ #
70
+ # To include a subfile as part of an object hash, include the key `read_file` in
71
+ # the hash, with the value being the file to be read. (Other keys besides
72
+ # `read_file` may be included.) The subfile should itself contain YAML for a
73
+ # hash with further keys for the object.
74
+ #
75
+ # To include multiple subfiles in an array, set the first element of the array
76
+ # to the string `read_file`, and then the other elements of the array should be
77
+ # filenames. These subfiles should contain YAML for arrays.
78
+ #
79
+ # Consider a Structured object for a book, containing elements for the title,
80
+ # subtitle, and an array of authors. The input file could look like this:
81
+ #
82
+ # ---
83
+ # title: A Book
84
+ # subtitle: Containing Many Pages
85
+ # author:
86
+ # - John Q. Public
87
+ # - Jane Doe
88
+ #
89
+ # Using the subfile feature, the input file could instead look like:
90
+ #
91
+ # ---
92
+ # title: A Book
93
+ # read_file: subtitle_file.yaml
94
+ # author:
95
+ # - read_file
96
+ # - author_file.yaml
97
+ #
98
+ # This would instruct Structured to read hash keys out of `subtitle_file.yaml`,
99
+ # and to read array elements out of `author_file.yaml`. These two files, in
100
+ # turn, should look like:
101
+ #
102
+ # # subtitle_file.yaml
103
+ # ---
104
+ # subtitle: Containing Many Pages
105
+ #
106
+ # # author_file.yaml
107
+ # ---
108
+ # - John Q. Public
109
+ # - Jane Doe
110
+ #
111
+ # When incorporated, Structured will combine these subfiles as if they were a
112
+ # single object specification.
113
+ #
61
114
  module Structured
62
115
 
63
116
  #
@@ -172,7 +225,8 @@ module Structured
172
225
  # raises an error, but classes may override this method to use the undefined
173
226
  # elements.
174
227
  #
175
- # @param element The unknown element name, converted to a symbol.
228
+ # @param element The unknown element name. For a YAML file, this is typically
229
+ # a string.
176
230
  #
177
231
  # @param val The value associated with the unknown element.
178
232
  #
@@ -212,6 +266,7 @@ module Structured
212
266
  def reset_elements
213
267
  @elements = {}
214
268
  @default_element = nil
269
+ @default_key = nil
215
270
  @class_description = nil
216
271
  end
217
272
 
@@ -266,14 +321,32 @@ module Structured
266
321
 
267
322
  #
268
323
  # Accepts a default element for this class. The arguments are the same as
269
- # those for element_data.
324
+ # those for element_data except as noted below.
270
325
  #
271
- # **Caution**: The type argument should almost always be a single class, and
272
- # not a hash. This is because the default arguments are automatically
326
+ # If this method is called, then for any keys found in an input hash that
327
+ # have no corresponding #element declaration in the Structured class, the
328
+ # method receive_any will be invoked. The value from the input hash
329
+ # will be processed based on any type declaration, `preproc`, and `check`
330
+ # given to default_element.
331
+ #
332
+ # The default element keys can be processed based on the argument `key`,
333
+ # which should be a hash corresponding to the element_data arguments plus
334
+ # the key :type with the default key's expected type. If `key` is not given,
335
+ # then the key must be and is automatically converted to a Symbol.
336
+ #
337
+ # **Caution**: The `type` argument should almost always be a single class,
338
+ # and not a hash. This is because the default arguments are automatically
273
339
  # treated like a hash, with the otherwise-undefined element names being the
274
340
  # keys of the hash.
275
341
  #
276
342
  def default_element(*args, **params)
343
+ if (key_params = params.delete(:key))
344
+ @default_key = element_data(
345
+ key_params.delete(:type) || Object, **key_params
346
+ )
347
+ else
348
+ @default_key = element_data(Symbol, preproc: proc { |s| s.to_sym })
349
+ end
277
350
  @default_element = element_data(*args, **params)
278
351
  end
279
352
 
@@ -384,47 +457,28 @@ module Structured
384
457
  input_err("Initializer is not a Hash") unless hash.is_a?(Hash)
385
458
  hash = try_read_file(hash)
386
459
 
387
- @elements.each do |elt, data|
388
- Structured.trace(elt.to_s) do
389
- val = hash[elt] || hash[elt.to_s]
390
- next if process_nil_val(obj, elt, val, data)
391
-
392
- if data[:preproc]
393
- val = try_run(data[:preproc], obj, val, "preproc")
394
- next if process_nil_val(obj, elt, val, data)
395
- end
396
-
397
- cval = convert_item(val, data[:type], obj)
398
-
399
- # Check for validity after preproc and conversion are run
400
- if data[:check] && !try_run(data[:check], obj, cval, "check")
401
- input_err "Value #{cval} failed check for #{elt}"
402
- end
403
-
404
- # Use the converted value
405
- apply_val(obj, elt, cval)
460
+ @elements.each do |key, data|
461
+ Structured.trace(key.to_s) do
462
+ val = hash[key] || hash[key.to_s]
463
+ cval = process_value(obj, val, data)
464
+ apply_val(obj, key, cval) if cval
406
465
  end
407
466
  end
408
467
 
409
468
  # Process unknown elements
410
- unknown_elts = (hash.keys.map(&:to_sym) - @elements.keys)
411
- return if unknown_elts.empty?
469
+ unknown_keys = hash.keys.reject { |k| @elements.include?(k.to_sym) }
470
+ return if unknown_keys.empty?
412
471
  unless @default_element
413
- input_err("Unexpected element(s): #{unknown_elts.join(', ')}")
472
+ input_err("Unexpected element(s): #{unknown_keys.join(', ')}")
414
473
  end
415
- unknown_elts.each do |elt|
416
- Structured.trace(elt.to_s) do
417
- de = @default_element
418
- val = hash[elt] || hash[elt.to_s]
419
- if de[:preproc]
420
- val = try_run(de[:preproc], obj, val, "default preproc")
421
- end
422
- item = convert_item(val, de[:type], obj)
423
- if de[:check] && !try_run(de[:check], obj, item, "check")
424
- input_err "Value #{item} failed default element check"
425
- end
426
- item.receive_key(elt) if item.is_a?(Structured)
427
- obj.receive_any(elt, item)
474
+ unknown_keys.each do |key|
475
+ Structured.trace(key.to_s) do
476
+ val = hash[key]
477
+ ckey = process_value(obj, key, @default_key)
478
+ cval = process_value(obj, val, @default_element)
479
+ next unless cval
480
+ cval.receive_key(ckey) if cval.is_a?(Structured)
481
+ obj.receive_any(ckey, cval)
428
482
  end
429
483
  end
430
484
  end
@@ -450,24 +504,68 @@ module Structured
450
504
  end
451
505
  end
452
506
 
453
- # Deals with a nil value (either because no value was given, or because a
454
- # preproc deleted it).
507
+ def try_read_array(filenames)
508
+ new_item = []
509
+ begin
510
+ filenames.each do |file|
511
+ begin
512
+ res = YAML.load_file(file)
513
+ raise InputError unless res.is_a?(Array)
514
+ new_item.concat(res)
515
+ rescue
516
+ input_err("Failed to read array from #{file}: #$!")
517
+ end
518
+ end
519
+ end
520
+ return new_item
521
+ end
522
+
523
+ #
524
+ # Given an element value and an #element_data hash of processing tools
525
+ # element, applies those processing tools. Namely, apply any preproc, check
526
+ # the type and perform other checks, and perform any conversions. The return
527
+ # value should be usable as the received value for the corresponding
528
+ # element.
529
+ #
530
+ # If this method returns nil, then there is no element to process. This
531
+ # method may also raise an InputError.
532
+ #
533
+ def process_value(obj, val, data)
534
+ val, ret = process_nil_val(val, data)
535
+ return val if ret
536
+ if data[:preproc]
537
+ val = try_run(data[:preproc], obj, val, "preproc")
538
+ val, ret = process_nil_val(val, data)
539
+ return val if ret
540
+ end
541
+
542
+ cval = convert_item(val, data[:type], obj)
543
+ if data[:check] && !try_run(data[:check], obj, cval, "check")
544
+ input_err "Value #{cval} failed check"
545
+ end
546
+ return cval
547
+ end
548
+
455
549
  #
456
- # * If val is non-nil, then this method returns false.
550
+ # Performs processing of an element value to deal with the possibility that
551
+ # the value is nil. This method returns [ the new value, boolean of whether
552
+ # to stop processing ] according to the following rules:
553
+ #
554
+ # * If val is non-nil, then this method returns val itself, and processing
555
+ # should not stop.
457
556
  # * If val is nil and this element is non-optional, then this method raises
458
557
  # an error.
459
- # * If val is nil and the element is optional, *and* the element has a
460
- # default value, then the object has the default value applied to the
461
- # element.
462
- # * In any event, if val is nil and the element is optional, returns true
463
- # which should signal to the caller to stop further processing of the
464
- # element.
465
- #
466
- def process_nil_val(obj, elt, val, data)
467
- return false if val
468
- input_err("Missing (or preproc deleted) #{elt}") unless data[:optional]
469
- apply_val(obj, elt, data[:default]) unless data[:default].nil?
470
- return true
558
+ # * If val is nil and the element is optional, then the object's default
559
+ # value is returned, and processing should stop.
560
+ # * If there is no default value for an optional element, then nil is
561
+ # returned, and processing should also stop.
562
+ #
563
+ def process_nil_val(val, data)
564
+ return [ val, false ] if val
565
+ unless data[:optional]
566
+ input_err("Required element is missing (or was deleted by a preproc)")
567
+ end
568
+ return [ data[:default], true ]
471
569
  end
472
570
 
473
571
  # Applies a value to an element for an object, after all processing for the
@@ -504,6 +602,7 @@ module Structured
504
602
  when Array
505
603
  input_err("#{item} is not Array") unless item.is_a?(Array)
506
604
  Structured.trace(Array) do
605
+ item = try_read_array(item[1..-1]) if item.first.to_s == 'read_file'
507
606
  return item.map.with_index { |i, idx|
508
607
  Structured.trace(idx) do
509
608
  convert_item(i, type.first, parent)
@@ -542,12 +641,22 @@ module Structured
542
641
  end
543
642
  end
544
643
 
644
+ #
645
+ # Several types can be automatically converted:
646
+ #
647
+ # * Symbol into String
648
+ # * String into Regexp
649
+ #
545
650
  def try_autoconvert(type, item)
546
651
 
547
652
  if type == String && item.is_a?(Symbol)
548
653
  return item.to_s
549
654
  end
550
655
 
656
+ if type == Symbol && item.is_a?(String)
657
+ return item.to_sym
658
+ end
659
+
551
660
  # Special case in which strings will be converted to Regexps
552
661
  if type == Regexp && item.is_a?(String)
553
662
  begin
@@ -563,7 +672,11 @@ module Structured
563
672
  # Receive hash values that are to be converted to Structured objects
564
673
  def convert_structured(item, type, parent)
565
674
  unless item.is_a?(Hash)
566
- input_err("#{item.inspect} not a #{type} or Structured hash")
675
+ if type.include?(Structured)
676
+ input_err("#{item.inspect} not a Structured hash for #{type}")
677
+ else
678
+ input_err("#{item.inspect} not a #{type}")
679
+ end
567
680
  end
568
681
 
569
682
  unless type.include?(Structured) || type.include?(StructuredPolymorphic)
@@ -604,7 +717,8 @@ module Structured
604
717
 
605
718
  if @default_element
606
719
  io.puts(
607
- " All other elements: #{describe_type(@default_element[:type])}"
720
+ " All other elements: #{describe_type(@default_key[:type])} => " \
721
+ "#{describe_type(@default_element[:type])}"
608
722
  )
609
723
  if @default_element[:description]
610
724
  io.puts(TextTools.line_break(
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cli-dispatcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.12
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charles Duan
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-13 00:00:00.000000000 Z
11
+ date: 2024-12-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Library for creating command-line programs that accept commands. Also
@@ -28,7 +28,8 @@ licenses:
28
28
  - MIT
29
29
  metadata:
30
30
  source_code_uri: https://github.com/charlesduan/cli-dispatcher
31
- post_install_message:
31
+ documentation_uri: https://rubydoc.info/gems/cli-dispatcher/
32
+ post_install_message:
32
33
  rdoc_options: []
33
34
  require_paths:
34
35
  - lib
@@ -43,8 +44,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
44
  - !ruby/object:Gem::Version
44
45
  version: '0'
45
46
  requirements: []
46
- rubygems_version: 3.0.3.1
47
- signing_key:
47
+ rubygems_version: 3.5.11
48
+ signing_key:
48
49
  specification_version: 4
49
50
  summary: Command-line command dispatcher
50
51
  test_files: []