1use 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 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 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#[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}