Class: Toys::Loader

Inherits:
Object
  • Object
show all
Defined in:
lib/toys/loader.rb,
lib/toys/loader/load_state.rb,
lib/toys/loader/tool_registry.rb

Overview

The Loader service loads tools from tool sources, and finds the appropriate tool given a set of command line arguments.

Constant Summary collapse

FALLBACK_ROOT_PRIORITY =

The priority of the always-present fallback root tool. This is the priority of the root tool that will be returned from lookups if no sources have been added. If any source is added, it will have a higher priority than this, and its tools will supersede this fallback.

Returns:

  • (Integer)
-999_999_999

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source_list, tool_name_splitter: nil, middleware_stack: [], mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil, git_cache: nil, gems_util: nil) ⇒ Loader

Create a Loader.

Parameters:

  • source_list (Toys::SourceList)

    The list of sources to use. The source specs are snapshotted from the SourceList on construction, so if the SourceList is modified later, those modifications are not reflected in the constructed Loader. The specs themselves are not resolved until the Loader first looks up a tool.

  • tool_name_splitter (Toys::ToolNameSplitter) (defaults to: nil)

    The splitter that interprets delimiters in tool names. Defaults to ToolNameSplitter::DEFAULT, which recognizes only whitespace.

  • middleware_stack (Array<Toys::Middleware::Spec>) (defaults to: [])

    An array of middleware that will be used by default for all tools loaded by this loader.

  • mixin_lookup (Toys::ModuleLookup) (defaults to: nil)

    A lookup for well-known mixin modules. Defaults to an empty lookup.

  • middleware_lookup (Toys::ModuleLookup) (defaults to: nil)

    A lookup for well-known middleware classes. Defaults to an empty lookup.

  • template_lookup (Toys::ModuleLookup) (defaults to: nil)

    A lookup for well-known template classes. Defaults to an empty lookup.

  • git_cache (Toys::Utils::GitCache, nil) (defaults to: nil)

    A custom GitCache instance to use when resolving git sources. Optional. If nil or not specified, uses a process-wide default GitCache.

  • gems_util (Toys::Utils::Gems, nil) (defaults to: nil)

    A custom Gems utility instance to use when resolving gem sources. Optional. If nil or not specified, uses a process-wide default Gems utility.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/toys/loader.rb', line 55

def initialize(source_list,
               tool_name_splitter: nil,
               middleware_stack: [],
               mixin_lookup: nil,
               middleware_lookup: nil,
               template_lookup: nil,
               git_cache: nil,
               gems_util: nil)
  # This mutex serializes all loading. It could be held for arbitrary
  # amounts of time because it surrounds the loading of tools files.
  # Note that ToolRegistry does not have its own lock, so this mutex should
  # protect calls to @tool_registry.
  require "monitor"
  @mutex = ::Monitor.new

  # General references
  @mixin_lookup = mixin_lookup || ModuleLookup.new
  @template_lookup = template_lookup || ModuleLookup.new
  @tool_name_splitter = tool_name_splitter || ToolNameSplitter::DEFAULT
  @git_cache = git_cache
  @gems_util = gems_util

  # Set up the registry and priorities
  @stop_priority = FALLBACK_ROOT_PRIORITY
  @min_loaded_priority = HIGHEST_PRIORITY
  @tool_registry = ToolRegistry.new(middleware_stack: middleware_stack,
                                    middleware_lookup: middleware_lookup)
  # A root tool must always exist, at the lowest possible priority.
  @tool_registry.record_root(FALLBACK_ROOT_PRIORITY)
  @tool_registry.get_tool([], FALLBACK_ROOT_PRIORITY)

  # Worklist entries are pairs of either [source_spec, priority] or
  # [source_info, words]. The entries seeded here hold unresolved source
  # specs; the entries pushed back during a directory walk hold resolved
  # SourceInfo objects.
  @worklist = []
  source_list.each_with_priority do |spec, priority|
    @worklist << [spec, priority]
  end
end

Instance Attribute Details

#tool_name_splitterToys::ToolNameSplitter (readonly)

The splitter that interprets delimiters in the tool names handled by this loader. Use it to convert a delimited name into words.



102
103
104
# File 'lib/toys/loader.rb', line 102

def tool_name_splitter
  @tool_name_splitter
end

Instance Method Details

#has_subtools?(words) ⇒ boolean

Returns true if the given path has at least one subtool, even if they are hidden or non-runnable. Loads from the sources if necessary.

Parameters:

  • words (Array<String>)

    The name of the parent tool. It must be an array of strings; it cannot be a single string with delimiters.

Returns:

  • (boolean)

Raises:



210
211
212
213
# File 'lib/toys/loader.rb', line 210

def has_subtools?(words) # rubocop:disable Naming/PredicatePrefix
  load_for_prefix(words)
  each_definition_in_subtree(words) { |_tool| break :found } == :found # rubocop:disable Lint/UnreachableLoop
end

#list_subtools(words, recursive: false, include_hidden: false, include_namespaces: false, include_non_runnable: false) ⇒ Array<Toys::ToolDefinition>

Returns a list of subtools for the given path, loading from their sources and ensuring they are finished. The list will be sorted by name.

Parameters:

  • words (Array<String>)

    The name of the parent tool. It must be an array of strings; it cannot be a single string with delimiters.

  • recursive (boolean) (defaults to: false)

    If true, return all subtools recursively rather than just the immediate children (the default)

  • include_hidden (boolean) (defaults to: false)

    If true, include hidden subtools, i.e. names beginning with underscores. Defaults to false.

  • include_namespaces (boolean) (defaults to: false)

    If true, include namespaces, i.e. tools that are not runnable but have descendents that would have been listed by the current filters. Defaults to false.

  • include_non_runnable (boolean) (defaults to: false)

    If true, include tools that have no children and are not runnable. Defaults to false.

Returns:

Raises:



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/toys/loader.rb', line 175

def list_subtools(words,
                  recursive: false,
                  include_hidden: false,
                  include_namespaces: false,
                  include_non_runnable: false)
  words_len = words.length
  found_tools = []
  load_for_prefix(words)
  # We need to pull the entire subtree recursively even if we're not
  # returning recursive descendants, because we need the deeper data to
  # filter immediate children. If not returning recursive data, we'll
  # post-filter below.
  each_definition_in_subtree(words, recursive: true) do |tool|
    # each_definition_in_subtree iterates within the mutex, so this code
    # will execute within the mutex.
    if include_hidden || tool.full_name[words_len..].none? { |word| word.start_with?("_") }
      found_tools << tool.finish_definition(self)
    end
  end
  found_tools.sort_by!(&:full_name)
  found_tools = filter_non_runnable_tools(found_tools, include_namespaces, include_non_runnable)
  found_tools.select! { |tool| tool.full_name.length == words_len + 1 } unless recursive
  found_tools
end

#lookup(args) ⇒ Array(Toys::ToolDefinition,Array<String>)

Given a list of command line arguments, find the appropriate tool to handle the command, loading it from its source if necessary and ensuring it has been finished. This always returns a tool. If the specific tool path is not defined and cannot be found in any source, it finds the nearest namespace that would contain that tool, up to the root tool (which always exists.)

Returns a tuple of the found tool, and the array of remaining arguments that are not part of the tool name and should be passed as tool args.

Parameters:

  • args (Array<String>)

    Command line arguments. The first argument may be a full tool name with delimiters.

Returns:

Raises:



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/toys/loader.rb', line 121

def lookup(args)
  orig_prefix, args = find_orig_prefix(args)
  # Start looking for a tool with the entire prefix, and continue to
  # shorten it until a tool is found. Because the root tool always exists,
  # the final fallback of the empty prefix will always succeed.
  prefix = orig_prefix
  loop do
    tool = lookup_specific(prefix)
    return [tool, args.slice(prefix.length..-1)] if tool
    prefix = prefix.slice(0..-2)
  end
end

#lookup_specific(words) ⇒ Toys::ToolDefinition?

Given a tool name, looks up the specific tool, loading it from its source if necessary and ensuring it has been finished.

If there is an active tool, returns it; otherwise, returns the highest priority tool that has been defined. If no tool has been defined with the given name, returns nil.

Parameters:

  • words (Array<String>)

    The tool name. It must be in the form of an array of strings; it cannot be a single string with delimiters.

Returns:

Raises:



149
150
151
152
153
154
# File 'lib/toys/loader.rb', line 149

def lookup_specific(words)
  load_for_prefix(words)
  @mutex.synchronize do
    @tool_registry.cur_definition(words)&.finish_definition(self)
  end
end

#resolve_sourcesself

Ensures all root sources get resolved eagerly. Does not actually load any tools.

Can be called pre-emptively prior to other methods to prevent them from raising ToolSourceError directly. (It is still possible for them to raise ToolSourceError wrapped in a ContextualError if a tool invokes another source via one of the load directives.)

Returns:

  • (self)

Raises:



227
228
229
# File 'lib/toys/loader.rb', line 227

def resolve_sources
  load_for_prefix(nil)
end