use super::sql;
use crate::Derive;
use crate::schema::{Table, mentions_generated_type};
pub(super) const MAX_WIDTH: usize = 100;
pub(super) const CHAIN_WIDTH: usize = 60;
pub(super) const FN_CALL_WIDTH: usize = 60;
pub fn push_chain(output: &mut String, indent: usize, lead: &str, head: &str, calls: &[String]) {
let pad = " ".repeat(indent);
let chain = format!("{head}{}", calls.concat());
if chain.len() <= CHAIN_WIDTH {
output.push_str(&format!("{pad}{lead}{chain}\n"));
return;
}
output.push_str(&format!("{pad}{lead}{head}\n"));
for call in calls {
output.push_str(&format!("{pad} {call}\n"));
}
}
pub fn serde_derives(serde: bool) -> &'static [&'static str] {
if serde {
&["serde::Serialize", "serde::Deserialize"]
} else {
&[]
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DeriveOptions {
pub serde: bool,
pub validate: bool,
}
impl DeriveOptions {
pub fn of(derive: &[Derive]) -> Self {
DeriveOptions {
serde: derive.contains(&Derive::Serde),
validate: derive.contains(&Derive::Validate),
}
}
pub fn input_derives(self) -> Vec<&'static str> {
let mut derives = serde_derives(self.serde).to_vec();
if self.validate {
derives.push("validator::Validate");
}
derives
}
}
pub fn push_derives(output: &mut String, extra: &[&str]) {
if extra.is_empty() {
return;
}
let without_newline = output.strip_suffix('\n').unwrap_or(output);
let start = without_newline.rfind('\n').map_or(0, |index| index + 1);
let derives = output[start..]
.trim_end()
.strip_prefix("#[derive(")
.and_then(|rest| rest.strip_suffix(")]"))
.expect("derives follow a `#[derive(...)]` line")
.to_owned();
output.truncate(start);
let derives = format!("{derives}, {}", extra.join(", "));
let one_line = format!("#[derive({derives})]");
let packed = format!(" {derives},");
if one_line.len() < MAX_WIDTH - 1 {
output.push_str(&one_line);
output.push('\n');
} else if packed.len() < MAX_WIDTH {
output.push_str(&format!("#[derive(\n{packed}\n)]\n"));
} else {
output.push_str("#[derive(\n");
for derive in derives.split(", ") {
output.push_str(&format!(" {derive},\n"));
}
output.push_str(")]\n");
}
}
pub fn push_serde_snake_case(output: &mut String) {
output.push_str("#[serde(rename_all = \"snake_case\")]\n");
}
pub fn use_list(prefix: &str, names: &[String]) -> String {
if let [name] = names {
return format!("use {prefix}::{name};\n");
}
let one_line = format!("use {prefix}::{{{}}};", names.join(", "));
if one_line.len() < MAX_WIDTH - 1 {
return format!("{one_line}\n");
}
let mut output = format!("use {prefix}::{{\n");
let mut line = String::new();
for name in names {
if !line.is_empty() && line.len() + 1 + name.len() + 1 > MAX_WIDTH - 1 {
output.push_str(&line);
output.push('\n');
line.clear();
}
if line.is_empty() {
line.push_str(&format!(" {name},"));
} else {
line.push_str(&format!(" {name},"));
}
}
output.push_str(&line);
output.push_str("\n};\n");
output
}
pub fn use_paths(paths: &[String]) -> String {
let mut sorted: Vec<&String> = paths.iter().collect();
sorted.sort_by_key(|path| {
path.split("::")
.map(|segment| (segment != "super", segment.to_owned()))
.collect::<Vec<_>>()
});
sorted.dedup();
sorted.iter().map(|path| format!("use {path};\n")).collect()
}
pub fn types_import<'a>(db_path: &str, rust_types: impl IntoIterator<Item = &'a str>) -> String {
if rust_types.into_iter().any(mentions_generated_type) {
format!("use {db_path}::types;\n")
} else {
String::new()
}
}
pub fn doc_name(sql_name: &str) -> String {
format!("`{}`", sql_name.replace(['\r', '\n'], " "))
}
pub fn push_doc(output: &mut String, indent: usize, lines: &[&str]) {
let pad = " ".repeat(indent);
for line in lines {
if line.is_empty() {
output.push_str(&format!("{pad}///\n"));
} else {
output.push_str(&format!("{pad}/// {line}\n"));
}
}
}
pub fn push_fn_head(output: &mut String, name: &str, params: &[String], returns: &str) {
let mut all_params = vec!["executor: E".to_owned()];
all_params.extend(params.iter().cloned());
let open = format!("pub async fn {name}<'e, E>(");
let close = format!(") -> {returns}");
let one_line = format!("{open}{}{close}", all_params.join(", "));
if one_line.len() <= MAX_WIDTH {
output.push_str(&one_line);
output.push('\n');
} else {
output.push_str(&open);
output.push('\n');
for param in &all_params {
output.push_str(&format!(" {param},\n"));
}
output.push_str(&close);
output.push('\n');
}
output.push_str("where\n E: sqlx::PgExecutor<'e>,\n{\n");
}
pub fn push_and_chain(
output: &mut String,
indent: usize,
lead: &str,
terms: &[String],
trail: &str,
) {
let pad = " ".repeat(indent);
let one_line = format!("{pad}{lead}{}{trail}", terms.join(" && "));
if one_line.len() <= MAX_WIDTH {
output.push_str(&one_line);
output.push('\n');
return;
}
output.push_str(&format!("{pad}{lead}{}\n", terms[0]));
for term in &terms[1..] {
output.push_str(&format!("{pad} && {term}\n"));
}
let trail = trail.trim_start();
if !trail.is_empty() {
output.push_str(&format!("{pad}{trail}\n"));
}
}
pub fn push_let_call(
output: &mut String,
indent: usize,
binding: &str,
call: &str,
argument: &str,
) {
let pad = " ".repeat(indent);
let one_line = format!("{pad}{binding} = {call}({argument});");
let below = format!("{pad} {call}({argument});");
if one_line.len() <= MAX_WIDTH {
output.push_str(&one_line);
output.push('\n');
} else if below.len() <= MAX_WIDTH {
output.push_str(&format!("{pad}{binding} =\n{below}\n"));
} else {
output.push_str(&format!(
"{pad}{binding} = {call}(\n{pad} {argument},\n{pad});\n"
));
}
}
pub struct StaticQuery<'a> {
pub call: &'a str,
pub sql: Vec<String>,
pub binds: Vec<String>,
pub execute: &'a str,
}
impl StaticQuery<'_> {
pub fn push(&self, output: &mut String, indent: usize, lead: &str, trail: &str) {
let pad = " ".repeat(indent);
output.push_str(&format!("{pad}{lead}{}(\n", self.call));
output.push_str(&sql::raw_block(&self.sql, indent + 4));
output.push_str(",\n");
output.push_str(&format!("{pad})\n"));
for bind in &self.binds {
output.push_str(&format!("{pad}.bind({bind})\n"));
}
output.push_str(&format!("{pad}.{}\n", self.execute));
output.push_str(&format!("{pad}.await{trail}\n"));
}
}
pub fn push_build_and_fetch(output: &mut String, table: &Table, fetch: &str) {
let calls = [
format!(".build_query_as::<{}>()", table.struct_name),
format!(".{fetch}(executor)"),
".await".to_owned(),
];
push_chain(output, 4, "", "query", &calls);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_derives_join_the_derive_list_as_rustfmt_lays_it_out() {
let mut short = "/// Doc.\n#[derive(Debug, Clone)]\n".to_owned();
push_derives(&mut short, serde_derives(true));
assert_eq!(
short,
"/// Doc.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n"
);
let mut long =
"#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)]\n"
.to_owned();
push_derives(&mut long, serde_derives(true));
assert_eq!(
long,
"#[derive(\n Debug,\n Clone,\n Copy,\n PartialEq,\n Eq,\n PartialOrd,\n Ord,\n Hash,\n sqlx::Type,\n serde::Serialize,\n serde::Deserialize,\n)]\n"
);
let mut packed =
"#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]\n".to_owned();
push_derives(&mut packed, serde_derives(true));
assert_eq!(
packed,
"#[derive(\n Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,\n)]\n"
);
let mut off = "#[derive(Debug)]\n".to_owned();
push_derives(&mut off, serde_derives(false));
assert_eq!(off, "#[derive(Debug)]\n");
}
#[test]
fn fn_head_stays_on_one_line_when_it_fits() {
let mut output = String::new();
push_fn_head(&mut output, "list", &[], "Result<(), sqlx::Error>");
assert_eq!(
output,
"pub async fn list<'e, E>(executor: E) -> Result<(), sqlx::Error>\nwhere\n E: sqlx::PgExecutor<'e>,\n{\n"
);
}
#[test]
fn fn_head_goes_vertical_when_too_wide() {
let mut output = String::new();
let params = vec![
"order_id: uuid::Uuid".to_owned(),
"product_id: uuid::Uuid".to_owned(),
];
push_fn_head(
&mut output,
"get_by_id",
¶ms,
"Result<Option<LineItem>, sqlx::Error>",
);
assert!(output.starts_with(
"pub async fn get_by_id<'e, E>(\n executor: E,\n order_id: uuid::Uuid,\n product_id: uuid::Uuid,\n) -> Result<Option<LineItem>, sqlx::Error>\nwhere\n"
));
}
}