Skip to main content

activecube_rs/cube/
definition.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub enum DimType {
3    String,
4    Int,
5    Float,
6    DateTime,
7    Bool,
8}
9
10#[derive(Debug, Clone)]
11pub struct Dimension {
12    pub graphql_name: String,
13    pub column: String,
14    pub dim_type: DimType,
15    pub description: Option<String>,
16}
17
18#[derive(Debug, Clone)]
19pub enum DimensionNode {
20    Leaf(Dimension),
21    Group {
22        graphql_name: String,
23        description: Option<String>,
24        children: Vec<DimensionNode>,
25    },
26}
27
28/// A named selector defines a filterable field on a Cube.
29/// Each selector maps a GraphQL argument name to a column + type,
30/// enabling `eq`, `gt`, `in`, `any` etc.
31#[derive(Debug, Clone)]
32pub struct SelectorDef {
33    pub graphql_name: String,
34    pub column: String,
35    pub dim_type: DimType,
36}
37
38pub fn selector(graphql_name: &str, column: &str, dim_type: DimType) -> SelectorDef {
39    SelectorDef {
40        graphql_name: graphql_name.to_string(),
41        column: column.to_string(),
42        dim_type,
43    }
44}
45
46/// Declares a LEFT JOIN relationship from this cube to another cube.
47#[derive(Debug, Clone)]
48pub struct JoinDef {
49    /// GraphQL field name on the source record, e.g. "joinTransfers"
50    pub field_name: String,
51    /// Target cube name as registered in the CubeRegistry, e.g. "Transfers"
52    pub target_cube: String,
53    /// (local_column, remote_column) pairs for the ON clause.
54    pub conditions: Vec<(String, String)>,
55    pub description: Option<String>,
56}
57
58pub fn join_def(field_name: &str, target_cube: &str, conditions: &[(&str, &str)]) -> JoinDef {
59    JoinDef {
60        field_name: field_name.to_string(),
61        target_cube: target_cube.to_string(),
62        conditions: conditions.iter().map(|(l, r)| (l.to_string(), r.to_string())).collect(),
63        description: None,
64    }
65}
66
67pub fn join_def_desc(field_name: &str, target_cube: &str, conditions: &[(&str, &str)], desc: &str) -> JoinDef {
68    JoinDef {
69        field_name: field_name.to_string(),
70        target_cube: target_cube.to_string(),
71        conditions: conditions.iter().map(|(l, r)| (l.to_string(), r.to_string())).collect(),
72        description: Some(desc.to_string()),
73    }
74}
75
76#[derive(Debug, Clone)]
77pub struct CubeDefinition {
78    pub name: String,
79    pub schema: String,
80    /// Table name pattern. Use `{chain}` as placeholder for chain-prefixed tables
81    /// (e.g. `{chain}_trades` → `sol_trades`). For tables without chain prefix
82    /// (e.g. `dex_pool_liquidities`), use the literal table name and set
83    /// `chain_column` instead.
84    pub table_pattern: String,
85    /// If set, the table doesn't use a `{chain}` prefix in its name. Instead,
86    /// the chain is filtered via `WHERE <chain_column> = ?`. Example:
87    /// `dex_pool_liquidities` has a `chain` column rather than `sol_dex_pool_liquidities`.
88    pub chain_column: Option<String>,
89    pub dimensions: Vec<DimensionNode>,
90    pub metrics: Vec<String>,
91    pub selectors: Vec<SelectorDef>,
92    pub default_filters: Vec<(String, String)>,
93    pub default_limit: u32,
94    pub max_limit: u32,
95    /// Append FINAL to FROM clause for ReplacingMergeTree tables in ClickHouse.
96    pub use_final: bool,
97    /// Human-readable description of the cube's purpose, exposed via _cubeMetadata.
98    pub description: String,
99    /// Declarative LEFT JOIN relationships to other cubes.
100    pub joins: Vec<JoinDef>,
101}
102
103impl CubeDefinition {
104    pub fn table_for_chain(&self, chain: &str) -> String {
105        self.table_pattern.replace("{chain}", chain)
106    }
107
108    pub fn qualified_table(&self, chain: &str) -> String {
109        format!("{}.{}", self.schema, self.table_for_chain(chain))
110    }
111
112    pub fn flat_dimensions(&self) -> Vec<(String, Dimension)> {
113        let mut out = Vec::new();
114        for node in &self.dimensions {
115            collect_leaves(node, "", &mut out);
116        }
117        out
118    }
119}
120
121fn collect_leaves(node: &DimensionNode, prefix: &str, out: &mut Vec<(String, Dimension)>) {
122    match node {
123        DimensionNode::Leaf(dim) => {
124            let path = if prefix.is_empty() {
125                dim.graphql_name.clone()
126            } else {
127                format!("{}_{}", prefix, dim.graphql_name)
128            };
129            out.push((path, dim.clone()));
130        }
131        DimensionNode::Group { graphql_name, children, .. } => {
132            let new_prefix = if prefix.is_empty() {
133                graphql_name.clone()
134            } else {
135                format!("{prefix}_{graphql_name}")
136            };
137            for child in children {
138                collect_leaves(child, &new_prefix, out);
139            }
140        }
141    }
142}
143
144// ---------------------------------------------------------------------------
145// CubeBuilder — ergonomic builder pattern for CubeDefinition
146// ---------------------------------------------------------------------------
147
148pub struct CubeBuilder {
149    def: CubeDefinition,
150}
151
152impl CubeBuilder {
153    pub fn new(name: &str) -> Self {
154        Self {
155            def: CubeDefinition {
156                name: name.to_string(),
157                schema: String::new(),
158                table_pattern: String::new(),
159                chain_column: None,
160                dimensions: Vec::new(),
161                metrics: Vec::new(),
162                selectors: Vec::new(),
163                default_filters: Vec::new(),
164                default_limit: 25,
165                max_limit: 10000,
166                use_final: false,
167                description: String::new(),
168                joins: Vec::new(),
169            },
170        }
171    }
172
173    pub fn schema(mut self, schema: &str) -> Self {
174        self.def.schema = schema.to_string();
175        self
176    }
177
178    pub fn table(mut self, pattern: &str) -> Self {
179        self.def.table_pattern = pattern.to_string();
180        self
181    }
182
183    pub fn chain_column(mut self, column: &str) -> Self {
184        self.def.chain_column = Some(column.to_string());
185        self
186    }
187
188    pub fn dimension(mut self, node: DimensionNode) -> Self {
189        self.def.dimensions.push(node);
190        self
191    }
192
193    pub fn metric(mut self, name: &str) -> Self {
194        self.def.metrics.push(name.to_string());
195        self
196    }
197
198    pub fn metrics(mut self, names: &[&str]) -> Self {
199        self.def.metrics.extend(names.iter().map(|s| s.to_string()));
200        self
201    }
202
203    pub fn selector(mut self, sel: SelectorDef) -> Self {
204        self.def.selectors.push(sel);
205        self
206    }
207
208    pub fn default_filter(mut self, column: &str, value: &str) -> Self {
209        self.def.default_filters.push((column.to_string(), value.to_string()));
210        self
211    }
212
213    pub fn default_limit(mut self, limit: u32) -> Self {
214        self.def.default_limit = limit;
215        self
216    }
217
218    pub fn max_limit(mut self, limit: u32) -> Self {
219        self.def.max_limit = limit;
220        self
221    }
222
223    pub fn use_final(mut self, val: bool) -> Self {
224        self.def.use_final = val;
225        self
226    }
227
228    pub fn description(mut self, desc: &str) -> Self {
229        self.def.description = desc.to_string();
230        self
231    }
232
233    pub fn join(mut self, j: JoinDef) -> Self {
234        self.def.joins.push(j);
235        self
236    }
237
238    pub fn joins(mut self, js: Vec<JoinDef>) -> Self {
239        self.def.joins.extend(js);
240        self
241    }
242
243    pub fn build(self) -> CubeDefinition {
244        self.def
245    }
246}
247
248// ---------------------------------------------------------------------------
249// Helper functions for concise dimension/selector construction
250// ---------------------------------------------------------------------------
251
252pub fn dim(graphql_name: &str, column: &str, dim_type: DimType) -> DimensionNode {
253    DimensionNode::Leaf(Dimension {
254        graphql_name: graphql_name.to_string(),
255        column: column.to_string(),
256        dim_type,
257        description: None,
258    })
259}
260
261pub fn dim_desc(graphql_name: &str, column: &str, dim_type: DimType, desc: &str) -> DimensionNode {
262    DimensionNode::Leaf(Dimension {
263        graphql_name: graphql_name.to_string(),
264        column: column.to_string(),
265        dim_type,
266        description: Some(desc.to_string()),
267    })
268}
269
270pub fn dim_group(graphql_name: &str, children: Vec<DimensionNode>) -> DimensionNode {
271    DimensionNode::Group {
272        graphql_name: graphql_name.to_string(),
273        description: None,
274        children,
275    }
276}
277
278pub fn dim_group_desc(graphql_name: &str, desc: &str, children: Vec<DimensionNode>) -> DimensionNode {
279    DimensionNode::Group {
280        graphql_name: graphql_name.to_string(),
281        description: Some(desc.to_string()),
282        children,
283    }
284}