scythe-codegen 0.12.0

Polyglot code generation backends for scythe
Documentation
use scythe_backend::manifest::BackendManifest;
use scythe_backend::naming::to_snake_case;
use scythe_backend::types::resolve_type_pair;

use scythe_core::analyzer::{AnalyzedColumn, AnalyzedParam};
use scythe_core::errors::{ErrorCode, ScytheError};

use crate::backend_trait::{ResolvedColumn, ResolvedParam};
use crate::overrides::{TypeOverride, find_override};

/// Resolve analyzed columns into resolved columns using a backend manifest.
///
/// When `overrides` is non-empty, each column is checked against the override
/// list before the normal type-resolution path. The first matching override
/// replaces the column's neutral type.
pub fn resolve_columns(
    columns: &[AnalyzedColumn],
    manifest: &BackendManifest,
    overrides: &[TypeOverride],
    source_table: &str,
) -> Result<Vec<ResolvedColumn>, ScytheError> {
    columns
        .iter()
        .map(|col| {
            let column_match = if source_table.is_empty() {
                String::new()
            } else {
                format!("{}.{}", source_table, col.name)
            };
            let effective_neutral_type =
                find_override(overrides, &column_match, &col.neutral_type).unwrap_or(&col.neutral_type);

            let (full_type, lang_type) = resolve_type_pair(effective_neutral_type, manifest, col.nullable)
                .map(|(f, l)| (f.into_owned(), l.into_owned()))
                .map_err(|e| {
                    ScytheError::new(
                        ErrorCode::InternalError,
                        format!("type resolution failed for column '{}': {}", col.name, e),
                    )
                })?;
            Ok(ResolvedColumn {
                name: col.name.clone(),
                field_name: to_snake_case(&col.name).into_owned(),
                lang_type,
                full_type,
                neutral_type: effective_neutral_type.to_string(),
                nullable: col.nullable,
                join_group: col.join_group.clone(),
                nullable_before_join: col.nullable_before_join,
                sql_type: col.sql_type.clone(),
            })
        })
        .collect()
}

/// Resolve analyzed params into resolved params using a backend manifest.
///
/// When `overrides` is non-empty, each param is checked against the override
/// list before the normal type-resolution path.
pub fn resolve_params(
    params: &[AnalyzedParam],
    manifest: &BackendManifest,
    overrides: &[TypeOverride],
    source_table: &str,
) -> Result<Vec<ResolvedParam>, ScytheError> {
    params
        .iter()
        .map(|param| {
            let column_match = if source_table.is_empty() {
                String::new()
            } else {
                format!("{}.{}", source_table, param.name)
            };
            let effective_neutral_type =
                find_override(overrides, &column_match, &param.neutral_type).unwrap_or(&param.neutral_type);

            let (full_type, lang_type) = resolve_type_pair(effective_neutral_type, manifest, param.nullable)
                .map(|(f, l)| (f.into_owned(), l.into_owned()))
                .map_err(|e| {
                    ScytheError::new(
                        ErrorCode::InternalError,
                        format!("type resolution failed for param '{}': {}", param.name, e),
                    )
                })?;
            let borrowed_type = param_type_to_borrowed(&full_type);
            Ok(ResolvedParam {
                name: param.name.clone(),
                field_name: to_snake_case(&param.name).into_owned(),
                lang_type,
                full_type,
                borrowed_type,
                neutral_type: effective_neutral_type.to_string(),
                nullable: param.nullable,
            })
        })
        .collect()
}

/// Convert a resolved Rust type to its borrowed form for function parameters.
/// Copy types (primitives) stay as-is; String becomes &str; other non-Copy types get a & prefix.
pub fn param_type_to_borrowed(rust_type: &str) -> String {
    let copy_types = ["bool", "i16", "i32", "i64", "f32", "f64", "u64"];
    if copy_types.contains(&rust_type) {
        return rust_type.to_string();
    }
    if rust_type == "String" {
        return "&str".to_string();
    }
    if let Some(inner) = rust_type.strip_prefix("Option<").and_then(|s| s.strip_suffix('>')) {
        let borrowed_inner = param_type_to_borrowed(inner);
        return format!("Option<{}>", borrowed_inner);
    }
    if let Some(inner) = rust_type.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
        return format!("&[{}]", inner);
    }
    format!("&{}", rust_type)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_param_type_to_borrowed_string() {
        assert_eq!(param_type_to_borrowed("String"), "&str");
    }

    #[test]
    fn test_param_type_to_borrowed_vec() {
        assert_eq!(param_type_to_borrowed("Vec<i32>"), "&[i32]");
        assert_eq!(param_type_to_borrowed("Vec<String>"), "&[String]");
    }

    #[test]
    fn test_param_type_to_borrowed_passthrough() {
        assert_eq!(param_type_to_borrowed("i32"), "i32");
        assert_eq!(param_type_to_borrowed("i64"), "i64");
        assert_eq!(param_type_to_borrowed("bool"), "bool");
        assert_eq!(param_type_to_borrowed("f64"), "f64");
    }

    #[test]
    fn test_param_type_to_borrowed_option_string() {
        assert_eq!(param_type_to_borrowed("Option<String>"), "Option<&str>");
    }

    #[test]
    fn test_param_type_to_borrowed_option_copy() {
        assert_eq!(param_type_to_borrowed("Option<i32>"), "Option<i32>");
    }

    #[test]
    fn test_param_type_to_borrowed_other() {
        assert_eq!(param_type_to_borrowed("Uuid"), "&Uuid");
        assert_eq!(param_type_to_borrowed("NaiveDateTime"), "&NaiveDateTime");
    }
}