ifc_lite_server/routes/parse/
json.rs1use super::cache_keys::{json_response_cache_key, request_cache_key};
8use super::{cache_symbolic_data_off_runtime, extract_file, ParseQuery};
9use crate::error::ApiError;
10use crate::services::axis::mesh_to_yup_in_place;
11use crate::services::streaming::detect_schema_version;
12use crate::services::process_streaming;
13use crate::types::{finish_meshes, MetadataResponse, ParseResponse, SymbolicParseResponse, StreamEvent};
14use ifc_lite_processing::style::ModelFinishes;
15use crate::AppState;
16use axum::{
17 body::Body,
18 extract::{Multipart, Query, State},
19 http::header,
20 response::sse::{Event, KeepAlive, Sse},
21 response::{IntoResponse, Response},
22 Json,
23};
24use bytes::Bytes;
25use futures::stream::StreamExt;
26use ifc_lite_core::EntityScanner;
27use ifc_lite_processing::process_geometry_filtered_with_quality;
28use std::convert::Infallible;
29
30pub async fn parse_full(
32 State(state): State<AppState>,
33 Query(query): Query<ParseQuery>,
34 mut multipart: Multipart,
35) -> Result<Response, ApiError> {
36 let admission_guard = state
42 .admission
43 .acquire(state.config.max_file_size_mb as u64 * 1024 * 1024)
44 .await?;
45 let data = extract_file(&mut multipart, state.config.max_file_size_mb).await?;
46
47 let tessellation_quality = query.resolved_tessellation_quality()?;
49 let cache_key = request_cache_key(&data, &query, tessellation_quality);
50 let response_cache_key = json_response_cache_key(&cache_key);
54
55 if let Some(cached) = state.cache.get_bytes(&response_cache_key).await? {
59 tracing::info!(cache_key = %cache_key, "Cache HIT");
60 let (body, _admission) = tokio::task::spawn_blocking(move || {
61 let decoded = serde_json::from_slice::<SymbolicParseResponse>(&cached);
62 drop(cached);
63 let mut cached = decoded?;
64 cached.mark_from_cache();
65 Ok::<_, ApiError>((encode_response(&cached)?, admission_guard))
66 })
67 .await??;
68 return Ok(json_body(body));
69 }
70
71 tracing::info!(cache_key = %cache_key, size = data.len(), "Cache MISS - processing");
72
73 let content = data;
75 let opening_filter = query.opening_filter;
76 let response_key = cache_key.clone();
77
78 let (body, symbolic_data, _admission) = tokio::task::spawn_blocking(move || {
90 let result =
91 process_geometry_filtered_with_quality(&content, opening_filter, tessellation_quality);
92 let symbolic = ifc_lite_processing::extract_symbolic_data_with_provenance_in_frame(
97 &content,
98 result.frame,
99 );
100 let mut meshes = finish_meshes(&mut ModelFinishes::from_content(&content), result.meshes);
102 drop(content);
103
104 for mesh in &mut meshes {
110 mesh_to_yup_in_place(mesh);
111 }
112
113 let response = SymbolicParseResponse::new(ParseResponse {
114 cache_key: response_key,
115 meshes: Vec::new(),
116 mesh_coordinate_space: Some(result.mesh_coordinate_space),
117 site_transform: result.site_transform,
118 building_transform: result.building_transform,
119 metadata: result.metadata,
120 stats: result.stats,
121 symbolic_data: Default::default(),
122 }, symbolic)
123 .with_meshes(meshes);
124 let body = encode_response(&response)?;
125 Ok::<_, ApiError>((body, response.symbolic_data, admission_guard))
126 })
127 .await??;
128
129 let cache = state.cache.clone();
134 let cached_body = body.clone();
135 tokio::spawn(async move {
136 cache_symbolic_data_off_runtime(cache.clone(), cache_key, symbolic_data).await;
137 if let Err(e) = cache.set_bytes(&response_cache_key, &cached_body).await {
138 tracing::error!(error = %e, "Failed to cache result");
139 }
140 });
141
142 Ok(json_body(body))
143}
144
145fn encode_response(response: &SymbolicParseResponse) -> Result<Bytes, ApiError> {
149 let body = serde_json::to_vec(response)?;
150 tracing::info!(cache_key = %response.cache_key, size = body.len(), "JSON parse response encoded");
151 Ok(body.into())
152}
153
154fn json_body(body: Bytes) -> Response {
156 ([(header::CONTENT_TYPE, "application/json")], Body::from(body)).into_response()
157}
158
159pub async fn parse_stream(
161 State(state): State<AppState>,
162 Query(query): Query<ParseQuery>,
163 mut multipart: Multipart,
164) -> Result<Response, ApiError> {
165 let tessellation_quality = query.resolved_tessellation_quality()?;
166
167 let admission_guard = state
173 .admission
174 .acquire(state.config.max_file_size_mb as u64 * 1024 * 1024)
175 .await?;
176 let data = extract_file(&mut multipart, state.config.max_file_size_mb).await?;
177
178 let content = data;
179 let initial_batch_size = state.config.initial_batch_size;
180 let max_batch_size = state.config.max_batch_size;
181
182 let stream = process_streaming(
184 content,
185 initial_batch_size,
186 max_batch_size,
187 query.opening_filter,
188 tessellation_quality,
189 Some(admission_guard),
190 )
191 .map(|mut event: StreamEvent| {
192 if let StreamEvent::Batch { meshes, .. } = &mut event {
197 for mesh in meshes.iter_mut() {
198 mesh_to_yup_in_place(mesh);
199 }
200 }
201 let json = serde_json::to_string(&event).unwrap_or_else(|e| {
202 serde_json::to_string(&StreamEvent::Error {
203 message: e.to_string(),
204 })
205 .unwrap()
206 });
207 Ok::<_, Infallible>(Event::default().data(json))
208 });
209
210 Ok(Sse::new(stream)
211 .keep_alive(KeepAlive::default())
212 .into_response())
213}
214
215pub async fn parse_metadata(
217 State(state): State<AppState>,
218 mut multipart: Multipart,
219) -> Result<Json<MetadataResponse>, ApiError> {
220 let admission_guard = state
226 .admission
227 .acquire(state.config.max_file_size_mb as u64 * 1024 * 1024)
228 .await?;
229 let data = extract_file(&mut multipart, state.config.max_file_size_mb).await?;
230
231 let file_size = data.len();
232 let content = data;
233
234 let result = tokio::task::spawn_blocking(move || {
236 let mut scanner = EntityScanner::new(&content);
237 let mut entity_count = 0usize;
238 let mut geometry_count = 0usize;
239
240 while let Some((_, type_name, _, _)) = scanner.next_entity() {
241 entity_count += 1;
242 if ifc_lite_core::has_geometry_by_name(type_name) {
243 geometry_count += 1;
244 }
245 }
246
247 let oversized_id_count = scanner.skipped_oversized_ids();
256 let malformed_record_found = scanner.malformed_record_start().is_some();
257 ifc_lite_core::report_scan_diagnostics(oversized_id_count, malformed_record_found);
258
259 let schema_version = detect_schema_version(&content);
260
261 (
262 MetadataResponse {
263 entity_count,
264 geometry_count,
265 schema_version: schema_version.to_string(),
266 file_size,
267 oversized_id_count,
268 malformed_record_found,
269 },
270 admission_guard,
271 )
272 })
273 .await?;
274 let (result, _admission) = result;
275
276 Ok(Json(result))
277}