Skip to main content

escriba_api/
lib.rs

1//! `escriba-api` — OpenAPI 3.1 spec generator. Every public type with
2//! `schemars::JsonSchema` is emitted into the spec; every Command is a path.
3
4use 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}