datasources 0.1.5

Async datasource interfaces, middleware, registry, and test doubles
Documentation
use super::default_value_options::DefaultValueOptions;
use crate::datasource_error::DatasourceError;
use crate::result::Result;
use regex::Regex;
use std::collections::HashMap;

pub const EMPTY_UUID: &str = "00000000-0000-0000-0000-000000000000";

struct DefaultEntry {
    spec: Regex,
    reverse: Regex,
    native: Box<dyn Fn(&str) -> String + Send + Sync>,
    spec_text: Option<fn(&str) -> String>,
}

pub struct DefaultValueConverter {
    dictionary: HashMap<String, HashMap<String, DefaultEntry>>,
}

impl DefaultValueConverter {
    pub fn new(options: DefaultValueOptions) -> Self {
        Self { dictionary: dictionary_of(options) }
    }

    pub fn sql_string_literal(value: impl AsRef<str>) -> String {
        sql_string_literal(value.as_ref())
    }

    pub fn convert(&self, spec_type: &str, text: &str) -> Result<Option<String>> {
        let (token, arg) = parse(spec_type, Some(text));
        if token == "None" {
            return Ok(None);
        }
        let entries = self.dictionary.get(spec_type).ok_or_else(|| {
            DatasourceError::message(format!("Unknown spec field type: {spec_type}"))
        })?;
        let Some(entry) = entries.get(&token) else {
            return Ok(None);
        };
        if !entry.spec.is_match(text) {
            return Err(DatasourceError::message(format!(
                "default_value '{text}' is not a valid {spec_type} {token} default"
            )));
        }
        let expr = (entry.native)(arg.as_deref().unwrap_or(""));
        Ok(if expr.is_empty() { None } else { Some(expr) })
    }

    pub fn reverse_convert(&self, spec_type: &str, native: &str) -> Result<String> {
        let entries = self.dictionary.get(spec_type).ok_or_else(|| {
            DatasourceError::message(format!("Unknown spec field type: {spec_type}"))
        })?;
        for (name, entry) in entries {
            if let Some(caps) = entry.reverse.captures(native) {
                let arg = caps.get(1).map(|m| m.as_str()).unwrap_or("");
                return Ok(entry.spec_text.map(|f| f(arg)).unwrap_or_else(|| name.clone()));
            }
        }
        Err(DatasourceError::message(format!(
            "Native default '{native}' is not a valid {spec_type} default"
        )))
    }
}

fn parse(spec_type: &str, value: Option<&str>) -> (String, Option<String>) {
    let Some(v) = value else {
        return ("None".into(), None);
    };
    if let Some(caps) = Regex::new(r"^([A-Za-z]+)\('(.*)'\)$")
        .expect("call")
        .captures(v)
    {
        let name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
        let token = if name == "uuid" { "Uuid" } else { name };
        return (token.into(), caps.get(2).map(|m| m.as_str().to_string()));
    }
    if spec_type == "boolean" {
        return ("Boolean".into(), Some(if v == "true" { "true" } else { "false" }.into()));
    }
    if matches!(
        spec_type,
        "number"
            | "integer"
            | "unsignedinteger"
            | "smallinteger"
            | "unsignedsmallinteger"
            | "biginteger"
            | "unsignedbiginteger"
            | "float"
            | "decimal"
            | "reference"
    ) {
        return ("Numeric".into(), Some(v.into()));
    }
    if spec_type == "datetime" {
        return if matches!(v, "Now" | "UtcNow") {
            (v.into(), None)
        } else {
            ("DateTime".into(), Some(v.into()))
        };
    }
    if spec_type == "uuid" {
        return if matches!(v, "NewId" | "Empty") {
            (v.into(), None)
        } else {
            ("Uuid".into(), Some(v.into()))
        };
    }
    if spec_type == "binary" {
        return ("Hex".into(), Some(v.into()));
    }
    ("String".into(), Some(v.into()))
}

fn sql_string_literal(value: &str) -> String {
    format!("'{}'", value.replace('\'', "''"))
}

fn exact(text: &str) -> Regex {
    Regex::new(&format!("^{}$", regex::escape(text))).expect("exact")
}

fn dictionary_of(opts: DefaultValueOptions) -> HashMap<String, HashMap<String, DefaultEntry>> {
    let new_id = (opts.new_id)();
    let now = (opts.now)();
    let utc_now = (opts.utc_now)();
    let quoted = Regex::new(r"^'(.*)'$").expect("quoted");
    let mut dictionary = HashMap::new();
    let quoted_string = HashMap::from([
        entry("String", r"^.*$", quoted.clone(), |arg| sql_string_literal(arg), Some(|arg| arg.to_string())),
        entry("Numeric", r"^.*$", quoted.clone(), |arg| sql_string_literal(arg), Some(|arg| arg.to_string())),
    ]);
    dictionary.insert("string".into(), quoted_string);
    dictionary.insert(
        "boolean".into(),
        HashMap::from([entry(
            "Boolean",
            r"^(true|false)$",
            Regex::new(&format!(
                "^({}|{})$",
                regex::escape(&opts.boolean_on),
                regex::escape(&opts.boolean_off)
            ))
            .expect("bool"),
            {
                let on = opts.boolean_on.clone();
                let off = opts.boolean_off.clone();
                move |arg| if arg == "true" { on.clone() } else { off.clone() }
            },
            None,
        )]),
    );
    dictionary.insert(
        "datetime".into(),
        HashMap::from([
            entry("Now", r"^Now$", exact(&now), {
                let now = now.clone();
                move |_| now.clone()
            }, None),
            entry("UtcNow", r"^UtcNow$", exact(&utc_now), {
                let utc_now = utc_now.clone();
                move |_| utc_now.clone()
            }, None),
        ]),
    );
    dictionary.insert(
        "uuid".into(),
        HashMap::from([entry("NewId", r"^NewId$", exact(&new_id), {
            let new_id = new_id.clone();
            move |_| new_id.clone()
        }, None)]),
    );
    dictionary
}

fn entry(
    name: &str,
    spec: &str,
    reverse: Regex,
    native: impl Fn(&str) -> String + Send + Sync + 'static,
    spec_text: Option<fn(&str) -> String>,
) -> (String, DefaultEntry) {
    (
        name.into(),
        DefaultEntry {
            spec: Regex::new(spec).expect("spec"),
            reverse,
            native: Box::new(native),
            spec_text,
        },
    )
}

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

    #[test]
    fn converts_utc_now() {
        let converter = DefaultValueConverter::new(DefaultValueOptions {
            new_id: || "gen_random_uuid()".into(),
            now: || "LOCALTIMESTAMP".into(),
            utc_now: || "(NOW() AT TIME ZONE 'UTC')".into(),
            boolean_on: "TRUE".into(),
            boolean_off: "FALSE".into(),
            hex: |arg| format!("'\\x{arg}'"),
            hex_reverse: Regex::new(r"^'\\x(.*)'$").expect("hex"),
        });
        assert_eq!(
            converter.convert("datetime", "UtcNow").expect("ok"),
            Some("(NOW() AT TIME ZONE 'UTC')".into())
        );
    }
}