use serde_json::Value;
use crate::resources::traits::{ResourceKind, ResourceRef};
pub const SEARCH_STABLE_API_VERSION: &str = "2026-04-01";
pub const SEARCH_PREVIEW_API_VERSION: &str = "2026-08-01-preview";
pub const FOUNDRY_API_VERSION: &str = "v1";
pub const ARM_COGNITIVE_API_VERSION: &str = "2026-05-01";
pub const ARM_SEARCH_API_VERSION: &str = "2025-05-01";
pub const ARM_STORAGE_API_VERSION: &str = "2026-06-01";
pub const ARM_WEB_API_VERSION: &str = "2026-07-15";
pub const ARM_AUTHORIZATION_API_VERSION: &str = "2022-04-01";
pub const ARM_RESOURCES_API_VERSION: &str = "2022-12-01";
pub const ARM_MANAGED_IDENTITY_API_VERSION: &str = "2024-11-30";
pub const ARM_KEYVAULT_API_VERSION: &str = "2026-02-01";
pub const KEYVAULT_SECRETS_API_VERSION: &str = "2025-07-01";
pub const GRAPH_API_VERSION: &str = "v1.0";
pub const ARM_BASE_URL: &str = "https://management.azure.com";
pub const GRAPH_BASE_URL: &str = "https://graph.microsoft.com/v1.0";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provider {
SearchData,
FoundryData,
CognitiveServicesArm,
SearchArm,
StorageArm,
WebArm,
AuthorizationArm,
ResourcesArm,
ManagedIdentityArm,
KeyVaultArm,
KeyVaultData,
Graph,
}
#[derive(Debug, Clone, Copy)]
pub struct ArmRegistration {
pub namespace: &'static str,
pub resource_types: &'static [&'static str],
}
#[derive(Debug, Clone, Copy)]
pub struct Hold {
pub newer: &'static str,
pub reason: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct ProviderMeta {
pub provider: Provider,
pub label: &'static str,
pub stable: &'static str,
pub preview: Option<&'static str>,
pub audience: &'static str,
pub spec_path: Option<&'static str>,
pub preview_spec_path: Option<&'static str>,
pub route_versioned: bool,
pub arm: Option<ArmRegistration>,
pub hold: Option<Hold>,
}
static PROVIDERS: &[ProviderMeta] = &[
ProviderMeta {
provider: Provider::SearchData,
label: "Azure AI Search data plane",
stable: SEARCH_STABLE_API_VERSION,
preview: Some(SEARCH_PREVIEW_API_VERSION),
audience: "https://search.azure.com",
spec_path: Some("specification/search/data-plane/Search/stable"),
preview_spec_path: Some("specification/search/data-plane/Search/preview"),
route_versioned: false,
arm: None,
hold: None,
},
ProviderMeta {
provider: Provider::FoundryData,
label: "Microsoft Foundry data plane",
stable: FOUNDRY_API_VERSION,
preview: None,
audience: "https://ai.azure.com",
spec_path: None,
preview_spec_path: None,
route_versioned: true,
arm: None,
hold: None,
},
ProviderMeta {
provider: Provider::CognitiveServicesArm,
label: "Microsoft.CognitiveServices ARM",
stable: ARM_COGNITIVE_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some(
"specification/cognitiveservices/resource-manager/Microsoft.CognitiveServices/stable",
),
preview_spec_path: None,
route_versioned: false,
arm: Some(ArmRegistration {
namespace: "Microsoft.CognitiveServices",
resource_types: &[
"accounts",
"accounts/projects",
"accounts/projects/connections",
],
}),
hold: Some(Hold {
newer: "2026-07-01",
reason: "not registered for accounts/projects/connections (max 2026-05-01 stable)",
}),
},
ProviderMeta {
provider: Provider::SearchArm,
label: "Microsoft.Search ARM",
stable: ARM_SEARCH_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some("specification/search/resource-manager/Microsoft.Search/Search/stable"),
preview_spec_path: None,
route_versioned: false,
arm: Some(ArmRegistration {
namespace: "Microsoft.Search",
resource_types: &["searchServices"],
}),
hold: None,
},
ProviderMeta {
provider: Provider::StorageArm,
label: "Microsoft.Storage ARM",
stable: ARM_STORAGE_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some("specification/storage/resource-manager/Microsoft.Storage/stable"),
preview_spec_path: None,
route_versioned: false,
arm: Some(ArmRegistration {
namespace: "Microsoft.Storage",
resource_types: &["storageAccounts"],
}),
hold: None,
},
ProviderMeta {
provider: Provider::WebArm,
label: "Microsoft.Web ARM",
stable: ARM_WEB_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some("specification/web/resource-manager/Microsoft.Web/AppService/stable"),
preview_spec_path: None,
route_versioned: false,
arm: Some(ArmRegistration {
namespace: "Microsoft.Web",
resource_types: &["sites"],
}),
hold: None,
},
ProviderMeta {
provider: Provider::AuthorizationArm,
label: "Microsoft.Authorization ARM",
stable: ARM_AUTHORIZATION_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some(
"specification/authorization/resource-manager/Microsoft.Authorization/Authorization/stable",
),
preview_spec_path: None,
route_versioned: false,
arm: Some(ArmRegistration {
namespace: "Microsoft.Authorization",
resource_types: &["roleAssignments"],
}),
hold: None,
},
ProviderMeta {
provider: Provider::ResourcesArm,
label: "Microsoft.Resources ARM",
stable: ARM_RESOURCES_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some(
"specification/resources/resource-manager/Microsoft.Resources/subscriptions/stable",
),
preview_spec_path: None,
route_versioned: false,
arm: None,
hold: None,
},
ProviderMeta {
provider: Provider::ManagedIdentityArm,
label: "Microsoft.ManagedIdentity ARM",
stable: ARM_MANAGED_IDENTITY_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some(
"specification/msi/resource-manager/Microsoft.ManagedIdentity/ManagedIdentity/stable",
),
preview_spec_path: None,
route_versioned: false,
arm: Some(ArmRegistration {
namespace: "Microsoft.ManagedIdentity",
resource_types: &["userAssignedIdentities"],
}),
hold: None,
},
ProviderMeta {
provider: Provider::KeyVaultArm,
label: "Microsoft.KeyVault ARM",
stable: ARM_KEYVAULT_API_VERSION,
preview: None,
audience: "https://management.azure.com",
spec_path: Some(
"specification/keyvault/resource-manager/Microsoft.KeyVault/KeyVault/stable",
),
preview_spec_path: None,
route_versioned: false,
arm: Some(ArmRegistration {
namespace: "Microsoft.KeyVault",
resource_types: &["vaults"],
}),
hold: None,
},
ProviderMeta {
provider: Provider::KeyVaultData,
label: "Key Vault data plane (secrets)",
stable: KEYVAULT_SECRETS_API_VERSION,
preview: None,
audience: "https://vault.azure.net",
spec_path: Some("specification/keyvault/data-plane/Secrets/stable"),
preview_spec_path: None,
route_versioned: false,
arm: None,
hold: None,
},
ProviderMeta {
provider: Provider::Graph,
label: "Microsoft Graph",
stable: GRAPH_API_VERSION,
preview: None,
audience: "https://graph.microsoft.com",
spec_path: None,
preview_spec_path: None,
route_versioned: true,
arm: None,
hold: None,
},
];
pub fn providers() -> &'static [ProviderMeta] {
PROVIDERS
}
pub fn provider(p: Provider) -> &'static ProviderMeta {
PROVIDERS
.iter()
.find(|m| m.provider == p)
.expect("every Provider has a table entry")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Domain {
Search,
FoundryData,
FoundryArm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Channel {
Stable,
Preview,
}
#[derive(Debug, Clone, Copy)]
pub struct RefField {
pub path: &'static str,
pub to: ResourceKind,
}
#[derive(Debug, Clone, Copy)]
pub struct KindMeta {
pub kind: ResourceKind,
pub domain: Domain,
pub collection_path: &'static str,
pub dir_name: &'static str,
pub channel: Channel,
pub volatile_fields: &'static [&'static str],
pub read_only_fields: &'static [&'static str],
pub secret_fields: &'static [&'static str],
pub write_only_fields: &'static [&'static str],
pub sidecar_fields: &'static [&'static str],
pub reference_fields: &'static [RefField],
pub immutable_fields: &'static [&'static str],
pub schema_definition: &'static str,
}
const COMMON_VOLATILE: &[&str] = &["@odata.etag", "@odata.context", "e_tag", "etag"];
static KINDS: &[KindMeta] = &[
KindMeta {
kind: ResourceKind::DataSource,
domain: Domain::Search,
collection_path: "datasources",
dir_name: "data-sources",
channel: Channel::Stable,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[],
secret_fields: &["credentials.connectionString"],
write_only_fields: &["credentials.connectionString"],
sidecar_fields: &[],
reference_fields: &[],
immutable_fields: &[],
schema_definition: "SearchIndexerDataSource",
},
KindMeta {
kind: ResourceKind::Index,
domain: Domain::Search,
collection_path: "indexes",
dir_name: "indexes",
channel: Channel::Stable,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[],
secret_fields: &[
"encryptionKey.accessCredentials.applicationSecret",
"vectorSearch.vectorizers[].azureOpenAIParameters.apiKey",
],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[],
immutable_fields: &[],
schema_definition: "SearchIndex",
},
KindMeta {
kind: ResourceKind::Skillset,
domain: Domain::Search,
collection_path: "skillsets",
dir_name: "skillsets",
channel: Channel::Stable,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[],
secret_fields: &[
"cognitiveServices.key",
"skills[].apiKey",
"encryptionKey.accessCredentials.applicationSecret",
],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[
RefField {
path: "knowledgeStore.projections[].objects[].storageContainer",
to: ResourceKind::Index,
},
RefField {
path: "indexProjections.selectors[].targetIndexName",
to: ResourceKind::Index,
},
],
immutable_fields: &[],
schema_definition: "SearchIndexerSkillset",
},
KindMeta {
kind: ResourceKind::Indexer,
domain: Domain::Search,
collection_path: "indexers",
dir_name: "indexers",
channel: Channel::Stable,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[],
secret_fields: &[],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[
RefField {
path: "dataSourceName",
to: ResourceKind::DataSource,
},
RefField {
path: "targetIndexName",
to: ResourceKind::Index,
},
RefField {
path: "skillsetName",
to: ResourceKind::Skillset,
},
],
immutable_fields: &[],
schema_definition: "SearchIndexer",
},
KindMeta {
kind: ResourceKind::SynonymMap,
domain: Domain::Search,
collection_path: "synonymmaps",
dir_name: "synonym-maps",
channel: Channel::Stable,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[],
secret_fields: &["encryptionKey.accessCredentials.applicationSecret"],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[],
immutable_fields: &[],
schema_definition: "SynonymMap",
},
KindMeta {
kind: ResourceKind::Alias,
domain: Domain::Search,
collection_path: "aliases",
dir_name: "aliases",
channel: Channel::Stable,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[],
secret_fields: &[],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[RefField {
path: "indexes[]",
to: ResourceKind::Index,
}],
immutable_fields: &[],
schema_definition: "SearchAlias",
},
KindMeta {
kind: ResourceKind::KnowledgeSource,
domain: Domain::Search,
collection_path: "knowledgeSources",
dir_name: "knowledge-sources",
channel: Channel::Stable,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[
"azureBlobParameters.createdResources",
"indexedOneLakeParameters.createdResources",
],
secret_fields: &[
"searchIndexParameters.apiKey",
"azureBlobParameters.connectionString",
],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[RefField {
path: "searchIndexParameters.searchIndexName",
to: ResourceKind::Index,
}],
immutable_fields: &["kind"],
schema_definition: "KnowledgeSource",
},
KindMeta {
kind: ResourceKind::KnowledgeBase,
domain: Domain::Search,
collection_path: "knowledgeBases",
dir_name: "knowledge-bases",
channel: Channel::Preview,
volatile_fields: COMMON_VOLATILE,
read_only_fields: &[],
secret_fields: &["models[].apiKey", "models[].azureOpenAIParameters.apiKey"],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[RefField {
path: "knowledgeSources[].name",
to: ResourceKind::KnowledgeSource,
}],
immutable_fields: &[],
schema_definition: "KnowledgeBase",
},
KindMeta {
kind: ResourceKind::Agent,
domain: Domain::FoundryData,
collection_path: "agents",
dir_name: "agents",
channel: Channel::Stable,
volatile_fields: &[
"@odata.etag",
"@odata.context",
"id",
"object",
"created_at",
"updated_at",
"version",
"metadata.modified_at",
],
read_only_fields: &[],
secret_fields: &[],
write_only_fields: &[],
sidecar_fields: &["instructions"],
reference_fields: &[
RefField {
path: "model",
to: ResourceKind::Deployment,
},
RefField {
path: "tools[].project_connection_id",
to: ResourceKind::Connection,
},
],
immutable_fields: &[],
schema_definition: "",
},
KindMeta {
kind: ResourceKind::Deployment,
domain: Domain::FoundryArm,
collection_path: "deployments",
dir_name: "deployments",
channel: Channel::Stable,
volatile_fields: &[
"id",
"type",
"systemData",
"etag",
"properties.provisioningState",
"properties.capabilities",
"properties.rateLimits",
"properties.model.callRateLimit",
"properties.currentCapacity",
"properties.deploymentState",
],
read_only_fields: &[],
secret_fields: &[],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[RefField {
path: "properties.raiPolicyName",
to: ResourceKind::Guardrail,
}],
immutable_fields: &[],
schema_definition: "Deployment",
},
KindMeta {
kind: ResourceKind::Connection,
domain: Domain::FoundryArm,
collection_path: "connections",
dir_name: "connections",
channel: Channel::Stable,
volatile_fields: &[
"id",
"type",
"systemData",
"etag",
"properties.provisioningState",
],
read_only_fields: &[],
secret_fields: &[
"properties.credentials.key",
"properties.credentials.keys",
"properties.credentials.secret",
"properties.credentials.clientSecret",
"properties.credentials.pat",
"properties.credentials.sas",
],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[],
immutable_fields: &[],
schema_definition: "ConnectionPropertiesV2",
},
KindMeta {
kind: ResourceKind::Guardrail,
domain: Domain::FoundryArm,
collection_path: "raiPolicies",
dir_name: "guardrails",
channel: Channel::Stable,
volatile_fields: &["id", "type", "systemData", "etag"],
read_only_fields: &[],
secret_fields: &[],
write_only_fields: &[],
sidecar_fields: &[],
reference_fields: &[],
immutable_fields: &[],
schema_definition: "RaiPolicy",
},
];
pub fn all_kinds() -> &'static [ResourceKind] {
static ORDER: &[ResourceKind] = &[
ResourceKind::DataSource,
ResourceKind::Index,
ResourceKind::Skillset,
ResourceKind::Indexer,
ResourceKind::SynonymMap,
ResourceKind::Alias,
ResourceKind::KnowledgeSource,
ResourceKind::KnowledgeBase,
ResourceKind::Agent,
ResourceKind::Deployment,
ResourceKind::Connection,
ResourceKind::Guardrail,
];
ORDER
}
pub fn meta(kind: ResourceKind) -> &'static KindMeta {
KINDS
.iter()
.find(|m| m.kind == kind)
.expect("registry entry exists for every ResourceKind")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InfraForm {
StorageResourceId,
UserAssignedIdentity,
OpenAiEndpoint,
AiServicesSubdomain,
ApiUri,
KeyVaultUri,
SearchKbMcpUrl,
Endpoint,
}
impl InfraForm {
pub fn binding_type_label(&self) -> &'static str {
match self {
InfraForm::StorageResourceId => "storage",
InfraForm::UserAssignedIdentity => "identity",
InfraForm::OpenAiEndpoint | InfraForm::AiServicesSubdomain => "ai-services",
InfraForm::ApiUri => "function-app or api",
InfraForm::KeyVaultUri => "key-vault",
InfraForm::SearchKbMcpUrl => "search",
InfraForm::Endpoint => "search, ai-services, function-app or api",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct InfraRef {
pub path: &'static str,
pub form: InfraForm,
pub only_odata_type: Option<&'static str>,
}
static DATA_SOURCE_INFRA: &[InfraRef] = &[
InfraRef {
path: "credentials.connectionString",
form: InfraForm::StorageResourceId,
only_odata_type: None,
},
InfraRef {
path: "identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.keyVaultUri",
form: InfraForm::KeyVaultUri,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
];
static INDEX_INFRA: &[InfraRef] = &[
InfraRef {
path: "vectorSearch.vectorizers[].azureOpenAIParameters.resourceUri",
form: InfraForm::OpenAiEndpoint,
only_odata_type: None,
},
InfraRef {
path: "vectorSearch.vectorizers[].azureOpenAIParameters.authIdentity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.keyVaultUri",
form: InfraForm::KeyVaultUri,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
];
static SKILLSET_INFRA: &[InfraRef] = &[
InfraRef {
path: "skills[].resourceUri",
form: InfraForm::OpenAiEndpoint,
only_odata_type: Some("#Microsoft.Skills.Text.AzureOpenAIEmbeddingSkill"),
},
InfraRef {
path: "skills[].authIdentity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "skills[].uri",
form: InfraForm::ApiUri,
only_odata_type: Some("#Microsoft.Skills.Custom.WebApiSkill"),
},
InfraRef {
path: "cognitiveServices.subdomainUrl",
form: InfraForm::AiServicesSubdomain,
only_odata_type: None,
},
InfraRef {
path: "cognitiveServices.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "knowledgeStore.storageConnectionString",
form: InfraForm::StorageResourceId,
only_odata_type: None,
},
InfraRef {
path: "knowledgeStore.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.keyVaultUri",
form: InfraForm::KeyVaultUri,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
];
static INDEXER_INFRA: &[InfraRef] = &[
InfraRef {
path: "encryptionKey.keyVaultUri",
form: InfraForm::KeyVaultUri,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
];
static KNOWLEDGE_SOURCE_INFRA: &[InfraRef] = &[
InfraRef {
path: "azureBlobParameters.connectionString",
form: InfraForm::StorageResourceId,
only_odata_type: None,
},
InfraRef {
path: "azureBlobParameters.ingestionParameters.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "azureBlobParameters.ingestionParameters.embeddingModel.azureOpenAIParameters.resourceUri",
form: InfraForm::OpenAiEndpoint,
only_odata_type: None,
},
InfraRef {
path: "azureBlobParameters.ingestionParameters.embeddingModel.azureOpenAIParameters.authIdentity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "azureBlobParameters.ingestionParameters.chatCompletionModel.azureOpenAIParameters.resourceUri",
form: InfraForm::OpenAiEndpoint,
only_odata_type: None,
},
InfraRef {
path: "azureBlobParameters.ingestionParameters.chatCompletionModel.azureOpenAIParameters.authIdentity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "azureBlobParameters.ingestionParameters.aiServices.uri",
form: InfraForm::AiServicesSubdomain,
only_odata_type: None,
},
InfraRef {
path: "azureBlobParameters.ingestionParameters.assetStore.connectionString",
form: InfraForm::StorageResourceId,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.keyVaultUri",
form: InfraForm::KeyVaultUri,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
];
static KNOWLEDGE_BASE_INFRA: &[InfraRef] = &[
InfraRef {
path: "models[].azureOpenAIParameters.resourceUri",
form: InfraForm::OpenAiEndpoint,
only_odata_type: None,
},
InfraRef {
path: "models[].azureOpenAIParameters.authIdentity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.keyVaultUri",
form: InfraForm::KeyVaultUri,
only_odata_type: None,
},
InfraRef {
path: "encryptionKey.identity",
form: InfraForm::UserAssignedIdentity,
only_odata_type: None,
},
];
static AGENT_INFRA: &[InfraRef] = &[InfraRef {
path: "tools[].server_url",
form: InfraForm::Endpoint,
only_odata_type: None,
}];
static CONNECTION_INFRA: &[InfraRef] = &[InfraRef {
path: "properties.target",
form: InfraForm::Endpoint,
only_odata_type: None,
}];
pub fn infra_refs(kind: ResourceKind) -> &'static [InfraRef] {
match kind {
ResourceKind::DataSource => DATA_SOURCE_INFRA,
ResourceKind::Index => INDEX_INFRA,
ResourceKind::Skillset => SKILLSET_INFRA,
ResourceKind::Indexer => INDEXER_INFRA,
ResourceKind::SynonymMap => &[],
ResourceKind::Alias => &[],
ResourceKind::KnowledgeSource => KNOWLEDGE_SOURCE_INFRA,
ResourceKind::KnowledgeBase => KNOWLEDGE_BASE_INFRA,
ResourceKind::Agent => AGENT_INFRA,
ResourceKind::Deployment => &[],
ResourceKind::Connection => CONNECTION_INFRA,
ResourceKind::Guardrail => &[],
}
}
pub fn valid_datasource_types(_channel: Channel) -> &'static [&'static str] {
&["azureblob", "adlsgen2"]
}
pub const X_RIGG_REF: &str = "x-rigg-ref";
pub const X_RIGG_API: &str = "x-rigg-api";
pub const X_RIGG_PIN: &str = "x-rigg-pin";
pub const X_RIGG_AUTH: &str = "x-rigg-auth";
pub const X_RIGG_AUTH_FUNCTION_KEY: &str = "function-key";
pub const X_RIGG_AUTH_KEY_VAULT_PREFIX: &str = "key-vault:";
pub fn parse_key_vault_auth(value: &str) -> Option<(&str, &str)> {
let rest = value.strip_prefix(X_RIGG_AUTH_KEY_VAULT_PREFIX)?;
let (secret, binding) = rest.rsplit_once('@')?;
let (secret, binding) = (secret.trim(), binding.trim());
(!secret.is_empty() && !binding.is_empty()).then_some((secret, binding))
}
pub fn is_known_auth_annotation(value: &str) -> bool {
value == X_RIGG_AUTH_FUNCTION_KEY || parse_key_vault_auth(value).is_some()
}
fn collect_path_mut(v: &mut Value, path: &str, f: &mut dyn FnMut(&mut Value)) {
fn walk(v: &mut Value, segments: &[&str], f: &mut dyn FnMut(&mut Value)) {
let Some((head, rest)) = segments.split_first() else {
f(v);
return;
};
if let Some(key) = head.strip_suffix("[]") {
let target = if key.is_empty() {
Some(v)
} else {
v.get_mut(key)
};
if let Some(Value::Array(arr)) = target {
for item in arr {
walk(item, rest, f);
}
}
} else if let Some(next) = v.get_mut(*head) {
walk(next, rest, f);
}
}
let segments: Vec<&str> = path.split('.').collect();
walk(v, &segments, f);
}
pub fn rename_reference(
kind: ResourceKind,
body: &mut Value,
to: ResourceKind,
old: &str,
new: &str,
) {
for rf in meta(kind).reference_fields {
if rf.to != to {
continue;
}
collect_path_mut(body, rf.path, &mut |v| {
if v.as_str() == Some(old) {
*v = Value::String(new.to_string());
}
});
}
}
pub fn rename_x_rigg_ref(body: &mut Value, dir_name: &str, old: &str, new: &str) {
fn walk(v: &mut Value, from: &str, to: &str) {
match v {
Value::Object(map) => {
for (k, val) in map.iter_mut() {
if k == X_RIGG_REF {
if val.as_str() == Some(from) {
*val = Value::String(to.to_string());
}
} else {
walk(val, from, to);
}
}
}
Value::Array(arr) => {
for item in arr {
walk(item, from, to);
}
}
_ => {}
}
}
walk(
body,
&format!("{dir_name}/{old}"),
&format!("{dir_name}/{new}"),
);
}
pub fn extract_references(kind: ResourceKind, body: &Value) -> Vec<(ResourceKind, String)> {
let mut out = Vec::new();
for rf in meta(kind).reference_fields {
collect_path(body, rf.path, &mut |v| {
if let Some(s) = v.as_str()
&& !s.is_empty()
{
out.push((rf.to, s.to_string()));
}
});
}
collect_x_rigg_refs(body, &mut out);
if kind == ResourceKind::Agent {
collect_portal_agent_refs(body, &mut out);
}
out.sort();
out.dedup();
out
}
fn collect_portal_agent_refs(v: &Value, out: &mut Vec<(ResourceKind, String)>) {
match v {
Value::Object(map) => {
if let Some(url) = map.get("server_url").and_then(Value::as_str)
&& let Some(kb) = parse_kb_mcp_url(url)
{
out.push((ResourceKind::KnowledgeBase, kb));
}
for val in map.values() {
collect_portal_agent_refs(val, out);
}
}
Value::Array(arr) => {
for item in arr {
collect_portal_agent_refs(item, out);
}
}
_ => {}
}
}
fn parse_kb_mcp_url(url: &str) -> Option<String> {
let rest = url.strip_prefix("https://")?;
let (host, path) = rest.split_once('/')?;
if !host.to_ascii_lowercase().ends_with(".search.windows.net") {
return None;
}
let path = path.split('?').next().unwrap_or(path);
let mut segs = path.split('/').filter(|s| !s.is_empty());
let (a, name, c) = (segs.next()?, segs.next()?, segs.next()?);
(a.eq_ignore_ascii_case("knowledgebases") && c.eq_ignore_ascii_case("mcp"))
.then(|| name.to_string())
}
pub fn is_platform_managed(kind: ResourceKind, body: &Value) -> bool {
match kind {
ResourceKind::Guardrail => {
let system = body
.pointer("/properties/type")
.and_then(Value::as_str)
.map(|t| t.eq_ignore_ascii_case("SystemManaged"))
.unwrap_or(false);
let name = body.get("name").and_then(Value::as_str).unwrap_or("");
system || name.starts_with("Microsoft.")
}
_ => false,
}
}
pub fn auto_created_by(
snapshot: &[(ResourceRef, Value)],
) -> std::collections::BTreeMap<String, String> {
let mut out = std::collections::BTreeMap::new();
for (r, doc) in snapshot {
if r.kind != ResourceKind::KnowledgeSource {
continue;
}
collect_created_resources(doc, &r.name, &mut out);
}
out
}
fn collect_created_resources(
v: &Value,
ks_name: &str,
out: &mut std::collections::BTreeMap<String, String>,
) {
if let Value::Object(map) = v {
if let Some(Value::Object(created)) = map.get("createdResources") {
for (member, name) in created {
let kind = match member.as_str() {
"datasource" => Some(ResourceKind::DataSource),
"indexer" => Some(ResourceKind::Indexer),
"skillset" => Some(ResourceKind::Skillset),
"index" => Some(ResourceKind::Index),
_ => None, };
if let (Some(kind), Some(name)) = (kind, name.as_str()) {
out.insert(
ResourceRef::new(kind, name.to_string()).key(),
ks_name.to_string(),
);
}
}
}
for val in map.values() {
collect_created_resources(val, ks_name, out);
}
} else if let Value::Array(arr) = v {
for item in arr {
collect_created_resources(item, ks_name, out);
}
}
}
pub fn immutable_diff(
kind: ResourceKind,
local: &Value,
remote: &Value,
) -> Vec<(&'static str, String, String)> {
fn values_at(doc: &Value, path: &str) -> Vec<Value> {
let mut vals = Vec::new();
collect_path(doc, path, &mut |v| vals.push(v.clone()));
vals
}
fn show(vals: &[Value]) -> String {
vals.iter()
.map(|v| {
v.as_str()
.map(str::to_string)
.unwrap_or_else(|| v.to_string())
})
.collect::<Vec<_>>()
.join(",")
}
let mut out = Vec::new();
for path in meta(kind).immutable_fields {
let l = values_at(local, path);
let r = values_at(remote, path);
if l != r {
out.push((*path, show(&r), show(&l)));
}
}
out
}
fn collect_x_rigg_refs(v: &Value, out: &mut Vec<(ResourceKind, String)>) {
match v {
Value::Object(map) => {
for (k, val) in map {
if k == X_RIGG_REF {
if let Some(s) = val.as_str()
&& let Some((dir, name)) = s.split_once('/')
&& let Some(kind) = ResourceKind::from_directory_name(dir)
{
out.push((kind, name.to_string()));
}
} else {
collect_x_rigg_refs(val, out);
}
}
}
Value::Array(arr) => {
for item in arr {
collect_x_rigg_refs(item, out);
}
}
_ => {}
}
}
pub fn collect_path(v: &Value, path: &str, f: &mut dyn FnMut(&Value)) {
fn walk(v: &Value, segments: &[&str], f: &mut dyn FnMut(&Value)) {
let Some((head, rest)) = segments.split_first() else {
f(v);
return;
};
if let Some(key) = head.strip_suffix("[]") {
let target = if key.is_empty() { Some(v) } else { v.get(key) };
if let Some(Value::Array(arr)) = target {
for item in arr {
walk(item, rest, f);
}
}
} else if let Some(next) = v.get(*head) {
walk(next, rest, f);
}
}
let segments: Vec<&str> = path.split('.').collect();
walk(v, &segments, f);
}
pub fn restore_path(dst: &mut Value, src: &Value, path: &str) {
let segments: Vec<&str> = path.split('.').collect();
restore_path_walk(dst, src, &segments);
}
fn restore_path_walk(dst: &mut Value, src: &Value, segments: &[&str]) {
let Some((head, rest)) = segments.split_first() else {
*dst = src.clone();
return;
};
if let Some(key) = head.strip_suffix("[]") {
if key.is_empty() {
pair_arrays(dst, src, rest);
} else {
let Value::Object(src_map) = src else { return };
let Some(src_val) = src_map.get(key) else {
return;
};
let Value::Object(dst_map) = dst else { return };
let entry = dst_map
.entry(key.to_string())
.or_insert_with(|| Value::Array(Vec::new()));
pair_arrays(entry, src_val, rest);
}
} else {
let Value::Object(src_map) = src else { return };
let Some(src_val) = src_map.get(*head) else {
return;
};
let Value::Object(dst_map) = dst else { return };
if rest.is_empty() {
dst_map.insert((*head).to_string(), src_val.clone());
} else {
let entry = dst_map
.entry((*head).to_string())
.or_insert_with(|| Value::Object(serde_json::Map::new()));
restore_path_walk(entry, src_val, rest);
}
}
}
fn pair_arrays(dst: &mut Value, src: &Value, rest: &[&str]) {
let (Value::Array(d), Value::Array(s)) = (dst, src) else {
return;
};
let n = d.len().min(s.len());
for i in 0..n {
restore_path_walk(&mut d[i], &s[i], rest);
}
if s.len() > d.len() {
d.extend(s[n..].iter().cloned());
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn registry_paths_exist_in_the_pinned_schema() {
use crate::schema::fixture_for;
for kind in ResourceKind::search_kinds() {
let m = meta(kind);
let f = fixture_for(kind);
let props = f
.definition(m.schema_definition)
.expect(m.schema_definition);
let infra: Vec<&'static str> = infra_refs(kind).iter().map(|r| r.path).collect();
for path in m
.volatile_fields
.iter()
.chain(m.read_only_fields)
.chain(m.secret_fields)
.chain(m.write_only_fields)
.chain(m.immutable_fields)
.chain(m.reference_fields.iter().map(|r| &r.path))
.chain(infra.iter())
{
let head = path.split('.').next().unwrap().trim_end_matches("[]");
if head.starts_with("@odata") || head == "etag" || head == "e_tag" {
continue;
}
assert!(
props.contains(head),
"{kind:?}: `{path}` not in {} ({})",
m.schema_definition,
f.version
);
}
}
}
#[test]
fn key_vault_auth_annotations_parse_and_are_recognized() {
assert_eq!(
parse_key_vault_auth("key-vault:fn-key@secrets"),
Some(("fn-key", "secrets"))
);
assert_eq!(
parse_key_vault_auth("key-vault:a@b@vault"),
Some(("a@b", "vault"))
);
assert_eq!(parse_key_vault_auth("key-vault:@vault"), None);
assert_eq!(parse_key_vault_auth("key-vault:secret@"), None);
assert_eq!(parse_key_vault_auth("key-vault:secret"), None);
assert_eq!(parse_key_vault_auth(X_RIGG_AUTH_FUNCTION_KEY), None);
assert!(is_known_auth_annotation(X_RIGG_AUTH_FUNCTION_KEY));
assert!(is_known_auth_annotation("key-vault:fn-key@secrets"));
assert!(!is_known_auth_annotation("managed-identity"));
assert!(!is_known_auth_annotation(""));
}
#[test]
fn infra_ref_table_matches_the_spec() {
let ks: Vec<&str> = infra_refs(ResourceKind::KnowledgeSource)
.iter()
.map(|r| r.path)
.collect();
for p in [
"azureBlobParameters.connectionString",
"azureBlobParameters.ingestionParameters.identity",
"azureBlobParameters.ingestionParameters.embeddingModel.azureOpenAIParameters.resourceUri",
"azureBlobParameters.ingestionParameters.chatCompletionModel.azureOpenAIParameters.resourceUri",
"azureBlobParameters.ingestionParameters.aiServices.uri",
"azureBlobParameters.ingestionParameters.assetStore.connectionString",
"encryptionKey.keyVaultUri",
] {
assert!(ks.contains(&p), "missing {p}");
}
assert!(infra_refs(ResourceKind::Deployment).is_empty());
assert_eq!(
infra_refs(ResourceKind::Agent)[0].path,
"tools[].server_url"
);
let index: Vec<&str> = infra_refs(ResourceKind::Index)
.iter()
.map(|r| r.path)
.collect();
assert!(index.contains(&"vectorSearch.vectorizers[].azureOpenAIParameters.resourceUri"));
let skillset: Vec<&str> = infra_refs(ResourceKind::Skillset)
.iter()
.map(|r| r.path)
.collect();
assert!(skillset.contains(&"skills[].uri"));
let kb: Vec<&str> = infra_refs(ResourceKind::KnowledgeBase)
.iter()
.map(|r| r.path)
.collect();
assert!(kb.contains(&"models[].azureOpenAIParameters.resourceUri"));
let connection: Vec<&str> = infra_refs(ResourceKind::Connection)
.iter()
.map(|r| r.path)
.collect();
assert!(connection.contains(&"properties.target"));
for kind in all_kinds() {
let paths: Vec<&str> = infra_refs(*kind).iter().map(|r| r.path).collect();
if paths.contains(&"encryptionKey.keyVaultUri") {
assert!(
paths.contains(&"encryptionKey.identity"),
"{kind:?} has a CMK vault reference but no identity reference"
);
}
}
let total: usize = all_kinds().iter().map(|k| infra_refs(*k).len()).sum();
assert_eq!(total, 35);
}
#[test]
fn meta_is_total_and_consistent() {
for kind in all_kinds() {
let m = meta(*kind);
assert_eq!(m.kind, *kind);
assert!(!m.collection_path.is_empty());
assert!(!m.dir_name.is_empty());
}
assert_eq!(all_kinds().len(), 12);
}
#[test]
fn dir_names_unique() {
let mut dirs: Vec<_> = all_kinds().iter().map(|k| meta(*k).dir_name).collect();
dirs.sort();
dirs.dedup();
assert_eq!(dirs.len(), 12);
}
#[test]
fn indexer_references() {
let indexer = json!({
"name": "idxr",
"dataSourceName": "my-ds",
"targetIndexName": "my-index",
"skillsetName": "my-skills"
});
let refs = extract_references(ResourceKind::Indexer, &indexer);
assert!(refs.contains(&(ResourceKind::DataSource, "my-ds".into())));
assert!(refs.contains(&(ResourceKind::Index, "my-index".into())));
assert!(refs.contains(&(ResourceKind::Skillset, "my-skills".into())));
}
#[test]
fn knowledge_base_and_alias_references() {
let kb = json!({
"name": "kb",
"knowledgeSources": [{"name": "ks-a"}, {"name": "ks-b"}]
});
let refs = extract_references(ResourceKind::KnowledgeBase, &kb);
assert_eq!(
refs,
vec![
(ResourceKind::KnowledgeSource, "ks-a".to_string()),
(ResourceKind::KnowledgeSource, "ks-b".to_string()),
]
);
let alias = json!({"name": "a", "indexes": ["i1"]});
let refs = extract_references(ResourceKind::Alias, &alias);
assert_eq!(refs, vec![(ResourceKind::Index, "i1".to_string())]);
}
#[test]
fn x_rigg_ref_extracted_at_depth() {
let agent = json!({
"name": "agent",
"model": "gpt-5-mini",
"tools": [
{"type": "mcp", "x-rigg-ref": "knowledge-bases/support-kb", "server_url": ""}
]
});
let refs = extract_references(ResourceKind::Agent, &agent);
assert!(refs.contains(&(ResourceKind::KnowledgeBase, "support-kb".into())));
assert!(refs.contains(&(ResourceKind::Deployment, "gpt-5-mini".into())));
}
#[test]
fn agent_extracts_portal_kb_url_and_connection_id() {
let agent = serde_json::json!({
"name": "Regulus",
"model": "gpt-5.2-chat",
"tools": [{
"type": "mcp",
"server_label": "kb_regulatory_kb",
"server_url": "https://mklabsrch.search.windows.net/knowledgebases/regulatory-kb/mcp?api-version=2026-08-01-preview",
"project_connection_id": "kb-regulatory-kb-9kdyn"
}]
});
let refs = extract_references(ResourceKind::Agent, &agent);
assert!(
refs.contains(&(ResourceKind::KnowledgeBase, "regulatory-kb".to_string())),
"{refs:?}"
);
assert!(
refs.contains(&(
ResourceKind::Connection,
"kb-regulatory-kb-9kdyn".to_string()
)),
"{refs:?}"
);
assert!(
refs.contains(&(ResourceKind::Deployment, "gpt-5.2-chat".to_string())),
"{refs:?}"
);
}
#[test]
fn agent_ignores_non_search_mcp_urls() {
let agent = serde_json::json!({
"name": "a",
"tools": [{"type": "mcp", "server_url": "https://example.com/knowledgebases/x/mcp"}]
});
let refs = extract_references(ResourceKind::Agent, &agent);
assert!(
!refs.iter().any(|(k, _)| *k == ResourceKind::KnowledgeBase),
"{refs:?}"
);
}
#[test]
fn deployment_runtime_state_is_volatile() {
let vf = meta(ResourceKind::Deployment).volatile_fields;
assert!(vf.contains(&"properties.currentCapacity"));
assert!(vf.contains(&"properties.deploymentState"));
}
#[test]
fn agent_portal_timestamp_is_volatile() {
assert!(
meta(ResourceKind::Agent)
.volatile_fields
.contains(&"metadata.modified_at")
);
}
#[test]
fn is_platform_managed_true_for_system_managed_guardrail() {
let doc = json!({"name": "Microsoft.DefaultV2", "properties": {"type": "SystemManaged"}});
assert!(is_platform_managed(ResourceKind::Guardrail, &doc));
}
#[test]
fn is_platform_managed_false_for_user_managed_guardrail() {
let doc = json!({"name": "my-policy", "properties": {"type": "UserManaged"}});
assert!(!is_platform_managed(ResourceKind::Guardrail, &doc));
}
#[test]
fn is_platform_managed_falls_back_to_name_prefix_without_properties() {
let doc = json!({"name": "Microsoft.Default"});
assert!(is_platform_managed(ResourceKind::Guardrail, &doc));
}
#[test]
fn is_platform_managed_false_for_user_named_guardrail_without_properties() {
let doc = json!({"name": "my-policy"});
assert!(!is_platform_managed(ResourceKind::Guardrail, &doc));
}
#[test]
fn is_platform_managed_only_applies_to_guardrail_kind() {
let doc = json!({"name": "Microsoft.whatever"});
assert!(!is_platform_managed(ResourceKind::Index, &doc));
}
#[test]
fn auto_created_by_finds_nested_created_resources() {
let ks = serde_json::json!({
"name": "regulatory",
"kind": "azureBlob",
"azureBlobParameters": {
"containerName": "regulatory",
"createdResources": {
"datasource": "regulatory-datasource",
"indexer": "regulatory-indexer",
"skillset": "regulatory-skillset",
"index": "regulatory-index",
"somethingFuture": "ignored-name"
}
}
});
let index_doc = serde_json::json!({"name": "regulatory-index"});
let snapshot = vec![
(
ResourceRef::new(ResourceKind::KnowledgeSource, "regulatory".to_string()),
ks,
),
(
ResourceRef::new(ResourceKind::Index, "regulatory-index".to_string()),
index_doc,
),
];
let map = auto_created_by(&snapshot);
assert_eq!(
map.get("indexes/regulatory-index").map(String::as_str),
Some("regulatory")
);
assert_eq!(
map.get("indexers/regulatory-indexer").map(String::as_str),
Some("regulatory")
);
assert_eq!(
map.get("data-sources/regulatory-datasource")
.map(String::as_str),
Some("regulatory")
);
assert_eq!(
map.get("skillsets/regulatory-skillset").map(String::as_str),
Some("regulatory")
);
assert!(
!map.values().any(|v| v == "ignored-name"),
"unknown member names ignored: {map:?}"
);
assert_eq!(map.len(), 4);
}
#[test]
fn auto_created_by_ignores_non_knowledge_source_docs() {
let idx = serde_json::json!({
"name": "i",
"createdResources": {"index": "x"}
});
let snapshot = vec![(ResourceRef::new(ResourceKind::Index, "i".to_string()), idx)];
assert!(auto_created_by(&snapshot).is_empty());
}
#[test]
fn datasource_types_are_blob_only_on_both_channels() {
assert_eq!(
valid_datasource_types(Channel::Stable),
&["azureblob", "adlsgen2"]
);
assert_eq!(
valid_datasource_types(Channel::Preview),
&["azureblob", "adlsgen2"]
);
}
#[test]
fn ks_points_at_index() {
let ks = json!({
"name": "ks",
"kind": "searchIndex",
"searchIndexParameters": {"searchIndexName": "docs"}
});
let refs = extract_references(ResourceKind::KnowledgeSource, &ks);
assert_eq!(refs, vec![(ResourceKind::Index, "docs".to_string())]);
}
#[test]
fn immutable_diff_detects_kind_change() {
let local = json!({"name": "ks", "kind": "searchIndex",
"searchIndexParameters": {"searchIndexName": "docs"}});
let remote = json!({"name": "ks", "kind": "azureBlob",
"azureBlobParameters": {"containerName": "c"}});
let diff = immutable_diff(ResourceKind::KnowledgeSource, &local, &remote);
assert_eq!(
diff,
vec![("kind", "azureBlob".to_string(), "searchIndex".to_string())]
);
}
#[test]
fn immutable_diff_empty_when_kind_unchanged() {
let local = json!({"name": "ks", "kind": "azureBlob", "description": "new"});
let remote = json!({"name": "ks", "kind": "azureBlob"});
assert!(immutable_diff(ResourceKind::KnowledgeSource, &local, &remote).is_empty());
}
#[test]
fn immutable_diff_empty_for_kinds_without_immutable_fields() {
let local = json!({"name": "i", "kind": "a"});
let remote = json!({"name": "i", "kind": "b"});
assert!(immutable_diff(ResourceKind::Index, &local, &remote).is_empty());
}
#[test]
fn immutable_diff_counts_missing_side_as_difference() {
let local = json!({"name": "ks", "kind": "searchIndex"});
let remote = json!({"name": "ks"});
let diff = immutable_diff(ResourceKind::KnowledgeSource, &local, &remote);
assert_eq!(
diff,
vec![("kind", String::new(), "searchIndex".to_string())]
);
}
#[test]
fn knowledge_source_blob_connection_is_credential_material() {
assert!(
meta(ResourceKind::KnowledgeSource)
.secret_fields
.contains(&"azureBlobParameters.connectionString")
);
}
#[test]
fn restore_path_plain_field() {
let mut dst = json!({"name": "b-name", "model": "m1"});
let src = json!({"name": "a-name", "model": "m2"});
restore_path(&mut dst, &src, "name");
assert_eq!(dst["name"], json!("a-name"));
assert_eq!(dst["model"], json!("m1"), "unrelated field untouched");
}
#[test]
fn restore_path_creates_missing_intermediate_objects() {
let mut dst = json!({"name": "x"});
let src = json!({"name": "x", "credentials": {"connectionString": "secret"}});
restore_path(&mut dst, &src, "credentials.connectionString");
assert_eq!(dst["credentials"]["connectionString"], json!("secret"));
}
#[test]
fn restore_path_array_paired_by_index_not_identity() {
let mut dst = json!({
"tools": [
{"type": "mcp", "server_url": "https://dst-a"},
{"type": "mcp", "server_url": "https://dst-b"}
]
});
let src = json!({
"tools": [
{"type": "mcp", "server_url": "https://src-a"},
{"type": "mcp", "server_url": "https://src-b"}
]
});
restore_path(&mut dst, &src, "tools[].server_url");
assert_eq!(dst["tools"][0]["server_url"], json!("https://src-a"));
assert_eq!(dst["tools"][1]["server_url"], json!("https://src-b"));
assert_eq!(
dst["tools"][0]["type"],
json!("mcp"),
"unrelated sibling kept"
);
}
#[test]
fn restore_path_array_min_prefix_when_lengths_differ() {
let mut dst = json!({
"tools": [{"server_url": "d1"}, {"server_url": "d2"}, {"server_url": "d3"}]
});
let src = json!({"tools": [{"server_url": "s1"}, {"server_url": "s2"}]});
restore_path(&mut dst, &src, "tools[].server_url");
assert_eq!(dst["tools"][0]["server_url"], json!("s1"));
assert_eq!(dst["tools"][1]["server_url"], json!("s2"));
assert_eq!(
dst["tools"][2]["server_url"],
json!("d3"),
"no src counterpart — left untouched"
);
}
#[test]
fn restore_path_appends_src_only_array_elements_wholesale() {
let mut dst = json!({
"tools": [{"type": "mcp", "server_url": "https://src-a"}]
});
let src = json!({
"tools": [
{"type": "mcp", "server_url": "https://tgt-a"},
{"type": "file_search", "vector_store_ids": ["vs1"]},
{"type": "mcp", "server_url": "https://tgt-c"}
]
});
restore_path(&mut dst, &src, "tools[].server_url");
let tools = dst["tools"].as_array().unwrap();
assert_eq!(tools.len(), 3, "target-only elements survive: {tools:?}");
assert_eq!(tools[0]["server_url"], json!("https://tgt-a"), "paired");
assert_eq!(
tools[1],
json!({"type": "file_search", "vector_store_ids": ["vs1"]}),
"extra element appended wholesale, not just the leaf field"
);
assert_eq!(tools[2]["server_url"], json!("https://tgt-c"));
}
#[test]
fn restore_path_missing_in_src_leaves_dst_untouched() {
let mut dst = json!({"name": "b", "model": "kept"});
let src = json!({"name": "a"});
restore_path(&mut dst, &src, "model");
assert_eq!(dst["model"], json!("kept"));
}
#[test]
fn restore_path_missing_array_in_src_leaves_dst_untouched() {
let mut dst = json!({"tools": [{"server_url": "kept"}]});
let src = json!({"name": "a"});
restore_path(&mut dst, &src, "tools[].server_url");
assert_eq!(dst["tools"][0]["server_url"], json!("kept"));
}
#[test]
fn provider_table_is_complete_and_current() {
for p in [
Provider::SearchData,
Provider::FoundryData,
Provider::CognitiveServicesArm,
Provider::SearchArm,
Provider::StorageArm,
Provider::WebArm,
Provider::AuthorizationArm,
Provider::ResourcesArm,
Provider::ManagedIdentityArm,
Provider::KeyVaultArm,
Provider::KeyVaultData,
Provider::Graph,
] {
let m = provider(p);
assert_eq!(m.provider, p);
assert!(!m.stable.is_empty());
assert!(m.audience.starts_with("https://"));
}
assert_eq!(provider(Provider::SearchData).stable, "2026-04-01");
assert_eq!(
provider(Provider::SearchData).preview,
Some("2026-08-01-preview")
);
assert_eq!(
provider(Provider::CognitiveServicesArm).stable,
"2026-05-01"
);
assert_eq!(provider(Provider::SearchArm).stable, "2025-05-01");
assert_eq!(provider(Provider::StorageArm).stable, "2026-06-01");
assert_eq!(provider(Provider::WebArm).stable, "2026-07-15");
assert_eq!(provider(Provider::KeyVaultData).stable, "2025-07-01");
assert!(provider(Provider::FoundryData).route_versioned);
assert!(provider(Provider::Graph).route_versioned);
assert_eq!(providers().len(), 12);
}
#[test]
fn cognitive_services_is_held_at_the_version_arm_registers_for_connections() {
let m = provider(Provider::CognitiveServicesArm);
assert_eq!(m.stable, "2026-05-01");
let hold = m.hold.expect("hold documented");
assert_eq!(hold.newer, "2026-07-01");
let arm = m.arm.expect("arm registration");
assert_eq!(arm.namespace, "Microsoft.CognitiveServices");
assert!(
arm.resource_types
.contains(&"accounts/projects/connections")
);
}
#[test]
fn every_arm_provider_declares_its_registration() {
for m in providers() {
if m.audience == "https://management.azure.com" && m.spec_path.is_some() {
if m.provider == Provider::ResourcesArm {
continue;
}
assert!(m.arm.is_some(), "{} lacks ArmRegistration", m.label);
}
}
}
}
#[cfg(test)]
mod index_projection_ref_tests {
use super::*;
use serde_json::json;
#[test]
fn skillset_index_projections_reference_the_index() {
let ss = json!({
"name": "ss",
"skills": [],
"indexProjections": {
"selectors": [
{"targetIndexName": "proj-index-a"},
{"targetIndexName": "proj-index-b"}
]
}
});
let refs = extract_references(ResourceKind::Skillset, &ss);
assert!(refs.contains(&(ResourceKind::Index, "proj-index-a".into())));
assert!(refs.contains(&(ResourceKind::Index, "proj-index-b".into())));
}
#[test]
fn rename_reference_rewrites_only_matching_values() {
let mut ss = json!({
"name": "ss",
"indexProjections": {
"selectors": [
{"targetIndexName": "old-index"},
{"targetIndexName": "other-index"}
]
}
});
rename_reference(
ResourceKind::Skillset,
&mut ss,
ResourceKind::Index,
"old-index",
"new-index",
);
assert_eq!(
ss["indexProjections"]["selectors"][0]["targetIndexName"],
"new-index"
);
assert_eq!(
ss["indexProjections"]["selectors"][1]["targetIndexName"],
"other-index"
);
}
#[test]
fn rename_reference_rewrites_indexer_fields() {
let mut idxr = json!({
"name": "i",
"dataSourceName": "old-ds",
"targetIndexName": "old-index",
"skillsetName": "old-ss"
});
rename_reference(
ResourceKind::Indexer,
&mut idxr,
ResourceKind::DataSource,
"old-ds",
"new-ds",
);
assert_eq!(idxr["dataSourceName"], "new-ds");
assert_eq!(
idxr["targetIndexName"], "old-index",
"other kinds untouched"
);
}
#[test]
fn rename_x_rigg_ref_rewrites_only_the_matching_annotation() {
let mut agent = json!({
"name": "a",
"tools": [
{"type": "mcp", "x-rigg-ref": "knowledge-bases/kb-dev"},
{"type": "mcp", "x-rigg-ref": "knowledge-bases/other"},
{"type": "mcp", "x-rigg-ref": "connections/kb-dev"},
{"type": "mcp", "server_url": "knowledge-bases/kb-dev"}
]
});
rename_x_rigg_ref(&mut agent, "knowledge-bases", "kb-dev", "kb");
assert_eq!(agent["tools"][0][X_RIGG_REF], "knowledge-bases/kb");
assert_eq!(agent["tools"][1][X_RIGG_REF], "knowledge-bases/other");
assert_eq!(
agent["tools"][2][X_RIGG_REF], "connections/kb-dev",
"another directory is a different resource"
);
assert_eq!(
agent["tools"][3]["server_url"], "knowledge-bases/kb-dev",
"only the annotation key is rewritten"
);
}
}