diff --git a/api/src/main/java/run/halo/app/extension/ListOptions.java b/api/src/main/java/run/halo/app/extension/ListOptions.java index c52222d30..09c72cf96 100644 --- a/api/src/main/java/run/halo/app/extension/ListOptions.java +++ b/api/src/main/java/run/halo/app/extension/ListOptions.java @@ -1,9 +1,13 @@ package run.halo.app.extension; +import java.util.List; import lombok.Data; import lombok.experimental.Accessors; +import run.halo.app.extension.index.query.Query; +import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.extension.router.selector.LabelSelector; +import run.halo.app.extension.router.selector.SelectorMatcher; @Data @Accessors(chain = true) @@ -15,14 +19,109 @@ public class ListOptions { public String toString() { var sb = new StringBuilder(); if (fieldSelector != null) { - sb.append("fieldSelector: ").append(fieldSelector.query()); + var query = fieldSelector.query().toString(); + sb.append("fieldSelector: ") + .append(query.startsWith("(") ? query : "(" + query + ")"); } if (labelSelector != null) { if (!sb.isEmpty()) { sb.append(", "); } - sb.append("labelSelector: ").append(labelSelector); + sb.append("labelSelector: (").append(labelSelector).append(")"); } return sb.toString(); } + + public static ListOptionsBuilder builder() { + return new ListOptionsBuilder(); + } + + public static ListOptionsBuilder builder(ListOptions listOptions) { + return new ListOptionsBuilder(listOptions); + } + + public static class ListOptionsBuilder { + private LabelSelectorBuilder labelSelectorBuilder; + private Query query; + + public ListOptionsBuilder() { + } + + /** + * Create a new list options builder with the given list options. + */ + public ListOptionsBuilder(ListOptions listOptions) { + if (listOptions.getLabelSelector() != null) { + this.labelSelectorBuilder = new LabelSelectorBuilder( + listOptions.getLabelSelector().getMatchers(), this); + } + if (listOptions.getFieldSelector() != null) { + this.query = listOptions.getFieldSelector().query(); + } + } + + /** + * Create a new label selector builder. + */ + public LabelSelectorBuilder labelSelector() { + if (labelSelectorBuilder == null) { + labelSelectorBuilder = new LabelSelectorBuilder(this); + } + return labelSelectorBuilder; + } + + public ListOptionsBuilder fieldQuery(Query query) { + this.query = query; + return this; + } + + /** + * And the given query to the current query. + */ + public ListOptionsBuilder andQuery(Query query) { + this.query = (this.query == null ? query : QueryFactory.and(this.query, query)); + return this; + } + + /** + * Or the given query to the current query. + */ + public ListOptionsBuilder orQuery(Query query) { + this.query = (this.query == null ? query : QueryFactory.or(this.query, query)); + return this; + } + + /** + * Build the list options. + */ + public ListOptions build() { + var listOptions = new ListOptions(); + if (labelSelectorBuilder != null) { + listOptions.setLabelSelector(labelSelectorBuilder.build()); + } + if (query != null) { + listOptions.setFieldSelector(FieldSelector.of(query)); + } + return listOptions; + } + } + + public static class LabelSelectorBuilder + extends LabelSelector.LabelSelectorBuilder { + private final ListOptionsBuilder listOptionsBuilder; + + public LabelSelectorBuilder(List givenMatchers, + ListOptionsBuilder listOptionsBuilder) { + super(givenMatchers); + this.listOptionsBuilder = listOptionsBuilder; + } + + public LabelSelectorBuilder(ListOptionsBuilder listOptionsBuilder) { + this.listOptionsBuilder = listOptionsBuilder; + } + + public ListOptionsBuilder end() { + return this.listOptionsBuilder; + } + } } diff --git a/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java b/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java index e2cead3b4..1e33e2a01 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Predicate; +import java.util.stream.Collectors; import lombok.Data; import lombok.experimental.Accessors; import org.springframework.lang.NonNull; @@ -24,6 +25,16 @@ public class LabelSelector implements Predicate> { .allMatch(matcher -> matcher.test(labels.get(matcher.getKey()))); } + @Override + public String toString() { + if (matchers == null || matchers.isEmpty()) { + return ""; + } + return matchers.stream() + .map(SelectorMatcher::toString) + .collect(Collectors.joining(", ")); + } + /** * Returns a new label selector that is the result of ANDing the current selector with the * given selector. @@ -40,41 +51,58 @@ public class LabelSelector implements Predicate> { return labelSelector; } - public static LabelSelectorBuilder builder() { - return new LabelSelectorBuilder(); + public static LabelSelectorBuilder builder() { + return new LabelSelectorBuilder<>(); } - public static class LabelSelectorBuilder { + public static class LabelSelectorBuilder> { private final List matchers = new ArrayList<>(); - public LabelSelectorBuilder eq(String key, String value) { + public LabelSelectorBuilder() { + } + + /** + * Create a new label selector builder with the given matchers. + */ + public LabelSelectorBuilder(List givenMatchers) { + if (givenMatchers != null) { + matchers.addAll(givenMatchers); + } + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + public T eq(String key, String value) { matchers.add(EqualityMatcher.equal(key, value)); - return this; + return self(); } - public LabelSelectorBuilder notEq(String key, String value) { + public T notEq(String key, String value) { matchers.add(EqualityMatcher.notEqual(key, value)); - return this; + return self(); } - public LabelSelectorBuilder in(String key, String... values) { + public T in(String key, String... values) { matchers.add(SetMatcher.in(key, values)); - return this; + return self(); } - public LabelSelectorBuilder notIn(String key, String... values) { + public T notIn(String key, String... values) { matchers.add(SetMatcher.notIn(key, values)); - return this; + return self(); } - public LabelSelectorBuilder exists(String key) { + public T exists(String key) { matchers.add(SetMatcher.exists(key)); - return this; + return self(); } - public LabelSelectorBuilder notExists(String key) { + public T notExists(String key) { matchers.add(SetMatcher.notExists(key)); - return this; + return self(); } /** diff --git a/api/src/main/java/run/halo/app/extension/router/selector/SetMatcher.java b/api/src/main/java/run/halo/app/extension/router/selector/SetMatcher.java index 474db19b5..20d5ff9f6 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/SetMatcher.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/SetMatcher.java @@ -46,6 +46,14 @@ public class SetMatcher implements SelectorMatcher { return operator.with(values).test(s); } + @Override + public String toString() { + if (Operator.EXISTS.equals(operator) || Operator.NOT_EXISTS.equals(operator)) { + return key + " " + operator; + } + return key + " " + operator + " (" + String.join(", ", values) + ")"; + } + private enum Operator { IN(values -> v -> contains(values, v)), NOT_IN(values -> v -> !contains(values, v)), diff --git a/api/src/test/java/run/halo/app/extension/ListOptionsTest.java b/api/src/test/java/run/halo/app/extension/ListOptionsTest.java new file mode 100644 index 000000000..a5ed75abb --- /dev/null +++ b/api/src/test/java/run/halo/app/extension/ListOptionsTest.java @@ -0,0 +1,51 @@ +package run.halo.app.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static run.halo.app.extension.index.query.QueryFactory.equal; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link ListOptions}. + * + * @author guqing + * @since 2.17.0 + */ +class ListOptionsTest { + + @Nested + class ListOptionsBuilderTest { + + @Test + void buildTest() { + var listOptions = ListOptions.builder() + .labelSelector() + .eq("key-1", "value-1") + .notEq("key-2", "value-1") + .exists("key-3") + .end() + .andQuery(equal("spec.slug", "fake-slug")) + .orQuery(equal("spec.slug", "test")) + .build(); + System.out.println(listOptions); + assertThat(listOptions.toString()).isEqualTo( + "fieldSelector: (spec.slug = 'fake-slug' OR spec.slug = 'test'), labelSelector: " + + "(key-1 equal value-1, key-2 not_equal value-1, key-3 EXISTS)"); + } + + @Test + void buildTest2() { + var listOptions = ListOptions.builder() + .labelSelector() + .notEq("key-2", "value-1") + .end() + .fieldQuery(equal("spec.slug", "fake-slug")) + .build(); + assertThat(listOptions.toString()) + .isEqualTo( + "fieldSelector: (spec.slug = 'fake-slug'), labelSelector: (key-2 not_equal " + + "value-1)"); + } + } +} diff --git a/api/src/test/java/run/halo/app/extension/router/selector/LabelSelectorTest.java b/api/src/test/java/run/halo/app/extension/router/selector/LabelSelectorTest.java new file mode 100644 index 000000000..b91b59e38 --- /dev/null +++ b/api/src/test/java/run/halo/app/extension/router/selector/LabelSelectorTest.java @@ -0,0 +1,24 @@ +package run.halo.app.extension.router.selector; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link LabelSelector}. + * + * @author guqing + * @since 2.17.0 + */ +class LabelSelectorTest { + + @Test + void builderTest() { + var labelSelector = LabelSelector.builder() + .eq("a", "v1") + .in("b", "v2", "v3") + .build(); + assertThat(labelSelector.toString()) + .isEqualTo("a equal v1, b IN (v2, v3)"); + } +} \ No newline at end of file