hyperstack_server/view/
spec.rs1use crate::materialized_view::{CompareOp, FilterConfig, SortConfig, SortOrder, ViewPipeline};
2use crate::websocket::frame::Mode;
3
4#[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 pub pipeline: Option<ViewPipeline>,
36 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}