Skip to main content

teaql_core/
request_tree.rs

1//! Immutable request headers. Parent links are execution-owned, not query JSON
2//! or a Context-global stack. Sharing a header never clones a parent's AST.
3use std::sync::Arc;
4
5use crate::{QueryIntent, RequestIntentError, RequestKind, TraceKind, TraceNode};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum QueryOperationKind {
9    NamedCount,
10    Facet,
11    RelationCount,
12    RelationSelect,
13    Aggregate,
14    Pagination,
15}
16
17impl QueryOperationKind {
18    fn label(self) -> &'static str {
19        match self {
20            Self::NamedCount => "named-count",
21            Self::Facet => "facet",
22            Self::RelationCount => "relation-count",
23            Self::RelationSelect => "relation-select",
24            Self::Aggregate => "aggregate",
25            Self::Pagination => "pagination",
26        }
27    }
28}
29
30#[derive(Clone, PartialEq, Eq)]
31pub struct QueryOperation {
32    kind: QueryOperationKind,
33    method: Option<String>,
34    alias: Option<String>,
35    relation: Option<String>,
36}
37
38impl std::fmt::Debug for QueryOperation {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("QueryOperation")
41            .field("kind", &self.kind)
42            .field("method", &self.method)
43            .field("has_alias", &self.alias.is_some())
44            .finish()
45    }
46}
47
48impl QueryOperation {
49    /// Exact generator-supplied native identifier, never reconstructed from SQL.
50    pub fn new(kind: QueryOperationKind, method: impl Into<String>) -> Self {
51        let method = method.into();
52        assert!(
53            method
54                .bytes()
55                .enumerate()
56                .all(|(i, c)| c == b'_' || c.is_ascii_alphabetic() || i > 0 && c.is_ascii_digit())
57                && !method.is_empty(),
58            "QUERY_OPERATION_INVALID: native method must be an ASCII identifier"
59        );
60        Self {
61            kind,
62            method: Some(method),
63            alias: None,
64            relation: None,
65        }
66    }
67    pub fn framework(kind: QueryOperationKind) -> Self {
68        Self {
69            kind,
70            method: None,
71            alias: None,
72            relation: None,
73        }
74    }
75    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
76        self.alias = Some(alias.into());
77        self
78    }
79    pub fn with_relation(mut self, relation: impl Into<String>) -> Self {
80        self.relation = Some(relation.into());
81        self
82    }
83    pub fn alias(&self) -> Option<&str> {
84        self.alias.as_deref()
85    }
86    pub fn relation(&self) -> Option<&str> {
87        self.relation.as_deref()
88    }
89    pub fn kind(&self) -> QueryOperationKind {
90        self.kind
91    }
92    pub fn method(&self) -> Option<&str> {
93        self.method.as_deref()
94    }
95    pub fn comment(&self) -> String {
96        // Rust Debug uses native string escapes (including \u{...}), unlike
97        // JSON's \uXXXX form, which is not a valid Rust string literal.
98        let literal = |text: &str| format!("{text:?}");
99        if let Some(method) = &self.method {
100            let argument = self.alias.as_deref().map(literal).unwrap_or_else(|| {
101                if self.kind == QueryOperationKind::RelationSelect {
102                    "...".to_owned()
103                } else {
104                    String::new()
105                }
106            });
107            format!("{method}({argument})")
108        } else {
109            format!(
110                "framework: {}{}{}",
111                self.kind.label(),
112                self.relation
113                    .as_ref()
114                    .map(|value| format!(" {value}"))
115                    .unwrap_or_default(),
116                self.alias
117                    .as_deref()
118                    .map(|value| format!(" {}", literal(value)))
119                    .unwrap_or_default()
120            )
121        }
122    }
123}
124
125struct Header {
126    parent: Option<QueryRequestScope>,
127    name: String,
128    comment: String,
129    intent: Arc<QueryIntent>,
130    operation: Option<QueryOperation>,
131}
132
133/// A read-only view of an actual request's identity and direct parent. Bodies
134/// remain in their own SelectQuery; a relation shares only this small header.
135#[derive(Clone)]
136pub struct QueryRequestScope(Arc<Header>);
137
138impl std::fmt::Debug for QueryRequestScope {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("QueryRequestScope")
141            .field("name", &self.0.name)
142            .field("has_parent", &self.0.parent.is_some())
143            .finish()
144    }
145}
146impl PartialEq for QueryRequestScope {
147    fn eq(&self, other: &Self) -> bool {
148        Arc::ptr_eq(&self.0, &other.0)
149    }
150}
151impl Eq for QueryRequestScope {}
152
153impl QueryRequestScope {
154    pub fn root(name: impl Into<String>, intent: QueryIntent) -> Self {
155        Self(Arc::new(Header {
156            parent: None,
157            name: name.into(),
158            comment: intent.comment().to_owned(),
159            intent: Arc::new(intent),
160            operation: None,
161        }))
162    }
163    pub fn parent_request(&self) -> Option<&Self> {
164        self.0.parent.as_ref()
165    }
166    pub fn root_request(&self) -> &Self {
167        let mut root = self;
168        while let Some(parent) = root.parent_request() {
169            root = parent;
170        }
171        root
172    }
173    pub fn request_name(&self) -> &str {
174        &self.0.name
175    }
176    pub fn comment(&self) -> &str {
177        &self.0.comment
178    }
179    pub fn purpose(&self) -> &str {
180        self.0.intent.purpose()
181    }
182    pub fn intent(&self) -> &QueryIntent {
183        &self.0.intent
184    }
185    pub fn operation(&self) -> Option<&QueryOperation> {
186        self.0.operation.as_ref()
187    }
188
189    pub fn derive_operation(&self, name: impl Into<String>, operation: QueryOperation) -> Self {
190        Self(Arc::new(Header {
191            parent: Some(self.clone()),
192            name: name.into(),
193            comment: operation.comment(),
194            intent: Arc::clone(&self.0.intent),
195            operation: Some(operation),
196        }))
197    }
198    pub fn derive_helper(
199        &self,
200        name: impl Into<String>,
201        comment: impl Into<String>,
202    ) -> Result<Self, RequestIntentError> {
203        let comment = comment.into();
204        if comment.trim().is_empty() {
205            return Err(RequestIntentError {
206                request_kind: RequestKind::Query,
207                field: "comment",
208            });
209        }
210        Ok(Self(Arc::new(Header {
211            parent: Some(self.clone()),
212            name: name.into(),
213            comment,
214            intent: Arc::clone(&self.0.intent),
215            operation: None,
216        })))
217    }
218    pub fn derive_child(
219        &self,
220        name: impl Into<String>,
221        comment: Option<&str>,
222    ) -> Result<Self, RequestIntentError> {
223        let name = name.into();
224        self.derive_helper(
225            name.clone(),
226            comment
227                .map(str::to_owned)
228                .unwrap_or_else(|| format!("load {name}")),
229        )
230    }
231    pub fn trace_chain(&self) -> Vec<TraceNode> {
232        let mut nodes = Vec::new();
233        let mut current = Some(self);
234        while let Some(scope) = current {
235            nodes.push(TraceNode::typed(
236                TraceKind::Comment,
237                scope.request_name(),
238                None,
239                scope.comment(),
240            ));
241            current = scope.parent_request();
242        }
243        nodes.push(TraceNode::typed(
244            TraceKind::Purpose,
245            self.root_request().request_name(),
246            None,
247            self.purpose(),
248        ));
249        nodes.reverse();
250        nodes
251    }
252}