Skip to main content

drasi_source_sqlite/
descriptor.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! SQLite source plugin descriptor and configuration DTOs.
16
17use crate::config::{RestApiConfig, SqliteSourceConfig, TableKeyConfig};
18use drasi_plugin_sdk::prelude::*;
19use utoipa::OpenApi;
20
21// ── DTO types ────────────────────────────────────────────────────────────────
22
23/// SQLite source configuration DTO.
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
25#[schema(as = source::sqlite::SqliteSourceConfig)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct SqliteSourceConfigDto {
28    /// SQLite file path. Omit or set to null for an in-memory database.
29    #[serde(default)]
30    #[schema(value_type = Option<ConfigValueString>)]
31    pub path: Option<ConfigValue<String>>,
32
33    /// Optional explicit table allow-list. Omit or set to null for all user tables.
34    #[serde(default)]
35    pub tables: Option<Vec<String>>,
36
37    /// Optional explicit key config for element ID generation.
38    #[serde(default)]
39    #[schema(value_type = Vec<source::sqlite::TableKeyConfig>)]
40    pub table_keys: Vec<TableKeyConfigDto>,
41
42    /// Optional REST API configuration. When provided, CRUD endpoints are exposed.
43    #[serde(default)]
44    #[schema(value_type = Option<source::sqlite::RestApiConfig>)]
45    pub rest_api: Option<RestApiConfigDto>,
46}
47
48/// REST API configuration DTO.
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
50#[schema(as = source::sqlite::RestApiConfig)]
51#[serde(rename_all = "camelCase", deny_unknown_fields)]
52pub struct RestApiConfigDto {
53    /// Address to bind (default: "0.0.0.0").
54    #[serde(default = "default_rest_host")]
55    #[schema(value_type = ConfigValueString)]
56    pub host: ConfigValue<String>,
57
58    /// Port to bind (default: 8080).
59    #[serde(default = "default_rest_port")]
60    #[schema(value_type = ConfigValueU16)]
61    pub port: ConfigValue<u16>,
62}
63
64/// Table key configuration DTO.
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
66#[schema(as = source::sqlite::TableKeyConfig)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68pub struct TableKeyConfigDto {
69    pub table: String,
70    pub key_columns: Vec<String>,
71}
72
73// ── From impls ───────────────────────────────────────────────────────────────
74
75impl From<&TableKeyConfig> for TableKeyConfigDto {
76    fn from(tk: &TableKeyConfig) -> Self {
77        Self {
78            table: tk.table.clone(),
79            key_columns: tk.key_columns.clone(),
80        }
81    }
82}
83
84impl From<&RestApiConfig> for RestApiConfigDto {
85    fn from(config: &RestApiConfig) -> Self {
86        Self {
87            host: ConfigValue::Static(config.host.clone()),
88            port: ConfigValue::Static(config.port),
89        }
90    }
91}
92
93impl From<&SqliteSourceConfig> for SqliteSourceConfigDto {
94    fn from(config: &SqliteSourceConfig) -> Self {
95        Self {
96            path: config.path.as_ref().map(|p| ConfigValue::Static(p.clone())),
97            tables: config.tables.clone(),
98            table_keys: config
99                .table_keys
100                .iter()
101                .map(TableKeyConfigDto::from)
102                .collect(),
103            rest_api: config.rest_api.as_ref().map(RestApiConfigDto::from),
104        }
105    }
106}
107
108fn default_rest_host() -> ConfigValue<String> {
109    ConfigValue::Static("0.0.0.0".to_string())
110}
111
112fn default_rest_port() -> ConfigValue<u16> {
113    ConfigValue::Static(8080)
114}
115
116// ── OpenAPI schema ───────────────────────────────────────────────────────────
117
118#[derive(OpenApi)]
119#[openapi(components(schemas(SqliteSourceConfigDto, RestApiConfigDto, TableKeyConfigDto,)))]
120struct SqliteSourceSchemas;
121
122// ── Descriptor ───────────────────────────────────────────────────────────────
123
124/// Plugin descriptor for the SQLite source plugin.
125pub struct SqliteSourceDescriptor;
126
127#[async_trait]
128impl SourcePluginDescriptor for SqliteSourceDescriptor {
129    fn kind(&self) -> &str {
130        "sqlite"
131    }
132
133    fn config_version(&self) -> &str {
134        "1.0.0"
135    }
136
137    fn config_schema_name(&self) -> &str {
138        "source.sqlite.SqliteSourceConfig"
139    }
140
141    fn config_schema_json(&self) -> String {
142        let api = SqliteSourceSchemas::openapi();
143        serde_json::to_string(
144            &api.components
145                .as_ref()
146                .expect("OpenAPI components missing")
147                .schemas,
148        )
149        .expect("Failed to serialize config schema")
150    }
151
152    async fn create_source(
153        &self,
154        id: &str,
155        config_json: &serde_json::Value,
156        auto_start: bool,
157    ) -> anyhow::Result<Box<dyn drasi_lib::sources::Source>> {
158        let dto: SqliteSourceConfigDto = serde_json::from_value(config_json.clone())?;
159        let mapper = DtoMapper::new();
160
161        let path = match &dto.path {
162            Some(cv) => Some(mapper.resolve_string(cv).await?),
163            None => None,
164        };
165
166        let rest_api = match &dto.rest_api {
167            Some(rest_dto) => Some(RestApiConfig {
168                host: mapper.resolve_string(&rest_dto.host).await?,
169                port: mapper.resolve_typed(&rest_dto.port).await?,
170            }),
171            None => None,
172        };
173
174        let table_keys = dto
175            .table_keys
176            .iter()
177            .map(|tk| TableKeyConfig {
178                table: tk.table.clone(),
179                key_columns: tk.key_columns.clone(),
180            })
181            .collect();
182
183        let config = SqliteSourceConfig {
184            path,
185            tables: dto.tables,
186            table_keys,
187            rest_api,
188            ..Default::default()
189        };
190
191        let mut builder = crate::SqliteSourceBuilder::new(id);
192        if let Some(p) = &config.path {
193            builder = builder.with_path(p);
194        }
195        if let Some(tables) = config.tables {
196            builder = builder.with_tables(tables);
197        }
198        if !config.table_keys.is_empty() {
199            builder = builder.with_table_keys(config.table_keys);
200        }
201        if let Some(rest) = config.rest_api {
202            builder = builder.with_rest_api(rest);
203        }
204        builder = builder.auto_start(auto_start);
205
206        let mut source = builder.build()?;
207        source.base.set_raw_config(config_json.clone());
208
209        Ok(Box::new(source))
210    }
211}