✨ 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
Showing 4 changed files+112-1
▼crates/justapi-core/Cargo.toml
+1-0
| @@ -15,6 +15,7 @@ opentelemetry = ["dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetr | ||
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,6 +10,7 @@ crate-type = ["cdylib", "lib"] | ||
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,4 +40,5 @@ uuid = { version = "1", features = ["v4"] } | ||
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,9 +44,10 @@ justapi = "justapi.cli:main" | ||
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,6 +112,15 @@ pub struct JustAPIApp { | ||
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,9 +482,88 @@ impl JustAPIApp { | ||
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,6 +1781,8 @@ impl JustAPIApp { | ||
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,6 +1852,7 @@ impl JustAPIApp { | ||
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,6 +2033,22 @@ impl JustAPIApp { | ||
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 | |