Class: Aspera::Cli::CommandRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/cli/command_registry.rb

Overview

Stores CommandSpec objects indexed by their full path (Array). Each plugin class gets its own instance (not shared across the inheritance chain).

Public API:

register(spec)          - store a CommandSpec; raises on duplicate full_path
register_option(spec)   - store an OptionSpec by name
option_specs            - Hash{Symbol => OptionSpec} of all registered options
[](path)                - retrieve a CommandSpec by full path
children_of(path)       - Hash{Symbol => CommandSpec} of direct children (O(1))
all_paths               - Array of all registered full paths
any?                    - true if at least one spec has been registered
validate!               - cross-spec consistency checks; raises on violation

Instance Method Summary collapse

Instance Method Details

#[](path) ⇒ CommandSpec?

Parameters:

  • full path to look up

Returns:



22
23
24
# File 'lib/aspera/cli/command_registry.rb', line 22

def [](path)
  @specs[Array(path)]
end

#all_pathsArray<Array<Symbol>>

Returns all registered full paths.

Returns:

  • all registered full paths



51
52
53
# File 'lib/aspera/cli/command_registry.rb', line 51

def all_paths
  @specs.keys
end

#any?Boolean

Returns true if at least one spec is registered.

Returns:

  • true if at least one spec is registered



70
71
72
# File 'lib/aspera/cli/command_registry.rb', line 70

def any?
  !@specs.empty?
end

#children_of(path) ⇒ Hash{Symbol => CommandSpec}

Returns a Hash mapping each child id to its CommandSpec for all direct children of path. Empty hash if no children are registered. O(1) lookup via the children index built in register().

Parameters:

  • parent path ([] for root-level commands)

Returns:



46
47
48
# File 'lib/aspera/cli/command_registry.rb', line 46

def children_of(path)
  @children_index[Array(path)] || {}
end

#none?Boolean

Returns true if no specs have been registered.

Returns:

  • true if no specs have been registered



75
76
77
# File 'lib/aspera/cli/command_registry.rb', line 75

def none?
  @specs.empty?
end

#option_specsHash{Symbol => OptionSpec}

Returns all registered option specs.

Returns:

  • all registered option specs



65
66
67
# File 'lib/aspera/cli/command_registry.rb', line 65

def option_specs
  @option_specs.dup
end

#register(spec) ⇒ CommandSpec

Register a CommandSpec. Raises if the full_path is already registered. Also updates the children index so children_of remains O(1).

Parameters:

Returns:

  • the registered spec

Raises:

  • on duplicate full path



31
32
33
34
35
36
37
38
39
# File 'lib/aspera/cli/command_registry.rb', line 31

def register(spec)
  path = spec.full_path
  raise ArgumentError, "Duplicate command path: #{path.inspect}" if @specs.key?(path)
  @specs[path] = spec
  # Index: parent_path -> { child_id -> spec }
  parent = path[0..-2] # [] for root-level commands
  (@children_index[parent] ||= {})[spec.id] = spec
  spec
end

#register_option(spec) ⇒ OptionSpec

Register an OptionSpec. Raises if the option name is already registered.

Parameters:

Returns:

  • the registered spec

Raises:

  • on duplicate option name



59
60
61
62
# File 'lib/aspera/cli/command_registry.rb', line 59

def register_option(spec)
  raise ArgumentError, "Duplicate option: #{spec.name.inspect}" if @option_specs.key?(spec.name)
  @option_specs[spec.name] = spec
end

#validate!(plugin_class: nil) ⇒ self

Cross-spec consistency checks.

Parameters:

  • (defaults to: nil)

    when given, also verify that implicit action methods exist

Returns:

Raises:

  • on any violation



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/aspera/cli/command_registry.rb', line 83

def validate!(plugin_class: nil)
  # Rule: every non-root parent path that appears in the children index must have
  # a registered CommandSpec. A missing parent means commands_under(:x) was used
  # without a matching command :x declaration.
  @children_index.each_key do |parent_path|
    next if parent_path.empty? # root is never a CommandSpec
    unless @specs.key?(parent_path)
      raise ArgumentError,
        "commands_under(#{parent_path.map(&:inspect).join(', ')}) used but #{parent_path.last.inspect} has no command declaration"
    end
  end

  @specs.each_value do |spec|
    path = spec.full_path

    # Rule: delegates_to must point to a known path when present (non-empty array or symbol)
    if spec.delegates_to
      dt_path =
        case spec.delegates_to
        when Symbol then [spec.delegates_to]
        when Array  then spec.delegates_to
        end
      # An empty array [] means re-enter the root - always valid
      unless dt_path.empty? || @specs.key?(dt_path)
        raise ArgumentError,
          "#{path.inspect}: delegates_to #{dt_path.inspect} points to unknown path"
      end
    end

    # Rule: delegate_instance requires delegates_to
    if spec.delegate_instance && spec.delegates_to.nil?
      raise ArgumentError,
        "#{path.inspect}: delegate_instance requires delegates_to to be set"
    end

    # Rule: leaf commands with no explicit action must have a matching instance method
    next if spec.action # explicit action: skip
    next if @children_index[path]&.any? # intermediate node: skip
    next if spec.delegates_to || spec.entity_execute # delegated: skip
    next unless plugin_class
    implicit_method = CommandSpec.action_method(path)
    unless plugin_class.method_defined?(implicit_method) || plugin_class.private_method_defined?(implicit_method)
      raise ArgumentError,
        "#{path.inspect}: no action: and no method #{implicit_method} on #{plugin_class}"
    end
  end
  self
end