toys-core 0.9.2 → 0.10.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +47 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +3 -3
  6. data/lib/toys-core.rb +14 -21
  7. data/lib/toys/acceptor.rb +0 -21
  8. data/lib/toys/arg_parser.rb +1 -22
  9. data/lib/toys/cli.rb +102 -70
  10. data/lib/toys/compat.rb +49 -41
  11. data/lib/toys/completion.rb +0 -21
  12. data/lib/toys/context.rb +0 -23
  13. data/lib/toys/core.rb +1 -22
  14. data/lib/toys/dsl/flag.rb +0 -21
  15. data/lib/toys/dsl/flag_group.rb +0 -21
  16. data/lib/toys/dsl/positional_arg.rb +0 -21
  17. data/lib/toys/dsl/tool.rb +136 -51
  18. data/lib/toys/errors.rb +1 -22
  19. data/lib/toys/flag.rb +0 -21
  20. data/lib/toys/flag_group.rb +0 -21
  21. data/lib/toys/input_file.rb +0 -21
  22. data/lib/toys/loader.rb +42 -78
  23. data/lib/toys/middleware.rb +146 -77
  24. data/lib/toys/mixin.rb +0 -21
  25. data/lib/toys/module_lookup.rb +3 -26
  26. data/lib/toys/positional_arg.rb +0 -21
  27. data/lib/toys/source_info.rb +49 -38
  28. data/lib/toys/standard_middleware/add_verbosity_flags.rb +0 -23
  29. data/lib/toys/standard_middleware/apply_config.rb +42 -0
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +7 -28
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +0 -23
  32. data/lib/toys/standard_middleware/show_help.rb +0 -23
  33. data/lib/toys/standard_middleware/show_root_version.rb +0 -23
  34. data/lib/toys/standard_mixins/bundler.rb +89 -0
  35. data/lib/toys/standard_mixins/exec.rb +478 -128
  36. data/lib/toys/standard_mixins/fileutils.rb +0 -21
  37. data/lib/toys/standard_mixins/gems.rb +2 -24
  38. data/lib/toys/standard_mixins/highline.rb +0 -21
  39. data/lib/toys/standard_mixins/terminal.rb +0 -21
  40. data/lib/toys/template.rb +0 -21
  41. data/lib/toys/tool.rb +22 -34
  42. data/lib/toys/utils/completion_engine.rb +0 -21
  43. data/lib/toys/utils/exec.rb +142 -71
  44. data/lib/toys/utils/gems.rb +181 -63
  45. data/lib/toys/utils/help_text.rb +0 -21
  46. data/lib/toys/utils/terminal.rb +46 -37
  47. data/lib/toys/wrappable_string.rb +0 -21
  48. metadata +25 -9
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  ##
26
5
  # An exception indicating an error in a tool definition.
@@ -96,7 +75,7 @@ module Toys
96
75
  rescue ::SyntaxError => e
97
76
  if (match = /#{::Regexp.escape(path)}:(\d+)/.match(e.message))
98
77
  opts = opts.merge(config_path: path, config_line: match[1].to_i)
99
- e = ContextualError.new(e, banner, opts)
78
+ e = ContextualError.new(e, banner, **opts)
100
79
  end
101
80
  raise e
102
81
  rescue ::ScriptError, ::StandardError => e
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  ##
26
5
  # Representation of a formal set of flags that set a particular context
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  ##
26
5
  # A FlagGroup is a group of flags with the same requirement settings.
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  ##
25
4
  # This module is a namespace for constant scopes. Whenever a configuration file
26
5
  # is parsed, a module is created under this parent for that file's constants.
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  require "monitor"
25
4
 
26
5
  module Toys
@@ -28,11 +7,7 @@ module Toys
28
7
  # The Loader service loads tools from configuration files, and finds the
29
8
  # appropriate tool given a set of command line arguments.
30
9
  #
31
- # This class is not thread-safe.
32
- #
33
10
  class Loader
34
- include ::MonitorMixin
35
-
36
11
  ##
37
12
  # Create a Loader
38
13
  #
@@ -50,6 +25,9 @@ module Toys
50
25
  # @param data_dir_name [String,nil] A directory with this name that appears
51
26
  # in any configuration directory is added to the data directory search
52
27
  # path for any tool file in that directory.
28
+ # @param lib_dir_name [String,nil] A directory with this name that appears
29
+ # in any configuration directory is added to the Ruby load path for any
30
+ # tool file in that directory.
53
31
  # @param middleware_stack [Array<Toys::Middleware::Spec>] An array of
54
32
  # middleware that will be used by default for all tools loaded by this
55
33
  # loader.
@@ -63,13 +41,22 @@ module Toys
63
41
  # @param template_lookup [Toys::ModuleLookup] A lookup for
64
42
  # well-known template classes. Defaults to an empty lookup.
65
43
  #
66
- def initialize(index_file_name: nil, preload_dir_name: nil, preload_file_name: nil,
67
- data_dir_name: nil, middleware_stack: [], extra_delimiters: "",
68
- mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil)
69
- super()
44
+ def initialize(
45
+ index_file_name: nil,
46
+ preload_dir_name: nil,
47
+ preload_file_name: nil,
48
+ data_dir_name: nil,
49
+ lib_dir_name: nil,
50
+ middleware_stack: [],
51
+ extra_delimiters: "",
52
+ mixin_lookup: nil,
53
+ middleware_lookup: nil,
54
+ template_lookup: nil
55
+ )
70
56
  if index_file_name && ::File.extname(index_file_name) != ".rb"
71
57
  raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
72
58
  end
59
+ @mutex = ::Monitor.new
73
60
  @mixin_lookup = mixin_lookup || ModuleLookup.new
74
61
  @template_lookup = template_lookup || ModuleLookup.new
75
62
  @middleware_lookup = middleware_lookup || ModuleLookup.new
@@ -77,11 +64,12 @@ module Toys
77
64
  @preload_file_name = preload_file_name
78
65
  @preload_dir_name = preload_dir_name
79
66
  @data_dir_name = data_dir_name
67
+ @lib_dir_name = lib_dir_name
80
68
  @loading_started = false
81
69
  @worklist = []
82
70
  @tool_data = {}
83
71
  @max_priority = @min_priority = 0
84
- @middleware_stack = Middleware.resolve_specs(*middleware_stack)
72
+ @middleware_stack = Middleware.stack(middleware_stack)
85
73
  @delimiter_handler = DelimiterHandler.new(extra_delimiters)
86
74
  get_tool([], -999_999)
87
75
  end
@@ -97,7 +85,7 @@ module Toys
97
85
  #
98
86
  def add_path(paths, high_priority: false)
99
87
  paths = Array(paths)
100
- synchronize do
88
+ @mutex.synchronize do
101
89
  raise "Cannot add a path after tool loading has started" if @loading_started
102
90
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
103
91
  paths.each do |path|
@@ -123,7 +111,7 @@ module Toys
123
111
  #
124
112
  def add_block(high_priority: false, name: nil, &block)
125
113
  name ||= "(Code block #{block.object_id})"
126
- synchronize do
114
+ @mutex.synchronize do
127
115
  raise "Cannot add a block after tool loading has started" if @loading_started
128
116
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
129
117
  source = SourceInfo.create_proc_root(block, name)
@@ -238,8 +226,6 @@ module Toys
238
226
  ##
239
227
  # Get or create the tool definition for the given name and priority.
240
228
  #
241
- # @return [Toys::Tool]
242
- #
243
229
  # @private
244
230
  #
245
231
  def get_tool(words, priority)
@@ -253,12 +239,6 @@ module Toys
253
239
  # the active priority, returns `nil`. If the given priority is higher than
254
240
  # the active priority, returns and activates a new tool.
255
241
  #
256
- # @param words [Array<String>] The name of the tool.
257
- # @param priority [Integer] The priority of the request.
258
- #
259
- # @return [Toys::Tool] The tool found.
260
- # @return [nil] if the given priority is insufficient.
261
- #
262
242
  # @private
263
243
  #
264
244
  def activate_tool(words, priority)
@@ -269,9 +249,6 @@ module Toys
269
249
  # Returns true if the given tool name currently exists in the loader.
270
250
  # Does not load the tool if not found.
271
251
  #
272
- # @param words [Array<String>] The name of the tool.
273
- # @return [Boolean]
274
- #
275
252
  # @private
276
253
  #
277
254
  def tool_defined?(words)
@@ -282,29 +259,21 @@ module Toys
282
259
  # Build a new tool.
283
260
  # Called only from ToolData.
284
261
  #
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.
289
- #
290
262
  # @private
291
263
  #
292
264
  def build_tool(words, priority)
293
265
  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)
266
+ middleware_stack = parent ? parent.subtool_middleware_stack : @middleware_stack
267
+ Tool.new(self, parent, words, priority, middleware_stack, @middleware_lookup)
296
268
  end
297
269
 
298
270
  ##
299
271
  # Loads the subtree under the given prefix.
300
272
  #
301
- # @param prefix [Array<String>] The name prefix.
302
- # @return [self]
303
- #
304
273
  # @private
305
274
  #
306
275
  def load_for_prefix(prefix)
307
- synchronize do
276
+ @mutex.synchronize do
308
277
  @loading_started = true
309
278
  cur_worklist = @worklist
310
279
  @worklist = []
@@ -323,10 +292,6 @@ module Toys
323
292
  ##
324
293
  # Attempt to get a well-known mixin module for the given symbolic name.
325
294
  #
326
- # @param name [Symbol] Mixin name
327
- # @return [Module] The mixin
328
- # @return [nil] if not found.
329
- #
330
295
  # @private
331
296
  #
332
297
  def resolve_standard_mixin(name)
@@ -336,10 +301,6 @@ module Toys
336
301
  ##
337
302
  # Attempt to get a well-known template class for the given symbolic name.
338
303
  #
339
- # @param name [Symbol] Template name
340
- # @return [Class] The template.
341
- # @return [nil] if not found.
342
- #
343
304
  # @private
344
305
  #
345
306
  def resolve_standard_template(name)
@@ -350,27 +311,29 @@ module Toys
350
311
  # Load configuration from the given path. This is called from the `load`
351
312
  # directive in the DSL.
352
313
  #
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
- #
360
314
  # @private
361
315
  #
362
316
  def load_path(parent_source, path, words, remaining_words, priority)
363
317
  source = parent_source.absolute_child(path)
364
- synchronize do
318
+ @mutex.synchronize do
365
319
  load_validated_path(source, words, remaining_words, priority)
366
320
  end
367
321
  end
368
322
 
369
323
  ##
370
- # Determine the next setting for remaining_words, given a word.
324
+ # Load a subtool block. Called from the `tool` directive in the DSL.
325
+ #
326
+ # @private
371
327
  #
372
- # @param remaining_words [Array<String>] The remaining words.
373
- # @param word [String] The next word to parse.
328
+ def load_block(parent_source, block, words, remaining_words, priority)
329
+ source = parent_source.proc_child(block)
330
+ @mutex.synchronize do
331
+ load_proc(source, words, remaining_words, priority)
332
+ end
333
+ end
334
+
335
+ ##
336
+ # Determine the next setting for remaining_words, given a word.
374
337
  #
375
338
  # @private
376
339
  #
@@ -387,11 +350,11 @@ module Toys
387
350
  private
388
351
 
389
352
  def tool_data_snapshot
390
- synchronize { @tool_data.dup }
353
+ @mutex.synchronize { @tool_data.dup }
391
354
  end
392
355
 
393
356
  def get_tool_data(words)
394
- synchronize { @tool_data[words] ||= ToolData.new(words) }
357
+ @mutex.synchronize { @tool_data[words] ||= ToolData.new(words) }
395
358
  end
396
359
 
397
360
  ##
@@ -443,15 +406,16 @@ module Toys
443
406
 
444
407
  def load_index_in(source, words, remaining_words, priority)
445
408
  return unless @index_file_name
446
- index_source = source.relative_child(@index_file_name, @data_dir_name)
409
+ index_source = source.relative_child(@index_file_name, @data_dir_name, @lib_dir_name)
447
410
  load_relevant_path(index_source, words, remaining_words, priority) if index_source
448
411
  end
449
412
 
450
413
  def load_child_in(source, child, words, remaining_words, priority)
451
414
  return if child.start_with?(".") || child == @index_file_name ||
452
415
  child == @preload_file_name || child == @preload_dir_name ||
453
- child == @data_dir_name
454
- child_source = source.relative_child(child, @data_dir_name)
416
+ child == @data_dir_name || child == @lib_dir_name
417
+ child_source = source.relative_child(child, @data_dir_name, @lib_dir_name)
418
+ return unless child_source
455
419
  child_word = ::File.basename(child, ".rb")
456
420
  next_words = words + [child_word]
457
421
  next_remaining = Loader.next_remaining_words(remaining_words, child_word)
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  ##
26
5
  # A middleware is an object that has the opportunity to alter the
@@ -39,15 +18,15 @@ module Toys
39
18
  # middleware, a Toys middleware can wrap execution with its own code,
40
19
  # replace it outright, or leave it unmodified.
41
20
  #
42
- # Generally, a middleware is a class that implements the two methods defined
43
- # in this module: {Toys::Middleware#config} and {Toys::Middleware#run}. To
44
- # get default implementations that do nothing, a middleware can
45
- # `include Toys::Middleware` or subclass {Toys::Middleware::Base}, but this
46
- # is not required.
21
+ # Generally, a middleware is a class that implements one or more of the
22
+ # methods defined in this module: {Toys::Middleware#config}, and
23
+ # {Toys::Middleware#run}. This module provides default implementations that
24
+ # do nothing, but using them is not required. Middleware objects need respond
25
+ # only to methods they care about.
47
26
  #
48
27
  module Middleware
49
28
  ##
50
- # This method is called after a tool has been defined, and gives this
29
+ # This method is called *after* a tool has been defined, and gives this
51
30
  # middleware the opportunity to modify the tool definition. It is passed
52
31
  # the tool definition object and the loader, and can make any changes to
53
32
  # the tool definition. In most cases, this method should also call
@@ -95,12 +74,6 @@ module Toys
95
74
  ##
96
75
  # Create a middleware spec.
97
76
  #
98
- # @overload spec(middleware_object)
99
- # Create a spec wrapping an existing middleware object
100
- #
101
- # @param middleware_object [Toys::Middleware] The middleware object
102
- # @return [Toys::Middleware::Spec] A spec
103
- #
104
77
  # @overload spec(name, *args, **kwargs, &block)
105
78
  # Create a spec indicating a given middleware name should be
106
79
  # instantiated with the given arguments.
@@ -111,27 +84,63 @@ module Toys
111
84
  # @param block [Proc,nil] The block to pass to the constructor
112
85
  # @return [Toys::Middleware::Spec] A spec
113
86
  #
87
+ # @overload spec(array)
88
+ # Create a middleware spec from an array specification.
89
+ #
90
+ # The array must be 1-4 elements long. The first element must be the
91
+ # middleware name or class. The other three arguments may include any
92
+ # or all of the following optional elements, in any order:
93
+ # * An array for the positional arguments to pass to the constructor
94
+ # * A hash for the keyword arguments to pass to the constructor
95
+ # * A proc for the block to pass to the constructor
96
+ #
97
+ # @param array [Array] The array input
98
+ # @return [Toys::Middleware::Spec] A spec
99
+ #
100
+ # @overload spec(middleware_object)
101
+ # Create a spec wrapping an existing middleware object
102
+ #
103
+ # @param middleware_object [Toys::Middleware] The middleware object
104
+ # @return [Toys::Middleware::Spec] A spec
105
+ #
114
106
  def spec(middleware, *args, **kwargs, &block)
115
- if middleware.is_a?(::String) || middleware.is_a?(::Symbol) || middleware.is_a?(::Class)
107
+ case middleware
108
+ when ::Array
109
+ spec_from_array(middleware)
110
+ when ::String, ::Symbol, ::Class
116
111
  Spec.new(nil, middleware, args, kwargs, block)
112
+ when Spec
113
+ middleware
117
114
  else
118
115
  Spec.new(middleware, nil, nil, nil, nil)
119
116
  end
120
117
  end
121
118
 
122
119
  ##
123
- # Create a middleware spec from an array specification.
120
+ # Create a {Toys::Middleware::Stack} from an array of middleware specs.
121
+ # Each element may be one of the following:
124
122
  #
125
- # The array must be 1-4 elements long. The first element must be the
126
- # middleware name or class. The other three arguments may include any or
127
- # all of the following optional elements, in any order:
128
- # * An array for the positional arguments to pass to the constructor
129
- # * A hash for the keyword arguments to pass to the constructor
130
- # * A proc for the block to pass to the constructor
123
+ # * A {Toys::Middleware} object
124
+ # * A {Toys::Middleware::Spec}
125
+ # * An array whose first element is a middleware name or class, and the
126
+ # subsequent elements are params that define what to pass to the class
127
+ # constructor (see {Toys::Middleware.spec})
131
128
  #
132
- # @param array [Array] The array input
133
- # @return [Toys::Middleware::Spec] A spec
129
+ # @param input [Array<Toys::Middleware,Toys::Middleware::Spec,Array>]
130
+ # @return [Toys::Middleware::Stack]
134
131
  #
132
+ def stack(input)
133
+ case input
134
+ when Stack
135
+ input
136
+ when ::Array
137
+ Stack.new(default_specs: input.map { |spec| spec(spec) })
138
+ else
139
+ raise ::ArgumentError, "Illegal middleware stack: #{input.inspect}"
140
+ end
141
+ end
142
+
143
+ ## @private
135
144
  def spec_from_array(array)
136
145
  middleware = array.first
137
146
  if !middleware.is_a?(::String) && !middleware.is_a?(::Symbol) && !middleware.is_a?(::Class)
@@ -154,32 +163,6 @@ module Toys
154
163
  end
155
164
  Spec.new(nil, middleware, args, kwargs, block)
156
165
  end
157
-
158
- ##
159
- # Resolve all arguments into an array of middleware specs. Each argument
160
- # may be one of the following:
161
- #
162
- # * A {Toys::Middleware} object
163
- # * A {Toys::Middleware::Spec}
164
- # * An array whose first element is a middleware name or class, and the
165
- # subsequent elements are params that define what to pass to the class
166
- # constructor (see {Toys::Middleware.spec_from_array})
167
- #
168
- # @param items [Array<Toys::Middleware,Toys::Middleware::Spec,Array>]
169
- # @return [Array<Toys::Middleware::Spec>]
170
- #
171
- def resolve_specs(*items)
172
- items.map do |item|
173
- case item
174
- when ::Array
175
- spec_from_array(item)
176
- when Spec
177
- item
178
- else
179
- spec(item)
180
- end
181
- end
182
- end
183
166
  end
184
167
 
185
168
  ##
@@ -216,14 +199,7 @@ module Toys
216
199
  else
217
200
  klass = @name
218
201
  end
219
- # Due to a bug in Ruby < 2.7, passing an empty **kwargs splat to
220
- # initialize will fail if there are no formal keyword args.
221
- formals = klass.instance_method(:initialize).parameters
222
- if @kwargs.empty? && formals.all? { |arg| arg.first != :key && arg.first != :keyrest }
223
- klass.new(*@args, &@block)
224
- else
225
- klass.new(*@args, **@kwargs, &@block)
226
- end
202
+ Compat.instantiate(klass, @args, @kwargs, @block)
227
203
  end
228
204
 
229
205
  ##
@@ -270,6 +246,99 @@ module Toys
270
246
  @kwargs = kwargs
271
247
  @block = block
272
248
  end
249
+
250
+ ## @private
251
+ def ==(other)
252
+ other.is_a?(Spec) &&
253
+ object.eql?(other.object) &&
254
+ name.eql?(other.name) &&
255
+ args.eql?(other.args) &&
256
+ kwargs.eql?(other.kwargs) &&
257
+ block.eql?(other.block)
258
+ end
259
+ alias eql? ==
260
+
261
+ ## @private
262
+ def hash
263
+ [object, name, args, kwargs, block].hash
264
+ end
265
+ end
266
+
267
+ ##
268
+ # A stack of middleware specs.
269
+ #
270
+ class Stack
271
+ ##
272
+ # The middleware specs that precede the default set.
273
+ # @return [Array<Toys::Middleware:Spec>]
274
+ #
275
+ attr_reader :pre_specs
276
+
277
+ ##
278
+ # The default set of middleware specs.
279
+ # @return [Array<Toys::Middleware:Spec>]
280
+ #
281
+ attr_reader :default_specs
282
+
283
+ ##
284
+ # The middleware specs that follow the default set.
285
+ # @return [Array<Toys::Middleware:Spec>]
286
+ #
287
+ attr_reader :post_specs
288
+
289
+ ##
290
+ # Add a middleware spec to the stack, in the default location, which is
291
+ # at the end of pre_specs). See {Toys::Middleware.spec} for a description
292
+ # of the arguments you can pass.
293
+ #
294
+ # @overload add(name, *args, **kwargs, &block)
295
+ # @overload add(array)
296
+ # @overload add(middleware_object)
297
+ #
298
+ def add(middleware, *args, **kwargs, &block)
299
+ pre_specs.push(Middleware.spec(middleware, *args, **kwargs, &block))
300
+ end
301
+
302
+ ##
303
+ # Duplicate this stack.
304
+ #
305
+ # @return [Toys::Middleware::Stack]
306
+ #
307
+ def dup
308
+ Stack.new(pre_specs: pre_specs.dup,
309
+ post_specs: post_specs.dup,
310
+ default_specs: default_specs.dup)
311
+ end
312
+
313
+ ##
314
+ # Build the middleware in this stack.
315
+ #
316
+ # @return [Array<Toys::Middleware>]
317
+ #
318
+ def build(middleware_lookup)
319
+ (@pre_specs + @default_specs + @post_specs).map { |spec| spec.build(middleware_lookup) }
320
+ end
321
+
322
+ ## @private
323
+ def initialize(default_specs: nil, pre_specs: nil, post_specs: nil)
324
+ @pre_specs = pre_specs || []
325
+ @post_specs = post_specs || []
326
+ @default_specs = default_specs || []
327
+ end
328
+
329
+ ## @private
330
+ def ==(other)
331
+ other.is_a?(Stack) &&
332
+ pre_specs.eql?(other.pre_specs) &&
333
+ default_specs.eql?(other.default_specs) &&
334
+ post_specs.eql?(other.post_specs)
335
+ end
336
+ alias eql? ==
337
+
338
+ ## @private
339
+ def hash
340
+ [@pre_specs, @default_specs, @post_specs].hash
341
+ end
273
342
  end
274
343
  end
275
344
  end