Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user