pub mod manifest;
pub mod query;
use std::sync::Arc;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use crate::AppState;
use manifest::ArchitectureGraph;
struct ServedGraph {
graph: Arc<ArchitectureGraph>,
json: Bytes,
}
pub(crate) fn install(state: &AppState, graph: ArchitectureGraph) {
state.insert_extension(ServedGraph {
json: Bytes::from(serde_json::to_vec(&graph).unwrap_or_default()),
graph: Arc::new(graph),
});
}
#[must_use]
pub fn served(state: &AppState) -> Option<Arc<ArchitectureGraph>> {
state
.extension::<ServedGraph>()
.map(|served| Arc::clone(&served.graph))
}
pub(crate) fn served_json(state: &AppState) -> Option<Bytes> {
state
.extension::<ServedGraph>()
.map(|served| served.json.clone())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RouteGraphDescriptor {
pub handler: &'static str,
pub module_path: &'static str,
pub method: &'static str,
pub path: &'static str,
pub static_route: bool,
pub crate_version: &'static str,
pub file: &'static str,
pub line: u32,
pub signature_symbols: &'static [&'static str],
pub body_symbols: &'static [&'static str],
pub sql_symbols: &'static [&'static str],
}
inventory::collect!(RouteGraphDescriptor);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModelGraphDescriptor {
pub model: &'static str,
pub model_path: &'static str,
pub table: &'static str,
pub relations: &'static [&'static str],
pub module_path: &'static str,
pub crate_version: &'static str,
pub file: &'static str,
pub line: u32,
}
inventory::collect!(ModelGraphDescriptor);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RepositoryGraphDescriptor {
pub repository: &'static str,
pub implementation: &'static str,
pub model: &'static str,
pub table: &'static str,
pub api: &'static str,
pub module_path: &'static str,
pub crate_version: &'static str,
pub file: &'static str,
pub line: u32,
}
inventory::collect!(RepositoryGraphDescriptor);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JobKind {
Job,
Scheduled,
Task,
}
impl std::fmt::Display for JobKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Job => "job",
Self::Scheduled => "scheduled",
Self::Task => "task",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JobGraphDescriptor {
pub name: &'static str,
pub kind: JobKind,
pub handler: &'static str,
pub module_path: &'static str,
pub schedule: &'static str,
pub mutating: bool,
pub mutating_symbols: &'static [&'static str],
pub crate_version: &'static str,
pub file: &'static str,
pub line: u32,
pub signature_symbols: &'static [&'static str],
pub signature_bindings: &'static [(&'static str, &'static str)],
pub dot_mutation_receivers: &'static [&'static str],
pub body_symbols: &'static [&'static str],
pub sql_symbols: &'static [&'static str],
}
inventory::collect!(JobGraphDescriptor);
#[allow(
clippy::struct_excessive_bools,
reason = "a serialized document field per independent, co-occurring declaration"
)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteAuth {
pub secured: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scopes: Vec<String>,
#[serde(default)]
pub policy: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub repository_scope: bool,
#[serde(default)]
pub public: bool,
}
impl RouteAuth {
#[must_use]
pub fn label(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if self.secured {
parts.push("secured".to_owned());
}
if !self.roles.is_empty() {
parts.push(format!("roles[{}]", self.roles.join(",")));
}
if !self.scopes.is_empty() {
parts.push(format!("scopes[{}]", self.scopes.join(",")));
}
if self.policy {
parts.push("policy".to_owned());
}
if self.repository_scope {
parts.push("repository-scope".to_owned());
}
if self.public {
parts.push("public".to_owned());
}
if parts.is_empty() {
"none".to_owned()
} else {
parts.join("+")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountedRoute {
pub method: String,
pub path: String,
pub handler: String,
pub module_path: String,
pub auth: RouteAuth,
pub repository_api: Option<RepositoryApiOwner>,
pub auth_declared: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepositoryApiOwner {
pub api: String,
pub model: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Route,
StaticRoute,
Model,
Repository,
Job,
ScheduledTask,
OneOffTask,
}
impl NodeKind {
#[must_use]
pub const fn id_prefix(self) -> &'static str {
match self {
Self::Route | Self::StaticRoute => "route",
Self::Model => "model",
Self::Repository => "repository",
Self::Job | Self::ScheduledTask | Self::OneOffTask => "job",
}
}
#[must_use]
pub const fn is_entry_point(self) -> bool {
matches!(
self,
Self::Route | Self::StaticRoute | Self::Job | Self::ScheduledTask | Self::OneOffTask
)
}
}
impl std::fmt::Display for NodeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Route => "route",
Self::StaticRoute => "static route",
Self::Model => "model",
Self::Repository => "repository",
Self::Job => "job",
Self::ScheduledTask => "scheduled task",
Self::OneOffTask => "one-off task",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Provenance {
Declaration,
Signature,
Body,
}
impl std::fmt::Display for Provenance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Declaration => "declaration",
Self::Signature => "signature",
Self::Body => "body",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Access {
Read,
Write,
ReadWrite,
}
impl std::fmt::Display for Access {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Read => "read",
Self::Write => "write",
Self::ReadWrite => "read/write",
})
}
}
const SAFE_METHODS: &[&str] = &["GET", "HEAD", "OPTIONS", "TRACE"];
#[must_use]
pub fn is_safe_method(method: &str) -> bool {
SAFE_METHODS.contains(&method.to_ascii_uppercase().as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn route_auth_label_names_every_declared_requirement() {
let auth = RouteAuth {
secured: true,
roles: vec!["admin".to_owned()],
scopes: vec!["posts:write".to_owned()],
policy: true,
repository_scope: false,
public: false,
};
assert_eq!(
auth.label(),
"secured+roles[admin]+scopes[posts:write]+policy"
);
}
#[test]
fn route_auth_label_says_none_when_nothing_is_declared() {
assert_eq!(RouteAuth::default().label(), "none");
}
#[test]
fn a_repository_scope_guard_is_never_reported_as_no_auth() {
let auth = RouteAuth {
repository_scope: true,
..RouteAuth::default()
};
assert_ne!(auth.label(), "none");
assert_eq!(auth.label(), "repository-scope");
}
#[test]
fn the_repository_scope_flag_is_absent_from_a_document_that_has_none() {
let json = serde_json::to_string(&RouteAuth::default()).expect("serializable");
assert!(
!json.contains("repository_scope"),
"an unguarded route must serialize as it did before the field existed: {json}"
);
let guarded = serde_json::to_string(&RouteAuth {
repository_scope: true,
..RouteAuth::default()
})
.expect("serializable");
assert!(guarded.contains("\"repository_scope\":true"), "{guarded}");
}
#[test]
fn safe_methods_are_case_insensitive() {
assert!(is_safe_method("get"));
assert!(is_safe_method("GET"));
assert!(!is_safe_method("post"));
}
fn graph_serving(path: &str) -> ArchitectureGraph {
let mounted = MountedRoute {
method: "GET".to_owned(),
path: path.to_owned(),
handler: "h".to_owned(),
module_path: "m".to_owned(),
auth: RouteAuth::default(),
repository_api: None,
auth_declared: false,
};
manifest::build(&[mounted], 0, &[], &[], &[], &[])
}
#[test]
fn each_state_serves_its_own_graph() {
let first = crate::AppState::test_default();
let second = crate::AppState::test_default();
install(&first, graph_serving("/first"));
install(&second, graph_serving("/second"));
assert_eq!(*served(&first).expect("first"), graph_serving("/first"));
assert_eq!(*served(&second).expect("second"), graph_serving("/second"));
}
#[test]
fn a_clone_made_before_install_serves_the_graph() {
let state = crate::AppState::test_default();
let router_state = state.clone();
install(&state, graph_serving("/a"));
assert_eq!(*served(&router_state).expect("graph"), graph_serving("/a"));
assert!(served_json(&router_state).is_some());
}
#[test]
fn a_state_with_no_install_serves_no_graph() {
let state = crate::AppState::test_default();
assert!(served(&state).is_none());
assert!(served_json(&state).is_none());
}
#[test]
fn only_routes_and_jobs_are_entry_points() {
assert!(NodeKind::Route.is_entry_point());
assert!(NodeKind::StaticRoute.is_entry_point());
assert!(NodeKind::Job.is_entry_point());
assert!(NodeKind::ScheduledTask.is_entry_point());
assert!(NodeKind::OneOffTask.is_entry_point());
assert!(!NodeKind::Model.is_entry_point());
assert!(!NodeKind::Repository.is_entry_point());
}
}