csv-codegen 0.2.3

A Rust procedural macro that transforms CSV data into safe, zero-cost code. Generate match arms, loops, and nested queries directly from CSV files, ensuring type safety and deterministic code generation.
Documentation
use crate::expression::SelectedValue;
use crate::predicate::{FilterExpression, FilterExpressionInner, PredicateComparison};
use crate::{FieldIndex, QueryError, QueryErrorInner};
use csv::StringRecord;
use syn::{Ident, LitStr};

/// A wrapper around StringRecord that provides convenient field access and evaluation methods
pub struct RecordOps<'a> {
    record: &'a StringRecord,
}

impl<'a> RecordOps<'a> {
    pub fn new(record: &'a StringRecord) -> Self {
        Self { record }
    }

    /// Extract a field value by index, returning an error if the field doesn't exist
    pub fn get_field(&self, field_index: &FieldIndex) -> Result<&str, QueryError> {
        match field_index {
            FieldIndex::Index(idx) => self.record.get(*idx).ok_or_else(|| {
                Box::new(QueryErrorInner::InvalidSource {
                    file: LitStr::new("unknown", proc_macro2::Span::call_site()),
                    message: format!(
                        "Not enough values on line {:?}",
                        self.record.position().map(|p| p.line())
                    ),
                })
            }),
            FieldIndex::PivotKey => {
                panic!("PivotKey field access should be handled in pivot context")
            }
            FieldIndex::PivotValue => {
                panic!("PivotValue field access should be handled in pivot context")
            }
        }
    }

    /// Extract a field value by index and convert it to SelectedValue
    pub fn get_selected_value(
        &self,
        field_index: &FieldIndex,
    ) -> Result<SelectedValue, QueryError> {
        self.get_field(field_index)
            .map(|value| SelectedValue(value.to_string()))
    }

    /// Evaluate a predicate against this record
    pub fn matches_predicate(
        &self,
        predicate: &FilterExpression,
        field_resolver: &impl FieldResolver,
    ) -> bool {
        self.matches_predicate_inner(&predicate.expr, field_resolver)
    }

    fn matches_predicate_inner(
        &self,
        predicate: &FilterExpressionInner,
        field_resolver: &impl FieldResolver,
    ) -> bool {
        match predicate {
            FilterExpressionInner::Comparison(comparison) => {
                self.matches_comparison(comparison, field_resolver)
            }
            FilterExpressionInner::All(all) => all
                .iter()
                .all(|pred| self.matches_predicate_inner(pred, field_resolver)),
            FilterExpressionInner::Any(any) => any
                .iter()
                .any(|pred| self.matches_predicate_inner(pred, field_resolver)),
        }
    }

    fn matches_comparison(
        &self,
        comparison: &PredicateComparison,
        field_resolver: &impl FieldResolver,
    ) -> bool {
        let field_index = match field_resolver.get_field_index(&comparison.field) {
            Ok(idx) => idx,
            Err(_) => return false,
        };

        match field_index {
            FieldIndex::Index(idx) => {
                if let Some(record_value) = self.record.get(idx) {
                    let record_value =
                        syn::parse_str::<syn::Lit>(record_value).unwrap_or_else(|_| {
                            LitStr::new(record_value, comparison.field.span()).into()
                        });
                    comparison.evaluate(record_value)
                } else {
                    false
                }
            }
            FieldIndex::PivotKey => {
                // Pivot key filtering is handled separately in pivot context
                true
            }
            FieldIndex::PivotValue => {
                // Filtering on pivot values not yet supported
                false
            }
        }
    }
}

/// Trait for resolving field names to field indices
pub trait FieldResolver {
    fn get_field_index(&self, field_ident: &Ident) -> Result<FieldIndex, QueryError>;
}