Skip to content

Commit 28501bd

Browse files
committed
Refactor: DRY up codebase and reduce complexity
- BlockDSL: replace manual delegation with method_missing - Unify command method via command_base_for hook (CommandDSL + Tool) - Extract long_flag/short_flag/signature onto OptionDefinition - HelpFormatter: self-managing sections, remove option_signature - derive_command_name: use demodulize/underscore from core_ext - Runner: resolve aliases via terms, add find_subcommand - Tool#start: collapse rescue chain to Athena::Error - Util::Formatting: extend self instead of self. prefix - Condense getter/setter DSL methods to ternaries
1 parent 41c6f3a commit 28501bd

File tree

8 files changed

+88
-132
lines changed

8 files changed

+88
-132
lines changed

lib/athena/command.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,9 @@ def inherited(subclass)
4545

4646
def derive_command_name
4747
return nil if self == Command || abstract_class?
48-
base = name&.split("::")&.last
48+
base = name&.demodulize
4949
return nil unless base
50-
base.gsub(/Command$/, "")
51-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
52-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
53-
.downcase
54-
.to_sym
50+
base.sub(/Command$/, "").underscore.to_sym
5551
end
5652

5753
def register_subcommand(subclass)

lib/athena/dsl/block_dsl.rb

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,26 @@
22

33
module Athena
44
module DSL
5-
# Evaluation context for block-based command definitions.
6-
# Translates block DSL calls into class-level DSL calls on the
7-
# underlying Command subclass.
85
class BlockDSL
96
def initialize(command_class)
107
@command_class = command_class
118
end
129

13-
def description(text)
14-
@command_class.description(text)
15-
end
16-
17-
def aliases(*names)
18-
@command_class.aliases(*names)
19-
end
20-
21-
def option(name, type = nil, **opts)
22-
@command_class.option(name, type, **opts)
23-
end
24-
25-
def flag(name, **opts)
26-
@command_class.flag(name, **opts)
10+
def run(&block)
11+
@command_class.define_method(:run) { |*args| instance_exec(*args, &block) }
2712
end
2813

29-
def argument(name, type = String, **opts)
30-
@command_class.argument(name, type, **opts)
31-
end
14+
private
3215

33-
def command(name, **opts, &block)
34-
@command_class.command(name, **opts, &block)
16+
def respond_to_missing?(name, include_private = false)
17+
@command_class.respond_to?(name) || super
3518
end
3619

37-
def run(&block)
38-
@command_class.define_method(:run) do |*args|
39-
instance_exec(*args, &block)
20+
def method_missing(name, *args, **opts, &block)
21+
if @command_class.respond_to?(name)
22+
@command_class.public_send(name, *args, **opts, &block)
23+
else
24+
super
4025
end
4126
end
4227
end

lib/athena/dsl/command_dsl.rb

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,13 @@
22

33
module Athena
44
module DSL
5-
# Class-level DSL methods extended into Athena::Command.
6-
# These are called during class body evaluation.
75
module CommandDSL
86
def description(text = nil)
9-
if text
10-
@description = text
11-
else
12-
@description || ""
13-
end
7+
text ? (@description = text) : (@description || "")
148
end
159

1610
def aliases(*names)
17-
if names.any?
18-
@aliases = names.flatten.map(&:to_sym)
19-
else
20-
@aliases || []
21-
end
11+
names.any? ? (@aliases = names.flatten.map(&:to_sym)) : (@aliases || [])
2212
end
2313

2414
def option(name, type = nil, short: nil, description: nil, default: nil, required: false)
@@ -38,21 +28,23 @@ def argument(name, type = String, description: nil, required: true, default: nil
3828
)
3929
end
4030

41-
# Block-based subcommand definition.
42-
# Creates an anonymous Command subclass, evaluates the block, and
43-
# registers it as a subcommand of the receiver.
4431
def command(name, aliases: [], &block)
45-
klass = Class.new(self)
32+
klass = Class.new(command_base_for(name))
4633
klass.command_name = name.to_sym
4734
klass.aliases(*aliases) if aliases.any?
4835

49-
# Give it a const name for inspect/debugging
5036
const_name = name.to_s.split("_").map(&:capitalize).join
5137
const_set(const_name, klass) if const_name.match?(/\A[A-Z]/)
5238

53-
Athena::DSL::BlockDSL.new(klass).instance_eval(&block) if block
39+
BlockDSL.new(klass).instance_eval(&block) if block
5440
klass
5541
end
42+
43+
private
44+
45+
def command_base_for(_name)
46+
self
47+
end
5648
end
5749
end
5850
end

lib/athena/help_formatter.rb

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,29 @@ def initialize(command_class, command_path: [])
1010
end
1111

1212
def format
13-
lines = []
14-
lines << description_section if command_class.description.present?
15-
lines << version_section if command_class.respond_to?(:version) && command_class.version
16-
lines << usage_section
17-
lines << subcommands_section if command_class.subcommands.any?
18-
lines << options_section if command_class.option_definitions.any?
19-
lines << arguments_section if command_class.argument_definitions.any?
20-
lines.compact.join("\n\n") + "\n"
13+
[
14+
description_section,
15+
version_section,
16+
usage_section,
17+
subcommands_section,
18+
options_section,
19+
arguments_section
20+
].compact.join("\n\n") + "\n"
2121
end
2222

2323
private
2424

2525
def description_section
26-
command_class.description
26+
desc = command_class.description
27+
desc if desc.present?
2728
end
2829

2930
def version_section
30-
"Version: #{command_class.version.to_s.light_blue}"
31+
ver = command_class.respond_to?(:version) && command_class.version
32+
"Version: #{ver.to_s.light_blue}" if ver
3133
end
3234

3335
def usage_section
34-
parts = ["Usage:".light_cyan]
3536
path = command_path.any? ? command_path.join(" ") : command_class.command_name.to_s
3637
usage = path.light_red
3738
usage += " [options]".light_cyan if command_class.option_definitions.any?
@@ -40,13 +41,12 @@ def usage_section
4041
label = arg.required ? "<#{arg.name}>" : "[#{arg.name}]"
4142
usage += " #{label}".light_yellow
4243
end
43-
parts << usage
44-
parts.join(" ")
44+
"Usage:".light_cyan + " " + usage
4545
end
4646

4747
def subcommands_section
4848
subs = command_class.subcommands
49-
return nil if subs.empty?
49+
return if subs.empty?
5050

5151
max_width = subs.keys.map { |k| k.to_s.length }.max
5252
Util::Formatting.reset_colors!
@@ -68,13 +68,13 @@ def subcommands_section
6868

6969
def options_section
7070
opts = command_class.option_definitions
71-
return nil if opts.empty?
71+
return if opts.empty?
7272

73-
max_width = opts.values.map { |o| option_signature(o).length }.max
73+
max_width = opts.values.map { |o| o.signature.length }.max
7474

7575
lines = ["Options:".light_cyan]
7676
opts.each_value do |opt|
77-
sig = option_signature(opt).ljust(max_width + 2)
77+
sig = opt.signature.ljust(max_width + 2)
7878
desc = opt.description || ""
7979
default_note = opt.default_value ? " (default: #{opt.default_value})".light_black : ""
8080
lines << " #{sig.light_green} #{desc}#{default_note}"
@@ -84,7 +84,7 @@ def options_section
8484

8585
def arguments_section
8686
args = command_class.argument_definitions
87-
return nil if args.empty?
87+
return if args.empty?
8888

8989
max_width = args.map { |a| a.name.to_s.length }.max
9090

@@ -97,14 +97,5 @@ def arguments_section
9797
end
9898
lines.join("\n")
9999
end
100-
101-
def option_signature(opt)
102-
parts = []
103-
parts << "-#{opt.short}," if opt.short
104-
long = "--#{opt.name.to_s.tr('_', '-')}"
105-
long += "=VALUE" unless opt.boolean?
106-
parts << long
107-
parts.join(" ")
108-
end
109100
end
110101
end

lib/athena/option_definition.rb

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,26 @@ def default_value
2121
boolean? ? (default.nil? ? false : default) : default
2222
end
2323

24-
# Register this option with a stdlib OptionParser instance.
25-
# When the option is encountered, its value is stored in +store+.
24+
def long_flag
25+
flag = "--#{name.to_s.tr('_', '-')}"
26+
flag += "=VALUE" unless boolean?
27+
flag
28+
end
29+
30+
def short_flag
31+
"-#{short}" if short
32+
end
33+
34+
def signature
35+
[("#{short_flag}," if short), long_flag].compact.join(" ")
36+
end
37+
2638
def attach(parser, store)
27-
args = []
28-
args << "-#{short}" if short
29-
long_flag = "--#{name.to_s.tr('_', '-')}"
30-
long_flag += "=VALUE" unless boolean?
31-
args << long_flag
39+
args = [short_flag, long_flag].compact
3240
args << type if type && !boolean?
3341
args << description if description
3442

35-
parser.on(*args) do |value|
36-
store[name] = value
37-
end
43+
parser.on(*args) { |value| store[name] = value }
3844
end
3945
end
4046
end

lib/athena/runner.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ def initialize(root, argv)
99
@argv = argv.dup
1010
end
1111

12-
# Resolve the ARGV to a command class, instantiate, and run.
1312
def execute
1413
command_class, remaining, path = resolve(root, argv)
1514

@@ -24,8 +23,7 @@ def execute
2423
end
2524

2625
instance = command_class.new(remaining)
27-
args = instance.args
28-
instance.run(*args)
26+
instance.run(*instance.args)
2927
end
3028

3129
private
@@ -38,24 +36,25 @@ def version_requested?(args)
3836
args.include?("--version") || args.include?("-V")
3937
end
4038

41-
# Walk the command tree, consuming subcommand tokens from argv.
42-
# Returns [CommandClass, remaining_argv, command_path].
4339
def resolve(command_class, args, path = [])
4440
path << (command_class.command_name || command_class.name || "command").to_s
4541
return [command_class, args, path] if args.empty?
4642

4743
token = args.first
48-
49-
# Don't treat flags as subcommand names
5044
return [command_class, args, path] if token.start_with?("-")
5145

52-
sub = command_class.subcommands[token.to_sym]
46+
sub = find_subcommand(command_class, token.to_sym)
5347
if sub
5448
args.shift
5549
resolve(sub, args, path)
5650
else
5751
[command_class, args, path]
5852
end
5953
end
54+
55+
def find_subcommand(command_class, token)
56+
command_class.subcommands[token] ||
57+
command_class.subcommands.each_value.find { |cmd| cmd.terms.include?(token) }
58+
end
6059
end
6160
end

lib/athena/tool.rb

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,19 @@ def command_class(klass = nil)
1515
end
1616

1717
def tool_name(name = nil)
18-
if name
19-
self.command_name = name
20-
else
21-
command_name
22-
end
18+
name ? (self.command_name = name) : command_name
2319
end
2420

2521
def version(ver = nil)
26-
if ver
27-
@version = ver
28-
else
29-
@version
30-
end
22+
ver ? (@version = ver) : @version
3123
end
3224

3325
def start(argv = ARGV)
34-
runner = Runner.new(self, argv.dup)
35-
runner.execute
26+
Runner.new(self, argv.dup).execute
3627
rescue Interrupt
3728
$stderr.puts "\nAborted."
3829
exit 130
39-
rescue CommandNotFound => e
40-
$stderr.puts e.message
41-
exit 1
42-
rescue MissingArgument => e
43-
$stderr.puts e.message
44-
exit 1
45-
rescue AbstractCommand => e
30+
rescue Athena::Error => e
4631
$stderr.puts e.message
4732
exit 1
4833
end
@@ -60,6 +45,10 @@ def inherited(subclass)
6045

6146
private
6247

48+
def command_base_for(_name)
49+
command_class || self
50+
end
51+
6352
def create_command_base(tool_subclass)
6453
return if tool_subclass.command_class
6554

@@ -75,26 +64,22 @@ def wire_command_class(klass)
7564

7665
klass.define_singleton_method(:inherited) do |subclass|
7766
super(subclass)
67+
cmd_name = subclass.command_name
68+
if cmd_name && !subclass.abstract_class?
69+
subclass.instance_variable_set(:@_derived_name, cmd_name)
70+
tool.subcommands[cmd_name] = subclass
71+
end
7872
end
7973

8074
klass.define_singleton_method(:inherited_command_name_set) do |subclass|
8175
cmd_name = subclass.command_name
82-
tool.subcommands[cmd_name] = subclass if cmd_name && !subclass.abstract_class?
76+
if cmd_name && !subclass.abstract_class?
77+
derived = subclass.instance_variable_get(:@_derived_name)
78+
tool.subcommands.delete(derived) if derived && derived != cmd_name
79+
tool.subcommands[cmd_name] = subclass
80+
end
8381
end
8482
end
8583
end
86-
87-
def self.command(name, aliases: [], &block)
88-
base = command_class || self
89-
klass = Class.new(base)
90-
klass.command_name = name.to_sym
91-
klass.aliases(*aliases) if aliases.any?
92-
93-
const_name = name.to_s.split("_").map(&:capitalize).join
94-
const_set(const_name, klass) if const_name.match?(/\A[A-Z]/)
95-
96-
Athena::DSL::BlockDSL.new(klass).instance_eval(&block) if block
97-
klass
98-
end
9984
end
10085
end

0 commit comments

Comments
 (0)