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