Skip to content

Commit e088e70

Browse files
committed
feat(renderer): show both ungrouped and grouped subcommands in help output
- Update renderHelp to always display ungrouped subcommands followed by groups - Add full test coverage for Renderer, including renderFailures and renderWarnings
1 parent 2a7f6e2 commit e088e70

2 files changed

Lines changed: 164 additions & 12 deletions

File tree

src/toasted_cli/Renderer.lua

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ function Renderer.renderWarnings(warnings)
6363
return table.concat(lines, "\n")
6464
end
6565

66-
6766
--- Render help text for a command, and list subcommands.
6867
-- @tparam table command The command object.
6968
-- @treturn string The help text.
@@ -91,28 +90,31 @@ function Renderer.renderHelp(command)
9190
end
9291
end
9392

94-
if command.subcommands and #command.subcommands > 0 then
93+
local ungrouped = (command.subcommands and #command.subcommands > 0) and command.subcommands or nil
94+
local groups = command.subcommand_groups
95+
96+
if ungrouped or (groups and #groups > 0) then
9597
table.insert(lines, "\nSubcommands:")
96-
if command.subcommand_groups then
97-
for _, group in ipairs(command.subcommand_groups) do
98+
if ungrouped then
99+
for _, sub in ipairs(ungrouped) do
100+
local name = sub.name or ""
101+
local desc = sub.description or ""
102+
table.insert(lines, string.format(" %-12s %s", name, desc))
103+
end
104+
end
105+
if groups then
106+
for _, group in ipairs(groups) do
98107
table.insert(lines, string.format(" %s:", group.name))
99-
for _, sub in ipairs(group.subcommands) do
108+
for _, sub in ipairs(group.subcommands or {}) do
100109
local name = sub.name or ""
101110
local desc = sub.description or ""
102111
table.insert(lines, string.format(" %-10s %s", name, desc))
103112
end
104113
end
105-
else
106-
for _, sub in ipairs(command.subcommands) do
107-
local name = sub.name or ""
108-
local desc = sub.description or ""
109-
table.insert(lines, string.format(" %-12s %s", name, desc))
110-
end
111114
end
112115
end
113116

114117
return table.concat(lines, "\n")
115118
end
116119

117-
118120
return Renderer
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
local Renderer = require "toasted_cli.Renderer"
2+
3+
describe("Renderer", function()
4+
describe("renderFailures", function()
5+
it("returns blank for empty input", function()
6+
assert.equals("", Renderer.renderFailures())
7+
assert.equals("", Renderer.renderFailures({}))
8+
end)
9+
10+
it("renders basic error entry", function()
11+
local failures = {{
12+
message = "Failed at {step}",
13+
code = "E001",
14+
field = "foo",
15+
step = "parse"
16+
}}
17+
local output = Renderer.renderFailures(failures)
18+
assert.is_not_nil(output:match("Error %[E001%] %(foo%)"))
19+
assert.is_not_nil(output:match("Failed at parse"))
20+
end)
21+
22+
it("renders multiple failures in order", function()
23+
local failures = {{
24+
message = "First"
25+
}, {
26+
message = "Second"
27+
}}
28+
local output = Renderer.renderFailures(failures)
29+
assert.is_not_nil(output:match("First"))
30+
assert.is_not_nil(output:match("Second"))
31+
local pos1 = assert(output:find("First"))
32+
local pos2 = assert(output:find("Second"))
33+
assert.is_true(pos1 < pos2)
34+
end)
35+
36+
it("includes detail if present", function()
37+
local failures = {{
38+
message = "Oh no",
39+
detail = "extra info"
40+
}, {
41+
message = "Err",
42+
detail = {
43+
abc = 1
44+
}
45+
}}
46+
local output = Renderer.renderFailures(failures)
47+
assert.is_not_nil(output:match("Detail: extra info"))
48+
assert.is_not_nil(output:match("Detail: %[table%]"))
49+
end)
50+
end)
51+
52+
describe("renderWarnings", function()
53+
it("returns blank for empty input", function()
54+
assert.equals("", Renderer.renderWarnings())
55+
assert.equals("", Renderer.renderWarnings({}))
56+
end)
57+
58+
it("renders basic warning entry", function()
59+
local warnings = {{
60+
message = "Something may be wrong",
61+
code = "W42",
62+
field = "bar"
63+
}}
64+
local output = Renderer.renderWarnings(warnings)
65+
assert.is_not_nil(output:match("Warning %[W42%] %(bar%)"))
66+
assert.is_not_nil(output:match("Something may be wrong"))
67+
end)
68+
end)
69+
70+
describe("renderHelp", function()
71+
it("renders minimal usage", function()
72+
local command = {
73+
name = "app"
74+
}
75+
local output = Renderer.renderHelp(command)
76+
assert.is_not_nil(output:match("Usage: app"))
77+
end)
78+
79+
it("includes description, arguments, and options where available", function()
80+
local command = {
81+
name = "app",
82+
description = "Does stuff",
83+
arguments = {{
84+
name = "file",
85+
description = "A file to use"
86+
}},
87+
options = {{
88+
names = {"-v", "--verbose"},
89+
description = "Be noisy"
90+
}}
91+
}
92+
local output = Renderer.renderHelp(command)
93+
assert.is_not_nil(output:match("Does stuff"))
94+
assert.is_not_nil(output:match("Arguments:"))
95+
assert.is_not_nil(output:match("file"))
96+
assert.is_not_nil(output:match("Options:"))
97+
assert.is_not_nil(output:match("verbose"))
98+
end)
99+
100+
it("renders both ungrouped and grouped subcommands if present", function()
101+
local cmd = {
102+
name = "demo",
103+
subcommands = {{
104+
name = "foo",
105+
description = "Foo desc"
106+
}, {
107+
name = "bar",
108+
description = "Bar desc"
109+
}},
110+
subcommand_groups = {{
111+
name = "group 1",
112+
subcommands = {{
113+
name = "baz",
114+
description = "Baz desc"
115+
}, {
116+
name = "qux",
117+
description = "Qux desc"
118+
}}
119+
}, {
120+
name = "tools",
121+
subcommands = {{
122+
name = "deploy",
123+
description = "Deploy stuff"
124+
}}
125+
}}
126+
}
127+
local output = Renderer.renderHelp(cmd)
128+
129+
local pos_foo = assert(output:find("foo"))
130+
local pos_bar = assert(output:find("bar"))
131+
local pos_supergroup = assert(output:find("group 1"))
132+
local pos_tools = assert(output:find("tools"))
133+
local pos_baz = assert(output:find("baz"))
134+
local pos_qux = assert(output:find("qux"))
135+
local pos_deploy = assert(output:find("deploy"))
136+
137+
assert.is_true(pos_foo < pos_supergroup)
138+
assert.is_true(pos_bar < pos_supergroup)
139+
140+
assert.is_true(pos_supergroup < pos_baz)
141+
assert.is_true(pos_tools < pos_deploy)
142+
143+
assert.is_not_nil(output:match("Foo desc"))
144+
assert.is_not_nil(output:match("Bar desc"))
145+
assert.is_not_nil(output:match("Baz desc"))
146+
assert.is_not_nil(output:match("Qux desc"))
147+
assert.is_not_nil(output:match("Deploy stuff"))
148+
end)
149+
end)
150+
end)

0 commit comments

Comments
 (0)