Skip to content

(4 of 4) Allow explicit dependencies injected into Command with dry-auto_inject (if accepted, stacks on top of #174) - #176

Open
kigster wants to merge 5 commits into
dry-rb:mainfrom
kigster:kig/auto-inject-compatibility
Open

kigster wants to merge 5 commits into
dry-rb:mainfrom
kigster:kig/auto-inject-compatibility

Conversation

@kigster

@kigster kigster commented Oct 8, 2026 •

Copy link
Copy Markdown

Proposal 4 of 4 in the dry-cli plugin-hooks series. It should be stacked on #175, but I don't have permission to set the base to a branch on a fork, it seems. Upstream issue/forum thread: pending.

Why

It's pretty common from what I've seen, and useful, to be able to define your custom BaseCommand (typically abstract), that decorates all of the inheriting commands with a specific behavior. One of such cases is to set a ui object and make it available throughout command lifecycle, to show errors in boxes, progress bars and concurrenly updating multiple spinners, and so on.

Other examples include injecting a database or Redis client driver.

Who uses it

Among my gems:

Summary

No library code changes. Shared per-command objects are dry-rb's job already: commands work with dry-auto_inject (and so dry-system containers) through Command.new. This PR documents it on Command and pins it with specs.

Deps = Dry::AutoInject(MyApp::Container)

class BaseCommand < Dry::CLI::Command
  include Deps["logger", "config"]
end

class Deploy < BaseCommand
  def initialize(target: "production", **)
    super(**)            # required, as for any class using dry-auto_inject
    @target = target
  end
end

Objects built from a command's streams can't come from a container. The docs give the rule for memoizing them, since an instance command is reused across calls (and across tests with Launcher):

def logger
  @logger = nil unless @logger_stream.equal?(stdout)
  @logger_stream = stdout
  @logger ||= Logger.new(stdout)
end

Evidence

  • Before: undocumented; a subclass #initialize without super(**) fails with ArgumentError: unknown keyword: :clock, with no guidance.

  • After: spec/integration/auto_inject_spec.rb (6): base-command injection, streams set before #initialize, instance commands, explicit keyword overriding the container, own #initialize with super(**), and the documented failure without it. The stream-memoization rule is exercised in launcher_spec.rb (Basic commands #1). Full suite 687 examples, 0 failures; rubocop clean; no new undocumented YARD objects.

Merge Danger

Docs and specs only; dry-auto_inject is a new development-only dependency.

Dry::CLI#call and Command.new take `kernel:`, defaulting to Kernel. Help,
errors and typos stop the CLI by raising a private Halt, which #call turns
into kernel.exit, so a kernel whose exit returns (as Aruba's does) still
stops the CLI. A command's own `exit` goes through its kernel too.

Dry::CLI::Launcher[registry] runs a CLI with the arguments, streams and
kernel in the order Aruba's in-process launcher passes them, so a CLI can
be tested in the same process with StringIO streams.
Registry#tree, Dry::CLI#tree and Dry::CLI::Tree.for return a live view of
the registered commands, for gems that describe a CLI rather than run it:
help screens, shell completion, documentation generators. Until now they
could only walk CommandRegistry internals.

Tree::Node#resolve shares its lookup with dispatch (CommandRegistry.lookup),
so the command it finds is the one the CLI would run. Tree::Param is a
frozen snapshot of an option or argument, including every declared key.
Help and command listings are built as a Dry::CLI::Screen, passed through
a renderer and a list of filters, then printed to the stream its status
chooses. Renderer and filters are any #call(screen) -> screen, so they
compose with >> (procs, dry-transformer functions).

Dry::CLI.configure sets them process-wide; Dry.CLI(..., config:) per CLI.
The default renderer wraps Banner and Usage, and the existing suite passes
unchanged, so unconfigured output is byte-for-byte what it was.

This replaces overriding the private #help and #spell_checker, which is
what dry-cli-help has to do today.
Commands already work with dry-auto_inject and dry-system containers,
through Command.new. Document how, including the one rule dry-auto_inject
has for any class (a subclass's own #initialize must call super(**)), and
pin it with specs, including the failure when that rule is broken.

Also document memoizing objects built from a command's streams against
the stream, since an instance command is reused across calls, and across
tests when a CLI runs in-process.
@kigster kigster changed the title Document dependencies injected with dry-auto_inject (4 of 4), stacks on kig/help-rendering-pipeline (4 of 4) Document dependencies injected with dry-auto_inject, stacks on kig/help-rendering-pipeline Oct 8, 2026
@kigster kigster changed the title (4 of 4) Document dependencies injected with dry-auto_inject, stacks on kig/help-rendering-pipeline (4 of 4) Document dependencies injected with dry-auto_inject (if accepted, stacks on top of #175) Oct 8, 2026
@kigster kigster changed the title (4 of 4) Document dependencies injected with dry-auto_inject (if accepted, stacks on top of #175) (4 of 4) Document dependencies injected with dry-auto_inject (if accepted, stacks on top of #174) Oct 8, 2026
@kigster kigster changed the title (4 of 4) Document dependencies injected with dry-auto_inject (if accepted, stacks on top of #174) (4 of 4) Allow explicit dependencies injected into Command with dry-auto_inject (if accepted, stacks on top of #174) Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant