Skip to main content

ssh_cli/cli/
schema_cmd.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-E2E-02: runtime JSON Schema catalog for agent discovery.
3#![forbid(unsafe_code)]
4//! Embed and emit JSON Schemas from `docs/schemas/` (compile-time include).
5//!
6//! Workload: pure memory lookup (sequential; no fan-out). One-shot: list or body.
7
8use crate::errors::{SshCliError, SshCliResult};
9
10/// Embedded schema catalog: `(name, file leaf, body)`.
11///
12/// Names omit `.schema.json` and match `docs/schemas/README.md`.
13const SCHEMAS: &[(&str, &str, &str)] = &[
14    (
15        "error-envelope",
16        "error-envelope.schema.json",
17        include_str!("../../docs/schemas/error-envelope.schema.json"),
18    ),
19    (
20        "exec",
21        "exec.schema.json",
22        include_str!("../../docs/schemas/exec.schema.json"),
23    ),
24    (
25        "exec-batch",
26        "exec-batch.schema.json",
27        include_str!("../../docs/schemas/exec-batch.schema.json"),
28    ),
29    (
30        "health-check",
31        "health-check.schema.json",
32        include_str!("../../docs/schemas/health-check.schema.json"),
33    ),
34    (
35        "health-check-batch",
36        "health-check-batch.schema.json",
37        include_str!("../../docs/schemas/health-check-batch.schema.json"),
38    ),
39    (
40        "scp-batch",
41        "scp-batch.schema.json",
42        include_str!("../../docs/schemas/scp-batch.schema.json"),
43    ),
44    (
45        "scp-transfer",
46        "scp-transfer.schema.json",
47        include_str!("../../docs/schemas/scp-transfer.schema.json"),
48    ),
49    (
50        "secrets-init",
51        "secrets-init.schema.json",
52        include_str!("../../docs/schemas/secrets-init.schema.json"),
53    ),
54    (
55        "secrets-reencrypt",
56        "secrets-reencrypt.schema.json",
57        include_str!("../../docs/schemas/secrets-reencrypt.schema.json"),
58    ),
59    (
60        "sftp-batch",
61        "sftp-batch.schema.json",
62        include_str!("../../docs/schemas/sftp-batch.schema.json"),
63    ),
64    (
65        "sftp-fs-op",
66        "sftp-fs-op.schema.json",
67        include_str!("../../docs/schemas/sftp-fs-op.schema.json"),
68    ),
69    (
70        "sftp-list",
71        "sftp-list.schema.json",
72        include_str!("../../docs/schemas/sftp-list.schema.json"),
73    ),
74    (
75        "sftp-transfer",
76        "sftp-transfer.schema.json",
77        include_str!("../../docs/schemas/sftp-transfer.schema.json"),
78    ),
79    (
80        "su-exec",
81        "su-exec.schema.json",
82        include_str!("../../docs/schemas/su-exec.schema.json"),
83    ),
84    (
85        "sudo-exec",
86        "sudo-exec.schema.json",
87        include_str!("../../docs/schemas/sudo-exec.schema.json"),
88    ),
89    (
90        "tunnel-listening",
91        "tunnel-listening.schema.json",
92        include_str!("../../docs/schemas/tunnel-listening.schema.json"),
93    ),
94    (
95        "vps-doctor",
96        "vps-doctor.schema.json",
97        include_str!("../../docs/schemas/vps-doctor.schema.json"),
98    ),
99    (
100        "vps-export",
101        "vps-export.schema.json",
102        include_str!("../../docs/schemas/vps-export.schema.json"),
103    ),
104    (
105        "vps-list",
106        "vps-list.schema.json",
107        include_str!("../../docs/schemas/vps-list.schema.json"),
108    ),
109    (
110        "vps-show",
111        "vps-show.schema.json",
112        include_str!("../../docs/schemas/vps-show.schema.json"),
113    ),
114];
115
116/// Runs `ssh-cli schema [NAME]`.
117///
118/// * No name → catalog JSON (`event: schema-catalog`)
119/// * Name → raw JSON Schema document body
120pub fn run_schema(name: Option<&str>, json: bool) -> SshCliResult<()> {
121    match name {
122        None => {
123            let items: Vec<serde_json::Value> = SCHEMAS
124                .iter()
125                .map(|(n, file, _)| {
126                    serde_json::json!({
127                        "name": n,
128                        "file": file,
129                    })
130                })
131                .collect();
132            if json {
133                crate::output::print_json_value(&serde_json::json!({
134                    "ok": true,
135                    "event": "schema-catalog",
136                    "schemas": items,
137                }))?;
138            } else {
139                for (n, file, _) in SCHEMAS {
140                    crate::output::write_line_fmt(format_args!("{n}\t{file}"))?;
141                }
142            }
143            Ok(())
144        }
145        Some(n) => {
146            let body = SCHEMAS
147                .iter()
148                .find(|(name, _, _)| *name == n)
149                .map(|(_, _, body)| *body)
150                .ok_or_else(|| {
151                    SshCliError::InvalidArgument(format!(
152                        "unknown schema '{n}'; run `ssh-cli schema` for the catalog"
153                    ))
154                })?;
155            // Schema body is already JSON; emit raw on stdout (agent contract).
156            crate::output::write_line(body.trim_end())?;
157            Ok(())
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn catalog_non_empty() {
168        assert!(SCHEMAS.len() >= 18);
169    }
170
171    #[test]
172    fn vps_list_present() {
173        assert!(SCHEMAS.iter().any(|(n, _, b)| *n == "vps-list" && b.contains("schema")));
174    }
175}