Skip to main content

sqry_core/relations/
identity.rs

1// RKG: CODE:RELATIONS-SHARED implements REQ:SQRY-RUBY-QUALIFIED-CALLERS
2use serde::{Deserialize, Serialize};
3
4/// Method kind for canonical caller identities.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum CallIdentityKind {
7    /// Instance method (`Namespace::Class#method`).
8    Instance,
9    /// Singleton/class method defined as `def self.method` or similar.
10    Singleton,
11    /// Methods defined inside `class << self` blocks.
12    SingletonClass,
13}
14
15impl Default for CallIdentityKind {
16    fn default() -> Self {
17        Self::Instance
18    }
19}
20
21/// Structured metadata describing a caller.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
23pub struct CallIdentityMetadata {
24    /// Fully qualified caller identifier (e.g., `Admin::Users::Controller#show`).
25    pub qualified: String,
26    /// Simple caller name (e.g., `show`).
27    pub simple: String,
28    /// Namespace stack (modules/classes) enclosing the method.
29    pub namespace: Vec<String>,
30    /// Method kind (instance, singleton, singleton-class).
31    pub method_kind: CallIdentityKind,
32    /// Optional receiver expression captured at call site.
33    #[serde(default)]
34    pub receiver: Option<String>,
35}
36
37/// Builder used by language adapters to construct canonical caller identities.
38#[derive(Debug, Default)]
39pub struct CallIdentityBuilder {
40    namespace: Vec<String>,
41    simple: String,
42    kind: CallIdentityKind,
43    receiver: Option<String>,
44}
45
46impl CallIdentityBuilder {
47    /// Create a new builder for the provided method name/kind.
48    #[must_use]
49    pub fn new(simple: impl Into<String>, kind: CallIdentityKind) -> Self {
50        Self {
51            namespace: Vec::new(),
52            simple: simple.into(),
53            kind,
54            receiver: None,
55        }
56    }
57
58    /// Replace the namespace stack with the provided iterator.
59    #[must_use]
60    pub fn with_namespace<I, S>(mut self, segments: I) -> Self
61    where
62        I: IntoIterator<Item = S>,
63        S: Into<String>,
64    {
65        self.namespace = segments.into_iter().map(Into::into).collect();
66        self
67    }
68
69    /// Add a single namespace segment (e.g., module or class name).
70    #[must_use]
71    pub fn push_namespace(mut self, segment: impl Into<String>) -> Self {
72        self.namespace.push(segment.into());
73        self
74    }
75
76    /// Attach an explicit receiver expression (used for diagnostics/metadata).
77    #[must_use]
78    pub fn with_receiver(mut self, receiver: impl Into<String>) -> Self {
79        self.receiver = Some(receiver.into());
80        self
81    }
82
83    /// Consume the builder and emit serialized metadata.
84    #[must_use]
85    pub fn build(self) -> CallIdentityMetadata {
86        let qualified = build_qualified_name(&self.namespace, &self.simple, self.kind);
87        CallIdentityMetadata {
88            qualified,
89            simple: self.simple,
90            namespace: self.namespace,
91            method_kind: self.kind,
92            receiver: self.receiver,
93        }
94    }
95}
96
97fn build_qualified_name(namespace: &[String], simple: &str, kind: CallIdentityKind) -> String {
98    if namespace.is_empty() {
99        return simple.to_string();
100    }
101
102    let mut qualified = namespace.join("::");
103    qualified.push(method_separator(kind));
104    qualified.push_str(simple);
105    qualified
106}
107
108fn method_separator(kind: CallIdentityKind) -> char {
109    match kind {
110        CallIdentityKind::Instance => '#',
111        CallIdentityKind::Singleton | CallIdentityKind::SingletonClass => '.',
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::{CallIdentityBuilder, CallIdentityKind, build_qualified_name};
118
119    #[test]
120    fn qualified_name_for_instance_method() {
121        let namespace = vec![
122            "Admin".to_string(),
123            "Users".to_string(),
124            "Controller".to_string(),
125        ];
126        let qualified = build_qualified_name(&namespace, "show", CallIdentityKind::Instance);
127        assert_eq!(qualified, "Admin::Users::Controller#show");
128    }
129
130    #[test]
131    fn qualified_name_for_singleton_method() {
132        let namespace = vec!["User".to_string()];
133        let qualified =
134            build_qualified_name(&namespace, "authenticate", CallIdentityKind::Singleton);
135        assert_eq!(qualified, "User.authenticate");
136    }
137
138    #[test]
139    fn builder_emits_metadata() {
140        let metadata = CallIdentityBuilder::new("find", CallIdentityKind::Singleton)
141            .with_namespace(["User"])
142            .with_receiver("self")
143            .build();
144
145        assert_eq!(metadata.qualified, "User.find");
146        assert_eq!(metadata.simple, "find");
147        assert_eq!(metadata.namespace, vec!["User".to_string()]);
148        assert_eq!(metadata.method_kind, CallIdentityKind::Singleton);
149        assert_eq!(metadata.receiver.as_deref(), Some("self"));
150    }
151
152    #[test]
153    fn qualified_name_for_global_scope() {
154        let qualified = build_qualified_name(&[], "main", CallIdentityKind::Instance);
155        assert_eq!(qualified, "main");
156    }
157}