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