Skip to main content

hoist_core/resources/
traits.rs

1//! Resource trait definition for Azure AI Search and Microsoft Foundry resources
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::service::ServiceDomain;
7
8/// Enumeration of all supported resource types across service domains
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "kebab-case")]
11pub enum ResourceKind {
12    // Azure AI Search resources
13    Index,
14    Indexer,
15    DataSource,
16    Skillset,
17    SynonymMap,
18    Alias,
19    KnowledgeBase,
20    KnowledgeSource,
21    // Microsoft Foundry resources
22    Agent,
23}
24
25impl ResourceKind {
26    /// Returns the service domain this resource belongs to
27    pub fn domain(&self) -> ServiceDomain {
28        match self {
29            ResourceKind::Agent => ServiceDomain::Foundry,
30            _ => ServiceDomain::Search,
31        }
32    }
33
34    /// Returns the API path segment for this resource type
35    pub fn api_path(&self) -> &'static str {
36        match self {
37            ResourceKind::Index => "indexes",
38            ResourceKind::Indexer => "indexers",
39            ResourceKind::DataSource => "datasources",
40            ResourceKind::Skillset => "skillsets",
41            ResourceKind::SynonymMap => "synonymmaps",
42            ResourceKind::Alias => "aliases",
43            ResourceKind::KnowledgeBase => "knowledgebases",
44            ResourceKind::KnowledgeSource => "knowledgesources",
45            ResourceKind::Agent => "assistants",
46        }
47    }
48
49    /// Returns the directory path for local storage (relative to resource root)
50    pub fn directory_name(&self) -> &'static str {
51        match self {
52            ResourceKind::Index => "search-management/indexes",
53            ResourceKind::Indexer => "search-management/indexers",
54            ResourceKind::DataSource => "search-management/data-sources",
55            ResourceKind::Skillset => "search-management/skillsets",
56            ResourceKind::SynonymMap => "search-management/synonym-maps",
57            ResourceKind::Alias => "search-management/aliases",
58            ResourceKind::KnowledgeBase => "agentic-retrieval/knowledge-bases",
59            ResourceKind::KnowledgeSource => "agentic-retrieval/knowledge-sources",
60            ResourceKind::Agent => "agents",
61        }
62    }
63
64    /// Returns true if this resource type uses the preview API
65    pub fn is_preview(&self) -> bool {
66        matches!(
67            self,
68            ResourceKind::Alias | ResourceKind::KnowledgeBase | ResourceKind::KnowledgeSource
69        )
70    }
71
72    /// Returns the display name for this resource type
73    pub fn display_name(&self) -> &'static str {
74        match self {
75            ResourceKind::Index => "Index",
76            ResourceKind::Indexer => "Indexer",
77            ResourceKind::DataSource => "Data Source",
78            ResourceKind::Skillset => "Skillset",
79            ResourceKind::SynonymMap => "Synonym Map",
80            ResourceKind::Alias => "Alias",
81            ResourceKind::KnowledgeBase => "Knowledge Base",
82            ResourceKind::KnowledgeSource => "Knowledge Source",
83            ResourceKind::Agent => "Agent",
84        }
85    }
86
87    /// Returns all resource kinds across all domains
88    pub fn all() -> &'static [ResourceKind] {
89        &[
90            ResourceKind::Index,
91            ResourceKind::Indexer,
92            ResourceKind::DataSource,
93            ResourceKind::Skillset,
94            ResourceKind::SynonymMap,
95            ResourceKind::Alias,
96            ResourceKind::KnowledgeBase,
97            ResourceKind::KnowledgeSource,
98            ResourceKind::Agent,
99        ]
100    }
101
102    /// Returns non-preview Search resource kinds (stable search resources only)
103    pub fn stable() -> &'static [ResourceKind] {
104        &[
105            ResourceKind::Index,
106            ResourceKind::Indexer,
107            ResourceKind::DataSource,
108            ResourceKind::Skillset,
109            ResourceKind::SynonymMap,
110        ]
111    }
112
113    /// Returns all Search resource kinds
114    pub fn search_kinds() -> Vec<ResourceKind> {
115        ResourceKind::all()
116            .iter()
117            .filter(|k| k.domain() == ServiceDomain::Search)
118            .copied()
119            .collect()
120    }
121
122    /// Returns all Foundry resource kinds
123    pub fn foundry_kinds() -> Vec<ResourceKind> {
124        ResourceKind::all()
125            .iter()
126            .filter(|k| k.domain() == ServiceDomain::Foundry)
127            .copied()
128            .collect()
129    }
130}
131
132impl fmt::Display for ResourceKind {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}", self.display_name())
135    }
136}
137
138/// Trait for Azure AI Search resources
139pub trait Resource: Serialize + for<'de> Deserialize<'de> + Clone {
140    /// Returns the resource kind
141    fn kind() -> ResourceKind;
142
143    /// Returns the resource name (identifier)
144    fn name(&self) -> &str;
145
146    /// Returns fields that should be stripped during normalization (pull and push).
147    /// These are truly transient or sensitive: OData metadata, secrets, credentials.
148    fn volatile_fields() -> &'static [&'static str] {
149        &["@odata.etag", "@odata.context"]
150    }
151
152    /// Returns fields that are read-only — Azure returns them in GET but rejects
153    /// them in PUT. These are kept in local files for documentation (e.g. showing
154    /// which resources are connected) but stripped before pushing to Azure.
155    fn read_only_fields() -> &'static [&'static str] {
156        &[]
157    }
158
159    /// Returns the identity key for array sorting within this resource type
160    fn identity_key() -> &'static str {
161        "name"
162    }
163
164    /// Returns dependencies on other resources (resource kind, name)
165    fn dependencies(&self) -> Vec<(ResourceKind, String)> {
166        Vec::new()
167    }
168
169    /// Returns fields that are immutable after creation
170    fn immutable_fields() -> &'static [&'static str] {
171        &[]
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_all_returns_nine_kinds() {
181        assert_eq!(ResourceKind::all().len(), 9);
182    }
183
184    #[test]
185    fn test_stable_excludes_preview() {
186        let stable = ResourceKind::stable();
187        assert_eq!(stable.len(), 5);
188        for kind in stable {
189            assert!(!kind.is_preview());
190        }
191    }
192
193    #[test]
194    fn test_preview_kinds() {
195        assert!(ResourceKind::KnowledgeBase.is_preview());
196        assert!(ResourceKind::KnowledgeSource.is_preview());
197        assert!(!ResourceKind::Index.is_preview());
198        assert!(!ResourceKind::Indexer.is_preview());
199        assert!(!ResourceKind::DataSource.is_preview());
200        assert!(!ResourceKind::Skillset.is_preview());
201        assert!(!ResourceKind::SynonymMap.is_preview());
202        assert!(ResourceKind::Alias.is_preview());
203        assert!(!ResourceKind::Agent.is_preview());
204    }
205
206    #[test]
207    fn test_api_paths() {
208        assert_eq!(ResourceKind::Index.api_path(), "indexes");
209        assert_eq!(ResourceKind::Indexer.api_path(), "indexers");
210        assert_eq!(ResourceKind::DataSource.api_path(), "datasources");
211        assert_eq!(ResourceKind::Skillset.api_path(), "skillsets");
212        assert_eq!(ResourceKind::SynonymMap.api_path(), "synonymmaps");
213        assert_eq!(ResourceKind::Alias.api_path(), "aliases");
214        assert_eq!(ResourceKind::KnowledgeBase.api_path(), "knowledgebases");
215        assert_eq!(ResourceKind::KnowledgeSource.api_path(), "knowledgesources");
216        assert_eq!(ResourceKind::Agent.api_path(), "assistants");
217    }
218
219    #[test]
220    fn test_directory_names() {
221        assert_eq!(
222            ResourceKind::Index.directory_name(),
223            "search-management/indexes"
224        );
225        assert_eq!(
226            ResourceKind::DataSource.directory_name(),
227            "search-management/data-sources"
228        );
229        assert_eq!(
230            ResourceKind::SynonymMap.directory_name(),
231            "search-management/synonym-maps"
232        );
233        assert_eq!(
234            ResourceKind::Alias.directory_name(),
235            "search-management/aliases"
236        );
237        assert_eq!(
238            ResourceKind::KnowledgeBase.directory_name(),
239            "agentic-retrieval/knowledge-bases"
240        );
241        assert_eq!(
242            ResourceKind::KnowledgeSource.directory_name(),
243            "agentic-retrieval/knowledge-sources"
244        );
245        assert_eq!(ResourceKind::Agent.directory_name(), "agents");
246    }
247
248    #[test]
249    fn test_stable_kinds_under_search_management() {
250        for kind in ResourceKind::stable() {
251            assert!(
252                kind.directory_name().starts_with("search-management/"),
253                "{:?} should be under search-management/",
254                kind
255            );
256        }
257    }
258
259    #[test]
260    fn test_agentic_retrieval_kinds_are_preview() {
261        for kind in ResourceKind::all() {
262            if kind.directory_name().starts_with("agentic-retrieval/") {
263                assert!(
264                    kind.is_preview(),
265                    "{:?} under agentic-retrieval/ should be preview",
266                    kind
267                );
268            }
269        }
270    }
271
272    #[test]
273    fn test_display_names() {
274        assert_eq!(ResourceKind::Index.display_name(), "Index");
275        assert_eq!(ResourceKind::DataSource.display_name(), "Data Source");
276        assert_eq!(ResourceKind::KnowledgeBase.display_name(), "Knowledge Base");
277        assert_eq!(ResourceKind::Alias.display_name(), "Alias");
278        assert_eq!(ResourceKind::Agent.display_name(), "Agent");
279    }
280
281    #[test]
282    fn test_display_trait() {
283        assert_eq!(format!("{}", ResourceKind::Index), "Index");
284        assert_eq!(format!("{}", ResourceKind::Skillset), "Skillset");
285        assert_eq!(format!("{}", ResourceKind::Alias), "Alias");
286        assert_eq!(format!("{}", ResourceKind::Agent), "Agent");
287    }
288
289    #[test]
290    fn test_serde_roundtrip() {
291        let kind = ResourceKind::DataSource;
292        let json = serde_json::to_string(&kind).unwrap();
293        assert_eq!(json, "\"data-source\"");
294        let back: ResourceKind = serde_json::from_str(&json).unwrap();
295        assert_eq!(back, kind);
296    }
297
298    #[test]
299    fn test_serde_roundtrip_alias() {
300        let kind = ResourceKind::Alias;
301        let json = serde_json::to_string(&kind).unwrap();
302        assert_eq!(json, "\"alias\"");
303        let back: ResourceKind = serde_json::from_str(&json).unwrap();
304        assert_eq!(back, kind);
305    }
306
307    #[test]
308    fn test_serde_roundtrip_agent() {
309        let kind = ResourceKind::Agent;
310        let json = serde_json::to_string(&kind).unwrap();
311        assert_eq!(json, "\"agent\"");
312        let back: ResourceKind = serde_json::from_str(&json).unwrap();
313        assert_eq!(back, kind);
314    }
315
316    #[test]
317    fn test_all_kinds_in_stable_or_preview() {
318        for kind in ResourceKind::all() {
319            if kind.domain() == ServiceDomain::Search {
320                if kind.is_preview() {
321                    assert!(!ResourceKind::stable().contains(kind));
322                } else {
323                    assert!(ResourceKind::stable().contains(kind));
324                }
325            }
326        }
327    }
328
329    #[test]
330    fn test_domain_search_kinds() {
331        let search = ResourceKind::search_kinds();
332        assert_eq!(search.len(), 8);
333        for kind in &search {
334            assert_eq!(kind.domain(), ServiceDomain::Search);
335        }
336    }
337
338    #[test]
339    fn test_domain_foundry_kinds() {
340        let foundry = ResourceKind::foundry_kinds();
341        assert_eq!(foundry.len(), 1);
342        assert_eq!(foundry[0], ResourceKind::Agent);
343        for kind in &foundry {
344            assert_eq!(kind.domain(), ServiceDomain::Foundry);
345        }
346    }
347
348    #[test]
349    fn test_agent_domain_is_foundry() {
350        assert_eq!(ResourceKind::Agent.domain(), ServiceDomain::Foundry);
351    }
352
353    #[test]
354    fn test_search_resources_domain_is_search() {
355        for kind in ResourceKind::stable() {
356            assert_eq!(kind.domain(), ServiceDomain::Search);
357        }
358        assert_eq!(ResourceKind::Alias.domain(), ServiceDomain::Search);
359        assert_eq!(ResourceKind::KnowledgeBase.domain(), ServiceDomain::Search);
360        assert_eq!(
361            ResourceKind::KnowledgeSource.domain(),
362            ServiceDomain::Search
363        );
364    }
365
366    #[test]
367    fn test_search_plus_foundry_equals_all() {
368        let mut combined = ResourceKind::search_kinds();
369        combined.extend(ResourceKind::foundry_kinds());
370        assert_eq!(combined.len(), ResourceKind::all().len());
371        for kind in ResourceKind::all() {
372            assert!(combined.contains(kind));
373        }
374    }
375}