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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd26e85c431cf630dda5d6c7f49f43a13b917880169d47ca8d2df84df94a4b90
4
- data.tar.gz: 87338976eafafd543f1b66c83c3c675739bddcffe1db8a2c595b3353c558d716
3
+ metadata.gz: 3e7a8292d7e4fb6fc31e4f308159f50050c455055a5333d89cebe448f8a2d472
4
+ data.tar.gz: 9e96d5f69ca1d072e64f0e2b012479ed1602b73ec94315a13dd6902f9ac5c189
5
5
  SHA512:
6
- metadata.gz: 3d6e67e2800287709a3a09ddff87145999e4712db5a69b96ae479c58a586ab14596e42ad52ce8c9aca08146163ff6113318d70f426c08e8e18f6f2842b679f8e
7
- data.tar.gz: e45bda72586ad53fa0b3ed51bb7b1a1b67f0da3a91a3c83344af61d616692f3bc435911cd5ab9fd8b2d086894ca03d976617213573c50c44b85454d7a0c4728b
6
+ metadata.gz: d8bd19a911524c3a776c947e8dfed37466c877a03f475f472c1082b7287f446e9881cbe666bee87663fcc9d966e76a6426ef9db072c07d47946d5a0a9cd2f7ce
7
+ data.tar.gz: 909d606e1bb8864264ee25e175b9422b5754071788bd2dcb590d617dd00aa7041f30a788380717a18d3242230939feff69e7a167d5b45096e22d29d6ae613f2f
@@ -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 the `tool` directive no longer crashes if not passed a block.
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, but they're
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: ->(e) {
172
+ error_handler: ->(_err) {
173
173
  puts "Dude, an error happened..."
174
174
  return 1
175
175
  }
@@ -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. You
9
- can generally write
10
-
11
- This user's guide covers everything you need to know to build your own command
12
- line executables in Ruby using the Toys-Core framework.
13
-
14
- This guide assumes you are already familiar with Toys itself, including how to
15
- define tools by writing Toys files, parsing arguments and flags, and how tools
16
- are executed. For background, please see the
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. It
25
- provides libraries to handle basic functions such as argumet parsing and online
26
- help, and you provide the actual behavior.
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
- ## Writing tools
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
- ## Customizing the tool environment
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
@@ -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 middleware that will be used
121
- # by default for all tools loaded by this CLI.
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 paths in the loader.
219
- # This is sometimes useful for running sub-tools that have to be loaded
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 _opts [Hash] Unused options that can be used by subclasses.
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(**_opts)
228
- cli = CLI.new(executable_name: @executable_name,
229
- config_dir_name: @config_dir_name,
230
- config_file_name: @config_file_name,
231
- index_file_name: @index_file_name,
232
- preload_dir_name: @preload_dir_name,
233
- preload_file_name: @preload_file_name,
234
- data_dir_name: @data_dir_name,
235
- middleware_stack: @middleware_stack,
236
- extra_delimiters: @extra_delimiters,
237
- mixin_lookup: @mixin_lookup,
238
- middleware_lookup: @middleware_lookup,
239
- template_lookup: @template_lookup,
240
- logger: @logger,
241
- base_level: @base_level,
242
- error_handler: @error_handler,
243
- completion: @completion)
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
- [:set_default_descriptions],
608
- [:show_help, help_flags: true, fallback_execution: true],
609
- [:handle_usage_errors],
610
- [:add_verbosity_flags],
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
 
@@ -30,7 +30,7 @@ module Toys
30
30
  # Current version of Toys core.
31
31
  # @return [String]
32
32
  #
33
- VERSION = "0.9.1"
33
+ VERSION = "0.9.2"
34
34
  end
35
35
 
36
36
  ## @private deprecated
@@ -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
@@ -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
- ## @private
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 middleware that will be used
63
- # by default for all tools loaded by this loader.
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
- @middleware_stack = middleware_stack
80
+ @loading_started = false
88
81
  @worklist = []
89
82
  @tool_data = {}
90
83
  @max_priority = @min_priority = 0
91
- @extra_delimiters = process_extra_delimiters(extra_delimiters)
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
- priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
107
- paths.each do |path|
108
- source = SourceInfo.create_path_root(path)
109
- @worklist << [source, [], priority]
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
- priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
130
- source = SourceInfo.create_proc_root(block, name)
131
- @worklist << [source, [], priority]
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
- tool_data = get_tool_data(words)
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
- @tool_data.each do |n, td|
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.active_definition || td.top_definition
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
- @tool_data.each do |n, td|
219
- if !n.empty? && n.length > len && n.slice(0, len) == words && !td.definitions.empty?
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 = str.to_s
237
- @extra_delimiters ? str.split(@extra_delimiters) : [str]
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
- tool_data = get_tool_data(words)
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
- # Loads the subtree under the given prefix.
282
+ # Build a new tool.
283
+ # Called only from ToolData.
278
284
  #
279
- # @param prefix [Array<String>] The name prefix.
280
- # @return [self]
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 load_for_prefix(prefix)
285
- cur_worklist = @worklist
286
- @worklist = []
287
- cur_worklist.each do |source, words, priority|
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
- # Get or create the tool definition for the given name and priority.
299
+ # Loads the subtree under the given prefix.
300
300
  #
301
- # @return [Toys::Tool]
301
+ # @param prefix [Array<String>] The name prefix.
302
+ # @return [self]
302
303
  #
303
304
  # @private
304
305
  #
305
- def get_tool(words, priority)
306
- parent = words.empty? ? nil : get_tool(words.slice(0..-2), priority)
307
- tool_data = get_tool_data(words)
308
- if tool_data.top_priority.nil? || tool_data.top_priority < priority
309
- tool_data.top_priority = priority
310
- end
311
- tool_data.definitions[priority] ||= begin
312
- middlewares = @middleware_stack.map { |m| resolve_middleware(m) }
313
- Tool.new(self, parent, words, priority, middlewares)
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
- load_validated_path(source, words, remaining_words, priority)
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
- ALLOWED_DELIMITERS = %r{^[\./:]*$}.freeze
372
- private_constant :ALLOWED_DELIMITERS
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({}, nil, nil)
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
- @tool_data.each do |n, td|
404
+ tool_data_snapshot.each do |n, td|
447
405
  next if n.length < len || n.slice(0, len) != words
448
- tool = td.active_definition || td.top_definition
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