diff --git a/src/git.rs b/src/git.rs index e54e26e..01ad22f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -118,19 +118,19 @@ pub fn parse_commit_message( /// Note that exclamation mark is not respected as the existing commitlint /// does not have any rules for it. /// See: https://commitlint.js.org/#/reference-rules -pub fn parse_subject(subject: &str) -> (String, Option, String) { +pub fn parse_subject(subject: &str) -> (Option, Option, Option) { let re = regex::Regex::new(r"^(?P\w+)(?:\((?P[^\)]+)\))?(!)?\:\s(?P.+)$") .unwrap(); if let Some(captures) = re.captures(subject) { - let r#type = captures.name("type").unwrap().as_str().to_string(); + let r#type = captures.name("type").map(|m| m.as_str().to_string()); let scope = captures.name("scope").map(|m| m.as_str().to_string()); - let description = captures.name("description").unwrap().as_str().to_string(); + let description = captures.name("description").map(|m| m.as_str().to_string()); return (r#type, scope, description); } // Fall back to the description. - ("".to_string(), None, subject.to_string()) + (None, None, Some(subject.to_string())) } #[cfg(test)] @@ -200,9 +200,9 @@ Name: Keke"; assert_eq!( parse_subject(input), ( - "feat".to_string(), + Some("feat".to_string()), Some("cli".to_string()), - "add dummy option".to_string() + Some("add dummy option".to_string()) ) ); } @@ -213,9 +213,9 @@ Name: Keke"; assert_eq!( parse_subject(input), ( - "feat".to_string(), + Some("feat".to_string()), Some("cli".to_string()), - "add dummy option".to_string() + Some("add dummy option".to_string()) ) ); } @@ -225,29 +225,35 @@ Name: Keke"; let input = "feat: add dummy option"; assert_eq!( parse_subject(input), - ("feat".to_string(), None, "add dummy option".to_string()) + ( + Some("feat".to_string()), + None, + Some("add dummy option".to_string()) + ) ); } + #[test] fn test_parse_subject_with_emphasized_type_without_scope() { let input = "feat!: add dummy option"; assert_eq!( parse_subject(input), - ("feat".to_string(), None, "add dummy option".to_string()) + ( + Some("feat".to_string()), + None, + Some("add dummy option".to_string()) + ) ); } #[test] fn test_parse_subject_without_message() { let input = ""; - assert_eq!(parse_subject(input), ("".to_string(), None, "".to_string())); + assert_eq!(parse_subject(input), (None, None, Some("".to_string()))); } #[test] fn test_parse_subject_with_error_message() { let input = "test"; - assert_eq!( - parse_subject(input), - ("".to_string(), None, "test".to_string()) - ); + assert_eq!(parse_subject(input), (None, None, Some("test".to_string()))); } } diff --git a/src/message.rs b/src/message.rs index 527ec91..5f0dbcc 100644 --- a/src/message.rs +++ b/src/message.rs @@ -48,10 +48,10 @@ impl Message { let (r#type, scope, description) = parse_subject(&subject); Self { body, - description: Some(description), + description, footers, raw, - r#type: Some(r#type), + r#type, scope, subject: Some(subject), } diff --git a/src/rule/scope.rs b/src/rule/scope.rs index 2cf307a..a0fdd06 100644 --- a/src/rule/scope.rs +++ b/src/rule/scope.rs @@ -35,10 +35,21 @@ impl Rule for Scope { } fn validate(&self, message: &Message) -> Option { - if let Some(scope) = &message.scope { - if self.options.contains(scope) { + match &message.scope { + None => { + if self.options.is_empty() { + return None; + } + } + Some(scope) if scope.is_empty() => { + if self.options.is_empty() { + return None; + } + } + Some(scope) if self.options.contains(scope) => { return None; } + _ => {} } Some(Violation { @@ -62,92 +73,159 @@ impl Default for Scope { mod tests { use super::*; - #[test] - fn test_empty_scope() { - let mut rule = Scope::default(); - rule.options = vec!["api".to_string(), "web".to_string()]; - - let message = Message { - body: None, - description: None, - footers: None, - r#type: None, - raw: "".to_string(), - scope: None, - subject: None, - }; - - let violation = rule.validate(&message); - assert!(violation.is_some()); - assert_eq!(violation.clone().unwrap().level, Level::Error); - assert_eq!( - violation.unwrap().message, - "scope is not allowed. Only [\"api\", \"web\"] are allowed" - ); - } + mod empty_options { + use super::*; + + #[test] + fn test_empty_scope() { + let rule = Scope::default(); + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: Some("".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_none()); + } - #[test] - fn test_valid_scope() { - let mut rule = Scope::default(); - rule.options = vec!["api".to_string(), "web".to_string()]; - - let message = Message { - body: None, - description: None, - footers: None, - r#type: Some("feat".to_string()), - raw: "feat(web): broadcast $destroy event on scope destruction".to_string(), - scope: Some("web".to_string()), - subject: None, - }; - - assert!(rule.validate(&message).is_none()); - } + #[test] + fn test_none_scope() { + let rule = Scope::default(); + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: None, + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_none()); + } - #[test] - fn test_invalid_scope() { - let mut rule = Scope::default(); - rule.options = vec!["api".to_string(), "web".to_string()]; - - let message = Message { - body: None, - description: None, - footers: None, - r#type: Some("feat".to_string()), - raw: "feat(invalid): broadcast $destroy event on scope destruction".to_string(), - scope: Some("invalid".to_string()), - subject: None, - }; - - let violation = rule.validate(&message); - assert!(violation.is_some()); - assert_eq!(violation.clone().unwrap().level, Level::Error); - assert_eq!( - violation.unwrap().message, - "scope invalid is not allowed. Only [\"api\", \"web\"] are allowed".to_string() - ); + #[test] + fn test_scope() { + let rule = Scope::default(); + + let message = Message { + body: None, + description: None, + footers: None, + r#type: Some("feat".to_string()), + raw: "feat(web): broadcast $destroy event on scope destruction".to_string(), + scope: Some("web".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "scopes are not allowed".to_string() + ); + } } - #[test] - fn test_no_options() { - let rule = Scope::default(); - - let message = Message { - body: None, - description: None, - footers: None, - r#type: Some("feat".to_string()), - raw: "feat(invalid): broadcast $destroy event on scope destruction".to_string(), - scope: Some("invalid".to_string()), - subject: None, - }; - - let violation = rule.validate(&message); - assert!(violation.is_some()); - assert_eq!(violation.clone().unwrap().level, Level::Error); - assert_eq!( - violation.unwrap().message, - "scopes are not allowed".to_string() - ); + mod scopes { + use super::*; + #[test] + fn test_empty_scope() { + let mut rule = Scope::default(); + rule.options = vec!["api".to_string(), "web".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: Some("".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "scope is not allowed. Only [\"api\", \"web\"] are allowed" + ); + } + + #[test] + fn test_none_scope() { + let mut rule = Scope::default(); + rule.options = vec!["api".to_string(), "web".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: None, + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "scope is not allowed. Only [\"api\", \"web\"] are allowed".to_string() + ); + } + + #[test] + fn test_valid_scope() { + let mut rule = Scope::default(); + rule.options = vec!["api".to_string(), "web".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: Some("feat".to_string()), + raw: "feat(web): broadcast $destroy event on scope destruction".to_string(), + scope: Some("web".to_string()), + subject: None, + }; + + assert!(rule.validate(&message).is_none()); + } + + #[test] + fn test_invalid_scope() { + let mut rule = Scope::default(); + rule.options = vec!["api".to_string(), "web".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: Some("feat".to_string()), + raw: "feat(invalid): broadcast $destroy event on scope destruction".to_string(), + scope: Some("invalid".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "scope invalid is not allowed. Only [\"api\", \"web\"] are allowed".to_string() + ); + } } } diff --git a/src/rule/type.rs b/src/rule/type.rs index 2043d53..ba4f4b5 100644 --- a/src/rule/type.rs +++ b/src/rule/type.rs @@ -34,10 +34,21 @@ impl Rule for Type { } fn validate(&self, message: &Message) -> Option { - if let Some(t) = &message.r#type { - if self.options.contains(t) { + match &message.r#type { + None => { + if self.options.is_empty() { + return None; + } + } + Some(r#type) if r#type.is_empty() => { + if self.options.is_empty() { + return None; + } + } + Some(r#type) if self.options.contains(r#type) => { return None; } + _ => {} } Some(Violation { @@ -61,133 +72,159 @@ impl Default for Type { mod tests { use super::*; - #[test] - fn test_empty_type() { - let mut rule = Type::default(); - rule.options = vec!["doc".to_string(), "feat".to_string()]; - - let message = Message { - body: None, - description: None, - footers: None, - r#type: None, - raw: "".to_string(), - scope: None, - subject: None, - }; - assert_eq!(rule.validate(&message).unwrap().level, Level::Error); - assert_eq!( - rule.validate(&message).unwrap().message, - "type is not allowed. Only [\"doc\", \"feat\"] are allowed".to_string() - ); - } + mod empty_options { + use super::*; + + #[test] + fn test_empty_type() { + let rule = Type::default(); + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: Some("".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_none()); + } - #[test] - fn test_invalid_type() { - let mut rule = Type::default(); - rule.options = vec!["doc".to_string(), "feat".to_string()]; - - let message = Message { - body: None, - description: None, - footers: None, - r#type: Some("invalid".to_string()), - raw: "invalid(scope): broadcast $destroy event on scope destruction".to_string(), - scope: None, - subject: None, - }; - - let violation = rule.validate(&message); - assert!(violation.is_some()); - assert_eq!(violation.clone().unwrap().level, Level::Error); - assert_eq!( - violation.unwrap().message, - "type invalid is not allowed. Only [\"doc\", \"feat\"] are allowed".to_string() - ); - } + #[test] + fn test_none_type() { + let rule = Type::default(); + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: None, + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_none()); + } - #[test] - fn test_no_options() { - let rule = Type::default(); - - let message = Message { - body: None, - description: None, - footers: None, - r#type: Some("invalid".to_string()), - raw: "invalid(scope): broadcast $destroy event on scope destruction".to_string(), - scope: None, - subject: None, - }; - - let violation = rule.validate(&message); - assert!(violation.is_some()); - assert_eq!(violation.clone().unwrap().level, Level::Error); - assert_eq!( - violation.unwrap().message, - "types are not allowed".to_string() - ); + #[test] + fn test_type() { + let rule = Type::default(); + + let message = Message { + body: None, + description: None, + footers: None, + r#type: Some("feat".to_string()), + raw: "feat(web): broadcast $destroy event on scope destruction".to_string(), + scope: Some("web".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "types are not allowed".to_string() + ); + } } - #[test] - fn test_no_options_with_empty_type() { - let rule = Type::default(); - - let message = Message { - body: None, - description: None, - footers: None, - r#type: None, - raw: "(scope): broadcast $destroy event on scope destruction".to_string(), - scope: None, - subject: None, - }; - - let violation = rule.validate(&message); - assert!(violation.is_some()); - assert_eq!(violation.clone().unwrap().level, Level::Error); - assert_eq!( - violation.unwrap().message, - "types are not allowed".to_string() - ); - } + mod scopes { + use super::*; + #[test] + fn test_empty_type() { + let mut rule = Type::default(); + rule.options = vec!["feat".to_string(), "chore".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: Some("".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "type is not allowed. Only [\"feat\", \"chore\"] are allowed" + ); + } - #[test] - fn test_missing_type() { - let rule = Type::default(); - let input = "test".to_string(); - - let message = Message { - body: None, - description: None, - footers: None, - r#type: None, - raw: input.clone(), - scope: None, - subject: None, - }; - assert_eq!(rule.validate(&message).unwrap().level, Level::Error); - assert_eq!( - rule.validate(&message).unwrap().message, - "types are not allowed".to_string() - ); - } + #[test] + fn test_none_type() { + let mut rule = Type::default(); + rule.options = vec!["feat".to_string(), "chore".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: None, + raw: "".to_string(), + scope: None, + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "type is not allowed. Only [\"feat\", \"chore\"] are allowed".to_string() + ); + } - #[test] - fn test_valid_type() { - let mut rule = Type::default(); - rule.options = vec!["doc".to_string(), "feat".to_string()]; - - let message = Message { - body: None, - description: None, - footers: None, - r#type: Some("feat".to_string()), - raw: "feat(scope): broadcast $destroy event on scope destruction".to_string(), - scope: None, - subject: None, - }; - - assert!(rule.validate(&message).is_none()); + #[test] + fn test_valid_type() { + let mut rule = Type::default(); + rule.options = vec!["feat".to_string(), "chore".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: Some("feat".to_string()), + raw: "feat(web): broadcast $destroy event on scope destruction".to_string(), + scope: Some("web".to_string()), + subject: None, + }; + + assert!(rule.validate(&message).is_none()); + } + + #[test] + fn test_invalid_type() { + let mut rule = Type::default(); + rule.options = vec!["feat".to_string(), "chore".to_string()]; + + let message = Message { + body: None, + description: None, + footers: None, + r#type: Some("invalid".to_string()), + raw: "invalid(web): broadcast $destroy event on scope destruction".to_string(), + scope: Some("web".to_string()), + subject: None, + }; + + let violation = rule.validate(&message); + assert!(violation.is_some()); + assert_eq!(violation.clone().unwrap().level, Level::Error); + assert_eq!( + violation.unwrap().message, + "type invalid is not allowed. Only [\"feat\", \"chore\"] are allowed".to_string() + ); + } } }