1use serde_json::Value;
13
14use crate::resources::traits::ResourceKind;
15
16pub const SEARCH_STABLE_API_VERSION: &str = "2026-04-01";
18pub const SEARCH_PREVIEW_API_VERSION: &str = "2026-05-01-preview";
19pub const FOUNDRY_API_VERSION: &str = "v1";
20pub const ARM_COGNITIVE_API_VERSION: &str = "2026-05-01";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Domain {
26 Search,
28 FoundryData,
30 FoundryArm,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Channel {
37 Stable,
38 Preview,
39}
40
41#[derive(Debug, Clone, Copy)]
48pub struct RefField {
49 pub path: &'static str,
50 pub to: ResourceKind,
51}
52
53#[derive(Debug, Clone, Copy)]
55pub struct KindMeta {
56 pub kind: ResourceKind,
57 pub domain: Domain,
58 pub collection_path: &'static str,
60 pub dir_name: &'static str,
62 pub channel: Channel,
64 pub volatile_fields: &'static [&'static str],
67 pub read_only_fields: &'static [&'static str],
69 pub secret_fields: &'static [&'static str],
72 pub sidecar_fields: &'static [&'static str],
74 pub reference_fields: &'static [RefField],
76}
77
78const COMMON_VOLATILE: &[&str] = &["@odata.etag", "@odata.context", "e_tag", "etag"];
79
80static KINDS: &[KindMeta] = &[
81 KindMeta {
82 kind: ResourceKind::DataSource,
83 domain: Domain::Search,
84 collection_path: "datasources",
85 dir_name: "data-sources",
86 channel: Channel::Stable,
87 volatile_fields: COMMON_VOLATILE,
88 read_only_fields: &[],
89 secret_fields: &["credentials.connectionString"],
90 sidecar_fields: &[],
91 reference_fields: &[],
92 },
93 KindMeta {
94 kind: ResourceKind::Index,
95 domain: Domain::Search,
96 collection_path: "indexes",
97 dir_name: "indexes",
98 channel: Channel::Stable,
99 volatile_fields: COMMON_VOLATILE,
100 read_only_fields: &[],
101 secret_fields: &[
102 "encryptionKey.accessCredentials.applicationSecret",
103 "vectorSearch.vectorizers[].azureOpenAIParameters.apiKey",
104 ],
105 sidecar_fields: &[],
106 reference_fields: &[],
107 },
108 KindMeta {
109 kind: ResourceKind::Skillset,
110 domain: Domain::Search,
111 collection_path: "skillsets",
112 dir_name: "skillsets",
113 channel: Channel::Stable,
114 volatile_fields: COMMON_VOLATILE,
115 read_only_fields: &[],
116 secret_fields: &[
117 "cognitiveServices.key",
118 "skills[].apiKey",
119 "encryptionKey.accessCredentials.applicationSecret",
120 ],
121 sidecar_fields: &[],
122 reference_fields: &[
123 RefField {
125 path: "knowledgeStore.projections[].objects[].storageContainer",
126 to: ResourceKind::Index,
127 },
128 ],
129 },
130 KindMeta {
131 kind: ResourceKind::Indexer,
132 domain: Domain::Search,
133 collection_path: "indexers",
134 dir_name: "indexers",
135 channel: Channel::Stable,
136 volatile_fields: COMMON_VOLATILE,
137 read_only_fields: &["status", "lastResult", "executionHistory", "limits"],
138 secret_fields: &[],
139 sidecar_fields: &[],
140 reference_fields: &[
141 RefField {
142 path: "dataSourceName",
143 to: ResourceKind::DataSource,
144 },
145 RefField {
146 path: "targetIndexName",
147 to: ResourceKind::Index,
148 },
149 RefField {
150 path: "skillsetName",
151 to: ResourceKind::Skillset,
152 },
153 ],
154 },
155 KindMeta {
156 kind: ResourceKind::SynonymMap,
157 domain: Domain::Search,
158 collection_path: "synonymmaps",
159 dir_name: "synonym-maps",
160 channel: Channel::Stable,
161 volatile_fields: COMMON_VOLATILE,
162 read_only_fields: &[],
163 secret_fields: &["encryptionKey.accessCredentials.applicationSecret"],
164 sidecar_fields: &[],
165 reference_fields: &[],
166 },
167 KindMeta {
168 kind: ResourceKind::Alias,
169 domain: Domain::Search,
170 collection_path: "aliases",
171 dir_name: "aliases",
172 channel: Channel::Stable,
173 volatile_fields: COMMON_VOLATILE,
174 read_only_fields: &[],
175 secret_fields: &[],
176 sidecar_fields: &[],
177 reference_fields: &[RefField {
178 path: "indexes[]",
179 to: ResourceKind::Index,
180 }],
181 },
182 KindMeta {
183 kind: ResourceKind::KnowledgeSource,
184 domain: Domain::Search,
185 collection_path: "knowledgeSources",
186 dir_name: "knowledge-sources",
187 channel: Channel::Stable,
188 volatile_fields: COMMON_VOLATILE,
189 read_only_fields: &["createdResources", "ingestionPermissionOptions"],
191 secret_fields: &["searchIndexParameters.apiKey"],
192 sidecar_fields: &[],
193 reference_fields: &[RefField {
194 path: "searchIndexParameters.searchIndexName",
195 to: ResourceKind::Index,
196 }],
197 },
198 KindMeta {
199 kind: ResourceKind::KnowledgeBase,
200 domain: Domain::Search,
201 collection_path: "knowledgeBases",
202 dir_name: "knowledge-bases",
203 channel: Channel::Stable,
204 volatile_fields: COMMON_VOLATILE,
205 read_only_fields: &[],
206 secret_fields: &["models[].apiKey", "models[].azureOpenAIParameters.apiKey"],
207 sidecar_fields: &[],
208 reference_fields: &[RefField {
209 path: "knowledgeSources[].name",
210 to: ResourceKind::KnowledgeSource,
211 }],
212 },
213 KindMeta {
214 kind: ResourceKind::Agent,
215 domain: Domain::FoundryData,
216 collection_path: "agents",
217 dir_name: "agents",
218 channel: Channel::Stable,
219 volatile_fields: &[
220 "@odata.etag",
221 "@odata.context",
222 "id",
223 "object",
224 "created_at",
225 "updated_at",
226 "version",
227 ],
228 read_only_fields: &[],
229 secret_fields: &[],
230 sidecar_fields: &["instructions"],
231 reference_fields: &[RefField {
232 path: "model",
233 to: ResourceKind::Deployment,
234 }],
235 },
236 KindMeta {
237 kind: ResourceKind::Deployment,
238 domain: Domain::FoundryArm,
239 collection_path: "deployments",
240 dir_name: "deployments",
241 channel: Channel::Stable,
242 volatile_fields: &[
243 "id",
244 "type",
245 "systemData",
246 "etag",
247 "properties.provisioningState",
248 "properties.capabilities",
249 "properties.rateLimits",
250 "properties.model.callRateLimit",
251 ],
252 read_only_fields: &[],
253 secret_fields: &[],
254 sidecar_fields: &[],
255 reference_fields: &[RefField {
256 path: "properties.raiPolicyName",
257 to: ResourceKind::Guardrail,
258 }],
259 },
260 KindMeta {
261 kind: ResourceKind::Connection,
262 domain: Domain::FoundryArm,
263 collection_path: "connections",
264 dir_name: "connections",
265 channel: Channel::Stable,
266 volatile_fields: &[
267 "id",
268 "type",
269 "systemData",
270 "etag",
271 "properties.provisioningState",
272 ],
273 read_only_fields: &[],
274 secret_fields: &[
276 "properties.credentials.key",
277 "properties.credentials.keys",
278 "properties.credentials.secret",
279 "properties.credentials.clientSecret",
280 "properties.credentials.pat",
281 "properties.credentials.sas",
282 ],
283 sidecar_fields: &[],
284 reference_fields: &[],
285 },
286 KindMeta {
287 kind: ResourceKind::Guardrail,
288 domain: Domain::FoundryArm,
289 collection_path: "raiPolicies",
290 dir_name: "guardrails",
291 channel: Channel::Stable,
292 volatile_fields: &["id", "type", "systemData", "etag"],
293 read_only_fields: &[],
294 secret_fields: &[],
295 sidecar_fields: &[],
296 reference_fields: &[],
297 },
298];
299
300pub fn all_kinds() -> &'static [ResourceKind] {
302 static ORDER: &[ResourceKind] = &[
303 ResourceKind::DataSource,
304 ResourceKind::Index,
305 ResourceKind::Skillset,
306 ResourceKind::Indexer,
307 ResourceKind::SynonymMap,
308 ResourceKind::Alias,
309 ResourceKind::KnowledgeSource,
310 ResourceKind::KnowledgeBase,
311 ResourceKind::Agent,
312 ResourceKind::Deployment,
313 ResourceKind::Connection,
314 ResourceKind::Guardrail,
315 ];
316 ORDER
317}
318
319pub fn meta(kind: ResourceKind) -> &'static KindMeta {
321 KINDS
322 .iter()
323 .find(|m| m.kind == kind)
324 .expect("registry entry exists for every ResourceKind")
325}
326
327pub fn valid_datasource_types(channel: Channel) -> &'static [&'static str] {
333 const GA: &[&str] = &[
334 "azureblob",
335 "adlsgen2",
336 "azuretable",
337 "azuresql",
338 "cosmosdb",
339 "onelake",
340 ];
341 const PREVIEW: &[&str] = &[
342 "azureblob",
343 "adlsgen2",
344 "azuretable",
345 "azuresql",
346 "cosmosdb",
347 "onelake",
348 "mysql",
349 "sharepoint",
350 "azurefile",
351 "azurefiles",
352 ];
353 match channel {
354 Channel::Stable => GA,
355 Channel::Preview => PREVIEW,
356 }
357}
358
359pub fn preview_only_datasource_types() -> &'static [&'static str] {
361 &["mysql", "sharepoint", "azurefile", "azurefiles"]
362}
363
364pub const X_RIGG_REF: &str = "x-rigg-ref";
367pub const X_RIGG_API: &str = "x-rigg-api";
369
370pub fn extract_references(kind: ResourceKind, body: &Value) -> Vec<(ResourceKind, String)> {
373 let mut out = Vec::new();
374 for rf in meta(kind).reference_fields {
375 collect_path(body, rf.path, &mut |v| {
376 if let Some(s) = v.as_str() {
377 if !s.is_empty() {
378 out.push((rf.to, s.to_string()));
379 }
380 }
381 });
382 }
383 collect_x_rigg_refs(body, &mut out);
384 out.sort();
385 out.dedup();
386 out
387}
388
389fn collect_x_rigg_refs(v: &Value, out: &mut Vec<(ResourceKind, String)>) {
390 match v {
391 Value::Object(map) => {
392 for (k, val) in map {
393 if k == X_RIGG_REF {
394 if let Some(s) = val.as_str() {
395 if let Some((dir, name)) = s.split_once('/') {
396 if let Some(kind) = ResourceKind::from_directory_name(dir) {
397 out.push((kind, name.to_string()));
398 }
399 }
400 }
401 } else {
402 collect_x_rigg_refs(val, out);
403 }
404 }
405 }
406 Value::Array(arr) => {
407 for item in arr {
408 collect_x_rigg_refs(item, out);
409 }
410 }
411 _ => {}
412 }
413}
414
415pub fn collect_path(v: &Value, path: &str, f: &mut dyn FnMut(&Value)) {
418 fn walk(v: &Value, segments: &[&str], f: &mut dyn FnMut(&Value)) {
419 let Some((head, rest)) = segments.split_first() else {
420 f(v);
421 return;
422 };
423 if let Some(key) = head.strip_suffix("[]") {
424 let target = if key.is_empty() { Some(v) } else { v.get(key) };
425 if let Some(Value::Array(arr)) = target {
426 for item in arr {
427 walk(item, rest, f);
428 }
429 }
430 } else if let Some(next) = v.get(*head) {
431 walk(next, rest, f);
432 }
433 }
434 let segments: Vec<&str> = path.split('.').collect();
435 walk(v, &segments, f);
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use serde_json::json;
442
443 #[test]
444 fn meta_is_total_and_consistent() {
445 for kind in all_kinds() {
446 let m = meta(*kind);
447 assert_eq!(m.kind, *kind);
448 assert!(!m.collection_path.is_empty());
449 assert!(!m.dir_name.is_empty());
450 }
451 assert_eq!(all_kinds().len(), 12);
452 }
453
454 #[test]
455 fn dir_names_unique() {
456 let mut dirs: Vec<_> = all_kinds().iter().map(|k| meta(*k).dir_name).collect();
457 dirs.sort();
458 dirs.dedup();
459 assert_eq!(dirs.len(), 12);
460 }
461
462 #[test]
463 fn indexer_references() {
464 let indexer = json!({
465 "name": "idxr",
466 "dataSourceName": "my-ds",
467 "targetIndexName": "my-index",
468 "skillsetName": "my-skills"
469 });
470 let refs = extract_references(ResourceKind::Indexer, &indexer);
471 assert!(refs.contains(&(ResourceKind::DataSource, "my-ds".into())));
472 assert!(refs.contains(&(ResourceKind::Index, "my-index".into())));
473 assert!(refs.contains(&(ResourceKind::Skillset, "my-skills".into())));
474 }
475
476 #[test]
477 fn knowledge_base_and_alias_references() {
478 let kb = json!({
479 "name": "kb",
480 "knowledgeSources": [{"name": "ks-a"}, {"name": "ks-b"}]
481 });
482 let refs = extract_references(ResourceKind::KnowledgeBase, &kb);
483 assert_eq!(
484 refs,
485 vec![
486 (ResourceKind::KnowledgeSource, "ks-a".to_string()),
487 (ResourceKind::KnowledgeSource, "ks-b".to_string()),
488 ]
489 );
490
491 let alias = json!({"name": "a", "indexes": ["i1"]});
492 let refs = extract_references(ResourceKind::Alias, &alias);
493 assert_eq!(refs, vec![(ResourceKind::Index, "i1".to_string())]);
494 }
495
496 #[test]
497 fn x_rigg_ref_extracted_at_depth() {
498 let agent = json!({
499 "name": "agent",
500 "model": "gpt-5-mini",
501 "tools": [
502 {"type": "mcp", "x-rigg-ref": "knowledge-bases/support-kb", "server_url": ""}
503 ]
504 });
505 let refs = extract_references(ResourceKind::Agent, &agent);
506 assert!(refs.contains(&(ResourceKind::KnowledgeBase, "support-kb".into())));
507 assert!(refs.contains(&(ResourceKind::Deployment, "gpt-5-mini".into())));
508 }
509
510 #[test]
511 fn datasource_types_per_channel() {
512 assert!(valid_datasource_types(Channel::Stable).contains(&"cosmosdb"));
513 assert!(valid_datasource_types(Channel::Stable).contains(&"onelake"));
514 assert!(!valid_datasource_types(Channel::Stable).contains(&"sharepoint"));
515 assert!(valid_datasource_types(Channel::Preview).contains(&"sharepoint"));
516 assert!(valid_datasource_types(Channel::Preview).contains(&"azurefile"));
518 assert!(valid_datasource_types(Channel::Preview).contains(&"azurefiles"));
519 }
520
521 #[test]
522 fn ks_points_at_index() {
523 let ks = json!({
524 "name": "ks",
525 "kind": "searchIndex",
526 "searchIndexParameters": {"searchIndexName": "docs"}
527 });
528 let refs = extract_references(ResourceKind::KnowledgeSource, &ks);
529 assert_eq!(refs, vec![(ResourceKind::Index, "docs".to_string())]);
530 }
531}