259 lines
8.6 KiB
Rust
259 lines
8.6 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn database_scalar_source_preserves_exact_values_and_nullable_updates() {
|
|
use dever_runtime::value::decimal::Decimal;
|
|
let directory = Directory::new();
|
|
scalar_fixture(&directory.0);
|
|
let project = Project::load(&directory.0).unwrap();
|
|
let program = project.check().unwrap();
|
|
let binary = build::build(
|
|
&program,
|
|
&project.sources,
|
|
"main",
|
|
&directory.0.join("scalars"),
|
|
)
|
|
.unwrap();
|
|
std::fs::create_dir(directory.0.join("config")).unwrap();
|
|
std::fs::write(directory.0.join("config/setting.json"), r#"{"database":{"default":{"type":"sqlite","path":"data/scalars.db","max_connections":1,"max_page_size":20}}}"#).unwrap();
|
|
let run = |name: &str, input: serde_json::Value| {
|
|
let output = execute(&binary, &format!("demo.account.{name}"), &input.to_string());
|
|
assert!(
|
|
output.status.success(),
|
|
"{name}: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap()["data"].clone()
|
|
};
|
|
let base = serde_json::json!({
|
|
"amount":"1.0000", "price":"999.99", "uid":"00112233-4455-6677-8899-aabbccddeeff",
|
|
"day":"0000-01-01", "time":"23:59:59.999", "elapsed":i64::MIN,
|
|
"document":{"items":[true,null,{"name":"雪"}]},
|
|
"optional_uid":"ffffffff-ffff-ffff-ffff-ffffffffffff", "optional_day":"9999-12-31",
|
|
"optional_time":"00:00:00", "optional_elapsed":i64::MAX, "optional_document":{"value":42}
|
|
});
|
|
let created = run("create", base.clone());
|
|
assert_eq!(created["id"], 1);
|
|
assert_eq!(created["amount"], "1");
|
|
for field in [
|
|
"price",
|
|
"uid",
|
|
"day",
|
|
"time",
|
|
"elapsed",
|
|
"document",
|
|
"optional_uid",
|
|
"optional_day",
|
|
"optional_time",
|
|
"optional_elapsed",
|
|
"optional_document",
|
|
] {
|
|
assert_eq!(created[field], base[field], "{field}");
|
|
}
|
|
assert_eq!(run("get", serde_json::json!({"id":1})), created);
|
|
for field in [
|
|
"price",
|
|
"optional_uid",
|
|
"optional_day",
|
|
"optional_time",
|
|
"optional_elapsed",
|
|
"optional_document",
|
|
] {
|
|
let mut changed = base.clone();
|
|
changed["id"] = 1.into();
|
|
changed[field] = serde_json::Value::Null;
|
|
assert!(run("update", changed)[field].is_null(), "{field}");
|
|
}
|
|
for (field, value) in [
|
|
("amount", "0.00001"),
|
|
("amount", "1000000000000000000000000000000"),
|
|
("price", "1000"),
|
|
("price", "0.001"),
|
|
("day", "2025-02-29"),
|
|
("time", "24:00:00"),
|
|
("uid", "00112233445566778899aabbccddeeff"),
|
|
] {
|
|
let mut invalid = base.clone();
|
|
invalid[field] = value.into();
|
|
invalid["uid"] = if field == "uid" {
|
|
value
|
|
} else {
|
|
"11112233-4455-6677-8899-aabbccddeeff"
|
|
}
|
|
.into();
|
|
let failed = execute(&binary, "demo.account.create", &invalid.to_string());
|
|
assert!(!failed.status.success(), "accepted {field}: {value}");
|
|
assert_eq!(run("count", serde_json::json!({})), 1);
|
|
}
|
|
let mut changed = base.clone();
|
|
changed["id"] = 1.into();
|
|
changed["amount"] = "-999999999999999999999999999999.9999".into();
|
|
let minimum = run("update", changed.clone());
|
|
assert_eq!(minimum["amount"], changed["amount"]);
|
|
for (index, amount) in [
|
|
"-10",
|
|
"-0.0001",
|
|
"0",
|
|
"0.0001",
|
|
"10",
|
|
"999999999999999999999999999999.9999",
|
|
]
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
let mut value = base.clone();
|
|
value["amount"] = (*amount).into();
|
|
value["uid"] = format!("00000000-0000-0000-0000-{:012x}", index).into();
|
|
value["price"] = serde_json::Value::Null;
|
|
let row = run("create", value);
|
|
assert_eq!(
|
|
Decimal::parse(row["amount"].as_str().unwrap()).unwrap(),
|
|
Decimal::parse(amount).unwrap()
|
|
);
|
|
assert!(row["price"].is_null());
|
|
}
|
|
let page = run("search", serde_json::json!({"low":"-10"}));
|
|
let actual: Vec<_> = page["items"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|row| Decimal::parse(row["amount"].as_str().unwrap()).unwrap())
|
|
.collect();
|
|
assert_eq!(
|
|
actual,
|
|
[
|
|
"-10",
|
|
"-0.0001",
|
|
"0",
|
|
"0.0001",
|
|
"10",
|
|
"999999999999999999999999999999.9999"
|
|
]
|
|
.map(|text| Decimal::parse(text).unwrap())
|
|
);
|
|
assert_eq!(
|
|
run("price", serde_json::json!({"price":"999.990"}))["total"],
|
|
1
|
|
);
|
|
assert_eq!(run("price", serde_json::json!({}))["total"], 6);
|
|
let reused = run(
|
|
"document",
|
|
serde_json::json!({"id":1,"document":base["document"]}),
|
|
);
|
|
assert_eq!(reused["document"], base["document"]);
|
|
assert_eq!(reused["optional_document"], base["document"]);
|
|
assert_eq!(run("get", serde_json::json!({"id":1})), reused);
|
|
let raw =
|
|
r#"{"id":1,"document":{"precise":123456789012345678901234567890.0010,"tiny":1e-1000}}"#;
|
|
let output = execute(&binary, "demo.account.document", raw);
|
|
assert!(
|
|
output.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let output = String::from_utf8(output.stdout).unwrap();
|
|
assert!(output.contains("123456789012345678901234567890.0010"));
|
|
assert!(output.contains("1e-1000"));
|
|
let output = execute(&binary, "demo.account.get", r#"{"id":1}"#);
|
|
assert!(
|
|
String::from_utf8(output.stdout)
|
|
.unwrap()
|
|
.contains("123456789012345678901234567890.0010")
|
|
);
|
|
}
|
|
|
|
fn scalar_fixture(root: &Path) {
|
|
let fields = [
|
|
("amount", "Decimal(34, 4)", "Decimal"),
|
|
("price", "Decimal(5, 2)?", "Decimal?"),
|
|
("uid", "Uuid", "Uuid"),
|
|
("day", "Date", "Date"),
|
|
("time", "Time", "Time"),
|
|
("elapsed", "Duration", "Duration"),
|
|
("document", "Json", "Json"),
|
|
("optional_uid", "Uuid?", "Uuid?"),
|
|
("optional_day", "Date?", "Date?"),
|
|
("optional_time", "Time?", "Time?"),
|
|
("optional_elapsed", "Duration?", "Duration?"),
|
|
("optional_document", "Json?", "Json?"),
|
|
];
|
|
let root = root.join("module/demo/account");
|
|
std::fs::create_dir_all(&root).unwrap();
|
|
let columns = fields
|
|
.iter()
|
|
.map(|(name, kind, _)| {
|
|
format!(
|
|
"{name}: {kind}{}\n",
|
|
if *name == "uid" { " unique" } else { "" }
|
|
)
|
|
})
|
|
.collect::<String>();
|
|
std::fs::write(
|
|
root.join("model.dever"),
|
|
format!("type Account {{\n{columns}}}\n"),
|
|
)
|
|
.unwrap();
|
|
let input = fields
|
|
.iter()
|
|
.map(|(name, _, kind)| format!("{name}: {kind}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let values = fields
|
|
.iter()
|
|
.map(|(name, _, _)| format!("{name} = {name}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let methods = [
|
|
(
|
|
"create",
|
|
input.clone(),
|
|
"record: model.Account",
|
|
format!("record = model.create({{ {values} }})"),
|
|
),
|
|
(
|
|
"update",
|
|
format!("id: model.id, {input}"),
|
|
"record: model.Account",
|
|
format!("record = model.update(id, {{ {values} }})"),
|
|
),
|
|
(
|
|
"get",
|
|
"id: model.id".into(),
|
|
"record: model.Account",
|
|
"record = model.get(id)".into(),
|
|
),
|
|
(
|
|
"count",
|
|
"".into(),
|
|
"total: Int",
|
|
"total = model.count()".into(),
|
|
),
|
|
(
|
|
"search",
|
|
"low: Decimal".into(),
|
|
"page: model.Page",
|
|
"page = model.list({ where = amount >= low, order = [amount.asc] })".into(),
|
|
),
|
|
(
|
|
"price",
|
|
"price: Decimal?".into(),
|
|
"page: model.Page",
|
|
"page = model.list({ where = price == price })".into(),
|
|
),
|
|
(
|
|
"document",
|
|
"id: model.id, document: Json".into(),
|
|
"record: model.Account",
|
|
"record = model.update(id, { document = document, optional_document = document })\nrecord.document = document".into(),
|
|
),
|
|
];
|
|
let mut app = String::new();
|
|
let mut api = String::new();
|
|
for (name, inputs, output, body) in methods {
|
|
app.push_str(&format!("{name}({inputs}) ({output}) {{\n{body}\n}}\n"));
|
|
api.push_str(&format!("cmd {name} = app.{name}\n"));
|
|
}
|
|
std::fs::write(root.join("app.dever"), app).unwrap();
|
|
std::fs::write(root.join("api.dever"), api).unwrap();
|
|
}
|