toys-core 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|