Skip to content

Commit 9cd3064

Browse files
iWhattykanarus
andauthored
fix(router): preserve fang order (#655)
* fix(router): preserve fangs order during router merging by explicit inner/outer control on fangs application --------- Co-authored-by: kanarus <mail@kanarus.dev>
1 parent d748f9e commit 9cd3064

3 files changed

Lines changed: 206 additions & 33 deletions

File tree

ohkami/src/ohkami/mod.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,3 +1266,182 @@ mod test {
12661266
is_send_sync_static(o);
12671267
}
12681268
}
1269+
1270+
#[cfg(test)]
1271+
#[cfg(feature = "__rt_native__")]
1272+
mod nested_fang_regression_test {
1273+
use crate::claw::status;
1274+
use crate::fang::{Context, FangAction};
1275+
use crate::testing::{Status, TestRequest, Tester};
1276+
use crate::{Ohkami, Request, Response, Route};
1277+
1278+
#[derive(Clone, Debug, PartialEq, Eq)]
1279+
struct Principal(&'static str);
1280+
1281+
#[derive(Clone)]
1282+
struct ParentAuthFang;
1283+
1284+
impl FangAction for ParentAuthFang {
1285+
async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> {
1286+
match req.headers.authorization() {
1287+
Some("Bearer ops-token") => {
1288+
req.context.set(Principal("ops-user"));
1289+
Ok(())
1290+
}
1291+
_ => Err(Response::Unauthorized()),
1292+
}
1293+
}
1294+
}
1295+
1296+
#[derive(Clone)]
1297+
struct OpsAuthorizationFang;
1298+
1299+
impl FangAction for OpsAuthorizationFang {
1300+
async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> {
1301+
match req.context.get::<Principal>() {
1302+
Some(Principal("ops-user")) => Ok(()),
1303+
_ => Err(Response::Unauthorized()),
1304+
}
1305+
}
1306+
}
1307+
1308+
async fn routing_health_handler() -> &'static str {
1309+
"health"
1310+
}
1311+
1312+
async fn routing_override_set_handler() -> &'static str {
1313+
"set"
1314+
}
1315+
1316+
async fn routing_override_clear_handler() -> status::NoContent {
1317+
status::NoContent
1318+
}
1319+
1320+
async fn metrics_handler() -> &'static str {
1321+
"metrics"
1322+
}
1323+
1324+
async fn accounting_reconciliation_handler() -> &'static str {
1325+
"reconciliation"
1326+
}
1327+
1328+
#[test]
1329+
fn parent_context_auth_is_visible_to_nested_top_level_fang_in_realistic_order() {
1330+
crate::__rt__::testing::block_on(async {
1331+
let ops_routes = Ohkami::new((
1332+
OpsAuthorizationFang,
1333+
"/routing/health".GET(routing_health_handler),
1334+
"/routing/override".POST(routing_override_set_handler),
1335+
"/routing/override".DELETE(routing_override_clear_handler),
1336+
"/metrics".GET(metrics_handler),
1337+
"/accounting/reconciliation".GET(accounting_reconciliation_handler),
1338+
));
1339+
1340+
let protected_routes = Ohkami::new((ParentAuthFang, "/ops".By(ops_routes)));
1341+
1342+
let app = Ohkami::new((Context::new(()), "/api".By(protected_routes)));
1343+
1344+
let tester = app.test();
1345+
1346+
let health_res = tester
1347+
.oneshot(
1348+
TestRequest::GET("/api/ops/routing/health")
1349+
.header("Authorization", "Bearer ops-token"),
1350+
)
1351+
.await;
1352+
assert_eq!(health_res.status(), Status::OK);
1353+
1354+
let set_override_res = tester
1355+
.oneshot(
1356+
TestRequest::POST("/api/ops/routing/override")
1357+
.header("Authorization", "Bearer ops-token"),
1358+
)
1359+
.await;
1360+
assert_eq!(set_override_res.status(), Status::OK);
1361+
1362+
let clear_override_res = tester
1363+
.oneshot(
1364+
TestRequest::DELETE("/api/ops/routing/override")
1365+
.header("Authorization", "Bearer ops-token"),
1366+
)
1367+
.await;
1368+
assert_eq!(clear_override_res.status(), Status::NoContent);
1369+
1370+
let metrics_res = tester
1371+
.oneshot(
1372+
TestRequest::GET("/api/ops/metrics")
1373+
.header("Authorization", "Bearer ops-token"),
1374+
)
1375+
.await;
1376+
assert_eq!(metrics_res.status(), Status::OK);
1377+
1378+
let accounting_res = tester
1379+
.oneshot(
1380+
TestRequest::GET("/api/ops/accounting/reconciliation")
1381+
.header("Authorization", "Bearer ops-token"),
1382+
)
1383+
.await;
1384+
assert_eq!(accounting_res.status(), Status::OK);
1385+
});
1386+
}
1387+
1388+
#[test]
1389+
fn parent_context_auth_is_visible_to_nested_local_route_fangs_in_realistic_order() {
1390+
crate::__rt__::testing::block_on(async {
1391+
let ops_routes = Ohkami::new((
1392+
"/routing/health".GET((OpsAuthorizationFang, routing_health_handler)),
1393+
"/routing/override".POST((OpsAuthorizationFang, routing_override_set_handler)),
1394+
"/routing/override".DELETE((OpsAuthorizationFang, routing_override_clear_handler)),
1395+
"/metrics".GET((OpsAuthorizationFang, metrics_handler)),
1396+
"/accounting/reconciliation"
1397+
.GET((OpsAuthorizationFang, accounting_reconciliation_handler)),
1398+
));
1399+
1400+
let protected_routes = Ohkami::new((ParentAuthFang, "/ops".By(ops_routes)));
1401+
1402+
let app = Ohkami::new((Context::new(()), "/api".By(protected_routes)));
1403+
1404+
let tester = app.test();
1405+
1406+
let health_res = tester
1407+
.oneshot(
1408+
TestRequest::GET("/api/ops/routing/health")
1409+
.header("Authorization", "Bearer ops-token"),
1410+
)
1411+
.await;
1412+
assert_eq!(health_res.status(), Status::OK);
1413+
1414+
let set_override_res = tester
1415+
.oneshot(
1416+
TestRequest::POST("/api/ops/routing/override")
1417+
.header("Authorization", "Bearer ops-token"),
1418+
)
1419+
.await;
1420+
assert_eq!(set_override_res.status(), Status::OK);
1421+
1422+
let clear_override_res = tester
1423+
.oneshot(
1424+
TestRequest::DELETE("/api/ops/routing/override")
1425+
.header("Authorization", "Bearer ops-token"),
1426+
)
1427+
.await;
1428+
assert_eq!(clear_override_res.status(), Status::NoContent);
1429+
1430+
let metrics_res = tester
1431+
.oneshot(
1432+
TestRequest::GET("/api/ops/metrics")
1433+
.header("Authorization", "Bearer ops-token"),
1434+
)
1435+
.await;
1436+
assert_eq!(metrics_res.status(), Status::OK);
1437+
1438+
let accounting_res = tester
1439+
.oneshot(
1440+
TestRequest::GET("/api/ops/accounting/reconciliation")
1441+
.header("Authorization", "Bearer ops-token"),
1442+
)
1443+
.await;
1444+
assert_eq!(accounting_res.status(), Status::OK);
1445+
});
1446+
}
1447+
}

ohkami/src/router/base.rs

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -77,44 +77,42 @@ impl FangsList {
7777
Self(Vec::new())
7878
}
7979

80-
fn add(&mut self, id: ID, fangs: Arc<dyn Fangs>) {
80+
fn add_inner(&mut self, id: ID, fangs: Arc<dyn Fangs>) {
8181
if self.0.iter().all(|(_id, _)| *_id != id) {
8282
self.0.push((id, fangs));
8383
}
8484
}
85-
pub(super) fn append(&mut self, another: Self) {
86-
for (id, fangs) in another.0.into_iter() {
87-
self.add(id, fangs)
85+
fn add_outer(&mut self, id: ID, fangs: Arc<dyn Fangs>) {
86+
if self.0.iter().all(|(_id, _)| *_id != id) {
87+
self.0.insert(0, (id, fangs));
8888
}
8989
}
90-
91-
/// yield from most inner fangs
92-
fn into_iter(self) -> impl Iterator<Item = Arc<dyn Fangs>> {
93-
self.0.into_iter().map(|(_, fangs)| fangs)
90+
pub(super) fn append_inner(&mut self, another: Self) {
91+
for (id, fangs) in another.0.into_iter() {
92+
self.add_inner(id, fangs);
93+
}
9494
}
9595

9696
pub(super) fn into_proc_with(self, h: Handler) -> IntoProcWith {
97-
let mut iter = self.into_iter();
98-
9997
#[cfg(not(feature = "openapi"))]
100-
match iter.next() {
101-
None => h.proc,
102-
Some(most_inner) => {
103-
iter.fold(most_inner.build(h.proc), |proc, fangs| fangs.build(proc))
104-
}
98+
{
99+
self.0
100+
.into_iter()
101+
.rfold(h.proc, |proc, (_, most_inner_fangs)| {
102+
most_inner_fangs.build(proc)
103+
})
105104
}
106105
#[cfg(feature = "openapi")]
107-
match iter.next() {
108-
None => (h.proc, h.openapi_operation),
109-
Some(most_inner) => iter.fold(
110-
(
111-
most_inner.build(h.proc),
112-
most_inner.openapi_map_operation(h.openapi_operation),
113-
),
114-
|(proc, operation), fangs| {
115-
(fangs.build(proc), fangs.openapi_map_operation(operation))
106+
{
107+
self.0.into_iter().rfold(
108+
(h.proc, h.openapi_operation),
109+
|(proc, op), (_, most_inner_fangs)| {
110+
(
111+
most_inner_fangs.build(proc),
112+
most_inner_fangs.openapi_map_operation(op),
113+
)
116114
},
117-
),
115+
)
118116
}
119117
}
120118
}
@@ -362,10 +360,6 @@ impl Node {
362360
}
363361
}
364362

365-
fn append_fangs(&mut self, fangs: FangsList) {
366-
self.fangses.append(fangs);
367-
}
368-
369363
fn set_handler(&mut self, new_handler: Handler, allow_override: bool) -> Result<(), String> {
370364
if self.handler.is_some() && !allow_override {
371365
return Err(format!("Conflicting handler registering"));
@@ -414,7 +408,7 @@ impl Node {
414408
panic!("Unexpectedly called `Node::merge_here` where `another_root` is not root node")
415409
};
416410

417-
self.append_fangs(another_root_fangses);
411+
self.fangses.append_inner(another_root_fangses);
418412

419413
if let Some(h) = another_root_handler {
420414
self.set_handler(h, allow_override_handler)?;
@@ -432,10 +426,10 @@ impl Node {
432426
for child in &mut self.children {
433427
child.apply_fangs(id, fangs.clone())
434428
}
435-
436429
// Add even when `self.handler.is_none()`. They are used later
437430
// for applying to `Handler::default_notfound`s in `finalize`.
438-
self.fangses.add(id, fangs);
431+
// This `fangses` must be added by `_outer` to *wrap* existing fangs.
432+
self.fangses.add_outer(id, fangs);
439433
}
440434
}
441435

ohkami/src/router/final.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ const _: (/* conversions */) = {
287287
let child = base.children.pop().unwrap(/* base.children.len() == 1 */);
288288
base.children = child.children;
289289
base.handler = child.handler;
290-
base.fangses.append(child.fangses);
290+
base.fangses.append_inner(child.fangses);
291291
base.pattern = Some(match base.pattern {
292292
None => child.pattern.unwrap(/* not root */),
293293
Some(p) => p.merge_statics(child.pattern.unwrap(/* not root */)).unwrap(/* both are Pattern::Static */)

0 commit comments

Comments
 (0)