Skip to main content

arete_server/view/
spec.rs

1use crate::materialized_view::{CompareOp, FilterConfig, SortConfig, SortOrder, ViewPipeline};
2use crate::websocket::frame::{Mode, WireFormat};
3use arete_interpreter::ast::{FieldTypeInfo, ResolvedField, SerializableStreamSpec};
4use std::collections::BTreeSet;
5
6// # View System Architecture
7//
8// The view system uses hierarchical View IDs instead of simple entity names,
9// enabling sophisticated filtering and organization:
10//
11// ## View ID Structure
12// - Basic views: `EntityName/mode` (e.g., `SettlementGame/list`, `SettlementGame/state`)
13// - Filtered views: `EntityName/mode/filter1/filter2/...` (e.g., `SettlementGame/list/active/large`)
14//
15// ## Subscription Model
16// Clients subscribe using the full view ID:
17// ```json
18// {
19//   "view": "SettlementGame/list/active/large"
20// }
21// ```
22//
23// ## Future Filter Examples
24// - `SettlementGame/list/active/large` - Active games with large bets
25// - `SettlementGame/list/user/123` - Games for specific user
26// - `SettlementGame/list/recent` - Recently created games only
27
28#[derive(Clone, Debug)]
29pub struct ViewSpec {
30    pub id: String,
31    pub export: String,
32    pub mode: Mode,
33    pub wire_format: WireFormat,
34    pub projection: Projection,
35    pub filters: Filters,
36    pub delivery: Delivery,
37    /// Optional pipeline for derived views
38    pub pipeline: Option<ViewPipeline>,
39    /// Source view ID if this is a derived view
40    pub source_view: Option<String>,
41}
42
43#[derive(Clone, Debug, Default)]
44pub struct Projection {
45    pub fields: Option<Vec<String>>,
46}
47
48impl Projection {
49    pub fn all() -> Self {
50        Self { fields: None }
51    }
52
53    pub fn apply(&self, mut data: serde_json::Value) -> serde_json::Value {
54        if let Some(ref field_list) = self.fields {
55            if let Some(obj) = data.as_object_mut() {
56                obj.retain(|k, _| field_list.contains(&k.to_string()));
57            }
58        }
59        data
60    }
61}
62
63#[derive(Clone, Debug, Default)]
64pub struct Filters {
65    pub keys: Option<Vec<String>>,
66}
67
68impl Filters {
69    pub fn all() -> Self {
70        Self { keys: None }
71    }
72
73    pub fn matches(&self, key: &str) -> bool {
74        match &self.keys {
75            None => true,
76            Some(keys) => keys.iter().any(|k| k == key),
77        }
78    }
79}
80
81#[derive(Clone, Debug, Default)]
82pub struct Delivery {
83    pub coalesce_ms: Option<u64>,
84}
85
86impl ViewSpec {
87    pub fn is_derived(&self) -> bool {
88        self.pipeline.is_some()
89    }
90
91    pub fn from_view_def(
92        view_def: &arete_interpreter::ast::ViewDef,
93        export: &str,
94        wire_format: WireFormat,
95    ) -> Self {
96        use arete_interpreter::ast::{ViewOutput, ViewSource};
97
98        let mode = match &view_def.output {
99            ViewOutput::Collection => Mode::List,
100            ViewOutput::Single => Mode::State,
101            ViewOutput::Keyed { .. } => Mode::State,
102        };
103
104        let pipeline = Self::convert_pipeline(&view_def.pipeline);
105
106        let source_view = match &view_def.source {
107            ViewSource::Entity { name } => Some(format!("{}/list", name)),
108            ViewSource::View { id } => Some(id.clone()),
109        };
110
111        ViewSpec {
112            id: view_def.id.clone(),
113            export: export.to_string(),
114            mode,
115            wire_format,
116            projection: Projection::all(),
117            filters: Filters::all(),
118            delivery: Delivery::default(),
119            pipeline: Some(pipeline),
120            source_view,
121        }
122    }
123
124    pub fn wire_format_from_entity_spec(spec: &SerializableStreamSpec) -> WireFormat {
125        let mut wide_int_paths = BTreeSet::new();
126
127        for section in &spec.sections {
128            let prefix = if section.name.eq_ignore_ascii_case("root") {
129                Vec::new()
130            } else {
131                vec![section.name.clone()]
132            };
133
134            for field in &section.fields {
135                if !field.emit {
136                    continue;
137                }
138
139                let mut field_path = prefix.clone();
140                field_path.push(field.field_name.clone());
141                collect_wide_int_paths_from_field_info(&mut wide_int_paths, field_path, field);
142            }
143        }
144
145        for (target_path, field) in &spec.field_mappings {
146            if !field.emit {
147                continue;
148            }
149
150            let field_path = target_path
151                .split('.')
152                .filter(|segment| !segment.is_empty())
153                .map(|segment| segment.to_string())
154                .collect::<Vec<_>>();
155            collect_wide_int_paths_from_field_info(&mut wide_int_paths, field_path, field);
156        }
157
158        WireFormat {
159            wide_int_paths: wide_int_paths.into_iter().collect(),
160        }
161    }
162
163    fn convert_pipeline(transforms: &[arete_interpreter::ast::ViewTransform]) -> ViewPipeline {
164        use arete_interpreter::ast::ViewTransform as VT;
165
166        let mut pipeline = ViewPipeline {
167            filter: None,
168            sort: None,
169            limit: None,
170        };
171
172        for transform in transforms {
173            match transform {
174                VT::Filter { predicate } => {
175                    if let arete_interpreter::ast::Predicate::Compare { field, op, value } =
176                        predicate
177                    {
178                        use arete_interpreter::ast::CompareOp as CO;
179                        use arete_interpreter::ast::PredicateValue;
180
181                        let cmp_op = match op {
182                            CO::Eq => CompareOp::Eq,
183                            CO::Ne => CompareOp::Ne,
184                            CO::Gt => CompareOp::Gt,
185                            CO::Gte => CompareOp::Gte,
186                            CO::Lt => CompareOp::Lt,
187                            CO::Lte => CompareOp::Lte,
188                        };
189
190                        let filter_value = match value {
191                            PredicateValue::Literal(v) => v.clone(),
192                            PredicateValue::Dynamic(_) => serde_json::Value::Null,
193                            PredicateValue::Field(_) => serde_json::Value::Null,
194                        };
195
196                        pipeline.filter = Some(FilterConfig {
197                            field_path: field.segments.clone(),
198                            op: cmp_op,
199                            value: filter_value,
200                        });
201                    }
202                }
203                VT::Sort { key, order } => {
204                    use arete_interpreter::ast::SortOrder as SO;
205                    pipeline.sort = Some(SortConfig {
206                        field_path: key.segments.clone(),
207                        order: match order {
208                            SO::Asc => SortOrder::Asc,
209                            SO::Desc => SortOrder::Desc,
210                        },
211                    });
212                }
213                VT::Take { count } => {
214                    pipeline.limit = Some(*count);
215                }
216                VT::First | VT::Last | VT::MaxBy { .. } | VT::MinBy { .. } => {
217                    pipeline.limit = Some(1);
218                }
219                VT::Skip { .. } => {}
220            }
221        }
222
223        pipeline
224    }
225}
226
227fn collect_wide_int_paths_from_field_info(
228    output: &mut BTreeSet<Vec<String>>,
229    field_path: Vec<String>,
230    field: &FieldTypeInfo,
231) {
232    if is_wide_int_type(&field.rust_type_name)
233        || field.inner_type.as_deref().is_some_and(is_wide_int_type)
234    {
235        output.insert(field_path.clone());
236    }
237
238    if let Some(resolved_type) = &field.resolved_type {
239        for resolved_field in &resolved_type.fields {
240            let mut nested_path = field_path.clone();
241            nested_path.push(resolved_field.field_name.clone());
242            collect_wide_int_paths_from_resolved_field(output, nested_path, resolved_field);
243        }
244    }
245}
246
247fn collect_wide_int_paths_from_resolved_field(
248    output: &mut BTreeSet<Vec<String>>,
249    field_path: Vec<String>,
250    field: &ResolvedField,
251) {
252    if is_wide_int_type(&field.field_type) {
253        output.insert(field_path);
254    }
255}
256
257fn is_wide_int_type(type_name: &str) -> bool {
258    let trimmed = type_name.trim();
259    trimmed.contains("u64")
260        || trimmed.contains("i64")
261        || trimmed.contains("u128")
262        || trimmed.contains("i128")
263}