helios_sof/lib.rs
1//! # SQL-on-FHIR Implementation
2//!
3//! This crate provides a complete implementation of the [SQL-on-FHIR
4//! specification](https://sql-on-fhir.org/ig/latest),
5//! enabling the transformation of FHIR resources into tabular data using declarative
6//! ViewDefinitions. It supports all major FHIR versions (R4, R4B, R5, R6) through
7//! a version-agnostic abstraction layer.
8
9//!
10//! There are three consumers of this crate:
11//! - [sof_cli](../sof_cli/index.html) - A command-line interface for the SQL-on-FHIR implementation,
12//! allowing users to execute ViewDefinition transformations on FHIR Bundle resources
13//! and output the results in various formats.
14//! - [sof_server](../sof_server/index.html) - A stateless HTTP server implementation for the SQL-on-FHIR specification,
15//! enabling HTTP-based access to ViewDefinition transformation capabilities.
16//! - [hfs](../hfs/index.html) - The full featured Helios FHIR Server.
17//!
18//! ## Architecture
19//!
20//! The SOF crate is organized around these key components:
21//! - **Version-agnostic enums** ([`SofViewDefinition`], [`SofBundle`]): Multi-version containers
22//! - **Processing engine** ([`run_view_definition`]): Core transformation logic
23//! - **Output formats** ([`ContentType`]): Support for CSV, JSON, NDJSON, and Parquet
24//! - **Trait abstractions** ([`ViewDefinitionTrait`], [`BundleTrait`]): Version independence
25//!
26//! ## Key Features
27//!
28//! - **Multi-version FHIR support**: Works with R4, R4B, R5, and R6 resources
29//! - **FHIRPath evaluation**: Complex path expressions for data extraction
30//! - **forEach iteration**: Supports flattening of nested FHIR structures
31//! - **unionAll operations**: Combines multiple select statements
32//! - **Collection handling**: Proper array serialization for multi-valued fields
33//! - **Output formats**: CSV (with/without headers), JSON, NDJSON, Parquet support
34//!
35//! ## Usage Example
36//!
37//! ```rust
38//! use helios_sof::{SofViewDefinition, SofBundle, ContentType, run_view_definition};
39//! use helios_fhir::FhirVersion;
40//!
41//! # #[cfg(feature = "R4")]
42//! # {
43//! // Parse a ViewDefinition and Bundle from JSON
44//! let view_definition_json = r#"{
45//! "resourceType": "ViewDefinition",
46//! "status": "active",
47//! "resource": "Patient",
48//! "select": [{
49//! "column": [{
50//! "name": "id",
51//! "path": "id"
52//! }, {
53//! "name": "name",
54//! "path": "name.family"
55//! }]
56//! }]
57//! }"#;
58//!
59//! let bundle_json = r#"{
60//! "resourceType": "Bundle",
61//! "type": "collection",
62//! "entry": [{
63//! "resource": {
64//! "resourceType": "Patient",
65//! "id": "example",
66//! "name": [{
67//! "family": "Doe",
68//! "given": ["John"]
69//! }]
70//! }
71//! }]
72//! }"#;
73//!
74//! let view_definition: helios_fhir::r4::ViewDefinition = serde_json::from_str(view_definition_json)?;
75//! let bundle: helios_fhir::r4::Bundle = serde_json::from_str(bundle_json)?;
76//!
77//! // Wrap in version-agnostic containers
78//! let sof_view = SofViewDefinition::R4(view_definition);
79//! let sof_bundle = SofBundle::R4(bundle);
80//!
81//! // Transform to CSV with headers
82//! let csv_output = run_view_definition(
83//! sof_view,
84//! sof_bundle,
85//! ContentType::CsvWithHeader
86//! )?;
87//!
88//! // Check the CSV output
89//! let csv_string = String::from_utf8(csv_output)?;
90//! assert!(csv_string.contains("id,name"));
91//! // CSV values are quoted
92//! assert!(csv_string.contains("example") && csv_string.contains("Doe"));
93//! # }
94//! # Ok::<(), Box<dyn std::error::Error>>(())
95//! ```
96//!
97//! ## Advanced Features
98//!
99//! ### forEach Iteration
100//!
101//! ViewDefinitions can iterate over collections using `forEach` and `forEachOrNull`:
102//!
103//! ```json
104//! {
105//! "select": [{
106//! "forEach": "name",
107//! "column": [{
108//! "name": "family_name",
109//! "path": "family"
110//! }]
111//! }]
112//! }
113//! ```
114//!
115//! ### Constants and Variables
116//!
117//! Define reusable values in ViewDefinitions:
118//!
119//! ```json
120//! {
121//! "constant": [{
122//! "name": "system",
123//! "valueString": "http://loinc.org"
124//! }],
125//! "select": [{
126//! "where": [{
127//! "path": "code.coding.system = %system"
128//! }]
129//! }]
130//! }
131//! ```
132//!
133//! ### Where Clauses
134//!
135//! Filter resources using FHIRPath expressions:
136//!
137//! ```json
138//! {
139//! "where": [{
140//! "path": "active = true"
141//! }, {
142//! "path": "birthDate.exists()"
143//! }]
144//! }
145//! ```
146//!
147//! ## Error Handling
148//!
149//! The crate provides comprehensive error handling through [`SofError`]:
150//!
151//! ```rust,no_run
152//! use helios_sof::{SofError, SofViewDefinition, SofBundle, ContentType, run_view_definition};
153//!
154//! # let view = SofViewDefinition::R4(helios_fhir::r4::ViewDefinition::default());
155//! # let bundle = SofBundle::R4(helios_fhir::r4::Bundle::default());
156//! # let content_type = ContentType::Json;
157//! match run_view_definition(view, bundle, content_type) {
158//! Ok(output) => {
159//! // Process successful transformation
160//! },
161//! Err(SofError::InvalidViewDefinition(msg)) => {
162//! eprintln!("ViewDefinition validation failed: {}", msg);
163//! },
164//! Err(SofError::FhirPathError(msg)) => {
165//! eprintln!("FHIRPath evaluation failed: {}", msg);
166//! },
167//! Err(e) => {
168//! eprintln!("Other error: {}", e);
169//! }
170//! }
171//! ```
172//! ## Feature Flags
173//!
174//! Enable support for specific FHIR versions:
175//! - `R4`: FHIR 4.0.1 support
176//! - `R4B`: FHIR 4.3.0 support
177//! - `R5`: FHIR 5.0.0 support
178//! - `R6`: FHIR 6.0.0 support
179
180pub mod traits;
181
182use chrono::{DateTime, Utc};
183use helios_fhirpath::{EvaluationContext, EvaluationResult, evaluate_expression};
184use serde::{Deserialize, Serialize};
185use std::collections::HashMap;
186use thiserror::Error;
187use traits::*;
188
189// Re-export commonly used types and traits for easier access
190pub use helios_fhir::FhirVersion;
191pub use traits::{BundleTrait, ResourceTrait, ViewDefinitionTrait};
192
193/// Multi-version ViewDefinition container supporting version-agnostic operations.
194///
195/// This enum provides a unified interface for working with ViewDefinition resources
196/// across different FHIR specification versions. It enables applications to handle
197/// multiple FHIR versions simultaneously while maintaining type safety.
198///
199/// # Supported Versions
200///
201/// - **R4**: FHIR 4.0.1 ViewDefinition (normative)
202/// - **R4B**: FHIR 4.3.0 ViewDefinition (ballot)
203/// - **R5**: FHIR 5.0.0 ViewDefinition (ballot)
204/// - **R6**: FHIR 6.0.0 ViewDefinition (draft)
205///
206/// # Examples
207///
208/// ```rust
209/// use helios_sof::{SofViewDefinition, ContentType};
210/// # #[cfg(feature = "R4")]
211/// use helios_fhir::r4::ViewDefinition;
212///
213/// # #[cfg(feature = "R4")]
214/// # {
215/// // Parse from JSON
216/// let json = r#"{
217/// "resourceType": "ViewDefinition",
218/// "resource": "Patient",
219/// "select": [{
220/// "column": [{
221/// "name": "id",
222/// "path": "id"
223/// }]
224/// }]
225/// }"#;
226///
227/// let view_def: ViewDefinition = serde_json::from_str(json)?;
228/// let sof_view = SofViewDefinition::R4(view_def);
229///
230/// // Check version
231/// assert_eq!(sof_view.version(), helios_fhir::FhirVersion::R4);
232/// # }
233/// # Ok::<(), Box<dyn std::error::Error>>(())
234/// ```
235#[derive(Debug, Clone)]
236pub enum SofViewDefinition {
237 #[cfg(feature = "R4")]
238 R4(helios_fhir::r4::ViewDefinition),
239 #[cfg(feature = "R4B")]
240 R4B(helios_fhir::r4b::ViewDefinition),
241 #[cfg(feature = "R5")]
242 R5(helios_fhir::r5::ViewDefinition),
243 #[cfg(feature = "R6")]
244 R6(helios_fhir::r6::ViewDefinition),
245}
246
247impl SofViewDefinition {
248 /// Returns the FHIR specification version of this ViewDefinition.
249 ///
250 /// This method provides version detection for multi-version applications,
251 /// enabling version-specific processing logic and compatibility checks.
252 ///
253 /// # Returns
254 ///
255 /// The `FhirVersion` enum variant corresponding to this ViewDefinition's specification.
256 ///
257 /// # Examples
258 ///
259 /// ```rust
260 /// use helios_sof::SofViewDefinition;
261 /// use helios_fhir::FhirVersion;
262 ///
263 /// # #[cfg(feature = "R5")]
264 /// # {
265 /// # let view_def = helios_fhir::r5::ViewDefinition::default();
266 /// let sof_view = SofViewDefinition::R5(view_def);
267 /// assert_eq!(sof_view.version(), helios_fhir::FhirVersion::R5);
268 /// # }
269 /// ```
270 pub fn version(&self) -> helios_fhir::FhirVersion {
271 match self {
272 #[cfg(feature = "R4")]
273 SofViewDefinition::R4(_) => helios_fhir::FhirVersion::R4,
274 #[cfg(feature = "R4B")]
275 SofViewDefinition::R4B(_) => helios_fhir::FhirVersion::R4B,
276 #[cfg(feature = "R5")]
277 SofViewDefinition::R5(_) => helios_fhir::FhirVersion::R5,
278 #[cfg(feature = "R6")]
279 SofViewDefinition::R6(_) => helios_fhir::FhirVersion::R6,
280 }
281 }
282}
283
284/// Multi-version Bundle container supporting version-agnostic operations.
285///
286/// This enum provides a unified interface for working with FHIR Bundle resources
287/// across different FHIR specification versions. Bundles contain the actual FHIR
288/// resources that will be processed by ViewDefinitions.
289///
290/// # Supported Versions
291///
292/// - **R4**: FHIR 4.0.1 Bundle (normative)
293/// - **R4B**: FHIR 4.3.0 Bundle (ballot)
294/// - **R5**: FHIR 5.0.0 Bundle (ballot)
295/// - **R6**: FHIR 6.0.0 Bundle (draft)
296///
297/// # Examples
298///
299/// ```rust
300/// use helios_sof::SofBundle;
301/// # #[cfg(feature = "R4")]
302/// use helios_fhir::r4::Bundle;
303///
304/// # #[cfg(feature = "R4")]
305/// # {
306/// // Parse from JSON
307/// let json = r#"{
308/// "resourceType": "Bundle",
309/// "type": "collection",
310/// "entry": [{
311/// "resource": {
312/// "resourceType": "Patient",
313/// "id": "example"
314/// }
315/// }]
316/// }"#;
317///
318/// let bundle: Bundle = serde_json::from_str(json)?;
319/// let sof_bundle = SofBundle::R4(bundle);
320///
321/// // Check version compatibility
322/// assert_eq!(sof_bundle.version(), helios_fhir::FhirVersion::R4);
323/// # }
324/// # Ok::<(), Box<dyn std::error::Error>>(())
325/// ```
326#[derive(Debug, Clone)]
327pub enum SofBundle {
328 #[cfg(feature = "R4")]
329 R4(helios_fhir::r4::Bundle),
330 #[cfg(feature = "R4B")]
331 R4B(helios_fhir::r4b::Bundle),
332 #[cfg(feature = "R5")]
333 R5(helios_fhir::r5::Bundle),
334 #[cfg(feature = "R6")]
335 R6(helios_fhir::r6::Bundle),
336}
337
338impl SofBundle {
339 /// Returns the FHIR specification version of this Bundle.
340 ///
341 /// This method provides version detection for multi-version applications,
342 /// ensuring that ViewDefinitions and Bundles use compatible FHIR versions.
343 ///
344 /// # Returns
345 ///
346 /// The `FhirVersion` enum variant corresponding to this Bundle's specification.
347 ///
348 /// # Examples
349 ///
350 /// ```rust
351 /// use helios_sof::SofBundle;
352 /// use helios_fhir::FhirVersion;
353 ///
354 /// # #[cfg(feature = "R4")]
355 /// # {
356 /// # let bundle = helios_fhir::r4::Bundle::default();
357 /// let sof_bundle = SofBundle::R4(bundle);
358 /// assert_eq!(sof_bundle.version(), helios_fhir::FhirVersion::R4);
359 /// # }
360 /// ```
361 pub fn version(&self) -> helios_fhir::FhirVersion {
362 match self {
363 #[cfg(feature = "R4")]
364 SofBundle::R4(_) => helios_fhir::FhirVersion::R4,
365 #[cfg(feature = "R4B")]
366 SofBundle::R4B(_) => helios_fhir::FhirVersion::R4B,
367 #[cfg(feature = "R5")]
368 SofBundle::R5(_) => helios_fhir::FhirVersion::R5,
369 #[cfg(feature = "R6")]
370 SofBundle::R6(_) => helios_fhir::FhirVersion::R6,
371 }
372 }
373}
374
375/// Multi-version CapabilityStatement container supporting version-agnostic operations.
376///
377/// This enum provides a unified interface for working with CapabilityStatement resources
378/// across different FHIR specification versions. It enables applications to handle
379/// multiple FHIR versions simultaneously while maintaining type safety.
380///
381/// # Supported Versions
382///
383/// - **R4**: FHIR 4.0.1 CapabilityStatement (normative)
384/// - **R4B**: FHIR 4.3.0 CapabilityStatement (ballot)
385/// - **R5**: FHIR 5.0.0 CapabilityStatement (ballot)
386/// - **R6**: FHIR 6.0.0 CapabilityStatement (draft)
387#[derive(Debug, Clone, Serialize, Deserialize)]
388#[serde(untagged)]
389pub enum SofCapabilityStatement {
390 #[cfg(feature = "R4")]
391 R4(helios_fhir::r4::CapabilityStatement),
392 #[cfg(feature = "R4B")]
393 R4B(helios_fhir::r4b::CapabilityStatement),
394 #[cfg(feature = "R5")]
395 R5(helios_fhir::r5::CapabilityStatement),
396 #[cfg(feature = "R6")]
397 R6(helios_fhir::r6::CapabilityStatement),
398}
399
400impl SofCapabilityStatement {
401 /// Returns the FHIR specification version of this CapabilityStatement.
402 pub fn version(&self) -> helios_fhir::FhirVersion {
403 match self {
404 #[cfg(feature = "R4")]
405 SofCapabilityStatement::R4(_) => helios_fhir::FhirVersion::R4,
406 #[cfg(feature = "R4B")]
407 SofCapabilityStatement::R4B(_) => helios_fhir::FhirVersion::R4B,
408 #[cfg(feature = "R5")]
409 SofCapabilityStatement::R5(_) => helios_fhir::FhirVersion::R5,
410 #[cfg(feature = "R6")]
411 SofCapabilityStatement::R6(_) => helios_fhir::FhirVersion::R6,
412 }
413 }
414}
415
416/// Type alias for the version-independent Parameters container.
417///
418/// This alias provides backward compatibility while using the unified
419/// VersionIndependentParameters from the helios_fhir crate.
420pub type SofParameters = helios_fhir::VersionIndependentParameters;
421
422/// Comprehensive error type for SQL-on-FHIR operations.
423///
424/// This enum covers all possible error conditions that can occur during
425/// ViewDefinition processing, from validation failures to output formatting issues.
426/// Each variant provides specific context about the error to aid in debugging.
427///
428/// # Error Categories
429///
430/// - **Validation**: ViewDefinition structure and logic validation
431/// - **Evaluation**: FHIRPath expression evaluation failures
432/// - **I/O**: File and serialization operations
433/// - **Format**: Output format conversion issues
434///
435/// # Examples
436///
437/// ```rust,no_run
438/// use helios_sof::{SofError, SofViewDefinition, SofBundle, ContentType, run_view_definition};
439///
440/// # let view = SofViewDefinition::R4(helios_fhir::r4::ViewDefinition::default());
441/// # let bundle = SofBundle::R4(helios_fhir::r4::Bundle::default());
442/// # let content_type = ContentType::Json;
443/// match run_view_definition(view, bundle, content_type) {
444/// Ok(output) => {
445/// println!("Transformation successful");
446/// },
447/// Err(SofError::InvalidViewDefinition(msg)) => {
448/// eprintln!("ViewDefinition validation failed: {}", msg);
449/// },
450/// Err(SofError::FhirPathError(msg)) => {
451/// eprintln!("FHIRPath evaluation error: {}", msg);
452/// },
453/// Err(SofError::UnsupportedContentType(format)) => {
454/// eprintln!("Unsupported output format: {}", format);
455/// },
456/// Err(e) => {
457/// eprintln!("Other error: {}", e);
458/// }
459/// }
460/// ```
461#[derive(Debug, Error)]
462pub enum SofError {
463 /// ViewDefinition structure or logic validation failed.
464 ///
465 /// This error occurs when a ViewDefinition contains invalid or inconsistent
466 /// configuration, such as missing required fields, invalid FHIRPath expressions,
467 /// or incompatible select/unionAll structures.
468 #[error("Invalid ViewDefinition: {0}")]
469 InvalidViewDefinition(String),
470
471 /// FHIRPath expression evaluation failed.
472 ///
473 /// This error occurs when a FHIRPath expression in a ViewDefinition cannot
474 /// be evaluated, either due to syntax errors or runtime evaluation issues.
475 #[error("FHIRPath evaluation error: {0}")]
476 FhirPathError(String),
477
478 /// JSON serialization/deserialization failed.
479 ///
480 /// This error occurs when parsing input JSON or serializing output data fails,
481 /// typically due to malformed JSON or incompatible data structures.
482 #[error("Serialization error: {0}")]
483 SerializationError(#[from] serde_json::Error),
484
485 /// CSV processing failed.
486 ///
487 /// This error occurs during CSV output generation, such as when writing
488 /// headers or data rows to the CSV format.
489 #[error("CSV error: {0}")]
490 CsvError(#[from] csv::Error),
491
492 /// File I/O operation failed.
493 ///
494 /// This error occurs when reading input files or writing output files fails,
495 /// typically due to permission issues or missing files.
496 #[error("IO error: {0}")]
497 IoError(#[from] std::io::Error),
498
499 /// Unsupported output content type requested.
500 ///
501 /// This error occurs when an invalid or unimplemented content type is
502 /// specified for output formatting.
503 #[error("Unsupported content type: {0}")]
504 UnsupportedContentType(String),
505
506 /// CSV writer internal error.
507 ///
508 /// This error occurs when the CSV writer encounters an internal issue
509 /// that prevents successful output generation.
510 #[error("CSV writer error: {0}")]
511 CsvWriterError(String),
512}
513
514/// Supported output content types for ViewDefinition transformations.
515///
516/// This enum defines the available output formats for transformed FHIR data.
517/// Each format has specific characteristics and use cases for different
518/// integration scenarios.
519///
520/// # Format Descriptions
521///
522/// - **CSV**: Comma-separated values without headers
523/// - **CSV with Headers**: Comma-separated values with column headers
524/// - **JSON**: Pretty-printed JSON array of objects
525/// - **NDJSON**: Newline-delimited JSON (one object per line)
526/// - **Parquet**: Apache Parquet columnar format (planned)
527///
528/// # Examples
529///
530/// ```rust
531/// use helios_sof::ContentType;
532///
533/// // Parse from string
534/// let csv_type = ContentType::from_string("text/csv")?;
535/// assert_eq!(csv_type, ContentType::CsvWithHeader); // Default includes headers
536///
537/// let json_type = ContentType::from_string("application/json")?;
538/// assert_eq!(json_type, ContentType::Json);
539///
540/// // CSV without headers
541/// let csv_no_headers = ContentType::from_string("text/csv;header=false")?;
542/// assert_eq!(csv_no_headers, ContentType::Csv);
543/// # Ok::<(), helios_sof::SofError>(())
544/// ```
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546pub enum ContentType {
547 /// Comma-separated values format without headers
548 Csv,
549 /// Comma-separated values format with column headers
550 CsvWithHeader,
551 /// Pretty-printed JSON array format
552 Json,
553 /// Newline-delimited JSON format (NDJSON)
554 NdJson,
555 /// Apache Parquet columnar format (not yet implemented)
556 Parquet,
557}
558
559impl ContentType {
560 /// Parse a content type from its MIME type string representation.
561 ///
562 /// This method converts standard MIME type strings to the corresponding
563 /// ContentType enum variants. It supports the SQL-on-FHIR specification's
564 /// recommended content types.
565 ///
566 /// # Supported MIME Types
567 ///
568 /// - `"text/csv"` → [`ContentType::Csv`]
569 /// - `"text/csv"` → [`ContentType::CsvWithHeader`] (default: headers included)
570 /// - `"text/csv;header=true"` → [`ContentType::CsvWithHeader`]
571 /// - `"text/csv;header=false"` → [`ContentType::Csv`]
572 /// - `"application/json"` → [`ContentType::Json`]
573 /// - `"application/ndjson"` → [`ContentType::NdJson`]
574 /// - `"application/parquet"` → [`ContentType::Parquet`]
575 ///
576 /// # Arguments
577 ///
578 /// * `s` - The MIME type string to parse
579 ///
580 /// # Returns
581 ///
582 /// * `Ok(ContentType)` - Successfully parsed content type
583 /// * `Err(SofError::UnsupportedContentType)` - Unknown or unsupported MIME type
584 ///
585 /// # Examples
586 ///
587 /// ```rust
588 /// use helios_sof::ContentType;
589 ///
590 /// // Shortened format names
591 /// let csv = ContentType::from_string("csv")?;
592 /// assert_eq!(csv, ContentType::CsvWithHeader);
593 ///
594 /// let json = ContentType::from_string("json")?;
595 /// assert_eq!(json, ContentType::Json);
596 ///
597 /// let ndjson = ContentType::from_string("ndjson")?;
598 /// assert_eq!(ndjson, ContentType::NdJson);
599 ///
600 /// // Full MIME types still supported
601 /// let csv_mime = ContentType::from_string("text/csv")?;
602 /// assert_eq!(csv_mime, ContentType::CsvWithHeader);
603 ///
604 /// // CSV with headers explicitly
605 /// let csv_headers = ContentType::from_string("text/csv;header=true")?;
606 /// assert_eq!(csv_headers, ContentType::CsvWithHeader);
607 ///
608 /// // CSV without headers
609 /// let csv_no_headers = ContentType::from_string("text/csv;header=false")?;
610 /// assert_eq!(csv_no_headers, ContentType::Csv);
611 ///
612 /// // JSON format
613 /// let json_mime = ContentType::from_string("application/json")?;
614 /// assert_eq!(json_mime, ContentType::Json);
615 ///
616 /// // Error for unsupported type
617 /// assert!(ContentType::from_string("text/plain").is_err());
618 /// # Ok::<(), helios_sof::SofError>(())
619 /// ```
620 pub fn from_string(s: &str) -> Result<Self, SofError> {
621 match s {
622 // Shortened format names
623 "csv" => Ok(ContentType::CsvWithHeader),
624 "json" => Ok(ContentType::Json),
625 "ndjson" => Ok(ContentType::NdJson),
626 "parquet" => Ok(ContentType::Parquet),
627 // Full MIME types (for Accept header compatibility)
628 "text/csv;header=false" => Ok(ContentType::Csv),
629 "text/csv" | "text/csv;header=true" => Ok(ContentType::CsvWithHeader),
630 "application/json" => Ok(ContentType::Json),
631 "application/ndjson" => Ok(ContentType::NdJson),
632 "application/parquet" => Ok(ContentType::Parquet),
633 _ => Err(SofError::UnsupportedContentType(s.to_string())),
634 }
635 }
636}
637
638/// Returns the FHIR version string for the newest enabled version.
639///
640/// This function provides the version string that should be used in CapabilityStatements
641/// and other FHIR resources that need to specify their version.
642pub fn get_fhir_version_string() -> &'static str {
643 let newest_version = get_newest_enabled_fhir_version();
644
645 match newest_version {
646 #[cfg(feature = "R4")]
647 helios_fhir::FhirVersion::R4 => "4.0.1",
648 #[cfg(feature = "R4B")]
649 helios_fhir::FhirVersion::R4B => "4.3.0",
650 #[cfg(feature = "R5")]
651 helios_fhir::FhirVersion::R5 => "5.0.0",
652 #[cfg(feature = "R6")]
653 helios_fhir::FhirVersion::R6 => "6.0.0",
654 }
655}
656
657/// Returns the newest FHIR version that is enabled at compile time.
658///
659/// This function uses compile-time feature detection to determine which FHIR
660/// version should be used when multiple versions are enabled. The priority order
661/// is: R6 > R5 > R4B > R4, where newer versions take precedence.
662///
663/// # Examples
664///
665/// ```rust
666/// use helios_sof::{get_newest_enabled_fhir_version, FhirVersion};
667///
668/// # #[cfg(any(feature = "R4", feature = "R4B", feature = "R5", feature = "R6"))]
669/// # {
670/// let version = get_newest_enabled_fhir_version();
671/// // If R5 and R4 are both enabled, this returns R5
672/// # }
673/// ```
674///
675/// # Panics
676///
677/// This function will panic at compile time if no FHIR version features are enabled.
678pub fn get_newest_enabled_fhir_version() -> helios_fhir::FhirVersion {
679 #[cfg(feature = "R6")]
680 return helios_fhir::FhirVersion::R6;
681
682 #[cfg(all(feature = "R5", not(feature = "R6")))]
683 return helios_fhir::FhirVersion::R5;
684
685 #[cfg(all(feature = "R4B", not(feature = "R5"), not(feature = "R6")))]
686 return helios_fhir::FhirVersion::R4B;
687
688 #[cfg(all(
689 feature = "R4",
690 not(feature = "R4B"),
691 not(feature = "R5"),
692 not(feature = "R6")
693 ))]
694 return helios_fhir::FhirVersion::R4;
695
696 #[cfg(not(any(feature = "R4", feature = "R4B", feature = "R5", feature = "R6")))]
697 panic!("At least one FHIR version feature must be enabled");
698}
699
700/// A single row of processed tabular data from ViewDefinition transformation.
701///
702/// This struct represents one row in the output table, containing values for
703/// each column defined in the ViewDefinition. Values are stored as optional
704/// JSON values to handle nullable fields and diverse FHIR data types.
705///
706/// # Structure
707///
708/// Each `ProcessedRow` contains a vector of optional JSON values, where:
709/// - `Some(value)` represents a non-null column value
710/// - `None` represents a null/missing column value
711/// - The order matches the column order in [`ProcessedResult::columns`]
712///
713/// # Examples
714///
715/// ```rust
716/// use helios_sof::ProcessedRow;
717/// use serde_json::Value;
718///
719/// let row = ProcessedRow {
720/// values: vec![
721/// Some(Value::String("patient-123".to_string())),
722/// Some(Value::String("Doe".to_string())),
723/// None, // Missing birth date
724/// Some(Value::Bool(true)),
725/// ]
726/// };
727/// ```
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct ProcessedRow {
730 /// Column values for this row, ordered according to ProcessedResult::columns
731 pub values: Vec<Option<serde_json::Value>>,
732}
733
734/// Complete result of ViewDefinition transformation containing columns and data rows.
735///
736/// This struct represents the tabular output from processing a ViewDefinition
737/// against a Bundle of FHIR resources. It contains both the column definitions
738/// and the actual data rows in a format ready for serialization to various
739/// output formats.
740///
741/// # Structure
742///
743/// - [`columns`](Self::columns): Ordered list of column names from the ViewDefinition
744/// - [`rows`](Self::rows): Data rows where each row contains values in column order
745///
746/// # Examples
747///
748/// ```rust
749/// use helios_sof::{ProcessedResult, ProcessedRow};
750/// use serde_json::Value;
751///
752/// let result = ProcessedResult {
753/// columns: vec![
754/// "patient_id".to_string(),
755/// "family_name".to_string(),
756/// "given_name".to_string(),
757/// ],
758/// rows: vec![
759/// ProcessedRow {
760/// values: vec![
761/// Some(Value::String("patient-1".to_string())),
762/// Some(Value::String("Smith".to_string())),
763/// Some(Value::String("John".to_string())),
764/// ]
765/// },
766/// ProcessedRow {
767/// values: vec![
768/// Some(Value::String("patient-2".to_string())),
769/// Some(Value::String("Doe".to_string())),
770/// None, // Missing given name
771/// ]
772/// },
773/// ]
774/// };
775///
776/// assert_eq!(result.columns.len(), 3);
777/// assert_eq!(result.rows.len(), 2);
778/// ```
779#[derive(Debug, Clone, Serialize, Deserialize)]
780pub struct ProcessedResult {
781 /// Ordered list of column names as defined in the ViewDefinition
782 pub columns: Vec<String>,
783 /// Data rows containing values for each column
784 pub rows: Vec<ProcessedRow>,
785}
786
787/// Execute a SQL-on-FHIR ViewDefinition transformation on a FHIR Bundle.
788///
789/// This is the main entry point for SQL-on-FHIR transformations. It processes
790/// a ViewDefinition against a Bundle of FHIR resources and produces output in
791/// the specified format. The function handles version compatibility, validation,
792/// FHIRPath evaluation, and output formatting.
793///
794/// # Arguments
795///
796/// * `view_definition` - The ViewDefinition containing transformation logic
797/// * `bundle` - The Bundle containing FHIR resources to process
798/// * `content_type` - The desired output format
799///
800/// # Returns
801///
802/// * `Ok(Vec<u8>)` - Formatted output bytes ready for writing to file or stdout
803/// * `Err(SofError)` - Detailed error information about what went wrong
804///
805/// # Validation
806///
807/// The function performs comprehensive validation:
808/// - FHIR version compatibility between ViewDefinition and Bundle
809/// - ViewDefinition structure and logic validation
810/// - FHIRPath expression syntax and evaluation
811/// - Output format compatibility
812///
813/// # Examples
814///
815/// ```rust
816/// use helios_sof::{SofViewDefinition, SofBundle, ContentType, run_view_definition};
817///
818/// # #[cfg(feature = "R4")]
819/// # {
820/// // Create a simple ViewDefinition
821/// let view_json = serde_json::json!({
822/// "resourceType": "ViewDefinition",
823/// "status": "active",
824/// "resource": "Patient",
825/// "select": [{
826/// "column": [{
827/// "name": "id",
828/// "path": "id"
829/// }]
830/// }]
831/// });
832/// let view_def: helios_fhir::r4::ViewDefinition = serde_json::from_value(view_json)?;
833///
834/// // Create a simple Bundle
835/// let bundle_json = serde_json::json!({
836/// "resourceType": "Bundle",
837/// "type": "collection",
838/// "entry": []
839/// });
840/// let bundle: helios_fhir::r4::Bundle = serde_json::from_value(bundle_json)?;
841///
842/// let sof_view = SofViewDefinition::R4(view_def);
843/// let sof_bundle = SofBundle::R4(bundle);
844///
845/// // Generate CSV with headers
846/// let csv_output = run_view_definition(
847/// sof_view,
848/// sof_bundle,
849/// ContentType::CsvWithHeader
850/// )?;
851///
852/// // Write to file or stdout
853/// std::fs::write("output.csv", csv_output)?;
854/// # }
855/// # Ok::<(), Box<dyn std::error::Error>>(())
856/// ```
857///
858/// # Error Handling
859///
860/// Common error scenarios:
861///
862/// ```rust,no_run
863/// use helios_sof::{SofError, SofViewDefinition, SofBundle, ContentType, run_view_definition};
864///
865/// # let view = SofViewDefinition::R4(helios_fhir::r4::ViewDefinition::default());
866/// # let bundle = SofBundle::R4(helios_fhir::r4::Bundle::default());
867/// # let content_type = ContentType::Json;
868/// match run_view_definition(view, bundle, content_type) {
869/// Ok(output) => {
870/// println!("Success: {} bytes generated", output.len());
871/// },
872/// Err(SofError::InvalidViewDefinition(msg)) => {
873/// eprintln!("ViewDefinition error: {}", msg);
874/// },
875/// Err(SofError::FhirPathError(msg)) => {
876/// eprintln!("FHIRPath error: {}", msg);
877/// },
878/// Err(e) => {
879/// eprintln!("Other error: {}", e);
880/// }
881/// }
882/// ```
883pub fn run_view_definition(
884 view_definition: SofViewDefinition,
885 bundle: SofBundle,
886 content_type: ContentType,
887) -> Result<Vec<u8>, SofError> {
888 run_view_definition_with_options(view_definition, bundle, content_type, RunOptions::default())
889}
890
891/// Options for filtering and controlling ViewDefinition execution
892#[derive(Debug, Clone, Default)]
893pub struct RunOptions {
894 /// Filter resources modified after this time
895 pub since: Option<DateTime<Utc>>,
896 /// Limit the number of results
897 pub limit: Option<usize>,
898 /// Page number for pagination (1-based)
899 pub page: Option<usize>,
900}
901
902/// Execute a ViewDefinition transformation with additional filtering options.
903///
904/// This function extends the basic `run_view_definition` with support for:
905/// - Filtering resources by modification time (`since`)
906/// - Limiting results (`limit`)
907/// - Pagination (`page`)
908///
909/// # Arguments
910///
911/// * `view_definition` - The ViewDefinition to execute
912/// * `bundle` - The Bundle containing resources to transform
913/// * `content_type` - Desired output format
914/// * `options` - Additional filtering and control options
915///
916/// # Returns
917///
918/// The transformed data in the requested format, with filtering applied.
919pub fn run_view_definition_with_options(
920 view_definition: SofViewDefinition,
921 bundle: SofBundle,
922 content_type: ContentType,
923 options: RunOptions,
924) -> Result<Vec<u8>, SofError> {
925 // Filter bundle resources by since parameter before processing
926 let filtered_bundle = if let Some(since) = options.since {
927 filter_bundle_by_since(bundle, since)?
928 } else {
929 bundle
930 };
931
932 // Process the ViewDefinition to generate tabular data
933 let processed_result = process_view_definition(view_definition, filtered_bundle)?;
934
935 // Apply pagination if needed
936 let processed_result = if options.limit.is_some() || options.page.is_some() {
937 apply_pagination_to_result(processed_result, options.limit, options.page)?
938 } else {
939 processed_result
940 };
941
942 // Format the result according to the requested content type
943 format_output(processed_result, content_type)
944}
945
946fn process_view_definition(
947 view_definition: SofViewDefinition,
948 bundle: SofBundle,
949) -> Result<ProcessedResult, SofError> {
950 // Ensure both resources use the same FHIR version
951 if view_definition.version() != bundle.version() {
952 return Err(SofError::InvalidViewDefinition(
953 "ViewDefinition and Bundle must use the same FHIR version".to_string(),
954 ));
955 }
956
957 match (view_definition, bundle) {
958 #[cfg(feature = "R4")]
959 (SofViewDefinition::R4(vd), SofBundle::R4(bundle)) => {
960 process_view_definition_generic(vd, bundle)
961 }
962 #[cfg(feature = "R4B")]
963 (SofViewDefinition::R4B(vd), SofBundle::R4B(bundle)) => {
964 process_view_definition_generic(vd, bundle)
965 }
966 #[cfg(feature = "R5")]
967 (SofViewDefinition::R5(vd), SofBundle::R5(bundle)) => {
968 process_view_definition_generic(vd, bundle)
969 }
970 #[cfg(feature = "R6")]
971 (SofViewDefinition::R6(vd), SofBundle::R6(bundle)) => {
972 process_view_definition_generic(vd, bundle)
973 }
974 // This case should never happen due to the version check above,
975 // but is needed for exhaustive pattern matching when multiple features are enabled
976 #[cfg(any(
977 all(feature = "R4", any(feature = "R4B", feature = "R5", feature = "R6")),
978 all(feature = "R4B", any(feature = "R5", feature = "R6")),
979 all(feature = "R5", feature = "R6")
980 ))]
981 _ => {
982 unreachable!("Version mismatch should have been caught by the version check above")
983 }
984 }
985}
986
987// Generic version-agnostic constant extraction
988fn extract_view_definition_constants<VD: ViewDefinitionTrait>(
989 view_definition: &VD,
990) -> Result<HashMap<String, EvaluationResult>, SofError> {
991 let mut variables = HashMap::new();
992
993 if let Some(constants) = view_definition.constants() {
994 for constant in constants {
995 let name = constant
996 .name()
997 .ok_or_else(|| {
998 SofError::InvalidViewDefinition("Constant name is required".to_string())
999 })?
1000 .to_string();
1001
1002 let eval_result = constant.to_evaluation_result()?;
1003 variables.insert(name, eval_result);
1004 }
1005 }
1006
1007 Ok(variables)
1008}
1009
1010// Generic version-agnostic ViewDefinition processing
1011fn process_view_definition_generic<VD, B>(
1012 view_definition: VD,
1013 bundle: B,
1014) -> Result<ProcessedResult, SofError>
1015where
1016 VD: ViewDefinitionTrait,
1017 B: BundleTrait,
1018 B::Resource: ResourceTrait,
1019{
1020 validate_view_definition(&view_definition)?;
1021
1022 // Step 1: Extract constants/variables from ViewDefinition
1023 let variables = extract_view_definition_constants(&view_definition)?;
1024
1025 // Step 2: Filter resources by type and profile
1026 let target_resource_type = view_definition
1027 .resource()
1028 .ok_or_else(|| SofError::InvalidViewDefinition("Resource type is required".to_string()))?;
1029
1030 let filtered_resources = filter_resources(&bundle, target_resource_type)?;
1031
1032 // Step 3: Apply where clauses to filter resources
1033 let filtered_resources = apply_where_clauses(
1034 filtered_resources,
1035 view_definition.where_clauses(),
1036 &variables,
1037 )?;
1038
1039 // Step 4: Process all select clauses to generate rows with forEach support
1040 let select_clauses = view_definition.select().ok_or_else(|| {
1041 SofError::InvalidViewDefinition("At least one select clause is required".to_string())
1042 })?;
1043
1044 // Generate rows for each resource using the forEach-aware approach
1045 let (all_columns, rows) =
1046 generate_rows_from_selects(&filtered_resources, select_clauses, &variables)?;
1047
1048 Ok(ProcessedResult {
1049 columns: all_columns,
1050 rows,
1051 })
1052}
1053
1054// Generic version-agnostic validation
1055fn validate_view_definition<VD: ViewDefinitionTrait>(view_def: &VD) -> Result<(), SofError> {
1056 // Basic validation
1057 if view_def.resource().is_none_or(|s| s.is_empty()) {
1058 return Err(SofError::InvalidViewDefinition(
1059 "ViewDefinition must specify a resource type".to_string(),
1060 ));
1061 }
1062
1063 if view_def.select().is_none_or(|s| s.is_empty()) {
1064 return Err(SofError::InvalidViewDefinition(
1065 "ViewDefinition must have at least one select".to_string(),
1066 ));
1067 }
1068
1069 // Validate where clauses
1070 if let Some(where_clauses) = view_def.where_clauses() {
1071 validate_where_clauses(where_clauses)?;
1072 }
1073
1074 // Validate selects
1075 if let Some(selects) = view_def.select() {
1076 for select in selects {
1077 validate_select(select)?;
1078 }
1079 }
1080
1081 Ok(())
1082}
1083
1084// Generic where clause validation
1085fn validate_where_clauses<W: ViewDefinitionWhereTrait>(
1086 where_clauses: &[W],
1087) -> Result<(), SofError> {
1088 // Basic validation - just ensure paths are provided
1089 // Type checking will be done during actual evaluation
1090 for where_clause in where_clauses {
1091 if where_clause.path().is_none() {
1092 return Err(SofError::InvalidViewDefinition(
1093 "Where clause must have a path specified".to_string(),
1094 ));
1095 }
1096 }
1097 Ok(())
1098}
1099
1100// Generic helper - no longer needs to be version-specific
1101fn can_be_coerced_to_boolean(result: &EvaluationResult) -> bool {
1102 // Check if the result can be meaningfully used as a boolean in a where clause
1103 match result {
1104 // Boolean values are obviously OK
1105 EvaluationResult::Boolean(_, _) => true,
1106
1107 // Empty is OK (evaluates to false)
1108 EvaluationResult::Empty => true,
1109
1110 // Collections are OK - they evaluate based on whether they're empty or not
1111 EvaluationResult::Collection { .. } => true,
1112
1113 // Other types cannot be meaningfully coerced to boolean for where clauses
1114 // This includes: String, Integer, Decimal, Date, DateTime, Time, Quantity, Object
1115 _ => false,
1116 }
1117}
1118
1119// Generic select validation
1120fn validate_select<S: ViewDefinitionSelectTrait>(select: &S) -> Result<(), SofError> {
1121 validate_select_with_context(select, false)
1122}
1123
1124fn validate_select_with_context<S: ViewDefinitionSelectTrait>(
1125 select: &S,
1126 in_foreach_context: bool,
1127) -> Result<(), SofError>
1128where
1129 S::Select: ViewDefinitionSelectTrait,
1130{
1131 // Determine if we're entering a forEach context at this level
1132 let entering_foreach = select.for_each().is_some() || select.for_each_or_null().is_some();
1133 let current_foreach_context = in_foreach_context || entering_foreach;
1134
1135 // Validate collection attribute with the current forEach context
1136 if let Some(columns) = select.column() {
1137 for column in columns {
1138 if let Some(collection_value) = column.collection() {
1139 if !collection_value && !current_foreach_context {
1140 return Err(SofError::InvalidViewDefinition(
1141 "Column 'collection' attribute must be true when specified".to_string(),
1142 ));
1143 }
1144 }
1145 }
1146 }
1147
1148 // Validate unionAll column consistency
1149 if let Some(union_selects) = select.union_all() {
1150 validate_union_all_columns(union_selects)?;
1151 }
1152
1153 // Recursively validate nested selects
1154 if let Some(nested_selects) = select.select() {
1155 for nested_select in nested_selects {
1156 validate_select_with_context(nested_select, current_foreach_context)?;
1157 }
1158 }
1159
1160 // Validate unionAll selects with forEach context
1161 if let Some(union_selects) = select.union_all() {
1162 for union_select in union_selects {
1163 validate_select_with_context(union_select, current_foreach_context)?;
1164 }
1165 }
1166
1167 Ok(())
1168}
1169
1170// Generic union validation
1171fn validate_union_all_columns<S: ViewDefinitionSelectTrait>(
1172 union_selects: &[S],
1173) -> Result<(), SofError> {
1174 if union_selects.len() < 2 {
1175 return Ok(());
1176 }
1177
1178 // Get column names and order from first select
1179 let first_select = &union_selects[0];
1180 let first_columns = get_column_names(first_select)?;
1181
1182 // Validate all other selects have the same column names in the same order
1183 for (index, union_select) in union_selects.iter().enumerate().skip(1) {
1184 let current_columns = get_column_names(union_select)?;
1185
1186 if current_columns != first_columns {
1187 if current_columns.len() != first_columns.len()
1188 || !current_columns
1189 .iter()
1190 .all(|name| first_columns.contains(name))
1191 {
1192 return Err(SofError::InvalidViewDefinition(format!(
1193 "UnionAll branch {} has different column names than first branch",
1194 index
1195 )));
1196 } else {
1197 return Err(SofError::InvalidViewDefinition(format!(
1198 "UnionAll branch {} has columns in different order than first branch",
1199 index
1200 )));
1201 }
1202 }
1203 }
1204
1205 Ok(())
1206}
1207
1208// Generic column name extraction
1209fn get_column_names<S: ViewDefinitionSelectTrait>(select: &S) -> Result<Vec<String>, SofError> {
1210 let mut column_names = Vec::new();
1211
1212 // Collect direct column names
1213 if let Some(columns) = select.column() {
1214 for column in columns {
1215 if let Some(name) = column.name() {
1216 column_names.push(name.to_string());
1217 }
1218 }
1219 }
1220
1221 // If this select has unionAll but no direct columns, get columns from first unionAll branch
1222 if column_names.is_empty() {
1223 if let Some(union_selects) = select.union_all() {
1224 if !union_selects.is_empty() {
1225 return get_column_names(&union_selects[0]);
1226 }
1227 }
1228 }
1229
1230 Ok(column_names)
1231}
1232
1233// Generic resource filtering
1234fn filter_resources<'a, B: BundleTrait>(
1235 bundle: &'a B,
1236 resource_type: &str,
1237) -> Result<Vec<&'a B::Resource>, SofError> {
1238 Ok(bundle
1239 .entries()
1240 .into_iter()
1241 .filter(|resource| resource.resource_name() == resource_type)
1242 .collect())
1243}
1244
1245// Generic where clause application
1246fn apply_where_clauses<'a, R, W>(
1247 resources: Vec<&'a R>,
1248 where_clauses: Option<&[W]>,
1249 variables: &HashMap<String, EvaluationResult>,
1250) -> Result<Vec<&'a R>, SofError>
1251where
1252 R: ResourceTrait,
1253 W: ViewDefinitionWhereTrait,
1254{
1255 if let Some(wheres) = where_clauses {
1256 let mut filtered = Vec::new();
1257
1258 for resource in resources {
1259 let mut include_resource = true;
1260
1261 // All where clauses must evaluate to true for the resource to be included
1262 for where_clause in wheres {
1263 let fhir_resource = resource.to_fhir_resource();
1264 let mut context = EvaluationContext::new(vec![fhir_resource]);
1265
1266 // Add variables to the context
1267 for (name, value) in variables {
1268 context.set_variable_result(name, value.clone());
1269 }
1270
1271 let path = where_clause.path().ok_or_else(|| {
1272 SofError::InvalidViewDefinition("Where clause path is required".to_string())
1273 })?;
1274
1275 match evaluate_expression(path, &context) {
1276 Ok(result) => {
1277 // Check if the result can be meaningfully used as a boolean
1278 if !can_be_coerced_to_boolean(&result) {
1279 return Err(SofError::InvalidViewDefinition(format!(
1280 "Where clause path '{}' returns type '{}' which cannot be used as a boolean condition. \
1281 Where clauses must return boolean values, collections, or empty results.",
1282 path,
1283 result.type_name()
1284 )));
1285 }
1286
1287 // Check if result is truthy (non-empty and not false)
1288 if !is_truthy(&result) {
1289 include_resource = false;
1290 break;
1291 }
1292 }
1293 Err(e) => {
1294 return Err(SofError::FhirPathError(format!(
1295 "Error evaluating where clause '{}': {}",
1296 path, e
1297 )));
1298 }
1299 }
1300 }
1301
1302 if include_resource {
1303 filtered.push(resource);
1304 }
1305 }
1306
1307 Ok(filtered)
1308 } else {
1309 Ok(resources)
1310 }
1311}
1312
1313// Removed generate_rows_per_resource_r4 - replaced with new forEach-aware implementation
1314
1315// Removed generate_rows_with_for_each_r4 - replaced with new forEach-aware implementation
1316
1317// Helper functions for FHIRPath result processing
1318fn is_truthy(result: &EvaluationResult) -> bool {
1319 match result {
1320 EvaluationResult::Empty => false,
1321 EvaluationResult::Boolean(b, _) => *b,
1322 EvaluationResult::Collection { items, .. } => !items.is_empty(),
1323 _ => true, // Non-empty, non-false values are truthy
1324 }
1325}
1326
1327fn fhirpath_result_to_json_value_collection(result: EvaluationResult) -> Option<serde_json::Value> {
1328 match result {
1329 EvaluationResult::Empty => Some(serde_json::Value::Array(vec![])),
1330 EvaluationResult::Collection { items, .. } => {
1331 // Always return array for collection columns, even if empty
1332 let values: Vec<serde_json::Value> = items
1333 .into_iter()
1334 .filter_map(fhirpath_result_to_json_value)
1335 .collect();
1336 Some(serde_json::Value::Array(values))
1337 }
1338 // For non-collection results in collection columns, wrap in array
1339 single_result => {
1340 if let Some(json_val) = fhirpath_result_to_json_value(single_result) {
1341 Some(serde_json::Value::Array(vec![json_val]))
1342 } else {
1343 Some(serde_json::Value::Array(vec![]))
1344 }
1345 }
1346 }
1347}
1348
1349fn fhirpath_result_to_json_value(result: EvaluationResult) -> Option<serde_json::Value> {
1350 match result {
1351 EvaluationResult::Empty => None,
1352 EvaluationResult::Boolean(b, _) => Some(serde_json::Value::Bool(b)),
1353 EvaluationResult::Integer(i, _) => {
1354 Some(serde_json::Value::Number(serde_json::Number::from(i)))
1355 }
1356 EvaluationResult::Decimal(d, _) => {
1357 // Check if this Decimal represents a whole number
1358 if d.fract().is_zero() {
1359 // Convert to integer if no fractional part
1360 if let Ok(i) = d.to_string().parse::<i64>() {
1361 Some(serde_json::Value::Number(serde_json::Number::from(i)))
1362 } else {
1363 // Handle very large numbers as strings
1364 Some(serde_json::Value::String(d.to_string()))
1365 }
1366 } else {
1367 // Convert Decimal to a float for fractional numbers
1368 if let Ok(f) = d.to_string().parse::<f64>() {
1369 if let Some(num) = serde_json::Number::from_f64(f) {
1370 Some(serde_json::Value::Number(num))
1371 } else {
1372 Some(serde_json::Value::String(d.to_string()))
1373 }
1374 } else {
1375 Some(serde_json::Value::String(d.to_string()))
1376 }
1377 }
1378 }
1379 EvaluationResult::String(s, _) => Some(serde_json::Value::String(s)),
1380 EvaluationResult::Date(s, _) => Some(serde_json::Value::String(s)),
1381 EvaluationResult::DateTime(s, _) => Some(serde_json::Value::String(s)),
1382 EvaluationResult::Time(s, _) => Some(serde_json::Value::String(s)),
1383 EvaluationResult::Collection { items, .. } => {
1384 if items.len() == 1 {
1385 // Single item collection - unwrap to the item itself
1386 fhirpath_result_to_json_value(items.into_iter().next().unwrap())
1387 } else if items.is_empty() {
1388 None
1389 } else {
1390 // Multiple items - convert to array
1391 let values: Vec<serde_json::Value> = items
1392 .into_iter()
1393 .filter_map(fhirpath_result_to_json_value)
1394 .collect();
1395 Some(serde_json::Value::Array(values))
1396 }
1397 }
1398 EvaluationResult::Object { map, .. } => {
1399 let mut json_map = serde_json::Map::new();
1400 for (k, v) in map {
1401 if let Some(json_val) = fhirpath_result_to_json_value(v) {
1402 json_map.insert(k, json_val);
1403 }
1404 }
1405 Some(serde_json::Value::Object(json_map))
1406 }
1407 // Handle other result types as strings
1408 _ => Some(serde_json::Value::String(format!("{:?}", result))),
1409 }
1410}
1411
1412fn extract_iteration_items(result: EvaluationResult) -> Vec<EvaluationResult> {
1413 match result {
1414 EvaluationResult::Collection { items, .. } => items,
1415 EvaluationResult::Empty => Vec::new(),
1416 single_item => vec![single_item],
1417 }
1418}
1419
1420// Generic row generation functions
1421
1422fn generate_rows_from_selects<R, S>(
1423 resources: &[&R],
1424 selects: &[S],
1425 variables: &HashMap<String, EvaluationResult>,
1426) -> Result<(Vec<String>, Vec<ProcessedRow>), SofError>
1427where
1428 R: ResourceTrait,
1429 S: ViewDefinitionSelectTrait,
1430 S::Select: ViewDefinitionSelectTrait,
1431{
1432 let mut all_columns = Vec::new();
1433 let mut all_rows = Vec::new();
1434
1435 // For each resource, generate all possible row combinations
1436 for resource in resources {
1437 let resource_rows =
1438 generate_rows_for_resource(*resource, selects, &mut all_columns, variables)?;
1439 all_rows.extend(resource_rows);
1440 }
1441
1442 Ok((all_columns, all_rows))
1443}
1444
1445fn generate_rows_for_resource<R, S>(
1446 resource: &R,
1447 selects: &[S],
1448 all_columns: &mut Vec<String>,
1449 variables: &HashMap<String, EvaluationResult>,
1450) -> Result<Vec<ProcessedRow>, SofError>
1451where
1452 R: ResourceTrait,
1453 S: ViewDefinitionSelectTrait,
1454 S::Select: ViewDefinitionSelectTrait,
1455{
1456 let fhir_resource = resource.to_fhir_resource();
1457 let mut context = EvaluationContext::new(vec![fhir_resource]);
1458
1459 // Add variables to the context
1460 for (name, value) in variables {
1461 context.set_variable_result(name, value.clone());
1462 }
1463
1464 // Generate all possible row combinations for this resource
1465 let row_combinations = generate_row_combinations(&context, selects, all_columns, variables)?;
1466
1467 Ok(row_combinations)
1468}
1469
1470#[derive(Debug, Clone)]
1471struct RowCombination {
1472 values: Vec<Option<serde_json::Value>>,
1473}
1474
1475fn generate_row_combinations<S>(
1476 context: &EvaluationContext,
1477 selects: &[S],
1478 all_columns: &mut Vec<String>,
1479 variables: &HashMap<String, EvaluationResult>,
1480) -> Result<Vec<ProcessedRow>, SofError>
1481where
1482 S: ViewDefinitionSelectTrait,
1483 S::Select: ViewDefinitionSelectTrait,
1484{
1485 // First pass: collect all column names to ensure consistent ordering
1486 collect_all_columns(selects, all_columns)?;
1487
1488 // Second pass: generate all row combinations
1489 let mut row_combinations = vec![RowCombination {
1490 values: vec![None; all_columns.len()],
1491 }];
1492
1493 for select in selects {
1494 row_combinations =
1495 expand_select_combinations(context, select, &row_combinations, all_columns, variables)?;
1496 }
1497
1498 // Convert to ProcessedRow format
1499 Ok(row_combinations
1500 .into_iter()
1501 .map(|combo| ProcessedRow {
1502 values: combo.values,
1503 })
1504 .collect())
1505}
1506
1507fn collect_all_columns<S>(selects: &[S], all_columns: &mut Vec<String>) -> Result<(), SofError>
1508where
1509 S: ViewDefinitionSelectTrait,
1510{
1511 for select in selects {
1512 // Add columns from this select
1513 if let Some(columns) = select.column() {
1514 for col in columns {
1515 if let Some(name) = col.name() {
1516 if !all_columns.contains(&name.to_string()) {
1517 all_columns.push(name.to_string());
1518 }
1519 }
1520 }
1521 }
1522
1523 // Recursively collect from nested selects
1524 if let Some(nested_selects) = select.select() {
1525 collect_all_columns(nested_selects, all_columns)?;
1526 }
1527
1528 // Collect from unionAll
1529 if let Some(union_selects) = select.union_all() {
1530 collect_all_columns(union_selects, all_columns)?;
1531 }
1532 }
1533 Ok(())
1534}
1535
1536fn expand_select_combinations<S>(
1537 context: &EvaluationContext,
1538 select: &S,
1539 existing_combinations: &[RowCombination],
1540 all_columns: &[String],
1541 variables: &HashMap<String, EvaluationResult>,
1542) -> Result<Vec<RowCombination>, SofError>
1543where
1544 S: ViewDefinitionSelectTrait,
1545 S::Select: ViewDefinitionSelectTrait,
1546{
1547 // Handle forEach and forEachOrNull
1548 if let Some(for_each_path) = select.for_each() {
1549 return expand_for_each_combinations(
1550 context,
1551 select,
1552 existing_combinations,
1553 all_columns,
1554 for_each_path,
1555 false,
1556 variables,
1557 );
1558 }
1559
1560 if let Some(for_each_or_null_path) = select.for_each_or_null() {
1561 return expand_for_each_combinations(
1562 context,
1563 select,
1564 existing_combinations,
1565 all_columns,
1566 for_each_or_null_path,
1567 true,
1568 variables,
1569 );
1570 }
1571
1572 // Handle regular columns (no forEach)
1573 let mut new_combinations = Vec::new();
1574
1575 for existing_combo in existing_combinations {
1576 let mut new_combo = existing_combo.clone();
1577
1578 // Add values from this select's columns
1579 if let Some(columns) = select.column() {
1580 for col in columns {
1581 if let Some(col_name) = col.name() {
1582 if let Some(col_index) = all_columns.iter().position(|name| name == col_name) {
1583 let path = col.path().ok_or_else(|| {
1584 SofError::InvalidViewDefinition("Column path is required".to_string())
1585 })?;
1586
1587 match evaluate_expression(path, context) {
1588 Ok(result) => {
1589 // Check if this column is marked as a collection
1590 let is_collection = col.collection().unwrap_or(false);
1591
1592 new_combo.values[col_index] = if is_collection {
1593 fhirpath_result_to_json_value_collection(result)
1594 } else {
1595 fhirpath_result_to_json_value(result)
1596 };
1597 }
1598 Err(e) => {
1599 return Err(SofError::FhirPathError(format!(
1600 "Error evaluating column '{}' with path '{}': {}",
1601 col_name, path, e
1602 )));
1603 }
1604 }
1605 }
1606 }
1607 }
1608 }
1609
1610 new_combinations.push(new_combo);
1611 }
1612
1613 // Handle nested selects
1614 if let Some(nested_selects) = select.select() {
1615 for nested_select in nested_selects {
1616 new_combinations = expand_select_combinations(
1617 context,
1618 nested_select,
1619 &new_combinations,
1620 all_columns,
1621 variables,
1622 )?;
1623 }
1624 }
1625
1626 // Handle unionAll
1627 if let Some(union_selects) = select.union_all() {
1628 let mut union_combinations = Vec::new();
1629
1630 // Process each unionAll select independently, using the combinations that already have
1631 // values from this select's columns and nested selects
1632 for union_select in union_selects {
1633 let select_combinations = expand_select_combinations(
1634 context,
1635 union_select,
1636 &new_combinations,
1637 all_columns,
1638 variables,
1639 )?;
1640 union_combinations.extend(select_combinations);
1641 }
1642
1643 // unionAll replaces new_combinations with the union results
1644 // If no union results, this resource should be filtered out (no rows for this resource)
1645 new_combinations = union_combinations;
1646 }
1647
1648 Ok(new_combinations)
1649}
1650
1651fn expand_for_each_combinations<S>(
1652 context: &EvaluationContext,
1653 select: &S,
1654 existing_combinations: &[RowCombination],
1655 all_columns: &[String],
1656 for_each_path: &str,
1657 allow_null: bool,
1658 variables: &HashMap<String, EvaluationResult>,
1659) -> Result<Vec<RowCombination>, SofError>
1660where
1661 S: ViewDefinitionSelectTrait,
1662 S::Select: ViewDefinitionSelectTrait,
1663{
1664 // Evaluate the forEach expression to get iteration items
1665 let for_each_result = evaluate_expression(for_each_path, context).map_err(|e| {
1666 SofError::FhirPathError(format!(
1667 "Error evaluating forEach expression '{}': {}",
1668 for_each_path, e
1669 ))
1670 })?;
1671
1672 let iteration_items = extract_iteration_items(for_each_result);
1673
1674 if iteration_items.is_empty() {
1675 if allow_null {
1676 // forEachOrNull: generate null rows
1677 let mut new_combinations = Vec::new();
1678 for existing_combo in existing_combinations {
1679 let mut new_combo = existing_combo.clone();
1680
1681 // Set column values to null for this forEach scope
1682 if let Some(columns) = select.column() {
1683 for col in columns {
1684 if let Some(col_name) = col.name() {
1685 if let Some(col_index) =
1686 all_columns.iter().position(|name| name == col_name)
1687 {
1688 new_combo.values[col_index] = None;
1689 }
1690 }
1691 }
1692 }
1693
1694 new_combinations.push(new_combo);
1695 }
1696 return Ok(new_combinations);
1697 } else {
1698 // forEach with empty collection: no rows
1699 return Ok(Vec::new());
1700 }
1701 }
1702
1703 let mut new_combinations = Vec::new();
1704
1705 // For each iteration item, create new combinations
1706 for item in &iteration_items {
1707 // Create a new context with the iteration item
1708 let _item_context = create_iteration_context(item, variables);
1709
1710 for existing_combo in existing_combinations {
1711 let mut new_combo = existing_combo.clone();
1712
1713 // Evaluate columns in the context of the iteration item
1714 if let Some(columns) = select.column() {
1715 for col in columns {
1716 if let Some(col_name) = col.name() {
1717 if let Some(col_index) =
1718 all_columns.iter().position(|name| name == col_name)
1719 {
1720 let path = col.path().ok_or_else(|| {
1721 SofError::InvalidViewDefinition(
1722 "Column path is required".to_string(),
1723 )
1724 })?;
1725
1726 // Use the iteration item directly for path evaluation
1727 let result = if path == "$this" {
1728 // Special case: $this refers to the current iteration item
1729 item.clone()
1730 } else {
1731 // Evaluate the path on the iteration item
1732 evaluate_path_on_item(path, item, variables)?
1733 };
1734
1735 // Check if this column is marked as a collection
1736 let is_collection = col.collection().unwrap_or(false);
1737
1738 new_combo.values[col_index] = if is_collection {
1739 fhirpath_result_to_json_value_collection(result)
1740 } else {
1741 fhirpath_result_to_json_value(result)
1742 };
1743 }
1744 }
1745 }
1746 }
1747
1748 new_combinations.push(new_combo);
1749 }
1750 }
1751
1752 // Handle nested selects with the forEach context
1753 if let Some(nested_selects) = select.select() {
1754 let mut final_combinations = Vec::new();
1755
1756 for item in &iteration_items {
1757 let item_context = create_iteration_context(item, variables);
1758
1759 // For each iteration item, we need to start with the combinations that have
1760 // the correct column values for this forEach scope
1761 for existing_combo in existing_combinations {
1762 // Find the combination that corresponds to this iteration item
1763 // by looking at the values we set for columns in this forEach scope
1764 let mut base_combo = existing_combo.clone();
1765
1766 // Update the base combination with column values for this iteration item
1767 if let Some(columns) = select.column() {
1768 for col in columns {
1769 if let Some(col_name) = col.name() {
1770 if let Some(col_index) =
1771 all_columns.iter().position(|name| name == col_name)
1772 {
1773 let path = col.path().ok_or_else(|| {
1774 SofError::InvalidViewDefinition(
1775 "Column path is required".to_string(),
1776 )
1777 })?;
1778
1779 let result = if path == "$this" {
1780 item.clone()
1781 } else {
1782 evaluate_path_on_item(path, item, variables)?
1783 };
1784
1785 // Check if this column is marked as a collection
1786 let is_collection = col.collection().unwrap_or(false);
1787
1788 base_combo.values[col_index] = if is_collection {
1789 fhirpath_result_to_json_value_collection(result)
1790 } else {
1791 fhirpath_result_to_json_value(result)
1792 };
1793 }
1794 }
1795 }
1796 }
1797
1798 // Start with this base combination for nested processing
1799 let mut item_combinations = vec![base_combo];
1800
1801 // Process nested selects
1802 for nested_select in nested_selects {
1803 item_combinations = expand_select_combinations(
1804 &item_context,
1805 nested_select,
1806 &item_combinations,
1807 all_columns,
1808 variables,
1809 )?;
1810 }
1811
1812 final_combinations.extend(item_combinations);
1813 }
1814 }
1815
1816 new_combinations = final_combinations;
1817 }
1818
1819 // Handle unionAll within forEach context
1820 if let Some(union_selects) = select.union_all() {
1821 let mut union_combinations = Vec::new();
1822
1823 for item in &iteration_items {
1824 let item_context = create_iteration_context(item, variables);
1825
1826 // For each iteration item, process all unionAll selects
1827 for existing_combo in existing_combinations {
1828 let mut base_combo = existing_combo.clone();
1829
1830 // Update the base combination with column values for this iteration item
1831 if let Some(columns) = select.column() {
1832 for col in columns {
1833 if let Some(col_name) = col.name() {
1834 if let Some(col_index) =
1835 all_columns.iter().position(|name| name == col_name)
1836 {
1837 let path = col.path().ok_or_else(|| {
1838 SofError::InvalidViewDefinition(
1839 "Column path is required".to_string(),
1840 )
1841 })?;
1842
1843 let result = if path == "$this" {
1844 item.clone()
1845 } else {
1846 evaluate_path_on_item(path, item, variables)?
1847 };
1848
1849 // Check if this column is marked as a collection
1850 let is_collection = col.collection().unwrap_or(false);
1851
1852 base_combo.values[col_index] = if is_collection {
1853 fhirpath_result_to_json_value_collection(result)
1854 } else {
1855 fhirpath_result_to_json_value(result)
1856 };
1857 }
1858 }
1859 }
1860 }
1861
1862 // Also evaluate columns from nested selects and add them to base_combo
1863 if let Some(nested_selects) = select.select() {
1864 for nested_select in nested_selects {
1865 if let Some(nested_columns) = nested_select.column() {
1866 for col in nested_columns {
1867 if let Some(col_name) = col.name() {
1868 if let Some(col_index) =
1869 all_columns.iter().position(|name| name == col_name)
1870 {
1871 let path = col.path().ok_or_else(|| {
1872 SofError::InvalidViewDefinition(
1873 "Column path is required".to_string(),
1874 )
1875 })?;
1876
1877 let result = if path == "$this" {
1878 item.clone()
1879 } else {
1880 evaluate_path_on_item(path, item, variables)?
1881 };
1882
1883 // Check if this column is marked as a collection
1884 let is_collection = col.collection().unwrap_or(false);
1885
1886 base_combo.values[col_index] = if is_collection {
1887 fhirpath_result_to_json_value_collection(result)
1888 } else {
1889 fhirpath_result_to_json_value(result)
1890 };
1891 }
1892 }
1893 }
1894 }
1895 }
1896 }
1897
1898 // Process each unionAll select independently for this iteration item
1899 for union_select in union_selects {
1900 let mut select_combinations = vec![base_combo.clone()];
1901 select_combinations = expand_select_combinations(
1902 &item_context,
1903 union_select,
1904 &select_combinations,
1905 all_columns,
1906 variables,
1907 )?;
1908 union_combinations.extend(select_combinations);
1909 }
1910 }
1911 }
1912
1913 // unionAll replaces new_combinations with the union results
1914 // If no union results, filter out this resource (no rows for this resource)
1915 new_combinations = union_combinations;
1916 }
1917
1918 Ok(new_combinations)
1919}
1920
1921// Generic helper functions
1922fn evaluate_path_on_item(
1923 path: &str,
1924 item: &EvaluationResult,
1925 variables: &HashMap<String, EvaluationResult>,
1926) -> Result<EvaluationResult, SofError> {
1927 // Create a temporary context with the iteration item as the root resource
1928 let mut temp_context = match item {
1929 EvaluationResult::Object { .. } => {
1930 // Convert the iteration item to a resource-like structure for FHIRPath evaluation
1931 // For simplicity, we'll create a basic context where the item is available for evaluation
1932 let mut context = EvaluationContext::new(vec![]);
1933 context.this = Some(item.clone());
1934 context
1935 }
1936 _ => EvaluationContext::new(vec![]),
1937 };
1938
1939 // Add variables to the temporary context
1940 for (name, value) in variables {
1941 temp_context.set_variable_result(name, value.clone());
1942 }
1943
1944 // Evaluate the FHIRPath expression in the context of the iteration item
1945 match evaluate_expression(path, &temp_context) {
1946 Ok(result) => Ok(result),
1947 Err(_e) => {
1948 // If FHIRPath evaluation fails, try simple property access as fallback
1949 match item {
1950 EvaluationResult::Object { map, .. } => {
1951 if let Some(value) = map.get(path) {
1952 Ok(value.clone())
1953 } else {
1954 Ok(EvaluationResult::Empty)
1955 }
1956 }
1957 _ => Ok(EvaluationResult::Empty),
1958 }
1959 }
1960 }
1961}
1962
1963fn create_iteration_context(
1964 item: &EvaluationResult,
1965 variables: &HashMap<String, EvaluationResult>,
1966) -> EvaluationContext {
1967 // Create a new context with the iteration item as the root
1968 let mut context = EvaluationContext::new(vec![]);
1969 context.this = Some(item.clone());
1970
1971 // Preserve variables from the parent context
1972 for (name, value) in variables {
1973 context.set_variable_result(name, value.clone());
1974 }
1975
1976 context
1977}
1978
1979/// Filter a bundle's resources by their lastUpdated metadata
1980fn filter_bundle_by_since(bundle: SofBundle, since: DateTime<Utc>) -> Result<SofBundle, SofError> {
1981 match bundle {
1982 #[cfg(feature = "R4")]
1983 SofBundle::R4(mut b) => {
1984 if let Some(entries) = b.entry.as_mut() {
1985 entries.retain(|entry| {
1986 entry
1987 .resource
1988 .as_ref()
1989 .and_then(|r| r.get_last_updated())
1990 .map(|last_updated| last_updated > since)
1991 .unwrap_or(false)
1992 });
1993 }
1994 Ok(SofBundle::R4(b))
1995 }
1996 #[cfg(feature = "R4B")]
1997 SofBundle::R4B(mut b) => {
1998 if let Some(entries) = b.entry.as_mut() {
1999 entries.retain(|entry| {
2000 entry
2001 .resource
2002 .as_ref()
2003 .and_then(|r| r.get_last_updated())
2004 .map(|last_updated| last_updated > since)
2005 .unwrap_or(false)
2006 });
2007 }
2008 Ok(SofBundle::R4B(b))
2009 }
2010 #[cfg(feature = "R5")]
2011 SofBundle::R5(mut b) => {
2012 if let Some(entries) = b.entry.as_mut() {
2013 entries.retain(|entry| {
2014 entry
2015 .resource
2016 .as_ref()
2017 .and_then(|r| r.get_last_updated())
2018 .map(|last_updated| last_updated > since)
2019 .unwrap_or(false)
2020 });
2021 }
2022 Ok(SofBundle::R5(b))
2023 }
2024 #[cfg(feature = "R6")]
2025 SofBundle::R6(mut b) => {
2026 if let Some(entries) = b.entry.as_mut() {
2027 entries.retain(|entry| {
2028 entry
2029 .resource
2030 .as_ref()
2031 .and_then(|r| r.get_last_updated())
2032 .map(|last_updated| last_updated > since)
2033 .unwrap_or(false)
2034 });
2035 }
2036 Ok(SofBundle::R6(b))
2037 }
2038 }
2039}
2040
2041/// Apply pagination to processed results
2042fn apply_pagination_to_result(
2043 mut result: ProcessedResult,
2044 limit: Option<usize>,
2045 page: Option<usize>,
2046) -> Result<ProcessedResult, SofError> {
2047 if let Some(limit) = limit {
2048 let page_num = page.unwrap_or(1);
2049 if page_num == 0 {
2050 return Err(SofError::InvalidViewDefinition(
2051 "Page number must be greater than 0".to_string(),
2052 ));
2053 }
2054
2055 let start_index = (page_num - 1) * limit;
2056 if start_index >= result.rows.len() {
2057 // Return empty result if page is beyond data
2058 result.rows.clear();
2059 } else {
2060 let end_index = std::cmp::min(start_index + limit, result.rows.len());
2061 result.rows = result.rows[start_index..end_index].to_vec();
2062 }
2063 }
2064
2065 Ok(result)
2066}
2067
2068fn format_output(result: ProcessedResult, content_type: ContentType) -> Result<Vec<u8>, SofError> {
2069 match content_type {
2070 ContentType::Csv | ContentType::CsvWithHeader => {
2071 format_csv(result, content_type == ContentType::CsvWithHeader)
2072 }
2073 ContentType::Json => format_json(result),
2074 ContentType::NdJson => format_ndjson(result),
2075 ContentType::Parquet => Err(SofError::UnsupportedContentType(
2076 "Parquet not yet implemented".to_string(),
2077 )),
2078 }
2079}
2080
2081fn format_csv(result: ProcessedResult, include_header: bool) -> Result<Vec<u8>, SofError> {
2082 let mut wtr = csv::Writer::from_writer(vec![]);
2083
2084 if include_header {
2085 wtr.write_record(&result.columns)?;
2086 }
2087
2088 for row in result.rows {
2089 let record: Vec<String> = row
2090 .values
2091 .iter()
2092 .map(|v| match v {
2093 Some(val) => {
2094 // For string values, extract the raw string instead of JSON serializing
2095 if let serde_json::Value::String(s) = val {
2096 s.clone()
2097 } else {
2098 // For non-string values, use JSON serialization
2099 serde_json::to_string(val).unwrap_or_default()
2100 }
2101 }
2102 None => String::new(),
2103 })
2104 .collect();
2105 wtr.write_record(&record)?;
2106 }
2107
2108 wtr.into_inner()
2109 .map_err(|e| SofError::CsvWriterError(e.to_string()))
2110}
2111
2112fn format_json(result: ProcessedResult) -> Result<Vec<u8>, SofError> {
2113 let mut output = Vec::new();
2114
2115 for row in result.rows {
2116 let mut row_obj = serde_json::Map::new();
2117 for (i, column) in result.columns.iter().enumerate() {
2118 let value = row
2119 .values
2120 .get(i)
2121 .and_then(|v| v.as_ref())
2122 .cloned()
2123 .unwrap_or(serde_json::Value::Null);
2124 row_obj.insert(column.clone(), value);
2125 }
2126 output.push(serde_json::Value::Object(row_obj));
2127 }
2128
2129 Ok(serde_json::to_vec_pretty(&output)?)
2130}
2131
2132fn format_ndjson(result: ProcessedResult) -> Result<Vec<u8>, SofError> {
2133 let mut output = Vec::new();
2134
2135 for row in result.rows {
2136 let mut row_obj = serde_json::Map::new();
2137 for (i, column) in result.columns.iter().enumerate() {
2138 let value = row
2139 .values
2140 .get(i)
2141 .and_then(|v| v.as_ref())
2142 .cloned()
2143 .unwrap_or(serde_json::Value::Null);
2144 row_obj.insert(column.clone(), value);
2145 }
2146 let line = serde_json::to_string(&serde_json::Value::Object(row_obj))?;
2147 output.extend_from_slice(line.as_bytes());
2148 output.push(b'\n');
2149 }
2150
2151 Ok(output)
2152}