hyperstack_server/view/
spec.rs

1use crate::materialized_view::{CompareOp, FilterConfig, SortConfig, SortOrder, ViewPipeline};
2use crate::websocket::frame::Mode;
3
4// # View System Architecture
5//
6// The view system uses hierarchical View IDs instead of simple entity names,
7// enabling sophisticated filtering and organization:
8//
9// ## View ID Structure
10// - Basic views: `EntityName/mode` (e.g., `SettlementGame/list`, `SettlementGame/state`)
11// - Filtered views: `EntityName/mode/filter1/filter2/...` (e.g., `SettlementGame/list/active/large`)
12//
13// ## Subscription Model
14// Clients subscribe using the full view ID:
15// ```json
16// {
17//   "view": "SettlementGame/list/active/large"
18// }
19// ```
20//
21// ## Future Filter Examples
22// - `SettlementGame/list/active/large` - Active games with large bets
23// - `SettlementGame/list/user/123` - Games for specific user
24// - `SettlementGame/list/recent` - Recently created games only
25
26#[derive(Clone, Debug)]
27pub struct ViewSpec {
28    pub id: String,
29    pub export: String,
30    pub mode: Mode,
31    pub projection: Projection,
32    pub filters: Filters,
33    pub delivery: Delivery,
34    /// Optional pipeline for derived views
35    pub pipeline: Option<ViewPipeline>,
36    /// Source view ID if this is a derived view
37    pub source_view: Option<String>,
38}
39
40#[derive(Clone, Debug, Default)]
41pub struct Projection {
42    pub fields: Option<Vec<String>>,
43}
44
45impl Projection {
46    pub fn all() -> Self {
47        Self { fields: None }
48    }
49
50    pub fn apply(&self, mut data: serde_json::Value) -> serde_json::Value {
51        if let Some(ref field_list) = self.fields {
52            if let Some(obj) = data.as_object_mut() {
53                obj.retain(|k, _| field_list.contains(&k.to_string()));
54            }
55        }
56        data
57    }
58}
59
60#[derive(Clone, Debug, Default)]
61pub struct Filters {
62    pub keys: Option<Vec<String>>,
63}
64
65impl Filters {
66    pub fn all() -> Self {
67        Self { keys: None }
68    }
69
70    pub fn matches(&self, key: &str) -> bool {
71        match &self.keys {
72            None => true,
73            Some(keys) => keys.iter().any(|k| k == key),
74        }
75    }
76}
77
78#[derive(Clone, Debug, Default)]
79pub struct Delivery {
80    pub coalesce_ms: Option<u64>,
81}
82
83impl ViewSpec {
84    pub fn is_derived(&self) -> bool {
85        self.pipeline.is_some()
86    }
87
88    pub fn from_view_def(view_def: &hyperstack_interpreter::ast::ViewDef, export: &str) -> Self {
89        use hyperstack_interpreter::ast::{ViewOutput, ViewSource};
90
91        let mode = match &view_def.output {
92            ViewOutput::Collection => Mode::List,
93            ViewOutput::Single => Mode::State,
94            ViewOutput::Keyed { .. } => Mode::State,
95        };
96
97        let pipeline = Self::convert_pipeline(&view_def.pipeline);
98
99        let source_view = match &view_def.source {
100            ViewSource::Entity { name } => Some(format!("{}/list", name)),
101            ViewSource::View { id } => Some(id.clone()),
102        };
103
104        ViewSpec {
105            id: view_def.id.clone(),
106            export: export.to_string(),
107            mode,
108            projection: Projection::all(),
109            filters: Filters::all(),
110            delivery: Delivery::default(),
111            pipeline: Some(pipeline),
112            source_view,
113        }
114    }
115
116    fn convert_pipeline(transforms: &[hyperstack_interpreter::ast::ViewTransform]) -> ViewPipeline {
117        use hyperstack_interpreter::ast::ViewTransform as VT;
118
119        let mut pipeline = ViewPipeline {
120            filter: None,
121            sort: None,
122            limit: None,
123        };
124
125        for transform in transforms {
126            match transform {
127                VT::Filter { predicate } => {
128                    if let hyperstack_interpreter::ast::Predicate::Compare { field, op, value } =
129                        predicate
130                    {
131                        use hyperstack_interpreter::ast::CompareOp as CO;
132                        use hyperstack_interpreter::ast::PredicateValue;
133
134                        let cmp_op = match op {
135                            CO::Eq => CompareOp::Eq,
136                            CO::Ne => CompareOp::Ne,
137                            CO::Gt => CompareOp::Gt,
138                            CO::Gte => CompareOp::Gte,
139                            CO::Lt => CompareOp::Lt,
140                            CO::Lte => CompareOp::Lte,
141                        };
142
143                        let filter_value = match value {
144                            PredicateValue::Literal(v) => v.clone(),
145                            PredicateValue::Dynamic(_) => serde_json::Value::Null,
146                            PredicateValue::Field(_) => serde_json::Value::Null,
147                        };
148
149                        pipeline.filter = Some(FilterConfig {
150                            field_path: field.segments.clone(),
151                            op: cmp_op,
152                            value: filter_value,
153                        });
154                    }
155                }
156                VT::Sort { key, order } => {
157                    use hyperstack_interpreter::ast::SortOrder as SO;
158                    pipeline.sort = Some(SortConfig {
159                        field_path: key.segments.clone(),
160                        order: match order {
161                            SO::Asc => SortOrder::Asc,
162                            SO::Desc => SortOrder::Desc,
163                        },
164                    });
165                }
166                VT::Take { count } => {
167                    pipeline.limit = Some(*count);
168                }
169                VT::First | VT::Last | VT::MaxBy { .. } | VT::MinBy { .. } => {
170                    pipeline.limit = Some(1);
171                }
172                VT::Skip { .. } => {}
173            }
174        }
175
176        pipeline
177    }
178}