pr_01m47d15m3e54sn21z27rpy5n9/apps/api/src/openapi.rs

327 lines12,586 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API and MCP server in Rust; a public index at the API root1//! The OpenAPI document, generated from the same list the routes are.
2
3use serde_json::{Map, Value, json};
4
5use crate::operations::Op;
6use crate::rest::{ROUTES, Route};
7
8/// The section of the API reference an operation is listed under.
9fn tag(op: Op) -> &'static str {
10 let name = op.name();
Webhooks: every event, to your own addresses, signed and retried11 if name.contains("webhook") {
12 "Webhooks"
13 } else if name.contains("integration") || name.contains("model_routes") || op == Op::GetContext {
Integrations: your own model provider, alerts that open issues, tickets agents read14 "Integrations"
15 } else if op == Op::Whoami || name.contains("workspace") {
API and MCP server in Rust; a public index at the API root16 "Accounts"
17 } else if name.contains("session") {
18 "Sessions"
19 } else if name.contains("pull_request") {
20 "Pull requests"
21 } else if ["issue", "label", "comment"]
22 .iter()
23 .any(|word| name.contains(word))
24 {
25 "Issues"
26 } else {
27 "Repositories"
28 }
29}
30
31/// A short title from an operation name: `create_issue` is "Create issue".
32fn title(op: Op) -> String {
33 if op == Op::Whoami {
34 return "Get the current user".to_owned();
35 }
36 let words = op.name().replace('_', " ");
37 let mut letters = words.chars();
38 match letters.next() {
39 Some(first) => first.to_uppercase().chain(letters).collect(),
40 None => words,
41 }
42}
43
Agents as a team: lifecycle, merge queue, billing and a new shell44/// `/repos/:owner/:name` as OpenAPI writes it: `/repos/{owner}/{name}`.
API and MCP server in Rust; a public index at the API root45fn openapi_path(route: &Route) -> String {
46 route
47 .path
48 .split('/')
49 .map(|segment| match segment.strip_prefix(':') {
50 Some(name) => format!("{{{name}}}"),
51 None => segment.to_owned(),
52 })
53 .collect::<Vec<_>>()
54 .join("/")
55}
56
57fn error_response(description: &str) -> Value {
58 json!({
59 "description": description,
60 "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } },
61 })
62}
63
64fn operation(route: &Route) -> Value {
65 let op = route.op;
66 let path_params: Vec<&str> = route.params().collect();
67 // `owner` and `name` in the path stand for the operation's `repo` input.
68 let covered = |name: &str| name == "repo" || path_params.contains(&name);
69 let mut properties = op.properties();
70 properties.retain(|name, _| !covered(name));
71 let required: Vec<String> = op
72 .required()
73 .into_iter()
74 .filter(|name| !covered(name))
75 .collect();
76
77 let mut parameters: Vec<Value> = path_params
78 .iter()
79 .map(|name| {
80 json!({
81 "name": name,
82 "in": "path",
83 "required": true,
84 "schema": { "type": if *name == "number" { "integer" } else { "string" } },
85 })
86 })
87 .collect();
88 let mut body = Value::Null;
89 if route.method == "GET" {
90 for (name, key) in route.query {
91 parameters.push(json!({
92 "name": name,
93 "in": "query",
94 "required": false,
95 "schema": properties.get(*key).cloned().unwrap_or_else(|| json!({})),
96 }));
97 }
98 } else if !properties.is_empty() {
99 let mut schema = json!({ "type": "object", "properties": properties });
100 if !required.is_empty() {
101 schema["required"] = json!(required);
102 }
103 body = json!({
104 "required": !required.is_empty(),
105 "content": { "application/json": { "schema": schema } },
106 });
107 }
108
Webhooks: every event, to your own addresses, signed and retried109 // An operation reached at a workspace's address as well as a
110 // repository's is documented once for each, with its own id.
111 let id = if route.path.starts_with("/workspaces/") && ROUTES.iter().any(|other| other.op == op && other.path.starts_with("/repos/")) {
112 format!("{}_for_workspace", op.name())
113 } else {
114 op.name().to_owned()
115 };
API and MCP server in Rust; a public index at the API root116 let mut described = json!({
Webhooks: every event, to your own addresses, signed and retried117 "operationId": id,
API and MCP server in Rust; a public index at the API root118 "tags": [tag(op)],
119 "summary": title(op),
120 "description": op.description(),
121 "parameters": parameters,
122 "responses": {
123 "200": {
124 "description": "Success.",
125 "content": { "application/json": { "schema": {} } },
126 },
127 "401": error_response("A token is required, or the one sent is not valid."),
128 "403": error_response("Signed in, but not allowed to do this."),
129 "404": error_response("It does not exist, or you cannot see it."),
130 "409": error_response("The request conflicts with the current state."),
131 "422": error_response("The input is not valid."),
132 },
133 });
134 if !body.is_null() {
135 described["requestBody"] = body;
136 }
137 described
138}
139
140/// Entries for device sign-in, which is not an operation.
141fn onboarding() -> Map<String, Value> {
142 let paths = json!({
Agents as a team: lifecycle, merge queue, billing and a new shell143 "/device/code": {
API and MCP server in Rust; a public index at the API root144 "post": {
145 "operationId": "device_code",
146 "tags": ["Accounts"],
147 "summary": "Start signing in",
Agents as a team: lifecycle, merge queue, billing and a new shell148 "description": "Begins a device sign-in. Show the person `verification_uri_complete` and have them open it in a browser, where they sign in or register and approve the code. Then poll `/device/token`.",
API and MCP server in Rust; a public index at the API root149 "security": [],
150 "requestBody": {
151 "content": { "application/json": { "schema": {
152 "type": "object",
153 "properties": {
154 "client_name": {
155 "type": "string",
156 "description": "What is asking, shown to the person approving. For example, Claude Code.",
157 },
158 },
159 } } },
160 },
161 "responses": { "200": {
162 "description": "The codes for this sign-in.",
163 "content": { "application/json": { "schema": {
164 "type": "object",
165 "properties": {
Agents as a team: lifecycle, merge queue, billing and a new shell166 "device_code": { "type": "string", "description": "Secret. Send it to /device/token." },
API and MCP server in Rust; a public index at the API root167 "user_code": { "type": "string", "description": "Shown to the person, like WDJB-MJHT." },
168 "verification_uri": { "type": "string" },
169 "verification_uri_complete": {
170 "type": "string",
171 "description": "The link to give the person; it carries the code.",
172 },
173 "expires_in": { "type": "integer", "description": "Seconds until the codes expire." },
174 "interval": { "type": "integer", "description": "Seconds to wait between polls." },
175 },
176 } } },
177 } },
178 },
179 },
Agents as a team: lifecycle, merge queue, billing and a new shell180 "/device/token": {
API and MCP server in Rust; a public index at the API root181 "post": {
182 "operationId": "device_token",
183 "tags": ["Accounts"],
184 "summary": "Finish signing in",
185 "description": "Asks whether the person has approved. Poll no faster than the interval. The token is returned once.",
186 "security": [],
187 "requestBody": {
188 "required": true,
189 "content": { "application/json": { "schema": {
190 "type": "object",
191 "required": ["device_code"],
192 "properties": { "device_code": { "type": "string" } },
193 } } },
194 },
195 "responses": { "200": {
196 "description": "The state of the sign-in.",
197 "content": { "application/json": { "schema": {
198 "type": "object",
199 "required": ["status"],
200 "properties": {
201 "status": { "type": "string", "enum": ["pending", "approved", "denied", "expired"] },
202 "token": { "type": "string", "description": "Present when approved." },
203 "username": { "type": "string" },
204 "verified": {
205 "type": "boolean",
206 "description": "Whether the account's email is confirmed.",
207 },
208 },
209 } } },
210 } },
211 },
212 },
213 });
214 match paths {
215 Value::Object(paths) => paths,
216 _ => Map::new(),
217 }
218}
219
220pub fn document() -> Value {
221 let mut paths = onboarding();
222 for route in ROUTES {
223 let entry = paths
224 .entry(openapi_path(route))
225 .or_insert_with(|| json!({}));
226 entry[route.method.to_lowercase()] = operation(route);
227 }
228 json!({
229 "openapi": "3.1.0",
230 "info": {
231 "title": "g1t API",
232 "version": "1",
233 "description": "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh.",
234 "license": { "name": "MIT", "identifier": "MIT" },
235 },
236 "servers": [{ "url": "https://api.g1t.sh" }],
237 "security": [{ "token": [] }, {}],
238 "tags": [
239 { "name": "Accounts", "description": "Signing in from a tool, and the current user." },
240 { "name": "Repositories" },
241 {
242 "name": "Issues",
243 "description": "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.",
244 },
245 {
246 "name": "Pull requests",
247 "description": "A proposed change in its own fork or on a branch. Several can be made for one issue; the one merged resolves it.",
248 },
249 { "name": "Sessions", "description": "The record of how a pull request was made." },
250 ],
251 "paths": paths,
252 "components": {
253 "securitySchemes": {
254 "token": {
255 "type": "http",
256 "scheme": "bearer",
257 "description": "An access token, `g1t_…`. Public data needs none.",
258 },
259 },
260 "schemas": {
261 "Error": {
262 "type": "object",
263 "required": ["error"],
264 "properties": {
265 "error": {
266 "type": "object",
267 "required": ["code", "message"],
268 "properties": {
269 "code": {
270 "type": "string",
271 "enum": ["unauthenticated", "forbidden", "not_found", "conflict", "invalid"],
272 },
273 "message": { "type": "string" },
274 },
275 },
276 },
277 },
278 },
279 },
280 })
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn every_route_is_documented_once() {
289 let document = document();
290 let mut ids = Vec::new();
291 for (_, methods) in document["paths"].as_object().unwrap() {
292 for (_, operation) in methods.as_object().unwrap() {
293 ids.push(operation["operationId"].as_str().unwrap().to_owned());
294 }
295 }
296 for op in Op::ALL {
297 assert_eq!(
298 ids.iter().filter(|id| *id == op.name()).count(),
299 1,
300 "{}",
301 op.name()
302 );
303 }
Webhooks: every event, to your own addresses, signed and retried304 let mut unique = ids.clone();
305 unique.sort();
306 unique.dedup();
307 assert_eq!(unique.len(), ids.len(), "operation ids repeat");
API and MCP server in Rust; a public index at the API root308 }
309
310 #[test]
311 fn path_and_query_inputs_are_not_repeated_in_the_body() {
312 let document = document();
Agents as a team: lifecycle, merge queue, billing and a new shell313 let merge = &document["paths"]["/repos/{owner}/{name}/pulls/{number}/merge"]["post"];
API and MCP server in Rust; a public index at the API root314 let body = &merge["requestBody"]["content"]["application/json"]["schema"]["properties"];
315 assert!(body.get("keep_issue_open").is_some());
316 assert!(body.get("repo").is_none() && body.get("number").is_none());
Agents as a team: lifecycle, merge queue, billing and a new shell317 let list = &document["paths"]["/repos"]["get"];
API and MCP server in Rust; a public index at the API root318 assert_eq!(list["parameters"][0]["name"], "q");
319 assert!(list.get("requestBody").is_none());
320 }
321
322 #[test]
323 fn titles_read_as_sentences() {
324 assert_eq!(title(Op::CreateIssue), "Create issue");
325 assert_eq!(title(Op::Whoami), "Get the current user");
326 }
327}