1use crate::{Spec, SpecCommand};
2use std::sync::LazyLock;
3use tera::Tera;
4
5pub fn render_help(spec: &Spec, cmd: &SpecCommand, long: bool) -> String {
6 let docs_spec = crate::docs::models::Spec::from(spec.clone());
8 let docs_cmd = crate::docs::models::SpecCommand::from(cmd);
9
10 let mut ctx = tera::Context::new();
11 ctx.insert("spec", &docs_spec);
12 ctx.insert("cmd", &docs_cmd);
13 ctx.insert("long", &long);
14 let template = if long {
15 "spec_template_long.tera"
16 } else {
17 "spec_template_short.tera"
18 };
19 TERA.render(template, &ctx).unwrap().trim().to_string() + "\n"
20}
21
22static TERA: LazyLock<Tera> = LazyLock::new(|| {
23 let mut tera = Tera::default();
24
25 tera.register_filter(
27 "ljust",
28 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
29 let value = value.as_str().unwrap_or("");
30 let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
31 Ok(format!("{:<width$}", value, width = width))
32 },
33 );
34 tera.register_filter(
35 "default",
36 |value: &tera::Value,
37 kwargs: tera::Kwargs,
38 _: &tera::State|
39 -> tera::TeraResult<tera::Value> {
40 let default_val = kwargs.must_get::<tera::Value>("value")?;
41 let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
42 if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
43 Ok(default_val)
44 } else {
45 Ok(value.clone())
46 }
47 },
48 );
49
50 #[rustfmt::skip]
51 tera.add_raw_templates([
52 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
53 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
54 ]).unwrap();
55
56 tera
57});
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use insta::assert_snapshot;
63
64 #[test]
65 fn test_render_help_with_env() {
66 let spec = crate::spec! { r#"
67bin "testcli"
68flag "--color" env="MYCLI_COLOR" help="Enable color output"
69flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
70flag "--debug" help="Debug mode"
71 "# }
72 .unwrap();
73
74 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
75 Usage: testcli [FLAGS]
76
77 Flags:
78 --color Enable color output [env: MYCLI_COLOR]
79 --verbose Verbose output [env: MYCLI_VERBOSE]
80 --debug Debug mode
81 ");
82
83 assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
84 Usage: testcli [FLAGS]
85
86 Flags:
87 --color Enable color output
88 [env: MYCLI_COLOR]
89 --verbose Verbose output
90 [env: MYCLI_VERBOSE]
91 --debug Debug mode
92 ");
93 }
94
95 #[test]
96 fn test_render_help_with_arg_env() {
97 let spec = crate::spec! { r#"
98bin "testcli"
99arg "<input>" env="MY_INPUT" help="Input file"
100arg "<output>" env="MY_OUTPUT" help="Output file"
101arg "<extra>" help="Extra arg without env"
102arg "[default]" help="Arg with default value" default="default value"
103 "# }
104 .unwrap();
105
106 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
107 Usage: testcli <ARGS>…
108
109 Arguments:
110 <input> Input file [env: MY_INPUT]
111 <output> Output file [env: MY_OUTPUT]
112 <extra> Extra arg without env
113 [default] Arg with default value (default: default value)
114 ");
115
116 assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
117 Usage: testcli <ARGS>…
118
119 Arguments:
120 <input> Input file
121 [env: MY_INPUT]
122 <output> Output file
123 [env: MY_OUTPUT]
124 <extra> Extra arg without env
125 [default] Arg with default value
126 (default: default value)
127 ");
128 }
129
130 #[test]
131 fn test_render_help_with_negated_flag() {
132 let spec = crate::spec! { r#"
133bin "testcli"
134flag "--compress" negate="--no-compress" default=#true help="Compress output"
135flag "--verbose" help="Verbose output"
136 "# }
137 .unwrap();
138
139 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
140 Usage: testcli [--compress] [--verbose]
141
142 Flags:
143 --compress / --no-compress Compress output
144 --verbose Verbose output
145 ");
146
147 assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
148 Usage: testcli [--compress] [--verbose]
149
150 Flags:
151 --compress / --no-compress Compress output
152 --verbose Verbose output
153 ");
154 }
155
156 #[test]
157 fn test_render_help_with_before_after_help() {
158 let spec = crate::spec! { r#"
159bin "testcli"
160before_help "This text appears before the help"
161after_help "This text appears after the help"
162flag "--verbose" help="Enable verbose output"
163 "# }
164 .unwrap();
165
166 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
167 This text appears before the help
168
169 Usage: testcli [--verbose]
170
171 Flags:
172 --verbose Enable verbose output
173
174 This text appears after the help
175 ");
176 }
177
178 #[test]
179 fn test_render_help_with_before_after_help_long() {
180 let spec = crate::spec! { r#"
181bin "testcli"
182before_help "short before"
183before_help_long "This is the long version of before help"
184after_help "short after"
185after_help_long "This is the long version of after help"
186flag "--verbose" help="Enable verbose output"
187 "# }
188 .unwrap();
189
190 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
191 short before
192
193 Usage: testcli [--verbose]
194
195 Flags:
196 --verbose Enable verbose output
197
198 short after
199 ");
200
201 assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
202 This is the long version of before help
203
204 Usage: testcli [--verbose]
205
206 Flags:
207 --verbose Enable verbose output
208
209 This is the long version of after help
210 ");
211 }
212
213 #[test]
214 fn test_render_help_with_examples() {
215 let spec = crate::spec! { r#"
216bin "testcli"
217flag "--verbose" help="Enable verbose output"
218example "testcli --verbose" header="Run with verbose output"
219example "testcli" header="Run normally" help="Just runs the tool"
220 "# }
221 .unwrap();
222
223 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
224 Usage: testcli [--verbose]
225
226 Flags:
227 --verbose Enable verbose output
228
229 Examples:
230 Run with verbose output:
231 $ testcli --verbose
232 Run normally:
233 $ testcli
234 ");
235
236 assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
237 Usage: testcli [--verbose]
238
239 Flags:
240 --verbose Enable verbose output
241
242 Examples:
243 Run with verbose output:
244 $ testcli --verbose
245 Run normally:
246 Just runs the tool
247 $ testcli
248 ");
249 }
250
251 #[test]
252 fn test_render_help_with_version() {
253 let spec = crate::spec! { r#"
254bin "testcli"
255name "TestCLI"
256version "1.2.3"
257flag "--verbose" help="Enable verbose output"
258 "# }
259 .unwrap();
260
261 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
262 TestCLI 1.2.3
263 Usage: testcli [--verbose]
264
265 Flags:
266 --verbose Enable verbose output
267 ");
268 }
269
270 #[test]
271 fn test_render_help_with_author_license() {
272 let spec = crate::spec! { r#"
273bin "testcli"
274author "Test Author"
275license "MIT"
276flag "--verbose" help="Enable verbose output"
277 "# }
278 .unwrap();
279
280 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
282 Usage: testcli [--verbose]
283
284 Flags:
285 --verbose Enable verbose output
286 ");
287
288 assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
290 Usage: testcli [--verbose]
291
292 Flags:
293 --verbose Enable verbose output
294
295 Author: Test Author
296 License: MIT
297 ");
298 }
299
300 #[test]
301 fn test_render_help_with_deprecated_command() {
302 let spec = crate::spec! { r#"
303bin "testcli"
304cmd "old-cmd" help="Do something" deprecated="use new-cmd instead"
305cmd "new-cmd" help="Do something better"
306 "# }
307 .unwrap();
308
309 assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
310 Usage: testcli <SUBCOMMAND>
311
312 Commands:
313 new-cmd Do something better
314 old-cmd [deprecated: use new-cmd instead] Do something
315 help Print this message or the help of the given subcommand(s)
316 ");
317 }
318}