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