blob: dfca1f80538946b8a4d1247e3d85fc7b594b3f8f [file] [log] [blame]
/*
* Copyright (c) 2016 Google, Inc.
*
* 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 com.google.common.truth.extensions.proto;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.extensions.proto.FieldScopeUtil.join;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.ForOverride;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.Message;
import com.google.protobuf.TypeRegistry;
import java.util.List;
/**
* Implementations of all variations of {@link FieldScope} logic.
*
* <p>{@code FieldScopeLogic} is the abstract base class which provides common functionality to all
* sub-types. There are two classes of sub-types:
*
* <ul>
* <li>Concrete subtypes, which implements specific rules and perform no delegation.
* <li>Compound subtypes, which combine one or more {@code FieldScopeLogic}s with specific
* operations.
* </ul>
*/
abstract class FieldScopeLogic implements FieldScopeLogicContainer<FieldScopeLogic> {
/**
* Returns whether the given field is included in this FieldScopeLogic, along with whether it's
* included recursively or not.
*/
abstract FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId);
/** Returns whether the given field is included in this FieldScopeLogic. */
final boolean contains(Descriptor rootDescriptor, SubScopeId subScopeId) {
return policyFor(rootDescriptor, subScopeId).included();
}
/**
* Returns a {@code FieldScopeLogic} to handle the message pointed to by this descriptor.
*
* <p>Subclasses which can return non-recursive {@link FieldScopeResult}s must override {@link
* #subScopeImpl} to implement those cases.
*/
@Override
public final FieldScopeLogic subScope(Descriptor rootDescriptor, SubScopeId subScopeId) {
FieldScopeResult result = policyFor(rootDescriptor, subScopeId);
if (result.recursive()) {
return result.included() ? all() : none();
} else {
return subScopeImpl(rootDescriptor, subScopeId);
}
}
/**
* Returns {@link #subScope} for {@code NONRECURSIVE} results.
*
* <p>Throws an {@link UnsupportedOperationException} by default. Subclasses which can return
* {@code NONRECURSIVE} results must override this method.
*/
@ForOverride
FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
throw new UnsupportedOperationException("subScopeImpl not implemented for " + getClass());
}
/**
* Returns an accurate description for debugging purposes.
*
* <p>Compare to {@link FieldScope#usingCorrespondenceString(Optional)}, which returns a beautiful
* error message that makes as much sense to the user as possible.
*
* <p>Abstract so subclasses must implement.
*/
@Override
public abstract String toString();
@Override
public void validate(
Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {}
private static boolean isEmpty(Iterable<?> container) {
boolean isEmpty = true;
for (Object element : container) {
checkNotNull(element);
isEmpty = false;
}
return isEmpty;
}
// TODO(user): Rename these 'ignoring' and 'allowing' methods to 'plus' and 'minus', or
// something else that doesn't tightly couple FieldScopeLogic to the 'ignore' concept.
FieldScopeLogic ignoringFields(Iterable<Integer> fieldNumbers) {
if (isEmpty(fieldNumbers)) {
return this;
}
return and(
this,
new NegationFieldScopeLogic(new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ true)));
}
FieldScopeLogic ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
if (isEmpty(fieldDescriptors)) {
return this;
}
return and(
this,
new NegationFieldScopeLogic(
new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ true)));
}
FieldScopeLogic allowingFields(Iterable<Integer> fieldNumbers) {
if (isEmpty(fieldNumbers)) {
return this;
}
return or(this, new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ true));
}
FieldScopeLogic allowingFieldsNonRecursive(Iterable<Integer> fieldNumbers) {
if (isEmpty(fieldNumbers)) {
return this;
}
return or(this, new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ false));
}
FieldScopeLogic allowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
if (isEmpty(fieldDescriptors)) {
return this;
}
return or(this, new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ true));
}
FieldScopeLogic allowingFieldDescriptorsNonRecursive(Iterable<FieldDescriptor> fieldDescriptors) {
if (isEmpty(fieldDescriptors)) {
return this;
}
return or(this, new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ false));
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// CONCRETE SUBTYPES
//////////////////////////////////////////////////////////////////////////////////////////////////
/** Returns whether this is equivalent to {@code FieldScopeLogic.all()}. */
boolean isAll() {
return false;
}
private static final FieldScopeLogic ALL =
new FieldScopeLogic() {
@Override
public String toString() {
return "FieldScopes.all()";
}
@Override
final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
return FieldScopeResult.INCLUDED_RECURSIVELY;
}
@Override
final boolean isAll() {
return true;
}
};
private static final FieldScopeLogic NONE =
new FieldScopeLogic() {
@Override
public String toString() {
return "FieldScopes.none()";
}
@Override
final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
return FieldScopeResult.EXCLUDED_RECURSIVELY;
}
};
static FieldScopeLogic all() {
return ALL;
}
static FieldScopeLogic none() {
return NONE;
}
private static class PartialScopeLogic extends FieldScopeLogic {
private static final PartialScopeLogic EMPTY = new PartialScopeLogic(FieldNumberTree.empty());
private final FieldNumberTree fieldNumberTree;
PartialScopeLogic(FieldNumberTree fieldNumberTree) {
this.fieldNumberTree = fieldNumberTree;
}
@Override
public String toString() {
return String.format("PartialScopeLogic(%s)", fieldNumberTree);
}
@Override
final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
return fieldNumberTree.hasChild(subScopeId)
? FieldScopeResult.INCLUDED_NONRECURSIVELY
: FieldScopeResult.EXCLUDED_RECURSIVELY;
}
@Override
final FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
return newPartialScopeLogic(fieldNumberTree.child(subScopeId));
}
private static PartialScopeLogic newPartialScopeLogic(FieldNumberTree fieldNumberTree) {
return fieldNumberTree.isEmpty() ? EMPTY : new PartialScopeLogic(fieldNumberTree);
}
}
private static final class RootPartialScopeLogic extends PartialScopeLogic {
private final String repr;
private final Descriptor expectedDescriptor;
RootPartialScopeLogic(FieldNumberTree fieldNumberTree, String repr, Descriptor descriptor) {
super(fieldNumberTree);
this.repr = repr;
this.expectedDescriptor = descriptor;
}
@Override
public void validate(
Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
Verify.verify(
fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL,
"PartialScopeLogic doesn't support custom field validators.");
checkArgument(
expectedDescriptor.equals(rootDescriptor),
"Message given to FieldScopes.fromSetFields() does not have the same descriptor as the "
+ "message being tested. Expected %s, got %s.",
expectedDescriptor.getFullName(),
rootDescriptor.getFullName());
}
@Override
public String toString() {
return String.format("FieldScopes.fromSetFields(%s)", repr);
}
}
static FieldScopeLogic partialScope(
Message message, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
return new RootPartialScopeLogic(
FieldNumberTree.fromMessage(message, typeRegistry, extensionRegistry),
message.toString(),
message.getDescriptorForType());
}
static FieldScopeLogic partialScope(
Iterable<? extends Message> messages,
Descriptor descriptor,
TypeRegistry typeRegistry,
ExtensionRegistry extensionRegistry) {
return new RootPartialScopeLogic(
FieldNumberTree.fromMessages(messages, typeRegistry, extensionRegistry),
Joiner.on(", ").useForNull("null").join(messages),
descriptor);
}
// TODO(user): Performance: Optimize FieldNumbersLogic and FieldDescriptorsLogic for
// adding / ignoring field numbers and descriptors, respectively, to eliminate high recursion
// costs for long chains of allows/ignores.
// Common functionality for FieldNumbersLogic and FieldDescriptorsLogic.
private abstract static class FieldMatcherLogicBase extends FieldScopeLogic {
private final boolean isRecursive;
protected FieldMatcherLogicBase(boolean isRecursive) {
this.isRecursive = isRecursive;
}
/**
* Determines whether the FieldDescriptor is equal to one of the explicitly defined components
* of this FieldScopeLogic.
*
* @param descriptor Descriptor of the message being tested.
* @param fieldDescriptor FieldDescriptor being inspected for a direct match to the scope's
* definition.
*/
abstract boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor);
@Override
final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
FieldDescriptor fieldDescriptor = null;
switch (subScopeId.kind()) {
case FIELD_DESCRIPTOR:
fieldDescriptor = subScopeId.fieldDescriptor();
break;
case UNPACKED_ANY_VALUE_TYPE:
fieldDescriptor = AnyUtils.valueFieldDescriptor();
break;
case UNKNOWN_FIELD_DESCRIPTOR:
return FieldScopeResult.EXCLUDED_RECURSIVELY;
}
if (matchesFieldDescriptor(rootDescriptor, fieldDescriptor)) {
return FieldScopeResult.of(/* included = */ true, isRecursive);
}
// We return 'EXCLUDED_NONRECURSIVELY' for both field descriptor scopes and field number
// scopes. In the former case, the field descriptors are arbitrary, so it's always possible we
// find a hit on a sub-message somewhere. In the latter case, the message definition may be
// cyclic, so we need to return 'EXCLUDED_NONRECURSIVELY' even if the top level field number
// doesn't match.
return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
}
@Override
final FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
return this;
}
@Override
public void validate(
Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
if (isRecursive) {
Verify.verify(
fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL,
"Field descriptor validators are not supported "
+ "for non-recursive field matcher logics.");
}
}
}
// Matches any specific fields which fall under a sub-message field (or root) matching the root
// message type and one of the specified field numbers.
private static final class FieldNumbersLogic extends FieldMatcherLogicBase {
private final ImmutableSet<Integer> fieldNumbers;
FieldNumbersLogic(Iterable<Integer> fieldNumbers, boolean isRecursive) {
super(isRecursive);
this.fieldNumbers = ImmutableSet.copyOf(fieldNumbers);
}
@Override
public void validate(
Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
super.validate(rootDescriptor, fieldDescriptorValidator);
for (int fieldNumber : fieldNumbers) {
FieldDescriptor fieldDescriptor = rootDescriptor.findFieldByNumber(fieldNumber);
checkArgument(
fieldDescriptor != null,
"Message type %s has no field with number %s.",
rootDescriptor.getFullName(),
fieldNumber);
fieldDescriptorValidator.validate(fieldDescriptor);
}
}
@Override
boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
return fieldDescriptor.getContainingType() == descriptor
&& fieldNumbers.contains(fieldDescriptor.getNumber());
}
@Override
public String toString() {
return String.format("FieldScopes.allowingFields(%s)", join(fieldNumbers));
}
}
// Matches any specific fields which fall under one of the specified FieldDescriptors.
private static final class FieldDescriptorsLogic extends FieldMatcherLogicBase {
private final ImmutableSet<FieldDescriptor> fieldDescriptors;
FieldDescriptorsLogic(Iterable<FieldDescriptor> fieldDescriptors, boolean isRecursive) {
super(isRecursive);
this.fieldDescriptors = ImmutableSet.copyOf(fieldDescriptors);
}
@Override
boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
return fieldDescriptors.contains(fieldDescriptor);
}
@Override
public void validate(
Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
super.validate(rootDescriptor, fieldDescriptorValidator);
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
fieldDescriptorValidator.validate(fieldDescriptor);
}
}
@Override
public String toString() {
return String.format("FieldScopes.allowingFieldDescriptors(%s)", join(fieldDescriptors));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// COMPOUND SUBTYPES
//////////////////////////////////////////////////////////////////////////////////////////////////
private abstract static class CompoundFieldScopeLogic<T extends CompoundFieldScopeLogic<T>>
extends FieldScopeLogic {
final ImmutableList<FieldScopeLogic> elements;
CompoundFieldScopeLogic(FieldScopeLogic singleElem) {
this.elements = ImmutableList.of(singleElem);
}
CompoundFieldScopeLogic(FieldScopeLogic firstElem, FieldScopeLogic secondElem) {
this.elements = ImmutableList.of(firstElem, secondElem);
}
@Override
public final void validate(
Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
for (FieldScopeLogic elem : elements) {
elem.validate(rootDescriptor, fieldDescriptorValidator);
}
}
/** Helper to produce a new {@code CompoundFieldScopeLogic} of the same type as the subclass. */
abstract T newLogicOfSameType(List<FieldScopeLogic> newElements);
@Override
final FieldScopeLogic subScopeImpl(Descriptor rootDescriptor, SubScopeId subScopeId) {
ImmutableList.Builder<FieldScopeLogic> builder =
ImmutableList.builderWithExpectedSize(elements.size());
for (FieldScopeLogic elem : elements) {
builder.add(elem.subScope(rootDescriptor, subScopeId));
}
return newLogicOfSameType(builder.build());
}
}
private static final class IntersectionFieldScopeLogic
extends CompoundFieldScopeLogic<IntersectionFieldScopeLogic> {
IntersectionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
super(subject1, subject2);
}
@Override
IntersectionFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
checkArgument(newElements.size() == 2, "Expected 2 elements: %s", newElements);
return new IntersectionFieldScopeLogic(newElements.get(0), newElements.get(1));
}
@Override
FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
// The intersection of two scopes is ignorable if either scope is itself ignorable.
return intersection(
elements.get(0).policyFor(rootDescriptor, subScopeId),
elements.get(1).policyFor(rootDescriptor, subScopeId));
}
private static FieldScopeResult intersection(
FieldScopeResult result1, FieldScopeResult result2) {
if (result1 == FieldScopeResult.EXCLUDED_RECURSIVELY
|| result2 == FieldScopeResult.EXCLUDED_RECURSIVELY) {
// If either argument is excluded recursively, the result is too.
return FieldScopeResult.EXCLUDED_RECURSIVELY;
} else if (!result1.included() || !result2.included()) {
// Otherwise, we exclude non-recursively if either result is an exclusion.
return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
} else if (result1.recursive() && result2.recursive()) {
// We include recursively if both arguments are recursive.
return FieldScopeResult.INCLUDED_RECURSIVELY;
} else {
// Otherwise, we include non-recursively.
return FieldScopeResult.INCLUDED_NONRECURSIVELY;
}
}
@Override
public String toString() {
return String.format("(%s && %s)", elements.get(0), elements.get(1));
}
}
private static final class UnionFieldScopeLogic
extends CompoundFieldScopeLogic<UnionFieldScopeLogic> {
UnionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
super(subject1, subject2);
}
@Override
UnionFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
checkArgument(newElements.size() == 2, "Expected 2 elements: %s", newElements);
return new UnionFieldScopeLogic(newElements.get(0), newElements.get(1));
}
@Override
FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
// The union of two scopes is ignorable only if both scopes are themselves ignorable.
return union(
elements.get(0).policyFor(rootDescriptor, subScopeId),
elements.get(1).policyFor(rootDescriptor, subScopeId));
}
private static FieldScopeResult union(FieldScopeResult result1, FieldScopeResult result2) {
if (result1 == FieldScopeResult.INCLUDED_RECURSIVELY
|| result2 == FieldScopeResult.INCLUDED_RECURSIVELY) {
// If either argument is included recursively, the result is too.
return FieldScopeResult.INCLUDED_RECURSIVELY;
} else if (result1.included() || result2.included()) {
// Otherwise, if either is included, we include non-recursively.
return FieldScopeResult.INCLUDED_NONRECURSIVELY;
} else if (result1.recursive() && result2.recursive()) {
// If both arguments are recursive, we exclude recursively.
return FieldScopeResult.EXCLUDED_RECURSIVELY;
} else {
// Otherwise, we exclude exclude non-recursively.
return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
}
}
@Override
public String toString() {
return String.format("(%s || %s)", elements.get(0), elements.get(1));
}
}
private static final class NegationFieldScopeLogic
extends CompoundFieldScopeLogic<NegationFieldScopeLogic> {
NegationFieldScopeLogic(FieldScopeLogic subject) {
super(subject);
}
@Override
NegationFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
checkArgument(newElements.size() == 1, "Expected 1 element: %s", newElements);
return new NegationFieldScopeLogic(newElements.get(0));
}
@Override
FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
FieldScopeResult result = elements.get(0).policyFor(rootDescriptor, subScopeId);
return FieldScopeResult.of(!result.included(), result.recursive());
}
@Override
public String toString() {
return String.format("!(%s)", elements.get(0));
}
}
static FieldScopeLogic and(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
return new IntersectionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
}
static FieldScopeLogic or(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
return new UnionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
}
static FieldScopeLogic not(FieldScopeLogic fieldScopeLogic) {
return new NegationFieldScopeLogic(fieldScopeLogic);
}
}