use std::path::{Path, PathBuf};
use std::process::Command;
use diesel::{Connection as _, PgConnection, QueryableByName, RunQueryDsl as _, sql_query};
const fn autumn_bin() -> &'static str {
env!("CARGO_BIN_EXE_autumn")
}
fn run_autumn_ok(dir: &Path, args: &[&str], envs: &[(&str, &str)]) {
let output = Command::new(autumn_bin())
.args(args)
.current_dir(dir)
.envs(envs.iter().copied())
.output()
.expect("failed to run autumn");
assert!(
output.status.success(),
"autumn {args:?} failed (exit={:?})\nstdout: {}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
fn fresh_project(name: &str) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
run_autumn_ok(tmp.path(), &["new", name], &[]);
let project = tmp.path().join(name);
(tmp, project)
}
fn patch_generated_cargo_toml(project_dir: &Path) {
use std::fmt::Write as _;
let cargo_toml_path = project_dir.join("Cargo.toml");
let mut content = std::fs::read_to_string(&cargo_toml_path).unwrap();
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root");
let autumn_web = workspace_root.join("autumn");
write!(
content,
"\n[patch.crates-io]\nautumn-web = {{ path = \"{}\" }}\n",
autumn_web.display().to_string().replace('\\', "/")
)
.unwrap();
std::fs::write(&cargo_toml_path, content).unwrap();
}
fn cargo_check(project: &Path) -> (bool, String) {
let output = Command::new(env!("CARGO"))
.args(["check", "--quiet"])
.current_dir(project)
.output()
.expect("failed to run cargo check");
let combined = format!(
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
(output.status.success(), combined)
}
#[derive(QueryableByName)]
struct ConfigRow {
#[diesel(sql_type = diesel::sql_types::Jsonb)]
config: serde_json::Value,
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Jsonb>)]
extra: Option<serde_json::Value>,
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers) + cargo check; run with -- --ignored"]
async fn scaffolded_json_field_generates_compiles_inserts_and_selects() {
use testcontainers::runners::AsyncRunner as _;
use testcontainers_modules::postgres::Postgres;
let container = Postgres::default()
.start()
.await
.expect("failed to start Postgres testcontainer — is Docker running?");
let host = container.get_host().await.unwrap();
let port = container.get_host_port_ipv4(5432).await.unwrap();
let db_url = format!("postgres://postgres:postgres@{host}:{port}/postgres");
let envs = [("AUTUMN_DATABASE__URL", db_url.as_str())];
let (_tmp, project) = fresh_project("setting_app");
patch_generated_cargo_toml(&project);
run_autumn_ok(
&project,
&[
"generate",
"scaffold",
"Setting",
"name:String",
"config:json",
"extra:Option<jsonb>",
],
&[],
);
let model = std::fs::read_to_string(project.join("src/models/setting.rs")).unwrap();
assert!(
model.contains("pub config: serde_json::Value,"),
"generated model must carry a bare serde_json::Value field:\n{model}"
);
assert!(
model.contains("pub extra: Option<serde_json::Value>,"),
"generated model must carry the nullable field:\n{model}"
);
let schema = std::fs::read_to_string(project.join("src/schema.rs")).unwrap();
assert!(
schema.contains("config -> Jsonb,"),
"schema.rs must use diesel's Jsonb sql-type:\n{schema}"
);
assert!(
schema.contains("extra -> Nullable<Jsonb>,"),
"schema.rs must use a nullable Jsonb sql-type:\n{schema}"
);
let (ok, output) = cargo_check(&project);
assert!(ok, "generated project must compile:\n{output}");
run_autumn_ok(&project, &["migrate"], &envs);
let mut conn = PgConnection::establish(&db_url).expect("connect to postgres");
let config = serde_json::json!({"theme": "dark", "retries": 3, "nested": {"a": [1, 2, 3]}});
let extra = serde_json::json!(["x", "y", "z"]);
sql_query("INSERT INTO settings (name, config, extra) VALUES ($1, $2, $3)")
.bind::<diesel::sql_types::Text, _>("prefs")
.bind::<diesel::sql_types::Jsonb, _>(config.clone())
.bind::<diesel::sql_types::Nullable<diesel::sql_types::Jsonb>, _>(Some(extra.clone()))
.execute(&mut conn)
.expect("insert a row with real JSON object/array values");
let selected = sql_query("SELECT config, extra FROM settings WHERE name = 'prefs'")
.get_result::<ConfigRow>(&mut conn)
.expect("select the row back");
assert_eq!(
selected.config, config,
"the JSON object must round-trip byte-for-byte through the generated JSONB column"
);
assert_eq!(
selected.extra,
Some(extra),
"the JSON array must round-trip byte-for-byte through the nullable JSONB column"
);
sql_query("INSERT INTO settings (name, config) VALUES ($1, $2)")
.bind::<diesel::sql_types::Text, _>("no-extra")
.bind::<diesel::sql_types::Jsonb, _>(config)
.execute(&mut conn)
.expect("insert a row leaving the nullable column unset");
let selected_null = sql_query("SELECT config, extra FROM settings WHERE name = 'no-extra'")
.get_result::<ConfigRow>(&mut conn)
.expect("select the NULL-extra row back");
assert_eq!(selected_null.extra, None);
}