1use serde::{Deserialize, Serialize};
2use tauri::path::BaseDirectory;
3
4use crate::dotpath;
5use crate::error::{Error, Result};
6
7pub const DEFAULT_SQLITE_DB_NAME: &str = "configurate.db";
8pub const DEFAULT_SQLITE_TABLE_NAME: &str = "configurate_configs";
9
10#[derive(Debug, Clone, Deserialize, Serialize)]
12#[serde(rename_all = "lowercase")]
13pub enum StorageFormat {
14 Json,
15 #[serde(alias = "yml")]
16 Yaml,
17 Binary,
18}
19
20#[derive(Debug, Clone, Deserialize, Serialize)]
22#[serde(rename_all = "lowercase")]
23pub enum ProviderKind {
24 Json,
25 Yml,
26 Binary,
27 Sqlite,
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct ProviderPayload {
34 pub kind: ProviderKind,
35 pub encryption_key: Option<String>,
36 pub db_name: Option<String>,
37 pub table_name: Option<String>,
38}
39
40#[derive(Debug, Clone, Deserialize, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct PathOptions {
44 pub dir_name: Option<String>,
45 pub current_path: Option<String>,
46}
47
48#[derive(Debug, Clone, Deserialize, Serialize)]
50pub struct KeyringEntry {
51 pub id: String,
53 pub dotpath: String,
55 pub value: String,
57}
58
59#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct KeyringOptions {
65 pub service: String,
67 pub account: String,
69}
70
71#[derive(Debug, Clone, Deserialize, Serialize)]
73#[serde(rename_all = "lowercase")]
74pub enum SqliteValueType {
75 String,
76 Number,
77 Boolean,
78}
79
80#[derive(Debug, Clone, Deserialize, Serialize)]
82#[serde(rename_all = "camelCase")]
83pub struct SqliteColumn {
84 pub column_name: String,
85 pub dotpath: String,
86 pub value_type: SqliteValueType,
87 #[serde(default)]
88 pub is_keyring: bool,
89}
90
91#[derive(Debug, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct ConfiguratePayload {
98 pub file_name: Option<String>,
100 pub base_dir: Option<BaseDirectory>,
101 pub options: Option<PathOptions>,
102 pub provider: Option<ProviderPayload>,
103 #[serde(default)]
104 pub schema_columns: Vec<SqliteColumn>,
105
106 pub name: Option<String>,
108 pub dir: Option<BaseDirectory>,
109 pub dir_name: Option<String>,
110 pub path: Option<String>,
111 pub format: Option<StorageFormat>,
112 pub encryption_key: Option<String>,
113
114 pub data: Option<serde_json::Value>,
116 pub keyring_entries: Option<Vec<KeyringEntry>>,
117 pub keyring_options: Option<KeyringOptions>,
118 #[serde(default)]
119 pub with_unlock: bool,
120 pub return_data: Option<bool>,
123}
124
125#[derive(Debug, Clone)]
131pub enum NormalizedProvider {
132 Json,
133 Yml,
134 Binary { encryption_key: Option<String> },
135 Sqlite { db_name: String, table_name: String },
136}
137
138#[derive(Debug, Clone)]
140pub struct NormalizedConfiguratePayload {
141 pub file_name: String,
142 pub base_dir: BaseDirectory,
143 pub dir_name: Option<String>,
144 pub current_path: Option<String>,
145 pub provider: NormalizedProvider,
146 pub schema_columns: Vec<SqliteColumn>,
147 pub data: Option<serde_json::Value>,
148 pub keyring_entries: Option<Vec<KeyringEntry>>,
149 pub keyring_options: Option<KeyringOptions>,
150 pub with_unlock: bool,
151 pub return_data: bool,
152}
153
154impl ConfiguratePayload {
155 pub fn normalize(self) -> Result<NormalizedConfiguratePayload> {
156 let file_name = self
157 .file_name
158 .or(self.name)
159 .ok_or_else(|| Error::InvalidPayload("missing fileName/name".to_string()))?;
160
161 let base_dir = self
162 .base_dir
163 .or(self.dir)
164 .ok_or_else(|| Error::InvalidPayload("missing baseDir/dir".to_string()))?;
165
166 let (dir_name, current_path) = match self.options {
167 Some(opts) => (opts.dir_name, opts.current_path),
168 None => (self.dir_name, self.path),
169 };
170
171 let provider = match self.provider {
172 Some(provider) => {
173 let is_binary_provider = matches!(&provider.kind, ProviderKind::Binary);
177 if !is_binary_provider
178 && (provider.encryption_key.is_some() || self.encryption_key.is_some())
179 {
180 return Err(Error::InvalidPayload(
181 "encryptionKey is only supported with provider.kind='binary'".to_string(),
182 ));
183 }
184
185 match provider.kind {
186 ProviderKind::Json => NormalizedProvider::Json,
187 ProviderKind::Yml => NormalizedProvider::Yml,
188 ProviderKind::Binary => NormalizedProvider::Binary {
189 encryption_key: provider.encryption_key.or(self.encryption_key),
190 },
191 ProviderKind::Sqlite => NormalizedProvider::Sqlite {
192 db_name: provider
193 .db_name
194 .unwrap_or_else(|| DEFAULT_SQLITE_DB_NAME.to_string()),
195 table_name: provider
196 .table_name
197 .unwrap_or_else(|| DEFAULT_SQLITE_TABLE_NAME.to_string()),
198 },
199 }
200 }
201 None => {
202 let format = self
204 .format
205 .ok_or_else(|| Error::InvalidPayload("missing provider/format".to_string()))?;
206
207 if !matches!(format, StorageFormat::Binary) && self.encryption_key.is_some() {
209 return Err(Error::InvalidPayload(
210 "encryptionKey is only supported with provider.kind='binary'".to_string(),
211 ));
212 }
213
214 match format {
215 StorageFormat::Json => NormalizedProvider::Json,
216 StorageFormat::Yaml => NormalizedProvider::Yml,
217 StorageFormat::Binary => NormalizedProvider::Binary {
218 encryption_key: self.encryption_key,
219 },
220 }
221 }
222 };
223
224 let schema_columns = self.schema_columns;
225
226 for column in &schema_columns {
229 dotpath::validate_path(&column.dotpath).map_err(|e| {
230 Error::InvalidPayload(format!(
231 "invalid dotpath in column '{}': {}",
232 column.column_name, e
233 ))
234 })?;
235 }
236
237 Ok(NormalizedConfiguratePayload {
238 file_name,
239 base_dir,
240 dir_name,
241 current_path,
242 provider,
243 schema_columns,
244 data: self.data,
245 keyring_entries: self.keyring_entries,
246 keyring_options: self.keyring_options,
247 with_unlock: self.with_unlock,
248 return_data: self.return_data.unwrap_or(true),
249 })
250 }
251}
252
253#[derive(Debug, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct UnlockPayload {
257 pub data: serde_json::Value,
258 pub keyring_entries: Option<Vec<KeyringEntry>>,
259 pub keyring_options: Option<KeyringOptions>,
260}
261
262#[derive(Debug, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct BatchEntryPayload {
266 pub id: String,
267 pub payload: ConfiguratePayload,
268}
269
270#[derive(Debug, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct BatchPayload {
274 pub entries: Vec<BatchEntryPayload>,
275}
276
277#[derive(Debug, Serialize)]
279pub struct BatchEntrySuccess {
280 pub ok: bool,
281 pub data: serde_json::Value,
282}
283
284#[derive(Debug, Serialize)]
286pub struct BatchEntryFailure {
287 pub ok: bool,
288 pub error: serde_json::Value,
289}
290
291#[derive(Debug, Serialize)]
293#[serde(untagged)]
294pub enum BatchEntryResult {
295 Success(BatchEntrySuccess),
296 Failure(BatchEntryFailure),
297}
298
299#[derive(Debug, Serialize)]
301pub struct BatchRunResult {
302 pub results: std::collections::BTreeMap<String, BatchEntryResult>,
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn base_payload() -> ConfiguratePayload {
310 ConfiguratePayload {
311 file_name: Some("app.json".to_string()),
312 base_dir: Some(BaseDirectory::AppConfig),
313 options: None,
314 provider: None,
315 schema_columns: Vec::new(),
316 name: None,
317 dir: None,
318 dir_name: None,
319 path: None,
320 format: None,
321 encryption_key: None,
322 data: None,
323 keyring_entries: None,
324 keyring_options: None,
325 with_unlock: false,
326 return_data: None,
327 }
328 }
329
330 #[test]
331 fn normalize_rejects_legacy_encryption_key_with_non_binary_provider() {
332 let mut payload = base_payload();
333 payload.provider = Some(ProviderPayload {
334 kind: ProviderKind::Json,
335 encryption_key: None,
336 db_name: None,
337 table_name: None,
338 });
339 payload.encryption_key = Some("legacy-key".to_string());
340
341 let err = payload.normalize().expect_err("expected invalid payload");
342 match err {
343 Error::InvalidPayload(msg) => {
344 assert_eq!(
345 msg,
346 "encryptionKey is only supported with provider.kind='binary'"
347 );
348 }
349 _ => panic!("unexpected error variant"),
350 }
351 }
352
353 #[test]
354 fn normalize_allows_legacy_encryption_key_with_binary_provider() {
355 let mut payload = base_payload();
356 payload.provider = Some(ProviderPayload {
357 kind: ProviderKind::Binary,
358 encryption_key: None,
359 db_name: None,
360 table_name: None,
361 });
362 payload.encryption_key = Some("legacy-key".to_string());
363
364 let normalized = payload.normalize().expect("expected valid payload");
365 match normalized.provider {
366 NormalizedProvider::Binary { encryption_key } => {
367 assert_eq!(encryption_key.as_deref(), Some("legacy-key"));
368 }
369 _ => panic!("unexpected provider variant"),
370 }
371 }
372}