Skip to content

Commit

Permalink
Merge pull request #497 from SpineEventEngine/generate-java-validation
Browse files Browse the repository at this point in the history
Generate Java validation code at compile time
  • Loading branch information
dmdashenkov authored Dec 30, 2019
2 parents 441e772 + e19a583 commit 37d9114
Show file tree
Hide file tree
Showing 206 changed files with 7,110 additions and 4,663 deletions.
4 changes: 4 additions & 0 deletions base-validating-builders/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ apply plugin: 'com.google.protobuf'
apply plugin: 'io.spine.tools.spine-model-compiler'
apply plugin: 'idea'

modelCompiler {
generateValidation = true
}

repositories {
// This defines the `libs` directory of upstream projects as a local repository.
// See `dependencies` section below for definition of the dependency on the JAR produced by
Expand Down
13 changes: 9 additions & 4 deletions base/src/main/java/io/spine/code/java/ClassName.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@

import java.util.Deque;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newLinkedList;
import static io.spine.code.java.SimpleClassName.OR_BUILDER_SUFFIX;
import static io.spine.util.Preconditions2.checkNotEmptyOrBlank;
Expand Down Expand Up @@ -313,11 +313,16 @@ private static String afterDot(String fullName) {
* Obtains the name of the package of this class.
*/
public PackageName packageName() {
int packageEndIndex = packageEndIndex();
String result = value().substring(0, packageEndIndex);
return PackageName.of(result);
}

private int packageEndIndex() {
String fullName = value();
int lastDotIndex = fullName.lastIndexOf(DOT_SEPARATOR);
checkArgument(lastDotIndex > 0, "%s should be qualified.", fullName);
String result = fullName.substring(0, lastDotIndex);
return PackageName.of(result);
checkState(lastDotIndex > 0, "%s should be qualified.", fullName);
return lastDotIndex;
}

/**
Expand Down
31 changes: 28 additions & 3 deletions base/src/main/java/io/spine/code/proto/FieldContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ public FieldContext forChild(FieldDescriptor child) {
return new FieldContext(this, child);
}

/**
* Obtains {@code FieldContext} for the specified child.
*
* @param child
* the child declaration
* @return the child declaration context
*/
public FieldContext forChild(FieldDeclaration child) {
return forChild(child.descriptor());
}

/**
* Obtains target of this context.
*
Expand All @@ -105,6 +116,16 @@ public FieldDescriptor target() {
return checkNotNull(target, "Empty context cannot have a target.");
}

/**
* Obtains target of this context as a {@link FieldDeclaration}.
*
* @return the target declaration
*/
public FieldDeclaration targetDeclaration() {
FieldDescriptor target = target();
return new FieldDeclaration(target);
}

private Optional<FieldDescriptor> targetParent() {
return parent == null ? Optional.empty() : Optional.ofNullable(parent.target);
}
Expand Down Expand Up @@ -164,12 +185,16 @@ public boolean equals(Object o) {
return false;
}
FieldContext context = (FieldContext) o;
return Objects.equals(target, context.target) &&
Objects.equals(parent, context.parent);
return Objects.equals(targetNameOrEmpty(), context.targetNameOrEmpty())
&& Objects.equals(parent, context.parent);
}

@Override
public int hashCode() {
return Objects.hash(target, parent);
return Objects.hash(targetNameOrEmpty(), parent);
}

private String targetNameOrEmpty() {
return target != null ? target.getFullName() : "";
}
}
75 changes: 57 additions & 18 deletions base/src/main/java/io/spine/code/proto/FieldDeclaration.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import com.google.common.base.Objects;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.Any;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
Expand Down Expand Up @@ -128,21 +129,35 @@ private boolean sameMessageType(Message msg) {
}

/**
* Obtains fully-qualified name of the Java class that corresponds to the declared type
* of the field.
* Obtains fully-qualified canonical name of the Java class that corresponds to the declared
* type of the field.
*
* <p>If the field is {@code repeated}, obtains the name of the elements.
*
* <p>If the field is a {@code map}, obtains the name of the values.
*/
public String javaTypeName() {
return isMap()
? javaTypeName(valueDeclaration().field)
: javaTypeName(this.field);
}

private static String javaTypeName(FieldDescriptor field) {
FieldDescriptor.Type fieldType = field.getType();
if (fieldType == MESSAGE) {
return messageClassName();
MessageType messageType =
new MessageType(field.getMessageType());
return messageType.javaClassName()
.canonicalName();
}

if (fieldType == ENUM) {
return enumClassName();
EnumType enumType = EnumType.create(field.getEnumType());
return enumType.javaClassName()
.canonicalName();
}

return ScalarType.javaTypeName(field.toProto()
.getType());
return ScalarType.javaTypeName(field.toProto().getType());
}

private String messageClassName() {
Expand All @@ -161,11 +176,6 @@ private String messageClassName() {
}
}

private String enumClassName() {
EnumType enumType = EnumType.create(field.getEnumType());
return enumType.javaClassName().value();
}

/**
* Determines whether the field is an entity ID.
*
Expand All @@ -177,21 +187,28 @@ private String enumClassName() {
* </ul>
*
* @return {@code true} if the field is an entity ID, {@code false} otherwise
* @see #isId()
*/
public boolean isEntityId() {
return isFirstField() && isEntityField() && isNotCollection();
}

/**
* Determines whether the field is a command ID.
* Determines whether the field is an ID.
*
* <p>A command ID is the first field of a message declared in a
* {@link MessageFile#COMMANDS commands file}.
* <p>An ID satisfies the following conditions:
* <ul>
* <li>Declared as the first field.
* <li>Declared inside an {@linkplain EntityOption#getKind() entity state message} or
* a {@linkplain io.spine.base.CommandMessage command message};
* <li>Is not a map or a repeated field.
* </ul>
*
* @return {@code true} if the field is a command ID, {@code false} otherwise
* @return {@code true} if the field is an entity ID, {@code false} otherwise
*/
public boolean isCommandId() {
return isFirstField() && isCommandsFile();
public boolean isId() {
boolean fieldMatches = isFirstField() && isNotCollection();
return fieldMatches && (isCommandsFile() || isEntityField());
}

/**
Expand All @@ -209,16 +226,28 @@ public boolean isString() {
}

/**
* Tells if the field is of enum type.
* Tells if the field is of an enum type.
*/
public boolean isEnum() {
return field.getType() == ENUM;
}

/**
* Tells if the field is of a message type.
*/
public boolean isMessage() {
return field.getType() == MESSAGE;
}

/**
* Tells if the field is of type {@code google.protobuf.Any}.
*/
public boolean isAny() {
return isMessage() && field.getMessageType()
.getFullName()
.equals(Any.getDescriptor().getFullName());
}

/**
* Determines whether the declaration is a singular value.
*
Expand Down Expand Up @@ -356,4 +385,14 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hashCode(declaringMessage, field.getFullName());
}

/**
* Obtains qualified name of this field.
*
* <p>Example: {@code spine.net.Uri.protocol}.
*/
@Override
public String toString() {
return field.getFullName();
}
}
13 changes: 12 additions & 1 deletion base/src/main/java/io/spine/code/proto/Option.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,19 @@ public interface Option<@ImmutableTypeParameter T,
* Obtains the value of this option for the specified object that holds it.
*
* @param object
* holder of the option
* the option holder
* @return value of this option
*/
Optional<T> valueFrom(K object);

/**
* Checks if the option is declared on the given holder.
*
* @param object
* the option holder
* @return {@code true} if the option is declared and is non-default, {@code false} otherwise
*/
default boolean valuePresent(K object) {
return valueFrom(object).isPresent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ private OptionExtensionRegistry() {
}

/**
* Obtains the {@link ExtensionRegistry} with all the {@code
* spine/options.proto} extensions.
* Obtains the {@link ExtensionRegistry} with all the {@code spine/options.proto} extensions.
*/
public static ExtensionRegistry instance() {
return EXTENSIONS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,27 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.validate.option;
package io.spine.protobuf;

import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.ImmutableTypeParameter;
import com.google.protobuf.Message;
import io.spine.annotation.Beta;
import io.spine.validate.ConstraintViolation;

/**
* A rule that limits the set of possible values for {@code T} and produces
* {@code ConstraintViolations} upon finding values of {@code T} which do not fit into the
* set limits.
* A message with validation constraints.
*
* @param <T>
* a type of values that this constraint is applicable to
* <p>Please see {@code spine/options.proto} for the definitions of validation options.
*/
@Beta
@Immutable
public interface Constraint<@ImmutableTypeParameter T> {
public interface MessageWithConstraints extends Message {

/**
* Checks the specified value against this constraint.
* Validates this message according to the rules in the Protobuf definition.
*
* <p>If the value has breaks the rules imposed by this constraint, {@code ConstraintViolations}
* are produced and returned.
*
* @param value
* value that is being checked against this constraint
* @return violations of this constraint
* @return a list of {@code ConstraintViolation}s or an empty list if the message is valid
*/
ImmutableList<ConstraintViolation> check(T value);
ImmutableList<ConstraintViolation> validate();
}
25 changes: 25 additions & 0 deletions base/src/main/java/io/spine/protobuf/Messages.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.protobuf.Any;
import com.google.protobuf.Message;
import com.google.protobuf.MessageLite;
import com.google.protobuf.ProtocolMessageEnum;
import io.spine.annotation.Internal;

import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -137,6 +138,30 @@ public static boolean isNotDefault(Message object) {
return result;
}

/**
* Verifies if the passed Protobuf enum element is the enum's default state.
*
* @param messageEnum
* the enum element to inspect
* @return {@code true} if the passed enum is default, {@code false} otherwise
*/
public static boolean isDefault(ProtocolMessageEnum messageEnum) {
checkNotNull(messageEnum);
return messageEnum.getNumber() == 0;
}

/**
* Verifies if the passed Protobuf enum element is NOT the enum's default state.
*
* @param messageEnum
* the enum element to inspect
* @return {@code false} if the passed enum is default, {@code true} otherwise
*/
public static boolean isNotDefault(ProtocolMessageEnum messageEnum) {
checkNotNull(messageEnum);
return !isDefault(messageEnum);
}

/**
* The loader of the cache of default instances per {@link Message} class.
*
Expand Down
7 changes: 4 additions & 3 deletions base/src/main/java/io/spine/protobuf/TypeConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,6 @@ protected Message toMessage(Message input) {
* best-performant solution among options, such as {@link Class#getCanonicalName()
* Class.getCanonicalName()}.
*/
@SuppressWarnings("OverlyCoupledClass") // OK, as references a lot of type for casting.
private static final class PrimitiveTypeCaster<M extends Message, T>
extends MessageCaster<M, T> {

Expand Down Expand Up @@ -325,7 +324,8 @@ private static final class PrimitiveTypeCaster<M extends Message, T>
@Override
protected T toObject(M input) {
Class<?> boxedType = input.getClass();
@SuppressWarnings("unchecked") Converter<M, T> typeUnpacker =
@SuppressWarnings("unchecked")
Converter<M, T> typeUnpacker =
(Converter<M, T>) PROTO_WRAPPER_TO_HANDLER.get(boxedType);
checkArgument(typeUnpacker != null,
"Could not find a primitive type for %s.",
Expand All @@ -337,7 +337,8 @@ protected T toObject(M input) {
@Override
protected M toMessage(T input) {
Class<?> cls = input.getClass();
@SuppressWarnings("unchecked") Converter<M, T> converter =
@SuppressWarnings("unchecked")
Converter<M, T> converter =
(Converter<M, T>) PRIMITIVE_TO_HANDLER.get(cls);
checkArgument(converter != null,
"Could not find a wrapper type for %s.",
Expand Down
8 changes: 4 additions & 4 deletions base/src/main/java/io/spine/type/KnownTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,14 @@ public boolean contains(TypeUrl typeUrl) {
return type;
}

private Type get(TypeName name) throws UnknownTypeException {
Type result = typeSet.find(name)
.orElseThrow(() -> new UnknownTypeException(name.value()));
private Type<?, ?> get(TypeName name) throws UnknownTypeException {
Type<?, ?> result = typeSet.find(name)
.orElseThrow(() -> new UnknownTypeException(name.value()));
return result;
}

private ClassName get(TypeUrl typeUrl) {
Type type = get(typeUrl.toTypeName());
Type<?, ?> type = get(typeUrl.toTypeName());
ClassName result = type.javaClassName();
return result;
}
Expand Down
Loading

0 comments on commit 37d9114

Please sign in to comment.