1use escriba_command::{CommandRegistry, CommandSpec};
5use escriba_config::{CommandDecl, EscribaConfig, KeymapDecl, MajorMode, MinorMode, PluginDecl};
6use escriba_core::{Action, Mode, Motion, Operator, Position, Range};
7use escriba_mode::ModalState;
8use schemars::schema_for;
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct OpenApiSpec(pub Value);
14
15impl OpenApiSpec {
16 #[must_use]
17 pub fn to_json_pretty(&self) -> String {
18 serde_json::to_string_pretty(&self.0).unwrap_or_default()
19 }
20
21 #[must_use]
22 pub fn to_yaml(&self) -> String {
23 serde_yaml::to_string(&self.0).unwrap_or_default()
24 }
25}
26
27#[must_use]
28pub fn build_spec() -> OpenApiSpec {
29 let mut schemas = serde_json::Map::new();
30 insert_schema(&mut schemas, "Position", schema_for!(Position));
31 insert_schema(&mut schemas, "Range", schema_for!(Range));
32 insert_schema(&mut schemas, "Mode", schema_for!(Mode));
33 insert_schema(&mut schemas, "Motion", schema_for!(Motion));
34 insert_schema(&mut schemas, "Operator", schema_for!(Operator));
35 insert_schema(&mut schemas, "Action", schema_for!(Action));
36 insert_schema(&mut schemas, "ModalState", schema_for!(ModalState));
37 insert_schema(&mut schemas, "EscribaConfig", schema_for!(EscribaConfig));
38 insert_schema(&mut schemas, "KeymapDecl", schema_for!(KeymapDecl));
39 insert_schema(&mut schemas, "CommandDecl", schema_for!(CommandDecl));
40 insert_schema(&mut schemas, "PluginDecl", schema_for!(PluginDecl));
41 insert_schema(&mut schemas, "MajorMode", schema_for!(MajorMode));
42 insert_schema(&mut schemas, "MinorMode", schema_for!(MinorMode));
43 insert_schema(&mut schemas, "CommandSpec", schema_for!(CommandSpec));
44
45 let commands = CommandRegistry::default_set().specs();
46 let mut paths = serde_json::Map::new();
47 for c in &commands {
48 paths.insert(
49 format!("/commands/{}", c.name),
50 json!({
51 "post": {
52 "summary": c.description,
53 "operationId": format!("run_{}", c.name.replace('-', "_")),
54 "tags": ["commands"],
55 "requestBody": {
56 "required": false,
57 "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } }
58 },
59 "responses": {
60 "200": { "description": "command executed" },
61 "404": { "description": "command not found" },
62 "500": { "description": "command failed" }
63 }
64 }
65 }),
66 );
67 }
68
69 let spec = json!({
70 "openapi": "3.1.0",
71 "info": {
72 "title": "escriba — the Rust + tatara-lisp editor",
73 "version": env!("CARGO_PKG_VERSION"),
74 "description": "Public API surface for escriba. Every type below is derived from a Rust struct annotated with #[derive(JsonSchema)] or #[derive(TataraDomain)]; this spec is the source of truth for every SDK, the MCP server, and the documentation site.",
75 "license": { "name": "MIT" }
76 },
77 "servers": [ { "url": "unix:///tmp/escriba.sock", "description": "local editor control socket" } ],
78 "paths": Value::Object(paths),
79 "components": { "schemas": Value::Object(schemas) }
80 });
81 OpenApiSpec(spec)
82}
83
84fn insert_schema(
85 map: &mut serde_json::Map<String, Value>,
86 name: &str,
87 schema: schemars::schema::RootSchema,
88) {
89 let v = serde_json::to_value(&schema).unwrap_or(Value::Null);
90 map.insert(name.to_string(), v);
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn spec_has_core_schemas() {
99 let s = build_spec();
100 let schemas = s.0["components"]["schemas"].as_object().unwrap();
101 for name in [
102 "Position",
103 "Range",
104 "Mode",
105 "Motion",
106 "Operator",
107 "Action",
108 "EscribaConfig",
109 "KeymapDecl",
110 "CommandDecl",
111 "PluginDecl",
112 "MajorMode",
113 "MinorMode",
114 ] {
115 assert!(schemas.contains_key(name), "missing schema: {name}");
116 }
117 }
118
119 #[test]
120 fn spec_has_command_paths() {
121 let s = build_spec();
122 let paths = s.0["paths"].as_object().unwrap();
123 assert!(paths.contains_key("/commands/save"));
124 }
125
126 #[test]
127 fn spec_version_matches_crate() {
128 let s = build_spec();
129 assert_eq!(
130 s.0["info"]["version"].as_str().unwrap(),
131 env!("CARGO_PKG_VERSION")
132 );
133 }
134
135 #[test]
136 fn spec_is_valid_openapi_3_1() {
137 let s = build_spec();
138 assert_eq!(s.0["openapi"].as_str().unwrap(), "3.1.0");
139 }
140}