Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b082c3c
Add design spec for pluggable introspection routes
aram356 Jul 2, 2026
2e04b26
Add implementation plan for introspection routes
aram356 Jul 2, 2026
98d225a
Revise introspection plan per review: enum serialization, imports, co…
aram356 Jul 2, 2026
7301bce
Revise plan (round 2): real body assertions, oneshot config tests, bo…
aram356 Jul 2, 2026
4254ed4
Revise plan (round 3): single-filter test commands, scoped doc greps,…
aram356 Jul 2, 2026
9011684
Plan: stage roadmap.md alongside routing.md in Task 7 commit
aram356 Jul 2, 2026
5ab3546
Make Manifest serializable with secret-value redaction
aram356 Jul 2, 2026
d09c068
Inject IntrospectionData at router dispatch chokepoint
aram356 Jul 2, 2026
beed0a4
Add edgezero_core::introspection handlers (manifest/config/routes)
aram356 Jul 2, 2026
83dd8c4
app! macro: bake manifest JSON into build_router via with_manifest_json
aram356 Jul 2, 2026
5ed4e8c
Use workspace deps for edgezero-macros proc-macro2/quote/syn
aram356 Jul 2, 2026
9b3b820
Sync app-demo Cargo.lock for edgezero-macros serde_json dep
aram356 Jul 2, 2026
2cd9b0c
Remove legacy route-listing machinery and /__edgezero/routes
aram356 Jul 2, 2026
6869389
Wire default introspection triggers into app-demo and generated apps
aram356 Jul 2, 2026
adbc94d
Docs: replace route-listing with introspection routes
aram356 Jul 2, 2026
8999623
Track manifest as build input; strengthen introspection tests; doc ca…
aram356 Jul 2, 2026
56cca34
Spec/plan: introspection access via independent extractors (no gating)
aram356 Jul 2, 2026
0feb194
Expose introspection via ManifestJson/RouteTable extractors
aram356 Jul 2, 2026
c9c6f34
Spec/plan: per-route gated introspection injection (macro auto-flags,…
aram356 Jul 2, 2026
692dd17
Spec/plan: #[action(introspection)] opt-in drives gated injection (ap…
aram356 Jul 2, 2026
9cb3b79
Docs cleanup: single gated architecture in spec; plan status banner +…
aram356 Jul 2, 2026
a9ad7bc
Spec/plan: fully-atomic introspection (IntrospectionNeeds, per-capabi…
aram356 Jul 2, 2026
e7f9d57
Plan: prove per-capability atomicity (3-case probe); atomic header li…
aram356 Jul 2, 2026
be2e9a4
Add IntrospectionNeeds + DynHandler::introspection_needs
aram356 Jul 2, 2026
8273395
#[action]: accept atomic (manifest|routes) params; emit capability-ca…
aram356 Jul 2, 2026
c30e1e5
Opt manifest/routes into introspection via #[action(manifest|routes)]
aram356 Jul 2, 2026
15949c8
Gate introspection injection per capability via IntrospectionNeeds
aram356 Jul 2, 2026
e68df70
Review fixes: combined-caps atomicity test, no-params fn assertion, s…
aram356 Jul 2, 2026
2efa2da
Add macro-level test for combined #[action(manifest, routes)] codegen
aram356 Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ sha2 = "0.10"
similar = "2"
simple_logger = "5"
proc-macro2 = { version = "1", features = ["span-locations"] }
quote = "1"
syn = { version = "2", features = ["full", "extra-traits", "visit"] }
subtle = "2"
# Pinned to the `~6.0` range (allows 6.0.x, blocks 6.1+) so a minor
Expand Down
26 changes: 26 additions & 0 deletions crates/edgezero-cli/src/templates/root/edgezero.toml.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ methods = ["GET", "POST"]
handler = "{{proj_core_mod}}::handlers::proxy_demo"
adapters = [{{{adapter_list}}}]

# -- Introspection routes ------------------------------------------------------

[[triggers.http]]
id = "manifest"
path = "/_{{name}}/manifest"
methods = ["GET"]
handler = "edgezero_core::introspection::manifest"
adapters = [{{{adapter_list}}}]
description = "App manifest as JSON"

[[triggers.http]]
id = "config"
path = "/_{{name}}/config"
methods = ["GET"]
handler = "edgezero_core::introspection::config"
adapters = [{{{adapter_list}}}]
description = "Effective app config (secret-safe)"

[[triggers.http]]
id = "routes"
path = "/_{{name}}/routes"
methods = ["GET"]
handler = "edgezero_core::introspection::routes"
adapters = [{{{adapter_list}}}]
description = "Registered route table"

# -- Stores ----------------------------------------------------------------
#
# `[stores.<kind>]` declares logical store ids only. `default` is required
Expand Down
12 changes: 12 additions & 0 deletions crates/edgezero-core/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ impl RequestContext {
.and_then(|registry| registry.default_ref())
}

/// Clone a request extension of type `T`, if present. Used by the
/// introspection extractors (`ManifestJson` / `RouteTable`) to read the
/// payload the router injected for their route.
#[must_use]
#[inline]
pub(crate) fn extension<T>(&self) -> Option<T>
where
T: Clone + Send + Sync + 'static,
{
self.request.extensions().get::<T>().cloned()
}

/// # Errors
/// Returns [`EdgeError::bad_request`] if the body cannot be deserialized as form-urlencoded data into `T`, or the body is streaming.
#[inline]
Expand Down
52 changes: 52 additions & 0 deletions crates/edgezero-core/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,36 @@ use crate::error::EdgeError;
use crate::http::HandlerFuture;
use crate::response::IntoResponse;

/// Which introspection payloads a route's handler needs injected at dispatch.
///
/// Reported per handler via [`DynHandler::introspection_needs`]. Handlers written
/// with `#[action(manifest)]` / `#[action(routes)]` set the matching field(s);
/// every other handler reports the default (all-false).
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct IntrospectionNeeds {
pub manifest: bool,
pub routes: bool,
}

impl IntrospectionNeeds {
/// Whether this handler needs any introspection payload injected.
#[must_use]
#[inline]
pub fn any(self) -> bool {
self.manifest || self.routes
}
}

pub trait DynHandler: Send + Sync {
fn call(&self, ctx: RequestContext) -> HandlerFuture;

/// Introspection payloads a route bound to this handler needs injected into
/// the request at dispatch. Defaults to none; `#[action(manifest)]` /
/// `#[action(routes)]` handlers override it.
#[inline]
fn introspection_needs(&self) -> IntrospectionNeeds {
IntrospectionNeeds::default()
}
}

impl<F, Fut, Res> DynHandler for F
Expand All @@ -21,6 +49,13 @@ where
let fut = (self)(ctx);
Box::pin(async move { fut.await?.into_response() })
}

// `missing_trait_methods` (deny) forbids relying on the trait default here;
// spell out the same all-false result that fn/closure handlers report.
#[inline]
fn introspection_needs(&self) -> IntrospectionNeeds {
IntrospectionNeeds::default()
}
}

pub type BoxHandler = Arc<dyn DynHandler>;
Expand All @@ -38,3 +73,20 @@ where
Arc::new(self)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn fn_handler_reports_default_introspection_needs() {
// A plain closure handler uses the blanket `DynHandler` impl, which must
// report no introspection needs. (`&str: IntoResponse` satisfies the bound.)
let handler = |_ctx: RequestContext| async { Ok::<&'static str, EdgeError>("ok") };
assert_eq!(
DynHandler::introspection_needs(&handler),
IntrospectionNeeds::default()
);
assert!(!IntrospectionNeeds::default().any());
}
}
Loading