Skip to main content

hoist_core/resources/
knowledge_source.rs

1//! Knowledge Source resource definition (Agentic Search Preview)
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use super::traits::{Resource, ResourceKind};
7
8/// Azure AI Search Knowledge Source definition (Preview API)
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct KnowledgeSource {
12    pub name: String,
13    pub index_name: String,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub description: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub knowledge_base_name: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub query_type: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub semantic_configuration: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub top: Option<i32>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub filter: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub select_fields: Option<Vec<String>>,
28    /// Catch-all for additional fields from preview API
29    #[serde(flatten)]
30    pub extra: std::collections::HashMap<String, Value>,
31}
32
33impl Resource for KnowledgeSource {
34    fn kind() -> ResourceKind {
35        ResourceKind::KnowledgeSource
36    }
37
38    fn name(&self) -> &str {
39        &self.name
40    }
41
42    fn volatile_fields() -> &'static [&'static str] {
43        &["@odata.etag", "@odata.context"]
44    }
45
46    fn read_only_fields() -> &'static [&'static str] {
47        // ingestionPermissionOptions — Azure returns it in GET but rejects it in PUT.
48        // createdResources — Azure tracks which resources were auto-created (nested in azureBlobParameters).
49        // Both kept in local files to show resource relationships, stripped before push.
50        &["ingestionPermissionOptions", "createdResources"]
51    }
52
53    fn dependencies(&self) -> Vec<(ResourceKind, String)> {
54        let mut deps = vec![(ResourceKind::Index, self.index_name.clone())];
55        if let Some(ref kb) = self.knowledge_base_name {
56            deps.push((ResourceKind::KnowledgeBase, kb.clone()));
57        }
58        deps
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_knowledge_source_kind() {
68        assert_eq!(KnowledgeSource::kind(), ResourceKind::KnowledgeSource);
69    }
70
71    #[test]
72    fn test_knowledge_source_dependencies_without_kb() {
73        let ks = KnowledgeSource {
74            name: "ks1".to_string(),
75            index_name: "idx".to_string(),
76            description: None,
77            knowledge_base_name: None,
78            query_type: None,
79            semantic_configuration: None,
80            top: None,
81            filter: None,
82            select_fields: None,
83            extra: Default::default(),
84        };
85        let deps = ks.dependencies();
86        assert_eq!(deps.len(), 1);
87        assert_eq!(deps[0], (ResourceKind::Index, "idx".to_string()));
88    }
89
90    #[test]
91    fn test_knowledge_source_volatile_fields() {
92        let fields = KnowledgeSource::volatile_fields();
93        assert!(fields.contains(&"@odata.etag"));
94        assert!(fields.contains(&"@odata.context"));
95        // These should NOT be volatile — they're informational
96        assert!(!fields.contains(&"ingestionPermissionOptions"));
97        assert!(!fields.contains(&"createdResources"));
98    }
99
100    #[test]
101    fn test_knowledge_source_read_only_fields() {
102        let fields = KnowledgeSource::read_only_fields();
103        assert!(
104            fields.contains(&"ingestionPermissionOptions"),
105            "ingestionPermissionOptions is read-only: kept in local, stripped before push"
106        );
107        assert!(
108            fields.contains(&"createdResources"),
109            "createdResources is read-only: kept in local, stripped before push"
110        );
111    }
112
113    #[test]
114    fn test_knowledge_source_dependencies_with_kb() {
115        let ks = KnowledgeSource {
116            name: "ks1".to_string(),
117            index_name: "idx".to_string(),
118            description: None,
119            knowledge_base_name: Some("kb1".to_string()),
120            query_type: None,
121            semantic_configuration: None,
122            top: None,
123            filter: None,
124            select_fields: None,
125            extra: Default::default(),
126        };
127        let deps = ks.dependencies();
128        assert_eq!(deps.len(), 2);
129        assert!(deps.contains(&(ResourceKind::KnowledgeBase, "kb1".to_string())));
130    }
131}