Skip to content

Commit dadb8ce

Browse files
committed
feat(regex-validation): add new plugin regex-validation
1 parent 50774cd commit dadb8ce

File tree

8 files changed

+355
-0
lines changed

8 files changed

+355
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: 'Check Pull Request for plugin: regex-validation'
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
paths:
7+
- 'regex-validation/**'
8+
9+
jobs:
10+
tests:
11+
name: Unit tests
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- name: Set up JDK 21 for x64
16+
uses: actions/setup-java@v4
17+
with:
18+
java-version: '21'
19+
distribution: 'temurin'
20+
architecture: x64
21+
22+
- name: Install Pandoc
23+
run: sudo apt-get update && sudo apt-get install -y pandoc
24+
25+
- name: Execute unit tests
26+
run: mvn -ntp -pl regex-validation test
27+
28+
- name: Execute mutation tests
29+
run: mvn -ntp -pl regex-validation org.pitest:pitest-maven:mutationCoverage
30+
31+
- name: Extract summary from pitest
32+
run: |
33+
echo "<html><head></head><body><h1>Pit Test Coverage Report: regex-validation</h1><h3>Project Summary</h3>" > pitest.html
34+
perl -0777 -ne 'print "$1\n" if /(<table>.*?<\/table>)/s' regex-validation/target/pit-reports/index.html >> pitest.html
35+
echo "</body></html>" >> pitest.html
36+
37+
- name: Convert pitest report to markdown
38+
run: pandoc --from html --to markdown_github --no-highlight pitest.html
39+
40+
- name: Post comment
41+
uses: luukkemp/pr-comment@2024.1
42+
env:
43+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
with:
45+
path: pitest.html

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<modules>
1414
<module>template-plugin</module>
1515
<module>context-mapper-task</module>
16+
<module>regex-validation</module>
1617
</modules>
1718

1819
<licenses>

regex-validation/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# 🧪 RegexValidationPlugin
2+
3+
The `RegexValidationPlugin` is a validation plugin used to ensure that a value matches a specified regular expression pattern.
4+
5+
## ✅ Use Case
6+
7+
Use this plugin when:
8+
9+
* You need to validate that a value matches a specific regex pattern
10+
* You want the validation to optionally ignore case sensitivity
11+
* You want to easily enforce custom format rules in your flows
12+
13+
## 🔧 Configuration
14+
15+
### Minimal Example
16+
17+
```yaml
18+
- type: regex
19+
name: myRegexValidator
20+
pattern: ^[a-z0-9_-]{3,16}$
21+
```
22+
23+
This will validate that the value contains 3 to 16 characters consisting of lowercase letters, digits, hyphens, or underscores.
24+
25+
### Full Example
26+
27+
```yaml
28+
- type: regex
29+
name: myRegexValidator
30+
pattern: ^hello.*world$
31+
insensitive: true
32+
```
33+
34+
This will validate values against the regex `^hello.*world$`, ignoring case (e.g., `Hello my World`, `hello WORLD`, etc.).
35+
36+
### Configuration Fields
37+
38+
| Key | Required | Description |
39+
| ------------- | -------- | ---------------------------------------------------------------------------- |
40+
| `pattern` | ✅ | The regular expression used to validate the value. |
41+
| `insensitive` | ❌ | Boolean flag to enable case-insensitive matching (`true`). Default: `false`. |
42+
43+
## 🛠 Behavior
44+
45+
* The plugin retrieves the value to validate from the context (as defined by the hosting engine).
46+
* It applies the provided `pattern` to the value.
47+
* If `insensitive` is set to `true`, case is ignored during validation.
48+
* If the value does not match the pattern, a validation error is raised.
49+
50+
## 🧷 Notes
51+
52+
* The plugin uses Java regular expressions (`java.util.regex`).
53+
* If the `pattern` is invalid, a configuration error will be raised at startup.
54+
* The `insensitive` field is optional and defaults to `false`.

regex-validation/pom.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>io.github.linagora.linid.im</groupId>
9+
<artifactId>linid-im-api-community-plugins</artifactId>
10+
<version>0.1.0</version>
11+
<relativePath>../pom.xml</relativePath>
12+
</parent>
13+
14+
<properties>
15+
<sonar.projectBaseDir>regex-validation</sonar.projectBaseDir>
16+
</properties>
17+
18+
<artifactId>regex-validation-plugin</artifactId>
19+
<version>0.1.0</version>
20+
<name>regex-validation-plugin</name>
21+
<description>Plugin to validate field with regex</description>
22+
23+
<dependencies>
24+
<!-- Add here dependencies specific to this plugin if needed -->
25+
</dependencies>
26+
27+
</project>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (C) 2020-2025 Linagora
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
5+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
6+
* any later version, provided you comply with the Additional Terms applicable for LinID Directory Manager software by
7+
* LINAGORA pursuant to Section 7 of the GNU Affero General Public License, subsections (b), (c), and (e), pursuant to
8+
* which these Appropriate Legal Notices must notably (i) retain the display of the "LinID™" trademark/logo at the top
9+
* of the interface window, the display of the “You are using the Open Source and free version of LinID™, powered by
10+
* Linagora © 2009–2013. Contribute to LinID R&D by subscribing to an Enterprise offer!” infobox and in the e-mails
11+
* sent with the Program, notice appended to any type of outbound messages (e.g. e-mail and meeting requests) as well
12+
* as in the LinID Directory Manager user interface, (ii) retain all hypertext links between LinID Directory Manager
13+
* and https://linid.org/, as well as between LINAGORA and LINAGORA.com, and (iii) refrain from infringing LINAGORA
14+
* intellectual property rights over its trademarks and commercial brands. Other Additional Terms apply, see
15+
* <http://www.linagora.com/licenses/> for more details.
16+
*
17+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
18+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19+
* details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License and its applicable Additional Terms for
22+
* LinID Directory Manager along with this program. If not, see <http://www.gnu.org/licenses/> for the GNU Affero
23+
* General Public License version 3 and <http://www.linagora.com/licenses/> for the Additional Terms applicable to the
24+
* LinID Directory Manager software.
25+
*/
26+
27+
package io.github.linagora.linid.im.rvp;
28+
29+
import java.util.Map;
30+
import java.util.Optional;
31+
import java.util.regex.Pattern;
32+
import java.util.regex.PatternSyntaxException;
33+
import io.github.linagora.linid.im.corelib.exception.ApiException;
34+
import io.github.linagora.linid.im.corelib.i18n.I18nMessage;
35+
import io.github.linagora.linid.im.corelib.plugin.config.dto.ValidationConfiguration;
36+
import io.github.linagora.linid.im.corelib.plugin.validation.ValidationPlugin;
37+
import org.springframework.lang.NonNull;
38+
import org.springframework.stereotype.Component;
39+
40+
41+
/**
42+
* A {@link ValidationPlugin} implementation that validates a value using a regular expression.
43+
*/
44+
@Component
45+
public class RegexValidationPlugin implements ValidationPlugin {
46+
47+
@Override
48+
public boolean supports(@NonNull String type) {
49+
return "regex".equals(type);
50+
}
51+
52+
@Override
53+
public Optional<I18nMessage> validate(ValidationConfiguration configuration, Object value) {
54+
var regex = configuration.getOption("pattern")
55+
.orElseThrow(() -> new ApiException(
56+
500,
57+
I18nMessage.of("error.plugin.default.missing.option", Map.of("option", "pattern"))
58+
));
59+
boolean insensitive = configuration.getOption("insensitive", Boolean.class)
60+
.orElse(false);
61+
62+
Pattern pattern;
63+
64+
try {
65+
if (insensitive) {
66+
pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
67+
} else {
68+
pattern = Pattern.compile(regex);
69+
}
70+
71+
} catch (PatternSyntaxException exception) {
72+
throw new ApiException(
73+
500,
74+
I18nMessage.of("error.plugin.default.invalid.option", Map.of(
75+
"option", "pattern",
76+
"value", regex
77+
))
78+
);
79+
}
80+
81+
if (pattern.matcher(value.toString()).matches()) {
82+
return Optional.empty();
83+
}
84+
85+
return Optional.of(I18nMessage.of(
86+
"error.plugin.regexValidation.invalid.value",
87+
Map.of("pattern", regex, "value", value)
88+
));
89+
}
90+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright (C) 2020-2025 Linagora
3+
*
4+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
5+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
6+
* any later version, provided you comply with the Additional Terms applicable for LinID Directory Manager software by
7+
* LINAGORA pursuant to Section 7 of the GNU Affero General Public License, subsections (b), (c), and (e), pursuant to
8+
* which these Appropriate Legal Notices must notably (i) retain the display of the "LinID™" trademark/logo at the top
9+
* of the interface window, the display of the “You are using the Open Source and free version of LinID™, powered by
10+
* Linagora © 2009–2013. Contribute to LinID R&D by subscribing to an Enterprise offer!” infobox and in the e-mails
11+
* sent with the Program, notice appended to any type of outbound messages (e.g. e-mail and meeting requests) as well
12+
* as in the LinID Directory Manager user interface, (ii) retain all hypertext links between LinID Directory Manager
13+
* and https://linid.org/, as well as between LINAGORA and LINAGORA.com, and (iii) refrain from infringing LINAGORA
14+
* intellectual property rights over its trademarks and commercial brands. Other Additional Terms apply, see
15+
* <http://www.linagora.com/licenses/> for more details.
16+
*
17+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
18+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19+
* details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License and its applicable Additional Terms for
22+
* LinID Directory Manager along with this program. If not, see <http://www.gnu.org/licenses/> for the GNU Affero
23+
* General Public License version 3 and <http://www.linagora.com/licenses/> for the Additional Terms applicable to the
24+
* LinID Directory Manager software.
25+
*/
26+
27+
package io.github.linagora.linid.im.rvp;
28+
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertFalse;
31+
import static org.junit.jupiter.api.Assertions.assertNotNull;
32+
import static org.junit.jupiter.api.Assertions.assertThrows;
33+
import static org.junit.jupiter.api.Assertions.assertTrue;
34+
35+
import java.util.Map;
36+
import org.junit.jupiter.api.DisplayName;
37+
import org.junit.jupiter.api.Test;
38+
import io.github.linagora.linid.im.corelib.exception.ApiException;
39+
import io.github.linagora.linid.im.corelib.plugin.config.dto.ValidationConfiguration;
40+
41+
@DisplayName("Test class: RegexValidationPlugin")
42+
public class RegexValidationPluginTest {
43+
44+
@Test
45+
@DisplayName("test supports: should return true on valid type")
46+
void testSupports() {
47+
var plugin = new RegexValidationPlugin();
48+
49+
assertTrue(plugin.supports("regex"));
50+
assertFalse(plugin.supports("other"));
51+
}
52+
53+
@Test
54+
@DisplayName("test validate: should thrown an exception on missing pattern option")
55+
void testValidateMissingPatternOption() {
56+
var plugin = new RegexValidationPlugin();
57+
var configuration = new ValidationConfiguration();
58+
59+
ApiException exception = assertThrows(ApiException.class, () -> plugin.validate(configuration, ""));
60+
61+
assertEquals(500, exception.getStatusCode());
62+
assertEquals("error.plugin.default.missing.option", exception.getError().key());
63+
assertEquals(Map.of("option", "pattern"), exception.getError().context());
64+
}
65+
66+
@Test
67+
@DisplayName("test validate: should thrown an exception on invalid pattern option")
68+
void testValidateInvalidPatternOption() {
69+
var plugin = new RegexValidationPlugin();
70+
var configuration = new ValidationConfiguration();
71+
configuration.addOption("pattern", "[a-z");
72+
73+
ApiException exception = assertThrows(ApiException.class, () -> plugin.validate(configuration, ""));
74+
75+
assertEquals(500, exception.getStatusCode());
76+
assertEquals("error.plugin.default.invalid.option", exception.getError().key());
77+
assertEquals(Map.of("option", "pattern", "value", "[a-z"), exception.getError().context());
78+
}
79+
80+
@Test
81+
@DisplayName("test validate: should thrown an exception on invalid insensitive pattern option")
82+
void testValidateInvalidInsensitivePatternOption() {
83+
var plugin = new RegexValidationPlugin();
84+
var configuration = new ValidationConfiguration();
85+
configuration.addOption("pattern", "[a-z");
86+
configuration.addOption("insensitive", true);
87+
88+
ApiException exception = assertThrows(ApiException.class, () -> plugin.validate(configuration, ""));
89+
90+
assertEquals(500, exception.getStatusCode());
91+
assertEquals("error.plugin.default.invalid.option", exception.getError().key());
92+
assertEquals(Map.of("option", "pattern", "value", "[a-z"), exception.getError().context());
93+
}
94+
95+
@Test
96+
@DisplayName("test validate: should return message on invalid value")
97+
void testValidateInvalidValue() {
98+
var plugin = new RegexValidationPlugin();
99+
var configuration = new ValidationConfiguration();
100+
configuration.addOption("pattern", "[a-z]{1}");
101+
102+
var error = plugin.validate(configuration, "A");
103+
104+
assertNotNull(error);
105+
assertTrue(error.isPresent());
106+
assertEquals("error.plugin.regexValidation.invalid.value", error.get().key());
107+
assertEquals(Map.of("pattern", "[a-z]{1}", "value", "A"), error.get().context());
108+
}
109+
110+
@Test
111+
@DisplayName("test validate: should not return message on valid value")
112+
void testValidateValidValue() {
113+
var plugin = new RegexValidationPlugin();
114+
var configuration = new ValidationConfiguration();
115+
configuration.addOption("pattern", "[a-z]{1}");
116+
117+
var error = plugin.validate(configuration, "a");
118+
119+
assertNotNull(error);
120+
assertTrue(error.isEmpty());
121+
}
122+
123+
@Test
124+
@DisplayName("test validate: should not return message on insensitive valid value")
125+
void testValidateInsensitiveValidValue() {
126+
var plugin = new RegexValidationPlugin();
127+
var configuration = new ValidationConfiguration();
128+
configuration.addOption("pattern", "[a-z]{1}");
129+
configuration.addOption("insensitive", true);
130+
131+
var error = plugin.validate(configuration, "A");
132+
133+
assertNotNull(error);
134+
assertTrue(error.isEmpty());
135+
}
136+
}

0 commit comments

Comments
 (0)