1use crate::error::{BuiltinDomain, EmitError, UsageErrorKind};
4use crate::examples::{
5 discover_examples, format_examples_list, ExampleRecord, ExamplesDiscoverError,
6};
7use crate::extensions::resolve_wyvern_share;
8use wyvern_schema::{ErrorCode, SerializeError, StderrError};
9
10#[must_use]
12pub fn examples_usage_message() -> String {
13 concat!(
14 "Usage: wyvern examples [list] [--json]\n",
15 " wyvern examples --help\n",
16 "\n",
17 "Commands:\n",
18 " list List bundled examples discovered from README frontmatter\n",
19 "\n",
20 "Options:\n",
21 " --json Print ExampleRecord JSON array\n",
22 "\n",
23 "Each example README under {wyvern_share}/examples/ must begin with:\n",
24 " ---\n",
25 " name: Example title\n",
26 " description: One-line summary\n",
27 " ---\n",
28 "\n",
29 "See also: wyvern guide, wyvern --help\n",
30 )
31 .to_string()
32}
33
34pub fn run_examples_command(args: &[String]) -> Result<String, ExamplesCmdError> {
40 if args
41 .first()
42 .is_some_and(|token| token == "--help" || token == "-h")
43 {
44 return Ok(examples_usage_message());
45 }
46 match args.first().map(String::as_str) {
47 None => run_list(args),
48 Some("list") => run_list(&args[1..]),
49 Some("--json") => run_list(args),
50 Some(other) if other.starts_with('-') => Err(unknown_flag(other)),
51 Some(other) => Err(ExamplesCmdError::Usage {
52 kind: UsageErrorKind::UnknownSubcommand {
53 domain: BuiltinDomain::Examples,
54 token: other.to_string(),
55 },
56 message: format!(
57 "unknown examples subcommand '{other}'\n{}",
58 examples_usage_message()
59 ),
60 }),
61 }
62}
63
64fn run_list(args: &[String]) -> Result<String, ExamplesCmdError> {
65 if wants_help(args) {
66 return Ok(examples_usage_message());
67 }
68 let json = parse_list_flags(args)?;
69 let share_root = resolve_wyvern_share();
70 let records = discover_examples(&share_root).map_err(map_discover)?;
71 if json {
72 serialize_records_json(&records)
73 } else {
74 Ok(format_examples_list(&records))
75 }
76}
77
78fn wants_help(args: &[String]) -> bool {
79 args.iter().any(|arg| arg == "--help" || arg == "-h")
80}
81
82fn parse_list_flags(args: &[String]) -> Result<bool, ExamplesCmdError> {
83 let mut json = false;
84 for arg in args {
85 match arg.as_str() {
86 "--json" => json = true,
87 other => return Err(unknown_flag(other)),
88 }
89 }
90 Ok(json)
91}
92
93fn serialize_records_json(records: &[ExampleRecord]) -> Result<String, ExamplesCmdError> {
94 match serde_json::to_string_pretty(records) {
95 Ok(mut text) => {
96 if !text.ends_with('\n') {
97 text.push('\n');
98 }
99 Ok(text)
100 }
101 Err(err) => Err(ExamplesCmdError::Emit(EmitError::Serialize(
102 SerializeError {
103 message: err.to_string(),
104 },
105 ))),
106 }
107}
108
109fn unknown_flag(flag: &str) -> ExamplesCmdError {
110 match StderrError::new(ErrorCode::ValidationError, format!("unknown flag '{flag}'"))
111 .cause("examples list accepts only --json")
112 .recovery("Run wyvern examples list")
113 .recovery("Run wyvern examples list --json")
114 .recovery("Run wyvern examples --help")
115 .docs("docs/wyvern/requirements.md")
116 .to_json_string()
117 {
118 Ok(stderr) => ExamplesCmdError::Stage {
119 stderr,
120 exit_code: ErrorCode::ValidationError.exit_code(),
121 },
122 Err(err) => ExamplesCmdError::Emit(EmitError::Serialize(err)),
123 }
124}
125
126fn map_discover(err: ExamplesDiscoverError) -> ExamplesCmdError {
127 match StderrError::new(ErrorCode::ValidationError, err.to_string())
128 .cause("Could not scan bundled example README files")
129 .recovery("Run wyvern examples list")
130 .recovery("Ensure {wyvern_share}/examples exists and README files are readable")
131 .docs("docs/wyvern/requirements.md")
132 .to_json_string()
133 {
134 Ok(stderr) => ExamplesCmdError::Stage {
135 stderr,
136 exit_code: ErrorCode::ValidationError.exit_code(),
137 },
138 Err(e) => ExamplesCmdError::Emit(EmitError::Serialize(e)),
139 }
140}
141
142#[derive(Debug)]
144pub enum ExamplesCmdError {
145 Usage {
147 kind: UsageErrorKind,
149 message: String,
151 },
152 Stage {
154 stderr: String,
156 exit_code: i32,
158 },
159 Emit(EmitError),
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn examples_help_mentions_list_and_frontmatter() {
169 let text = examples_usage_message();
170 assert!(text.contains("list"), "{text}");
171 assert!(text.contains("name:"), "{text}");
172 assert!(text.contains("description:"), "{text}");
173 }
174
175 #[test]
176 fn bare_examples_defaults_to_list() {
177 let text = run_examples_command(&[]).expect("list");
178 assert!(text.contains("README:") || text.is_empty(), "{text}");
179 }
180}