AFI parity: close all 35 gaps — every adapter wires every AFI-common capability
The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
190
cores/mizan-rust/src/manifest.rs
Normal file
190
cores/mizan-rust/src/manifest.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
//! Edge manifest — the static JSON that Mizan Edge reads to configure CDN
|
||||
//! cache rules + invalidation routing.
|
||||
//!
|
||||
//! Mirrors `backends/mizan-django/src/mizan/export/__init__.py`'s
|
||||
//! `generate_edge_manifest`: a `{version, contexts, mutations}` document where
|
||||
//! each context carries its functions, endpoints, params, `user_scoped`, and
|
||||
//! `render_strategy` (the PSR axis), and each mutation carries its `affects`
|
||||
//! and `auto_scoped_params`. Keys are emitted alphabetically (the Django
|
||||
//! command serializes with `sort_keys=True`); `to_json_string` matches that.
|
||||
|
||||
use crate::registry::{context_members, CONTEXTS, FUNCTIONS};
|
||||
use crate::traits::FunctionSpec;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Params that imply a user-scoped context → `render_strategy:
|
||||
/// "dynamic_cached"`. Anything else renders as `"psr"`. Matches Python's
|
||||
/// `_USER_SCOPED_PARAMS`.
|
||||
const USER_SCOPED_PARAMS: [&str; 4] = ["user_id", "user", "owner_id", "account_id"];
|
||||
|
||||
/// Build the edge manifest as a `serde_json::Value`. `base_url` is the Mizan
|
||||
/// mount point (default `/api/mizan`).
|
||||
pub fn generate_edge_manifest(base_url: &str) -> Value {
|
||||
let mut contexts = Map::new();
|
||||
|
||||
// Contexts, alphabetical by name (BTreeSet over the registered names).
|
||||
let ctx_names: BTreeSet<&'static str> = CONTEXTS.iter().map(|c| c.name).collect();
|
||||
for ctx_name in &ctx_names {
|
||||
let members = context_members(ctx_name);
|
||||
if members.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut param_names: BTreeSet<&'static str> = BTreeSet::new();
|
||||
let mut functions_meta: Vec<Value> = Vec::new();
|
||||
let mut page_routes: Vec<String> = Vec::new();
|
||||
|
||||
for fn_spec in &members {
|
||||
for p in fn_spec.input_params() {
|
||||
param_names.insert(p.name);
|
||||
}
|
||||
// The Rust IR has no view-path/route metadata yet; every function
|
||||
// is an RPC path. (`route`/`view_path` land with the view-path
|
||||
// macro extension.)
|
||||
functions_meta.push(json!({ "name": fn_spec.name(), "path": "rpc" }));
|
||||
}
|
||||
|
||||
let user_scoped = param_names
|
||||
.iter()
|
||||
.any(|p| USER_SCOPED_PARAMS.contains(p));
|
||||
|
||||
let mut ctx_entry = Map::new();
|
||||
ctx_entry.insert("functions".into(), Value::Array(functions_meta));
|
||||
ctx_entry.insert(
|
||||
"endpoints".into(),
|
||||
json!([format!("{base_url}/ctx/{ctx_name}/")]),
|
||||
);
|
||||
ctx_entry.insert(
|
||||
"params".into(),
|
||||
Value::Array(
|
||||
param_names
|
||||
.iter()
|
||||
.map(|p| Value::String((*p).to_string()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
ctx_entry.insert("user_scoped".into(), Value::Bool(user_scoped));
|
||||
ctx_entry.insert(
|
||||
"render_strategy".into(),
|
||||
Value::String(
|
||||
if user_scoped {
|
||||
"dynamic_cached"
|
||||
} else {
|
||||
"psr"
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
if !page_routes.is_empty() {
|
||||
page_routes.sort();
|
||||
ctx_entry.insert(
|
||||
"page_routes".into(),
|
||||
Value::Array(page_routes.into_iter().map(Value::String).collect()),
|
||||
);
|
||||
}
|
||||
|
||||
contexts.insert((*ctx_name).to_string(), Value::Object(ctx_entry));
|
||||
}
|
||||
|
||||
// Mutations — every non-private function declaring `affects`, alphabetical.
|
||||
let mut fns: Vec<&'static dyn FunctionSpec> = FUNCTIONS.iter().copied().collect();
|
||||
fns.sort_by_key(|f| f.name());
|
||||
|
||||
let mut mutations = Map::new();
|
||||
for fn_spec in &fns {
|
||||
let affected: BTreeSet<&'static str> = fn_spec
|
||||
.affects()
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
crate::ir::AffectTarget::Context(name) => Some(*name),
|
||||
crate::ir::AffectTarget::Function { context, .. } => *context,
|
||||
})
|
||||
.collect();
|
||||
if affected.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut mutation = Map::new();
|
||||
mutation.insert(
|
||||
"affects".into(),
|
||||
Value::Array(
|
||||
affected
|
||||
.iter()
|
||||
.map(|c| Value::String((*c).to_string()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
|
||||
// Auto-scoped params: this mutation's params that also name a param of
|
||||
// an affected context.
|
||||
let fn_params: BTreeSet<&'static str> =
|
||||
fn_spec.input_params().iter().map(|p| p.name).collect();
|
||||
let mut auto_scoped: BTreeSet<&'static str> = BTreeSet::new();
|
||||
for ctx in &affected {
|
||||
let mut ctx_params: BTreeSet<&'static str> = BTreeSet::new();
|
||||
for m in context_members(ctx) {
|
||||
for p in m.input_params() {
|
||||
ctx_params.insert(p.name);
|
||||
}
|
||||
}
|
||||
for p in fn_params.intersection(&ctx_params) {
|
||||
auto_scoped.insert(*p);
|
||||
}
|
||||
}
|
||||
if !auto_scoped.is_empty() {
|
||||
mutation.insert(
|
||||
"auto_scoped_params".into(),
|
||||
Value::Array(
|
||||
auto_scoped
|
||||
.iter()
|
||||
.map(|p| Value::String((*p).to_string()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if fn_spec.private() {
|
||||
mutation.insert("private".into(), Value::Bool(true));
|
||||
}
|
||||
|
||||
mutations.insert(fn_spec.name().to_string(), Value::Object(mutation));
|
||||
}
|
||||
|
||||
json!({
|
||||
"version": 1,
|
||||
"contexts": Value::Object(contexts),
|
||||
"mutations": Value::Object(mutations),
|
||||
})
|
||||
}
|
||||
|
||||
/// JSON-serialize the manifest with sorted keys (the Django command uses
|
||||
/// `json.dumps(..., sort_keys=True)`); `indent` of 0 → compact.
|
||||
pub fn generate_edge_manifest_json(base_url: &str, indent: usize) -> String {
|
||||
let value = generate_edge_manifest(base_url);
|
||||
let sorted = sort_value(&value);
|
||||
if indent == 0 {
|
||||
serde_json::to_string(&sorted).unwrap()
|
||||
} else {
|
||||
serde_json::to_string_pretty(&sorted).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively re-key every object so serialization is sorted-key, matching
|
||||
/// Python's `sort_keys=True`. (serde_json::Map preserves insertion order, so
|
||||
/// we rebuild via BTreeMap ordering.)
|
||||
fn sort_value(v: &Value) -> Value {
|
||||
match v {
|
||||
Value::Object(m) => {
|
||||
let mut keys: Vec<&String> = m.keys().collect();
|
||||
keys.sort();
|
||||
let mut out = Map::new();
|
||||
for k in keys {
|
||||
out.insert(k.clone(), sort_value(&m[k]));
|
||||
}
|
||||
Value::Object(out)
|
||||
}
|
||||
Value::Array(a) => Value::Array(a.iter().map(sort_value).collect()),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user