secret_store_sdk/
models.rs

1//! Data models for the XJP Secret Store SDK
2//!
3//! This module contains all the data structures used for API requests and responses.
4//! The models are designed to provide a safe and ergonomic interface while mapping
5//! cleanly to the underlying API.
6//!
7//! # Key Types
8//!
9//! * [`Secret`] - The main type representing a secret with its value and metadata
10//! * [`GetOpts`], [`PutOpts`], [`ListOpts`] - Options for various operations
11//! * [`BatchOp`] - Batch operation definitions
12//! * [`ExportFormat`] - Supported export formats for environment variables
13
14use secrecy::SecretString;
15use serde::{Deserialize, Serialize};
16
17/// A secret value with metadata
18///
19/// This is the main type returned when retrieving secrets from the store.
20/// The secret value itself is protected using [`SecretString`] to prevent
21/// accidental exposure in logs or debug output.
22///
23/// # Example
24///
25/// ```no_run
26/// # use secret_store_sdk::{Client, ClientBuilder, Auth};
27/// # use secrecy::ExposeSecret;
28/// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
29/// let secret = client.get_secret("prod", "api-key", Default::default()).await?;
30///
31/// // Access the protected value
32/// let value = secret.value.expose_secret();
33///
34/// // Check metadata
35/// if let Some(owner) = secret.metadata.get("owner") {
36///     println!("Secret owned by: {}", owner);
37/// }
38///
39/// // Use ETag for conditional requests
40/// if let Some(etag) = &secret.etag {
41///     println!("ETag: {}", etag);
42/// }
43/// # Ok(())
44/// # }
45/// ```
46#[derive(Debug, Clone)]
47pub struct Secret {
48    /// Namespace the secret belongs to
49    pub namespace: String,
50    /// Key name
51    pub key: String,
52    /// Secret value (protected)
53    pub value: SecretString,
54    /// Version number
55    pub version: i32,
56    /// Optional expiration time
57    pub expires_at: Option<time::OffsetDateTime>,
58    /// JSON metadata
59    pub metadata: serde_json::Value,
60    /// Last update time
61    pub updated_at: time::OffsetDateTime,
62    /// ETag from response header
63    pub etag: Option<String>,
64    /// Last-Modified from response header
65    pub last_modified: Option<String>,
66    /// Request ID from response header
67    pub request_id: Option<String>,
68}
69
70/// Secret key info in list responses
71#[derive(Debug, Clone, Deserialize, Serialize)]
72pub struct SecretKeyInfo {
73    /// Key name
74    pub key: String,
75    /// Version number (from API)
76    pub version: i32,
77    /// Last update time
78    pub updated_at: String,
79    /// Optional KID
80    pub kid: Option<String>,
81}
82
83/// Options for getting a secret
84///
85/// Controls caching behavior and conditional requests when retrieving secrets.
86///
87/// # Example
88///
89/// ```
90/// use secret_store_sdk::GetOpts;
91///
92/// // Use defaults (cache enabled)
93/// let opts = GetOpts::default();
94///
95/// // Disable cache for this request
96/// let opts = GetOpts {
97///     use_cache: false,
98///     ..Default::default()
99/// };
100///
101/// // Conditional request with ETag
102/// let opts = GetOpts {
103///     if_none_match: Some("\"123abc\"".to_string()),
104///     ..Default::default()
105/// };
106/// ```
107#[derive(Debug, Clone)]
108pub struct GetOpts {
109    /// Whether to use cache (default: true)
110    pub use_cache: bool,
111    /// If-None-Match header value for conditional requests
112    pub if_none_match: Option<String>,
113    /// If-Modified-Since header value for conditional requests
114    pub if_modified_since: Option<String>,
115}
116
117impl Default for GetOpts {
118    fn default() -> Self {
119        Self {
120            use_cache: true,
121            if_none_match: None,
122            if_modified_since: None,
123        }
124    }
125}
126
127/// Options for putting a secret
128///
129/// Allows setting TTL, metadata, and idempotency key when creating or updating secrets.
130///
131/// # Example
132///
133/// ```
134/// use secret_store_sdk::PutOpts;
135/// use serde_json::json;
136///
137/// // Simple put with defaults
138/// let opts = PutOpts::default();
139///
140/// // Put with TTL and metadata
141/// let opts = PutOpts {
142///     ttl_seconds: Some(3600), // 1 hour
143///     metadata: Some(json!({
144///         "environment": "production",
145///         "rotation_policy": "30d",
146///         "owner": "backend-team"
147///     })),
148///     idempotency_key: Some("deploy-12345".to_string()),
149/// };
150/// ```
151#[derive(Debug, Clone, Default)]
152pub struct PutOpts {
153    /// TTL in seconds (secret will be auto-deleted after this time)
154    pub ttl_seconds: Option<i64>,
155    /// JSON metadata to attach to the secret
156    pub metadata: Option<serde_json::Value>,
157    /// Idempotency key to ensure exactly-once semantics
158    pub idempotency_key: Option<String>,
159}
160
161/// Result of put operation
162#[derive(Debug, Clone, Deserialize)]
163pub struct PutResult {
164    /// Success message
165    pub message: String,
166    /// Namespace
167    pub namespace: String,
168    /// Key
169    pub key: String,
170    /// Creation timestamp
171    pub created_at: String,
172    /// Request ID
173    pub request_id: String,
174}
175
176/// Result of delete operation
177#[derive(Debug, Clone)]
178pub struct DeleteResult {
179    /// Whether the secret was deleted
180    pub deleted: bool,
181    /// Request ID if available
182    pub request_id: Option<String>,
183}
184
185/// Options for listing secrets
186#[derive(Debug, Clone, Default)]
187pub struct ListOpts {
188    /// Key prefix to filter by
189    pub prefix: Option<String>,
190    /// Maximum number of results
191    pub limit: Option<usize>,
192}
193
194/// Result of list operation
195#[derive(Debug, Clone, Deserialize)]
196pub struct ListSecretsResult {
197    /// Namespace
198    pub namespace: String,
199    /// List of secrets
200    pub secrets: Vec<SecretKeyInfo>,
201    /// Total count
202    pub total: usize,
203    /// Limit used (optional in response)
204    #[serde(default)]
205    pub limit: usize,
206    /// Whether there are more results (optional in response)
207    #[serde(default)]
208    pub has_more: bool,
209    /// Request ID (optional in response)
210    #[serde(default)]
211    pub request_id: Option<String>,
212}
213
214/// Export format for batch operations
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
216pub enum ExportFormat {
217    /// JSON format
218    #[default]
219    Json,
220    /// .env file format
221    Dotenv,
222    /// Shell export format
223    Shell,
224    /// Docker compose format
225    DockerCompose,
226}
227
228impl ExportFormat {
229    /// Get the format string for API parameter
230    pub fn as_str(&self) -> &'static str {
231        match self {
232            ExportFormat::Json => "json",
233            ExportFormat::Dotenv => "dotenv",
234            ExportFormat::Shell => "shell",
235            ExportFormat::DockerCompose => "docker-compose",
236        }
237    }
238}
239
240/// Keys for batch get operation
241#[derive(Debug, Clone)]
242pub enum BatchKeys {
243    /// Specific keys
244    Keys(Vec<String>),
245    /// All keys (wildcard)
246    All,
247}
248
249// Implementation of batch and advanced operations types
250
251/// Result of batch get operation
252#[derive(Debug, Clone)]
253pub enum BatchGetResult {
254    /// JSON format with all secrets
255    Json(BatchGetJsonResult),
256    /// Text format (dotenv, shell, docker-compose)
257    Text(String),
258}
259
260/// Batch get result in JSON format
261#[derive(Debug, Clone, Deserialize, Serialize)]
262pub struct BatchGetJsonResult {
263    /// Namespace
264    pub namespace: String,
265    /// Map of key to secret value
266    pub secrets: std::collections::HashMap<String, String>,
267    /// List of missing keys
268    #[serde(default)]
269    pub missing: Vec<String>,
270    /// Total number of secrets
271    pub total: usize,
272    /// Request ID
273    pub request_id: String,
274}
275
276/// Batch operation
277#[derive(Debug, Clone, Serialize)]
278pub struct BatchOp {
279    /// Action type: "put" or "delete"
280    pub action: String,
281    /// Secret key
282    pub key: String,
283    /// Value (required for "put" action)
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub value: Option<String>,
286    /// TTL in seconds (optional for "put" action)
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub ttl_seconds: Option<i64>,
289    /// Metadata (optional for "put" action)
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub metadata: Option<serde_json::Value>,
292}
293
294impl BatchOp {
295    /// Create a put operation
296    pub fn put(key: impl Into<String>, value: impl Into<String>) -> Self {
297        Self {
298            action: "put".to_string(),
299            key: key.into(),
300            value: Some(value.into()),
301            ttl_seconds: None,
302            metadata: None,
303        }
304    }
305
306    /// Create a delete operation
307    pub fn delete(key: impl Into<String>) -> Self {
308        Self {
309            action: "delete".to_string(),
310            key: key.into(),
311            value: None,
312            ttl_seconds: None,
313            metadata: None,
314        }
315    }
316
317    /// Create a read operation
318    ///
319    /// Read operations retrieve the secret value without modifying it.
320    /// This is useful in batch operations where you need to read multiple secrets.
321    pub fn read(key: impl Into<String>) -> Self {
322        Self {
323            action: "read".to_string(),
324            key: key.into(),
325            value: None,
326            ttl_seconds: None,
327            metadata: None,
328        }
329    }
330
331    /// Set TTL for a put operation
332    pub fn with_ttl(mut self, ttl_seconds: i64) -> Self {
333        self.ttl_seconds = Some(ttl_seconds);
334        self
335    }
336
337    /// Set metadata for a put operation
338    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
339        self.metadata = Some(metadata);
340        self
341    }
342}
343
344/// Result of batch operations
345#[derive(Debug, Clone, Deserialize)]
346pub struct BatchOperateResult {
347    /// Namespace
348    pub namespace: String,
349    /// Results summary
350    pub results: BatchResultSummary,
351    /// Success rate
352    pub success_rate: f64,
353}
354
355/// Batch results summary
356#[derive(Debug, Clone, Deserialize)]
357pub struct BatchResultSummary {
358    /// Successful operations
359    pub succeeded: Vec<BatchOperationResult>,
360    /// Failed operations  
361    pub failed: Vec<BatchOperationResult>,
362    /// Total operations
363    pub total: usize,
364}
365
366/// Individual operation result in batch
367#[derive(Debug, Clone, Deserialize)]
368pub struct BatchOperationResult {
369    /// Key affected
370    pub key: String,
371    /// Action performed
372    pub action: String,
373    /// Whether the operation succeeded
374    pub success: bool,
375    /// Error message if failed
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub error: Option<String>,
378}
379
380/// Environment export result
381#[derive(Debug, Clone)]
382pub enum EnvExport {
383    /// JSON format
384    Json(EnvJsonExport),
385    /// Text format (dotenv, shell, docker-compose)
386    Text(String),
387}
388
389/// Options for environment export
390#[derive(Debug, Clone, Default)]
391pub struct ExportEnvOpts {
392    /// Export format
393    pub format: ExportFormat,
394    /// Enable caching for this request (currently not implemented, reserved for future use)
395    pub use_cache: bool,
396    /// If-None-Match header value for conditional requests
397    pub if_none_match: Option<String>,
398}
399
400/// Environment export in JSON format
401#[derive(Debug, Clone, Deserialize)]
402pub struct EnvJsonExport {
403    /// Namespace
404    pub namespace: String,
405    /// Environment variables
406    pub environment: std::collections::HashMap<String, String>,
407    /// ETag
408    pub etag: String,
409    /// Total count
410    pub total: usize,
411    /// Request ID
412    pub request_id: String,
413}
414
415/// List of namespaces
416#[derive(Debug, Clone, Deserialize)]
417pub struct ListNamespacesResult {
418    /// List of namespaces
419    pub namespaces: Vec<NamespaceListItem>,
420    /// Total count
421    pub total: usize,
422    /// Request ID
423    pub request_id: String,
424}
425
426/// Namespace list item
427#[derive(Debug, Clone, Deserialize)]
428pub struct NamespaceListItem {
429    /// Namespace name
430    pub name: String,
431    /// Creation time
432    pub created_at: String,
433    /// Last updated time
434    pub updated_at: String,
435    /// Number of secrets
436    pub secret_count: usize,
437}
438
439/// Namespace detailed information
440#[derive(Debug, Clone, Deserialize)]
441pub struct NamespaceInfo {
442    /// Namespace name
443    pub name: String,
444    /// Creation time
445    pub created_at: String,
446    /// Last updated time
447    pub updated_at: String,
448    /// Number of secrets
449    pub secret_count: usize,
450    /// Total size in bytes
451    pub total_size: usize,
452    /// Metadata
453    #[serde(default)]
454    pub metadata: serde_json::Value,
455    /// Request ID
456    pub request_id: String,
457}
458
459/// Namespace template for initialization
460#[derive(Debug, Clone, Serialize, Default)]
461pub struct NamespaceTemplate {
462    /// Template name
463    pub template: String,
464    /// Additional parameters
465    #[serde(flatten)]
466    pub params: serde_json::Value,
467}
468
469/// Result of namespace initialization
470#[derive(Debug, Clone, Deserialize)]
471pub struct InitNamespaceResult {
472    /// Success message
473    pub message: String,
474    /// Namespace
475    pub namespace: String,
476    /// Number of secrets created
477    pub secrets_created: usize,
478    /// Request ID
479    pub request_id: String,
480}
481
482/// Request for creating a namespace
483#[derive(Debug, Clone, Serialize)]
484pub struct CreateNamespaceRequest {
485    /// Namespace name to create
486    pub name: String,
487    /// Optional description
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub description: Option<String>,
490}
491
492/// Result of namespace creation
493#[derive(Debug, Clone, Deserialize)]
494pub struct CreateNamespaceResult {
495    /// Success message
496    pub message: String,
497    /// Created namespace name
498    pub namespace: String,
499    /// Request ID
500    pub request_id: String,
501}
502
503/// Result of namespace deletion
504#[derive(Debug, Clone, Deserialize)]
505pub struct DeleteNamespaceResult {
506    /// Success message
507    pub message: String,
508    /// Deleted namespace name
509    pub namespace: String,
510    /// Number of secrets deleted
511    pub secrets_deleted: usize,
512    /// Request ID from x-request-id header
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub request_id: Option<String>,
515}
516
517/// List of secret versions
518#[derive(Debug, Clone, Deserialize)]
519pub struct VersionList {
520    /// Namespace
521    pub namespace: String,
522    /// Key
523    pub key: String,
524    /// List of versions
525    pub versions: Vec<VersionInfo>,
526    /// Total count
527    pub total: usize,
528    /// Request ID
529    pub request_id: String,
530}
531
532/// Version information
533#[derive(Debug, Clone, Deserialize)]
534pub struct VersionInfo {
535    /// Version number
536    pub version: i32,
537    /// Creation time
538    pub created_at: String,
539    /// Actor who created this version
540    pub created_by: String,
541    /// Comment
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub comment: Option<String>,
544    /// Whether this is the current version
545    pub is_current: bool,
546}
547
548/// Result of rollback operation
549#[derive(Debug, Clone, Deserialize)]
550pub struct RollbackResult {
551    /// Success message
552    pub message: String,
553    /// Namespace
554    pub namespace: String,
555    /// Key
556    pub key: String,
557    /// New version (after rollback)
558    pub from_version: i32,
559    /// Rolled back to version
560    pub to_version: i32,
561    /// Request ID
562    pub request_id: String,
563}
564
565/// Audit query parameters
566#[derive(Debug, Clone, Serialize, Default)]
567pub struct AuditQuery {
568    /// Filter by namespace
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub namespace: Option<String>,
571    /// Filter by actor
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub actor: Option<String>,
574    /// Filter by action
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub action: Option<String>,
577    /// Start time (ISO 8601)
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub from: Option<String>,
580    /// End time (ISO 8601)
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub to: Option<String>,
583    /// Filter by success/failure
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub success: Option<bool>,
586    /// Limit
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub limit: Option<usize>,
589    /// Offset
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub offset: Option<usize>,
592}
593
594/// Audit log results
595#[derive(Debug, Clone, Deserialize)]
596pub struct AuditResult {
597    /// List of audit entries (mapped from "logs" in API response)
598    #[serde(rename = "logs")]
599    pub entries: Vec<AuditEntry>,
600    /// Total count (without limit)
601    pub total: usize,
602    /// Applied limit
603    pub limit: usize,
604    /// Applied offset
605    pub offset: usize,
606    /// Whether more results are available
607    pub has_more: bool,
608    /// Request ID
609    pub request_id: String,
610}
611
612/// Audit log entry
613#[derive(Debug, Clone, Deserialize)]
614pub struct AuditEntry {
615    /// Unique ID
616    pub id: i64,
617    /// Timestamp
618    pub timestamp: String,
619    /// Actor (user/service)
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub actor: Option<String>,
622    /// Action performed
623    pub action: String,
624    /// Namespace
625    #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")]
626    pub namespace: Option<String>,
627    /// Key name
628    #[serde(rename = "key_name", skip_serializing_if = "Option::is_none")]
629    pub key_name: Option<String>,
630    /// Whether the action succeeded
631    #[serde(rename = "success")]
632    pub success: bool,
633    /// IP address
634    #[serde(rename = "ip_address", skip_serializing_if = "Option::is_none")]
635    pub ip_address: Option<String>,
636    /// User agent
637    #[serde(rename = "user_agent", skip_serializing_if = "Option::is_none")]
638    pub user_agent: Option<String>,
639    /// Error message if failed
640    #[serde(rename = "error", skip_serializing_if = "Option::is_none")]
641    pub error: Option<String>,
642}
643
644/// Service discovery information
645#[derive(Debug, Clone, Deserialize)]
646pub struct Discovery {
647    /// Service name
648    pub service: String,
649    /// Service version
650    pub version: String,
651    /// API version
652    pub api_version: String,
653    /// Supported features
654    pub features: Vec<String>,
655    /// Build information
656    pub build: BuildInfo,
657    /// Endpoints
658    pub endpoints: EndpointInfo,
659}
660
661/// Build information
662#[derive(Debug, Clone, Deserialize)]
663pub struct BuildInfo {
664    /// Git commit hash
665    pub commit: String,
666    /// Build timestamp
667    pub timestamp: String,
668    /// Rust version
669    pub rust_version: String,
670}
671
672/// Endpoint information
673#[derive(Debug, Clone, Deserialize)]
674pub struct EndpointInfo {
675    /// Base URL
676    pub base_url: String,
677    /// Health check URL
678    pub health_url: String,
679    /// Metrics URL
680    pub metrics_url: String,
681}
682
683/// Health check result
684#[derive(Debug, Clone, Deserialize, Serialize)]
685pub struct HealthStatus {
686    /// Service status (healthy, degraded, unhealthy)
687    pub status: String,
688    /// ISO 8601 timestamp
689    pub timestamp: String,
690    /// Service version
691    pub version: Option<String>,
692    /// Additional checks (optional)
693    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
694    pub checks: std::collections::HashMap<String, HealthCheckResult>,
695}
696
697/// Individual health check result
698#[derive(Debug, Clone, Deserialize, Serialize)]
699pub struct HealthCheckResult {
700    /// Check status (healthy, degraded, unhealthy)
701    pub status: String,
702    /// Optional detailed message
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub message: Option<String>,
705    /// Optional error details
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub error: Option<String>,
708    /// Duration in milliseconds
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub duration_ms: Option<u64>,
711}
712
713/// API Key creation request
714#[derive(Debug, Clone, Serialize)]
715pub struct CreateApiKeyRequest {
716    /// Key name/description
717    pub name: String,
718    /// Expiration time (ISO 8601)
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub expires_at: Option<String>,
721    /// Allowed namespaces (empty = all)
722    #[serde(default)]
723    pub namespaces: Vec<String>,
724    /// Allowed permissions
725    pub permissions: Vec<String>,
726    /// Optional metadata
727    #[serde(skip_serializing_if = "Option::is_none")]
728    pub metadata: Option<serde_json::Value>,
729}
730
731/// API Key information
732#[derive(Debug, Clone, Deserialize)]
733pub struct ApiKeyInfo {
734    /// Key ID
735    pub id: String,
736    /// Key name
737    pub name: String,
738    /// API key value (only returned on creation)
739    #[serde(skip_serializing_if = "Option::is_none")]
740    pub key: Option<SecretString>,
741    /// Creation time
742    pub created_at: String,
743    /// Expiration time
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub expires_at: Option<String>,
746    /// Last used time
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub last_used_at: Option<String>,
749    /// Is active
750    pub active: bool,
751    /// Allowed namespaces
752    pub namespaces: Vec<String>,
753    /// Permissions
754    pub permissions: Vec<String>,
755    /// Optional metadata
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub metadata: Option<serde_json::Value>,
758}
759
760/// List API keys result
761#[derive(Debug, Clone, Deserialize)]
762pub struct ListApiKeysResult {
763    /// List of API keys
764    pub keys: Vec<ApiKeyInfo>,
765    /// Total count
766    pub total: usize,
767    /// Request ID
768    pub request_id: Option<String>,
769}
770
771/// Revoke API key result
772#[derive(Debug, Clone, Deserialize)]
773pub struct RevokeApiKeyResult {
774    /// Success message
775    pub message: String,
776    /// Revoked key ID
777    pub key_id: String,
778    /// Request ID
779    pub request_id: Option<String>,
780}
781
782// Cross-namespace search types
783
784/// Search mode for cross-namespace search
785#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
786pub enum SearchMode {
787    /// Exact match
788    Exact,
789    /// Prefix match
790    Prefix,
791    /// Contains match (default)
792    #[default]
793    Contains,
794}
795
796impl SearchMode {
797    /// Get the mode string for API parameter
798    pub fn as_str(&self) -> &'static str {
799        match self {
800            SearchMode::Exact => "exact",
801            SearchMode::Prefix => "prefix",
802            SearchMode::Contains => "contains",
803        }
804    }
805}
806
807/// Options for cross-namespace secret search
808#[derive(Debug, Clone, Default)]
809pub struct SearchSecretsOpts {
810    /// Search pattern (required)
811    pub pattern: String,
812    /// Search mode (default: contains)
813    pub mode: SearchMode,
814    /// Limit search to specific namespaces (empty = all accessible)
815    pub namespaces: Vec<String>,
816    /// Include secret values in results (default: false)
817    pub include_values: bool,
818}
819
820/// A single search match result
821#[derive(Debug, Clone, Deserialize)]
822pub struct SearchMatch {
823    /// Namespace containing the secret
824    pub namespace: String,
825    /// Secret key name
826    pub key: String,
827    /// Secret version
828    pub version: i32,
829    /// Last update time
830    pub updated_at: String,
831    /// Secret value (only if include_values was true)
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub value: Option<String>,
834}
835
836/// Result of cross-namespace search
837#[derive(Debug, Clone, Deserialize)]
838pub struct SearchSecretsResult {
839    /// List of matching secrets
840    pub matches: Vec<SearchMatch>,
841    /// Total number of matches
842    pub total: usize,
843}
844
845/// Options for bulk delete across namespaces
846#[derive(Debug, Clone)]
847pub struct BulkDeleteOpts {
848    /// Secret key to delete (required)
849    pub key: String,
850    /// Limit deletion to specific namespaces (empty = all accessible)
851    pub namespaces: Vec<String>,
852}
853
854/// Failure details for bulk delete
855#[derive(Debug, Clone, Deserialize)]
856pub struct BulkDeleteFailure {
857    /// Namespace where deletion failed
858    pub namespace: String,
859    /// Error message
860    pub error: String,
861}
862
863/// Result of bulk delete operation
864#[derive(Debug, Clone, Deserialize)]
865pub struct BulkDeleteResult {
866    /// Key that was deleted
867    pub key: String,
868    /// List of namespaces where deletion succeeded
869    pub deleted: Vec<String>,
870    /// List of failures with error details
871    pub failed: Vec<serde_json::Value>,
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877
878    #[test]
879    fn test_export_format() {
880        assert_eq!(ExportFormat::Json.as_str(), "json");
881        assert_eq!(ExportFormat::Dotenv.as_str(), "dotenv");
882        assert_eq!(ExportFormat::Shell.as_str(), "shell");
883        assert_eq!(ExportFormat::DockerCompose.as_str(), "docker-compose");
884    }
885
886    #[test]
887    fn test_search_mode() {
888        assert_eq!(SearchMode::Exact.as_str(), "exact");
889        assert_eq!(SearchMode::Prefix.as_str(), "prefix");
890        assert_eq!(SearchMode::Contains.as_str(), "contains");
891    }
892
893    #[test]
894    fn test_batch_op_read() {
895        let op = BatchOp::read("my-key");
896        assert_eq!(op.action, "read");
897        assert_eq!(op.key, "my-key");
898        assert!(op.value.is_none());
899    }
900}