#![allow(
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::doc_markdown,
clippy::too_long_first_doc_paragraph,
clippy::module_name_repetitions
)]
use std::collections::BTreeMap;
use scythe_core::analyzer::AnalyzedQuery;
use scythe_core::parser::QueryCommand;
use serde::{Deserialize, Serialize};
use super::annotations::HttpParamBinding;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Sidecar {
pub by_language: BTreeMap<String, BTreeMap<String, SidecarEntry>>,
}
impl Sidecar {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, language: &str, operation_id: &str, entry: SidecarEntry) {
self.by_language
.entry(language.to_string())
.or_default()
.insert(operation_id.to_string(), entry);
}
pub fn entry_for<'a>(&'a self, language: &str, operation_id: &str) -> Option<&'a SidecarEntry> {
self.by_language.get(language).and_then(|m| m.get(operation_id))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarEntry {
pub scythe_fn: String,
pub scythe_module: String,
pub params: Vec<SidecarParam>,
pub return_lang_type: String,
pub is_async: bool,
pub command: QueryCommand,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarParam {
pub name: String,
pub lang_type: String,
pub source: HttpParamBinding,
}
pub fn build_sidecar_entry<F>(
query: &AnalyzedQuery,
bindings: &BTreeMap<String, HttpParamBinding>,
scythe_module: &str,
scythe_fn: &str,
is_async: bool,
lang_type_for: F,
) -> SidecarEntry
where
F: Fn(&str, bool) -> String,
{
let params = query
.params
.iter()
.map(|p| {
let source = bindings.get(&p.name).copied().unwrap_or(HttpParamBinding::Body);
SidecarParam {
name: p.name.clone(),
lang_type: lang_type_for(&p.neutral_type, p.nullable),
source,
}
})
.collect();
let return_lang_type = compose_return_type(query, &lang_type_for);
SidecarEntry {
scythe_fn: scythe_fn.to_string(),
scythe_module: scythe_module.to_string(),
params,
return_lang_type,
is_async,
command: query.command.clone(),
}
}
fn compose_return_type<F>(query: &AnalyzedQuery, lang_type_for: &F) -> String
where
F: Fn(&str, bool) -> String,
{
match query.command {
QueryCommand::Exec => "void".to_string(),
QueryCommand::ExecRows => "rows".to_string(),
_ => {
let cols: Vec<String> = query
.columns
.iter()
.map(|c| lang_type_for(&c.neutral_type, c.nullable))
.collect();
cols.join(", ")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scythe_core::analyzer::{AnalyzedColumn, AnalyzedParam, AnalyzedQuery};
use scythe_core::parser::QueryCommand;
fn fake_query() -> AnalyzedQuery {
AnalyzedQuery {
name: "GetUser".to_string(),
command: QueryCommand::One,
sql: "SELECT id, name FROM users WHERE id = $1".to_string(),
columns: vec![
AnalyzedColumn {
name: "id".to_string(),
neutral_type: "int32".to_string(),
nullable: false,
},
AnalyzedColumn {
name: "name".to_string(),
neutral_type: "string".to_string(),
nullable: true,
},
],
params: vec![AnalyzedParam {
name: "id".to_string(),
neutral_type: "int32".to_string(),
nullable: false,
position: 1,
}],
deprecated: None,
source_table: Some("users".to_string()),
composites: vec![],
enums: vec![],
optional_params: vec![],
group_by: None,
custom: vec![],
}
}
fn py_lang_type(neutral: &str, nullable: bool) -> String {
let base = match neutral {
"int32" | "int64" | "int16" => "int",
"string" => "str",
"bool" => "bool",
_ => "Any",
};
if nullable {
format!("{base} | None")
} else {
base.to_string()
}
}
#[test]
fn carries_scythe_module_and_fn() {
let entry = build_sidecar_entry(
&fake_query(),
&BTreeMap::new(),
"queries",
"get_user",
true,
py_lang_type,
);
assert_eq!(entry.scythe_module, "queries");
assert_eq!(entry.scythe_fn, "get_user");
assert!(entry.is_async);
}
#[test]
fn binds_params_from_map() {
let mut bindings = BTreeMap::new();
bindings.insert("id".to_string(), HttpParamBinding::Path);
let entry = build_sidecar_entry(&fake_query(), &bindings, "queries", "get_user", true, py_lang_type);
assert_eq!(entry.params.len(), 1);
assert_eq!(entry.params[0].name, "id");
assert_eq!(entry.params[0].source, HttpParamBinding::Path);
assert_eq!(entry.params[0].lang_type, "int");
}
#[test]
fn unbound_params_default_to_body() {
let entry = build_sidecar_entry(
&fake_query(),
&BTreeMap::new(),
"queries",
"get_user",
true,
py_lang_type,
);
assert_eq!(entry.params[0].source, HttpParamBinding::Body);
}
#[test]
fn return_type_lists_columns_for_one_command() {
let entry = build_sidecar_entry(
&fake_query(),
&BTreeMap::new(),
"queries",
"get_user",
true,
py_lang_type,
);
assert_eq!(entry.return_lang_type, "int, str | None");
}
#[test]
fn return_type_is_void_for_exec() {
let mut q = fake_query();
q.command = QueryCommand::Exec;
let entry = build_sidecar_entry(&q, &BTreeMap::new(), "queries", "f", true, py_lang_type);
assert_eq!(entry.return_lang_type, "void");
}
#[test]
fn return_type_is_rows_for_exec_rows() {
let mut q = fake_query();
q.command = QueryCommand::ExecRows;
let entry = build_sidecar_entry(&q, &BTreeMap::new(), "queries", "f", true, py_lang_type);
assert_eq!(entry.return_lang_type, "rows");
}
#[test]
fn sidecar_insert_and_lookup() {
let mut sidecar = Sidecar::new();
let entry = build_sidecar_entry(
&fake_query(),
&BTreeMap::new(),
"queries",
"get_user",
true,
py_lang_type,
);
sidecar.insert("python", "GetUser", entry);
assert!(sidecar.entry_for("python", "GetUser").is_some());
assert!(sidecar.entry_for("typescript", "GetUser").is_none());
}
#[test]
fn sidecar_serializes_to_json() {
let mut sidecar = Sidecar::new();
let entry = build_sidecar_entry(
&fake_query(),
&BTreeMap::new(),
"queries",
"get_user",
true,
py_lang_type,
);
sidecar.insert("python", "GetUser", entry);
let json = serde_json::to_string(&sidecar).unwrap();
assert!(json.contains("\"by_language\""));
assert!(json.contains("\"python\""));
assert!(json.contains("\"GetUser\""));
}
}