Skip to content

Commit dc458ea

Browse files
ognis1205orhun
andauthored
feat(parser): support regex matching on JSON arrays with scalar elements (#1163)
* feat(parser): support regex matching on JSON arrays with scalar elements Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * test: improve code coverage Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * refactor: improve code readability Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * test: fix broken unit test Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * refactor: improve code readability Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * refactor: revert regex_checks to String and inline scalar array handling Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * test: add fixture test Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * fix(test): add new line Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * test: update fixture Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> * refactor: remove unnecessary comma --------- Signed-off-by: Shingo OKAWA <shingo.okawa.g.h.c@gmail.com> Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
1 parent deb29dc commit dc458ea

File tree

5 files changed

+312
-14
lines changed

5 files changed

+312
-14
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# git-cliff ~ configuration file
2+
# https://git-cliff.org/docs/configuration
3+
4+
[remote.github]
5+
owner = "mta-solutions"
6+
repo = "fsharp-data-validation"
7+
8+
[changelog]
9+
# A Tera template to be rendered for each release in the changelog.
10+
# See https://keats.github.io/tera/docs/#introduction
11+
body = """
12+
{% if version %}\
13+
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
14+
{% else %}\
15+
## [unreleased]
16+
{% endif %}\
17+
{% for group, commits in commits | unique(attribute="id") | filter(attribute="merge_commit", value=true) | group_by(attribute="group") %}
18+
### {{ group | upper_first }}
19+
{% for commit in commits %}
20+
- {{ commit.message | split(pat="\n") | nth(n=2) | trim_end }} \
21+
([PR #{{ commit.remote.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.remote.pr_number }}))\
22+
{% endfor %}
23+
{% endfor %}\n
24+
"""
25+
26+
[git]
27+
# Parse commits according to the conventional commits specification.
28+
# See https://www.conventionalcommits.org
29+
conventional_commits = false
30+
# Exclude commits that do not match the conventional commits specification.
31+
filter_unconventional = false
32+
# Split commits on newlines, treating each line as an individual commit.
33+
split_commits = false
34+
# An array of regex based parsers for extracting data from the commit message.
35+
# Assigns commits to groups.
36+
# Optionally sets the commit's `scope` and can decide to exclude commits from further processing.
37+
commit_parsers = [
38+
{ field = "remote.pr_labels", pattern = "duplicate|invalid|wontfix|skip changelog", skip = true },
39+
{ field = "remote.pr_labels", pattern = "breaking change", group = "<!-- 0 -->🏗️ Breaking Changes" },
40+
{ field = "remote.pr_labels", pattern = "feature|deprecation", group = "<!-- 1 -->🚀 Features" },
41+
{ field = "remote.pr_labels", pattern = "enhancement|refactor", group = "<!-- 1 -->🛠️ Enhancements" },
42+
{ field = "remote.pr_labels", pattern = "bug|regression", group = "<!-- 2 -->🐛 Bug Fixes" },
43+
{ field = "remote.pr_labels", pattern = "security", group = "<!-- 3 -->🔐 Security" },
44+
{ field = "remote.pr_labels", pattern = "documentation", group = "<!-- 4 -->📝 Documentation" },
45+
{ message = ".*", group = "<!-- 5 -->🌀 Miscellaneous" },
46+
]
47+
# Exclude commits that are not matched by any commit parser.
48+
filter_commits = true
49+
# Order of commits in each group/release within the changelog.
50+
# Allowed values: newest, oldest
51+
sort_commits = "newest"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
git remote add origin https://github.com/mta-solutions/fsharp-data-validation
5+
git pull origin main
6+
git fetch --tags
7+
git checkout 9201e2729ad3afb34171c493c2cb9984e9d64784
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [3.0.0] - 2025-01-25
6+
7+
### <!-- 5 -->🌀 Miscellaneous
8+
9+
- Drop Support for .NET 6.0 ([PR #27](https://github.com/mta-solutions/fsharp-data-validation/pull/27))
10+
11+
## [2.2.0] - 2024-12-13
12+
13+
### <!-- 1 -->🛠️ Enhancements
14+
15+
- Add additional VCtx functions ([PR #24](https://github.com/mta-solutions/fsharp-data-validation/pull/24))
16+
17+
### <!-- 5 -->🌀 Miscellaneous
18+
19+
- Prep FSharp.Data.Validation.Giraffe v2.0.1 Release ([PR #26](https://github.com/mta-solutions/fsharp-data-validation/pull/26))
20+
- Update CHANGELOG for version 2.2.0 ([PR #25](https://github.com/mta-solutions/fsharp-data-validation/pull/25))
21+
22+
## [2.1.0] - 2024-12-10
23+
24+
### <!-- 1 -->🛠️ Enhancements
25+
26+
- Remove internal scoping and add Async extension methods for validation contexts ([PR #22](https://github.com/mta-solutions/fsharp-data-validation/pull/22))
27+
28+
### <!-- 4 -->📝 Documentation
29+
30+
- Prep v2.1.0 Release ([PR #23](https://github.com/mta-solutions/fsharp-data-validation/pull/23))
31+
32+
## [2.0.0] - 2024-10-08
33+
34+
### <!-- 1 -->🛠️ Enhancements
35+
36+
- Use FSharpPlus ([PR #20](https://github.com/mta-solutions/fsharp-data-validation/pull/20))
37+
38+
### <!-- 4 -->📝 Documentation
39+
40+
- Prep for Release v2.0.0 ([PR #21](https://github.com/mta-solutions/fsharp-data-validation/pull/21))
41+
42+
## [giraffe-v1.0.0] - 2023-11-17
43+
44+
### <!-- 5 -->🌀 Miscellaneous
45+
46+
- Fix Giraffe Pipeline ([PR #19](https://github.com/mta-solutions/fsharp-data-validation/pull/19))
47+
48+
## [1.0.1] - 2023-11-17
49+
50+
### <!-- 1 -->🛠️ Enhancements
51+
52+
- Convert to NuGet ([PR #18](https://github.com/mta-solutions/fsharp-data-validation/pull/18))
53+
- Add Giraffe model validation library and examples ([PR #17](https://github.com/mta-solutions/fsharp-data-validation/pull/17))
54+
55+
## [1.0.0] - 2022-03-14
56+
57+
### <!-- 1 -->🛠️ Enhancements
58+
59+
- Add utility functions to help with using NonEmptyLists [MBI-53] ([PR #14](https://github.com/mta-solutions/fsharp-data-validation/pull/14))
60+
61+
### <!-- 5 -->🌀 Miscellaneous
62+
63+
- [MBI-48]: Wire Up Release Actions ([PR #15](https://github.com/mta-solutions/fsharp-data-validation/pull/15))
64+
- Add tests for DisputeWith, DisputeWithFact, RefuteWith, and RefuteWithProof [MBI-46] ([PR #13](https://github.com/mta-solutions/fsharp-data-validation/pull/13))
65+
- [MBI-47]: Add Documentation and NonEmptyList Type ([PR #12](https://github.com/mta-solutions/fsharp-data-validation/pull/12))
66+
- Add Documentation ([PR #11](https://github.com/mta-solutions/fsharp-data-validation/pull/11))
67+
- Rename Solution File ([PR #10](https://github.com/mta-solutions/fsharp-data-validation/pull/10))
68+
- Fixed random test failures ([PR #9](https://github.com/mta-solutions/fsharp-data-validation/pull/9))
69+
- [MBI-33] Added tests for the most interesting parts of VCtxBuilder ([PR #8](https://github.com/mta-solutions/fsharp-data-validation/pull/8))
70+
- [MBI-33] Renamed root namespace to FSharp.Data.Validation ([PR #7](https://github.com/mta-solutions/fsharp-data-validation/pull/7))
71+
- [MBI-33] Finished Proof and Library test coverage ([PR #6](https://github.com/mta-solutions/fsharp-data-validation/pull/6))
72+
- Add raiseIfInvalid Function ([PR #5](https://github.com/mta-solutions/fsharp-data-validation/pull/5))
73+
- [MBI-33] Broke out samples/src/tests and expanded unit testing ([PR #4](https://github.com/mta-solutions/fsharp-data-validation/pull/4))
74+
- [MBI-33]: Add Custom Serializer for Proof Type ([PR #3](https://github.com/mta-solutions/fsharp-data-validation/pull/3))
75+
- [MBI-33] Restructured modules for Proof, ValueCtx, VCtx ([PR #2](https://github.com/mta-solutions/fsharp-data-validation/pull/2))
76+
- [MBI-33] Initial unit tests ([PR #1](https://github.com/mta-solutions/fsharp-data-validation/pull/1))
77+
78+
<!-- generated by git-cliff -->

.github/workflows/test-fixtures.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ jobs:
129129
- fixtures-name: test-commit-range-with-given-range
130130
command: a140cef^..a9d4050 --ignore-tags "."
131131
- fixtures-name: test-override-scope
132+
- fixtures-name: test-regex-json-array
132133

133134
steps:
134135
- name: Checkout

git-cliff-core/src/commit.rs

Lines changed: 175 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -332,27 +332,47 @@ impl Commit<'_> {
332332
if let (Some(field_name), Some(pattern_regex)) =
333333
(parser.field.as_ref(), parser.pattern.as_ref())
334334
{
335-
let value = if field_name == "body" {
336-
body.clone()
335+
let values = if field_name == "body" {
336+
vec![body.clone()].into_iter().collect()
337337
} else {
338338
tera::dotted_pointer(&lookup_context, field_name).and_then(|v| {
339-
match &v {
340-
Value::String(s) => Some(s.clone()),
339+
match v {
340+
Value::String(s) => Some(vec![s.clone()]),
341341
Value::Number(_) | Value::Bool(_) | Value::Null => {
342-
Some(v.to_string())
342+
Some(vec![v.to_string()])
343+
}
344+
Value::Array(arr) => {
345+
let mut values = Vec::new();
346+
for item in arr {
347+
match item {
348+
Value::String(s) => values.push(s.clone()),
349+
Value::Number(_) |
350+
Value::Bool(_) |
351+
Value::Null => values.push(item.to_string()),
352+
_ => continue,
353+
}
354+
}
355+
Some(values)
343356
}
344357
_ => None,
345358
}
346359
})
347360
};
348-
match value {
349-
Some(value) => {
350-
regex_checks.push((pattern_regex, value));
361+
match values {
362+
Some(values) => {
363+
if values.is_empty() {
364+
trace!("field '{field_name}' is present but empty");
365+
} else {
366+
for value in values {
367+
regex_checks.push((pattern_regex, value));
368+
}
369+
}
351370
}
352371
None => {
353372
return Err(AppError::FieldError(format!(
354373
"field '{field_name}' is missing or has unsupported \
355-
type (expected String, Number, Bool, or Null)",
374+
type (expected a String, Number, Bool, or Null — or \
375+
an Array of these scalar values)",
356376
)));
357377
}
358378
}
@@ -727,11 +747,83 @@ mod test {
727747
);
728748
}
729749

750+
#[test]
751+
fn parse_body() -> Result<()> {
752+
let mut commit = Commit::new(
753+
String::from("8f55e69eba6e6ce811ace32bd84cc82215673cb6"),
754+
String::from(
755+
"fix: do something
756+
757+
Introduce something great
758+
759+
BREAKING CHANGE: drop support for something else
760+
Refs: #123
761+
",
762+
),
763+
);
764+
commit.author = Signature {
765+
name: Some("John Doe".to_string()),
766+
email: None,
767+
timestamp: 0x0,
768+
};
769+
commit.remote = Some(crate::contributor::RemoteContributor {
770+
username: None,
771+
pr_title: Some("feat: do something".to_string()),
772+
pr_number: None,
773+
pr_labels: vec![
774+
String::from("feature"),
775+
String::from("deprecation"),
776+
],
777+
is_first_time: true,
778+
});
779+
let commit = commit.into_conventional()?;
780+
let commit = commit.parse_links(&[
781+
LinkParser {
782+
pattern: Regex::new("RFC(\\d+)")?,
783+
href: String::from("rfc://$1"),
784+
text: None,
785+
},
786+
LinkParser {
787+
pattern: Regex::new("#(\\d+)")?,
788+
href: String::from("https://github.com/$1"),
789+
text: None,
790+
},
791+
])?;
792+
793+
let parsed_commit = commit.clone().parse(
794+
&[CommitParser {
795+
sha: None,
796+
message: None,
797+
body: Regex::new("something great").ok(),
798+
footer: None,
799+
group: Some(String::from("Test group")),
800+
default_scope: None,
801+
scope: None,
802+
skip: None,
803+
field: None,
804+
pattern: None,
805+
}],
806+
false,
807+
false,
808+
)?;
809+
assert_eq!(Some(String::from("Test group")), parsed_commit.group);
810+
811+
Ok(())
812+
}
813+
730814
#[test]
731815
fn parse_commit_field() -> Result<()> {
732816
let mut commit = Commit::new(
733817
String::from("8f55e69eba6e6ce811ace32bd84cc82215673cb6"),
734-
String::from("feat: do something"),
818+
String::from(
819+
"fix: do something
820+
821+
Introduce something great
822+
823+
BREAKING CHANGE: drop support for something else
824+
Refs: #123
825+
",
826+
),
735827
);
736828
commit.author = Signature {
737829
name: Some("John Doe".to_string()),
@@ -742,9 +834,25 @@ mod test {
742834
username: None,
743835
pr_title: Some("feat: do something".to_string()),
744836
pr_number: None,
745-
pr_labels: Vec::new(),
837+
pr_labels: vec![
838+
String::from("feature"),
839+
String::from("deprecation"),
840+
],
746841
is_first_time: true,
747842
});
843+
let commit = commit.into_conventional()?;
844+
let commit = commit.parse_links(&[
845+
LinkParser {
846+
pattern: Regex::new("RFC(\\d+)")?,
847+
href: String::from("rfc://$1"),
848+
text: None,
849+
},
850+
LinkParser {
851+
pattern: Regex::new("#(\\d+)")?,
852+
href: String::from("https://github.com/$1"),
853+
text: None,
854+
},
855+
])?;
748856

749857
let parsed_commit = commit.clone().parse(
750858
&[CommitParser {
@@ -782,7 +890,25 @@ mod test {
782890
)?;
783891
assert_eq!(Some(String::from("Test group")), parsed_commit.group);
784892

785-
let parse_result = commit.clone().parse(
893+
let parsed_commit = commit.clone().parse(
894+
&[CommitParser {
895+
sha: None,
896+
message: None,
897+
body: None,
898+
footer: None,
899+
group: Some(String::from("Test group")),
900+
default_scope: None,
901+
scope: None,
902+
skip: None,
903+
field: Some(String::from("body")),
904+
pattern: Regex::new("something great").ok(),
905+
}],
906+
false,
907+
false,
908+
)?;
909+
assert_eq!(Some(String::from("Test group")), parsed_commit.group);
910+
911+
let parsed_commit = commit.clone().parse(
786912
&[CommitParser {
787913
sha: None,
788914
message: None,
@@ -793,15 +919,50 @@ mod test {
793919
scope: None,
794920
skip: None,
795921
field: Some(String::from("remote.pr_labels")),
922+
pattern: Regex::new("feature|deprecation").ok(),
923+
}],
924+
false,
925+
false,
926+
)?;
927+
assert_eq!(Some(String::from("Test group")), parsed_commit.group);
928+
929+
let parsed_commit = commit.clone().parse(
930+
&[CommitParser {
931+
sha: None,
932+
message: None,
933+
body: None,
934+
footer: None,
935+
group: Some(String::from("Test group")),
936+
default_scope: None,
937+
scope: None,
938+
skip: None,
939+
field: Some(String::from("links")),
940+
pattern: Regex::new(".*").ok(),
941+
}],
942+
false,
943+
false,
944+
)?;
945+
assert_eq!(None, parsed_commit.group);
946+
947+
let parse_result = commit.clone().parse(
948+
&[CommitParser {
949+
sha: None,
950+
message: None,
951+
body: None,
952+
footer: None,
953+
group: Some(String::from("Test group")),
954+
default_scope: None,
955+
scope: None,
956+
skip: None,
957+
field: Some(String::from("remote")),
796958
pattern: Regex::new(".*").ok(),
797959
}],
798960
false,
799961
false,
800962
);
801963
assert!(
802964
parse_result.is_err(),
803-
"Expected error when using unsupported field `remote.pr_labels`, but \
804-
got Ok"
965+
"Expected error when using unsupported field `remote`, but got Ok"
805966
);
806967

807968
Ok(())

0 commit comments

Comments
 (0)