toys-core 0.9.4 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +30 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +3 -3
  6. data/lib/toys-core.rb +11 -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 +135 -51
  18. data/lib/toys/errors.rb +0 -21
  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 +41 -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 +124 -35
  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 +1 -21
  44. data/lib/toys/utils/gems.rb +174 -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.
@@ -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,15 @@ 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)
455
418
  return unless child_source
456
419
  child_word = ::File.basename(child, ".rb")
457
420
  next_words = 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