Skip to main content

hoist_core/resources/
indexer.rs

1//! Indexer resource definition
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use super::traits::{Resource, ResourceKind};
7
8/// Azure AI Search Indexer definition
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct Indexer {
12    pub name: String,
13    pub data_source_name: String,
14    pub target_index_name: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub skillset_name: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub description: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub schedule: Option<IndexerSchedule>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub parameters: Option<IndexerParameters>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub field_mappings: Option<Vec<FieldMapping>>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub output_field_mappings: Option<Vec<FieldMapping>>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub disabled: Option<bool>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub cache: Option<Value>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub encryption_key: Option<Value>,
33    #[serde(flatten)]
34    pub extra: std::collections::HashMap<String, Value>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct IndexerSchedule {
40    pub interval: String,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub start_time: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct IndexerParameters {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub batch_size: Option<i32>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub max_failed_items: Option<i32>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub max_failed_items_per_batch: Option<i32>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub configuration: Option<Value>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct FieldMapping {
61    pub source_field_name: String,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub target_field_name: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub mapping_function: Option<MappingFunction>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct MappingFunction {
71    pub name: String,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub parameters: Option<Value>,
74}
75
76impl Resource for Indexer {
77    fn kind() -> ResourceKind {
78        ResourceKind::Indexer
79    }
80
81    fn name(&self) -> &str {
82        &self.name
83    }
84
85    fn read_only_fields() -> &'static [&'static str] {
86        // startTime is server-managed — Azure updates it every time the indexer runs.
87        // Kept in local files to show scheduling info, stripped before push.
88        &["startTime"]
89    }
90
91    fn dependencies(&self) -> Vec<(ResourceKind, String)> {
92        let mut deps = vec![
93            (ResourceKind::DataSource, self.data_source_name.clone()),
94            (ResourceKind::Index, self.target_index_name.clone()),
95        ];
96        if let Some(ref skillset) = self.skillset_name {
97            deps.push((ResourceKind::Skillset, skillset.clone()));
98        }
99        deps
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    fn make_indexer(skillset: Option<&str>) -> Indexer {
108        Indexer {
109            name: "my-indexer".to_string(),
110            data_source_name: "my-ds".to_string(),
111            target_index_name: "my-index".to_string(),
112            skillset_name: skillset.map(String::from),
113            description: None,
114            schedule: None,
115            parameters: None,
116            field_mappings: None,
117            output_field_mappings: None,
118            disabled: None,
119            cache: None,
120            encryption_key: None,
121            extra: Default::default(),
122        }
123    }
124
125    #[test]
126    fn test_indexer_kind() {
127        assert_eq!(Indexer::kind(), ResourceKind::Indexer);
128    }
129
130    #[test]
131    fn test_indexer_dependencies_without_skillset() {
132        let indexer = make_indexer(None);
133        let deps = indexer.dependencies();
134        assert_eq!(deps.len(), 2);
135        assert!(deps.contains(&(ResourceKind::DataSource, "my-ds".to_string())));
136        assert!(deps.contains(&(ResourceKind::Index, "my-index".to_string())));
137    }
138
139    #[test]
140    fn test_indexer_dependencies_with_skillset() {
141        let indexer = make_indexer(Some("my-skillset"));
142        let deps = indexer.dependencies();
143        assert_eq!(deps.len(), 3);
144        assert!(deps.contains(&(ResourceKind::Skillset, "my-skillset".to_string())));
145    }
146
147    #[test]
148    fn test_indexer_deserialize() {
149        let json = r#"{
150            "name": "test-indexer",
151            "dataSourceName": "ds",
152            "targetIndexName": "idx"
153        }"#;
154        let indexer: Indexer = serde_json::from_str(json).unwrap();
155        assert_eq!(indexer.name, "test-indexer");
156        assert_eq!(indexer.data_source_name, "ds");
157        assert_eq!(indexer.target_index_name, "idx");
158    }
159
160    #[test]
161    fn test_indexer_read_only_fields_includes_start_time() {
162        let fields = Indexer::read_only_fields();
163        assert!(
164            fields.contains(&"startTime"),
165            "startTime is server-managed and must be stripped before push"
166        );
167    }
168}