Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 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 lib/saluki-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ workspace = true
axum = { workspace = true, features = ["http1", "tokio"] }
axum-extra = { workspace = true, features = ["query"] }
http = { workspace = true }
tonic = { workspace = true }
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saluki-api uses tonic::service::Routes and into_axum_router() in src/lib.rs, which requires Tonic's router feature (already enabled in saluki-app). As written, tonic = { workspace = true } may fail to compile when building saluki-api by itself. Enable the router feature for the tonic dependency here (or gate the gRPC-related API behind a crate feature).

Suggested change
tonic = { workspace = true }
tonic = { workspace = true, features = ["router"] }

Copilot uses AI. Check for mistakes.
98 changes: 98 additions & 0 deletions lib/saluki-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub use axum::response;
pub use axum::routing;
use axum::Router;
pub use http::StatusCode;
use tonic::service::Routes;

pub mod extract {
pub use axum::extract::*;
Expand All @@ -17,3 +18,100 @@ pub trait APIHandler {
fn generate_initial_state(&self) -> Self::State;
fn generate_routes(&self) -> Router<Self::State>;
}

/// API endpoint type.
///
/// Identifies whether or not a route should be exposed on the unprivileged or privileged API endpoint.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum EndpointType {
/// The unprivileged (plain HTTP) API endpoint.
Unprivileged,

/// The privileged (TLS-protected) API endpoint.
Privileged,
}

impl EndpointType {
/// Returns a human-readable name for this endpoint type.
pub fn name(&self) -> &'static str {
match self {
Self::Unprivileged => "unprivileged",
Self::Privileged => "privileged",
}
}
}

/// API endpoint protocol.
///
/// Identifies which application protocol the route should be exposed to.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum EndpointProtocol {
/// HTTP.
Http,

/// gRPC.
Grpc,
}

impl EndpointProtocol {
/// Returns a human-readable name for this endpoint protocol.
pub fn name(&self) -> &'static str {
match self {
Self::Http => "HTTP",
Self::Grpc => "gRPC",
}
}
}

/// A set of dynamic API routes.
///
/// Dynamic routes allow for processes to dynamically register/unregister themselves from running API endpoints,
/// adapting to changes in the process state and without requiring all routes to be known and declared upfront.
#[derive(Clone, Debug)]
pub struct DynamicRoute {
/// Which API endpoint these routes target.
endpoint_type: EndpointType,

/// Which API protocol these routes target.
endpoint_protocol: EndpointProtocol,

/// The routes to serve.
router: Router<()>,
}

impl DynamicRoute {
/// Creates a dynamic HTTP route from the given API handler.
pub fn http<T: APIHandler>(endpoint_type: EndpointType, handler: T) -> Self {
let router = handler.generate_routes().with_state(handler.generate_initial_state());
Self::new(endpoint_type, EndpointProtocol::Http, router)
}

/// Creates a dynamic gRPC route from the given Tonic routes.
pub fn grpc(endpoint_type: EndpointType, routes: Routes) -> Self {
let router = routes.prepare().into_axum_router();
Self::new(endpoint_type, EndpointProtocol::Grpc, router)
}

fn new(endpoint_type: EndpointType, endpoint_protocol: EndpointProtocol, router: Router<()>) -> Self {
Self {
endpoint_type,
endpoint_protocol,
router,
}
}

/// Returns the type of endpoint these routes target.
pub fn endpoint_type(&self) -> EndpointType {
self.endpoint_type
}

/// Returns the protocol of endpoint these routes target.
pub fn endpoint_protocol(&self) -> EndpointProtocol {
self.endpoint_protocol
}

/// Consumes this route and returns the underlying router.
pub fn into_router(self) -> Router<()> {
self.router
}
}
3 changes: 3 additions & 0 deletions lib/saluki-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ workspace = true
tls-fips = ["saluki-tls/fips"]

[dependencies]
arc-swap = { workspace = true }
async-trait = { workspace = true }
axum = { workspace = true }
bytesize = { workspace = true }
chrono = { workspace = true }
chrono-tz = { workspace = true }
http = { workspace = true }
hyper = { workspace = true }
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hyper is added as a direct dependency here, but this crate doesn't appear to reference hyper directly (only saluki_io::net::util::hyper::TowerToHyperService). If it's not needed for a feature flag or a public type, consider removing it to avoid carrying an unused dependency.

Suggested change
hyper = { workspace = true }

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hyper dependency appears to be unnecessary. The new code in dynamic_api.rs only uses types from the http crate (http::Request, http::Response) and utilities from saluki_io which already depends on hyper. There are no direct imports or usages of hyper types in the added code. Consider removing this dependency unless it's needed for other reasons not visible in this diff.

Suggested change
hyper = { workspace = true }

Copilot uses AI. Check for mistakes.
iana-time-zone = { workspace = true }
itoa = { workspace = true }
memory-accounting = { workspace = true }
Expand Down
Loading
Loading