cli-dispatcher 1.1.12 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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: []