Skip to content

Commit

Permalink
refactor: supports combining custom matchers for field selector
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Jan 8, 2024
1 parent 3b99e39 commit 5b308ef
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,8 @@ public boolean match(Extension extension) {
}

if (fieldSelector != null) {
for (var matcher : fieldSelector.getMatchers()) {
var fieldValue = PARSER.parseRaw(matcher.getKey())
.getValue(extension, String.class);
if (!matcher.test(fieldValue)) {
return false;
}
}
return fieldSelector.test(key -> PARSER.parseRaw(key)
.getValue(extension, String.class));
}
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/main/java/run/halo/app/extension/ListOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
public class ListOptions {
private LabelSelector labelSelector;
private FieldSelector fieldSelector;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package run.halo.app.extension.router.selector;

import lombok.Getter;

@Getter
public class AndSelectorMatcher extends LogicalMatcher {
private final SelectorMatcher left;
private final SelectorMatcher right;

public AndSelectorMatcher(SelectorMatcher left, SelectorMatcher right) {
this.left = left;
this.right = right;
}

public boolean test(String s) {
return left.test(s) && right.test(s);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package run.halo.app.extension.router.selector;

public class AnySelectorMatcher extends LogicalMatcher {

@Override
public boolean test(String s) {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,117 @@
package run.halo.app.extension.router.selector;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.Objects;
import java.util.function.UnaryOperator;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.util.Assert;

@Data
@Accessors(chain = true)
public class FieldSelector implements Predicate<String> {
private List<SelectorMatcher> matchers;
public class FieldSelector {
private SelectorMatcher matcher;

@Override
public boolean test(String fieldValue) {
if (matchers == null || matchers.isEmpty()) {
return true;
}
return matchers.stream()
.allMatch(matcher -> matcher.test(fieldValue));
public static FieldSelectorBuilder builder() {
return new FieldSelectorBuilder(null);
}

public static FieldSelectorBuilder builder() {
return new FieldSelectorBuilder();
public static FieldSelectorBuilder builder(SelectorMatcher rootMatcher) {
return new FieldSelectorBuilder(rootMatcher);
}

public boolean test(UnaryOperator<String> valueForKeyFunc) {
return evaluate(matcher, valueForKeyFunc);
}

boolean evaluate(SelectorMatcher matcher, UnaryOperator<String> valueForKeyFunc) {
if (matcher instanceof LogicalMatcher) {
if (matcher instanceof AndSelectorMatcher andNode) {
return evaluate(andNode.getLeft(), valueForKeyFunc)
&& evaluate(andNode.getRight(), valueForKeyFunc);
} else if (matcher instanceof OrSelectorMatcher orNode) {
return evaluate(orNode.getLeft(), valueForKeyFunc)
|| evaluate(orNode.getRight(), valueForKeyFunc);
} else if (matcher instanceof AnySelectorMatcher) {
return true;
}
}
String valueToTest = valueForKeyFunc.apply(matcher.getKey());
return matcher.test(valueToTest);
}

public static class FieldSelectorBuilder {
private final List<SelectorMatcher> matchers = new ArrayList<>();
private SelectorMatcher rootMatcher;

public FieldSelectorBuilder(SelectorMatcher rootMatcher) {
this.rootMatcher = rootMatcher;
}

public FieldSelectorBuilder eq(String fieldName, String fieldValue) {
return and(EqualityMatcher.equal(fieldName, fieldValue));
}

public FieldSelectorBuilder notEq(String fieldName, String fieldValue) {
return and(EqualityMatcher.notEqual(fieldName, fieldValue));
}

public FieldSelectorBuilder eq(String fieldPath, String value) {
matchers.add(EqualityMatcher.equal(fieldPath, value));
public FieldSelectorBuilder in(String fieldName, String... fieldValues) {
return and(SetMatcher.in(fieldName, fieldValues));
}

public FieldSelectorBuilder notIn(String fieldName, String... fieldValues) {
return and(SetMatcher.notIn(fieldName, fieldValues));
}

public FieldSelectorBuilder exists(String fieldName) {
return and(SetMatcher.exists(fieldName));
}

public FieldSelectorBuilder notExists(String fieldName) {
return and(SetMatcher.notExists(fieldName));
}

/**
* Combine the current selector matcher with another one with AND.
*
* @param other another selector matcher to be combined with the current one with AND
* @return the current selector matcher builder
*/
public FieldSelectorBuilder and(SelectorMatcher other) {
Assert.notNull(other, "Other selector matcher must not be null");
if (rootMatcher == null) {
this.rootMatcher = other;
return this;
}
this.rootMatcher = new AndSelectorMatcher(rootMatcher, other);
return this;
}

public FieldSelectorBuilder notEq(String fieldPath, String value) {
matchers.add(EqualityMatcher.notEqual(fieldPath, value));
/**
* Combine the current selector matcher with another one with OR.
*
* @param other another selector matcher to be combined with the current one with OR
* @return the current selector matcher builder
*/
public FieldSelectorBuilder or(SelectorMatcher other) {
Assert.notNull(other, "Other selector matcher must not be null");
if (rootMatcher == null) {
rootMatcher = other;
}
rootMatcher = new OrSelectorMatcher(rootMatcher, other);
return this;
}

/**
* Build a field selector.
* Build the selector matcher.
*/
public FieldSelector build() {
FieldSelector fieldSelector = new FieldSelector();
fieldSelector.setMatchers(matchers);
var fieldSelector = new FieldSelector();
fieldSelector.setMatcher(buildMatcher());
return fieldSelector;
}

public SelectorMatcher buildMatcher() {
return Objects.requireNonNullElseGet(rootMatcher, AnySelectorMatcher::new);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package run.halo.app.extension.router.selector;

public abstract class LogicalMatcher implements SelectorMatcher {

@Override
public String getKey() {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package run.halo.app.extension.router.selector;

import lombok.Getter;

@Getter
public class OrSelectorMatcher extends LogicalMatcher {
private final SelectorMatcher left;
private final SelectorMatcher right;

public OrSelectorMatcher(SelectorMatcher left, SelectorMatcher right) {
this.left = left;
this.right = right;
}

@Override
public String getKey() {
return null;
}

@Override
public boolean test(String s) {
return left.test(s) || right.test(s);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,14 @@ public static ListOptions labelAndFieldSelectorToListOptions(
.map(selectorConverter::convert)
.filter(Objects::nonNull)
.map(fieldConverter::convert)
.toList())
.orElse(List.of());
.toList()
)
.orElse(List.of())
.stream()
.reduce(AndSelectorMatcher::new);

return new ListOptions()
.setLabelSelector(new LabelSelector().setMatchers(labelMatchers))
.setFieldSelector(new FieldSelector().setMatchers(fieldMatchers));
.setFieldSelector(new FieldSelector().setMatcher(fieldMatchers.orElse(null)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package run.halo.app.extension.router.selector;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;
import org.junit.jupiter.api.Test;

/**
* Tests for {@link FieldSelector}.
*
* @author guqing
* @since 2.12.0
*/
class FieldSelectorTest {

@Test
void testEq() {
var fieldSelector = FieldSelector.builder()
.eq("name", "guqing").build();
assertThat(fieldSelector.test(key -> "guqing")).isTrue();
assertThat(fieldSelector.test(key -> "halo")).isFalse();
}

@Test
void testNotEq() {
var fieldSelector = FieldSelector.builder()
.notEq("name", "guqing").build();
assertThat(fieldSelector.test(key -> "guqing")).isFalse();
assertThat(fieldSelector.test(key -> "halo")).isTrue();
}

@Test
void testIn() {
var fieldSelector = FieldSelector.builder()
.in("name", "guqing", "guqing1").build();
assertThat(fieldSelector.test(key -> "guqing")).isTrue();
assertThat(fieldSelector.test(key -> "halo")).isFalse();
assertThat(fieldSelector.test(key -> "blog")).isFalse();
}

@Test
void testNotIn() {
var fieldSelector = FieldSelector.builder()
.notIn("name", "guqing", "guqing1").build();
assertThat(fieldSelector.test(key -> "guqing")).isFalse();
assertThat(fieldSelector.test(key -> "halo")).isTrue();
assertThat(fieldSelector.test(key -> "blog")).isTrue();
}

@Test
void testExists() {
var fieldSelector = FieldSelector.builder()
.exists("name").build();
assertThat(fieldSelector.test(key -> "guqing")).isTrue();
assertThat(fieldSelector.test(key -> "halo")).isTrue();
assertThat(fieldSelector.test(key -> "blog")).isTrue();
}

@Test
void testNotExists() {
var fieldSelector = FieldSelector.builder()
.notExists("name").build();
assertThat(fieldSelector.test(key -> "guqing")).isFalse();
assertThat(fieldSelector.test(key -> "halo")).isFalse();
assertThat(fieldSelector.test(key -> "blog")).isFalse();
}

@Test
void testAnd() {
var fieldSelector = FieldSelector.builder()
.eq("name", "guqing")
.and(FieldSelector.builder().eq("age", "18").build().getMatcher())
.build();
assertThat(fieldSelector.test(key -> "guqing")).isFalse();
assertThat(fieldSelector.test(key -> "halo")).isFalse();
assertThat(fieldSelector.test(key -> "18")).isFalse();
assertThat(fieldSelector.test(key -> "guqing18")).isFalse();
assertThat(fieldSelector.test(key -> {
var map = Map.of("name", "guqing", "age", "18");
return map.get(key);
})).isTrue();
assertThat(fieldSelector.test(key -> {
var map = Map.of("name", "guqing", "age", "19");
return map.get(key);
})).isFalse();
}

@Test
void testOr() {
var fieldSelector = FieldSelector.builder()
.eq("name", "guqing")
.or(FieldSelector.builder().eq("age", "18").build().getMatcher())
.build();
assertThat(fieldSelector.test(key -> "guqing")).isTrue();
assertThat(fieldSelector.test(key -> "halo")).isFalse();
assertThat(fieldSelector.test(key -> "blog")).isFalse();
assertThat(fieldSelector.test(key -> "18")).isTrue();
assertThat(fieldSelector.test(key -> "guqing18")).isFalse();
assertThat(fieldSelector.test(key -> {
var map = Map.of("name", "guqing", "age", "18");
return map.get(key);
})).isTrue();
}
}
Loading

0 comments on commit 5b308ef

Please sign in to comment.