Skip to main content

galeon_engine/
handler_function.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use std::marker::PhantomData;
4
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7
8use crate::system_param::{Access, SystemParam};
9use crate::world::{UnsafeWorldCell, World};
10
11// =============================================================================
12// Handler trait — trait-object interface for all handler types
13// =============================================================================
14
15/// Trait-object interface for all handler types.
16///
17/// Parallel to [`System`][crate::function_system::System], but shaped for
18/// request/response invocation: the caller supplies a `Req` value and receives
19/// `Result<Resp, String>` back. SystemParams are injected from the ECS world
20/// just as they are in regular systems.
21pub trait Handler<Req, Resp> {
22    /// Human-readable handler name (for diagnostics and conflict messages).
23    fn name(&self) -> &'static str;
24
25    /// Run the handler with the given request against the world.
26    fn run(&mut self, request: Req, world: &mut World) -> Result<Resp, String>;
27
28    /// Declare what world data this handler accesses.
29    ///
30    /// Returns the union of all parameter accesses for parameterized handlers.
31    fn access(&self) -> Vec<Access>;
32}
33
34// =============================================================================
35// IntoHandler — converts a compatible function into a boxed Handler
36// =============================================================================
37
38/// Converts a compatible function into a boxed [`Handler`].
39///
40/// Implemented for parameterized functions `fn(Req, P0, P1, ...) -> Result<Resp, E>`
41/// where each `P` is a [`SystemParam`] and `E: ToString`. The error is
42/// converted to `String` at the bridge boundary. Parallel to
43/// [`IntoSystem`][crate::function_system::IntoSystem].
44pub trait IntoHandler<Req, Resp, Params> {
45    fn into_handler(self, name: &'static str) -> Box<dyn Handler<Req, Resp>>;
46}
47
48// =============================================================================
49// Parameterized handler — fn(Req, P0, P1, ...) where each P: SystemParam
50// =============================================================================
51
52/// Internal bridge trait. Parallel to `SystemParamFunction`.
53///
54/// Bridges an `FnMut(Req, P::Item<'_>, ...) -> Result<Resp, E>` (where
55/// `E: ToString`) to the `Handler::run` interface, converting errors to
56/// `String` at the boundary.
57pub(crate) trait HandlerParamFunction<Req, Resp, Params>: 'static {
58    fn run(&mut self, request: Req, world: &mut World) -> Result<Resp, String>;
59    fn param_access() -> Vec<Access>;
60}
61
62/// Wraps a [`HandlerParamFunction`] into a [`Handler`] trait object.
63/// Parallel to `ParamSystem`.
64struct ParamHandler<F, Req, Resp, Params> {
65    name: &'static str,
66    func: F,
67    _marker: PhantomData<fn(Req) -> (Resp, Params)>,
68}
69
70impl<F, Req, Resp, Params> Handler<Req, Resp> for ParamHandler<F, Req, Resp, Params>
71where
72    F: HandlerParamFunction<Req, Resp, Params>,
73{
74    fn name(&self) -> &'static str {
75        self.name
76    }
77
78    fn run(&mut self, request: Req, world: &mut World) -> Result<Resp, String> {
79        self.func.run(request, world)
80    }
81
82    fn access(&self) -> Vec<Access> {
83        F::param_access()
84    }
85}
86
87// =============================================================================
88// Conflict validation (T2)
89// =============================================================================
90
91/// Panics if any two accesses within the same handler conflict.
92fn validate_no_self_conflicts(access: &[Access], handler_name: &'static str) {
93    for (i, a) in access.iter().enumerate() {
94        for b in &access[i + 1..] {
95            if a.conflicts_with(b) {
96                panic!(
97                    "handler '{}' has conflicting parameter access: {:?} vs {:?}",
98                    handler_name, a, b,
99                );
100            }
101        }
102    }
103}
104
105// =============================================================================
106// run_handler — transport-neutral invocation entrypoint (T3)
107// =============================================================================
108
109/// Execute a handler against the world with the given request.
110///
111/// This is the transport-neutral invocation entrypoint. Transport adapters
112/// (axum routes, WASM bridge, etc.) call this to run a handler function
113/// with ECS parameter injection.
114///
115/// # Example
116///
117/// ```rust,ignore
118/// let mut handler = (|req: MyRequest| Ok(MyResponse { ok: true }))
119///     .into_handler("my_handler");
120/// let result = run_handler(&mut *handler, MyRequest { id: 1 }, &mut world);
121/// ```
122pub fn run_handler<Req, Resp>(
123    handler: &mut dyn Handler<Req, Resp>,
124    request: Req,
125    world: &mut World,
126) -> Result<Resp, String> {
127    handler.run(request, world)
128}
129
130// =============================================================================
131// JSON boundary — serde at the transport edge (#173)
132// =============================================================================
133
134/// Deserialize JSON, run a [`Handler`], serialize the response as JSON.
135///
136/// Transport adapters (for example generated axum routes) use this at the
137/// HTTP boundary while keeping handler execution on [`World`] via
138/// [`run_handler`].
139pub fn run_json_handler<Req, Resp>(
140    handler: &mut dyn Handler<Req, Resp>,
141    json_body: &str,
142    world: &mut World,
143) -> Result<String, String>
144where
145    Req: DeserializeOwned,
146    Resp: Serialize,
147{
148    let request: Req = serde_json::from_str(json_body).map_err(|e| e.to_string())?;
149    let response = run_handler(handler, request, world)?;
150    serde_json::to_string(&response).map_err(|e| e.to_string())
151}
152
153/// Deserialize JSON, run a [`Handler`], and return the response as [`serde_json::Value`].
154///
155/// Same as [`run_json_handler`], but avoids a serialize-then-parse round trip
156/// when the transport layer needs a JSON value (for example axum `Json<Value>`).
157pub fn run_json_handler_value<Req, Resp>(
158    handler: &mut dyn Handler<Req, Resp>,
159    json_body: &str,
160    world: &mut World,
161) -> Result<serde_json::Value, String>
162where
163    Req: DeserializeOwned,
164    Resp: Serialize,
165{
166    let request: Req = serde_json::from_str(json_body).map_err(|e| e.to_string())?;
167    let response = run_handler(handler, request, world)?;
168    serde_json::to_value(&response).map_err(|e| e.to_string())
169}
170
171/// JSON boundary helper for any function that implements [`IntoHandler`].
172///
173/// Builds a fresh boxed handler from `f` each call (same cost model as
174/// per-request `into_handler` in generated glue). When `f` is a concrete
175/// function item, `Req`, `Resp`, and `Params` are inferred from its type.
176///
177/// Generated axum routes use the `#[handler]`-emitted `{name}__galeon_axum_json`
178/// shim for handlers with ECS [`SystemParam`] extras, because bare
179/// `.into_handler(...)` on a function path often cannot infer `Params`.
180pub fn run_json_handler_function<F, Req, Resp, Params>(
181    f: F,
182    handler_name: &'static str,
183    json_body: &str,
184    world: &mut World,
185) -> Result<String, String>
186where
187    F: IntoHandler<Req, Resp, Params>,
188    Req: DeserializeOwned + 'static,
189    Resp: Serialize + 'static,
190{
191    let mut handler = f.into_handler(handler_name);
192    run_json_handler(&mut *handler, json_body, world)
193}
194
195// =============================================================================
196// Zero-param impl — fn(Req) -> Result<Resp, E> where E: ToString
197// =============================================================================
198
199impl<Func, Req, Resp, Err> HandlerParamFunction<Req, Resp, ()> for Func
200where
201    Func: FnMut(Req) -> Result<Resp, Err> + 'static,
202    Req: 'static,
203    Resp: 'static,
204    Err: ToString + 'static,
205{
206    fn run(&mut self, request: Req, _world: &mut World) -> Result<Resp, String> {
207        self(request).map_err(|e| e.to_string())
208    }
209
210    fn param_access() -> Vec<Access> {
211        Vec::new()
212    }
213}
214
215impl<Func, Req, Resp> IntoHandler<Req, Resp, ()> for Func
216where
217    Func: HandlerParamFunction<Req, Resp, ()>,
218    Req: 'static,
219    Resp: 'static,
220{
221    fn into_handler(self, name: &'static str) -> Box<dyn Handler<Req, Resp>> {
222        Box::new(ParamHandler {
223            name,
224            func: self,
225            _marker: PhantomData,
226        })
227    }
228}
229
230// =============================================================================
231// Arity macros — 1..8 parameter handlers
232// =============================================================================
233
234macro_rules! impl_handler_param_function {
235    ($($P:ident),+) => {
236        #[allow(non_snake_case)]
237        impl<Func, Req, Resp, Err, $($P: SystemParam + 'static),+> HandlerParamFunction<Req, Resp, ($($P,)+)> for Func
238        where
239            Func: FnMut(Req, $($P::Item<'_>),+) -> Result<Resp, Err> + 'static,
240            Req: 'static,
241            Resp: 'static,
242            Err: ToString + 'static,
243        {
244            fn run(&mut self, request: Req, world: &mut World) -> Result<Resp, String> {
245                // SAFETY: same justification as SystemParamFunction — conflict
246                // detection at registration ensures no two params access the
247                // same TypeId mutably. UnsafeWorldCell provides field-level
248                // access via addr_of!, so fetch() impls never create
249                // intermediate &World / &mut World references — only
250                // field-level references to `resources` or `archetypes`,
251                // which live in separate memory regions.
252                let cell = unsafe { UnsafeWorldCell::new(world as *mut World) };
253                unsafe { self(request, $($P::fetch(cell),)+) }.map_err(|e| e.to_string())
254            }
255
256            fn param_access() -> Vec<Access> {
257                let mut acc = Vec::new();
258                $(acc.extend($P::access());)+
259                acc
260            }
261        }
262
263        impl<Func, Req, Resp, $($P: SystemParam + 'static),+> IntoHandler<Req, Resp, ($($P,)+)> for Func
264        where
265            Func: HandlerParamFunction<Req, Resp, ($($P,)+)>,
266            Req: 'static,
267            Resp: 'static,
268        {
269            fn into_handler(self, name: &'static str) -> Box<dyn Handler<Req, Resp>> {
270                let access = <Func as HandlerParamFunction<Req, Resp, ($($P,)+)>>::param_access();
271                validate_no_self_conflicts(&access, name);
272                Box::new(ParamHandler {
273                    name,
274                    func: self,
275                    _marker: PhantomData,
276                })
277            }
278        }
279    };
280}
281
282impl_handler_param_function!(P0);
283impl_handler_param_function!(P0, P1);
284impl_handler_param_function!(P0, P1, P2);
285impl_handler_param_function!(P0, P1, P2, P3);
286impl_handler_param_function!(P0, P1, P2, P3, P4);
287impl_handler_param_function!(P0, P1, P2, P3, P4, P5);
288impl_handler_param_function!(P0, P1, P2, P3, P4, P5, P6);
289impl_handler_param_function!(P0, P1, P2, P3, P4, P5, P6, P7);
290
291// =============================================================================
292// Tests
293// =============================================================================
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use crate::component::Component;
299    use crate::system_param::{Query, QueryMut, Res, ResMut};
300    use serde::{Deserialize, Serialize};
301
302    // -------------------------------------------------------------------------
303    // Test types
304    // -------------------------------------------------------------------------
305
306    #[derive(Debug)]
307    struct Counter(u32);
308    impl Component for Counter {}
309
310    struct Config {
311        multiplier: f32,
312    }
313
314    #[derive(Debug, Deserialize)]
315    struct SpawnRequest {
316        unit_id: u64,
317    }
318
319    #[derive(Debug, PartialEq, Serialize, Deserialize)]
320    struct SpawnResponse {
321        ok: bool,
322    }
323
324    // -------------------------------------------------------------------------
325    // T4: Positive execution tests
326    // -------------------------------------------------------------------------
327
328    fn spawn_no_params(req: SpawnRequest) -> Result<SpawnResponse, String> {
329        Ok(SpawnResponse {
330            ok: req.unit_id > 0,
331        })
332    }
333
334    #[test]
335    fn zero_param_handler() {
336        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
337            IntoHandler::<SpawnRequest, SpawnResponse, ()>::into_handler(
338                spawn_no_params,
339                "zero_param",
340            );
341        let mut world = World::new();
342        let result = handler.run(SpawnRequest { unit_id: 1 }, &mut world);
343        assert_eq!(result.unwrap(), SpawnResponse { ok: true });
344    }
345
346    fn spawn_with_res(_req: SpawnRequest, cfg: Res<'_, Config>) -> Result<SpawnResponse, String> {
347        Ok(SpawnResponse {
348            ok: cfg.multiplier > 0.0,
349        })
350    }
351
352    #[test]
353    fn one_param_res_handler() {
354        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
355            IntoHandler::<SpawnRequest, SpawnResponse, (Res<'_, Config>,)>::into_handler(
356                spawn_with_res,
357                "one_param_res",
358            );
359        let mut world = World::new();
360        world.insert_resource(Config { multiplier: 2.0 });
361        let result = handler.run(SpawnRequest { unit_id: 1 }, &mut world);
362        assert_eq!(result.unwrap(), SpawnResponse { ok: true });
363    }
364
365    fn spawn_with_res_mut(
366        req: SpawnRequest,
367        mut cfg: ResMut<'_, Config>,
368    ) -> Result<SpawnResponse, String> {
369        cfg.multiplier = req.unit_id as f32;
370        Ok(SpawnResponse { ok: true })
371    }
372
373    #[test]
374    fn one_param_res_mut_handler() {
375        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
376            IntoHandler::<SpawnRequest, SpawnResponse, (ResMut<'_, Config>,)>::into_handler(
377                spawn_with_res_mut,
378                "one_param_res_mut",
379            );
380        let mut world = World::new();
381        world.insert_resource(Config { multiplier: 0.0 });
382        let result = handler.run(SpawnRequest { unit_id: 42 }, &mut world);
383        assert_eq!(result.unwrap(), SpawnResponse { ok: true });
384        assert!((world.resource::<Config>().multiplier - 42.0).abs() < f32::EPSILON);
385    }
386
387    fn spawn_with_query(
388        req: SpawnRequest,
389        counters: Query<'_, Counter>,
390    ) -> Result<SpawnResponse, String> {
391        Ok(SpawnResponse {
392            ok: counters.len() as u64 == req.unit_id,
393        })
394    }
395
396    #[test]
397    fn one_param_query_handler() {
398        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
399            IntoHandler::<SpawnRequest, SpawnResponse, (Query<'_, Counter>,)>::into_handler(
400                spawn_with_query,
401                "one_param_query",
402            );
403        let mut world = World::new();
404        world.spawn((Counter(10),));
405        world.spawn((Counter(20),));
406        let result = handler.run(SpawnRequest { unit_id: 2 }, &mut world);
407        assert_eq!(result.unwrap(), SpawnResponse { ok: true });
408    }
409
410    fn spawn_with_query_mut(
411        req: SpawnRequest,
412        mut counters: QueryMut<'_, Counter>,
413    ) -> Result<SpawnResponse, String> {
414        for (_, c) in counters.iter_mut() {
415            c.0 += req.unit_id as u32;
416        }
417        Ok(SpawnResponse { ok: true })
418    }
419
420    #[test]
421    fn one_param_query_mut_handler() {
422        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
423            IntoHandler::<SpawnRequest, SpawnResponse, (QueryMut<'_, Counter>,)>::into_handler(
424                spawn_with_query_mut,
425                "one_param_query_mut",
426            );
427        let mut world = World::new();
428        world.spawn((Counter(0),));
429        world.spawn((Counter(5),));
430        let result = handler.run(SpawnRequest { unit_id: 10 }, &mut world);
431        assert_eq!(result.unwrap(), SpawnResponse { ok: true });
432        let mut vals: Vec<u32> = world.query::<&Counter>().map(|(_, c)| c.0).collect();
433        vals.sort();
434        assert_eq!(vals, vec![10, 15]);
435    }
436
437    fn spawn_two_params(
438        req: SpawnRequest,
439        cfg: Res<'_, Config>,
440        counters: Query<'_, Counter>,
441    ) -> Result<SpawnResponse, String> {
442        Ok(SpawnResponse {
443            ok: cfg.multiplier > 0.0 && counters.len() as u64 == req.unit_id,
444        })
445    }
446
447    #[test]
448    fn two_param_handler() {
449        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> = IntoHandler::<
450            SpawnRequest,
451            SpawnResponse,
452            (Res<'_, Config>, Query<'_, Counter>),
453        >::into_handler(
454            spawn_two_params,
455            "two_param",
456        );
457        let mut world = World::new();
458        world.insert_resource(Config { multiplier: 1.5 });
459        world.spawn((Counter(0),));
460        let result = handler.run(SpawnRequest { unit_id: 1 }, &mut world);
461        assert_eq!(result.unwrap(), SpawnResponse { ok: true });
462    }
463
464    #[test]
465    fn handler_reports_access() {
466        let handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
467            IntoHandler::<SpawnRequest, SpawnResponse, (Res<'_, Config>,)>::into_handler(
468                spawn_with_res,
469                "access_check",
470            );
471        let access = handler.access();
472        assert_eq!(access.len(), 1);
473        assert!(matches!(access[0], Access::ResRead(_)));
474    }
475
476    #[test]
477    fn run_handler_free_function() {
478        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
479            IntoHandler::<SpawnRequest, SpawnResponse, ()>::into_handler(
480                spawn_no_params,
481                "free_fn",
482            );
483        let mut world = World::new();
484        let result = run_handler(&mut *handler, SpawnRequest { unit_id: 99 }, &mut world);
485        assert_eq!(result.unwrap(), SpawnResponse { ok: true });
486    }
487
488    #[test]
489    fn run_json_handler_round_trip() {
490        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
491            IntoHandler::<SpawnRequest, SpawnResponse, ()>::into_handler(
492                spawn_no_params,
493                "json_round_trip",
494            );
495        let mut world = World::new();
496        let out = run_json_handler(&mut *handler, r#"{"unit_id":3}"#, &mut world).unwrap();
497        assert_eq!(out, r#"{"ok":true}"#);
498    }
499
500    #[test]
501    fn run_json_handler_function_infers_types() {
502        let mut world = World::new();
503        let out =
504            run_json_handler_function(spawn_no_params, "json_fn", r#"{"unit_id":5}"#, &mut world)
505                .unwrap();
506        assert_eq!(out, r#"{"ok":true}"#);
507    }
508
509    fn json_round_trip_res_mut(
510        req: SpawnRequest,
511        mut cfg: ResMut<'_, Config>,
512    ) -> Result<SpawnResponse, String> {
513        cfg.multiplier = req.unit_id as f32;
514        Ok(SpawnResponse { ok: true })
515    }
516
517    #[test]
518    fn run_json_handler_res_mut_round_trip() {
519        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
520            IntoHandler::<SpawnRequest, SpawnResponse, (ResMut<'_, Config>,)>::into_handler(
521                json_round_trip_res_mut,
522                "res_mut_json",
523            );
524        let mut world = World::new();
525        world.insert_resource(Config { multiplier: 0.0 });
526        let out = run_json_handler(&mut *handler, r#"{"unit_id":7}"#, &mut world).unwrap();
527        assert_eq!(out, r#"{"ok":true}"#);
528        assert!((world.resource::<Config>().multiplier - 7.0).abs() < f32::EPSILON);
529    }
530
531    #[test]
532    fn run_json_handler_rejects_bad_json() {
533        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
534            IntoHandler::<SpawnRequest, SpawnResponse, ()>::into_handler(
535                spawn_no_params,
536                "bad_json",
537            );
538        let mut world = World::new();
539        let err = run_json_handler(&mut *handler, "not json", &mut world).unwrap_err();
540        assert!(!err.is_empty());
541    }
542
543    // -------------------------------------------------------------------------
544    // T5: Negative tests
545    // -------------------------------------------------------------------------
546
547    fn conflicting_handler(
548        _req: SpawnRequest,
549        _a: Res<'_, Config>,
550        _b: ResMut<'_, Config>,
551    ) -> Result<SpawnResponse, String> {
552        Ok(SpawnResponse { ok: true })
553    }
554
555    #[test]
556    #[should_panic(expected = "conflicting parameter access")]
557    fn self_conflict_panics_on_registration() {
558        let _ = IntoHandler::<
559            SpawnRequest,
560            SpawnResponse,
561            (Res<'_, Config>, ResMut<'_, Config>),
562        >::into_handler(conflicting_handler, "conflict");
563    }
564
565    fn failing_handler(_req: SpawnRequest) -> Result<SpawnResponse, String> {
566        Err("some error".into())
567    }
568
569    #[test]
570    fn handler_error_propagates() {
571        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
572            IntoHandler::<SpawnRequest, SpawnResponse, ()>::into_handler(
573                failing_handler,
574                "error_handler",
575            );
576        let mut world = World::new();
577        let result = handler.run(SpawnRequest { unit_id: 1 }, &mut world);
578        assert!(result.is_err());
579        assert_eq!(result.unwrap_err(), "some error");
580    }
581
582    // -- Domain error type tests --
583
584    #[derive(Debug)]
585    struct ApiError {
586        code: u16,
587        message: String,
588    }
589
590    impl std::fmt::Display for ApiError {
591        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
592            write!(f, "ApiError({}): {}", self.code, self.message)
593        }
594    }
595
596    fn handler_with_domain_error(_req: SpawnRequest) -> Result<SpawnResponse, ApiError> {
597        Err(ApiError {
598            code: 404,
599            message: "not found".into(),
600        })
601    }
602
603    #[test]
604    fn domain_error_type_converts_to_string() {
605        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
606            IntoHandler::<SpawnRequest, SpawnResponse, ()>::into_handler(
607                handler_with_domain_error,
608                "domain_error",
609            );
610        let mut world = World::new();
611        let result = handler.run(SpawnRequest { unit_id: 1 }, &mut world);
612        assert!(result.is_err());
613        assert_eq!(result.unwrap_err(), "ApiError(404): not found");
614    }
615
616    fn handler_with_domain_error_and_params(
617        _req: SpawnRequest,
618        _cfg: Res<'_, Config>,
619    ) -> Result<SpawnResponse, ApiError> {
620        Err(ApiError {
621            code: 500,
622            message: "internal".into(),
623        })
624    }
625
626    #[test]
627    fn domain_error_type_with_params() {
628        let mut handler: Box<dyn Handler<SpawnRequest, SpawnResponse>> =
629            IntoHandler::<SpawnRequest, SpawnResponse, (Res<'_, Config>,)>::into_handler(
630                handler_with_domain_error_and_params,
631                "domain_error_params",
632            );
633        let mut world = World::new();
634        world.insert_resource(Config { multiplier: 1.0 });
635        let result = handler.run(SpawnRequest { unit_id: 1 }, &mut world);
636        assert!(result.is_err());
637        assert_eq!(result.unwrap_err(), "ApiError(500): internal");
638    }
639}