Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
// 使用实际三条调用路径记录接受/拒绝,不把 Axum 默认行为误当成 Dever 合同。
fn query_checks() {
let suffix = "&count=-7&enabled=true&ratio=1.25";
let mut cases = vec![
("plain", format!("name=Alice{suffix}")),
("empty_text", format!("name={suffix}")),
("unicode", format!("name=%E9%9B%AA+test{suffix}")),
("escaped_separator", format!("name=a%26b%3Dc{suffix}")),
("escaped_percent", format!("name=%25{suffix}")),
(
"valid_replacement_character",
format!("name=%EF%BF%BD{suffix}"),
),
("duplicate_required", format!("name=a&name=b{suffix}")),
("duplicate_optional", format!("name=a{suffix}&tag=x&tag=y")),
("escaped_duplicate", format!("name=a&%6Eame=b{suffix}")),
("unknown", format!("name=a{suffix}&extra=x")),
("missing_required", "name=a&enabled=true&ratio=1.25".into()),
("optional_present", format!("name=a{suffix}&tag=x")),
("empty_pairs", format!("&&name=a&&{suffix}&")),
("missing_equals", format!("name{suffix}")),
("truncated_percent", format!("name=%{suffix}")),
("short_percent", format!("name=%A{suffix}")),
("invalid_hex", format!("name=%GG{suffix}")),
("invalid_utf8", format!("name=%FF{suffix}")),
("overlong_utf8", format!("name=%C0%AF{suffix}")),
("encoded_surrogate", format!("name=%ED%A0%80{suffix}")),
("nul", format!("name=%00{suffix}")),
];
for (name, value) in [
("int_min", "-9223372036854775808"),
("int_max", "9223372036854775807"),
("int_overflow", "9223372036854775808"),
("int_fraction", "1.5"),
("int_leading_zero", "007"),
("int_encoded_plus", "%2B7"),
] {
cases.push((
name,
format!("name=a&count={value}&enabled=true&ratio=1.25"),
));
}
for (name, value) in [
("float_nan", "NaN"),
("float_inf", "inf"),
("float_overflow", "1e999"),
("float_negative_zero", "-0"),
("float_exponent", "1.5e3"),
] {
cases.push((name, format!("name=a&count=7&enabled=true&ratio={value}")));
}
cases.push(("invalid_bool", "name=a&count=7&enabled=1&ratio=1.25".into()));
println!("case,dever,axum,serde_urlencoded,equal_values");
for (name, query) in cases {
let input = [query_input(&query)];
let actual = runtime_query::<QueryArguments>(&input);
let axum = axum_query::<QueryArguments>(&input);
let serde = serde_query::<QueryArguments>(&input);
let equal = match (&actual, &axum, &serde) {
(Ok(actual), Ok(axum), Ok(serde)) => actual == axum && actual == serde,
(Err(_), Err(_), Err(_)) => true,
_ => false,
};
println!(
"{name},{},{},{},{equal}",
actual.is_ok(),
axum.is_ok(),
serde.is_ok()
);
}
}
+167
View File
@@ -0,0 +1,167 @@
// 对比实际运行库、Axum Query 和其底层 serde_urlencoded;不包含网络或路由耗时。
// 默认库的错误输入行为另行检查,合法输入计时不能证明完整边界语义等价。
use axum::{extract::Query, http::Uri};
use dever_runtime::api::{self, Request};
use serde::{Deserialize, de::DeserializeOwned};
#[derive(Debug, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
struct QueryArguments {
name: String,
count: i64,
enabled: bool,
ratio: f64,
tag: Option<String>,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
struct WideArguments {
field0: String,
field1: String,
field2: String,
field3: String,
field4: String,
field5: String,
field6: String,
field7: String,
field8: String,
field9: String,
field10: String,
field11: String,
field12: String,
field13: String,
field14: String,
field15: String,
field16: String,
field17: String,
field18: String,
field19: String,
field20: String,
field21: String,
field22: String,
field23: String,
field24: String,
field25: String,
field26: String,
field27: String,
field28: String,
field29: String,
field30: String,
field31: String,
}
struct QueryInput {
request: Request,
uri: Uri,
}
fn query_fault(_: impl std::fmt::Display) -> Fault {
&FaultInfo {
message: "invalid query",
source: "query-comparison",
line: 1,
column: 1,
}
}
fn runtime_query<Arguments: DeserializeOwned>(
values: &[QueryInput],
) -> Result<Option<Arguments>, Fault> {
let Some(input) = values.first() else {
return Ok(None);
};
api::input(&input.request)
.map_err(query_fault)?
.decode(dever_runtime::wire::decode)
.map(Some)
.map_err(query_fault)
}
fn axum_query<Arguments: DeserializeOwned>(
values: &[QueryInput],
) -> Result<Option<Arguments>, Fault> {
let Some(input) = values.first() else {
return Ok(None);
};
Query::<Arguments>::try_from_uri(&input.uri)
.map(|Query(arguments)| Some(arguments))
.map_err(query_fault)
}
fn serde_query<Arguments: DeserializeOwned>(
values: &[QueryInput],
) -> Result<Option<Arguments>, Fault> {
let Some(input) = values.first() else {
return Ok(None);
};
serde_urlencoded::from_str(input.uri.query().unwrap_or_default())
.map(Some)
.map_err(query_fault)
}
fn query_input(query: &str) -> QueryInput {
let target = format!("/test?{query}");
QueryInput {
uri: target.parse().unwrap(),
request: axum::http::Request::builder()
.uri(target)
.body(Default::default())
.unwrap(),
}
}
fn query_comparison<Arguments: DeserializeOwned + PartialEq + std::fmt::Debug>(
name: &str,
query: &str,
count: usize,
) {
let input = [query_input(query)];
for (label, decoder) in [
("axum", axum_query::<Arguments> as fn(&[QueryInput]) -> _),
(
"serde_urlencoded",
serde_query::<Arguments> as fn(&[QueryInput]) -> _,
),
] {
compare_named(
&format!("query_{name}_{label}"),
&input,
count,
runtime_query::<Arguments>,
decoder,
|output| u64::from(output.is_some()),
label,
);
}
}
fn main() {
if std::env::args().any(|argument| argument == "--check") {
query_checks();
return;
}
println!("case,sample,implementation,elapsed_ns,checksum");
for (name, value, count) in [
("plain", "Alice".to_owned(), 20_000),
("unicode", "%E9%9B%AA+test".to_owned(), 20_000),
("escaped", "%22%5C%00%26%3D%25".repeat(8), 10_000),
("long", "value%2B%E9%9B%AA".repeat(256), 1_000),
] {
query_comparison::<QueryArguments>(
name,
&format!("name={value}&count=-7&enabled=true&ratio=1.25&tag=demo"),
count,
);
}
for (name, value, count) in [
("wide_plain", "testvalue".to_owned(), 2_000),
("wide_escaped", "value%2B%E9%9B%AA".repeat(8), 500),
] {
let query = (0..32)
.map(|index| format!("field{index}={value}"))
.collect::<Vec<_>>()
.join("&");
query_comparison::<WideArguments>(name, &query, count);
}
}