use super::super::test_support::{column, defaulted, generated, nullable, table};
use super::*;
pub(super) fn render(table: &Table) -> String {
super::render(
table,
DeriveOptions {
serde: false,
validate: false,
openapi: false,
},
)
}
#[test]
fn serde_derives_rows_inputs_and_patches_on_request() {
let users = table(
vec![column("id", "i64"), column("email", "String")],
&["id"],
);
let serde = "serde::Serialize, serde::Deserialize)]\n";
assert!(!render(&users).contains("serde"));
let rendered = super::render(
&users,
DeriveOptions {
serde: true,
validate: false,
openapi: false,
},
);
for item in ["", "pub struct User {", "pub struct NewUser {"] {
assert!(
rendered.contains(&format!("{serde}{item}")),
"{item}:\n{rendered}"
);
}
assert!(
rendered.contains(&format!(
"{serde}#[serde(default)]\npub struct UpdateUser {{"
)),
"{rendered}"
);
}
#[test]
fn serde_patches_and_inputs_tell_absent_from_null() {
let users = table(
vec![
column("id", "i64"),
column("email", "String"),
nullable("note", "String"),
defaulted(nullable("tag", "String")),
],
&["id"],
);
let rendered = super::render(
&users,
DeriveOptions {
serde: true,
validate: false,
openapi: false,
},
);
let patch = rendered.split("pub struct UpdateUser {").nth(1).unwrap();
assert!(patch.contains(
" #[serde(skip_serializing_if = \"Option::is_none\")]\n pub email: Option<String>,\n"
), "{patch}");
assert!(patch.contains(
" #[serde(\n deserialize_with = \"super::super::nullable::deserialize\",\n skip_serializing_if = \"Option::is_none\"\n )]\n pub note: Option<Option<String>>,\n"
), "{patch}");
let input = rendered.split("pub struct NewUser {").nth(1).unwrap();
let input = input.split("\n}\n").next().unwrap();
assert!(input.contains(
" #[serde(\n default,\n deserialize_with = \"super::super::nullable::deserialize\",\n skip_serializing_if = \"Option::is_none\"\n )]\n pub tag: Option<Option<String>>,"
), "{input}");
assert_eq!(input.matches("#[serde(").count(), 1, "{input}");
}
#[test]
fn nullable_helper_reads_null_as_some_none() {
let rendered = render_nullable();
assert!(rendered.starts_with(HEADER));
assert!(rendered.contains(
"pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>"
));
assert!(rendered.contains("Option::<T>::deserialize(deserializer).map(Some)"));
}
#[test]
fn renders_row_struct_with_from_row() {
let rendered = render(&table(vec![column("email", "String")], &[]));
assert!(rendered.starts_with("// @generated by rok-sqlc. Do not edit by hand.\n"));
assert!(rendered.contains(
"/// A row of the `users` table.\n#[derive(Debug, Clone, PartialEq, sqlx::FromRow)]\npub struct User {\n /// The `email` column.\n pub email: String,\n}"
));
}
#[test]
fn row_doc_names_schema_qualified_table() {
let mut users = table(vec![column("email", "String")], &[]);
users.schema = Some("core".to_owned());
assert!(render(&users).contains("/// A row of the `core.users` table."));
}
#[test]
fn wraps_nullable_columns_in_option() {
let rendered = render(&table(vec![nullable("email", "String")], &[]));
assert!(rendered.contains("pub email: Option<String>,"));
}
#[test]
fn emits_rename_for_renamed_fields() {
let mut renamed = column("type", "String");
renamed.rust_name = "type_".to_owned();
let mut quoted = column("Mixed \"Case\"", "String");
quoted.rust_name = "mixed_case".to_owned();
let rendered = render(&table(vec![renamed, quoted], &[]));
assert!(rendered.contains(" #[sqlx(rename = \"type\")]\n pub type_: String,"));
assert!(
rendered
.contains(" #[sqlx(rename = r#\"Mixed \"Case\"\"#)]\n pub mixed_case: String,")
);
}
#[test]
fn insert_input_omits_generated_and_wraps_defaulted_columns() {
let rendered = render(&table(
vec![
generated(column("id", "i64")),
column("email", "String"),
nullable("nickname", "String"),
defaulted(column("created_at", "chrono::DateTime<chrono::Utc>")),
defaulted(nullable("tag", "String")),
],
&["id"],
));
let input = rendered.split("pub struct NewUser {").nth(1).unwrap();
let input = input.split('}').next().unwrap();
assert!(!input.contains("pub id:"));
assert!(input.contains(" pub email: String,\n"));
assert!(input.contains(" pub nickname: Option<String>,\n"));
assert!(input.contains(" pub created_at: Option<chrono::DateTime<chrono::Utc>>,\n"));
assert!(input.contains(
" /// `None` applies the column default; `Some(None)` inserts `NULL`.\n pub tag: Option<Option<String>>,\n"
));
assert!(rendered.contains("#[derive(Debug, Clone, PartialEq)]\npub struct NewUser {"));
}
#[test]
fn insert_input_derives_default_when_every_field_is_optional() {
let rendered = render(&table(
vec![
generated(column("id", "i64")),
defaulted(column("hits", "i32")),
nullable("note", "String"),
],
&["id"],
));
assert!(rendered.contains("#[derive(Debug, Clone, Default, PartialEq)]\npub struct NewUser {"));
}
#[test]
fn update_patch_documents_null_and_offers_is_empty() {
let rendered = render(&table(
vec![
column("id", "i64"),
column("email", "String"),
nullable("note", "String"),
],
&["id"],
));
assert!(
rendered.contains("#[derive(Debug, Clone, Default, PartialEq)]\npub struct UpdateUser {")
);
assert!(rendered.contains(" pub email: Option<String>,\n"));
assert!(rendered.contains(
" /// `Some(None)` sets the column to `NULL`.\n pub note: Option<Option<String>>,\n"
));
assert!(!rendered.contains(" pub id: Option"));
assert!(rendered.contains(
" pub fn is_empty(&self) -> bool {\n self.email.is_none() && self.note.is_none()\n }"
));
}
#[test]
fn omits_update_patch_without_primary_key() {
let rendered = render(&table(vec![column("email", "String")], &[]));
assert!(!rendered.contains("pub struct UpdateUser"));
}
#[test]
fn output_is_deterministic() {
let build = || {
render(&table(
vec![column("id", "i64"), column("email", "String")],
&["id"],
))
};
assert_eq!(build(), build());
}