1use crate::fix::FixStrategyEntry;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize)]
12pub struct KonveyorRuleset {
13 pub name: String,
14 pub description: String,
15 pub labels: Vec<String>,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
20pub struct KonveyorRule {
21 #[serde(rename = "ruleID")]
22 pub rule_id: String,
23 pub labels: Vec<String>,
24 pub effort: u32,
25 pub category: String,
26 pub description: String,
27 pub message: String,
28 #[serde(skip_serializing_if = "Vec::is_empty", default)]
29 pub links: Vec<KonveyorLink>,
30 pub when: KonveyorCondition,
31 #[serde(skip)]
34 pub fix_strategy: Option<FixStrategyEntry>,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
39pub struct KonveyorLink {
40 pub url: String,
41 pub title: String,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
50#[serde(untagged)]
51pub enum KonveyorCondition {
52 FileContent {
53 #[serde(rename = "builtin.filecontent")]
54 filecontent: FileContentFields,
55 },
56 Json {
57 #[serde(rename = "builtin.json")]
58 json: JsonFields,
59 },
60 FrontendReferenced {
61 #[serde(rename = "frontend.referenced")]
62 referenced: FrontendReferencedFields,
63 },
64 FrontendCssClass {
65 #[serde(rename = "frontend.cssclass")]
66 cssclass: FrontendPatternFields,
67 },
68 FrontendCssVar {
69 #[serde(rename = "frontend.cssvar")]
70 cssvar: FrontendPatternFields,
71 },
72 FrontendDependency {
73 #[serde(rename = "frontend.dependency")]
74 dependency: FrontendDependencyFields,
75 },
76 JavaReferenced {
77 #[serde(rename = "java.referenced")]
78 referenced: JavaReferencedFields,
79 },
80 JavaDependency {
81 #[serde(rename = "java.dependency")]
82 dependency: JavaDependencyFields,
83 },
84 Or {
85 or: Vec<KonveyorCondition>,
86 },
87 And {
88 and: Vec<KonveyorCondition>,
89 },
90 FileContentNegated {
92 #[serde(rename = "not")]
93 negated: bool,
94 #[serde(rename = "builtin.filecontent")]
95 filecontent: FileContentFields,
96 },
97}
98
99#[derive(Debug, Serialize, Deserialize)]
101pub struct FrontendPatternFields {
102 pub pattern: String,
103 #[serde(
105 rename = "filePattern",
106 skip_serializing_if = "Option::is_none",
107 default
108 )]
109 pub file_pattern: Option<String>,
110}
111
112#[derive(Debug, Serialize, Deserialize)]
117pub struct FrontendDependencyFields {
118 #[serde(skip_serializing_if = "Option::is_none", default)]
120 pub name: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none", default)]
123 pub nameregex: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none", default)]
126 pub upperbound: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none", default)]
129 pub lowerbound: Option<String>,
130}
131
132#[derive(Debug, Serialize, Deserialize)]
134pub struct FileContentFields {
135 pub pattern: String,
136 #[serde(rename = "filePattern")]
137 pub file_pattern: String,
138}
139
140#[derive(Debug, Serialize, Deserialize)]
142pub struct JsonFields {
143 pub xpath: String,
144 #[serde(skip_serializing_if = "Option::is_none", default)]
145 pub filepaths: Option<Vec<String>>,
146}
147
148#[derive(Debug, Serialize, Deserialize)]
153pub struct FrontendReferencedFields {
154 pub pattern: String,
156 pub location: String,
158 #[serde(skip_serializing_if = "Option::is_none", default)]
160 pub component: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none", default)]
163 pub parent: Option<String>,
164 #[serde(rename = "notParent", skip_serializing_if = "Option::is_none", default)]
167 pub not_parent: Option<String>,
168 #[serde(
170 rename = "parentFrom",
171 skip_serializing_if = "Option::is_none",
172 default
173 )]
174 pub parent_from: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none", default)]
181 pub child: Option<String>,
182 #[serde(rename = "notChild", skip_serializing_if = "Option::is_none", default)]
187 pub not_child: Option<String>,
188 #[serde(
195 rename = "requiresChild",
196 skip_serializing_if = "Option::is_none",
197 default
198 )]
199 pub requires_child: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none", default)]
202 pub value: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none", default)]
205 pub from: Option<String>,
206 #[serde(
209 rename = "filePattern",
210 skip_serializing_if = "Option::is_none",
211 default
212 )]
213 pub file_pattern: Option<String>,
214}
215
216#[derive(Debug, Serialize, Deserialize)]
221pub struct JavaReferencedFields {
222 pub pattern: String,
224 #[serde(skip_serializing_if = "Option::is_none", default)]
229 pub location: Option<String>,
230 #[serde(skip_serializing_if = "Option::is_none", default)]
232 pub annotated: Option<JavaAnnotatedFields>,
233}
234
235#[derive(Debug, Serialize, Deserialize)]
237pub struct JavaAnnotatedFields {
238 #[serde(skip_serializing_if = "Option::is_none", default)]
240 pub pattern: Option<String>,
241 #[serde(skip_serializing_if = "Vec::is_empty", default)]
243 pub elements: Vec<JavaAnnotationElement>,
244}
245
246#[derive(Debug, Serialize, Deserialize)]
248pub struct JavaAnnotationElement {
249 pub name: String,
251 pub value: String,
253}
254
255#[derive(Debug, Serialize, Deserialize)]
260pub struct JavaDependencyFields {
261 #[serde(skip_serializing_if = "Option::is_none", default)]
263 pub name: Option<String>,
264 #[serde(skip_serializing_if = "Option::is_none", default)]
266 pub nameregex: Option<String>,
267 #[serde(skip_serializing_if = "Option::is_none", default)]
269 pub upperbound: Option<String>,
270 #[serde(skip_serializing_if = "Option::is_none", default)]
272 pub lowerbound: Option<String>,
273}
274
275pub fn extract_frontend_refs(condition: &KonveyorCondition) -> Vec<&FrontendReferencedFields> {
278 match condition {
279 KonveyorCondition::FrontendReferenced { referenced } => vec![referenced],
280 KonveyorCondition::Or { or } => or.iter().flat_map(extract_frontend_refs).collect(),
281 KonveyorCondition::And { and } => and.iter().flat_map(extract_frontend_refs).collect(),
282 _ => vec![],
283 }
284}
285
286pub fn extract_file_pattern_from_condition(condition: &KonveyorCondition) -> Option<String> {
288 match condition {
289 KonveyorCondition::FileContent { filecontent } => Some(filecontent.file_pattern.clone()),
290 KonveyorCondition::Or { or } => or.first().and_then(extract_file_pattern_from_condition),
291 _ => None,
292 }
293}
294
295pub fn dedup_conditions(conditions: Vec<KonveyorCondition>) -> Vec<KonveyorCondition> {
297 use std::collections::BTreeSet;
298 let mut seen = BTreeSet::new();
299 let mut unique = Vec::new();
300 for cond in conditions {
301 let key = serde_json::to_string(&cond).unwrap_or_default();
302 if seen.insert(key) {
303 unique.push(cond);
304 }
305 }
306 unique
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_frontend_dependency_serializes_to_yaml() {
315 let condition = KonveyorCondition::FrontendDependency {
316 dependency: FrontendDependencyFields {
317 name: Some("@patternfly/react-core".into()),
318 nameregex: None,
319 upperbound: Some("5.99.99".into()),
320 lowerbound: None,
321 },
322 };
323 let yaml = serde_yaml::to_string(&condition).unwrap();
324 assert!(
325 yaml.contains("frontend.dependency"),
326 "Should serialize with frontend.dependency key"
327 );
328 assert!(yaml.contains("@patternfly/react-core"));
329 assert!(yaml.contains("5.99.99"));
330 assert!(!yaml.contains("nameregex"));
332 assert!(!yaml.contains("lowerbound"));
333 }
334
335 #[test]
336 fn test_frontend_dependency_roundtrips() {
337 let condition = KonveyorCondition::FrontendDependency {
338 dependency: FrontendDependencyFields {
339 name: Some("@patternfly/react-core".into()),
340 nameregex: None,
341 upperbound: Some("5.99.99".into()),
342 lowerbound: Some("4.0.0".into()),
343 },
344 };
345 let yaml = serde_yaml::to_string(&condition).unwrap();
346 let deserialized: KonveyorCondition = serde_yaml::from_str(&yaml).unwrap();
347 match deserialized {
348 KonveyorCondition::FrontendDependency { dependency } => {
349 assert_eq!(dependency.name, Some("@patternfly/react-core".into()));
350 assert_eq!(dependency.upperbound, Some("5.99.99".into()));
351 assert_eq!(dependency.lowerbound, Some("4.0.0".into()));
352 assert_eq!(dependency.nameregex, None);
353 }
354 _ => panic!("Should deserialize as FrontendDependency"),
355 }
356 }
357
358 #[test]
359 fn test_frontend_dependency_in_rule_yaml() {
360 let rule = KonveyorRule {
361 rule_id: "test-dep-rule".into(),
362 labels: vec!["source=test".into()],
363 effort: 1,
364 category: "mandatory".into(),
365 description: "Update dep".into(),
366 message: "Update this dependency".into(),
367 links: vec![],
368 when: KonveyorCondition::FrontendDependency {
369 dependency: FrontendDependencyFields {
370 name: Some("@patternfly/react-core".into()),
371 nameregex: None,
372 upperbound: Some("5.99.99".into()),
373 lowerbound: None,
374 },
375 },
376 fix_strategy: None,
377 };
378 let yaml = serde_yaml::to_string(&rule).unwrap();
379 assert!(yaml.contains("frontend.dependency:"));
380 assert!(
381 yaml.contains("name: '@patternfly/react-core'")
382 || yaml.contains("name: \"@patternfly/react-core\"")
383 || yaml.contains("name: '@patternfly/react-core'")
384 );
385 }
386}