cli-dispatcher 1.1.11
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.
- checksums.yaml +7 -0
- data/lib/cli-dispatcher.rb +187 -0
- data/lib/structured-poly.rb +179 -0
- data/lib/structured.rb +731 -0
- data/lib/texttools.rb +94 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 275e6d346e13d40131484394f8da7d754df96294e559ad1461628274d14755fb
|
4
|
+
data.tar.gz: c7eb8d374b5eda6861b70465413574b67ed8e6fd9fa57d1b837a0df3d73c79a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 781dd82a1715c122bd8293605bd0bb2480e51128efdec7758b982bfe3cd7c02b7cf93f25427716b1d4e302664524b329bcbebaa74d9ee956ba1a46e392d9c050
|
7
|
+
data.tar.gz: c3221e641bbc6b2461b500ab626de8954ab7f8e87cef2bb5bdcdebf910b2ba38bd7e0a4f069a9bd67ee2f82212cba7c2f49dacbca23f78e8439fa7a31b564b8e
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Constructs a program that can operate a number of user-provided commands. To
|
5
|
+
# use this class, subclass it and define methods of the form:
|
6
|
+
#
|
7
|
+
# def cmd_name(args...)
|
8
|
+
#
|
9
|
+
# Then create an instance of the class and call one of the dispatch methods.
|
10
|
+
#
|
11
|
+
# To provide help for a command, define a method:
|
12
|
+
#
|
13
|
+
# def help_name
|
14
|
+
#
|
15
|
+
# The first line should be a short description of the command, which will be
|
16
|
+
# used in a summary table describing the command.
|
17
|
+
#
|
18
|
+
# This class incorporates optparse, providing the commands setup_options and
|
19
|
+
# add_options to pass
|
20
|
+
# through options specifications.
|
21
|
+
#
|
22
|
+
class Dispatcher
|
23
|
+
|
24
|
+
#
|
25
|
+
# Reads ARGV and dispatches a command. If no arguments are given, an
|
26
|
+
# appropriate warning is issued and the program terminates.
|
27
|
+
#
|
28
|
+
def dispatch_argv
|
29
|
+
@option_parser ||= OptionParser.new
|
30
|
+
add_options(@option_parser)
|
31
|
+
@option_parser.banner = <<~EOF
|
32
|
+
Usage: #$0 [options] command [arguments...]
|
33
|
+
Run '#$0 help' for a list of commands.
|
34
|
+
|
35
|
+
Options:
|
36
|
+
EOF
|
37
|
+
@option_parser.on_tail('-h', '--help', 'Show this help') do
|
38
|
+
warn(@option_parser)
|
39
|
+
warn("\nCommands:")
|
40
|
+
cmd_help
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
|
44
|
+
@option_parser.parse!
|
45
|
+
if ARGV.empty?
|
46
|
+
STDERR.puts(@option_parser)
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
dispatch(*ARGV)
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Dispatches a single command with given arguments. If the command is not
|
54
|
+
# found, then issues a help warning.
|
55
|
+
#
|
56
|
+
def dispatch(cmd, *args)
|
57
|
+
cmd_sym = "cmd_#{cmd}".to_sym
|
58
|
+
begin
|
59
|
+
if respond_to?(cmd_sym)
|
60
|
+
send(cmd_sym, *args)
|
61
|
+
else
|
62
|
+
warn("Usage: #$0 [options] command [arguments...]")
|
63
|
+
warn("Run '#$0 help' for a list of commands.")
|
64
|
+
exit(1)
|
65
|
+
end
|
66
|
+
rescue ArgumentError
|
67
|
+
if $!.backtrace_locations.first.base_label == cmd_sym.to_s
|
68
|
+
warn("#{cmd}: wrong number of arguments")
|
69
|
+
warn("Usage: #{signature_string(cmd)}")
|
70
|
+
exit(1)
|
71
|
+
else
|
72
|
+
raise $!
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def help_string(cmd, all: true)
|
78
|
+
cmd_sym = "help_#{cmd}".to_sym
|
79
|
+
return signature_string(cmd) unless respond_to?(cmd_sym)
|
80
|
+
if all
|
81
|
+
return $0 + " " + signature_string(cmd) + "\n\n" + send(cmd_sym)
|
82
|
+
else
|
83
|
+
return send(cmd_sym).to_s.split("\n", 2).first
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def signature_string(cmd)
|
88
|
+
cmd_sym = "cmd_#{cmd}".to_sym
|
89
|
+
raise "No such command" unless respond_to?(cmd_sym)
|
90
|
+
return cmd + " " + method(cmd_sym).parameters.map { |type, name|
|
91
|
+
case type
|
92
|
+
when :req then name.to_s
|
93
|
+
when :opt then "[#{name}]"
|
94
|
+
when :rest then "*#{name}"
|
95
|
+
when :keyreq, :key, :keyrest, :block then nil
|
96
|
+
else raise "Unknown parameter type #{type}"
|
97
|
+
end
|
98
|
+
}.compact.join(" ")
|
99
|
+
end
|
100
|
+
|
101
|
+
def help_help
|
102
|
+
return <<~EOF
|
103
|
+
Displays help on commands.
|
104
|
+
|
105
|
+
Run 'help [command]' for further help on that command.
|
106
|
+
EOF
|
107
|
+
end
|
108
|
+
|
109
|
+
def cmd_help(cmd = nil, all: true)
|
110
|
+
|
111
|
+
if cmd
|
112
|
+
warn("")
|
113
|
+
warn(help_string(cmd))
|
114
|
+
warn("")
|
115
|
+
exit(1)
|
116
|
+
end
|
117
|
+
|
118
|
+
warn("Run 'help [command]' for further help on that command.")
|
119
|
+
warn("")
|
120
|
+
|
121
|
+
methods.map { |m|
|
122
|
+
s = m.to_s
|
123
|
+
s.start_with?("cmd_") ? s.delete_prefix("cmd_") : nil
|
124
|
+
}.compact.sort.each do |cmd|
|
125
|
+
warn("%-10s %s" % [ cmd, help_string(cmd, all: false) ])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# Adds commands relevant when this dispatcher uses Structured data inputs.
|
131
|
+
#
|
132
|
+
def self.add_structured_commands
|
133
|
+
def help_explain
|
134
|
+
return <<~EOF
|
135
|
+
Displays an explanation of a Structured class.
|
136
|
+
|
137
|
+
Use this to assist in generating or checking a Rubric file.
|
138
|
+
EOF
|
139
|
+
end
|
140
|
+
|
141
|
+
def cmd_explain(class_name)
|
142
|
+
c = Object.const_get(class_name)
|
143
|
+
unless c.is_a?(Class) && c.include?(Structured)
|
144
|
+
raise "Invalid class #{class_name}"
|
145
|
+
end
|
146
|
+
c.explain
|
147
|
+
end
|
148
|
+
|
149
|
+
def help_template
|
150
|
+
return <<~EOF
|
151
|
+
Produces a template for the given Structured class.
|
152
|
+
EOF
|
153
|
+
end
|
154
|
+
|
155
|
+
def cmd_template(class_name)
|
156
|
+
c = Object.const_get(class_name)
|
157
|
+
unless c.is_a?(Class) && c.include?(Structured)
|
158
|
+
raise("Invalid class #{class_name}")
|
159
|
+
end
|
160
|
+
puts c.template
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
# Receives options, passing them to OptionParser. The options are processed
|
166
|
+
# when dispatch_argv is called. The usage of this method is that after the
|
167
|
+
# Dispatcher object is created, this method is called to instantiate the
|
168
|
+
# options for the class. See #add_options for another way of doing this.
|
169
|
+
#
|
170
|
+
# The banner and -h/--help options will be added automatically.
|
171
|
+
#
|
172
|
+
def setup_options
|
173
|
+
@option_parser = OptionParser.new do |opts|
|
174
|
+
yield(opts)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Given an OptionParser object, add options. By default, this method does
|
180
|
+
# nothing. The usage of this method, in contrast to #setup_options, is to
|
181
|
+
# override this method, invoking calls to the +opts+ argument to add options.
|
182
|
+
# The method will be called automatically when the Dispatcher is invoked.
|
183
|
+
#
|
184
|
+
def add_options(opts)
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
#
|
2
|
+
# Creates a Structured class that can turn into multiple kinds of Structured
|
3
|
+
# objects.
|
4
|
+
#
|
5
|
+
# To use, include StructuredPolymorphic in a relevant class, and then within the
|
6
|
+
# class body use ClassMethods#type or ClassMethods#types to specify the
|
7
|
+
# different types of Structured objects that this class can produce.
|
8
|
+
#
|
9
|
+
# When a StructuredPolymorphic object is initialized based on a hash, the hash
|
10
|
+
# is checked for a key called +type+. (The key can be changed using
|
11
|
+
# ClassMethods#set_type_key.) The value of that +type+ key is used to determine
|
12
|
+
# what type of Structured object to create.
|
13
|
+
#
|
14
|
+
module StructuredPolymorphic
|
15
|
+
|
16
|
+
#
|
17
|
+
# This should never be called because the +new+ method is overridden.
|
18
|
+
#
|
19
|
+
def initialize(*args, **params)
|
20
|
+
raise TypeError, "Abstract StructuredPolymorphic class"
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def reset
|
26
|
+
@subclasses = {}
|
27
|
+
@class_description = nil
|
28
|
+
@type_key = :type
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Provides a description of this class, for use with the #explain method.
|
33
|
+
#
|
34
|
+
def set_description(desc)
|
35
|
+
@class_description = desc
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Returns the class's description. The given number can be used to limit the
|
40
|
+
# length of the description.
|
41
|
+
#
|
42
|
+
def description(len = nil)
|
43
|
+
desc = @class_description || ''
|
44
|
+
if len && desc.length > len
|
45
|
+
return desc[0, len] if len <= 5
|
46
|
+
return desc[0, len - 3] + '...'
|
47
|
+
end
|
48
|
+
return desc
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Sets the hash key in which the polymorphic subtype is identified. By
|
53
|
+
# default the key is +:type+.
|
54
|
+
#
|
55
|
+
def set_type_key(key)
|
56
|
+
@type_key = key.to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Adds a new subtype to this polymorphic superclass.
|
61
|
+
#
|
62
|
+
# @param name The textual name for identifying the subclass.
|
63
|
+
# @param subclass The Structured Class object to be created.
|
64
|
+
#
|
65
|
+
def type(name, subclass)
|
66
|
+
unless subclass.include?(Structured)
|
67
|
+
raise ArgumentError, "#{subclass} is not Structured"
|
68
|
+
end
|
69
|
+
@subclasses[name.to_sym] = subclass
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Adds multiple subtypes by repeatedly calling #type for all key-value
|
74
|
+
# pairs.
|
75
|
+
#
|
76
|
+
def types(**params)
|
77
|
+
params.each do |name, subclass|
|
78
|
+
type(name, subclass)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Returns the class corresponding to the given type.
|
84
|
+
#
|
85
|
+
def type_for(name)
|
86
|
+
return @subclasses[name.to_sym]
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Iterates through all the types.
|
91
|
+
#
|
92
|
+
def each
|
93
|
+
@subclasses.sort.each do |type, c| yield(type, c) end
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Prints out documentation for this class.
|
98
|
+
#
|
99
|
+
def explain(io = STDOUT)
|
100
|
+
io.puts("Polymorphic Structured Class #{self}:")
|
101
|
+
if @class_description
|
102
|
+
io.puts("\n" + TextTools.line_break(@class_description, prefix: ' '))
|
103
|
+
end
|
104
|
+
io.puts
|
105
|
+
io.puts "Available subtypes:"
|
106
|
+
max_type_len = @subclasses.keys.map(&:to_s).map(&:length).max
|
107
|
+
@subclasses.sort.each do |type, c|
|
108
|
+
desc = c.description(80 - max_type_len - 5)
|
109
|
+
desc = c.name if desc == ''
|
110
|
+
io.puts " #{type.to_s.ljust(max_type_len)} #{desc}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def template(indent: '')
|
115
|
+
res = "#{indent}# #{name}\n"
|
116
|
+
if @class_description
|
117
|
+
res << indent
|
118
|
+
res << TextTools.line_break(@class_description, prefix: "#{indent}# ")
|
119
|
+
res << "\n"
|
120
|
+
end
|
121
|
+
res << indent << "type: \n"
|
122
|
+
res << indent << "...\n"
|
123
|
+
return res
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# Constructs a new object of this StructuredPolymorphic type, by inspecting
|
128
|
+
# the hash's type identifier and calling the corresponding class's
|
129
|
+
# constructor.
|
130
|
+
#
|
131
|
+
def new(hash, parent = nil)
|
132
|
+
|
133
|
+
# For subclasses, don't use this overridden new method.
|
134
|
+
if self.include?(Structured)
|
135
|
+
return super(hash, parent)
|
136
|
+
end
|
137
|
+
|
138
|
+
Structured.trace(self) do
|
139
|
+
|
140
|
+
type = hash[@type_key] || hash[@type_key.to_s]
|
141
|
+
input_err("no type") unless type
|
142
|
+
type_class = @subclasses[type.to_sym]
|
143
|
+
input_err("Unknown #{name} type #{type}") unless type_class
|
144
|
+
|
145
|
+
# Remove the type key when initializing the subclass
|
146
|
+
new_hash = hash.dup
|
147
|
+
new_hash.delete(@type_key)
|
148
|
+
new_hash.delete(@type_key.to_s)
|
149
|
+
o = type_class.new(new_hash, parent)
|
150
|
+
|
151
|
+
# Set the type value
|
152
|
+
o.instance_variable_set(:@type, type)
|
153
|
+
return o
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def inherited(base)
|
159
|
+
base.include(Structured)
|
160
|
+
end
|
161
|
+
|
162
|
+
def input_err(text)
|
163
|
+
raise Structured::InputError, text
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
#
|
169
|
+
# Extends ClassMethods to the including class's class methods.
|
170
|
+
#
|
171
|
+
def self.included(base)
|
172
|
+
if base.is_a?(Class)
|
173
|
+
base.extend(ClassMethods)
|
174
|
+
base.reset
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
end
|
data/lib/structured.rb
ADDED
@@ -0,0 +1,731 @@
|
|
1
|
+
require_relative 'texttools'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Sets up a class to receive an initializing hash and to populate information
|
6
|
+
# about the class from that hash. The expected hash elements are
|
7
|
+
# self-documenting and type-checking to facilitate future generation of hash
|
8
|
+
# elements.
|
9
|
+
#
|
10
|
+
# The basic usage is to include the +Structured+ module in a class, which gives
|
11
|
+
# the class a method ClassMethods#element, used to declare elements expected in
|
12
|
+
# the initializing hash. Once an element is declared, a few things happen:
|
13
|
+
#
|
14
|
+
# * The element is looked for upon initialization
|
15
|
+
#
|
16
|
+
# * If found, the element's value is type-checked and possibly converted to a
|
17
|
+
# new object. In particular:
|
18
|
+
#
|
19
|
+
# * If the expected type is a Structured object, then the value is expected to
|
20
|
+
# be a hash, which is used as input to construct the expected Structured
|
21
|
+
# object. This subsidiary Structured object has its +@parent+ instance
|
22
|
+
# variable set so that a complete two-way tree of objects is maintained.
|
23
|
+
#
|
24
|
+
# * If the expected type is an Array of Structured objects, then the value is
|
25
|
+
# expected to be an array of hashes, each of which is converted to the
|
26
|
+
# expected Structured object. The +@parent+ variable is also set.
|
27
|
+
#
|
28
|
+
# * If the expected type is a Hash including Structured object, then the value
|
29
|
+
# is similarly converted to a hash of Structured objects. As an added
|
30
|
+
# benefit, besides +@parent+ being set, hash values have the +@key+ instance
|
31
|
+
# variable set, so that the values are aware of the hash key with which they
|
32
|
+
# are associated.
|
33
|
+
#
|
34
|
+
# * An instance variable +@[element]+ is set to the given value.
|
35
|
+
#
|
36
|
+
# As a result, at the end of the initialization of a Structured object, it will
|
37
|
+
# have instance variables set corresponding to all the defined elements.
|
38
|
+
#
|
39
|
+
# The above explanation is default behavior, and several customizations are
|
40
|
+
# available.
|
41
|
+
#
|
42
|
+
# * Methods +receive_[element]+ can be defined, taking a single parameter. By
|
43
|
+
# default, the method sets an instance variable +@[name]+ with the parameter
|
44
|
+
# value. Classes may override this method to provide different initialization
|
45
|
+
# actions. (Alternately, classes can accept the default initialization methods
|
46
|
+
# and override #initialize for further processing.)
|
47
|
+
#
|
48
|
+
# * Methods receive_parent and receive_key can be similarly redefined to change
|
49
|
+
# the processing of parent Structured objects and hash keys, respectively.
|
50
|
+
#
|
51
|
+
# * To process unknown elements, call ClassMethods#default_element to specify
|
52
|
+
# their expected type. (It should typically be just a class name, as that
|
53
|
+
# method's documentation explains.) Then define +receive_any+ to handle
|
54
|
+
# undefined elements, for example by placing them in a hash. For these
|
55
|
+
# elements, the +@key+ instance variable is also set for them if the expected
|
56
|
+
# type is a Structured class.
|
57
|
+
#
|
58
|
+
# Please read the documentation for Structured::ClassMethods for more on
|
59
|
+
# defining expected elements, type checking, and so on.
|
60
|
+
#
|
61
|
+
module Structured
|
62
|
+
|
63
|
+
#
|
64
|
+
# Error class when there is a defect in Structured input. This class will
|
65
|
+
# eventually provide more robust tracing information about where the error
|
66
|
+
# occurred.
|
67
|
+
#
|
68
|
+
class InputError < StandardError
|
69
|
+
attr_accessor :structured_stack
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
|
73
|
+
res = [ [ nil, nil ] ]
|
74
|
+
|
75
|
+
return super unless @structured_stack
|
76
|
+
|
77
|
+
@structured_stack.each do |item|
|
78
|
+
if item.is_a?(Class)
|
79
|
+
res.last[0] = item
|
80
|
+
else
|
81
|
+
res.push([ nil, nil ])
|
82
|
+
res.last[1] = item
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
return res.map { |cls, item|
|
87
|
+
case
|
88
|
+
when item && cls then "\"#{item}\" (#{cls})"
|
89
|
+
when item then "\"#{item}\""
|
90
|
+
when cls then "#{cls}"
|
91
|
+
else nil
|
92
|
+
end
|
93
|
+
}.compact.join(" -> ") + ": " + super
|
94
|
+
end
|
95
|
+
|
96
|
+
def backtrace
|
97
|
+
return []
|
98
|
+
end
|
99
|
+
|
100
|
+
def cause
|
101
|
+
return nil
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
#
|
108
|
+
# Initializes the object based on an initialization hash. All methods that
|
109
|
+
# include Structured should retain this initialization signature to the extent
|
110
|
+
# possible, because downstream Structured objects expect to be initialized
|
111
|
+
# this way.
|
112
|
+
#
|
113
|
+
# @param hash The initializing hash for this object.
|
114
|
+
# @param parent The parent object to this Structured object.
|
115
|
+
#
|
116
|
+
def initialize(hash, parent = nil)
|
117
|
+
Structured.trace(self.class) do
|
118
|
+
pre_initialize
|
119
|
+
receive_parent(parent) if parent
|
120
|
+
self.class.build_from_hash(self, hash)
|
121
|
+
post_initialize
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Subclasses may override this method to provide pre-initialization routines,
|
127
|
+
# run before the initializing hash is processed.
|
128
|
+
#
|
129
|
+
def pre_initialize
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Subclasses may override this method to provide post-initialization routines,
|
134
|
+
# run after the initializing hash is processed. This may be useful for global
|
135
|
+
# data checks (that depend on several values).
|
136
|
+
#
|
137
|
+
def post_initialize
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Processes the parent object for this Structured class. The parent is
|
142
|
+
# automatically given for subsidiary Structured objects, triggering a call to
|
143
|
+
# this method.
|
144
|
+
#
|
145
|
+
# By default, +@parent+ is set to the given object. Classes may override this
|
146
|
+
# method to do other things with the parent object (for example, test the
|
147
|
+
# parent object type).
|
148
|
+
#
|
149
|
+
def receive_parent(parent)
|
150
|
+
@parent = parent
|
151
|
+
end
|
152
|
+
|
153
|
+
attr_reader :parent
|
154
|
+
|
155
|
+
#
|
156
|
+
# Processes the key object for this Structured class. The key is automatically
|
157
|
+
# given when this Structured object is a subsidiary of another, within a
|
158
|
+
# key-value hash. It is also automatically given when this Structured object
|
159
|
+
# is created while processing a default element.
|
160
|
+
#
|
161
|
+
# By default, this method sets +@key+ to the given object. Classes may
|
162
|
+
# override this method to do other things with the key object.
|
163
|
+
#
|
164
|
+
def receive_key(key)
|
165
|
+
@key = key
|
166
|
+
end
|
167
|
+
|
168
|
+
attr_reader :key
|
169
|
+
|
170
|
+
#
|
171
|
+
# Processes an undefined element in the initializing hash. By default, this
|
172
|
+
# raises an error, but classes may override this method to use the undefined
|
173
|
+
# elements.
|
174
|
+
#
|
175
|
+
# @param element The unknown element name, converted to a symbol.
|
176
|
+
#
|
177
|
+
# @param val The value associated with the unknown element.
|
178
|
+
#
|
179
|
+
def receive_any(element, val)
|
180
|
+
raise NameError, "Unexpected element for #{self.class}: #{element}"
|
181
|
+
end
|
182
|
+
|
183
|
+
#
|
184
|
+
# Raises an InputError.
|
185
|
+
#
|
186
|
+
def input_err(text)
|
187
|
+
raise InputError, text
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
# Methods extended to a Structured class. A class would typically use the
|
192
|
+
# following methods within its class body:
|
193
|
+
#
|
194
|
+
# * #set_description to set a textual description of the object
|
195
|
+
#
|
196
|
+
# * #element to define expected elements of the input hash
|
197
|
+
#
|
198
|
+
# * #default_element to define processing of unknown element keys
|
199
|
+
#
|
200
|
+
# The #explain method is also useful for printing out documentation for a
|
201
|
+
# Structured class.
|
202
|
+
#
|
203
|
+
module ClassMethods
|
204
|
+
|
205
|
+
#
|
206
|
+
# Sets up a class to manage elements. This method is called when
|
207
|
+
# Structured is included in the class.
|
208
|
+
#
|
209
|
+
# As an implementation note: Information about a Structured class is stored
|
210
|
+
# in instance variables of the class's object.
|
211
|
+
#
|
212
|
+
def reset_elements
|
213
|
+
@elements = {}
|
214
|
+
@default_element = nil
|
215
|
+
@class_description = nil
|
216
|
+
end
|
217
|
+
|
218
|
+
#
|
219
|
+
# Provides a description of this class, for use with the #explain method.
|
220
|
+
#
|
221
|
+
def set_description(desc)
|
222
|
+
@class_description = desc
|
223
|
+
end
|
224
|
+
|
225
|
+
#
|
226
|
+
# Returns the class's description. The given number can be used to limit the
|
227
|
+
# length of the description.
|
228
|
+
#
|
229
|
+
def description(len = nil)
|
230
|
+
desc = @class_description || ''
|
231
|
+
if len && desc.length > len
|
232
|
+
return desc[0, len] if len <= 5
|
233
|
+
return desc[0, len - 3] + '...'
|
234
|
+
end
|
235
|
+
return desc
|
236
|
+
end
|
237
|
+
|
238
|
+
#
|
239
|
+
# Declares that the class expects an element with the given name and type.
|
240
|
+
# See element_data for an explanation of +*args+ and +**params+.
|
241
|
+
#
|
242
|
+
# @param [Symbol] name The name of the element.
|
243
|
+
# @param attr Whether to create an attribute (i.e., call +attr_reader+) for
|
244
|
+
# the given element. Default is true.
|
245
|
+
#
|
246
|
+
def element(name, *args, attr: true, **params)
|
247
|
+
@elements[name.to_sym] = element_data(*args, **params)
|
248
|
+
#
|
249
|
+
# By default, when an element is received, a corresponding instance
|
250
|
+
# variable is set. Classes using Structured can define +receive_[name]+ so
|
251
|
+
# that the element declaration will perform other tasks.
|
252
|
+
#
|
253
|
+
# This creates the reader attribute only if there is no other method of
|
254
|
+
# the same name.
|
255
|
+
#
|
256
|
+
attr_reader(name) if attr && !method_defined?(name)
|
257
|
+
end
|
258
|
+
|
259
|
+
#
|
260
|
+
# Removes an element. Note that the attribute definition if any and the
|
261
|
+
# +receive_[name]+ method are left intact.
|
262
|
+
#
|
263
|
+
def remove_element(name)
|
264
|
+
@elements.delete(name.to_sym)
|
265
|
+
end
|
266
|
+
|
267
|
+
#
|
268
|
+
# Accepts a default element for this class. The arguments are the same as
|
269
|
+
# those for element_data.
|
270
|
+
#
|
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
|
273
|
+
# treated like a hash, with the otherwise-undefined element names being the
|
274
|
+
# keys of the hash.
|
275
|
+
#
|
276
|
+
def default_element(*args, **params)
|
277
|
+
@default_element = element_data(*args, **params)
|
278
|
+
end
|
279
|
+
|
280
|
+
#
|
281
|
+
# Processes the definition of an element.
|
282
|
+
#
|
283
|
+
# @param type The expected type of the element value. This may be:
|
284
|
+
#
|
285
|
+
# * A class.
|
286
|
+
#
|
287
|
+
# * The value +:boolean+, indicating that a boolean is acceptable.
|
288
|
+
#
|
289
|
+
# * An array containing a single element being a class, signifying that the
|
290
|
+
# expected type is an array of elements matching that class.
|
291
|
+
#
|
292
|
+
# * A hash containing a single +Class1 => Class2+ pair, signifying that the
|
293
|
+
# expected type is a hash of key-value pairs matching the indicated
|
294
|
+
# classes. If Class2 is a Structured class, then Class2 objects will have
|
295
|
+
# their Structured#receive_key method called, with the corresponding
|
296
|
+
# Class1 object as the argument.
|
297
|
+
#
|
298
|
+
# @param optional Whether the element is optional. Set to :omit to omit it
|
299
|
+
# from templates.
|
300
|
+
#
|
301
|
+
# @param description A text description of the element.
|
302
|
+
#
|
303
|
+
# @param preproc A Proc that will be executed on the element value to
|
304
|
+
# convert it. The proc will be executed in the context of the receiving
|
305
|
+
# object.
|
306
|
+
#
|
307
|
+
# @param default A default value, entered into templates. The default value
|
308
|
+
# is also used for optional elements that are not specified in an input
|
309
|
+
# hash.
|
310
|
+
#
|
311
|
+
# @param check A mechanism for checking for the validity of an element
|
312
|
+
# value. This may be:
|
313
|
+
#
|
314
|
+
# * A Proc, in which case it should return true for valid values.
|
315
|
+
# * An Array of valid values (tested by +===+}).
|
316
|
+
# * Any other object, in which case validity is determined by whether the
|
317
|
+
# check value +===+ the element value.
|
318
|
+
#
|
319
|
+
def element_data(
|
320
|
+
type,
|
321
|
+
optional: false, description: nil,
|
322
|
+
preproc: nil, default: nil, check: nil
|
323
|
+
)
|
324
|
+
# Check the type argument
|
325
|
+
case type
|
326
|
+
when Class, :boolean
|
327
|
+
when Array
|
328
|
+
unless type.count == 1 && type.first.is_a?(Class)
|
329
|
+
raise TypeError, "Invalid Array type declaration"
|
330
|
+
end
|
331
|
+
when Hash
|
332
|
+
unless type.count == 1 && type.first.all? { |x| x.is_a?(Class) }
|
333
|
+
raise TypeError, "Invalid Hash type declaration"
|
334
|
+
end
|
335
|
+
else
|
336
|
+
raise TypeError, "Invalid type declaration #{type.inspect}"
|
337
|
+
end
|
338
|
+
|
339
|
+
if preproc
|
340
|
+
raise TypeError, "preproc must be a Proc" unless preproc.is_a?(Proc)
|
341
|
+
end
|
342
|
+
|
343
|
+
case check
|
344
|
+
when nil, Proc then check_obj = check # Pass through
|
345
|
+
when Array then check_obj = proc { |o| check.any? { |c| c === o } }
|
346
|
+
else check_obj = proc { |o| check === o }
|
347
|
+
end
|
348
|
+
|
349
|
+
return {
|
350
|
+
:type => type,
|
351
|
+
:optional => optional,
|
352
|
+
:description => description,
|
353
|
+
:preproc => preproc,
|
354
|
+
:default => default,
|
355
|
+
:check => check_obj,
|
356
|
+
}
|
357
|
+
|
358
|
+
end
|
359
|
+
|
360
|
+
#
|
361
|
+
# Iterates elements in a useful sorted order.
|
362
|
+
#
|
363
|
+
def each_element
|
364
|
+
@elements.sort_by { |e, data|
|
365
|
+
if data[:optional] == :omit
|
366
|
+
[ 3, e.to_s ]
|
367
|
+
else
|
368
|
+
[ data[:optional] ? 2 : 1, e.to_s ]
|
369
|
+
end
|
370
|
+
}.each do |e, data|
|
371
|
+
yield(e, data)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
#
|
376
|
+
# Given a hash, extracts all the elements from it and updates the object
|
377
|
+
# accordingly. This method is called automatically upon initialization of
|
378
|
+
# the Structured class.
|
379
|
+
#
|
380
|
+
# @param obj the object to update
|
381
|
+
# @param hash the data hash.
|
382
|
+
#
|
383
|
+
def build_from_hash(obj, hash)
|
384
|
+
input_err("Initializer is not a Hash") unless hash.is_a?(Hash)
|
385
|
+
hash = try_read_file(hash)
|
386
|
+
|
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)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Process unknown elements
|
410
|
+
unknown_elts = (hash.keys.map(&:to_sym) - @elements.keys)
|
411
|
+
return if unknown_elts.empty?
|
412
|
+
unless @default_element
|
413
|
+
input_err("Unexpected element(s): #{unknown_elts.join(', ')}")
|
414
|
+
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)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
#
|
433
|
+
# If the hash contains a key :read_file, then try reading a file containing
|
434
|
+
# additional keys, and return a new hash merging the two. This will not work
|
435
|
+
# recursively; the input file may not further contain a :read_file key.
|
436
|
+
#
|
437
|
+
# If the given hash and the :read_file hash contain duplicate keys, the
|
438
|
+
# given hash overrides the file values.
|
439
|
+
#
|
440
|
+
def try_read_file(hash)
|
441
|
+
file = hash['read_file'] || hash[:read_file]
|
442
|
+
return hash unless file
|
443
|
+
begin
|
444
|
+
res = YAML.load_file(file).merge(hash)
|
445
|
+
res.delete('read_file')
|
446
|
+
res.delete(:read_file)
|
447
|
+
return res
|
448
|
+
rescue
|
449
|
+
input_err("Failed to read Structured YAML input from #{file}: #$!")
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
# Deals with a nil value (either because no value was given, or because a
|
454
|
+
# preproc deleted it).
|
455
|
+
#
|
456
|
+
# * If val is non-nil, then this method returns false.
|
457
|
+
# * If val is nil and this element is non-optional, then this method raises
|
458
|
+
# 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
|
471
|
+
end
|
472
|
+
|
473
|
+
# Applies a value to an element for an object, after all processing for the
|
474
|
+
# value is done.
|
475
|
+
def apply_val(obj, elt, val)
|
476
|
+
if obj.respond_to?("receive_#{elt}")
|
477
|
+
obj.send("receive_#{elt}".to_sym, val)
|
478
|
+
else
|
479
|
+
obj.instance_variable_set("@#{elt}", val)
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
def try_run(block, obj, val, err_name)
|
484
|
+
begin
|
485
|
+
val = obj.instance_exec(val, &block)
|
486
|
+
rescue StandardError => e
|
487
|
+
input_err("#{err_name} failed: #{e.to_s}")
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
#
|
492
|
+
# Given an expected type and an item, checks that the item matches the
|
493
|
+
# expected type, and performs any necessary conversions.
|
494
|
+
#
|
495
|
+
def convert_item(item, type, parent)
|
496
|
+
case type
|
497
|
+
#
|
498
|
+
# In the when cases, the type is not just a class object
|
499
|
+
#
|
500
|
+
when :boolean
|
501
|
+
return item if item.is_a?(TrueClass) || item.is_a?(FalseClass)
|
502
|
+
input_err("#{item} is not boolean")
|
503
|
+
|
504
|
+
when Array
|
505
|
+
input_err("#{item} is not Array") unless item.is_a?(Array)
|
506
|
+
Structured.trace(Array) do
|
507
|
+
return item.map.with_index { |i, idx|
|
508
|
+
Structured.trace(idx) do
|
509
|
+
convert_item(i, type.first, parent)
|
510
|
+
end
|
511
|
+
}
|
512
|
+
end
|
513
|
+
|
514
|
+
when Hash
|
515
|
+
input_err("#{item} is not Hash") unless item.is_a?(Hash)
|
516
|
+
Structured.trace(Hash) do
|
517
|
+
return item.map { |k, v|
|
518
|
+
Structured.trace(k.to_s) do
|
519
|
+
conv_key = convert_item(k, type.first.first, parent)
|
520
|
+
conv_item = convert_item(v, type.first.last, parent)
|
521
|
+
conv_item.receive_key(conv_key) if conv_item.is_a?(Structured)
|
522
|
+
[ conv_key, conv_item ]
|
523
|
+
end
|
524
|
+
}.to_h
|
525
|
+
end
|
526
|
+
|
527
|
+
else
|
528
|
+
|
529
|
+
#
|
530
|
+
# In these cases, the type is a class object. It can't be tested with
|
531
|
+
# the === operator of a case/when.
|
532
|
+
#
|
533
|
+
# If the item can be automatically coverted to the expected type
|
534
|
+
citem = try_autoconvert(type, item)
|
535
|
+
|
536
|
+
# If the item is of the expected type, then return it
|
537
|
+
return citem if citem.is_a?(type)
|
538
|
+
|
539
|
+
# The only remaining hope for conversion is that type is Structured and
|
540
|
+
# item is a hash
|
541
|
+
return convert_structured(citem, type, parent)
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def try_autoconvert(type, item)
|
546
|
+
|
547
|
+
if type == String && item.is_a?(Symbol)
|
548
|
+
return item.to_s
|
549
|
+
end
|
550
|
+
|
551
|
+
# Special case in which strings will be converted to Regexps
|
552
|
+
if type == Regexp && item.is_a?(String)
|
553
|
+
begin
|
554
|
+
return Regexp.new(item)
|
555
|
+
rescue RegexpError
|
556
|
+
input_err("#{item} is not a valid regular expression")
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
return item
|
561
|
+
end
|
562
|
+
|
563
|
+
# Receive hash values that are to be converted to Structured objects
|
564
|
+
def convert_structured(item, type, parent)
|
565
|
+
unless item.is_a?(Hash)
|
566
|
+
input_err("#{item.inspect} not a #{type} or Structured hash")
|
567
|
+
end
|
568
|
+
|
569
|
+
unless type.include?(Structured) || type.include?(StructuredPolymorphic)
|
570
|
+
input_err("#{type} is not a Structured class")
|
571
|
+
end
|
572
|
+
return type.new(item, parent)
|
573
|
+
end
|
574
|
+
|
575
|
+
|
576
|
+
#
|
577
|
+
# Raises an InputError.
|
578
|
+
#
|
579
|
+
def input_err(text)
|
580
|
+
raise InputError, text
|
581
|
+
end
|
582
|
+
|
583
|
+
|
584
|
+
#
|
585
|
+
# Prints out documentation for this class.
|
586
|
+
#
|
587
|
+
def explain(io = STDOUT)
|
588
|
+
io.puts("Structured Class #{self}:")
|
589
|
+
if @class_description
|
590
|
+
io.puts("\n" + TextTools.line_break(@class_description, prefix: ' '))
|
591
|
+
end
|
592
|
+
io.puts
|
593
|
+
|
594
|
+
each_element do |elt, data|
|
595
|
+
io.puts(
|
596
|
+
" #{elt}: #{describe_type(data[:type])}" + \
|
597
|
+
"#{data[:optional] ? ' (optional)' : ''}"
|
598
|
+
)
|
599
|
+
if data[:description]
|
600
|
+
io.puts(TextTools.line_break(data[:description], prefix: ' '))
|
601
|
+
io.puts()
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
if @default_element
|
606
|
+
io.puts(
|
607
|
+
" All other elements: #{describe_type(@default_element[:type])}"
|
608
|
+
)
|
609
|
+
if @default_element[:description]
|
610
|
+
io.puts(TextTools.line_break(
|
611
|
+
@default_element[:description], prefix: ' '
|
612
|
+
))
|
613
|
+
end
|
614
|
+
io.puts()
|
615
|
+
end
|
616
|
+
|
617
|
+
end
|
618
|
+
|
619
|
+
#
|
620
|
+
# Provides a textual description of a type.
|
621
|
+
#
|
622
|
+
def describe_type(type)
|
623
|
+
case type
|
624
|
+
when :boolean then 'Boolean'
|
625
|
+
when Array then "Array of #{describe_type(type.first)}"
|
626
|
+
when Hash
|
627
|
+
desc1, desc2 = type.first.map { |x| describe_type(x) }
|
628
|
+
"Hash of #{desc1} => #{desc2}"
|
629
|
+
else return type.to_s
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
#
|
634
|
+
# Produces a template YAML file for this Structured object.
|
635
|
+
def template(indent: '')
|
636
|
+
res = "#{indent}# #{name}\n"
|
637
|
+
if @class_description
|
638
|
+
res << TextTools.line_break(@class_description, prefix: "#{indent}# ")
|
639
|
+
res << "\n"
|
640
|
+
end
|
641
|
+
|
642
|
+
in_opt = false
|
643
|
+
max_len = @elements.keys.map { |e| e.to_s.length }.max
|
644
|
+
|
645
|
+
each_element do |elt, data|
|
646
|
+
next if data[:optional] == :omit
|
647
|
+
if data[:optional] && !in_opt
|
648
|
+
res << "#{indent}#\n#{indent}# Optional\n"
|
649
|
+
in_opt = true
|
650
|
+
end
|
651
|
+
|
652
|
+
res << "#{indent}#{elt}:"
|
653
|
+
spacing = ' ' * (max_len - elt.to_s.length + 1)
|
654
|
+
if data[:default]
|
655
|
+
res << spacing << data[:default].inspect << "\n"
|
656
|
+
else
|
657
|
+
res << template_type(data[:type], indent, spacing)
|
658
|
+
end
|
659
|
+
end
|
660
|
+
return res
|
661
|
+
end
|
662
|
+
|
663
|
+
#
|
664
|
+
# @param type The Structured data type specification.
|
665
|
+
# @param indent The indent string before new lines.
|
666
|
+
# @param sp Spacing after the colon, if any.
|
667
|
+
def template_type(type, indent, sp = ' ')
|
668
|
+
res = ''
|
669
|
+
case type
|
670
|
+
when :boolean
|
671
|
+
res << " true/false\n"
|
672
|
+
when Class
|
673
|
+
if type == String
|
674
|
+
res << "#{sp}\"\"\n"
|
675
|
+
elsif type.include?(Structured)
|
676
|
+
res << "\n" << type.template(indent: indent + ' ')
|
677
|
+
else
|
678
|
+
res << "#{sp}# #{type}\n"
|
679
|
+
end
|
680
|
+
when Array
|
681
|
+
if type.first == String
|
682
|
+
res << "#{sp}[ \"\", ... ]\n"
|
683
|
+
else
|
684
|
+
res << "\n#{indent} -" << template_type(type.first, indent + ' ')
|
685
|
+
end
|
686
|
+
when Hash
|
687
|
+
if type.first.first == String
|
688
|
+
res << "\n#{indent} \"\":"
|
689
|
+
else
|
690
|
+
res << "\n#{indent} [#{type.first.first}]:"
|
691
|
+
end
|
692
|
+
res << template_type(type.first.last, indent + ' ')
|
693
|
+
end
|
694
|
+
return res
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
#
|
699
|
+
# Includes ClassMethods.
|
700
|
+
#
|
701
|
+
def self.included(base)
|
702
|
+
if base.is_a?(Class)
|
703
|
+
base.extend(ClassMethods)
|
704
|
+
base.reset_elements
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
#
|
709
|
+
# Enable tracing of object creation.
|
710
|
+
#
|
711
|
+
def self.trace(note)
|
712
|
+
begin
|
713
|
+
@trace_stack.push(note)
|
714
|
+
return yield
|
715
|
+
rescue InputError => e
|
716
|
+
e.structured_stack ||= @trace_stack.dup
|
717
|
+
raise e
|
718
|
+
ensure
|
719
|
+
@trace_stack.pop
|
720
|
+
end
|
721
|
+
end
|
722
|
+
|
723
|
+
# Stack of traced items
|
724
|
+
@trace_stack = []
|
725
|
+
|
726
|
+
end
|
727
|
+
|
728
|
+
|
729
|
+
|
730
|
+
|
731
|
+
require_relative 'structured-poly'
|
data/lib/texttools.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
module TextTools
|
2
|
+
|
3
|
+
extend TextTools
|
4
|
+
|
5
|
+
#
|
6
|
+
# Breaks a text into lines of a given length. If preserve_lines is set, then
|
7
|
+
# all line breaks are preserved; otherwise line breaks are treated as spaces.
|
8
|
+
# However, two consecutive line breaks are always preserved, treating them as
|
9
|
+
# paragraph breaks. Line breaks at the end of the text are never preserved.
|
10
|
+
#
|
11
|
+
def line_break(
|
12
|
+
text, len: 80, prefix: '', first_prefix: nil, preserve_lines: false
|
13
|
+
)
|
14
|
+
res = ''
|
15
|
+
text = text.split(/\s*\n\s*\n\s*/).map { |para|
|
16
|
+
preserve_lines ? para : para.gsub(/\s*\n\s*/, " ")
|
17
|
+
}.join("\n\n")
|
18
|
+
|
19
|
+
cur_prefix = first_prefix || prefix
|
20
|
+
strlen = len - cur_prefix.length
|
21
|
+
while text.length > strlen
|
22
|
+
if (m = /\A([^\n]{0,#{strlen}})(\s+)/.match(text))
|
23
|
+
res << cur_prefix + m[1]
|
24
|
+
res << (m[2].include?("\n") ? m[2].gsub(/[^\n]/, '') : "\n")
|
25
|
+
text = m.post_match
|
26
|
+
else
|
27
|
+
res << cur_prefix + text[0, strlen] + "\n"
|
28
|
+
text = text[strlen..-1]
|
29
|
+
end
|
30
|
+
cur_prefix = prefix
|
31
|
+
strlen = len - cur_prefix.length
|
32
|
+
end
|
33
|
+
|
34
|
+
# If there's no text left, then there were trailing spaces and the final \n
|
35
|
+
# is superfluous.
|
36
|
+
if text.length > 0
|
37
|
+
res << cur_prefix + text
|
38
|
+
else
|
39
|
+
res.rstrip!
|
40
|
+
end
|
41
|
+
|
42
|
+
return res
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
#
|
47
|
+
# Joins a list of items into a textual phrase. If there are two items, then
|
48
|
+
# +amp+ is used to join them. If there are three or more items, then +comma+
|
49
|
+
# is used for all but the last pair, for which +commaamp+ is used.
|
50
|
+
#
|
51
|
+
def text_join(list, comma: ", ", amp: " & ", commaamp: " & ")
|
52
|
+
return list unless list.is_a?(Array)
|
53
|
+
case list.count
|
54
|
+
when 0 then raise "Can't textjoin empty list"
|
55
|
+
when 1 then list.first
|
56
|
+
when 2 then list.join(amp)
|
57
|
+
else
|
58
|
+
list[0..-2].join(comma) + commaamp + list.last
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Processes simple markdown for a given text.
|
64
|
+
#
|
65
|
+
# @param i A two-element array of the starting and ending text for italicized
|
66
|
+
# content.
|
67
|
+
# @param b A two-element array of the starting and ending text for bold
|
68
|
+
# content.
|
69
|
+
#
|
70
|
+
def markdown(text, i: [ '<i>', '</i>' ], b: [ '<b>', '</b>' ])
|
71
|
+
return text.gsub(/(?<!\w)\*\*([^*]+)\*\*(?!\w)/) { |t|
|
72
|
+
"#{b.first}#$1#{b.last}"
|
73
|
+
}.gsub(/(?<!\w)\*([^*]+)\*(?!\w)/) { |t|
|
74
|
+
"#{i.first}#$1#{i.last}"
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Computes the ordinal number (using digits).
|
80
|
+
#
|
81
|
+
# @param legal Whether to use legal ordinals (2d, 3d)
|
82
|
+
#
|
83
|
+
def ordinal(num, legal: true)
|
84
|
+
case num.to_s
|
85
|
+
when /1\d\z/ then "#{num}th"
|
86
|
+
when /1\z/ then "#{num}st"
|
87
|
+
when /2\z/ then legal ? "#{num}d" : "#{num}nd"
|
88
|
+
when /3\z/ then legal ? "#{num}d" : "#{num}rd"
|
89
|
+
else "#{num}th"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cli-dispatcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.11
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Charles Duan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-11-13 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
Library for creating command-line programs that accept commands. Also
|
15
|
+
includes the Structured class for processing YAML files containing
|
16
|
+
structured data.
|
17
|
+
email: rubygems.org@cduan.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- lib/cli-dispatcher.rb
|
23
|
+
- lib/structured-poly.rb
|
24
|
+
- lib/structured.rb
|
25
|
+
- lib/texttools.rb
|
26
|
+
homepage: https://github.com/charlesduan/cli-dispatcher
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 2.6.0
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubygems_version: 3.0.3.1
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: Command-line command dispatcher
|
49
|
+
test_files: []
|