1#![forbid(unsafe_code)]
4use crate::errors::{SshCliError, SshCliResult};
9
10const SCHEMAS: &[(&str, &str, &str)] = &[
14 (
15 "dry-run",
16 "dry-run.schema.json",
17 include_str!("../../docs/schemas/dry-run.schema.json"),
18 ),
19 (
20 "error-envelope",
21 "error-envelope.schema.json",
22 include_str!("../../docs/schemas/error-envelope.schema.json"),
23 ),
24 (
25 "exec",
26 "exec.schema.json",
27 include_str!("../../docs/schemas/exec.schema.json"),
28 ),
29 (
30 "exec-batch",
31 "exec-batch.schema.json",
32 include_str!("../../docs/schemas/exec-batch.schema.json"),
33 ),
34 (
35 "health-check",
36 "health-check.schema.json",
37 include_str!("../../docs/schemas/health-check.schema.json"),
38 ),
39 (
40 "health-check-batch",
41 "health-check-batch.schema.json",
42 include_str!("../../docs/schemas/health-check-batch.schema.json"),
43 ),
44 (
45 "scp-batch",
46 "scp-batch.schema.json",
47 include_str!("../../docs/schemas/scp-batch.schema.json"),
48 ),
49 (
50 "scp-transfer",
51 "scp-transfer.schema.json",
52 include_str!("../../docs/schemas/scp-transfer.schema.json"),
53 ),
54 (
55 "secrets-init",
56 "secrets-init.schema.json",
57 include_str!("../../docs/schemas/secrets-init.schema.json"),
58 ),
59 (
60 "secrets-reencrypt",
61 "secrets-reencrypt.schema.json",
62 include_str!("../../docs/schemas/secrets-reencrypt.schema.json"),
63 ),
64 (
65 "sftp-batch",
66 "sftp-batch.schema.json",
67 include_str!("../../docs/schemas/sftp-batch.schema.json"),
68 ),
69 (
70 "sftp-fs-op",
71 "sftp-fs-op.schema.json",
72 include_str!("../../docs/schemas/sftp-fs-op.schema.json"),
73 ),
74 (
75 "sftp-list",
76 "sftp-list.schema.json",
77 include_str!("../../docs/schemas/sftp-list.schema.json"),
78 ),
79 (
80 "sftp-transfer",
81 "sftp-transfer.schema.json",
82 include_str!("../../docs/schemas/sftp-transfer.schema.json"),
83 ),
84 (
85 "su-exec",
86 "su-exec.schema.json",
87 include_str!("../../docs/schemas/su-exec.schema.json"),
88 ),
89 (
90 "sudo-exec",
91 "sudo-exec.schema.json",
92 include_str!("../../docs/schemas/sudo-exec.schema.json"),
93 ),
94 (
95 "tunnel-closed",
96 "tunnel-closed.schema.json",
97 include_str!("../../docs/schemas/tunnel-closed.schema.json"),
98 ),
99 (
100 "tunnel-listening",
101 "tunnel-listening.schema.json",
102 include_str!("../../docs/schemas/tunnel-listening.schema.json"),
103 ),
104 (
105 "vps-doctor",
106 "vps-doctor.schema.json",
107 include_str!("../../docs/schemas/vps-doctor.schema.json"),
108 ),
109 (
110 "vps-export",
111 "vps-export.schema.json",
112 include_str!("../../docs/schemas/vps-export.schema.json"),
113 ),
114 (
115 "vps-list",
116 "vps-list.schema.json",
117 include_str!("../../docs/schemas/vps-list.schema.json"),
118 ),
119 (
120 "vps-show",
121 "vps-show.schema.json",
122 include_str!("../../docs/schemas/vps-show.schema.json"),
123 ),
124];
125
126pub fn run_schema(name: Option<&str>, json: bool) -> SshCliResult<()> {
131 match name {
132 None => {
133 let items: Vec<serde_json::Value> = SCHEMAS
134 .iter()
135 .map(|(n, file, _)| {
136 serde_json::json!({
137 "name": n,
138 "file": file,
139 })
140 })
141 .collect();
142 if json {
143 crate::output::print_json_value(&serde_json::json!({
144 "ok": true,
145 "event": "schema-catalog",
146 "schemas": items,
147 }))?;
148 } else {
149 for (n, file, _) in SCHEMAS {
150 crate::output::write_line_fmt(format_args!("{n}\t{file}"))?;
151 }
152 }
153 Ok(())
154 }
155 Some(n) => {
156 let body = SCHEMAS
157 .iter()
158 .find(|(name, _, _)| *name == n)
159 .map(|(_, _, body)| *body)
160 .ok_or_else(|| {
161 SshCliError::InvalidArgument(format!(
162 "unknown schema '{n}'; run `ssh-cli schema` for the catalog"
163 ))
164 })?;
165 crate::output::write_line(body.trim_end())?;
167 Ok(())
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn catalog_non_empty() {
178 assert!(SCHEMAS.len() >= 22);
179 }
180
181 #[test]
182 fn vps_list_present() {
183 assert!(SCHEMAS
184 .iter()
185 .any(|(n, _, b)| *n == "vps-list" && b.contains("schema")));
186 }
187
188 #[test]
201 fn catalog_and_disk_agree() {
202 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/schemas");
203
204 let mut on_disk: Vec<String> = std::fs::read_dir(&dir)
205 .expect("read docs/schemas")
206 .flatten()
207 .filter_map(|e| e.file_name().to_str().map(str::to_owned))
208 .filter(|f| f.ends_with(".schema.json"))
209 .collect();
210 on_disk.sort();
211
212 let mut in_catalog: Vec<String> = SCHEMAS
213 .iter()
214 .map(|(_, file, _)| (*file).to_owned())
215 .collect();
216 in_catalog.sort();
217
218 let missing: Vec<&String> = on_disk.iter().filter(|f| !in_catalog.contains(f)).collect();
219 assert!(
220 missing.is_empty(),
221 "schemas published on disk but absent from the runtime catalog: {missing:?}"
222 );
223
224 let stale: Vec<&String> = in_catalog.iter().filter(|f| !on_disk.contains(f)).collect();
225 assert!(
226 stale.is_empty(),
227 "catalog entries whose file no longer exists on disk: {stale:?}"
228 );
229
230 for (name, file, _) in SCHEMAS {
232 assert_eq!(
233 *file,
234 format!("{name}.schema.json"),
235 "catalog name '{name}' does not match its file leaf '{file}'"
236 );
237 }
238 }
239}