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