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#[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 pub pipeline: Option<ViewPipeline>,
39 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 §ion.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}