use std::{
collections::BTreeMap,
path::{Path, PathBuf},
rc::Rc,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) enum Role {
Core,
Pipes,
Switchboard,
Wiring,
Application,
Tooling,
ThirdParty,
}
impl std::fmt::Display for Role {
fn fmt(&self, output: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
output.write_str(match self {
Self::Core => "core",
Self::Pipes => "pipes",
Self::Switchboard => "switchboard",
Self::Wiring => "wiring",
Self::Application => "application",
Self::Tooling => "tooling",
Self::ThirdParty => "third-party",
})
}
}
impl Role {
pub(super) fn pure(self) -> bool {
matches!(self, Self::Core | Self::Pipes | Self::Switchboard)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Declaration {
pub role: Role,
pub app: Option<String>,
pub scope: Option<Scope>,
#[serde(default)]
pub forbid_dependencies: Vec<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) enum Scope {
Shared,
}
impl Declaration {
pub(super) fn valid(&self) -> bool {
(match self.role {
Role::Core | Role::Tooling | Role::ThirdParty => {
self.app.is_none() && self.scope.is_none()
}
Role::Pipes | Role::Wiring => matches!(
(&self.app, self.scope),
(Some(_), None) | (None, Some(Scope::Shared))
),
Role::Switchboard | Role::Application => self.app.is_some() && self.scope.is_none(),
}) && self
.app
.as_ref()
.is_none_or(|app| !app.is_empty() && app != "shared")
}
pub(super) fn allows(&self, target: &Self) -> bool {
match self.role {
Role::Core => matches!(target.role, Role::Core | Role::ThirdParty),
Role::Pipes | Role::Switchboard => {
target.role == Role::Core
|| (target.role == Role::Pipes
&& (target.scope == Some(Scope::Shared)
|| (self.app.is_some() && self.app == target.app)))
}
Role::Wiring | Role::Application | Role::Tooling => true,
Role::ThirdParty => target.role == Role::ThirdParty,
}
}
}
#[derive(Debug, Serialize)]
pub(super) struct Package {
pub name: String,
pub manifest: PathBuf,
pub declaration: Option<Declaration>,
pub provenance: &'static str,
pub dependencies: Vec<Dependency>,
pub proc_macro: bool,
#[serde(skip)]
pub document: Rc<toml::Value>,
#[serde(skip)]
pub library: Option<PathBuf>,
#[serde(skip)]
pub workspace_lints: Rc<toml::Value>,
#[serde(skip)]
pub workspace_manifest: PathBuf,
}
#[derive(Debug, Serialize)]
pub(super) struct Dependency {
pub name: String,
pub alias: Option<String>,
pub path: Option<PathBuf>,
pub kind: String,
pub target: Option<String>,
pub optional: bool,
pub default_features: bool,
pub features: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(super) struct Finding {
pub code: &'static str,
pub manifest: PathBuf,
pub subject: String,
pub explanation: String,
pub guidance: &'static str,
}
#[derive(Default, Debug, Serialize)]
pub(super) struct WorkCounts {
pub metadata_invocations: usize,
pub manifests_read: usize,
pub sources_read: usize,
pub dependency_edges_checked: usize,
}
#[derive(Debug, Serialize)]
pub(super) struct Architecture {
pub schema: &'static str,
pub root: PathBuf,
pub packages: BTreeMap<PathBuf, Package>,
pub findings: Vec<Finding>,
pub limitations: Vec<&'static str>,
pub work: WorkCounts,
}
impl Architecture {
pub(super) fn report(
&mut self,
package: &Path,
code: &'static str,
subject: String,
explanation: String,
guidance: &'static str,
) {
self.findings.push(Finding {
code,
manifest: package.to_owned(),
subject,
explanation,
guidance,
});
}
}