Skip to main content

phi_core/config/
reference.rs

1//! Generic ID reference protocol for config objects.
2//!
3//! Any config field that references another object uses the `{{...}}` syntax:
4//!
5//! | Pattern | Meaning |
6//! |---------|---------|
7//! | `{{type.name}}` | Qualified reference, recreate |
8//! | `{{%type.name%}}` | Qualified reference, no recreation if exists |
9//! | `{{name}}` | Unqualified (unique resolve), recreate |
10//! | `{{%name%}}` | Unqualified (unique resolve), no recreation |
11//! | `{{#system_id#}}` | Literal system ID, no recreation |
12//!
13//! ## Resolution rules
14//!
15//! - **Qualified**: look up `name` in the specified `type` namespace
16//!   (e.g., `agent_profile` → `[[agent.profile.instances]]`)
17//! - **Unqualified**: search all namespaces; must resolve uniquely or error
18//! - **`%` delimiters**: skip creation if an object with matching description
19//!   already exists. If multiple matches, use the one with the latest creation date.
20//! - **No `%`**: always create a new instance
21//! - **`#` delimiters**: literal system ID (already exists in external system)
22//!
23//! ## Namespaces
24//!
25//! | Qualified prefix | Config section |
26//! |-----------------|----------------|
27//! | `agent_profile` | `[[agent.profile.instances]]` |
28//! | `provider` | `[[provider.instances]]` |
29//! | `sub_agent` | `[[sub_agents.instances]]` |
30
31use serde::{Deserialize, Serialize};
32
33/// A parsed config object reference.
34///
35/// Produced by [`parse_config_ref`] from a raw string in a config file.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(tag = "kind", rename_all = "snake_case")]
38pub enum ConfigRef {
39    /// `{{type.name}}` or `{{%type.name%}}` — qualified reference.
40    Qualified {
41        /// The namespace type (e.g., `"agent_profile"`, `"provider"`, `"sub_agent"`).
42        ref_type: String,
43        /// The instance name within that namespace.
44        name: String,
45        /// Whether to recreate the object if it already exists.
46        /// `false` when `%` delimiters are used.
47        recreate: bool,
48    },
49    /// `{{name}}` or `{{%name%}}` — unqualified reference (must resolve uniquely).
50    Unqualified {
51        /// The instance name (searched across all namespaces).
52        name: String,
53        /// Whether to recreate the object if it already exists.
54        recreate: bool,
55    },
56    /// `{{#system_id#}}` — literal system ID, no recreation.
57    SystemId {
58        /// The actual system ID.
59        id: String,
60    },
61    /// Plain string — not a `{{...}}` reference.
62    Literal(String),
63}
64
65impl ConfigRef {
66    /// Returns the effective name for config-internal lookups.
67    ///
68    /// For `Qualified`, returns the `name`. For `Unqualified`, returns the `name`.
69    /// For `SystemId`, returns the `id`. For `Literal`, returns the raw string.
70    pub fn effective_name(&self) -> &str {
71        match self {
72            Self::Qualified { name, .. } => name,
73            Self::Unqualified { name, .. } => name,
74            Self::SystemId { id } => id,
75            Self::Literal(s) => s,
76        }
77    }
78
79    /// Whether this reference requests recreation.
80    pub fn should_recreate(&self) -> bool {
81        match self {
82            Self::Qualified { recreate, .. } => *recreate,
83            Self::Unqualified { recreate, .. } => *recreate,
84            Self::SystemId { .. } => false,
85            Self::Literal(_) => true,
86        }
87    }
88
89    /// Whether this is a `{{...}}` reference (not a plain literal).
90    pub fn is_reference(&self) -> bool {
91        !matches!(self, Self::Literal(_))
92    }
93}
94
95/// Parse a string that may contain a `{{...}}` reference pattern.
96///
97/// # Examples
98///
99/// ```
100/// use phi_core::config::reference::{parse_config_ref, ConfigRef};
101///
102/// // Qualified, recreate
103/// assert_eq!(
104///     parse_config_ref("{{agent_profile.coder}}"),
105///     ConfigRef::Qualified { ref_type: "agent_profile".into(), name: "coder".into(), recreate: true }
106/// );
107///
108/// // Qualified, no recreation
109/// assert_eq!(
110///     parse_config_ref("{{%provider.openai%}}"),
111///     ConfigRef::Qualified { ref_type: "provider".into(), name: "openai".into(), recreate: false }
112/// );
113///
114/// // Unqualified, recreate
115/// assert_eq!(
116///     parse_config_ref("{{coder}}"),
117///     ConfigRef::Unqualified { name: "coder".into(), recreate: true }
118/// );
119///
120/// // System ID
121/// assert_eq!(
122///     parse_config_ref("{{#fctsidd-abc-123#}}"),
123///     ConfigRef::SystemId { id: "fctsidd-abc-123".into() }
124/// );
125///
126/// // Plain string
127/// assert_eq!(
128///     parse_config_ref("just-a-string"),
129///     ConfigRef::Literal("just-a-string".into())
130/// );
131/// ```
132pub fn parse_config_ref(s: &str) -> ConfigRef {
133    let trimmed = s.trim();
134
135    // Must start with {{ and end with }}
136    if !trimmed.starts_with("{{") || !trimmed.ends_with("}}") {
137        return ConfigRef::Literal(s.to_string());
138    }
139
140    let inner = &trimmed[2..trimmed.len() - 2];
141
142    if inner.is_empty() {
143        return ConfigRef::Literal(s.to_string());
144    }
145
146    // {{#system_id#}} — literal system ID
147    if inner.starts_with('#') && inner.ends_with('#') && inner.len() > 2 {
148        let id = &inner[1..inner.len() - 1];
149        return ConfigRef::SystemId { id: id.to_string() };
150    }
151
152    // {{%name%}} or {{%type.name%}} — no recreation
153    if inner.starts_with('%') && inner.ends_with('%') && inner.len() > 2 {
154        let body = &inner[1..inner.len() - 1];
155        return parse_qualified_or_unqualified(body, false);
156    }
157
158    // {{name}} or {{type.name}} — recreate
159    parse_qualified_or_unqualified(inner, true)
160}
161
162/// Parse the inner body (after stripping `{{`, `}}`, and optional `%`) into
163/// either a Qualified or Unqualified ref.
164fn parse_qualified_or_unqualified(body: &str, recreate: bool) -> ConfigRef {
165    // Check for type.name pattern (first dot separates type from name)
166    if let Some(dot_pos) = body.find('.') {
167        let ref_type = &body[..dot_pos];
168        let name = &body[dot_pos + 1..];
169        if !ref_type.is_empty() && !name.is_empty() {
170            return ConfigRef::Qualified {
171                ref_type: ref_type.to_string(),
172                name: name.to_string(),
173                recreate,
174            };
175        }
176    }
177
178    // Unqualified — just a name
179    ConfigRef::Unqualified {
180        name: body.to_string(),
181        recreate,
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_qualified_recreate() {
191        assert_eq!(
192            parse_config_ref("{{agent_profile.coder}}"),
193            ConfigRef::Qualified {
194                ref_type: "agent_profile".into(),
195                name: "coder".into(),
196                recreate: true,
197            }
198        );
199    }
200
201    #[test]
202    fn test_qualified_no_recreate() {
203        assert_eq!(
204            parse_config_ref("{{%provider.openai%}}"),
205            ConfigRef::Qualified {
206                ref_type: "provider".into(),
207                name: "openai".into(),
208                recreate: false,
209            }
210        );
211    }
212
213    #[test]
214    fn test_unqualified_recreate() {
215        assert_eq!(
216            parse_config_ref("{{coder}}"),
217            ConfigRef::Unqualified {
218                name: "coder".into(),
219                recreate: true,
220            }
221        );
222    }
223
224    #[test]
225    fn test_unqualified_no_recreate() {
226        assert_eq!(
227            parse_config_ref("{{%coder%}}"),
228            ConfigRef::Unqualified {
229                name: "coder".into(),
230                recreate: false,
231            }
232        );
233    }
234
235    #[test]
236    fn test_system_id() {
237        assert_eq!(
238            parse_config_ref("{{#fctsidd-abc-123#}}"),
239            ConfigRef::SystemId {
240                id: "fctsidd-abc-123".into(),
241            }
242        );
243    }
244
245    #[test]
246    fn test_literal() {
247        assert_eq!(
248            parse_config_ref("just-a-string"),
249            ConfigRef::Literal("just-a-string".into())
250        );
251    }
252
253    #[test]
254    fn test_empty_braces() {
255        assert_eq!(parse_config_ref("{{}}"), ConfigRef::Literal("{{}}".into()));
256    }
257
258    #[test]
259    fn test_effective_name() {
260        let r = parse_config_ref("{{agent_profile.coder}}");
261        assert_eq!(r.effective_name(), "coder");
262
263        let r = parse_config_ref("{{%coder%}}");
264        assert_eq!(r.effective_name(), "coder");
265
266        let r = parse_config_ref("{{#sys-id#}}");
267        assert_eq!(r.effective_name(), "sys-id");
268    }
269
270    #[test]
271    fn test_should_recreate() {
272        assert!(parse_config_ref("{{coder}}").should_recreate());
273        assert!(parse_config_ref("{{agent_profile.coder}}").should_recreate());
274        assert!(!parse_config_ref("{{%coder%}}").should_recreate());
275        assert!(!parse_config_ref("{{#sys-id#}}").should_recreate());
276    }
277}