use std::sync::Arc;
use axum::Router;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use crate::routing::RouterState;
use crate::uag::UagArtifact;
pub const PATH: &str = "/_arcature/uag.json";
#[derive(Clone)]
pub struct UagEndpoint(Arc<[u8]>);
impl UagEndpoint {
#[must_use]
pub fn new(artifact: &UagArtifact) -> Self {
let json = artifact
.to_json()
.expect("UagArtifact is plain String-keyed data, so serialization cannot fail");
UagEndpoint(json.into())
}
#[must_use]
pub fn allowed() -> bool {
cfg!(debug_assertions)
}
#[must_use]
pub fn json(&self) -> &[u8] {
&self.0
}
pub fn router<S: RouterState>(&self) -> Router<S> {
let endpoint = self.clone();
Router::new().route(
PATH,
axum::routing::get(move || {
let endpoint = endpoint.clone();
async move { endpoint.response() }
}),
)
}
fn response(&self) -> Response {
let mut response = (StatusCode::OK, Vec::from(&*self.0)).into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("no-store, max-age=0"),
);
response
}
}
impl std::fmt::Debug for UagEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UagEndpoint")
.field("path", &PATH)
.field("bytes", &self.0.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dx::application_graph::ApplicationGraph;
use crate::inertia::contracts::PageContracts;
fn endpoint() -> UagEndpoint {
let graph = ApplicationGraph::new_unchecked(Vec::new());
let contracts = PageContracts::new().artifact();
UagEndpoint::new(&crate::uag::build(&graph, &contracts))
}
#[test]
fn the_endpoint_serves_the_same_bytes_the_artifact_writes() {
let graph = ApplicationGraph::new_unchecked(Vec::new());
let contracts = PageContracts::new().artifact();
let artifact = crate::uag::build(&graph, &contracts);
let expected = artifact.to_json().expect("plain data serializes");
assert_eq!(UagEndpoint::new(&artifact).json(), expected.as_slice());
}
#[test]
fn the_response_is_json_and_uncached() {
let response = endpoint().response();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("application/json"))
);
assert_eq!(
response.headers().get(header::CACHE_CONTROL),
Some(&HeaderValue::from_static("no-store, max-age=0"))
);
}
#[test]
fn a_build_with_debug_assertions_off_is_never_allowed_to_serve_the_graph() {
assert_eq!(UagEndpoint::allowed(), cfg!(debug_assertions));
}
#[tokio::test]
async fn an_application_that_did_not_ask_for_it_does_not_serve_the_graph() {
use tower::ServiceExt as _;
let router = crate::application::Application::new()
.routes(crate::routing::Routes::new([crate::routing::Route::get(
"/",
|| async { "ok" },
)]))
.build()
.into_router();
let response = router
.oneshot(
axum::http::Request::builder()
.uri(PATH)
.body(axum::body::Body::empty())
.expect("a GET with an empty body is a valid request"),
)
.await
.expect("the router is infallible");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}