1use serde_json::Value;
13
14use crate::resources::traits::{ResourceKind, ResourceRef};
15
16pub const SEARCH_STABLE_API_VERSION: &str = "2026-04-01";
18pub const SEARCH_PREVIEW_API_VERSION: &str = "2026-08-01-preview";
19pub const FOUNDRY_API_VERSION: &str = "v1";
21pub const ARM_COGNITIVE_API_VERSION: &str = "2026-05-01";
27pub const ARM_SEARCH_API_VERSION: &str = "2025-05-01";
29pub const ARM_STORAGE_API_VERSION: &str = "2026-06-01";
31pub const ARM_WEB_API_VERSION: &str = "2026-07-15";
33pub const ARM_AUTHORIZATION_API_VERSION: &str = "2022-04-01";
35pub const ARM_RESOURCES_API_VERSION: &str = "2022-12-01";
37pub const ARM_MANAGED_IDENTITY_API_VERSION: &str = "2024-11-30";
39pub const ARM_KEYVAULT_API_VERSION: &str = "2026-02-01";
41pub const KEYVAULT_SECRETS_API_VERSION: &str = "2025-07-01";
43pub const GRAPH_API_VERSION: &str = "v1.0";
45pub const ARM_BASE_URL: &str = "https://management.azure.com";
46pub const GRAPH_BASE_URL: &str = "https://graph.microsoft.com/v1.0";
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum Provider {
51 SearchData,
52 FoundryData,
53 CognitiveServicesArm,
54 SearchArm,
55 StorageArm,
56 WebArm,
57 AuthorizationArm,
58 ResourcesArm,
59 ManagedIdentityArm,
60 KeyVaultArm,
61 KeyVaultData,
62 Graph,
63}
64
65#[derive(Debug, Clone, Copy)]
71pub struct ArmRegistration {
72 pub namespace: &'static str,
73 pub resource_types: &'static [&'static str],
74}
75
76#[derive(Debug, Clone, Copy)]
80pub struct Hold {
81 pub newer: &'static str,
82 pub reason: &'static str,
83}
84
85#[derive(Debug, Clone, Copy)]
86pub struct ProviderMeta {
87 pub provider: Provider,
88 pub label: &'static str,
90 pub stable: &'static str,
91 pub preview: Option<&'static str>,
92 pub audience: &'static str,
94 pub spec_path: Option<&'static str>,
97 pub preview_spec_path: Option<&'static str>,
99 pub route_versioned: bool,
100 pub arm: Option<ArmRegistration>,
104 pub hold: Option<Hold>,
107}
108
109static PROVIDERS: &[ProviderMeta] = &[
110 ProviderMeta {
111 provider: Provider::SearchData,
112 label: "Azure AI Search data plane",
113 stable: SEARCH_STABLE_API_VERSION,
114 preview: Some(SEARCH_PREVIEW_API_VERSION),
115 audience: "https://search.azure.com",
116 spec_path: Some("specification/search/data-plane/Search/stable"),
117 preview_spec_path: Some("specification/search/data-plane/Search/preview"),
118 route_versioned: false,
119 arm: None,
120 hold: None,
121 },
122 ProviderMeta {
123 provider: Provider::FoundryData,
124 label: "Microsoft Foundry data plane",
125 stable: FOUNDRY_API_VERSION,
126 preview: None,
127 audience: "https://ai.azure.com",
128 spec_path: None,
129 preview_spec_path: None,
130 route_versioned: true,
131 arm: None,
132 hold: None,
133 },
134 ProviderMeta {
135 provider: Provider::CognitiveServicesArm,
136 label: "Microsoft.CognitiveServices ARM",
137 stable: ARM_COGNITIVE_API_VERSION,
138 preview: None,
139 audience: "https://management.azure.com",
140 spec_path: Some(
141 "specification/cognitiveservices/resource-manager/Microsoft.CognitiveServices/stable",
142 ),
143 preview_spec_path: None,
144 route_versioned: false,
145 arm: Some(ArmRegistration {
146 namespace: "Microsoft.CognitiveServices",
147 resource_types: &[
148 "accounts",
149 "accounts/projects",
150 "accounts/projects/connections",
151 ],
152 }),
153 hold: Some(Hold {
154 newer: "2026-07-01",
155 reason: "not registered for accounts/projects/connections (max 2026-05-01 stable)",
156 }),
157 },
158 ProviderMeta {
159 provider: Provider::SearchArm,
160 label: "Microsoft.Search ARM",
161 stable: ARM_SEARCH_API_VERSION,
162 preview: None,
163 audience: "https://management.azure.com",
164 spec_path: Some("specification/search/resource-manager/Microsoft.Search/Search/stable"),
165 preview_spec_path: None,
166 route_versioned: false,
167 arm: Some(ArmRegistration {
168 namespace: "Microsoft.Search",
169 resource_types: &["searchServices"],
170 }),
171 hold: None,
172 },
173 ProviderMeta {
174 provider: Provider::StorageArm,
175 label: "Microsoft.Storage ARM",
176 stable: ARM_STORAGE_API_VERSION,
177 preview: None,
178 audience: "https://management.azure.com",
179 spec_path: Some("specification/storage/resource-manager/Microsoft.Storage/stable"),
180 preview_spec_path: None,
181 route_versioned: false,
182 arm: Some(ArmRegistration {
183 namespace: "Microsoft.Storage",
184 resource_types: &["storageAccounts"],
185 }),
186 hold: None,
187 },
188 ProviderMeta {
189 provider: Provider::WebArm,
190 label: "Microsoft.Web ARM",
191 stable: ARM_WEB_API_VERSION,
192 preview: None,
193 audience: "https://management.azure.com",
194 spec_path: Some("specification/web/resource-manager/Microsoft.Web/AppService/stable"),
195 preview_spec_path: None,
196 route_versioned: false,
197 arm: Some(ArmRegistration {
198 namespace: "Microsoft.Web",
199 resource_types: &["sites"],
200 }),
201 hold: None,
202 },
203 ProviderMeta {
204 provider: Provider::AuthorizationArm,
205 label: "Microsoft.Authorization ARM",
206 stable: ARM_AUTHORIZATION_API_VERSION,
207 preview: None,
208 audience: "https://management.azure.com",
209 spec_path: Some(
210 "specification/authorization/resource-manager/Microsoft.Authorization/Authorization/stable",
211 ),
212 preview_spec_path: None,
213 route_versioned: false,
214 arm: Some(ArmRegistration {
215 namespace: "Microsoft.Authorization",
216 resource_types: &["roleAssignments"],
217 }),
218 hold: None,
219 },
220 ProviderMeta {
221 provider: Provider::ResourcesArm,
222 label: "Microsoft.Resources ARM",
223 stable: ARM_RESOURCES_API_VERSION,
224 preview: None,
225 audience: "https://management.azure.com",
226 spec_path: Some(
227 "specification/resources/resource-manager/Microsoft.Resources/subscriptions/stable",
228 ),
229 preview_spec_path: None,
230 route_versioned: false,
231 arm: None,
232 hold: None,
233 },
234 ProviderMeta {
236 provider: Provider::ManagedIdentityArm,
237 label: "Microsoft.ManagedIdentity ARM",
238 stable: ARM_MANAGED_IDENTITY_API_VERSION,
239 preview: None,
240 audience: "https://management.azure.com",
241 spec_path: Some(
242 "specification/msi/resource-manager/Microsoft.ManagedIdentity/ManagedIdentity/stable",
243 ),
244 preview_spec_path: None,
245 route_versioned: false,
246 arm: Some(ArmRegistration {
247 namespace: "Microsoft.ManagedIdentity",
248 resource_types: &["userAssignedIdentities"],
249 }),
250 hold: None,
251 },
252 ProviderMeta {
254 provider: Provider::KeyVaultArm,
255 label: "Microsoft.KeyVault ARM",
256 stable: ARM_KEYVAULT_API_VERSION,
257 preview: None,
258 audience: "https://management.azure.com",
259 spec_path: Some(
260 "specification/keyvault/resource-manager/Microsoft.KeyVault/KeyVault/stable",
261 ),
262 preview_spec_path: None,
263 route_versioned: false,
264 arm: Some(ArmRegistration {
265 namespace: "Microsoft.KeyVault",
266 resource_types: &["vaults"],
267 }),
268 hold: None,
269 },
270 ProviderMeta {
272 provider: Provider::KeyVaultData,
273 label: "Key Vault data plane (secrets)",
274 stable: KEYVAULT_SECRETS_API_VERSION,
275 preview: None,
276 audience: "https://vault.azure.net",
277 spec_path: Some("specification/keyvault/data-plane/Secrets/stable"),
278 preview_spec_path: None,
279 route_versioned: false,
280 arm: None,
281 hold: None,
282 },
283 ProviderMeta {
285 provider: Provider::Graph,
286 label: "Microsoft Graph",
287 stable: GRAPH_API_VERSION,
288 preview: None,
289 audience: "https://graph.microsoft.com",
290 spec_path: None,
291 preview_spec_path: None,
292 route_versioned: true,
293 arm: None,
294 hold: None,
295 },
296];
297
298pub fn providers() -> &'static [ProviderMeta] {
299 PROVIDERS
300}
301
302pub fn provider(p: Provider) -> &'static ProviderMeta {
303 PROVIDERS
304 .iter()
305 .find(|m| m.provider == p)
306 .expect("every Provider has a table entry")
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum Domain {
312 Search,
314 FoundryData,
316 FoundryArm,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum Channel {
323 Stable,
324 Preview,
325}
326
327#[derive(Debug, Clone, Copy)]
334pub struct RefField {
335 pub path: &'static str,
336 pub to: ResourceKind,
337}
338
339#[derive(Debug, Clone, Copy)]
341pub struct KindMeta {
342 pub kind: ResourceKind,
343 pub domain: Domain,
344 pub collection_path: &'static str,
346 pub dir_name: &'static str,
348 pub channel: Channel,
350 pub volatile_fields: &'static [&'static str],
353 pub read_only_fields: &'static [&'static str],
355 pub secret_fields: &'static [&'static str],
358 pub write_only_fields: &'static [&'static str],
361 pub sidecar_fields: &'static [&'static str],
363 pub reference_fields: &'static [RefField],
365 pub immutable_fields: &'static [&'static str],
369 pub schema_definition: &'static str,
373}
374
375const COMMON_VOLATILE: &[&str] = &["@odata.etag", "@odata.context", "e_tag", "etag"];
376
377static KINDS: &[KindMeta] = &[
378 KindMeta {
379 kind: ResourceKind::DataSource,
380 domain: Domain::Search,
381 collection_path: "datasources",
382 dir_name: "data-sources",
383 channel: Channel::Stable,
384 volatile_fields: COMMON_VOLATILE,
385 read_only_fields: &[],
386 secret_fields: &["credentials.connectionString"],
387 write_only_fields: &["credentials.connectionString"],
388 sidecar_fields: &[],
389 reference_fields: &[],
390 immutable_fields: &[],
391 schema_definition: "SearchIndexerDataSource",
392 },
393 KindMeta {
394 kind: ResourceKind::Index,
395 domain: Domain::Search,
396 collection_path: "indexes",
397 dir_name: "indexes",
398 channel: Channel::Stable,
399 volatile_fields: COMMON_VOLATILE,
400 read_only_fields: &[],
401 secret_fields: &[
402 "encryptionKey.accessCredentials.applicationSecret",
403 "vectorSearch.vectorizers[].azureOpenAIParameters.apiKey",
404 ],
405 write_only_fields: &[],
406 sidecar_fields: &[],
407 reference_fields: &[],
408 immutable_fields: &[],
409 schema_definition: "SearchIndex",
410 },
411 KindMeta {
412 kind: ResourceKind::Skillset,
413 domain: Domain::Search,
414 collection_path: "skillsets",
415 dir_name: "skillsets",
416 channel: Channel::Stable,
417 volatile_fields: COMMON_VOLATILE,
418 read_only_fields: &[],
419 secret_fields: &[
420 "cognitiveServices.key",
421 "skills[].apiKey",
422 "encryptionKey.accessCredentials.applicationSecret",
423 ],
424 write_only_fields: &[],
425 sidecar_fields: &[],
426 reference_fields: &[
427 RefField {
429 path: "knowledgeStore.projections[].objects[].storageContainer",
430 to: ResourceKind::Index,
431 },
432 RefField {
434 path: "indexProjections.selectors[].targetIndexName",
435 to: ResourceKind::Index,
436 },
437 ],
438 immutable_fields: &[],
439 schema_definition: "SearchIndexerSkillset",
440 },
441 KindMeta {
442 kind: ResourceKind::Indexer,
443 domain: Domain::Search,
444 collection_path: "indexers",
445 dir_name: "indexers",
446 channel: Channel::Stable,
447 volatile_fields: COMMON_VOLATILE,
448 read_only_fields: &[],
454 secret_fields: &[],
455 write_only_fields: &[],
456 sidecar_fields: &[],
457 reference_fields: &[
458 RefField {
459 path: "dataSourceName",
460 to: ResourceKind::DataSource,
461 },
462 RefField {
463 path: "targetIndexName",
464 to: ResourceKind::Index,
465 },
466 RefField {
467 path: "skillsetName",
468 to: ResourceKind::Skillset,
469 },
470 ],
471 immutable_fields: &[],
472 schema_definition: "SearchIndexer",
473 },
474 KindMeta {
475 kind: ResourceKind::SynonymMap,
476 domain: Domain::Search,
477 collection_path: "synonymmaps",
478 dir_name: "synonym-maps",
479 channel: Channel::Stable,
480 volatile_fields: COMMON_VOLATILE,
481 read_only_fields: &[],
482 secret_fields: &["encryptionKey.accessCredentials.applicationSecret"],
483 write_only_fields: &[],
484 sidecar_fields: &[],
485 reference_fields: &[],
486 immutable_fields: &[],
487 schema_definition: "SynonymMap",
488 },
489 KindMeta {
490 kind: ResourceKind::Alias,
491 domain: Domain::Search,
492 collection_path: "aliases",
493 dir_name: "aliases",
494 channel: Channel::Stable,
495 volatile_fields: COMMON_VOLATILE,
496 read_only_fields: &[],
497 secret_fields: &[],
498 write_only_fields: &[],
499 sidecar_fields: &[],
500 reference_fields: &[RefField {
501 path: "indexes[]",
502 to: ResourceKind::Index,
503 }],
504 immutable_fields: &[],
505 schema_definition: "SearchAlias",
506 },
507 KindMeta {
508 kind: ResourceKind::KnowledgeSource,
509 domain: Domain::Search,
510 collection_path: "knowledgeSources",
511 dir_name: "knowledge-sources",
512 channel: Channel::Stable,
513 volatile_fields: COMMON_VOLATILE,
514 read_only_fields: &[
521 "azureBlobParameters.createdResources",
522 "indexedOneLakeParameters.createdResources",
523 ],
524 secret_fields: &[
529 "searchIndexParameters.apiKey",
530 "azureBlobParameters.connectionString",
531 ],
532 write_only_fields: &[],
533 sidecar_fields: &[],
534 reference_fields: &[RefField {
535 path: "searchIndexParameters.searchIndexName",
536 to: ResourceKind::Index,
537 }],
538 immutable_fields: &["kind"],
541 schema_definition: "KnowledgeSource",
542 },
543 KindMeta {
544 kind: ResourceKind::KnowledgeBase,
545 domain: Domain::Search,
546 collection_path: "knowledgeBases",
547 dir_name: "knowledge-bases",
548 channel: Channel::Preview,
554 volatile_fields: COMMON_VOLATILE,
555 read_only_fields: &[],
556 secret_fields: &["models[].apiKey", "models[].azureOpenAIParameters.apiKey"],
557 write_only_fields: &[],
558 sidecar_fields: &[],
559 reference_fields: &[RefField {
560 path: "knowledgeSources[].name",
561 to: ResourceKind::KnowledgeSource,
562 }],
563 immutable_fields: &[],
564 schema_definition: "KnowledgeBase",
565 },
566 KindMeta {
567 kind: ResourceKind::Agent,
568 domain: Domain::FoundryData,
569 collection_path: "agents",
570 dir_name: "agents",
571 channel: Channel::Stable,
572 volatile_fields: &[
573 "@odata.etag",
574 "@odata.context",
575 "id",
576 "object",
577 "created_at",
578 "updated_at",
579 "version",
580 "metadata.modified_at",
581 ],
582 read_only_fields: &[],
583 secret_fields: &[],
584 write_only_fields: &[],
585 sidecar_fields: &["instructions"],
586 reference_fields: &[
587 RefField {
588 path: "model",
589 to: ResourceKind::Deployment,
590 },
591 RefField {
592 path: "tools[].project_connection_id",
593 to: ResourceKind::Connection,
594 },
595 ],
596 immutable_fields: &[],
597 schema_definition: "",
598 },
599 KindMeta {
600 kind: ResourceKind::Deployment,
601 domain: Domain::FoundryArm,
602 collection_path: "deployments",
603 dir_name: "deployments",
604 channel: Channel::Stable,
605 volatile_fields: &[
606 "id",
607 "type",
608 "systemData",
609 "etag",
610 "properties.provisioningState",
611 "properties.capabilities",
612 "properties.rateLimits",
613 "properties.model.callRateLimit",
614 "properties.currentCapacity",
615 "properties.deploymentState",
616 ],
617 read_only_fields: &[],
618 secret_fields: &[],
619 write_only_fields: &[],
620 sidecar_fields: &[],
621 reference_fields: &[RefField {
622 path: "properties.raiPolicyName",
623 to: ResourceKind::Guardrail,
624 }],
625 immutable_fields: &[],
626 schema_definition: "Deployment",
627 },
628 KindMeta {
629 kind: ResourceKind::Connection,
630 domain: Domain::FoundryArm,
631 collection_path: "connections",
632 dir_name: "connections",
633 channel: Channel::Stable,
634 volatile_fields: &[
635 "id",
636 "type",
637 "systemData",
638 "etag",
639 "properties.provisioningState",
640 ],
641 read_only_fields: &[],
642 secret_fields: &[
644 "properties.credentials.key",
645 "properties.credentials.keys",
646 "properties.credentials.secret",
647 "properties.credentials.clientSecret",
648 "properties.credentials.pat",
649 "properties.credentials.sas",
650 ],
651 write_only_fields: &[],
652 sidecar_fields: &[],
653 reference_fields: &[],
654 immutable_fields: &[],
655 schema_definition: "ConnectionPropertiesV2",
656 },
657 KindMeta {
658 kind: ResourceKind::Guardrail,
659 domain: Domain::FoundryArm,
660 collection_path: "raiPolicies",
661 dir_name: "guardrails",
662 channel: Channel::Stable,
663 volatile_fields: &["id", "type", "systemData", "etag"],
664 read_only_fields: &[],
665 secret_fields: &[],
666 write_only_fields: &[],
667 sidecar_fields: &[],
668 reference_fields: &[],
669 immutable_fields: &[],
670 schema_definition: "RaiPolicy",
671 },
672];
673
674pub fn all_kinds() -> &'static [ResourceKind] {
676 static ORDER: &[ResourceKind] = &[
677 ResourceKind::DataSource,
678 ResourceKind::Index,
679 ResourceKind::Skillset,
680 ResourceKind::Indexer,
681 ResourceKind::SynonymMap,
682 ResourceKind::Alias,
683 ResourceKind::KnowledgeSource,
684 ResourceKind::KnowledgeBase,
685 ResourceKind::Agent,
686 ResourceKind::Deployment,
687 ResourceKind::Connection,
688 ResourceKind::Guardrail,
689 ];
690 ORDER
691}
692
693pub fn meta(kind: ResourceKind) -> &'static KindMeta {
695 KINDS
696 .iter()
697 .find(|m| m.kind == kind)
698 .expect("registry entry exists for every ResourceKind")
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
705pub enum InfraForm {
706 StorageResourceId,
708 UserAssignedIdentity,
712 OpenAiEndpoint,
716 AiServicesSubdomain,
719 ApiUri,
722 KeyVaultUri,
724 SearchKbMcpUrl,
728 Endpoint,
736}
737
738impl InfraForm {
739 pub fn binding_type_label(&self) -> &'static str {
749 match self {
750 InfraForm::StorageResourceId => "storage",
751 InfraForm::UserAssignedIdentity => "identity",
752 InfraForm::OpenAiEndpoint | InfraForm::AiServicesSubdomain => "ai-services",
753 InfraForm::ApiUri => "function-app or api",
754 InfraForm::KeyVaultUri => "key-vault",
755 InfraForm::SearchKbMcpUrl => "search",
756 InfraForm::Endpoint => "search, ai-services, function-app or api",
757 }
758 }
759}
760
761#[derive(Debug, Clone, Copy)]
768pub struct InfraRef {
769 pub path: &'static str,
770 pub form: InfraForm,
771 pub only_odata_type: Option<&'static str>,
772}
773
774static DATA_SOURCE_INFRA: &[InfraRef] = &[
775 InfraRef {
776 path: "credentials.connectionString",
777 form: InfraForm::StorageResourceId,
778 only_odata_type: None,
779 },
780 InfraRef {
781 path: "identity",
782 form: InfraForm::UserAssignedIdentity,
783 only_odata_type: None,
784 },
785 InfraRef {
786 path: "encryptionKey.keyVaultUri",
787 form: InfraForm::KeyVaultUri,
788 only_odata_type: None,
789 },
790 InfraRef {
791 path: "encryptionKey.identity",
792 form: InfraForm::UserAssignedIdentity,
793 only_odata_type: None,
794 },
795];
796
797static INDEX_INFRA: &[InfraRef] = &[
798 InfraRef {
799 path: "vectorSearch.vectorizers[].azureOpenAIParameters.resourceUri",
800 form: InfraForm::OpenAiEndpoint,
801 only_odata_type: None,
802 },
803 InfraRef {
804 path: "vectorSearch.vectorizers[].azureOpenAIParameters.authIdentity",
805 form: InfraForm::UserAssignedIdentity,
806 only_odata_type: None,
807 },
808 InfraRef {
809 path: "encryptionKey.keyVaultUri",
810 form: InfraForm::KeyVaultUri,
811 only_odata_type: None,
812 },
813 InfraRef {
814 path: "encryptionKey.identity",
815 form: InfraForm::UserAssignedIdentity,
816 only_odata_type: None,
817 },
818];
819
820static SKILLSET_INFRA: &[InfraRef] = &[
821 InfraRef {
822 path: "skills[].resourceUri",
823 form: InfraForm::OpenAiEndpoint,
824 only_odata_type: Some("#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill"),
825 },
826 InfraRef {
827 path: "skills[].authIdentity",
828 form: InfraForm::UserAssignedIdentity,
829 only_odata_type: None,
830 },
831 InfraRef {
832 path: "skills[].uri",
833 form: InfraForm::ApiUri,
834 only_odata_type: Some("#Microsoft.Skills.Custom.WebApiSkill"),
835 },
836 InfraRef {
837 path: "cognitiveServices.subdomainUrl",
838 form: InfraForm::AiServicesSubdomain,
839 only_odata_type: None,
840 },
841 InfraRef {
842 path: "cognitiveServices.identity",
843 form: InfraForm::UserAssignedIdentity,
844 only_odata_type: None,
845 },
846 InfraRef {
847 path: "knowledgeStore.storageConnectionString",
848 form: InfraForm::StorageResourceId,
849 only_odata_type: None,
850 },
851 InfraRef {
852 path: "knowledgeStore.identity",
853 form: InfraForm::UserAssignedIdentity,
854 only_odata_type: None,
855 },
856 InfraRef {
857 path: "encryptionKey.keyVaultUri",
858 form: InfraForm::KeyVaultUri,
859 only_odata_type: None,
860 },
861 InfraRef {
862 path: "encryptionKey.identity",
863 form: InfraForm::UserAssignedIdentity,
864 only_odata_type: None,
865 },
866];
867
868static INDEXER_INFRA: &[InfraRef] = &[
872 InfraRef {
873 path: "encryptionKey.keyVaultUri",
874 form: InfraForm::KeyVaultUri,
875 only_odata_type: None,
876 },
877 InfraRef {
878 path: "encryptionKey.identity",
879 form: InfraForm::UserAssignedIdentity,
880 only_odata_type: None,
881 },
882];
883
884static KNOWLEDGE_SOURCE_INFRA: &[InfraRef] = &[
885 InfraRef {
886 path: "azureBlobParameters.connectionString",
887 form: InfraForm::StorageResourceId,
888 only_odata_type: None,
889 },
890 InfraRef {
891 path: "azureBlobParameters.ingestionParameters.identity",
892 form: InfraForm::UserAssignedIdentity,
893 only_odata_type: None,
894 },
895 InfraRef {
896 path: "azureBlobParameters.ingestionParameters.embeddingModel.azureOpenAIParameters.resourceUri",
897 form: InfraForm::OpenAiEndpoint,
898 only_odata_type: None,
899 },
900 InfraRef {
901 path: "azureBlobParameters.ingestionParameters.embeddingModel.azureOpenAIParameters.authIdentity",
902 form: InfraForm::UserAssignedIdentity,
903 only_odata_type: None,
904 },
905 InfraRef {
906 path: "azureBlobParameters.ingestionParameters.chatCompletionModel.azureOpenAIParameters.resourceUri",
907 form: InfraForm::OpenAiEndpoint,
908 only_odata_type: None,
909 },
910 InfraRef {
911 path: "azureBlobParameters.ingestionParameters.chatCompletionModel.azureOpenAIParameters.authIdentity",
912 form: InfraForm::UserAssignedIdentity,
913 only_odata_type: None,
914 },
915 InfraRef {
916 path: "azureBlobParameters.ingestionParameters.aiServices.uri",
917 form: InfraForm::AiServicesSubdomain,
918 only_odata_type: None,
919 },
920 InfraRef {
921 path: "azureBlobParameters.ingestionParameters.assetStore.connectionString",
922 form: InfraForm::StorageResourceId,
923 only_odata_type: None,
924 },
925 InfraRef {
926 path: "encryptionKey.keyVaultUri",
927 form: InfraForm::KeyVaultUri,
928 only_odata_type: None,
929 },
930 InfraRef {
931 path: "encryptionKey.identity",
932 form: InfraForm::UserAssignedIdentity,
933 only_odata_type: None,
934 },
935];
936
937static KNOWLEDGE_BASE_INFRA: &[InfraRef] = &[
938 InfraRef {
939 path: "models[].azureOpenAIParameters.resourceUri",
940 form: InfraForm::OpenAiEndpoint,
941 only_odata_type: None,
942 },
943 InfraRef {
944 path: "models[].azureOpenAIParameters.authIdentity",
945 form: InfraForm::UserAssignedIdentity,
946 only_odata_type: None,
947 },
948 InfraRef {
949 path: "encryptionKey.keyVaultUri",
950 form: InfraForm::KeyVaultUri,
951 only_odata_type: None,
952 },
953 InfraRef {
954 path: "encryptionKey.identity",
955 form: InfraForm::UserAssignedIdentity,
956 only_odata_type: None,
957 },
958];
959
960static AGENT_INFRA: &[InfraRef] = &[InfraRef {
966 path: "tools[].server_url",
967 form: InfraForm::Endpoint,
968 only_odata_type: None,
969}];
970
971static CONNECTION_INFRA: &[InfraRef] = &[InfraRef {
972 path: "properties.target",
973 form: InfraForm::Endpoint,
974 only_odata_type: None,
975}];
976
977pub fn infra_refs(kind: ResourceKind) -> &'static [InfraRef] {
981 match kind {
982 ResourceKind::DataSource => DATA_SOURCE_INFRA,
983 ResourceKind::Index => INDEX_INFRA,
984 ResourceKind::Skillset => SKILLSET_INFRA,
985 ResourceKind::Indexer => INDEXER_INFRA,
986 ResourceKind::SynonymMap => &[],
987 ResourceKind::Alias => &[],
988 ResourceKind::KnowledgeSource => KNOWLEDGE_SOURCE_INFRA,
989 ResourceKind::KnowledgeBase => KNOWLEDGE_BASE_INFRA,
990 ResourceKind::Agent => AGENT_INFRA,
991 ResourceKind::Deployment => &[],
992 ResourceKind::Connection => CONNECTION_INFRA,
993 ResourceKind::Guardrail => &[],
994 }
995}
996
997pub fn valid_datasource_types(_channel: Channel) -> &'static [&'static str] {
1002 &["azureblob", "adlsgen2"]
1003}
1004
1005pub const X_RIGG_REF: &str = "x-rigg-ref";
1008pub const X_RIGG_API: &str = "x-rigg-api";
1010pub const X_RIGG_PIN: &str = "x-rigg-pin";
1015pub const X_RIGG_AUTH: &str = "x-rigg-auth";
1020pub const X_RIGG_AUTH_FUNCTION_KEY: &str = "function-key";
1023pub const X_RIGG_AUTH_KEY_VAULT_PREFIX: &str = "key-vault:";
1028
1029pub fn parse_key_vault_auth(value: &str) -> Option<(&str, &str)> {
1036 let rest = value.strip_prefix(X_RIGG_AUTH_KEY_VAULT_PREFIX)?;
1037 let (secret, binding) = rest.rsplit_once('@')?;
1038 let (secret, binding) = (secret.trim(), binding.trim());
1039 (!secret.is_empty() && !binding.is_empty()).then_some((secret, binding))
1040}
1041
1042pub fn is_known_auth_annotation(value: &str) -> bool {
1045 value == X_RIGG_AUTH_FUNCTION_KEY || parse_key_vault_auth(value).is_some()
1046}
1047
1048fn collect_path_mut(v: &mut Value, path: &str, f: &mut dyn FnMut(&mut Value)) {
1050 fn walk(v: &mut Value, segments: &[&str], f: &mut dyn FnMut(&mut Value)) {
1051 let Some((head, rest)) = segments.split_first() else {
1052 f(v);
1053 return;
1054 };
1055 if let Some(key) = head.strip_suffix("[]") {
1056 let target = if key.is_empty() {
1057 Some(v)
1058 } else {
1059 v.get_mut(key)
1060 };
1061 if let Some(Value::Array(arr)) = target {
1062 for item in arr {
1063 walk(item, rest, f);
1064 }
1065 }
1066 } else if let Some(next) = v.get_mut(*head) {
1067 walk(next, rest, f);
1068 }
1069 }
1070 let segments: Vec<&str> = path.split('.').collect();
1071 walk(v, &segments, f);
1072}
1073
1074pub fn rename_reference(
1080 kind: ResourceKind,
1081 body: &mut Value,
1082 to: ResourceKind,
1083 old: &str,
1084 new: &str,
1085) {
1086 for rf in meta(kind).reference_fields {
1087 if rf.to != to {
1088 continue;
1089 }
1090 collect_path_mut(body, rf.path, &mut |v| {
1091 if v.as_str() == Some(old) {
1092 *v = Value::String(new.to_string());
1093 }
1094 });
1095 }
1096}
1097
1098pub fn rename_x_rigg_ref(body: &mut Value, dir_name: &str, old: &str, new: &str) {
1104 fn walk(v: &mut Value, from: &str, to: &str) {
1105 match v {
1106 Value::Object(map) => {
1107 for (k, val) in map.iter_mut() {
1108 if k == X_RIGG_REF {
1109 if val.as_str() == Some(from) {
1110 *val = Value::String(to.to_string());
1111 }
1112 } else {
1113 walk(val, from, to);
1114 }
1115 }
1116 }
1117 Value::Array(arr) => {
1118 for item in arr {
1119 walk(item, from, to);
1120 }
1121 }
1122 _ => {}
1123 }
1124 }
1125 walk(
1126 body,
1127 &format!("{dir_name}/{old}"),
1128 &format!("{dir_name}/{new}"),
1129 );
1130}
1131
1132pub fn extract_references(kind: ResourceKind, body: &Value) -> Vec<(ResourceKind, String)> {
1135 let mut out = Vec::new();
1136 for rf in meta(kind).reference_fields {
1137 collect_path(body, rf.path, &mut |v| {
1138 if let Some(s) = v.as_str()
1139 && !s.is_empty()
1140 {
1141 out.push((rf.to, s.to_string()));
1142 }
1143 });
1144 }
1145 collect_x_rigg_refs(body, &mut out);
1146 if kind == ResourceKind::Agent {
1147 collect_portal_agent_refs(body, &mut out);
1148 }
1149 out.sort();
1150 out.dedup();
1151 out
1152}
1153
1154fn collect_portal_agent_refs(v: &Value, out: &mut Vec<(ResourceKind, String)>) {
1159 match v {
1160 Value::Object(map) => {
1161 if let Some(url) = map.get("server_url").and_then(Value::as_str)
1162 && let Some(kb) = parse_kb_mcp_url(url)
1163 {
1164 out.push((ResourceKind::KnowledgeBase, kb));
1165 }
1166 for val in map.values() {
1167 collect_portal_agent_refs(val, out);
1168 }
1169 }
1170 Value::Array(arr) => {
1171 for item in arr {
1172 collect_portal_agent_refs(item, out);
1173 }
1174 }
1175 _ => {}
1176 }
1177}
1178
1179fn parse_kb_mcp_url(url: &str) -> Option<String> {
1181 let rest = url.strip_prefix("https://")?;
1182 let (host, path) = rest.split_once('/')?;
1183 if !host.to_ascii_lowercase().ends_with(".search.windows.net") {
1184 return None;
1185 }
1186 let path = path.split('?').next().unwrap_or(path);
1187 let mut segs = path.split('/').filter(|s| !s.is_empty());
1188 let (a, name, c) = (segs.next()?, segs.next()?, segs.next()?);
1189 (a.eq_ignore_ascii_case("knowledgebases") && c.eq_ignore_ascii_case("mcp"))
1190 .then(|| name.to_string())
1191}
1192
1193pub fn is_platform_managed(kind: ResourceKind, body: &Value) -> bool {
1199 match kind {
1200 ResourceKind::Guardrail => {
1201 let system = body
1202 .pointer("/properties/type")
1203 .and_then(Value::as_str)
1204 .map(|t| t.eq_ignore_ascii_case("SystemManaged"))
1205 .unwrap_or(false);
1206 let name = body.get("name").and_then(Value::as_str).unwrap_or("");
1208 system || name.starts_with("Microsoft.")
1209 }
1210 _ => false,
1211 }
1212}
1213
1214pub fn auto_created_by(
1220 snapshot: &[(ResourceRef, Value)],
1221) -> std::collections::BTreeMap<String, String> {
1222 let mut out = std::collections::BTreeMap::new();
1223 for (r, doc) in snapshot {
1224 if r.kind != ResourceKind::KnowledgeSource {
1225 continue;
1226 }
1227 collect_created_resources(doc, &r.name, &mut out);
1228 }
1229 out
1230}
1231
1232fn collect_created_resources(
1233 v: &Value,
1234 ks_name: &str,
1235 out: &mut std::collections::BTreeMap<String, String>,
1236) {
1237 if let Value::Object(map) = v {
1238 if let Some(Value::Object(created)) = map.get("createdResources") {
1239 for (member, name) in created {
1240 let kind = match member.as_str() {
1241 "datasource" => Some(ResourceKind::DataSource),
1242 "indexer" => Some(ResourceKind::Indexer),
1243 "skillset" => Some(ResourceKind::Skillset),
1244 "index" => Some(ResourceKind::Index),
1245 _ => None, };
1247 if let (Some(kind), Some(name)) = (kind, name.as_str()) {
1248 out.insert(
1249 ResourceRef::new(kind, name.to_string()).key(),
1250 ks_name.to_string(),
1251 );
1252 }
1253 }
1254 }
1255 for val in map.values() {
1256 collect_created_resources(val, ks_name, out);
1257 }
1258 } else if let Value::Array(arr) = v {
1259 for item in arr {
1260 collect_created_resources(item, ks_name, out);
1261 }
1262 }
1263}
1264
1265pub fn immutable_diff(
1271 kind: ResourceKind,
1272 local: &Value,
1273 remote: &Value,
1274) -> Vec<(&'static str, String, String)> {
1275 fn values_at(doc: &Value, path: &str) -> Vec<Value> {
1276 let mut vals = Vec::new();
1277 collect_path(doc, path, &mut |v| vals.push(v.clone()));
1278 vals
1279 }
1280 fn show(vals: &[Value]) -> String {
1281 vals.iter()
1282 .map(|v| {
1283 v.as_str()
1284 .map(str::to_string)
1285 .unwrap_or_else(|| v.to_string())
1286 })
1287 .collect::<Vec<_>>()
1288 .join(",")
1289 }
1290 let mut out = Vec::new();
1291 for path in meta(kind).immutable_fields {
1292 let l = values_at(local, path);
1293 let r = values_at(remote, path);
1294 if l != r {
1295 out.push((*path, show(&r), show(&l)));
1296 }
1297 }
1298 out
1299}
1300
1301fn collect_x_rigg_refs(v: &Value, out: &mut Vec<(ResourceKind, String)>) {
1302 match v {
1303 Value::Object(map) => {
1304 for (k, val) in map {
1305 if k == X_RIGG_REF {
1306 if let Some(s) = val.as_str()
1307 && let Some((dir, name)) = s.split_once('/')
1308 && let Some(kind) = ResourceKind::from_directory_name(dir)
1309 {
1310 out.push((kind, name.to_string()));
1311 }
1312 } else {
1313 collect_x_rigg_refs(val, out);
1314 }
1315 }
1316 }
1317 Value::Array(arr) => {
1318 for item in arr {
1319 collect_x_rigg_refs(item, out);
1320 }
1321 }
1322 _ => {}
1323 }
1324}
1325
1326pub fn collect_path(v: &Value, path: &str, f: &mut dyn FnMut(&Value)) {
1329 fn walk(v: &Value, segments: &[&str], f: &mut dyn FnMut(&Value)) {
1330 let Some((head, rest)) = segments.split_first() else {
1331 f(v);
1332 return;
1333 };
1334 if let Some(key) = head.strip_suffix("[]") {
1335 let target = if key.is_empty() { Some(v) } else { v.get(key) };
1336 if let Some(Value::Array(arr)) = target {
1337 for item in arr {
1338 walk(item, rest, f);
1339 }
1340 }
1341 } else if let Some(next) = v.get(*head) {
1342 walk(next, rest, f);
1343 }
1344 }
1345 let segments: Vec<&str> = path.split('.').collect();
1346 walk(v, &segments, f);
1347}
1348
1349pub fn restore_path(dst: &mut Value, src: &Value, path: &str) {
1372 let segments: Vec<&str> = path.split('.').collect();
1373 restore_path_walk(dst, src, &segments);
1374}
1375
1376fn restore_path_walk(dst: &mut Value, src: &Value, segments: &[&str]) {
1377 let Some((head, rest)) = segments.split_first() else {
1378 *dst = src.clone();
1379 return;
1380 };
1381 if let Some(key) = head.strip_suffix("[]") {
1382 if key.is_empty() {
1383 pair_arrays(dst, src, rest);
1384 } else {
1385 let Value::Object(src_map) = src else { return };
1386 let Some(src_val) = src_map.get(key) else {
1387 return;
1388 };
1389 let Value::Object(dst_map) = dst else { return };
1390 let entry = dst_map
1391 .entry(key.to_string())
1392 .or_insert_with(|| Value::Array(Vec::new()));
1393 pair_arrays(entry, src_val, rest);
1394 }
1395 } else {
1396 let Value::Object(src_map) = src else { return };
1397 let Some(src_val) = src_map.get(*head) else {
1398 return;
1399 };
1400 let Value::Object(dst_map) = dst else { return };
1401 if rest.is_empty() {
1402 dst_map.insert((*head).to_string(), src_val.clone());
1406 } else {
1407 let entry = dst_map
1408 .entry((*head).to_string())
1409 .or_insert_with(|| Value::Object(serde_json::Map::new()));
1410 restore_path_walk(entry, src_val, rest);
1411 }
1412 }
1413}
1414
1415fn pair_arrays(dst: &mut Value, src: &Value, rest: &[&str]) {
1422 let (Value::Array(d), Value::Array(s)) = (dst, src) else {
1423 return;
1424 };
1425 let n = d.len().min(s.len());
1426 for i in 0..n {
1427 restore_path_walk(&mut d[i], &s[i], rest);
1428 }
1429 if s.len() > d.len() {
1430 d.extend(s[n..].iter().cloned());
1431 }
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436 use super::*;
1437 use serde_json::json;
1438
1439 #[test]
1440 fn registry_paths_exist_in_the_pinned_schema() {
1441 use crate::schema::fixture_for;
1442 for kind in ResourceKind::search_kinds() {
1443 let m = meta(kind);
1444 let f = fixture_for(kind);
1445 let props = f
1446 .definition(m.schema_definition)
1447 .expect(m.schema_definition);
1448 let infra: Vec<&'static str> = infra_refs(kind).iter().map(|r| r.path).collect();
1449 for path in m
1450 .volatile_fields
1451 .iter()
1452 .chain(m.read_only_fields)
1453 .chain(m.secret_fields)
1454 .chain(m.write_only_fields)
1455 .chain(m.immutable_fields)
1456 .chain(m.reference_fields.iter().map(|r| &r.path))
1457 .chain(infra.iter())
1458 {
1459 let head = path.split('.').next().unwrap().trim_end_matches("[]");
1460 if head.starts_with("@odata") || head == "etag" || head == "e_tag" {
1461 continue;
1462 }
1463 assert!(
1464 props.contains(head),
1465 "{kind:?}: `{path}` not in {} ({})",
1466 m.schema_definition,
1467 f.version
1468 );
1469 }
1470 }
1471 }
1472
1473 #[test]
1474 fn key_vault_auth_annotations_parse_and_are_recognized() {
1475 assert_eq!(
1476 parse_key_vault_auth("key-vault:fn-key@secrets"),
1477 Some(("fn-key", "secrets"))
1478 );
1479 assert_eq!(
1482 parse_key_vault_auth("key-vault:a@b@vault"),
1483 Some(("a@b", "vault"))
1484 );
1485 assert_eq!(parse_key_vault_auth("key-vault:@vault"), None);
1486 assert_eq!(parse_key_vault_auth("key-vault:secret@"), None);
1487 assert_eq!(parse_key_vault_auth("key-vault:secret"), None);
1488 assert_eq!(parse_key_vault_auth(X_RIGG_AUTH_FUNCTION_KEY), None);
1489
1490 assert!(is_known_auth_annotation(X_RIGG_AUTH_FUNCTION_KEY));
1491 assert!(is_known_auth_annotation("key-vault:fn-key@secrets"));
1492 assert!(!is_known_auth_annotation("managed-identity"));
1493 assert!(!is_known_auth_annotation(""));
1494 }
1495
1496 #[test]
1497 fn infra_ref_table_matches_the_spec() {
1498 let ks: Vec<&str> = infra_refs(ResourceKind::KnowledgeSource)
1499 .iter()
1500 .map(|r| r.path)
1501 .collect();
1502 for p in [
1503 "azureBlobParameters.connectionString",
1504 "azureBlobParameters.ingestionParameters.identity",
1505 "azureBlobParameters.ingestionParameters.embeddingModel.azureOpenAIParameters.resourceUri",
1506 "azureBlobParameters.ingestionParameters.chatCompletionModel.azureOpenAIParameters.resourceUri",
1507 "azureBlobParameters.ingestionParameters.aiServices.uri",
1508 "azureBlobParameters.ingestionParameters.assetStore.connectionString",
1509 "encryptionKey.keyVaultUri",
1510 ] {
1511 assert!(ks.contains(&p), "missing {p}");
1512 }
1513 assert!(infra_refs(ResourceKind::Deployment).is_empty());
1514 assert_eq!(
1515 infra_refs(ResourceKind::Agent)[0].path,
1516 "tools[].server_url"
1517 );
1518
1519 let index: Vec<&str> = infra_refs(ResourceKind::Index)
1520 .iter()
1521 .map(|r| r.path)
1522 .collect();
1523 assert!(index.contains(&"vectorSearch.vectorizers[].azureOpenAIParameters.resourceUri"));
1524
1525 let skillset: Vec<&str> = infra_refs(ResourceKind::Skillset)
1526 .iter()
1527 .map(|r| r.path)
1528 .collect();
1529 assert!(skillset.contains(&"skills[].uri"));
1530
1531 let kb: Vec<&str> = infra_refs(ResourceKind::KnowledgeBase)
1532 .iter()
1533 .map(|r| r.path)
1534 .collect();
1535 assert!(kb.contains(&"models[].azureOpenAIParameters.resourceUri"));
1536
1537 let connection: Vec<&str> = infra_refs(ResourceKind::Connection)
1538 .iter()
1539 .map(|r| r.path)
1540 .collect();
1541 assert!(connection.contains(&"properties.target"));
1542
1543 for kind in all_kinds() {
1547 let paths: Vec<&str> = infra_refs(*kind).iter().map(|r| r.path).collect();
1548 if paths.contains(&"encryptionKey.keyVaultUri") {
1549 assert!(
1550 paths.contains(&"encryptionKey.identity"),
1551 "{kind:?} has a CMK vault reference but no identity reference"
1552 );
1553 }
1554 }
1555
1556 let total: usize = all_kinds().iter().map(|k| infra_refs(*k).len()).sum();
1561 assert_eq!(total, 35);
1562 }
1563
1564 #[test]
1565 fn meta_is_total_and_consistent() {
1566 for kind in all_kinds() {
1567 let m = meta(*kind);
1568 assert_eq!(m.kind, *kind);
1569 assert!(!m.collection_path.is_empty());
1570 assert!(!m.dir_name.is_empty());
1571 }
1572 assert_eq!(all_kinds().len(), 12);
1573 }
1574
1575 #[test]
1576 fn dir_names_unique() {
1577 let mut dirs: Vec<_> = all_kinds().iter().map(|k| meta(*k).dir_name).collect();
1578 dirs.sort();
1579 dirs.dedup();
1580 assert_eq!(dirs.len(), 12);
1581 }
1582
1583 #[test]
1584 fn indexer_references() {
1585 let indexer = json!({
1586 "name": "idxr",
1587 "dataSourceName": "my-ds",
1588 "targetIndexName": "my-index",
1589 "skillsetName": "my-skills"
1590 });
1591 let refs = extract_references(ResourceKind::Indexer, &indexer);
1592 assert!(refs.contains(&(ResourceKind::DataSource, "my-ds".into())));
1593 assert!(refs.contains(&(ResourceKind::Index, "my-index".into())));
1594 assert!(refs.contains(&(ResourceKind::Skillset, "my-skills".into())));
1595 }
1596
1597 #[test]
1598 fn knowledge_base_and_alias_references() {
1599 let kb = json!({
1600 "name": "kb",
1601 "knowledgeSources": [{"name": "ks-a"}, {"name": "ks-b"}]
1602 });
1603 let refs = extract_references(ResourceKind::KnowledgeBase, &kb);
1604 assert_eq!(
1605 refs,
1606 vec![
1607 (ResourceKind::KnowledgeSource, "ks-a".to_string()),
1608 (ResourceKind::KnowledgeSource, "ks-b".to_string()),
1609 ]
1610 );
1611
1612 let alias = json!({"name": "a", "indexes": ["i1"]});
1613 let refs = extract_references(ResourceKind::Alias, &alias);
1614 assert_eq!(refs, vec![(ResourceKind::Index, "i1".to_string())]);
1615 }
1616
1617 #[test]
1618 fn x_rigg_ref_extracted_at_depth() {
1619 let agent = json!({
1620 "name": "agent",
1621 "model": "gpt-5-mini",
1622 "tools": [
1623 {"type": "mcp", "x-rigg-ref": "knowledge-bases/support-kb", "server_url": ""}
1624 ]
1625 });
1626 let refs = extract_references(ResourceKind::Agent, &agent);
1627 assert!(refs.contains(&(ResourceKind::KnowledgeBase, "support-kb".into())));
1628 assert!(refs.contains(&(ResourceKind::Deployment, "gpt-5-mini".into())));
1629 }
1630
1631 #[test]
1632 fn agent_extracts_portal_kb_url_and_connection_id() {
1633 let agent = serde_json::json!({
1634 "name": "Regulus",
1635 "model": "gpt-5.2-chat",
1636 "tools": [{
1637 "type": "mcp",
1638 "server_label": "kb_regulatory_kb",
1639 "server_url": "https://mklabsrch.search.windows.net/knowledgebases/regulatory-kb/mcp?api-version=2026-08-01-preview",
1640 "project_connection_id": "kb-regulatory-kb-9kdyn"
1641 }]
1642 });
1643 let refs = extract_references(ResourceKind::Agent, &agent);
1644 assert!(
1645 refs.contains(&(ResourceKind::KnowledgeBase, "regulatory-kb".to_string())),
1646 "{refs:?}"
1647 );
1648 assert!(
1649 refs.contains(&(
1650 ResourceKind::Connection,
1651 "kb-regulatory-kb-9kdyn".to_string()
1652 )),
1653 "{refs:?}"
1654 );
1655 assert!(
1656 refs.contains(&(ResourceKind::Deployment, "gpt-5.2-chat".to_string())),
1657 "{refs:?}"
1658 );
1659 }
1660
1661 #[test]
1662 fn agent_ignores_non_search_mcp_urls() {
1663 let agent = serde_json::json!({
1664 "name": "a",
1665 "tools": [{"type": "mcp", "server_url": "https://example.com/knowledgebases/x/mcp"}]
1666 });
1667 let refs = extract_references(ResourceKind::Agent, &agent);
1668 assert!(
1669 !refs.iter().any(|(k, _)| *k == ResourceKind::KnowledgeBase),
1670 "{refs:?}"
1671 );
1672 }
1673
1674 #[test]
1675 fn deployment_runtime_state_is_volatile() {
1676 let vf = meta(ResourceKind::Deployment).volatile_fields;
1677 assert!(vf.contains(&"properties.currentCapacity"));
1678 assert!(vf.contains(&"properties.deploymentState"));
1679 }
1680
1681 #[test]
1682 fn agent_portal_timestamp_is_volatile() {
1683 assert!(
1684 meta(ResourceKind::Agent)
1685 .volatile_fields
1686 .contains(&"metadata.modified_at")
1687 );
1688 }
1689
1690 #[test]
1691 fn is_platform_managed_true_for_system_managed_guardrail() {
1692 let doc = json!({"name": "Microsoft.DefaultV2", "properties": {"type": "SystemManaged"}});
1693 assert!(is_platform_managed(ResourceKind::Guardrail, &doc));
1694 }
1695
1696 #[test]
1697 fn is_platform_managed_false_for_user_managed_guardrail() {
1698 let doc = json!({"name": "my-policy", "properties": {"type": "UserManaged"}});
1699 assert!(!is_platform_managed(ResourceKind::Guardrail, &doc));
1700 }
1701
1702 #[test]
1703 fn is_platform_managed_falls_back_to_name_prefix_without_properties() {
1704 let doc = json!({"name": "Microsoft.Default"});
1705 assert!(is_platform_managed(ResourceKind::Guardrail, &doc));
1706 }
1707
1708 #[test]
1709 fn is_platform_managed_false_for_user_named_guardrail_without_properties() {
1710 let doc = json!({"name": "my-policy"});
1711 assert!(!is_platform_managed(ResourceKind::Guardrail, &doc));
1712 }
1713
1714 #[test]
1715 fn is_platform_managed_only_applies_to_guardrail_kind() {
1716 let doc = json!({"name": "Microsoft.whatever"});
1717 assert!(!is_platform_managed(ResourceKind::Index, &doc));
1718 }
1719
1720 #[test]
1721 fn auto_created_by_finds_nested_created_resources() {
1722 let ks = serde_json::json!({
1724 "name": "regulatory",
1725 "kind": "azureBlob",
1726 "azureBlobParameters": {
1727 "containerName": "regulatory",
1728 "createdResources": {
1729 "datasource": "regulatory-datasource",
1730 "indexer": "regulatory-indexer",
1731 "skillset": "regulatory-skillset",
1732 "index": "regulatory-index",
1733 "somethingFuture": "ignored-name"
1734 }
1735 }
1736 });
1737 let index_doc = serde_json::json!({"name": "regulatory-index"});
1738 let snapshot = vec![
1739 (
1740 ResourceRef::new(ResourceKind::KnowledgeSource, "regulatory".to_string()),
1741 ks,
1742 ),
1743 (
1744 ResourceRef::new(ResourceKind::Index, "regulatory-index".to_string()),
1745 index_doc,
1746 ),
1747 ];
1748 let map = auto_created_by(&snapshot);
1749 assert_eq!(
1750 map.get("indexes/regulatory-index").map(String::as_str),
1751 Some("regulatory")
1752 );
1753 assert_eq!(
1754 map.get("indexers/regulatory-indexer").map(String::as_str),
1755 Some("regulatory")
1756 );
1757 assert_eq!(
1758 map.get("data-sources/regulatory-datasource")
1759 .map(String::as_str),
1760 Some("regulatory")
1761 );
1762 assert_eq!(
1763 map.get("skillsets/regulatory-skillset").map(String::as_str),
1764 Some("regulatory")
1765 );
1766 assert!(
1767 !map.values().any(|v| v == "ignored-name"),
1768 "unknown member names ignored: {map:?}"
1769 );
1770 assert_eq!(map.len(), 4);
1771 }
1772
1773 #[test]
1774 fn auto_created_by_ignores_non_knowledge_source_docs() {
1775 let idx = serde_json::json!({
1776 "name": "i",
1777 "createdResources": {"index": "x"}
1778 });
1779 let snapshot = vec![(ResourceRef::new(ResourceKind::Index, "i".to_string()), idx)];
1780 assert!(auto_created_by(&snapshot).is_empty());
1781 }
1782
1783 #[test]
1784 fn datasource_types_are_blob_only_on_both_channels() {
1785 assert_eq!(
1786 valid_datasource_types(Channel::Stable),
1787 &["azureblob", "adlsgen2"]
1788 );
1789 assert_eq!(
1790 valid_datasource_types(Channel::Preview),
1791 &["azureblob", "adlsgen2"]
1792 );
1793 }
1794
1795 #[test]
1796 fn ks_points_at_index() {
1797 let ks = json!({
1798 "name": "ks",
1799 "kind": "searchIndex",
1800 "searchIndexParameters": {"searchIndexName": "docs"}
1801 });
1802 let refs = extract_references(ResourceKind::KnowledgeSource, &ks);
1803 assert_eq!(refs, vec![(ResourceKind::Index, "docs".to_string())]);
1804 }
1805
1806 #[test]
1807 fn immutable_diff_detects_kind_change() {
1808 let local = json!({"name": "ks", "kind": "searchIndex",
1809 "searchIndexParameters": {"searchIndexName": "docs"}});
1810 let remote = json!({"name": "ks", "kind": "azureBlob",
1811 "azureBlobParameters": {"containerName": "c"}});
1812 let diff = immutable_diff(ResourceKind::KnowledgeSource, &local, &remote);
1813 assert_eq!(
1814 diff,
1815 vec![("kind", "azureBlob".to_string(), "searchIndex".to_string())]
1816 );
1817 }
1818
1819 #[test]
1820 fn immutable_diff_empty_when_kind_unchanged() {
1821 let local = json!({"name": "ks", "kind": "azureBlob", "description": "new"});
1822 let remote = json!({"name": "ks", "kind": "azureBlob"});
1823 assert!(immutable_diff(ResourceKind::KnowledgeSource, &local, &remote).is_empty());
1824 }
1825
1826 #[test]
1827 fn immutable_diff_empty_for_kinds_without_immutable_fields() {
1828 let local = json!({"name": "i", "kind": "a"});
1829 let remote = json!({"name": "i", "kind": "b"});
1830 assert!(immutable_diff(ResourceKind::Index, &local, &remote).is_empty());
1831 }
1832
1833 #[test]
1834 fn immutable_diff_counts_missing_side_as_difference() {
1835 let local = json!({"name": "ks", "kind": "searchIndex"});
1836 let remote = json!({"name": "ks"});
1837 let diff = immutable_diff(ResourceKind::KnowledgeSource, &local, &remote);
1838 assert_eq!(
1839 diff,
1840 vec![("kind", String::new(), "searchIndex".to_string())]
1841 );
1842 }
1843
1844 #[test]
1845 fn knowledge_source_blob_connection_is_credential_material() {
1846 assert!(
1851 meta(ResourceKind::KnowledgeSource)
1852 .secret_fields
1853 .contains(&"azureBlobParameters.connectionString")
1854 );
1855 }
1856
1857 #[test]
1858 fn restore_path_plain_field() {
1859 let mut dst = json!({"name": "b-name", "model": "m1"});
1860 let src = json!({"name": "a-name", "model": "m2"});
1861 restore_path(&mut dst, &src, "name");
1862 assert_eq!(dst["name"], json!("a-name"));
1863 assert_eq!(dst["model"], json!("m1"), "unrelated field untouched");
1864 }
1865
1866 #[test]
1867 fn restore_path_creates_missing_intermediate_objects() {
1868 let mut dst = json!({"name": "x"});
1869 let src = json!({"name": "x", "credentials": {"connectionString": "secret"}});
1870 restore_path(&mut dst, &src, "credentials.connectionString");
1871 assert_eq!(dst["credentials"]["connectionString"], json!("secret"));
1872 }
1873
1874 #[test]
1875 fn restore_path_array_paired_by_index_not_identity() {
1876 let mut dst = json!({
1877 "tools": [
1878 {"type": "mcp", "server_url": "https://dst-a"},
1879 {"type": "mcp", "server_url": "https://dst-b"}
1880 ]
1881 });
1882 let src = json!({
1883 "tools": [
1884 {"type": "mcp", "server_url": "https://src-a"},
1885 {"type": "mcp", "server_url": "https://src-b"}
1886 ]
1887 });
1888 restore_path(&mut dst, &src, "tools[].server_url");
1889 assert_eq!(dst["tools"][0]["server_url"], json!("https://src-a"));
1890 assert_eq!(dst["tools"][1]["server_url"], json!("https://src-b"));
1891 assert_eq!(
1892 dst["tools"][0]["type"],
1893 json!("mcp"),
1894 "unrelated sibling kept"
1895 );
1896 }
1897
1898 #[test]
1899 fn restore_path_array_min_prefix_when_lengths_differ() {
1900 let mut dst = json!({
1903 "tools": [{"server_url": "d1"}, {"server_url": "d2"}, {"server_url": "d3"}]
1904 });
1905 let src = json!({"tools": [{"server_url": "s1"}, {"server_url": "s2"}]});
1906 restore_path(&mut dst, &src, "tools[].server_url");
1907 assert_eq!(dst["tools"][0]["server_url"], json!("s1"));
1908 assert_eq!(dst["tools"][1]["server_url"], json!("s2"));
1909 assert_eq!(
1910 dst["tools"][2]["server_url"],
1911 json!("d3"),
1912 "no src counterpart — left untouched"
1913 );
1914 }
1915
1916 #[test]
1917 fn restore_path_appends_src_only_array_elements_wholesale() {
1918 let mut dst = json!({
1923 "tools": [{"type": "mcp", "server_url": "https://src-a"}]
1924 });
1925 let src = json!({
1926 "tools": [
1927 {"type": "mcp", "server_url": "https://tgt-a"},
1928 {"type": "file_search", "vector_store_ids": ["vs1"]},
1929 {"type": "mcp", "server_url": "https://tgt-c"}
1930 ]
1931 });
1932 restore_path(&mut dst, &src, "tools[].server_url");
1933 let tools = dst["tools"].as_array().unwrap();
1934 assert_eq!(tools.len(), 3, "target-only elements survive: {tools:?}");
1935 assert_eq!(tools[0]["server_url"], json!("https://tgt-a"), "paired");
1936 assert_eq!(
1937 tools[1],
1938 json!({"type": "file_search", "vector_store_ids": ["vs1"]}),
1939 "extra element appended wholesale, not just the leaf field"
1940 );
1941 assert_eq!(tools[2]["server_url"], json!("https://tgt-c"));
1942 }
1943
1944 #[test]
1945 fn restore_path_missing_in_src_leaves_dst_untouched() {
1946 let mut dst = json!({"name": "b", "model": "kept"});
1947 let src = json!({"name": "a"});
1948 restore_path(&mut dst, &src, "model");
1949 assert_eq!(dst["model"], json!("kept"));
1950 }
1951
1952 #[test]
1953 fn restore_path_missing_array_in_src_leaves_dst_untouched() {
1954 let mut dst = json!({"tools": [{"server_url": "kept"}]});
1955 let src = json!({"name": "a"});
1956 restore_path(&mut dst, &src, "tools[].server_url");
1957 assert_eq!(dst["tools"][0]["server_url"], json!("kept"));
1958 }
1959
1960 #[test]
1961 fn provider_table_is_complete_and_current() {
1962 for p in [
1963 Provider::SearchData,
1964 Provider::FoundryData,
1965 Provider::CognitiveServicesArm,
1966 Provider::SearchArm,
1967 Provider::StorageArm,
1968 Provider::WebArm,
1969 Provider::AuthorizationArm,
1970 Provider::ResourcesArm,
1971 Provider::ManagedIdentityArm,
1972 Provider::KeyVaultArm,
1973 Provider::KeyVaultData,
1974 Provider::Graph,
1975 ] {
1976 let m = provider(p);
1977 assert_eq!(m.provider, p);
1978 assert!(!m.stable.is_empty());
1979 assert!(m.audience.starts_with("https://"));
1980 }
1981 assert_eq!(provider(Provider::SearchData).stable, "2026-04-01");
1982 assert_eq!(
1983 provider(Provider::SearchData).preview,
1984 Some("2026-08-01-preview")
1985 );
1986 assert_eq!(
1987 provider(Provider::CognitiveServicesArm).stable,
1988 "2026-05-01"
1989 );
1990 assert_eq!(provider(Provider::SearchArm).stable, "2025-05-01");
1991 assert_eq!(provider(Provider::StorageArm).stable, "2026-06-01");
1992 assert_eq!(provider(Provider::WebArm).stable, "2026-07-15");
1993 assert_eq!(provider(Provider::KeyVaultData).stable, "2025-07-01");
1994 assert!(provider(Provider::FoundryData).route_versioned);
1995 assert!(provider(Provider::Graph).route_versioned);
1996 assert_eq!(providers().len(), 12);
1997 }
1998
1999 #[test]
2000 fn cognitive_services_is_held_at_the_version_arm_registers_for_connections() {
2001 let m = provider(Provider::CognitiveServicesArm);
2002 assert_eq!(m.stable, "2026-05-01");
2003 let hold = m.hold.expect("hold documented");
2004 assert_eq!(hold.newer, "2026-07-01");
2005 let arm = m.arm.expect("arm registration");
2006 assert_eq!(arm.namespace, "Microsoft.CognitiveServices");
2007 assert!(
2008 arm.resource_types
2009 .contains(&"accounts/projects/connections")
2010 );
2011 }
2012
2013 #[test]
2014 fn every_arm_provider_declares_its_registration() {
2015 for m in providers() {
2016 if m.audience == "https://management.azure.com" && m.spec_path.is_some() {
2017 if m.provider == Provider::ResourcesArm {
2019 continue;
2020 }
2021 assert!(m.arm.is_some(), "{} lacks ArmRegistration", m.label);
2022 }
2023 }
2024 }
2025}
2026
2027#[cfg(test)]
2028mod index_projection_ref_tests {
2029 use super::*;
2030 use serde_json::json;
2031
2032 #[test]
2033 fn skillset_index_projections_reference_the_index() {
2034 let ss = json!({
2035 "name": "ss",
2036 "skills": [],
2037 "indexProjections": {
2038 "selectors": [
2039 {"targetIndexName": "proj-index-a"},
2040 {"targetIndexName": "proj-index-b"}
2041 ]
2042 }
2043 });
2044 let refs = extract_references(ResourceKind::Skillset, &ss);
2045 assert!(refs.contains(&(ResourceKind::Index, "proj-index-a".into())));
2046 assert!(refs.contains(&(ResourceKind::Index, "proj-index-b".into())));
2047 }
2048
2049 #[test]
2050 fn rename_reference_rewrites_only_matching_values() {
2051 let mut ss = json!({
2052 "name": "ss",
2053 "indexProjections": {
2054 "selectors": [
2055 {"targetIndexName": "old-index"},
2056 {"targetIndexName": "other-index"}
2057 ]
2058 }
2059 });
2060 rename_reference(
2061 ResourceKind::Skillset,
2062 &mut ss,
2063 ResourceKind::Index,
2064 "old-index",
2065 "new-index",
2066 );
2067 assert_eq!(
2068 ss["indexProjections"]["selectors"][0]["targetIndexName"],
2069 "new-index"
2070 );
2071 assert_eq!(
2072 ss["indexProjections"]["selectors"][1]["targetIndexName"],
2073 "other-index"
2074 );
2075 }
2076
2077 #[test]
2078 fn rename_reference_rewrites_indexer_fields() {
2079 let mut idxr = json!({
2080 "name": "i",
2081 "dataSourceName": "old-ds",
2082 "targetIndexName": "old-index",
2083 "skillsetName": "old-ss"
2084 });
2085 rename_reference(
2086 ResourceKind::Indexer,
2087 &mut idxr,
2088 ResourceKind::DataSource,
2089 "old-ds",
2090 "new-ds",
2091 );
2092 assert_eq!(idxr["dataSourceName"], "new-ds");
2093 assert_eq!(
2094 idxr["targetIndexName"], "old-index",
2095 "other kinds untouched"
2096 );
2097 }
2098
2099 #[test]
2100 fn rename_x_rigg_ref_rewrites_only_the_matching_annotation() {
2101 let mut agent = json!({
2102 "name": "a",
2103 "tools": [
2104 {"type": "mcp", "x-rigg-ref": "knowledge-bases/kb-dev"},
2105 {"type": "mcp", "x-rigg-ref": "knowledge-bases/other"},
2106 {"type": "mcp", "x-rigg-ref": "connections/kb-dev"},
2107 {"type": "mcp", "server_url": "knowledge-bases/kb-dev"}
2108 ]
2109 });
2110 rename_x_rigg_ref(&mut agent, "knowledge-bases", "kb-dev", "kb");
2111 assert_eq!(agent["tools"][0][X_RIGG_REF], "knowledge-bases/kb");
2112 assert_eq!(agent["tools"][1][X_RIGG_REF], "knowledge-bases/other");
2113 assert_eq!(
2114 agent["tools"][2][X_RIGG_REF], "connections/kb-dev",
2115 "another directory is a different resource"
2116 );
2117 assert_eq!(
2118 agent["tools"][3]["server_url"], "knowledge-bases/kb-dev",
2119 "only the annotation key is rewritten"
2120 );
2121 }
2122}