Skip to main content

appctl_plugin_sdk/
schema.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4
5#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
6pub struct Schema {
7    pub source: SyncSource,
8    #[serde(default)]
9    pub base_url: Option<String>,
10    pub auth: AuthStrategy,
11    #[serde(default)]
12    pub resources: Vec<Resource>,
13    #[serde(default)]
14    pub metadata: Map<String, Value>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
18pub struct Resource {
19    pub name: String,
20    #[serde(default)]
21    pub description: Option<String>,
22    #[serde(default)]
23    pub fields: Vec<Field>,
24    #[serde(default)]
25    pub actions: Vec<Action>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
29#[serde(rename_all = "snake_case")]
30pub enum Provenance {
31    /// Route / operation was guessed from framework conventions (may 404).
32    #[default]
33    Inferred,
34    /// Declared by OpenAPI, explicit plugin, or introspected DB schema.
35    Declared,
36    /// Confirmed reachable by `appctl doctor` (HTTP probe).
37    Verified,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
41pub struct Action {
42    pub name: String,
43    #[serde(default)]
44    pub description: Option<String>,
45    pub verb: Verb,
46    pub transport: Transport,
47    #[serde(default)]
48    pub parameters: Vec<Field>,
49    pub safety: Safety,
50    #[serde(default)]
51    pub resource: Option<String>,
52    /// How we know this action exists (inferred routes are often wrong for non-API Django apps).
53    #[serde(default)]
54    pub provenance: Provenance,
55    #[serde(default)]
56    pub metadata: Map<String, Value>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60pub struct Field {
61    pub name: String,
62    #[serde(default)]
63    pub description: Option<String>,
64    pub field_type: FieldType,
65    #[serde(default)]
66    pub required: bool,
67    #[serde(default)]
68    pub location: Option<ParameterLocation>,
69    #[serde(default)]
70    pub default: Option<Value>,
71    #[serde(default)]
72    pub enum_values: Vec<Value>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
76#[serde(rename_all = "snake_case")]
77pub enum SyncSource {
78    Openapi,
79    Django,
80    Db,
81    Url,
82    Mcp,
83    Rails,
84    Laravel,
85    Aspnet,
86    Strapi,
87    Supabase,
88    Plugin,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "snake_case")]
93pub enum Verb {
94    List,
95    Get,
96    Create,
97    Update,
98    Delete,
99    Custom,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
103#[serde(rename_all = "snake_case")]
104pub enum Safety {
105    ReadOnly,
106    Mutating,
107    Destructive,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
111#[serde(tag = "kind", rename_all = "snake_case")]
112pub enum Transport {
113    Http {
114        method: HttpMethod,
115        path: String,
116        #[serde(default)]
117        query: Vec<String>,
118    },
119    Sql {
120        database_kind: DatabaseKind,
121        table: String,
122        operation: SqlOperation,
123        #[serde(default)]
124        primary_key: Option<String>,
125    },
126    Form {
127        method: HttpMethod,
128        action: String,
129    },
130    Mcp {
131        server_url: String,
132    },
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
136#[serde(rename_all = "snake_case")]
137pub enum DatabaseKind {
138    Postgres,
139    Mysql,
140    Sqlite,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
144#[serde(rename_all = "snake_case")]
145pub enum SqlOperation {
146    Select,
147    GetByPk,
148    Insert,
149    UpdateByPk,
150    DeleteByPk,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
154#[serde(rename_all = "UPPERCASE")]
155pub enum HttpMethod {
156    GET,
157    POST,
158    PUT,
159    PATCH,
160    DELETE,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
164#[serde(tag = "kind", rename_all = "snake_case")]
165pub enum AuthStrategy {
166    None,
167    ApiKey {
168        header: String,
169        env_ref: String,
170    },
171    Bearer {
172        env_ref: String,
173    },
174    Basic {
175        username_ref: String,
176        password_ref: String,
177    },
178    Cookie {
179        #[serde(default)]
180        env_ref: Option<String>,
181        #[serde(default)]
182        session_file: Option<String>,
183    },
184    OAuth2 {
185        #[serde(default)]
186        provider: Option<String>,
187        client_id_ref: String,
188        #[serde(default)]
189        client_secret_ref: Option<String>,
190        auth_url: String,
191        token_url: String,
192        #[serde(default)]
193        scopes: Vec<String>,
194        #[serde(default = "default_redirect_port")]
195        redirect_port: u16,
196    },
197}
198
199fn default_redirect_port() -> u16 {
200    8421
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum FieldType {
206    String,
207    Integer,
208    Number,
209    Boolean,
210    Object,
211    Array,
212    DateTime,
213    Date,
214    Uuid,
215    Json,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
219#[serde(rename_all = "snake_case")]
220pub enum ParameterLocation {
221    Path,
222    Query,
223    Body,
224    Header,
225}
226
227impl Schema {
228    pub fn action(&self, name: &str) -> Option<&Action> {
229        self.resources
230            .iter()
231            .flat_map(|resource| resource.actions.iter())
232            .find(|action| action.name == name)
233    }
234}
235
236impl FieldType {
237    pub fn from_openapi_type(ty: Option<&str>, format: Option<&str>) -> Self {
238        match (ty.unwrap_or("string"), format.unwrap_or_default()) {
239            ("integer", _) => Self::Integer,
240            ("number", _) => Self::Number,
241            ("boolean", _) => Self::Boolean,
242            ("object", _) => Self::Object,
243            ("array", _) => Self::Array,
244            ("string", "date-time") => Self::DateTime,
245            ("string", "date") => Self::Date,
246            ("string", "uuid") => Self::Uuid,
247            ("string", _) => Self::String,
248            _ => Self::Json,
249        }
250    }
251}