Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
264 linesCodeBlameRaw
1//! Every operation of the g1t API, as the CLI knows it.
2//!
3//! The table in `ops_table.rs` is generated from the API's own description,
4//! `apps/docs/src/data/openapi.json`, which apps/api keeps current. A test
5//! fails when the two differ; regenerate with
6//!
7//! ```sh
8//! G1T_WRITE_OPS=1 cargo test -p g1t ops_table
9//! ```
10//!
11//! Commands never spell out a path: they name the operation, and
12//! [`Client::call`](crate::client::Client::call) finds its method and path
13//! here. So a path that changes in the API changes in the CLI when the table
14//! is regenerated, and an operation that goes away fails the test that
15//! checks every command's operations exist ([`crate::mapping`]).
16
17pub use crate::ops_table::OPERATIONS;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum In {
21 Path,
22 Query,
23}
24
25#[derive(Debug)]
26pub struct Param {
27 pub name: &'static str,
28 pub location: In,
29 /// string, integer, number, boolean or array.
30 pub kind: &'static str,
31 pub required: bool,
32 /// The values it takes, when they are a fixed few.
33 pub values: &'static [&'static str],
34}
35
36#[derive(Debug)]
37pub struct Field {
38 pub name: &'static str,
39 pub kind: &'static str,
40 pub required: bool,
41 pub values: &'static [&'static str],
42}
43
44#[derive(Debug)]
45pub struct Operation {
46 pub id: &'static str,
47 pub method: &'static str,
48 pub path: &'static str,
49 pub tag: &'static str,
50 pub summary: &'static str,
51 /// The token scope it needs, when it needs one.
52 pub scope: Option<&'static str>,
53 pub params: &'static [Param],
54 pub body: &'static [Field],
55}
56
57impl Operation {
58 pub fn path_params(&self) -> impl Iterator<Item = &Param> {
59 self.params.iter().filter(|p| p.location == In::Path)
60 }
61
62 pub fn query_params(&self) -> impl Iterator<Item = &Param> {
63 self.params.iter().filter(|p| p.location == In::Query)
64 }
65}
66
67pub fn find(id: &str) -> Option<&'static Operation> {
68 OPERATIONS.iter().find(|op| op.id == id)
69}
70
71/// The operation, which must exist: commands name only operations the
72/// mapping test has checked.
73pub fn get(id: &str) -> &'static Operation {
74 find(id).unwrap_or_else(|| panic!("no operation {id}: regenerate crates/g1t/src/ops_table.rs"))
75}
76
77#[cfg(test)]
78pub mod generate {
79 //! Writes `ops_table.rs` from the OpenAPI document.
80
81 use serde_json::Value;
82 use std::fmt::Write as _;
83
84 pub fn openapi() -> Value {
85 let path = concat!(
86 env!("CARGO_MANIFEST_DIR"),
87 "/../../apps/docs/src/data/openapi.json"
88 );
89 let text = std::fs::read_to_string(path).expect("apps/docs/src/data/openapi.json");
90 serde_json::from_str(&text).expect("openapi.json parses")
91 }
92
93 fn kind(schema: &Value) -> String {
94 match &schema["type"] {
95 Value::String(s) => s.clone(),
96 Value::Array(list) => list
97 .iter()
98 .filter_map(Value::as_str)
99 .find(|t| *t != "null")
100 .unwrap_or("string")
101 .to_owned(),
102 _ if schema.get("oneOf").is_some() || schema.get("anyOf").is_some() => {
103 "string".to_owned()
104 }
105 _ => "object".to_owned(),
106 }
107 }
108
109 fn values(schema: &Value) -> Vec<String> {
110 schema["enum"]
111 .as_array()
112 .map(|list| {
113 list.iter()
114 .filter_map(Value::as_str)
115 .map(str::to_owned)
116 .collect()
117 })
118 .unwrap_or_default()
119 }
120
121 fn quoted(list: &[String]) -> String {
122 let inner: Vec<String> = list.iter().map(|v| format!("{v:?}")).collect();
123 format!("&[{}]", inner.join(", "))
124 }
125
126 pub fn table(doc: &Value) -> String {
127 struct Row {
128 tag: String,
129 id: String,
130 text: String,
131 }
132 let mut rows = Vec::new();
133 for (path, item) in doc["paths"].as_object().expect("paths") {
134 for (method, op) in item.as_object().expect("path item") {
135 if !["get", "post", "put", "patch", "delete"].contains(&method.as_str()) {
136 continue;
137 }
138 let id = op["operationId"].as_str().expect("operationId").to_owned();
139 let tag = op["tags"][0].as_str().unwrap_or("Other").to_owned();
140 let summary = op["summary"].as_str().unwrap_or("").to_owned();
141 let scope = match op["x-scope"].as_str() {
142 Some(s) => format!("Some({s:?})"),
143 None => "None".to_owned(),
144 };
145 let mut text = String::new();
146 writeln!(text, " Operation {{").unwrap();
147 writeln!(text, " id: {id:?},").unwrap();
148 writeln!(text, " method: {:?},", method.to_uppercase()).unwrap();
149 writeln!(text, " path: {path:?},").unwrap();
150 writeln!(text, " tag: {tag:?},").unwrap();
151 writeln!(text, " summary: {summary:?},").unwrap();
152 writeln!(text, " scope: {scope},").unwrap();
153 let params: Vec<&Value> = op["parameters"]
154 .as_array()
155 .map(|l| l.iter().collect())
156 .unwrap_or_default();
157 if params.is_empty() {
158 writeln!(text, " params: &[],").unwrap();
159 } else {
160 writeln!(text, " params: &[").unwrap();
161 for p in params {
162 let location = match p["in"].as_str() {
163 Some("path") => "In::Path",
164 Some("query") => "In::Query",
165 _ => continue,
166 };
167 writeln!(
168 text,
169 " Param {{ name: {:?}, location: {location}, kind: {:?}, required: {}, values: {} }},",
170 p["name"].as_str().unwrap_or(""),
171 kind(&p["schema"]),
172 p["required"].as_bool().unwrap_or(false),
173 quoted(&values(&p["schema"])),
174 )
175 .unwrap();
176 }
177 writeln!(text, " ],").unwrap();
178 }
179 let schema = &op["requestBody"]["content"]["application/json"]["schema"];
180 let required: Vec<&str> = schema["required"]
181 .as_array()
182 .map(|l| l.iter().filter_map(Value::as_str).collect())
183 .unwrap_or_default();
184 match schema["properties"].as_object() {
185 Some(props) if !props.is_empty() => {
186 writeln!(text, " body: &[").unwrap();
187 for (name, field) in props {
188 writeln!(
189 text,
190 " Field {{ name: {name:?}, kind: {:?}, required: {}, values: {} }},",
191 kind(field),
192 required.contains(&name.as_str()),
193 quoted(&values(field)),
194 )
195 .unwrap();
196 }
197 writeln!(text, " ],").unwrap();
198 }
199 _ => writeln!(text, " body: &[],").unwrap(),
200 }
201 writeln!(text, " }},").unwrap();
202 rows.push(Row { tag, id, text });
203 }
204 }
205 rows.sort_by(|a, b| (&a.tag, &a.id).cmp(&(&b.tag, &b.id)));
206 let mut out = String::from(
207 "// @generated from apps/docs/src/data/openapi.json by crates/g1t/src/ops.rs.\n\
208 // Do not edit: run `G1T_WRITE_OPS=1 cargo test -p g1t ops_table`.\n\n\
209 use crate::ops::{Field, In, Operation, Param};\n\n\
210 #[rustfmt::skip]\n\
211 pub static OPERATIONS: &[Operation] = &[\n",
212 );
213 for row in rows {
214 out.push_str(&row.text);
215 }
216 out.push_str("];\n");
217 out
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn ops_table_is_current() {
227 let doc = generate::openapi();
228 let want = generate::table(&doc);
229 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/ops_table.rs");
230 if std::env::var_os("G1T_WRITE_OPS").is_some() {
231 std::fs::write(path, &want).unwrap();
232 return;
233 }
234 let have = std::fs::read_to_string(path)
235 .unwrap_or_default()
236 .replace("\r\n", "\n");
237 assert!(
238 have == want,
239 "crates/g1t/src/ops_table.rs is out of date with apps/docs/src/data/openapi.json: run G1T_WRITE_OPS=1 cargo test -p g1t ops_table"
240 );
241 }
242
243 #[test]
244 fn ids_are_unique() {
245 let mut seen = std::collections::HashSet::new();
246 for op in OPERATIONS {
247 assert!(seen.insert(op.id), "{} twice", op.id);
248 }
249 }
250
251 #[test]
252 fn every_path_parameter_is_in_the_path() {
253 for op in OPERATIONS {
254 for p in op.path_params() {
255 assert!(
256 op.path.contains(&format!("{{{}}}", p.name)),
257 "{}: {}",
258 op.id,
259 p.name
260 );
261 }
262 }
263 }
264}