Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions base/src/main/java/io/spine/validate/MessageValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import io.spine.code.proto.FieldContext;
import io.spine.code.proto.FieldDeclaration;
import io.spine.code.proto.FieldName;
import io.spine.option.PatternOption;
import io.spine.type.MessageType;
import io.spine.type.TypeName;
import io.spine.validate.option.DistinctConstraint;
Expand All @@ -45,6 +44,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.checkNotNull;
Expand Down Expand Up @@ -106,18 +106,32 @@ public void visitRequired(RequiredConstraint constraint) {
@Override
public void visitPattern(PatternConstraint constraint) {
FieldValue fieldValue = message.valueOf(constraint.field());
PatternOption pattern = constraint.optionValue();
String regex = pattern.getRegex();
Pattern compiledPattern = Pattern.compile(regex);
String regex = constraint.regex();
int flags = constraint.flagsMask();
@SuppressWarnings("MagicConstant")
Pattern compiledPattern = Pattern.compile(regex, flags);
boolean partialMatch = constraint.allowsPartialMatch();
fieldValue.nonDefault()
.filter(value -> !compiledPattern.matcher((CharSequence) value).matches())
.filter(value -> partialMatch
? noPartialMatch(compiledPattern, (String) value)
: noCompleteMatch(compiledPattern, (String) value))
.map(value -> violation(constraint, fieldValue, value)
.toBuilder()
.addParam(regex)
.build())
.forEach(violations::add);
}

private static boolean noCompleteMatch(Pattern pattern, String value) {
Matcher matcher = pattern.matcher(value);
return !matcher.matches();
}

private static boolean noPartialMatch(Pattern pattern, String value) {
Matcher matcher = pattern.matcher(value);
return !matcher.find();
}

@Override
public void visitDistinct(DistinctConstraint constraint) {
FieldValue fieldValue = message.valueOf(constraint.field());
Expand Down
49 changes: 49 additions & 0 deletions base/src/main/java/io/spine/validate/option/PatternConstraint.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@
import io.spine.code.proto.FieldContext;
import io.spine.code.proto.FieldDeclaration;
import io.spine.option.PatternOption;
import io.spine.option.PatternOption.Modifier;
import io.spine.validate.ConstraintTranslator;
import io.spine.validate.diags.ViolationText;

import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.MULTILINE;
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;

/**
* A constraint, which when applied to a string field, checks whether that field matches the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document the public API methods, for the sake of uniformity.

* specified pattern.
Expand All @@ -48,4 +54,47 @@ public String errorMessage(FieldContext field) {
public void accept(ConstraintTranslator<?> visitor) {
visitor.visitPattern(this);
}

/**
* Obtains the regular expression as a string.
*/
public String regex() {
return optionValue().getRegex();
}

/**
* Checks if the pattern allows a partial match.
*
* <p>If {@code true}, the whole string value does not have to match the regex, but only its
* substring.
*/
public boolean allowsPartialMatch() {
PatternOption option = optionValue();
Modifier modifier = option.getModifier();
return modifier.getPartialMatch();
}

/**
* Obtains the pattern modifiers as a bit mask for the {@link Pattern} flags.
*
* <p>If no modifiers are specified, returns {@code 0}.
*/
public int flagsMask() {
int result = 0;
PatternOption option = optionValue();
Modifier modifier = option.getModifier();
if (modifier.getDotAll()) {
result |= DOTALL;
}
if (modifier.getUnicode()) {
result |= UNICODE_CHARACTER_CLASS;
}
if (modifier.getCaseInsensitive()) {
result |= CASE_INSENSITIVE;
}
if (modifier.getMultiline()) {
result |= MULTILINE;
}
return result;
}
}
59 changes: 58 additions & 1 deletion base/src/main/proto/spine/options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -615,10 +615,67 @@ message PatternOption {
string regex = 1;

// The regex flag.
int32 flag = 2;
//
// Has no effect. Use `modifier` instead.
//
int32 flag = 2 [deprecated = true];

// Modifiers for this pattern.
Modifier modifier = 4;

// A user-defined validation error format message.
string msg_format = 3;

// Regular expression modifiers.
//
// These modifiers are specifically selected to be supported in many implementation platforms.
//
message Modifier {

// Enables the dot (`.`) symbol to match all the characters.
//
// By default, the dot does not match line break characters.
//
// May also be known in some platforms as "single line" mode and be encoded with the `s`
// flag.
//
bool dot_all = 1;

// Allows to ignore the case of the matched symbols.
//
// For example, this modifier is specified, string `ABC` would be a complete match for
// the regex `[a-z]+`.
//
// On some platforms may be represented by the `i` flag.
//
bool case_insensitive = 2;

// Enables the `^` (caret) and `$` (dollar) signs to match a start and an end of a line
// instead of a start and an end of the whole expression.
//
// On some platforms may be represented by the `m` flag.
//
bool multiline = 3;

// Enables matching the whole UTF-8 sequences,
//
// On some platforms may be represented by the `u` flag.
//
bool unicode = 4;

// Allows the matched strings to contain a full match to the pattern and some other
// characters as well.
//
// By default, a string only matches a pattern if it is a full match, i.e. there are no
// unaccounted for leading and/or trailing characters.
//
// This modifier is usually not represented programming languages, as the control over
// weather to match an entire string or only its part is provided to the user by other
// language means. For example, in Java, this would be the difference between methods
// `matches()` and `find()` of the `java.util.regex.Matcher` class.
//
bool partial_match = 5;
}
}

// Specifies the message to show if a validated field happens to be invalid.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

package io.spine.validate;

import com.google.common.truth.Truth;
import com.google.protobuf.Message;

import java.util.List;
Expand All @@ -29,7 +28,6 @@
import static io.spine.validate.Validate.violationsOf;
import static io.spine.validate.given.MessageValidatorTestEnv.assertFieldPathIs;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

public abstract class ValidationOfConstraintTest {
Expand Down
75 changes: 75 additions & 0 deletions base/src/test/java/io/spine/validate/option/PatternTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package io.spine.validate.option;

import com.google.protobuf.StringValue;
import io.spine.test.validate.AllThePatterns;
import io.spine.test.validate.PatternStringFieldValue;
import io.spine.test.validate.SimpleStringValue;
import io.spine.test.validate.WithStringValue;
Expand Down Expand Up @@ -84,6 +85,80 @@ void findOutThatStringDoesNotMatchExternalConstraint() {
assertNotValid(msg);
}

@Test
@DisplayName("validate with `case_insensitive` modifier")
void caseInsensitive() {
AllThePatterns message = AllThePatterns
.newBuilder()
.setLetters("AbC")
.buildPartial();
assertValid(message);

AllThePatterns invalid = AllThePatterns
.newBuilder()
.setLetters("12345")
.buildPartial();
assertNotValid(invalid);
}

@Test
@DisplayName("validate with `multiline` modifier")
void multiline() {
AllThePatterns message = AllThePatterns
.newBuilder()
.setManyLines("text" + System.lineSeparator() + "more text")
.buildPartial();
assertValid(message);

AllThePatterns invalid = AllThePatterns
.newBuilder()
.setManyLines("single line text")
.buildPartial();
assertNotValid(invalid);
}

@Test
@DisplayName("validate with `partial` modifier")
void partial() {
AllThePatterns message = AllThePatterns
.newBuilder()
.setPartial("Hello World!")
.buildPartial();
assertValid(message);

AllThePatterns invalid = AllThePatterns
.newBuilder()
.setPartial("123456")
.buildPartial();
assertNotValid(invalid);
}

@Test
@DisplayName("validate with `unicode` modifier")
void utf8() {
AllThePatterns message = AllThePatterns
.newBuilder()
.setUtf8("ґ")
.buildPartial();
assertValid(message);

AllThePatterns invalid = AllThePatterns
.newBuilder()
.setUtf8("\\\\")
.buildPartial();
assertNotValid(invalid);
}

@Test
@DisplayName("validate with `dot_all` modifier")
void dotAll() {
AllThePatterns message = AllThePatterns
.newBuilder()
.setDotAll("ab" + System.lineSeparator() + "cd")
.buildPartial();
assertValid(message);
}

private static PatternStringFieldValue patternStringFor(String email) {
return PatternStringFieldValue
.newBuilder()
Expand Down
38 changes: 38 additions & 0 deletions base/src/test/proto/spine/test/validate/patterns.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
syntax = "proto3";

package spine.test.validate;

import "spine/options.proto";

option (type_url_prefix) = "type.spine.io";
option java_package = "io.spine.test.validate";
option java_outer_classname = "PatternsProto";
option java_multiple_files = true;

message AllThePatterns {

string letters = 1 [
(pattern).regex = "[a-z]+",
(pattern).modifier.case_insensitive = true
];

string many_lines = 2 [
(pattern).regex = ".+$\r?\n^.+",
(pattern).modifier.multiline = true
];

string partial = 3 [
(pattern).regex = "World",
(pattern).modifier.partial_match = true
];

string utf8 = 4 [
(pattern).regex = "[їґє]",
(pattern).modifier.unicode = true
];

string dot_all = 5 [
(pattern).regex = ".*",
(pattern).modifier.dot_all = true
];
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading