use std::fmt::Write as _;
use scythe_backend::manifest::BackendManifest;
use crate::GeneratedCode;
pub(crate) fn go_file_header(
preamble: &str,
stdlib_imports: &[&str],
third_party_imports: &[&str],
manifest: &BackendManifest,
generated: &[GeneratedCode],
) -> String {
let (extra_stdlib, extra_third_party) = resolve_manifest_imports(manifest, generated);
let mut header = String::from(preamble);
header.push_str("\n\nimport (\n");
for import in stdlib_imports {
let _ = writeln!(header, "\t\"{import}\"");
}
for import in &extra_stdlib {
let _ = writeln!(header, "\t{import}");
}
if !third_party_imports.is_empty() || !extra_third_party.is_empty() {
header.push('\n');
for import in third_party_imports {
let _ = writeln!(header, "\t\"{import}\"");
}
for import in &extra_third_party {
let _ = writeln!(header, "\t{import}");
}
}
header.push_str(")\n");
header
}
fn resolve_manifest_imports(manifest: &BackendManifest, generated: &[GeneratedCode]) -> (Vec<String>, Vec<String>) {
let mut stdlib = Vec::new();
let mut third_party = Vec::new();
if let Some(imports) = &manifest.imports {
for (prefix, import) in &imports.rules {
if !generated_code_uses_prefix(generated, prefix) {
continue;
}
if is_stdlib_import(import) {
stdlib.push(import.clone());
} else {
third_party.push(import.clone());
}
}
}
stdlib.sort();
stdlib.dedup();
third_party.sort();
third_party.dedup();
(stdlib, third_party)
}
fn is_stdlib_import(import: &str) -> bool {
let path = import.trim_matches('"');
let first_segment = path.split('/').next().unwrap_or(path);
!first_segment.contains('.')
}
fn generated_code_contains(code: &GeneratedCode, needle: &str) -> bool {
[
code.enum_def.as_deref(),
code.model_struct.as_deref(),
code.row_struct.as_deref(),
code.query_fn.as_deref(),
]
.into_iter()
.flatten()
.any(|fragment| fragment.contains(needle))
|| code.nested_struct_defs.iter().any(|def| def.code.contains(needle))
}
pub(crate) fn generated_code_uses_prefix(generated: &[GeneratedCode], needle: &str) -> bool {
generated.iter().any(|code| generated_code_contains(code, needle))
}
#[cfg(test)]
mod tests {
use super::*;
fn manifest_with_rules(rules: &[(&str, &str)]) -> BackendManifest {
let mut toml = String::from(
"[backend]\nname = \"test\"\nlanguage = \"go\"\nfile_extension = \"go\"\nengine = \"test\"\n\n\
[types.scalars]\n[types.containers]\n\n\
[naming]\nstruct_case = \"PascalCase\"\nfn_case = \"PascalCase\"\n\
enum_variant_case = \"PascalCase\"\nrow_suffix = \"Row\"\n\n[imports.rules]\n",
);
for (key, value) in rules {
let _ = writeln!(toml, "{key:?} = {value:?}");
}
toml::from_str(&toml).expect("test manifest must parse")
}
fn manifest_without_imports() -> BackendManifest {
toml::from_str(
"[backend]\nname = \"test\"\nlanguage = \"go\"\nfile_extension = \"go\"\nengine = \"test\"\n\n\
[types.scalars]\n[types.containers]\n\n\
[naming]\nstruct_case = \"PascalCase\"\nfn_case = \"PascalCase\"\n\
enum_variant_case = \"PascalCase\"\nrow_suffix = \"Row\"\n",
)
.expect("test manifest must parse")
}
fn code_with_row_struct(row_struct: &str) -> GeneratedCode {
GeneratedCode::build(|c| {
c.row_struct = Some(row_struct.to_string());
})
}
#[test]
fn test_go_file_header_single_group_omits_extras_when_manifest_has_no_rules() {
let manifest = manifest_without_imports();
let header = go_file_header("package queries", &["context", "database/sql"], &[], &manifest, &[]);
assert_eq!(
header,
"package queries\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n)\n"
);
}
#[test]
fn test_go_file_header_single_group_includes_time_when_generated_code_uses_it() {
let manifest = manifest_with_rules(&[("time.", "\"time\"")]);
let generated = [code_with_row_struct("type Row struct {\n\tCreatedAt time.Time\n}")];
let header = go_file_header(
"package queries",
&["context", "database/sql"],
&[],
&manifest,
&generated,
);
assert_eq!(
header,
"package queries\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"time\"\n)\n"
);
}
#[test]
fn test_go_file_header_two_group_matches_pgx_fixture_when_time_and_decimal_used() {
let preamble =
"// Code generated by scythe. DO NOT EDIT.\n// Run `goimports -w .` to fix imports.\npackage queries";
let manifest = manifest_with_rules(&[("time.", "\"time\""), ("decimal.", "\"github.com/shopspring/decimal\"")]);
let generated = [code_with_row_struct(
"type Row struct {\n\tCreatedAt time.Time\n\tTotal decimal.Decimal\n}",
)];
let header = go_file_header(
preamble,
&["context"],
&["github.com/jackc/pgx/v5/pgxpool"],
&manifest,
&generated,
);
assert_eq!(
header,
"// Code generated by scythe. DO NOT EDIT.\n// Run `goimports -w .` to fix imports.\npackage queries\n\n\
import (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/shopspring/\
decimal\"\n)\n"
);
}
#[test]
fn test_go_file_header_two_group_keeps_third_party_group_without_manifest_extras() {
let manifest = manifest_without_imports();
let header = go_file_header(
"package queries",
&["context"],
&["github.com/jackc/pgx/v5/pgxpool"],
&manifest,
&[],
);
assert_eq!(
header,
"package queries\n\nimport (\n\t\"context\"\n\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n)\n"
);
}
#[test]
fn test_go_file_header_adds_stdlib_and_third_party_imports_for_uuid_json_and_netip() {
let manifest = manifest_with_rules(&[
("uuid.", "\"github.com/google/uuid\""),
("json.", "\"encoding/json\""),
("netip.", "\"net/netip\""),
]);
let generated = [code_with_row_struct(
"type Row struct {\n\tId uuid.UUID\n\tPayload json.RawMessage\n\tIp netip.Addr\n}",
)];
let header = go_file_header(
"package queries",
&["context"],
&["github.com/jackc/pgx/v5/pgxpool"],
&manifest,
&generated,
);
assert_eq!(
header,
"package queries\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/netip\"\n\n\
\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/google/uuid\"\n)\n"
);
}
#[test]
fn test_go_file_header_omits_rule_whose_prefix_never_appears_in_generated_code() {
let manifest = manifest_with_rules(&[("decimal.", "\"github.com/shopspring/decimal\"")]);
let generated = [code_with_row_struct("type Row struct {\n\tId int32\n\tName string\n}")];
let header = go_file_header(
"package queries",
&["context", "database/sql"],
&[],
&manifest,
&generated,
);
assert_eq!(
header,
"package queries\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n)\n"
);
}
#[test]
fn test_generated_code_uses_prefix_checks_nested_struct_defs() {
let generated = [GeneratedCode::build(|c| {
c.nested_struct_defs.push(crate::NestedStructDef {
name: "nested".to_string(),
code: "type Nested struct {\n\tSeenAt time.Time\n}".to_string(),
});
})];
assert!(generated_code_uses_prefix(&generated, "time."));
}
#[test]
fn test_is_stdlib_import_distinguishes_by_first_path_segment() {
assert!(is_stdlib_import("\"time\""));
assert!(is_stdlib_import("\"encoding/json\""));
assert!(is_stdlib_import("\"net/netip\""));
assert!(!is_stdlib_import("\"github.com/google/uuid\""));
assert!(!is_stdlib_import("\"github.com/shopspring/decimal\""));
}
}