diff --git a/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/AndFilter.kt b/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/AndFilter.kt new file mode 100644 index 0000000..e242411 --- /dev/null +++ b/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/AndFilter.kt @@ -0,0 +1,50 @@ +// Copyright 2022 The Cross-Media Measurement Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.wfanet.virtualpeople.common.fieldfilter + +import com.google.protobuf.Descriptors +import com.google.protobuf.MessageOrBuilder +import org.wfanet.virtualpeople.common.FieldFilterProto + +/** + * The implementation of [FieldFilter] when op is AND in config. + * + * Always use [FieldFilter.create]. Users should never construct a [AndFilter] directly. + */ +internal class AndFilter(descriptor: Descriptors.Descriptor, config: FieldFilterProto) : + FieldFilter { + + private val subFilters: List + + init { + if (config.op != FieldFilterProto.Op.AND) { + error("Op must be AND. Input FieldFilterProto: $config") + } + if (config.subFiltersCount == 0) { + error("sub_filters must be set when op is AND. Input FieldFilterProto: $config") + } + subFilters = config.subFiltersList.map { FieldFilter.create(descriptor, it) } + } + + /** Returns true if all [subFilters] match */ + override fun matches(messageOrBuilder: MessageOrBuilder): Boolean { + for (subFilter in subFilters) { + if (!subFilter.matches(messageOrBuilder)) { + return false + } + } + return true + } +} diff --git a/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/FieldFilter.kt b/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/FieldFilter.kt index f1853f0..60a3b8c 100644 --- a/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/FieldFilter.kt +++ b/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/FieldFilter.kt @@ -41,13 +41,13 @@ sealed interface FieldFilter { return when (config.op) { Op.HAS -> HasFilter(descriptor, config) Op.EQUAL -> EqualFilter(descriptor, config) + Op.AND -> AndFilter(descriptor, config) + Op.OR -> OrFilter(descriptor, config) Op.ANY_IN -> AnyInFilter.create(descriptor, config) Op.GT, Op.LT, Op.IN, Op.REGEXP, - Op.OR, - Op.AND, Op.NOT, Op.PARTIAL, Op.TRUE -> TODO("Not yet implemented") diff --git a/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/OrFilter.kt b/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/OrFilter.kt new file mode 100644 index 0000000..135ed14 --- /dev/null +++ b/src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter/OrFilter.kt @@ -0,0 +1,50 @@ +// Copyright 2022 The Cross-Media Measurement Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.wfanet.virtualpeople.common.fieldfilter + +import com.google.protobuf.Descriptors +import com.google.protobuf.MessageOrBuilder +import org.wfanet.virtualpeople.common.FieldFilterProto + +/** + * The implementation of [FieldFilter] when op is OR in config. + * + * Always use [FieldFilter.create]. Users should never construct a [OrFilter] directly. + */ +internal class OrFilter(descriptor: Descriptors.Descriptor, config: FieldFilterProto) : + FieldFilter { + + private val subFilters: List + + init { + if (config.op != FieldFilterProto.Op.OR) { + error("Op must be OR. Input FieldFilterProto: $config") + } + if (config.subFiltersCount == 0) { + error("sub_filters must be set when op is AND. Input FieldFilterProto: $config") + } + subFilters = config.subFiltersList.map { FieldFilter.create(descriptor, it) } + } + + /** Returns true if any of the [subFilters] matches. */ + override fun matches(messageOrBuilder: MessageOrBuilder): Boolean { + for (subFilter in subFilters) { + if (subFilter.matches(messageOrBuilder)) { + return true + } + } + return false + } +} diff --git a/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/AndFilterTest.kt b/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/AndFilterTest.kt new file mode 100644 index 0000000..0bbbd2f --- /dev/null +++ b/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/AndFilterTest.kt @@ -0,0 +1,101 @@ +// Copyright 2022 The Cross-Media Measurement Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.wfanet.virtualpeople.common.fieldfilter + +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.wfanet.virtualpeople.common.FieldFilterProto.Op +import org.wfanet.virtualpeople.common.fieldFilterProto +import org.wfanet.virtualpeople.common.test.* + +@RunWith(JUnit4::class) +class AndFilterTest { + + @Test + fun `no sub filter should fail`() { + val fieldFilter = fieldFilterProto { op = Op.AND } + assertFailsWith { + FieldFilter.create(TestProto.getDescriptor(), fieldFilter) + } + } + + @Test + fun `all subfilters match should pass`() { + val fieldFilter = fieldFilterProto { + op = Op.AND + subFilters.add( + fieldFilterProto { + name = "a.b.int32_value" + op = Op.EQUAL + value = "1" + } + ) + subFilters.add( + fieldFilterProto { + name = "a.b.int64_value" + op = Op.EQUAL + value = "1" + } + ) + } + val filter = FieldFilter.create(TestProto.getDescriptor(), fieldFilter) + val testProto1 = testProto { + a = testProtoA { + b = testProtoB { + int32Value = 1 + int64Value = 1 + } + } + } + assertTrue(filter.matches(testProto1)) + assertTrue(filter.matches(testProto1.toBuilder())) + } + + @Test + fun `any subfilter mismatch should fail`() { + val fieldFilter = fieldFilterProto { + op = Op.AND + subFilters.add( + fieldFilterProto { + name = "a.b.int32_value" + op = Op.EQUAL + value = "1" + } + ) + subFilters.add( + fieldFilterProto { + name = "a.b.int64_value" + op = Op.EQUAL + value = "1" + } + ) + } + val filter = FieldFilter.create(TestProto.getDescriptor(), fieldFilter) + val testProto1 = testProto { + a = testProtoA { + b = testProtoB { + int32Value = 1 + int64Value = 2 + } + } + } + assertFalse(filter.matches(testProto1)) + assertFalse(filter.matches(testProto1.toBuilder())) + } +} diff --git a/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/BUILD.bazel b/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/BUILD.bazel index f24a228..09f2194 100644 --- a/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/BUILD.bazel +++ b/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/BUILD.bazel @@ -44,3 +44,33 @@ kt_jvm_test( "@wfa_common_jvm//imports/kotlin/kotlin/test", ], ) + +kt_jvm_test( + name = "AndFilterTest", + srcs = ["AndFilterTest.kt"], + test_class = "org.wfanet.virtualpeople.common.fieldfilter.AndFilterTest", + deps = [ + "//src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter", + "//src/main/proto/wfa/virtual_people/common/field_filter/test:test_kt_jvm_proto", + "@wfa_common_jvm//imports/java/com/google/common/truth", + "@wfa_common_jvm//imports/java/com/google/common/truth/extensions/proto", + "@wfa_common_jvm//imports/java/com/google/protobuf/util", + "@wfa_common_jvm//imports/java/org/junit", + "@wfa_common_jvm//imports/kotlin/kotlin/test", + ], +) + +kt_jvm_test( + name = "OrFilterTest", + srcs = ["OrFilterTest.kt"], + test_class = "org.wfanet.virtualpeople.common.fieldfilter.OrFilterTest", + deps = [ + "//src/main/kotlin/org/wfanet/virtualpeople/common/fieldfilter", + "//src/main/proto/wfa/virtual_people/common/field_filter/test:test_kt_jvm_proto", + "@wfa_common_jvm//imports/java/com/google/common/truth", + "@wfa_common_jvm//imports/java/com/google/common/truth/extensions/proto", + "@wfa_common_jvm//imports/java/com/google/protobuf/util", + "@wfa_common_jvm//imports/java/org/junit", + "@wfa_common_jvm//imports/kotlin/kotlin/test", + ], +) diff --git a/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/OrFilterTest.kt b/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/OrFilterTest.kt new file mode 100644 index 0000000..78af36f --- /dev/null +++ b/src/test/kotlin/org/wfanet/virtualpeople/common/fieldfilter/OrFilterTest.kt @@ -0,0 +1,106 @@ +// Copyright 2022 The Cross-Media Measurement Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.wfanet.virtualpeople.common.fieldfilter + +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.wfanet.virtualpeople.common.FieldFilterProto.Op +import org.wfanet.virtualpeople.common.fieldFilterProto +import org.wfanet.virtualpeople.common.test.* + +@RunWith(JUnit4::class) +class OrFilterTest { + + @Test + fun `no sub filter should fail`() { + val fieldFilter = fieldFilterProto { op = Op.OR } + assertFailsWith { + FieldFilter.create(TestProto.getDescriptor(), fieldFilter) + } + } + + @Test + fun `any subfilter matches should pass`() { + val fieldFilter = fieldFilterProto { + op = Op.OR + subFilters.add( + fieldFilterProto { + name = "a.b.int32_value" + op = Op.EQUAL + value = "1" + } + ) + subFilters.add( + fieldFilterProto { + name = "a.b.int64_value" + op = Op.EQUAL + value = "1" + } + ) + } + val filter = FieldFilter.create(TestProto.getDescriptor(), fieldFilter) + + val testProto1 = testProto { + a = testProtoA { + b = testProtoB { + int32Value = 2 + int64Value = 1 + } + } + } + assertTrue(filter.matches(testProto1)) + assertTrue(filter.matches(testProto1.toBuilder())) + + val testProto2 = testProto { a = testProtoA { b = testProtoB { int64Value = 1 } } } + assertTrue(filter.matches(testProto1)) + assertTrue(filter.matches(testProto1.toBuilder())) + } + + @Test + fun `All subfilters mismatch should fail`() { + val fieldFilter = fieldFilterProto { + op = Op.OR + subFilters.add( + fieldFilterProto { + name = "a.b.int32_value" + op = Op.EQUAL + value = "1" + } + ) + subFilters.add( + fieldFilterProto { + name = "a.b.int64_value" + op = Op.EQUAL + value = "1" + } + ) + } + val filter = FieldFilter.create(TestProto.getDescriptor(), fieldFilter) + val testProto1 = testProto { + a = testProtoA { + b = testProtoB { + int32Value = 2 + int64Value = 2 + } + } + } + assertFalse(filter.matches(testProto1)) + assertFalse(filter.matches(testProto1.toBuilder())) + } +}