toys-core 0.9.1 → 0.9.2
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 +4 -4
- data/CHANGELOG.md +9 -1
- data/README.md +3 -3
- data/docs/guide.md +109 -14
- data/lib/toys/cli.rb +33 -28
- data/lib/toys/core.rb +1 -1
- data/lib/toys/dsl/tool.rb +4 -3
- data/lib/toys/loader.rb +177 -132
- data/lib/toys/middleware.rb +185 -3
- data/lib/toys/module_lookup.rb +28 -16
- data/lib/toys/standard_mixins/exec.rb +42 -32
- data/lib/toys/standard_mixins/gems.rb +1 -1
- data/lib/toys/standard_mixins/terminal.rb +1 -1
- data/lib/toys/tool.rb +5 -4
- data/lib/toys/utils/exec.rb +28 -23
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e7a8292d7e4fb6fc31e4f308159f50050c455055a5333d89cebe448f8a2d472
|
4
|
+
data.tar.gz: 9e96d5f69ca1d072e64f0e2b012479ed1602b73ec94315a13dd6902f9ac5c189
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8bd19a911524c3a776c947e8dfed37466c877a03f475f472c1082b7287f446e9881cbe666bee87663fcc9d966e76a6426ef9db072c07d47946d5a0a9cd2f7ce
|
7
|
+
data.tar.gz: 909d606e1bb8864264ee25e175b9422b5754071788bd2dcb590d617dd00aa7041f30a788380717a18d3242230939feff69e7a167d5b45096e22d29d6ae613f2f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Release History
|
2
2
|
|
3
|
+
### 0.9.2 / 2020-01-03
|
4
|
+
|
5
|
+
* IMPROVED: Mixins can now take real keyword arguments, and will pass them on properly to `on_initialize` and `on_include` blocks.
|
6
|
+
* CHANGED: `Toys::Utils::Exec` and the `:exec` mixin methods now take real keyword arguments rather than an `opts` hash. This means you should use keywords (or the double-splat operator) to avoid a deprecation warning on Ruby 2.7.
|
7
|
+
* IMPROVED: `Toys::CLI#clone` can be passed keyword arguments to modify the configuration.
|
8
|
+
* IMPROVED: `Toys::Loader` is now thread-safe. This means it is now possible for a single `Toys::CLI` to run multiple tools in different threads.
|
9
|
+
* IMPROVED: There is now a class for middleware specs, making possible a nicer syntax for building a middleware stack.
|
10
|
+
|
3
11
|
### 0.9.1 / 2019-12-22
|
4
12
|
|
5
13
|
* IMPROVED: `delegate_to` and `alias_tool` can take symbols as well as strings.
|
@@ -15,7 +23,7 @@ Functional changes:
|
|
15
23
|
* IMPROVED: `alias_tool` is now just shorthand for delegating. This means, aliases can now point to namespaces and will resolve subtools of their targets, and they now support tab completion and online help.
|
16
24
|
* IMPROVED: This release of Toys is now compatible with Ruby 2.7.0-preview3. It fixes some Ruby 2.7 specific bugs, and sanitizes keyword argument usage to eliminate Ruby 2.7 warnings.
|
17
25
|
* IMPROVED: JRuby is now supported for most operations. However, JRuby is generally not recommended because of JVM boot latency, lack of Kernel#fork support, and other issues.
|
18
|
-
* FIXED: The
|
26
|
+
* FIXED: The `tool` directive no longer crashes if no block is provided.
|
19
27
|
|
20
28
|
Internal interface changes:
|
21
29
|
|
data/README.md
CHANGED
@@ -64,8 +64,8 @@ happens when you run it:
|
|
64
64
|
Just as with Toys itself, you get a help screen by default (since we haven't
|
65
65
|
yet actually implemented any behavior.) As you can see, some of the same
|
66
66
|
features from Toys are present already: online help, and `--verbose` and
|
67
|
-
`--quiet` flags. These features can of course all be customized
|
68
|
-
useful to have to start off.
|
67
|
+
`--quiet` flags. These features can of course all be customized or disabled,
|
68
|
+
but they're often useful to have to start off.
|
69
69
|
|
70
70
|
### Add some functionality
|
71
71
|
|
@@ -169,7 +169,7 @@ modifies error handling and delimiter parsing.
|
|
169
169
|
#### Pass some additional options to the CLI constructor ...
|
170
170
|
cli = Toys::CLI.new(
|
171
171
|
extra_delimiters: ":",
|
172
|
-
error_handler: ->(
|
172
|
+
error_handler: ->(_err) {
|
173
173
|
puts "Dude, an error happened..."
|
174
174
|
return 1
|
175
175
|
}
|
data/docs/guide.md
CHANGED
@@ -5,25 +5,33 @@
|
|
5
5
|
Toys-Core is the command line framework underlying Toys. It implements most of
|
6
6
|
the core functionality of Toys, including the tool DSL, argument parsing,
|
7
7
|
loading Toys files, online help, subprocess control, and so forth. It can be
|
8
|
-
used to create custom command line executables using the same facilities.
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
used to create custom command line executables using the same facilities.
|
9
|
+
|
10
|
+
If this is your first time using Toys-Core, we recommend starting with the
|
11
|
+
[README](https://dazuma.github.io/toys/gems/toys-core/latest), which includes a
|
12
|
+
tutorial that introduces how to create simple command line executables using
|
13
|
+
Toys-Core, customize the behavior, and package your executable in a gem. You
|
14
|
+
should also be familiar with Toys itself, including how to define tools by
|
15
|
+
writing Toys files, how to interpret arguments and flags, and how to use the
|
16
|
+
Toys execution environment. For background, please see the
|
17
|
+
[Toys README](https://dazuma.github.io/toys/gems/toys/latest) and
|
17
18
|
[Toys User's Guide](https://dazuma.github.io/toys/gems/toys/latest/file.guide.html).
|
19
|
+
Together, those resources will likely give you enough information to begin
|
20
|
+
creating your own basic command line executables.
|
21
|
+
|
22
|
+
This user's guide covers all the features of Toys-Core in much more depth. Read
|
23
|
+
it when you're ready to unlock all the capabilities of Toys-Core to create
|
24
|
+
sophisticated command line tools.
|
18
25
|
|
19
26
|
**(This user's guide is still under construction.)**
|
20
27
|
|
21
28
|
## Conceptual overview
|
22
29
|
|
23
30
|
Toys-Core is a command line *framework* in the traditional sense. It is
|
24
|
-
intended to be used to write custom command line executables in Ruby.
|
25
|
-
provides
|
26
|
-
|
31
|
+
intended to be used to write custom command line executables in Ruby. The
|
32
|
+
framework provides common facilities such as argumentparsing and online help,
|
33
|
+
while your executable chooses and configures those facilities, and implements
|
34
|
+
the actual behavior.
|
27
35
|
|
28
36
|
The entry point for Toys-Core is the **cli object**. Typically your executable
|
29
37
|
script instantiates a CLI, configures it with the desired tool implementations,
|
@@ -47,10 +55,97 @@ Finally, an executable may customize many aspects of its behavior, such as the
|
|
47
55
|
|
48
56
|
## Using the CLI object
|
49
57
|
|
50
|
-
|
58
|
+
The CLI object is the main entry point for Toys-Core. Most command line
|
59
|
+
executables based on Toys-Core use it as follows:
|
60
|
+
|
61
|
+
* Instantiate a CLI object, passing configuration parameters to the
|
62
|
+
constructor.
|
63
|
+
* Define the functionality of the CLI, either inline by passing it blocks, or
|
64
|
+
by providing paths to tool files.
|
65
|
+
* Call the {Toys::CLI#run} method, passing it the command line arguments
|
66
|
+
(e.g. from `ARGV`).
|
67
|
+
* Handle the result code, normally by passing it to `Kernel#exit`.
|
68
|
+
|
69
|
+
Following is a simple "hello world" example using the CLI:
|
70
|
+
|
71
|
+
#!/usr/bin/env ruby
|
72
|
+
|
73
|
+
require "toys-core"
|
74
|
+
|
75
|
+
# Instantiate a CLI with the default options
|
76
|
+
cli = Toys::CLI.new
|
77
|
+
|
78
|
+
# Define the functionality
|
79
|
+
cli.add_config_block do
|
80
|
+
desc "My first executable!"
|
81
|
+
flag :whom, default: "world"
|
82
|
+
def run
|
83
|
+
puts "Hello, #{whom}!"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Run the CLI, passing the command line arguments
|
88
|
+
result = cli.run(*ARGV)
|
89
|
+
|
90
|
+
# Handle the result code.
|
91
|
+
exit(result)
|
92
|
+
|
93
|
+
### CLI execution
|
94
|
+
|
95
|
+
This section provides some detail on how a CLI executes your code.
|
96
|
+
|
97
|
+
(TODO)
|
98
|
+
|
99
|
+
### Configuring the CLI
|
100
|
+
|
101
|
+
Generally, you control CLI features by passing arguments to its constructor.
|
102
|
+
These features include:
|
51
103
|
|
52
|
-
|
104
|
+
* How to find toys files and related code and data. See the section on
|
105
|
+
[writing tool files](#Writing_tool_files).
|
106
|
+
* Middleware, providing common behavior for all tools. See the section on
|
107
|
+
[customizing the middleware stack](#Customizing_default_behavior).
|
108
|
+
* Common mixins and templates available to all tools. See the section on
|
109
|
+
[how to define mixins and templates](#Defining_mixins_and_templates).
|
110
|
+
* How logs and errors are reported. See the section on
|
111
|
+
[customizing tool output](#Customizing_tool_output).
|
112
|
+
* How the executable interacts with the shell, including setting up tab
|
113
|
+
completion. See the
|
114
|
+
[corresponding section](#Shell_and_command_line_integration).
|
115
|
+
|
116
|
+
Each of the actual parameters is covered in detail in the documentation for
|
117
|
+
{Toys::CLI#initialize}. The configuration of a CLI cannot be changed once the
|
118
|
+
CLI is constructed. If you need to a CLI with a modified configuration, use
|
119
|
+
{Toys::CLI#child}.
|
120
|
+
|
121
|
+
## Defining functionality
|
122
|
+
|
123
|
+
### Writing tools in blocks
|
124
|
+
|
125
|
+
### Writing tool files
|
126
|
+
|
127
|
+
### Tool priority
|
128
|
+
|
129
|
+
### Defining mixins and templates
|
130
|
+
|
131
|
+
## Customizing tool output
|
132
|
+
|
133
|
+
### Logging and verbosity
|
134
|
+
|
135
|
+
### Handling errors
|
53
136
|
|
54
137
|
## Customizing default behavior
|
55
138
|
|
139
|
+
### Introducing middleware
|
140
|
+
|
141
|
+
### Built-in middlewares
|
142
|
+
|
143
|
+
### Writing your own middleware
|
144
|
+
|
145
|
+
## Shell and command line integration
|
146
|
+
|
147
|
+
### Interpreting tool names
|
148
|
+
|
149
|
+
### Tab completion
|
150
|
+
|
56
151
|
## Packaging your executable
|
data/lib/toys/cli.rb
CHANGED
@@ -117,8 +117,8 @@ module Toys
|
|
117
117
|
# {Toys::CLI::DefaultCompletion}, which delegates completion to the
|
118
118
|
# relevant tool.
|
119
119
|
#
|
120
|
-
# @param middleware_stack [Array] An array of
|
121
|
-
# by default for all tools
|
120
|
+
# @param middleware_stack [Array<Toys::Middleware::Spec>] An array of
|
121
|
+
# middleware that will be used by default for all tools.
|
122
122
|
# Optional. If not provided, uses a default set of middleware defined
|
123
123
|
# in {Toys::CLI.default_middleware_stack}. To include no middleware,
|
124
124
|
# pass the empty array explicitly.
|
@@ -215,32 +215,37 @@ module Toys
|
|
215
215
|
end
|
216
216
|
|
217
217
|
##
|
218
|
-
# Make a clone with the same settings but no
|
219
|
-
# This is sometimes useful for
|
220
|
-
# from a different configuration.
|
218
|
+
# Make a clone with the same settings but no no config blocks and no paths
|
219
|
+
# in the loader. This is sometimes useful for calling another tool that has
|
220
|
+
# to be loaded from a different configuration.
|
221
221
|
#
|
222
|
-
# @param
|
222
|
+
# @param opts [keywords] Any configuration arguments that should be
|
223
|
+
# modified from the original. See {#initialize} for a list of
|
224
|
+
# recognized keywords.
|
223
225
|
# @return [Toys::CLI]
|
224
226
|
# @yieldparam cli [Toys::CLI] If you pass a block, the new CLI is yielded
|
225
227
|
# to it so you can add paths and make other modifications.
|
226
228
|
#
|
227
|
-
def child(**
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
229
|
+
def child(**opts)
|
230
|
+
args = {
|
231
|
+
executable_name: @executable_name,
|
232
|
+
config_dir_name: @config_dir_name,
|
233
|
+
config_file_name: @config_file_name,
|
234
|
+
index_file_name: @index_file_name,
|
235
|
+
preload_dir_name: @preload_dir_name,
|
236
|
+
preload_file_name: @preload_file_name,
|
237
|
+
data_dir_name: @data_dir_name,
|
238
|
+
middleware_stack: @middleware_stack,
|
239
|
+
extra_delimiters: @extra_delimiters,
|
240
|
+
mixin_lookup: @mixin_lookup,
|
241
|
+
middleware_lookup: @middleware_lookup,
|
242
|
+
template_lookup: @template_lookup,
|
243
|
+
logger: @logger,
|
244
|
+
base_level: @base_level,
|
245
|
+
error_handler: @error_handler,
|
246
|
+
completion: @completion,
|
247
|
+
}.merge(opts)
|
248
|
+
cli = CLI.new(**args)
|
244
249
|
yield cli if block_given?
|
245
250
|
cli
|
246
251
|
end
|
@@ -600,14 +605,14 @@ module Toys
|
|
600
605
|
# * {Toys::StandardMiddleware::AddVerbosityFlags} adding the `--verbose`
|
601
606
|
# and `--quiet` flags for managing the logger level.
|
602
607
|
#
|
603
|
-
# @return [Array<Toys::Middleware>]
|
608
|
+
# @return [Array<Toys::Middleware::Spec>]
|
604
609
|
#
|
605
610
|
def default_middleware_stack
|
606
611
|
[
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
612
|
+
Middleware.spec(:set_default_descriptions),
|
613
|
+
Middleware.spec(:show_help, help_flags: true, fallback_execution: true),
|
614
|
+
Middleware.spec(:handle_usage_errors),
|
615
|
+
Middleware.spec(:add_verbosity_flags),
|
611
616
|
]
|
612
617
|
end
|
613
618
|
|
data/lib/toys/core.rb
CHANGED
data/lib/toys/dsl/tool.rb
CHANGED
@@ -1464,9 +1464,10 @@ module Toys
|
|
1464
1464
|
#
|
1465
1465
|
# @param mod [Module,Symbol,String] Module or module name.
|
1466
1466
|
# @param args [Object...] Arguments to pass to the initializer
|
1467
|
+
# @param kwargs [keywords] Keyword arguments to pass to the initializer
|
1467
1468
|
# @return [self]
|
1468
1469
|
#
|
1469
|
-
def include(mod, *args)
|
1470
|
+
def include(mod, *args, **kwargs)
|
1470
1471
|
cur_tool = DSL::Tool.current_tool(self, true)
|
1471
1472
|
return self if cur_tool.nil?
|
1472
1473
|
mod = DSL::Tool.resolve_mixin(mod, cur_tool, @__loader)
|
@@ -1477,11 +1478,11 @@ module Toys
|
|
1477
1478
|
super(mod)
|
1478
1479
|
if mod.respond_to?(:initializer)
|
1479
1480
|
callback = mod.initializer
|
1480
|
-
cur_tool.add_initializer(callback, *args) if callback
|
1481
|
+
cur_tool.add_initializer(callback, *args, **kwargs) if callback
|
1481
1482
|
end
|
1482
1483
|
if mod.respond_to?(:inclusion)
|
1483
1484
|
callback = mod.inclusion
|
1484
|
-
class_exec(*args, &callback) if callback
|
1485
|
+
class_exec(*args, **kwargs, &callback) if callback
|
1485
1486
|
end
|
1486
1487
|
self
|
1487
1488
|
end
|
data/lib/toys/loader.rb
CHANGED
@@ -21,6 +21,8 @@
|
|
21
21
|
# IN THE SOFTWARE.
|
22
22
|
;
|
23
23
|
|
24
|
+
require "monitor"
|
25
|
+
|
24
26
|
module Toys
|
25
27
|
##
|
26
28
|
# The Loader service loads tools from configuration files, and finds the
|
@@ -29,18 +31,7 @@ module Toys
|
|
29
31
|
# This class is not thread-safe.
|
30
32
|
#
|
31
33
|
class Loader
|
32
|
-
|
33
|
-
ToolData = ::Struct.new(:definitions, :top_priority, :active_priority) do
|
34
|
-
## @private
|
35
|
-
def top_definition
|
36
|
-
top_priority ? definitions[top_priority] : nil
|
37
|
-
end
|
38
|
-
|
39
|
-
## @private
|
40
|
-
def active_definition
|
41
|
-
active_priority ? definitions[active_priority] : nil
|
42
|
-
end
|
43
|
-
end
|
34
|
+
include ::MonitorMixin
|
44
35
|
|
45
36
|
##
|
46
37
|
# Create a Loader
|
@@ -59,8 +50,9 @@ module Toys
|
|
59
50
|
# @param data_dir_name [String,nil] A directory with this name that appears
|
60
51
|
# in any configuration directory is added to the data directory search
|
61
52
|
# path for any tool file in that directory.
|
62
|
-
# @param middleware_stack [Array] An array of
|
63
|
-
# by default for all tools loaded by this
|
53
|
+
# @param middleware_stack [Array<Toys::Middleware::Spec>] An array of
|
54
|
+
# middleware that will be used by default for all tools loaded by this
|
55
|
+
# loader.
|
64
56
|
# @param extra_delimiters [String] A string containing characters that can
|
65
57
|
# function as delimiters in a tool name. Defaults to empty. Allowed
|
66
58
|
# characters are period, colon, and slash.
|
@@ -74,21 +66,23 @@ module Toys
|
|
74
66
|
def initialize(index_file_name: nil, preload_dir_name: nil, preload_file_name: nil,
|
75
67
|
data_dir_name: nil, middleware_stack: [], extra_delimiters: "",
|
76
68
|
mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil)
|
69
|
+
super()
|
77
70
|
if index_file_name && ::File.extname(index_file_name) != ".rb"
|
78
71
|
raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
|
79
72
|
end
|
80
73
|
@mixin_lookup = mixin_lookup || ModuleLookup.new
|
81
|
-
@middleware_lookup = middleware_lookup || ModuleLookup.new
|
82
74
|
@template_lookup = template_lookup || ModuleLookup.new
|
75
|
+
@middleware_lookup = middleware_lookup || ModuleLookup.new
|
83
76
|
@index_file_name = index_file_name
|
84
77
|
@preload_file_name = preload_file_name
|
85
78
|
@preload_dir_name = preload_dir_name
|
86
79
|
@data_dir_name = data_dir_name
|
87
|
-
@
|
80
|
+
@loading_started = false
|
88
81
|
@worklist = []
|
89
82
|
@tool_data = {}
|
90
83
|
@max_priority = @min_priority = 0
|
91
|
-
@
|
84
|
+
@middleware_stack = Middleware.resolve_specs(*middleware_stack)
|
85
|
+
@delimiter_handler = DelimiterHandler.new(extra_delimiters)
|
92
86
|
get_tool([], -999_999)
|
93
87
|
end
|
94
88
|
|
@@ -103,10 +97,13 @@ module Toys
|
|
103
97
|
#
|
104
98
|
def add_path(paths, high_priority: false)
|
105
99
|
paths = Array(paths)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
100
|
+
synchronize do
|
101
|
+
raise "Cannot add a path after tool loading has started" if @loading_started
|
102
|
+
priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
|
103
|
+
paths.each do |path|
|
104
|
+
source = SourceInfo.create_path_root(path)
|
105
|
+
@worklist << [source, [], priority]
|
106
|
+
end
|
110
107
|
end
|
111
108
|
self
|
112
109
|
end
|
@@ -126,9 +123,12 @@ module Toys
|
|
126
123
|
#
|
127
124
|
def add_block(high_priority: false, name: nil, &block)
|
128
125
|
name ||= "(Code block #{block.object_id})"
|
129
|
-
|
130
|
-
|
131
|
-
|
126
|
+
synchronize do
|
127
|
+
raise "Cannot add a block after tool loading has started" if @loading_started
|
128
|
+
priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
|
129
|
+
source = SourceInfo.create_proc_root(block, name)
|
130
|
+
@worklist << [source, [], priority]
|
131
|
+
end
|
132
132
|
self
|
133
133
|
end
|
134
134
|
|
@@ -146,7 +146,7 @@ module Toys
|
|
146
146
|
# @return [Array(Toys::Tool,Array<String>)]
|
147
147
|
#
|
148
148
|
def lookup(args)
|
149
|
-
orig_prefix, args = find_orig_prefix(args)
|
149
|
+
orig_prefix, args = @delimiter_handler.find_orig_prefix(args)
|
150
150
|
prefix = orig_prefix
|
151
151
|
loop do
|
152
152
|
tool = lookup_specific(prefix)
|
@@ -168,10 +168,9 @@ module Toys
|
|
168
168
|
# @return [nil] if no such tool exists
|
169
169
|
#
|
170
170
|
def lookup_specific(words)
|
171
|
-
words = split_path(words.first) if words.size == 1
|
171
|
+
words = @delimiter_handler.split_path(words.first) if words.size == 1
|
172
172
|
load_for_prefix(words)
|
173
|
-
|
174
|
-
tool = tool_data.active_definition || tool_data.top_definition
|
173
|
+
tool = get_tool_data(words).cur_definition
|
175
174
|
finish_definitions_in_tree(words) if tool
|
176
175
|
tool
|
177
176
|
end
|
@@ -191,14 +190,14 @@ module Toys
|
|
191
190
|
load_for_prefix(words)
|
192
191
|
found_tools = []
|
193
192
|
len = words.length
|
194
|
-
|
193
|
+
tool_data_snapshot.each do |n, td|
|
195
194
|
next if n.empty?
|
196
195
|
if recursive
|
197
196
|
next if n.length <= len || n.slice(0, len) != words
|
198
197
|
else
|
199
198
|
next unless n.slice(0..-2) == words
|
200
199
|
end
|
201
|
-
tool = td.
|
200
|
+
tool = td.cur_definition
|
202
201
|
found_tools << tool unless tool.nil?
|
203
202
|
end
|
204
203
|
sort_tools_by_name(found_tools)
|
@@ -215,8 +214,8 @@ module Toys
|
|
215
214
|
def has_subtools?(words) # rubocop:disable Naming/PredicateName
|
216
215
|
load_for_prefix(words)
|
217
216
|
len = words.length
|
218
|
-
|
219
|
-
if !n.empty? && n.length > len && n.slice(0, len) == words && !td.
|
217
|
+
tool_data_snapshot.each do |n, td|
|
218
|
+
if !n.empty? && n.length > len && n.slice(0, len) == words && !td.empty?
|
220
219
|
return true
|
221
220
|
end
|
222
221
|
end
|
@@ -233,8 +232,18 @@ module Toys
|
|
233
232
|
#
|
234
233
|
def split_path(str)
|
235
234
|
return str.map(&:to_s) if str.is_a?(::Array)
|
236
|
-
str
|
237
|
-
|
235
|
+
@delimiter_handler.split_path(str.to_s)
|
236
|
+
end
|
237
|
+
|
238
|
+
##
|
239
|
+
# Get or create the tool definition for the given name and priority.
|
240
|
+
#
|
241
|
+
# @return [Toys::Tool]
|
242
|
+
#
|
243
|
+
# @private
|
244
|
+
#
|
245
|
+
def get_tool(words, priority)
|
246
|
+
get_tool_data(words).get_tool(priority, self)
|
238
247
|
end
|
239
248
|
|
240
249
|
##
|
@@ -253,11 +262,7 @@ module Toys
|
|
253
262
|
# @private
|
254
263
|
#
|
255
264
|
def activate_tool(words, priority)
|
256
|
-
|
257
|
-
return tool_data.active_definition if tool_data.active_priority == priority
|
258
|
-
return nil if tool_data.active_priority && tool_data.active_priority > priority
|
259
|
-
tool_data.active_priority = priority
|
260
|
-
get_tool(words, priority)
|
265
|
+
get_tool_data(words).activate_tool(priority, self)
|
261
266
|
end
|
262
267
|
|
263
268
|
##
|
@@ -274,44 +279,45 @@ module Toys
|
|
274
279
|
end
|
275
280
|
|
276
281
|
##
|
277
|
-
#
|
282
|
+
# Build a new tool.
|
283
|
+
# Called only from ToolData.
|
278
284
|
#
|
279
|
-
# @param
|
280
|
-
# @
|
285
|
+
# @param words [Array<String>] The name of the tool.
|
286
|
+
# @param priority [Integer] The priority of the request.
|
287
|
+
#
|
288
|
+
# @return [Toys::Tool] A new tool object.
|
281
289
|
#
|
282
290
|
# @private
|
283
291
|
#
|
284
|
-
def
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
remaining_words = calc_remaining_words(prefix, words)
|
289
|
-
if source.source_proc
|
290
|
-
load_proc(source, words, remaining_words, priority)
|
291
|
-
elsif source.source_path
|
292
|
-
load_validated_path(source, words, remaining_words, priority)
|
293
|
-
end
|
294
|
-
end
|
295
|
-
self
|
292
|
+
def build_tool(words, priority)
|
293
|
+
parent = words.empty? ? nil : get_tool(words.slice(0..-2), priority)
|
294
|
+
built_middleware_stack = @middleware_stack.map { |m| m.build(@middleware_lookup) }
|
295
|
+
Tool.new(self, parent, words, priority, built_middleware_stack)
|
296
296
|
end
|
297
297
|
|
298
298
|
##
|
299
|
-
#
|
299
|
+
# Loads the subtree under the given prefix.
|
300
300
|
#
|
301
|
-
# @
|
301
|
+
# @param prefix [Array<String>] The name prefix.
|
302
|
+
# @return [self]
|
302
303
|
#
|
303
304
|
# @private
|
304
305
|
#
|
305
|
-
def
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
306
|
+
def load_for_prefix(prefix)
|
307
|
+
synchronize do
|
308
|
+
@loading_started = true
|
309
|
+
cur_worklist = @worklist
|
310
|
+
@worklist = []
|
311
|
+
cur_worklist.each do |source, words, priority|
|
312
|
+
remaining_words = calc_remaining_words(prefix, words)
|
313
|
+
if source.source_proc
|
314
|
+
load_proc(source, words, remaining_words, priority)
|
315
|
+
elsif source.source_path
|
316
|
+
load_validated_path(source, words, remaining_words, priority)
|
317
|
+
end
|
318
|
+
end
|
314
319
|
end
|
320
|
+
self
|
315
321
|
end
|
316
322
|
|
317
323
|
##
|
@@ -344,16 +350,28 @@ module Toys
|
|
344
350
|
# Load configuration from the given path. This is called from the `load`
|
345
351
|
# directive in the DSL.
|
346
352
|
#
|
353
|
+
# @param parent_source [Toys::SourceInfo] The source of the caller.
|
354
|
+
# @param path [String] The file or directory to load.
|
355
|
+
# @param words [Array<String>] The name of the caller, i.e. the context in
|
356
|
+
# which to load.
|
357
|
+
# @param remaining_words [Array<String>] The remaining words.
|
358
|
+
# @param priority [Integer] The priority.
|
359
|
+
#
|
347
360
|
# @private
|
348
361
|
#
|
349
362
|
def load_path(parent_source, path, words, remaining_words, priority)
|
350
363
|
source = parent_source.absolute_child(path)
|
351
|
-
|
364
|
+
synchronize do
|
365
|
+
load_validated_path(source, words, remaining_words, priority)
|
366
|
+
end
|
352
367
|
end
|
353
368
|
|
354
369
|
##
|
355
370
|
# Determine the next setting for remaining_words, given a word.
|
356
371
|
#
|
372
|
+
# @param remaining_words [Array<String>] The remaining words.
|
373
|
+
# @param word [String] The next word to parse.
|
374
|
+
#
|
357
375
|
# @private
|
358
376
|
#
|
359
377
|
def self.next_remaining_words(remaining_words, word)
|
@@ -368,72 +386,12 @@ module Toys
|
|
368
386
|
|
369
387
|
private
|
370
388
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
def process_extra_delimiters(input)
|
375
|
-
unless ALLOWED_DELIMITERS =~ input
|
376
|
-
raise ::ArgumentError, "Illegal delimiters in #{input.inspect}"
|
377
|
-
end
|
378
|
-
chars = ::Regexp.escape(input.chars.uniq.join)
|
379
|
-
chars.empty? ? nil : ::Regexp.new("[#{chars}]")
|
380
|
-
end
|
381
|
-
|
382
|
-
def find_orig_prefix(args)
|
383
|
-
if @extra_delimiters
|
384
|
-
first_split = (args.first || "").split(@extra_delimiters)
|
385
|
-
if first_split.size > 1
|
386
|
-
args = first_split + args.slice(1..-1)
|
387
|
-
return [first_split, args]
|
388
|
-
end
|
389
|
-
end
|
390
|
-
orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
|
391
|
-
[orig_prefix, args]
|
389
|
+
def tool_data_snapshot
|
390
|
+
synchronize { @tool_data.dup }
|
392
391
|
end
|
393
392
|
|
394
393
|
def get_tool_data(words)
|
395
|
-
@tool_data[words] ||= ToolData.new(
|
396
|
-
end
|
397
|
-
|
398
|
-
def resolve_middleware(input)
|
399
|
-
input = Array(input).dup
|
400
|
-
middleware = input.shift
|
401
|
-
if middleware.is_a?(::String) || middleware.is_a?(::Symbol)
|
402
|
-
middleware = @middleware_lookup.lookup(middleware)
|
403
|
-
if middleware.nil?
|
404
|
-
raise ::ArgumentError, "Unknown middleware name #{input.first.inspect}"
|
405
|
-
end
|
406
|
-
end
|
407
|
-
if middleware.is_a?(::Class)
|
408
|
-
middleware = build_middleware(middleware, input)
|
409
|
-
end
|
410
|
-
unless input.empty?
|
411
|
-
raise ::ArgumentError, "Unrecognized middleware arguments: #{input.inspect}"
|
412
|
-
end
|
413
|
-
middleware
|
414
|
-
end
|
415
|
-
|
416
|
-
def build_middleware(middleware_class, input)
|
417
|
-
args = input.first
|
418
|
-
if args.is_a?(::Array)
|
419
|
-
input.shift
|
420
|
-
else
|
421
|
-
args = []
|
422
|
-
end
|
423
|
-
kwargs = input.first
|
424
|
-
if kwargs.is_a?(::Hash)
|
425
|
-
input.shift
|
426
|
-
else
|
427
|
-
kwargs = {}
|
428
|
-
end
|
429
|
-
# Due to a bug in Ruby < 2.7, passing an empty **kwargs splat to
|
430
|
-
# initialize will fail if there are no formal keyword args.
|
431
|
-
formals = middleware_class.instance_method(:initialize).parameters
|
432
|
-
if kwargs.empty? && formals.all? { |(type, _name)| type != :key && type != :keyrest }
|
433
|
-
middleware_class.new(*args)
|
434
|
-
else
|
435
|
-
middleware_class.new(*args, **kwargs)
|
436
|
-
end
|
394
|
+
synchronize { @tool_data[words] ||= ToolData.new(words) }
|
437
395
|
end
|
438
396
|
|
439
397
|
##
|
@@ -443,10 +401,9 @@ module Toys
|
|
443
401
|
def finish_definitions_in_tree(words)
|
444
402
|
load_for_prefix(words)
|
445
403
|
len = words.length
|
446
|
-
|
404
|
+
tool_data_snapshot.each do |n, td|
|
447
405
|
next if n.length < len || n.slice(0, len) != words
|
448
|
-
|
449
|
-
tool.finish_definition(self) if tool.is_a?(Tool)
|
406
|
+
td.cur_definition&.finish_definition(self)
|
450
407
|
end
|
451
408
|
end
|
452
409
|
|
@@ -555,5 +512,93 @@ module Toys
|
|
555
512
|
index += 1
|
556
513
|
end
|
557
514
|
end
|
515
|
+
|
516
|
+
##
|
517
|
+
# Tool data
|
518
|
+
#
|
519
|
+
# @private
|
520
|
+
#
|
521
|
+
class ToolData
|
522
|
+
## @private
|
523
|
+
def initialize(words)
|
524
|
+
@words = words
|
525
|
+
@definitions = {}
|
526
|
+
@top_priority = @active_priority = nil
|
527
|
+
end
|
528
|
+
|
529
|
+
## @private
|
530
|
+
def cur_definition
|
531
|
+
active_definition || top_definition
|
532
|
+
end
|
533
|
+
|
534
|
+
## @private
|
535
|
+
def empty?
|
536
|
+
@definitions.empty?
|
537
|
+
end
|
538
|
+
|
539
|
+
## @private
|
540
|
+
def get_tool(priority, loader)
|
541
|
+
if @top_priority.nil? || @top_priority < priority
|
542
|
+
@top_priority = priority
|
543
|
+
end
|
544
|
+
@definitions[priority] ||= loader.build_tool(@words, priority)
|
545
|
+
end
|
546
|
+
|
547
|
+
## @private
|
548
|
+
def activate_tool(priority, loader)
|
549
|
+
return active_definition if @active_priority == priority
|
550
|
+
return nil if @active_priority && @active_priority > priority
|
551
|
+
@active_priority = priority
|
552
|
+
get_tool(priority, loader)
|
553
|
+
end
|
554
|
+
|
555
|
+
private
|
556
|
+
|
557
|
+
def top_definition
|
558
|
+
@top_priority ? @definitions[@top_priority] : nil
|
559
|
+
end
|
560
|
+
|
561
|
+
def active_definition
|
562
|
+
@active_priority ? @definitions[@active_priority] : nil
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
##
|
567
|
+
# An object that handles name delimiting.
|
568
|
+
#
|
569
|
+
# @private
|
570
|
+
#
|
571
|
+
class DelimiterHandler
|
572
|
+
## @private
|
573
|
+
ALLOWED_DELIMITERS = %r{^[\./:]*$}.freeze
|
574
|
+
private_constant :ALLOWED_DELIMITERS
|
575
|
+
|
576
|
+
## @private
|
577
|
+
def initialize(extra_delimiters)
|
578
|
+
unless ALLOWED_DELIMITERS =~ extra_delimiters
|
579
|
+
raise ::ArgumentError, "Illegal delimiters in #{extra_delimiters.inspect}"
|
580
|
+
end
|
581
|
+
chars = ::Regexp.escape(extra_delimiters.chars.uniq.join)
|
582
|
+
@extra_delimiters = chars.empty? ? nil : ::Regexp.new("[#{chars}]")
|
583
|
+
end
|
584
|
+
|
585
|
+
## @private
|
586
|
+
def split_path(str)
|
587
|
+
@extra_delimiters ? str.split(@extra_delimiters) : [str]
|
588
|
+
end
|
589
|
+
|
590
|
+
## @private
|
591
|
+
def find_orig_prefix(args)
|
592
|
+
if @extra_delimiters
|
593
|
+
first_split = (args.first || "").split(@extra_delimiters)
|
594
|
+
if first_split.size > 1
|
595
|
+
args = first_split + args.slice(1..-1)
|
596
|
+
return [first_split, args]
|
597
|
+
end
|
598
|
+
end
|
599
|
+
orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
|
600
|
+
[orig_prefix, args]
|
601
|
+
end
|
602
|
+
end
|
558
603
|
end
|
559
604
|
end
|