use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
pub enum UsedClassMemberRule {
Name(String),
Scoped(ScopedUsedClassMemberRule),
}
impl From<&str> for UsedClassMemberRule {
fn from(value: &str) -> Self {
Self::Name(value.to_string())
}
}
impl From<String> for UsedClassMemberRule {
fn from(value: String) -> Self {
Self::Name(value)
}
}
#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ScopedUsedClassMemberRule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extends: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub implements: Option<String>,
pub members: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct ScopedUsedClassMemberRuleDef {
#[serde(default)]
extends: Option<String>,
#[serde(default)]
implements: Option<String>,
members: Vec<String>,
}
impl TryFrom<ScopedUsedClassMemberRuleDef> for ScopedUsedClassMemberRule {
type Error = &'static str;
fn try_from(value: ScopedUsedClassMemberRuleDef) -> Result<Self, Self::Error> {
if value.extends.is_none() && value.implements.is_none() {
return Err("scoped usedClassMembers rules require `extends` or `implements`");
}
Ok(Self {
extends: value.extends,
implements: value.implements,
members: value.members,
})
}
}
impl<'de> Deserialize<'de> for ScopedUsedClassMemberRule {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
ScopedUsedClassMemberRuleDef::deserialize(deserializer)?
.try_into()
.map_err(serde::de::Error::custom)
}
}
impl ScopedUsedClassMemberRule {
#[must_use]
pub fn matches_heritage(
&self,
super_class: Option<&str>,
implemented_interfaces: &[String],
) -> bool {
let extends_matches = self
.extends
.as_deref()
.is_none_or(|expected| super_class == Some(expected));
let implements_matches = self
.implements
.as_deref()
.is_none_or(|expected| implemented_interfaces.iter().any(|iface| iface == expected));
extends_matches && implements_matches
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_plain_member_name() {
let rule: UsedClassMemberRule = serde_json::from_str(r#""agInit""#).unwrap();
assert_eq!(rule, UsedClassMemberRule::Name("agInit".to_string()));
}
#[test]
fn deserialize_scoped_rule() {
let rule: UsedClassMemberRule = serde_json::from_str(
r#"{"implements":"ICellRendererAngularComp","members":["refresh"]}"#,
)
.unwrap();
assert_eq!(
rule,
UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
extends: None,
implements: Some("ICellRendererAngularComp".to_string()),
members: vec!["refresh".to_string()],
})
);
}
#[test]
fn scoped_rule_matches_extends_and_implements() {
let rule = ScopedUsedClassMemberRule {
extends: Some("BaseCommand".to_string()),
implements: Some("Runnable".to_string()),
members: vec!["execute".to_string()],
};
assert!(rule.matches_heritage(
Some("BaseCommand"),
&["Runnable".to_string(), "Disposable".to_string()]
));
assert!(!rule.matches_heritage(Some("OtherBase"), &["Runnable".to_string()]));
assert!(!rule.matches_heritage(Some("BaseCommand"), &["Other".to_string()]));
}
#[test]
fn deserialize_scoped_rule_requires_constraint() {
let error = serde_json::from_str::<ScopedUsedClassMemberRule>(r#"{"members":["refresh"]}"#)
.unwrap_err()
.to_string();
assert!(
error.contains("require `extends` or `implements`"),
"unexpected error: {error}"
);
}
}