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