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]
88 pub fn matches_heritage(
89 &self,
90 super_class: Option<&str>,
91 implemented_interfaces: &[String],
92 ) -> bool {
93 let extends_matches = self
94 .extends
95 .as_deref()
96 .is_none_or(|expected| super_class == Some(expected));
97 let implements_matches = self
98 .implements
99 .as_deref()
100 .is_none_or(|expected| implemented_interfaces.iter().any(|iface| iface == expected));
101
102 extends_matches && implements_matches
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn deserialize_plain_member_name() {
112 let rule: UsedClassMemberRule = serde_json::from_str(r#""agInit""#).unwrap();
113 assert_eq!(rule, UsedClassMemberRule::Name("agInit".to_string()));
114 }
115
116 #[test]
117 fn deserialize_scoped_rule() {
118 let rule: UsedClassMemberRule = serde_json::from_str(
119 r#"{"implements":"ICellRendererAngularComp","members":["refresh"]}"#,
120 )
121 .unwrap();
122 assert_eq!(
123 rule,
124 UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
125 extends: None,
126 implements: Some("ICellRendererAngularComp".to_string()),
127 members: vec!["refresh".to_string()],
128 })
129 );
130 }
131
132 #[test]
133 fn scoped_rule_matches_extends_and_implements() {
134 let rule = ScopedUsedClassMemberRule {
135 extends: Some("BaseCommand".to_string()),
136 implements: Some("Runnable".to_string()),
137 members: vec!["execute".to_string()],
138 };
139
140 assert!(rule.matches_heritage(
141 Some("BaseCommand"),
142 &["Runnable".to_string(), "Disposable".to_string()]
143 ));
144 assert!(!rule.matches_heritage(Some("OtherBase"), &["Runnable".to_string()]));
145 assert!(!rule.matches_heritage(Some("BaseCommand"), &["Other".to_string()]));
146 }
147
148 #[test]
149 fn deserialize_scoped_rule_requires_constraint() {
150 let error = serde_json::from_str::<ScopedUsedClassMemberRule>(r#"{"members":["refresh"]}"#)
151 .unwrap_err()
152 .to_string();
153 assert!(
154 error.contains("require `extends` or `implements`"),
155 "unexpected error: {error}"
156 );
157 }
158}