use std::fmt;
use ahash::HashMap;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use serde::de;
use serde::de::Deserializer;
use serde::de::MapAccess;
use serde::de::Visitor;
use serde::ser::SerializeStruct;
use serde::ser::Serializer;
use crate::path::NamespacePath;
use crate::path::Path;
use crate::path::is_valid_identifier_part;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct Settings {
pub perimeter: PerimeterSettings,
pub structural: StructuralSettings,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct PerimeterSettings {
pub layers: HashMap<String, Vec<Path>>,
pub layering: Vec<NamespacePath>,
pub rules: Vec<PerimeterRule>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PerimeterRule {
pub namespace: NamespacePath,
pub permit: Vec<PermittedDependency>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
#[schemars(untagged)]
pub enum PermittedDependency {
Dependency(Path),
DependencyOfKind { path: Path, kinds: Vec<PermittedDependencyKind> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum PermittedDependencyKind {
ClassLike,
Function,
Constant,
Attribute,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct StructuralSettings {
pub rules: Vec<StructuralRule>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct StructuralRule {
pub on: String,
pub not_on: Option<String>,
pub target: Option<StructuralSymbolKind>,
pub must_be: Option<Vec<StructuralSymbolKind>>,
pub must_be_named: Option<String>,
pub must_be_final: Option<bool>,
pub must_be_abstract: Option<bool>,
pub must_be_readonly: Option<bool>,
pub must_implement: Option<StructuralInheritanceConstraint>,
pub must_extend: Option<StructuralInheritanceConstraint>,
pub must_use_trait: Option<StructuralInheritanceConstraint>,
pub must_use_attribute: Option<StructuralInheritanceConstraint>,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum StructuralSymbolKind {
ClassLike,
Class,
Interface,
Trait,
Enum,
Constant,
Function,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema)]
#[serde(untagged)]
#[schemars(untagged)]
pub enum StructuralInheritanceConstraint {
AnyOfAllOf(Vec<Vec<String>>),
AllOf(Vec<String>),
Single(String),
Nothing,
}
impl PermittedDependencyKind {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
PermittedDependencyKind::ClassLike => "class-like",
PermittedDependencyKind::Function => "function",
PermittedDependencyKind::Constant => "constant",
PermittedDependencyKind::Attribute => "attribute",
}
}
}
impl StructuralSymbolKind {
#[must_use]
pub const fn is_constant(&self) -> bool {
matches!(self, StructuralSymbolKind::Constant)
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
StructuralSymbolKind::ClassLike => "class-like",
StructuralSymbolKind::Class => "class",
StructuralSymbolKind::Interface => "interface",
StructuralSymbolKind::Trait => "trait",
StructuralSymbolKind::Enum => "enum",
StructuralSymbolKind::Constant => "constant",
StructuralSymbolKind::Function => "function",
}
}
}
impl<'de> Deserialize<'de> for PermittedDependency {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct AllowedPathVisitor;
impl<'de> Visitor<'de> for AllowedPathVisitor {
type Value = PermittedDependency;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a path string or a detailed object with path and types")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let path = Path::deserialize(de::value::StrDeserializer::new(value))?;
Ok(PermittedDependency::Dependency(path))
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
#[derive(Deserialize)]
struct DetailedHelper {
path: Path,
kinds: Vec<PermittedDependencyKind>,
}
let helper: DetailedHelper = Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(PermittedDependency::DependencyOfKind { path: helper.path, kinds: helper.kinds })
}
}
deserializer.deserialize_any(AllowedPathVisitor)
}
}
impl<'de> Deserialize<'de> for StructuralInheritanceConstraint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Untagged {
AnyOfAllOf(Vec<Vec<String>>),
AllOf(Vec<String>),
Single(String),
}
match Untagged::deserialize(deserializer)? {
Untagged::Single(s) => {
if s.eq_ignore_ascii_case("@nothing") {
Ok(Self::Nothing)
} else if s.split('\\').all(is_valid_identifier_part) {
Ok(Self::Single(s))
} else {
Err(de::Error::custom(format!("Expected a valid fully qualified name or '@nothing', found '{s}'")))
}
}
Untagged::AllOf(items) => {
for item in &items {
if !item.split('\\').all(is_valid_identifier_part) {
return Err(de::Error::custom(format!("'{item}' is not a valid fully qualified name")));
}
}
Ok(Self::AllOf(items))
}
Untagged::AnyOfAllOf(groups) => {
for group in &groups {
for item in group {
if !item.split('\\').all(is_valid_identifier_part) {
return Err(de::Error::custom(format!("'{item}' is not a valid fully qualified name")));
}
}
}
Ok(Self::AnyOfAllOf(groups))
}
}
}
}
impl Serialize for PermittedDependency {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
PermittedDependency::Dependency(path) => path.serialize(serializer),
PermittedDependency::DependencyOfKind { path, kinds } => {
let mut state = serializer.serialize_struct("DependencyOfKind", 2)?;
state.serialize_field("path", path)?;
state.serialize_field("kinds", kinds)?;
state.end()
}
}
}
}
impl fmt::Display for PermittedDependencyKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Display for StructuralInheritanceConstraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nothing => write!(f, "<nothing>"),
Self::Single(item) => write!(f, "`{item}`"),
Self::AllOf(items) => {
let formatted = items.iter().map(|item| format!("`{item}`")).collect::<Vec<_>>().join(" and ");
write!(f, "{formatted}")
}
Self::AnyOfAllOf(groups) => {
let formatted = groups
.iter()
.map(|group| {
let inner = group.iter().map(|item| format!("`{item}`")).collect::<Vec<_>>().join(" and ");
if group.len() > 1 { format!("({inner})") } else { inner }
})
.collect::<Vec<_>>()
.join(" or ");
write!(f, "{formatted}")
}
}
}
}
impl fmt::Display for StructuralSymbolKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_structural_inheritance_constraint_display() {
let single = StructuralInheritanceConstraint::Single("SomeInterface".to_string());
assert_eq!(single.to_string(), "`SomeInterface`");
let all_of = StructuralInheritanceConstraint::AllOf(vec!["InterfaceA".to_string(), "InterfaceB".to_string()]);
assert_eq!(all_of.to_string(), "`InterfaceA` and `InterfaceB`");
let any_of_all_of = StructuralInheritanceConstraint::AnyOfAllOf(vec![
vec!["InterfaceA".to_string(), "InterfaceB".to_string()],
vec!["InterfaceC".to_string()],
]);
assert_eq!(any_of_all_of.to_string(), "(`InterfaceA` and `InterfaceB`) or `InterfaceC`");
let none = StructuralInheritanceConstraint::Nothing;
assert_eq!(none.to_string(), "<nothing>");
}
#[derive(Deserialize)]
struct Wrapper {
constraint: StructuralInheritanceConstraint,
}
#[test]
fn it_deserializes_none_keyword() {
let toml = r#"constraint = "@nothing""#;
let wrapped: Wrapper = toml::from_str(toml).unwrap();
assert_eq!(wrapped.constraint, StructuralInheritanceConstraint::Nothing);
}
#[test]
fn it_deserializes_valid_single_string() {
let toml = r#"constraint = "App\\Domain\\MyInterface""#;
let wrapped: Wrapper = toml::from_str(toml).unwrap();
assert_eq!(wrapped.constraint, StructuralInheritanceConstraint::Single("App\\Domain\\MyInterface".to_string()));
}
#[test]
fn it_deserializes_valid_array_of_strings() {
let toml = r#"constraint = ["App\\InterfaceA", "App\\InterfaceB"]"#;
let wrapped: Wrapper = toml::from_str(toml).unwrap();
assert_eq!(
wrapped.constraint,
StructuralInheritanceConstraint::AllOf(vec!["App\\InterfaceA".to_string(), "App\\InterfaceB".to_string()])
);
}
#[test]
fn it_deserializes_valid_array_of_arrays() {
let toml = r#"constraint = [["App\\A", "App\\B"], ["App\\C"]]"#;
let wrapped: Wrapper = toml::from_str(toml).unwrap();
assert_eq!(
wrapped.constraint,
StructuralInheritanceConstraint::AnyOfAllOf(vec![
vec!["App\\A".to_string(), "App\\B".to_string()],
vec!["App\\C".to_string()]
])
);
}
#[test]
fn it_fails_on_invalid_identifier_in_single_string() {
let toml = r#"constraint = "Invalid-Interface""#;
assert!(toml::from_str::<Wrapper>(toml).is_err());
}
#[test]
fn it_fails_on_invalid_identifier_in_array() {
let toml = r#"constraint = ["App\\InterfaceA", "Invalid-Interface"]"#;
assert!(toml::from_str::<Wrapper>(toml).is_err());
}
#[test]
fn it_fails_on_invalid_identifier_in_nested_array() {
let toml = r#"constraint = [["App\\A", "Invalid-B"], ["App\\C"]]"#;
assert!(toml::from_str::<Wrapper>(toml).is_err());
}
}