Skip to main content

rigg_core/resources/
traits.rs

1//! Resource kind definitions for Azure AI Search and Microsoft Foundry resources.
2//!
3//! Per-kind metadata (API paths, directory names, volatile/read-only/secret
4//! fields, references, capabilities) lives in [`crate::registry`]; the methods
5//! on [`ResourceKind`] delegate there.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10use crate::registry::{self, Domain};
11use crate::service::ServiceDomain;
12
13/// Enumeration of all supported resource types across service domains
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum ResourceKind {
17    // Azure AI Search resources
18    DataSource,
19    Index,
20    Skillset,
21    Indexer,
22    SynonymMap,
23    Alias,
24    KnowledgeSource,
25    KnowledgeBase,
26    // Microsoft Foundry resources
27    Agent,
28    Deployment,
29    Connection,
30    Guardrail,
31}
32
33impl ResourceKind {
34    /// Returns the service domain this resource belongs to
35    pub fn domain(&self) -> ServiceDomain {
36        match registry::meta(*self).domain {
37            Domain::Search => ServiceDomain::Search,
38            Domain::FoundryData | Domain::FoundryArm => ServiceDomain::Foundry,
39        }
40    }
41
42    /// Returns the API collection path segment for this resource type
43    pub fn api_path(&self) -> &'static str {
44        registry::meta(*self).collection_path
45    }
46
47    /// Returns the directory name for local storage, relative to the
48    /// project's domain directory (`search/` or `foundry/`).
49    pub fn directory_name(&self) -> &'static str {
50        registry::meta(*self).dir_name
51    }
52
53    /// Parse a directory name back to a kind.
54    pub fn from_directory_name(dir: &str) -> Option<ResourceKind> {
55        Self::all()
56            .iter()
57            .find(|k| k.directory_name() == dir)
58            .copied()
59    }
60
61    /// Returns the CLI name for this resource type (`rigg new <kind>`).
62    pub fn cli_name(&self) -> &'static str {
63        match self {
64            ResourceKind::DataSource => "data-source",
65            ResourceKind::Index => "index",
66            ResourceKind::Skillset => "skillset",
67            ResourceKind::Indexer => "indexer",
68            ResourceKind::SynonymMap => "synonym-map",
69            ResourceKind::Alias => "alias",
70            ResourceKind::KnowledgeSource => "knowledge-source",
71            ResourceKind::KnowledgeBase => "knowledge-base",
72            ResourceKind::Agent => "agent",
73            ResourceKind::Deployment => "deployment",
74            ResourceKind::Connection => "connection",
75            ResourceKind::Guardrail => "guardrail",
76        }
77    }
78
79    /// Parse a CLI name (as used by `rigg new`) into a kind.
80    pub fn from_cli_name(s: &str) -> Option<ResourceKind> {
81        Self::all().iter().find(|k| k.cli_name() == s).copied()
82    }
83
84    /// Returns the display name for this resource type
85    pub fn display_name(&self) -> &'static str {
86        match self {
87            ResourceKind::DataSource => "Data Source",
88            ResourceKind::Index => "Index",
89            ResourceKind::Skillset => "Skillset",
90            ResourceKind::Indexer => "Indexer",
91            ResourceKind::SynonymMap => "Synonym Map",
92            ResourceKind::Alias => "Alias",
93            ResourceKind::KnowledgeSource => "Knowledge Source",
94            ResourceKind::KnowledgeBase => "Knowledge Base",
95            ResourceKind::Agent => "Agent",
96            ResourceKind::Deployment => "Model Deployment",
97            ResourceKind::Connection => "Connection",
98            ResourceKind::Guardrail => "Guardrail",
99        }
100    }
101
102    /// Returns all resource kinds across all domains
103    pub fn all() -> &'static [ResourceKind] {
104        registry::all_kinds()
105    }
106
107    /// Legacy shim (pre-0.18 semantics): core search-management kinds.
108    /// Retired together with the old command implementations.
109    pub fn stable() -> &'static [ResourceKind] {
110        &[
111            ResourceKind::Index,
112            ResourceKind::Indexer,
113            ResourceKind::DataSource,
114            ResourceKind::Skillset,
115            ResourceKind::SynonymMap,
116        ]
117    }
118
119    /// Legacy shim: old singular flag name. Retired with the old CLI.
120    pub fn cli_flag_name(&self) -> &'static str {
121        match self {
122            ResourceKind::DataSource => "datasource",
123            ResourceKind::SynonymMap => "synonymmap",
124            ResourceKind::KnowledgeBase => "knowledgebase",
125            ResourceKind::KnowledgeSource => "knowledgesource",
126            other => other.cli_name(),
127        }
128    }
129
130    /// Legacy shim: old plural flag name. Retired with the old CLI.
131    pub fn cli_flag_name_plural(&self) -> &'static str {
132        match self {
133            ResourceKind::Index => "indexes",
134            ResourceKind::Indexer => "indexers",
135            ResourceKind::DataSource => "datasources",
136            ResourceKind::Skillset => "skillsets",
137            ResourceKind::SynonymMap => "synonymmaps",
138            ResourceKind::Alias => "aliases",
139            ResourceKind::KnowledgeBase => "knowledgebases",
140            ResourceKind::KnowledgeSource => "knowledgesources",
141            ResourceKind::Agent => "agents",
142            ResourceKind::Deployment => "deployments",
143            ResourceKind::Connection => "connections",
144            ResourceKind::Guardrail => "guardrails",
145        }
146    }
147
148    /// Returns all Search resource kinds
149    pub fn search_kinds() -> Vec<ResourceKind> {
150        Self::all()
151            .iter()
152            .filter(|k| k.domain() == ServiceDomain::Search)
153            .copied()
154            .collect()
155    }
156
157    /// Returns all Foundry resource kinds
158    pub fn foundry_kinds() -> Vec<ResourceKind> {
159        Self::all()
160            .iter()
161            .filter(|k| k.domain() == ServiceDomain::Foundry)
162            .copied()
163            .collect()
164    }
165}
166
167impl fmt::Display for ResourceKind {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "{}", self.display_name())
170    }
171}
172
173/// A (kind, name) reference to a resource.
174#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
175pub struct ResourceRef {
176    pub kind: ResourceKind,
177    pub name: String,
178}
179
180impl ResourceRef {
181    pub fn new(kind: ResourceKind, name: impl Into<String>) -> Self {
182        ResourceRef {
183            kind,
184            name: name.into(),
185        }
186    }
187
188    /// Stable string key, e.g. `indexes/my-index` — used in state files.
189    pub fn key(&self) -> String {
190        format!("{}/{}", self.kind.directory_name(), self.name)
191    }
192}
193
194impl fmt::Display for ResourceRef {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        write!(f, "{}/{}", self.kind.directory_name(), self.name)
197    }
198}
199
200/// Legacy typed-resource trait. The registry (`crate::registry`) is the new
201/// source of truth for per-kind metadata; this trait remains only while the
202/// old typed resource modules and their consumers are being retired during
203/// the 0.18 rewrite.
204pub trait Resource: Serialize + for<'de> Deserialize<'de> + Clone {
205    fn kind() -> ResourceKind;
206    fn name(&self) -> &str;
207    fn volatile_fields() -> &'static [&'static str] {
208        &["@odata.etag", "@odata.context"]
209    }
210    fn read_only_fields() -> &'static [&'static str] {
211        &[]
212    }
213    fn identity_key() -> &'static str {
214        "name"
215    }
216    fn dependencies(&self) -> Vec<(ResourceKind, String)> {
217        Vec::new()
218    }
219    fn immutable_fields() -> &'static [&'static str] {
220        &[]
221    }
222}
223
224/// Validate a resource name returned from Azure API responses.
225///
226/// Rejects:
227/// - Empty names
228/// - Names longer than 260 characters
229/// - Names containing `/`, `\`, or null bytes
230/// - Names that are exactly `.` or `..`
231pub fn validate_resource_name(name: &str) -> Result<(), anyhow::Error> {
232    if name.is_empty() {
233        anyhow::bail!("Resource name must not be empty");
234    }
235    if name.len() > 260 {
236        anyhow::bail!(
237            "Resource name must not exceed 260 characters (got {})",
238            name.len()
239        );
240    }
241    if name.contains('/') || name.contains('\\') || name.contains('\0') {
242        anyhow::bail!(
243            "Resource name must not contain '/', '\\', or null bytes: '{}'",
244            name
245        );
246    }
247    if name == "." || name == ".." {
248        anyhow::bail!("Resource name must not be '.' or '..'");
249    }
250    Ok(())
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn all_returns_twelve_kinds() {
259        assert_eq!(ResourceKind::all().len(), 12);
260    }
261
262    #[test]
263    fn api_paths() {
264        assert_eq!(ResourceKind::Index.api_path(), "indexes");
265        assert_eq!(ResourceKind::DataSource.api_path(), "datasources");
266        assert_eq!(ResourceKind::KnowledgeBase.api_path(), "knowledgeBases");
267        assert_eq!(ResourceKind::KnowledgeSource.api_path(), "knowledgeSources");
268        assert_eq!(ResourceKind::Agent.api_path(), "agents");
269        assert_eq!(ResourceKind::Deployment.api_path(), "deployments");
270    }
271
272    #[test]
273    fn directory_names_are_flat_and_roundtrip() {
274        for kind in ResourceKind::all() {
275            let dir = kind.directory_name();
276            assert!(!dir.contains('/'), "{kind:?} dir must be flat: {dir}");
277            assert_eq!(ResourceKind::from_directory_name(dir), Some(*kind));
278        }
279        assert_eq!(
280            ResourceKind::KnowledgeSource.directory_name(),
281            "knowledge-sources"
282        );
283        assert_eq!(ResourceKind::Guardrail.directory_name(), "guardrails");
284    }
285
286    #[test]
287    fn cli_names_roundtrip() {
288        for kind in ResourceKind::all() {
289            assert_eq!(ResourceKind::from_cli_name(kind.cli_name()), Some(*kind));
290        }
291    }
292
293    #[test]
294    fn domains() {
295        assert_eq!(ResourceKind::Index.domain(), ServiceDomain::Search);
296        assert_eq!(ResourceKind::KnowledgeBase.domain(), ServiceDomain::Search);
297        assert_eq!(ResourceKind::Agent.domain(), ServiceDomain::Foundry);
298        assert_eq!(ResourceKind::Deployment.domain(), ServiceDomain::Foundry);
299        assert_eq!(ResourceKind::Connection.domain(), ServiceDomain::Foundry);
300        assert_eq!(ResourceKind::Guardrail.domain(), ServiceDomain::Foundry);
301        assert_eq!(ResourceKind::search_kinds().len(), 8);
302        assert_eq!(ResourceKind::foundry_kinds().len(), 4);
303    }
304
305    #[test]
306    fn resource_ref_key() {
307        let r = ResourceRef::new(ResourceKind::Index, "docs");
308        assert_eq!(r.key(), "indexes/docs");
309        assert_eq!(r.to_string(), "indexes/docs");
310    }
311
312    #[test]
313    fn serde_roundtrip() {
314        for kind in ResourceKind::all() {
315            let json = serde_json::to_string(kind).unwrap();
316            let back: ResourceKind = serde_json::from_str(&json).unwrap();
317            assert_eq!(back, *kind);
318        }
319        assert_eq!(
320            serde_json::to_string(&ResourceKind::DataSource).unwrap(),
321            "\"data-source\""
322        );
323    }
324
325    #[test]
326    fn validate_resource_name_rules() {
327        assert!(validate_resource_name("my-index").is_ok());
328        assert!(validate_resource_name(&"a".repeat(260)).is_ok());
329        assert!(validate_resource_name("").is_err());
330        assert!(validate_resource_name(&"a".repeat(261)).is_err());
331        assert!(validate_resource_name("foo/bar").is_err());
332        assert!(validate_resource_name("foo\\bar").is_err());
333        assert!(validate_resource_name("foo\0bar").is_err());
334        assert!(validate_resource_name(".").is_err());
335        assert!(validate_resource_name("..").is_err());
336    }
337}