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    Flask,
81    Db,
82    Url,
83    Mcp,
84    Rails,
85    Laravel,
86    Aspnet,
87    Strapi,
88    Supabase,
89    Plugin,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum Verb {
95    List,
96    Get,
97    Create,
98    Update,
99    Delete,
100    Custom,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
104#[serde(rename_all = "snake_case")]
105pub enum Safety {
106    ReadOnly,
107    Mutating,
108    Destructive,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
112#[serde(tag = "kind", rename_all = "snake_case")]
113pub enum Transport {
114    Http {
115        method: HttpMethod,
116        path: String,
117        #[serde(default)]
118        query: Vec<String>,
119    },
120    Sql {
121        database_kind: DatabaseKind,
122        table: String,
123        operation: SqlOperation,
124        #[serde(default)]
125        primary_key: Option<String>,
126    },
127    NoSql {
128        database_kind: DatabaseKind,
129        collection: String,
130        operation: NoSqlOperation,
131        #[serde(default)]
132        primary_key: Option<String>,
133        #[serde(default)]
134        secondary_key: Option<String>,
135    },
136    Form {
137        method: HttpMethod,
138        action: String,
139    },
140    Mcp {
141        server_url: String,
142    },
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
146#[serde(rename_all = "snake_case")]
147pub enum DatabaseKind {
148    Postgres,
149    Mysql,
150    Sqlite,
151    Mongodb,
152    Redis,
153    Firestore,
154    Dynamodb,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
158#[serde(rename_all = "snake_case")]
159pub enum SqlOperation {
160    Select,
161    GetByPk,
162    Insert,
163    UpdateByPk,
164    DeleteByPk,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
168#[serde(rename_all = "snake_case")]
169pub enum NoSqlOperation {
170    List,
171    GetByPk,
172    Insert,
173    UpdateByPk,
174    DeleteByPk,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
178#[serde(rename_all = "UPPERCASE")]
179pub enum HttpMethod {
180    GET,
181    POST,
182    PUT,
183    PATCH,
184    DELETE,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
188#[serde(tag = "kind", rename_all = "snake_case")]
189pub enum AuthStrategy {
190    None,
191    ApiKey {
192        header: String,
193        env_ref: String,
194    },
195    Bearer {
196        env_ref: String,
197    },
198    Basic {
199        username_ref: String,
200        password_ref: String,
201    },
202    Cookie {
203        #[serde(default)]
204        env_ref: Option<String>,
205        #[serde(default)]
206        session_file: Option<String>,
207    },
208    OAuth2 {
209        #[serde(default)]
210        provider: Option<String>,
211        client_id_ref: String,
212        #[serde(default)]
213        client_secret_ref: Option<String>,
214        auth_url: String,
215        token_url: String,
216        #[serde(default)]
217        scopes: Vec<String>,
218        #[serde(default = "default_redirect_port")]
219        redirect_port: u16,
220    },
221}
222
223fn default_redirect_port() -> u16 {
224    8421
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
228#[serde(rename_all = "snake_case")]
229pub enum FieldType {
230    String,
231    Integer,
232    Number,
233    Boolean,
234    Object,
235    Array,
236    DateTime,
237    Date,
238    Uuid,
239    Json,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
243#[serde(rename_all = "snake_case")]
244pub enum ParameterLocation {
245    Path,
246    Query,
247    Body,
248    Header,
249}
250
251impl Schema {
252    pub fn action(&self, name: &str) -> Option<&Action> {
253        self.resources
254            .iter()
255            .flat_map(|resource| resource.actions.iter())
256            .find(|action| action.name == name)
257    }
258}
259
260impl FieldType {
261    pub fn from_openapi_type(ty: Option<&str>, format: Option<&str>) -> Self {
262        match (ty.unwrap_or("string"), format.unwrap_or_default()) {
263            ("integer", _) => Self::Integer,
264            ("number", _) => Self::Number,
265            ("boolean", _) => Self::Boolean,
266            ("object", _) => Self::Object,
267            ("array", _) => Self::Array,
268            ("string", "date-time") => Self::DateTime,
269            ("string", "date") => Self::Date,
270            ("string", "uuid") => Self::Uuid,
271            ("string", _) => Self::String,
272            _ => Self::Json,
273        }
274    }
275}