fallow_config/config/
used_class_members.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
10#[serde(untagged)]
11pub enum UsedClassMemberRule {
12 Name(String),
14 Scoped(ScopedUsedClassMemberRule),
16}
17
18impl From<&str> for UsedClassMemberRule {
19 fn from(value: &str) -> Self {
20 Self::Name(value.to_string())
21 }
22}
23
24impl From<String> for UsedClassMemberRule {
25 fn from(value: String) -> Self {
26 Self::Name(value)
27 }
28}
29
30#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
32#[serde(rename_all = "camelCase", deny_unknown_fields)]
33pub struct ScopedUsedClassMemberRule {
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub extends: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub implements: Option<String>,
40 pub members: Vec<String>,
42}
43
44#[derive(Debug, Clone, Deserialize)]
45#[serde(rename_all = "camelCase", deny_unknown_fields)]
46struct ScopedUsedClassMemberRuleDef {
47 #[serde(default)]
48 extends: Option<String>,
49 #[serde(default)]
50 implements: Option<String>,
51 members: Vec<String>,
52}
53
54impl TryFrom<ScopedUsedClassMemberRuleDef> for ScopedUsedClassMemberRule {
55 type Error = &'static str;
56
57 fn try_from(value: ScopedUsedClassMemberRuleDef) -> Result<Self, Self::Error> {
58 if value.extends.is_none() && value.implements.is_none() {
59 return Err("scoped usedClassMembers rules require `extends` or `implements`");
60 }
61
62 Ok(Self {
63 extends: value.extends,
64 implements: value.implements,
65 members: value.members,
66 })
67 }
68}
69
70impl<'de> Deserialize<'de> for ScopedUsedClassMemberRule {
71 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72 where
73 D: serde::Deserializer<'de>,
74 {
75 ScopedUsedClassMemberRuleDef::deserialize(deserializer)?
76 .try_into()
77 .map_err(serde::de::Error::custom)
78 }
79}
80
81impl ScopedUsedClassMemberRule {
82 #[must_use]
83 pub fn matches_heritage(
84 &self,
85 super_class: Option<&str>,
86 implemented_interfaces: &[String],
87 ) -> bool {
88 let extends_matches = self
89 .extends
90 .as_deref()
91 .is_none_or(|expected| super_class == Some(expected));
92 let implements_matches = self
93 .implements
94 .as_deref()
95 .is_none_or(|expected| implemented_interfaces.iter().any(|iface| iface == expected));
96
97 extends_matches && implements_matches
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn deserialize_plain_member_name() {
107 let rule: UsedClassMemberRule = serde_json::from_str(r#""agInit""#).unwrap();
108 assert_eq!(rule, UsedClassMemberRule::Name("agInit".to_string()));
109 }
110
111 #[test]
112 fn deserialize_scoped_rule() {
113 let rule: UsedClassMemberRule = serde_json::from_str(
114 r#"{"implements":"ICellRendererAngularComp","members":["refresh"]}"#,
115 )
116 .unwrap();
117 assert_eq!(
118 rule,
119 UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
120 extends: None,
121 implements: Some("ICellRendererAngularComp".to_string()),
122 members: vec!["refresh".to_string()],
123 })
124 );
125 }
126
127 #[test]
128 fn scoped_rule_matches_extends_and_implements() {
129 let rule = ScopedUsedClassMemberRule {
130 extends: Some("BaseCommand".to_string()),
131 implements: Some("Runnable".to_string()),
132 members: vec!["execute".to_string()],
133 };
134
135 assert!(rule.matches_heritage(
136 Some("BaseCommand"),
137 &["Runnable".to_string(), "Disposable".to_string()]
138 ));
139 assert!(!rule.matches_heritage(Some("OtherBase"), &["Runnable".to_string()]));
140 assert!(!rule.matches_heritage(Some("BaseCommand"), &["Other".to_string()]));
141 }
142
143 #[test]
144 fn deserialize_scoped_rule_requires_constraint() {
145 let error = serde_json::from_str::<ScopedUsedClassMemberRule>(r#"{"members":["refresh"]}"#)
146 .unwrap_err()
147 .to_string();
148 assert!(
149 error.contains("require `extends` or `implements`"),
150 "unexpected error: {error}"
151 );
152 }
153}