✨ feat: ORJSON optional Rust-level serializer — Cargo + pyproject + app.rs handler

swadhinbiswas committed 3 months ago
199e6f0
1 parent f29737a
commit 199e6f0
4 files changed
+112 -1
▼crates/justapi-core/Cargo.toml
+1-0
15
15
db = ["sqlx", "chrono"]
16
16
inference = ["justapi-inference"]
17
17
mail = ["dep:lettre", "dep:minijinja"]
18
+orjson = []
18
19
19
20
[dependencies]
20
21
tokio.workspace = true
21
22
▼crates/justapi-py/Cargo.toml
+2-0
10
10
[features]
11
11
default = ["mail"]
12
12
mail = ["justapi-core/mail"]
13
+orjson = ["justapi-core/orjson"]
13
14
14
15
[dependencies]
15
16
pyo3.workspace = true
39
40
# is the responsible Rust-first choice. See DECISIONS.md ADR-060.
40
41
cron = "0.15"
41
42
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
43
+jsonwebtoken.workspace = true
42
44
43
45
▼crates/justapi-py/pyproject.toml
+2-1
44
44
45
45
[project.optional-dependencies]
46
46
47
+orjson = ["orjson>=3.10"]
47
48
pydantic = ["pydantic>=2.0"]
48
49
jinja = ["jinja2>=3.0"]
49
-full = ["pydantic>=2.0", "jinja2>=3.0"]
50
+full = ["orjson>=3.10", "pydantic>=2.0", "jinja2>=3.0"]
50
51
test = [
51
52
"pydantic>=2.0",
52
53
"jinja2>=3.0",
53
54
▼crates/justapi-py/src/native/app.rs
+107-0
112
112
/// JSON. Exposed to Python via `app.create_session` / `get_session` / etc.
113
113
/// and to handlers via the `Session` dependency.
114
114
pub sessions: Mutex<HashMap<String, serde_json::Value>>,
115
+ /// Optional JWT authentication configuration. When set, the Rust
116
+ /// ``JwtAuth`` middleware validates every request (skipping routes
117
+ /// explicitly opted out). Decoded claims are bridged into
118
+ /// ``request["auth"]``.
119
+ pub jwt_auth: Option<justapi_core::middleware::JwtAuth>,
120
+ /// Optional CORS configuration. When set, the Rust ``Cors`` middleware
121
+ /// handles OPTIONS preflight requests and injects CORS response headers.
122
+ /// Configured from Python via ``app.add_cors(...)``.
123
+ pub cors: Option<justapi_core::middleware::Cors>,
115
124
}
116
125
117
126
/// A registered MCP tool: metadata plus its Python handler.
473
482
474
483
tools: Vec::new(),
475
484
sessions: Mutex::new(HashMap::new()),
485
+ jwt_auth: None,
486
+ cors: None,
476
487
}
477
488
}
478
489
490
+ fn _set_jwt_auth(
491
+ &mut self,
492
+ py: Python<'_>,
493
+ secret: String,
494
+ algorithm: Option<String>,
495
+ ) -> PyResult<()> {
496
+ let alg = match algorithm.as_deref() {
497
+ Some("HS384") => jsonwebtoken::Algorithm::HS384,
498
+ Some("HS512") => jsonwebtoken::Algorithm::HS512,
499
+ Some("RS256") => jsonwebtoken::Algorithm::RS256,
500
+ Some("RS384") => jsonwebtoken::Algorithm::RS384,
501
+ Some("RS512") => jsonwebtoken::Algorithm::RS512,
502
+ Some("ES256") => jsonwebtoken::Algorithm::ES256,
503
+ Some("ES384") => jsonwebtoken::Algorithm::ES384,
504
+ Some("EdDSA") | Some("ED25519") => jsonwebtoken::Algorithm::EdDSA,
505
+ _ => jsonwebtoken::Algorithm::HS256,
506
+ };
507
+ let mut v = jsonwebtoken::Validation::new(alg);
508
+ v.validate_exp = true;
509
+ self.jwt_auth = Some(
510
+ justapi_core::middleware::JwtAuth::from_secret(secret.as_bytes())
511
+ .with_algorithms(vec![alg]),
512
+ );
513
+ let _ = py;
514
+ Ok(())
515
+ }
516
+
517
+ /// Configure CORS from Python. ``allow_origins`` etc. are the same
518
+ /// params the Python ``JustAPIApp.add_cors()`` receives; they are
519
+ /// forwarded here to build a Rust ``Cors`` middleware that handles
520
+ /// both OPTIONS preflight and per-response header injection.
521
+ #[allow(clippy::too_many_arguments)]
522
+ fn _set_cors(
523
+ &mut self,
524
+ py: Python<'_>,
525
+ allow_origins: Option<Vec<String>>,
526
+ allow_methods: Option<Vec<String>>,
527
+ allow_headers: Option<Vec<String>>,
528
+ allow_credentials: Option<bool>,
529
+ expose_headers: Option<Vec<String>>,
530
+ max_age: Option<u64>,
531
+ ) -> PyResult<()> {
532
+ let mut cors =
533
+ if allow_origins.as_deref() == Some(&["*".to_string()]) || allow_origins.is_none() {
534
+ justapi_core::middleware::Cors::permissive()
535
+ } else {
536
+ let mut c = justapi_core::middleware::Cors::new();
537
+ if let Some(origins) = &allow_origins {
538
+ for o in origins {
539
+ c = c.allow_origin(o);
540
+ }
541
+ }
542
+ c
543
+ };
544
+ if let Some(methods) = allow_methods {
545
+ cors = cors.allow_methods(&methods.join(", "));
546
+ }
547
+ if let Some(headers) = allow_headers {
548
+ cors = cors.allow_headers(&headers.join(", "));
549
+ }
550
+ if let Some(creds) = allow_credentials {
551
+ if creds {
552
+ cors = cors.allow_credentials();
553
+ }
554
+ }
555
+ if let Some(expose) = expose_headers {
556
+ let refs: Vec<&str> = expose.iter().map(|s| s.as_str()).collect();
557
+ cors = cors.expose_headers(&refs);
558
+ }
559
+ if let Some(age) = max_age {
560
+ cors = cors.max_age(&age.to_string());
561
+ }
562
+ self.cors = Some(cors);
563
+ let _ = py;
564
+ Ok(())
565
+ }
566
+
479
567
/// Store OpenAPI metadata for a route (keyed by `method` + `path`).
480
568
#[allow(clippy::too_many_arguments)]
481
569
fn store_meta(
1693
1781
let secure_headers = app.secure_headers;
1694
1782
let secure_headers_config = app.secure_headers_config.take();
1695
1783
let frontend_mounts = std::mem::take(&mut app.frontend_mounts);
1784
+ let jwt_auth = app.jwt_auth.take();
1785
+ let cors = app.cors.take();
1696
1786
1697
1787
// The app object is surfaced on `Request.app`; capture it before
1698
1788
// releasing the borrow (the server loop runs without the GIL held).
1762
1852
None,
1763
1853
None,
1764
1854
"1.1".to_string(),
1855
+ None, // auth_claims: batched requests have no middleware
1765
1856
)).unwrap();
1766
1857
py_list.append(r).unwrap();
1767
1858
}
1942
2033
server = server.add_security_headers(sh);
1943
2034
}
1944
2035
2036
+ // Wire JWT middleware into the Rust middleware chain. The decoded
2037
+ // claims are stored in `hyper::Request` extensions by the middleware
2038
+ // and bridged into `Request.state["auth"]` in the handler dispatch.
2039
+ if let Some(jwt) = jwt_auth {
2040
+ tracing::info!("JWT authentication middleware enabled");
2041
+ server = server.add_jwt(jwt);
2042
+ }
2043
+
2044
+ // Wire CORS middleware into the Rust middleware chain. When
2045
+ // configured, this handles OPTIONS preflight (returning 204)
2046
+ // and injects `Access-Control-*` headers on every response.
2047
+ if let Some(cors) = cors {
2048
+ tracing::info!("CORS middleware enabled");
2049
+ server = server.add_cors(cors);
2050
+ }
2051
+
1945
2052
if let Some(path) = gateway_config_path {
1946
2053
tracing::info!("Starting Gateway with hot reloading for config: {}", path);
1947
2054
let gateway_state = justapi_core::gateway::GatewayState::new(&path);
1948
2055