toys-core 0.9.4 → 0.10.0

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 +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
  module StandardMiddleware
26
5
  ##
@@ -28,8 +7,6 @@ module Toys
28
7
  # `--version` flag is given.
29
8
  #
30
9
  class ShowRootVersion
31
- include Middleware
32
-
33
10
  ##
34
11
  # Default version flags
35
12
  # @return [Array<String>]
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toys
4
+ module StandardMixins
5
+ ##
6
+ # Ensures that a bundle is installed and set up when this tool is run.
7
+ #
8
+ # The following parameters can be passed when including this mixin:
9
+ #
10
+ # * `:groups` (Array<String>) The groups to include in setup
11
+ #
12
+ # * `:search_dirs` (Array<String,Symbol>) Directories to search for a
13
+ # Gemfile.
14
+ #
15
+ # You can pass full directory paths, and/or any of the following:
16
+ # * `:context` - the current context directory
17
+ # * `:current` - the current working directory
18
+ # * `:toys` - the Toys directory containing the tool definition
19
+ #
20
+ # The default is to search `[:toys, :context, :current]` in that order.
21
+ #
22
+ # * `:on_missing` (Symbol) What to do if a needed gem is not installed.
23
+ #
24
+ # Supported values:
25
+ # * `:confirm` - prompt the user on whether to install (default)
26
+ # * `:error` - raise an exception
27
+ # * `:install` - just install the gem
28
+ #
29
+ # * `:on_conflict` (Symbol) What to do if bundler has already been run
30
+ # with a different Gemfile.
31
+ #
32
+ # Supported values:
33
+ # * `:error` - raise an exception (default)
34
+ # * `:ignore` - just silently proceed without bundling again
35
+ # * `:warn` - print a warning and proceed without bundling again
36
+ #
37
+ # * `:terminal` (Toys::Utils::Terminal) Terminal to use (optional)
38
+ # * `:input` (IO) Input IO (optional, defaults to STDIN)
39
+ # * `:output` (IO) Output IO (optional, defaults to STDOUT)
40
+ #
41
+ module Bundler
42
+ include Mixin
43
+
44
+ on_initialize do
45
+ |groups: nil,
46
+ search_dirs: nil,
47
+ on_missing: nil,
48
+ on_conflict: nil,
49
+ terminal: nil,
50
+ input: nil,
51
+ output: nil|
52
+ require "toys/utils/gems"
53
+ search_dirs = ::Toys::StandardMixins::Bundler.resolve_search_dirs(search_dirs, self)
54
+ gems = ::Toys::Utils::Gems.new(on_missing: on_missing, on_conflict: on_conflict,
55
+ terminal: terminal, input: input, output: output)
56
+ gems.bundle(groups: groups, search_dirs: search_dirs)
57
+ end
58
+
59
+ ## @private
60
+ def self.resolve_search_dirs(search_dirs, context)
61
+ search_dirs ||= [:toys, :context, :current]
62
+ Array(search_dirs).flat_map do |dir|
63
+ case dir
64
+ when :context
65
+ context[::Toys::Context::Key::CONTEXT_DIRECTORY]
66
+ when :current
67
+ ::Dir.getwd
68
+ when :toys
69
+ toys_dir_stack(context[::Toys::Context::Key::TOOL_SOURCE])
70
+ when ::String
71
+ dir
72
+ else
73
+ raise ::ArgumentError, "Unrecognized search_dir: #{dir.inspect}"
74
+ end
75
+ end
76
+ end
77
+
78
+ ## @private
79
+ def self.toys_dir_stack(source_info)
80
+ dirs = []
81
+ while source_info
82
+ dirs << source_info.source_path if source_info.source_type == :directory
83
+ source_info = source_info.parent
84
+ end
85
+ dirs
86
+ end
87
+ end
88
+ end
89
+ end
@@ -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
  module StandardMixins
26
5
  ##
@@ -173,11 +152,14 @@ module Toys
173
152
  alias ruby exec_ruby
174
153
 
175
154
  ##
176
- # Execute a proc in a subprocess.
155
+ # Execute a proc in a forked subprocess.
177
156
  #
178
157
  # If the process is not set to run in the background, and a block is
179
158
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
180
159
  #
160
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
161
+ # do not support this method because they do not support fork.
162
+ #
181
163
  # @param func [Proc] The proc to call.
182
164
  # @param opts [keywords] The command options. Most options listed in the
183
165
  # {Toys::Utils::Exec} documentation are supported, plus the
@@ -196,12 +178,17 @@ module Toys
196
178
  end
197
179
 
198
180
  ##
199
- # Execute a tool. The command may be given as a single string or an array
200
- # of strings, representing the tool to run and the arguments to pass.
181
+ # Execute a tool in the current CLI in a forked process.
182
+ #
183
+ # The command may be given as a single string or an array of strings,
184
+ # representing the tool to run and the arguments to pass.
201
185
  #
202
186
  # If the process is not set to run in the background, and a block is
203
187
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
204
188
  #
189
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
190
+ # do not support this method because they do not support fork.
191
+ #
205
192
  # @param cmd [String,Array<String>] The tool to execute.
206
193
  # @param opts [keywords] The command options. Most options listed in the
207
194
  # {Toys::Utils::Exec} documentation are supported, plus the
@@ -220,6 +207,46 @@ module Toys
220
207
  self[KEY].exec_proc(func, **opts, &block)
221
208
  end
222
209
 
210
+ ##
211
+ # Execute a tool in a separately spawned process.
212
+ #
213
+ # The command may be given as a single string or an array of strings,
214
+ # representing the tool to run and the arguments to pass.
215
+ #
216
+ # If the process is not set to run in the background, and a block is
217
+ # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
218
+ #
219
+ # An entirely separate spawned process is run for this tool, using the
220
+ # setting of {Toys.executable_path}. Thus, this method can be run only if
221
+ # that setting is present. The normal Toys gem does set it, but if you
222
+ # are writing your own executable using Toys-Core, you will need to set
223
+ # it explicitly for this method to work. Furthermore, Bundler, if
224
+ # present, is reset to its "unbundled" environment. Thus, the tool found,
225
+ # the behavior of the CLI, and the gem environment, might not be the same
226
+ # as those of the calling tool.
227
+ #
228
+ # This method is often used if you are already in a bundle and need to
229
+ # run a tool that uses a different bundle. It may also be necessary on
230
+ # environments without "fork" (such as JRuby or Ruby on Windows).
231
+ #
232
+ # @param cmd [String,Array<String>] The tool to execute.
233
+ # @param opts [keywords] The command options. Most options listed in the
234
+ # {Toys::Utils::Exec} documentation are supported, plus the
235
+ # `exit_on_nonzero_status` option.
236
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
237
+ # for the subprocess streams.
238
+ #
239
+ # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
240
+ # the process is running in the background.
241
+ # @return [Toys::Utils::Exec::Result] The result, if the process ran in
242
+ # the foreground.
243
+ #
244
+ def exec_separate_tool(cmd, **opts, &block)
245
+ Exec._setup_clean_process(cmd) do |clean_cmd|
246
+ exec(clean_cmd, **opts, &block)
247
+ end
248
+ end
249
+
223
250
  ##
224
251
  # Execute a command. The command may be given as a single string to pass
225
252
  # to a shell, or an array of strings indicating a posix command.
@@ -268,7 +295,7 @@ module Toys
268
295
  end
269
296
 
270
297
  ##
271
- # Execute a proc in a subprocess.
298
+ # Execute a proc in a forked subprocess.
272
299
  #
273
300
  # Captures standard out and returns it as a string.
274
301
  # Cannot be run in the background.
@@ -276,6 +303,9 @@ module Toys
276
303
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
277
304
  # yielded to it.
278
305
  #
306
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
307
+ # do not support this method because they do not support fork.
308
+ #
279
309
  # @param func [Proc] The proc to call.
280
310
  # @param opts [keywords] The command options. Most options listed in the
281
311
  # {Toys::Utils::Exec} documentation are supported, plus the
@@ -291,15 +321,20 @@ module Toys
291
321
  end
292
322
 
293
323
  ##
294
- # Execute a tool. The command may be given as a single string or an array
295
- # of strings, representing the tool to run and the arguments to pass.
324
+ # Execute a tool in the current CLI in a forked process.
296
325
  #
297
326
  # Captures standard out and returns it as a string.
298
327
  # Cannot be run in the background.
299
328
  #
329
+ # The command may be given as a single string or an array of strings,
330
+ # representing the tool to run and the arguments to pass.
331
+ #
300
332
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
301
333
  # yielded to it.
302
334
  #
335
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
336
+ # do not support this method because they do not support fork.
337
+ #
303
338
  # @param cmd [String,Array<String>] The tool to execute.
304
339
  # @param opts [keywords] The command options. Most options listed in the
305
340
  # {Toys::Utils::Exec} documentation are supported, plus the
@@ -315,6 +350,46 @@ module Toys
315
350
  self[KEY].capture_proc(func, **opts, &block)
316
351
  end
317
352
 
353
+ ##
354
+ # Execute a tool in a separately spawned process.
355
+ #
356
+ # Captures standard out and returns it as a string.
357
+ # Cannot be run in the background.
358
+ #
359
+ # The command may be given as a single string or an array of strings,
360
+ # representing the tool to run and the arguments to pass.
361
+ #
362
+ # If a block is provided, a {Toys::Utils::Exec::Controller} will be
363
+ # yielded to it.
364
+ #
365
+ # An entirely separate spawned process is run for this tool, using the
366
+ # setting of {Toys.executable_path}. Thus, this method can be run only if
367
+ # that setting is present. The normal Toys gem does set it, but if you
368
+ # are writing your own executable using Toys-Core, you will need to set
369
+ # it explicitly for this method to work. Furthermore, Bundler, if
370
+ # present, is reset to its "unbundled" environment. Thus, the tool found,
371
+ # the behavior of the CLI, and the gem environment, might not be the same
372
+ # as those of the calling tool.
373
+ #
374
+ # This method is often used if you are already in a bundle and need to
375
+ # run a tool that uses a different bundle. It may also be necessary on
376
+ # environments without "fork" (such as JRuby or Ruby on Windows).
377
+ #
378
+ # @param cmd [String,Array<String>] The tool to execute.
379
+ # @param opts [keywords] The command options. Most options listed in the
380
+ # {Toys::Utils::Exec} documentation are supported, plus the
381
+ # `exit_on_nonzero_status` option.
382
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
383
+ # for the subprocess streams.
384
+ #
385
+ # @return [String] What was written to standard out.
386
+ #
387
+ def capture_separate_tool(cmd, **opts, &block)
388
+ Exec._setup_clean_process(cmd) do |clean_cmd|
389
+ capture(clean_cmd, **opts, &block)
390
+ end
391
+ end
392
+
318
393
  ##
319
394
  # Execute the given string in a shell. Returns the exit code.
320
395
  # Cannot be run in the background.
@@ -368,14 +443,12 @@ module Toys
368
443
 
369
444
  ## @private
370
445
  def self._setup_e_option(opts, context)
371
- result_callback =
372
- if opts[:exit_on_nonzero_status] || opts[:e]
373
- proc { |r| context.exit(r.exit_code) if r.error? }
374
- end
375
- opts = opts.merge(result_callback: result_callback)
376
- opts.delete(:exit_on_nonzero_status)
377
- opts.delete(:e)
378
- opts
446
+ e_options = [:exit_on_nonzero_status, :e]
447
+ if e_options.any? { |k| opts[k] }
448
+ result_callback = proc { |r| context.exit(r.exit_code) if r.error? }
449
+ opts = opts.merge(result_callback: result_callback)
450
+ end
451
+ opts.reject { |k, _v| e_options.include?(k) }
379
452
  end
380
453
 
381
454
  ## @private
@@ -389,6 +462,22 @@ module Toys
389
462
  end
390
463
  opts.merge(result_callback: result_callback)
391
464
  end
465
+
466
+ ## @private
467
+ def self._setup_clean_process(cmd)
468
+ raise ::ArgumentError, "Toys process is unknown" unless ::Toys.executable_path
469
+ cmd = ::Shellwords.split(cmd) if cmd.is_a?(::String)
470
+ cmd = Array(::Toys.executable_path) + cmd
471
+ if defined?(::Bundler)
472
+ if ::Bundler.respond_to?(:with_unbundled_env)
473
+ ::Bundler.with_unbundled_env { yield(cmd) }
474
+ else
475
+ ::Bundler.with_clean_env { yield(cmd) }
476
+ end
477
+ else
478
+ yield(cmd)
479
+ end
480
+ end
392
481
  end
393
482
  end
394
483
  end
@@ -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 "fileutils"
25
4
 
26
5
  module Toys
@@ -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
  module StandardMixins
26
5
  ##
@@ -39,10 +18,9 @@ module Toys
39
18
  # If you pass additional options to the include directive, those are used
40
19
  # to initialize settings for the gem install process. For example:
41
20
  #
42
- # include :gems, output: $stdout, default_confirm: false
21
+ # include :gems, on_missing: :error
43
22
  #
44
- # This is a frontend for {Toys::Utils::Gems}. More information is
45
- # available in that class's documentation.
23
+ # See {Toys::Utils::Gems#initialize} for a list of supported options.
46
24
  #
47
25
  module Gems
48
26
  include Mixin
@@ -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
  module StandardMixins
26
5
  ##