ormer-derive 0.2.3

A minimalist ORM framework that supports SQLite, PostgreSQL, MySQL, and SqlServer
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Expr, LitStr};

enum Segment {
    Text(String),
    Expr(Expr),
}

pub fn expand(input: TokenStream) -> TokenStream {
    let lit = match syn::parse2::<LitStr>(input) {
        Ok(lit) => lit,
        Err(err) => return err.to_compile_error(),
    };

    let segments = match parse_segments(&lit.value()) {
        Ok(segments) => segments,
        Err(message) => return syn::Error::new(lit.span(), message).to_compile_error(),
    };

    let segments = segments.into_iter().map(|segment| match segment {
        Segment::Text(text) => quote! {
            ::ormer::RawExprSegment::text(#text)
        },
        Segment::Expr(expr) => quote! {
            ::ormer::RawExprSegment::expr(&(#expr))
        },
    });

    quote! {
        ::ormer::RawExpr::<()>::new(::ormer::RawSqlExpr::new(vec![#(#segments),*]))
    }
}

fn parse_segments(input: &str) -> Result<Vec<Segment>, String> {
    let mut segments = Vec::new();
    let mut text = String::new();
    let mut index = 0;

    while index < input.len() {
        let rest = &input[index..];
        if rest.starts_with("{{") {
            text.push('{');
            index += 2;
        } else if rest.starts_with("}}") {
            text.push('}');
            index += 2;
        } else if rest.starts_with('{') {
            push_text(&mut segments, &mut text);
            let (expr_src, next) = read_braced_expr(input, index + 1)?;
            let expr_src = expr_src.trim();
            if expr_src.is_empty() {
                return Err("raw expression placeholder cannot be empty".to_string());
            }
            let expr = syn::parse_str::<Expr>(expr_src)
                .map_err(|err| format!("invalid raw expression `{expr_src}`: {err}"))?;
            segments.push(Segment::Expr(expr));
            index = next;
        } else if rest.starts_with('}') {
            return Err(
                "unmatched `}` in raw expression; use `}}` for a literal brace".to_string(),
            );
        } else {
            let ch = rest.chars().next().expect("non-empty string slice");
            text.push(ch);
            index += ch.len_utf8();
        }
    }

    push_text(&mut segments, &mut text);
    Ok(segments)
}

fn push_text(segments: &mut Vec<Segment>, text: &mut String) {
    if !text.is_empty() {
        segments.push(Segment::Text(std::mem::take(text)));
    }
}

fn read_braced_expr(input: &str, mut index: usize) -> Result<(&str, usize), String> {
    let start = index;
    let mut paren_depth = 0usize;
    let mut bracket_depth = 0usize;
    let mut brace_depth = 0usize;

    while index < input.len() {
        if let Some(next) = skip_raw_string(input, index) {
            index = next;
            continue;
        }

        let rest = &input[index..];
        let ch = rest.chars().next().expect("non-empty string slice");
        match ch {
            '"' | '\'' => {
                index = skip_quoted(input, index, ch);
            }
            '(' => {
                paren_depth += 1;
                index += 1;
            }
            ')' => {
                paren_depth = paren_depth.saturating_sub(1);
                index += 1;
            }
            '[' => {
                bracket_depth += 1;
                index += 1;
            }
            ']' => {
                bracket_depth = bracket_depth.saturating_sub(1);
                index += 1;
            }
            '{' => {
                brace_depth += 1;
                index += 1;
            }
            '}' => {
                if paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 {
                    return Ok((&input[start..index], index + 1));
                }
                brace_depth = brace_depth.saturating_sub(1);
                index += 1;
            }
            _ => {
                index += ch.len_utf8();
            }
        }
    }

    Err("unclosed `{` in raw expression".to_string())
}

fn skip_quoted(input: &str, mut index: usize, quote: char) -> usize {
    index += quote.len_utf8();
    while index < input.len() {
        let rest = &input[index..];
        let ch = rest.chars().next().expect("non-empty string slice");
        index += ch.len_utf8();
        if ch == '\\' {
            if let Some(next) = input[index..].chars().next() {
                index += next.len_utf8();
            }
        } else if ch == quote {
            break;
        }
    }
    index
}

fn skip_raw_string(input: &str, index: usize) -> Option<usize> {
    let bytes = input.as_bytes();
    if bytes.get(index) != Some(&b'r') {
        return None;
    }

    let mut cursor = index + 1;
    while bytes.get(cursor) == Some(&b'#') {
        cursor += 1;
    }
    if bytes.get(cursor) != Some(&b'"') {
        return None;
    }

    let hashes = cursor - index - 1;
    cursor += 1;
    while cursor < input.len() {
        if bytes.get(cursor) == Some(&b'"') {
            let mut hash_cursor = cursor + 1;
            let mut matched = 0usize;
            while matched < hashes && bytes.get(hash_cursor) == Some(&b'#') {
                matched += 1;
                hash_cursor += 1;
            }
            if matched == hashes {
                return Some(hash_cursor);
            }
        }
        cursor += 1;
    }

    Some(input.len())
}