Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for maps #258

Merged
merged 3 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pbj-core/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Version number
version=0.8.7-SNAPSHOT
version=0.9.0-SNAPSHOT

# Need increased heap for running Gradle itself, or SonarQube will run the JVM out of metaspace
org.gradle.jvmargs=-Xmx2048m
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ oneofField
// Map field

mapField
: MAP LT keyType COMMA type_ GT mapName
: docComment MAP LT keyType COMMA type_ GT mapName
EQ fieldNumber ( LB fieldOptions RB )? SEMI
;
keyType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ public static String getFieldsHashCode(final List<Field> fields, String generate
result = 31 * result + Integer.hashCode($fieldName.protoOrdinal());
}
""").replace("$fieldName", f.nameCamelFirstLower());
} else if (f.type() == Field.FieldType.MAP) {
generatedCodeSoFar += getMapHashCodeGeneration(generatedCodeSoFar, f);
} else if (f.type() == Field.FieldType.STRING ||
f.parent() == null) { // process sub message
generatedCodeSoFar += (
Expand Down Expand Up @@ -350,6 +352,33 @@ private static String getRepeatedHashCodeGeneration(String generatedCodeSoFar, F
return generatedCodeSoFar;
}

/**
* Get the hashcode codegen for a map field.
* @param generatedCodeSoFar The string that the codegen is generated into.
* @param f The field for which to generate the hash code.
* @return Updated codegen string.
*/
@NonNull
private static String getMapHashCodeGeneration(String generatedCodeSoFar, final Field f) {
generatedCodeSoFar += (
"""
for (Object k : ((PbjMap) $fieldName).getSortedKeys()) {
if (k != null) {
result = 31 * result + k.hashCode();
} else {
result = 31 * result;
}
Object v = $fieldName.get(k);
if (v != null) {
result = 31 * result + v.hashCode();
} else {
result = 31 * result;
}
}
""").replace("$fieldName", f.nameCamelFirstLower());
return generatedCodeSoFar;
}

/**
* Recursively calculates `equals` statement for a message fields.
*
Expand Down Expand Up @@ -417,6 +446,7 @@ else if (f.repeated()) {
} else if (f.type() == Field.FieldType.STRING ||
f.type() == Field.FieldType.BYTES ||
f.type() == Field.FieldType.ENUM ||
f.type() == Field.FieldType.MAP ||
f.parent() == null /* Process a sub-message */) {
generatedCodeSoFar += (
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ public String getPackageFieldMessageType(final FileType fileType, final FieldCon
return lookupHelper.getPackage(srcProtoFileContext, fileType, fieldContext.type_().messageType());
}

/**
* Get the Java package a class should be generated into for a given typeContext and file type.
*
* @param fileType The type of file we want the package for
* @param typeContext The field to get package for message type for
* @return java package to put model class in
*/
public String getPackageFieldMessageType(final FileType fileType, final Type_Context typeContext) {
return lookupHelper.getPackage(srcProtoFileContext, fileType, typeContext.messageType());
}

/**
* Get the PBJ Java package a class should be generated into for a given fieldContext and file type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,49 +189,73 @@ default OneOfField parent() {
return null;
}

/**
* Extract the name of the Java model class for a message type,
* or null if the type is not a message.
*/
static String extractMessageTypeName(final Protobuf3Parser.Type_Context typeContext) {
return typeContext.messageType() == null ? null : typeContext.messageType().messageName().getText();
}

/**
* Extract the name of the Java package for a given FileType for a message type,
* or null if the type is not a message.
*/
static String extractMessageTypePackage(
final Protobuf3Parser.Type_Context typeContext,
final FileType fileType,
final ContextualLookupHelper lookupHelper) {
return typeContext.messageType() == null || typeContext.messageType().messageName().getText() == null ? null :
lookupHelper.getPackageFieldMessageType(fileType, typeContext);
}

/**
* Field type enum for use in field classes
*/
enum FieldType {
/** Protobuf message field type */
MESSAGE("Object", "null", TYPE_LENGTH_DELIMITED),
MESSAGE("Object", "Object", "null", TYPE_LENGTH_DELIMITED),
/** Protobuf enum(unsigned varint encoded int of ordinal) field type */
ENUM("int", "null", TYPE_VARINT),
ENUM("int", "Integer", "null", TYPE_VARINT),
/** Protobuf int32(signed varint encoded int) field type */
INT32("int", "0", TYPE_VARINT),
INT32("int", "Integer", "0", TYPE_VARINT),
/** Protobuf uint32(unsigned varint encoded int) field type */
UINT32("int", "0", TYPE_VARINT),
UINT32("int", "Integer", "0", TYPE_VARINT),
/** Protobuf sint32(signed zigzag varint encoded int) field type */
SINT32("int", "0", TYPE_VARINT),
SINT32("int", "Integer", "0", TYPE_VARINT),
/** Protobuf int64(signed varint encoded long) field type */
INT64("long", "0", TYPE_VARINT),
INT64("long", "Long", "0", TYPE_VARINT),
/** Protobuf uint64(unsigned varint encoded long) field type */
UINT64("long", "0", TYPE_VARINT),
UINT64("long", "Long", "0", TYPE_VARINT),
/** Protobuf sint64(signed zigzag varint encoded long) field type */
SINT64("long", "0", TYPE_VARINT),
SINT64("long", "Long", "0", TYPE_VARINT),
/** Protobuf float field type */
FLOAT("float", "0", TYPE_FIXED32),
FLOAT("float", "Float", "0", TYPE_FIXED32),
/** Protobuf fixed int32(fixed encoding int) field type */
FIXED32("int", "0", TYPE_FIXED32),
FIXED32("int", "Integer", "0", TYPE_FIXED32),
/** Protobuf sfixed int32(signed fixed encoding int) field type */
SFIXED32("int", "0", TYPE_FIXED32),
SFIXED32("int", "Integer", "0", TYPE_FIXED32),
/** Protobuf double field type */
DOUBLE("double", "0", TYPE_FIXED64),
DOUBLE("double", "Double", "0", TYPE_FIXED64),
/** Protobuf sfixed64(fixed encoding long) field type */
FIXED64("long", "0", TYPE_FIXED64),
FIXED64("long", "Long", "0", TYPE_FIXED64),
/** Protobuf sfixed64(signed fixed encoding long) field type */
SFIXED64("long", "0", TYPE_FIXED64),
SFIXED64("long", "Long", "0", TYPE_FIXED64),
/** Protobuf string field type */
STRING("String", "\"\"", TYPE_LENGTH_DELIMITED),
STRING("String", "String", "\"\"", TYPE_LENGTH_DELIMITED),
/** Protobuf bool(boolean) field type */
BOOL("boolean", "false", TYPE_VARINT),
BOOL("boolean", "Boolean", "false", TYPE_VARINT),
/** Protobuf bytes field type */
BYTES("Bytes", "Bytes.EMPTY", TYPE_LENGTH_DELIMITED),
BYTES("Bytes", "Bytes", "Bytes.EMPTY", TYPE_LENGTH_DELIMITED),
/** Protobuf oneof field type, this is not a true field type in protobuf. Needed here for a few edge cases */
ONE_OF("OneOf", "null", 0 );// BAD TYPE
ONE_OF("OneOf", "OneOf", "null", 0 ),// BAD TYPE
// On the wire, a map is a repeated Message {key, value}, sorted in the natural order of keys for determinism.
MAP("Map", "Map", "Collections.EMPTY_MAP", TYPE_LENGTH_DELIMITED );

/** The type of field type in Java code */
public final String javaType;
/** The type of boxed field type in Java code */
public final String boxedType;
/** The field type default value in Java code */
public final String javaDefault;
/** The protobuf wire type for field type */
Expand All @@ -241,11 +265,13 @@ enum FieldType {
* Construct a new FieldType enum
*
* @param javaType The type of field type in Java code
* @param boxedType The boxed type of the field type, e.g. Integer for an int field.
* @param javaDefault The field type default value in Java code
* @param wireType The protobuf wire type for field type
*/
FieldType(String javaType, final String javaDefault, int wireType) {
FieldType(String javaType, final String boxedType, final String javaDefault, int wireType) {
anthony-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
this.javaType = javaType;
this.boxedType = boxedType;
this.javaDefault = javaDefault;
this.wireType = wireType;
}
Expand Down Expand Up @@ -337,5 +363,42 @@ static FieldType of(Protobuf3Parser.Type_Context typeContext, final ContextualL
throw new IllegalArgumentException("Unknown field type: "+typeContext);
}
}

/**
* Get the field type for a given map key type parser context
*
* @param typeContext The parser context to get field type for
* @param lookupHelper Lookup helper with global context
* @return The field type enum for parser context
*/
static FieldType of(Protobuf3Parser.KeyTypeContext typeContext, final ContextualLookupHelper lookupHelper) {
if (typeContext.INT32() != null) {
return FieldType.INT32;
} else if (typeContext.UINT32() != null) {
return FieldType.UINT32;
} else if (typeContext.SINT32() != null) {
return FieldType.SINT32;
} else if (typeContext.INT64() != null) {
return FieldType.INT64;
} else if (typeContext.UINT64() != null) {
return FieldType.UINT64;
} else if (typeContext.SINT64() != null) {
return FieldType.SINT64;
} else if (typeContext.FIXED32() != null) {
return FieldType.FIXED32;
} else if (typeContext.SFIXED32() != null) {
return FieldType.SFIXED32;
} else if (typeContext.FIXED64() != null) {
return FieldType.FIXED64;
} else if (typeContext.SFIXED64() != null) {
return FieldType.SFIXED64;
} else if (typeContext.STRING() != null) {
return FieldType.STRING;
} else if (typeContext.BOOL() != null) {
return FieldType.BOOL;
} else {
throw new IllegalArgumentException("Unknown map key type: " + typeContext);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.hedera.pbj.compiler.impl;

import java.util.Set;
import com.hedera.pbj.compiler.impl.grammar.Protobuf3Parser;
import static com.hedera.pbj.compiler.impl.SingleField.getDeprecatedOption;

/**
* A field of type map.
* <p>
* In protobuf, a map is essentially a repeated map entry message with two fields: key and value.
* However, we don't model the map entry message explicitly for performance reasons. Instead,
* we deal with the keys and values directly, and define synthetic Field objects for them here
* for convenience, so that we can reuse the majority of the code generation code.
* <p>
* In model implementations we use a custom implementation of the Map interface named PbjMap
* which is an immutable map that exposes a SortedKeys list which allows one to iterate
* the map deterministically which is useful for serializing, computing the hash code, etc.
*/
public record MapField(
/** A synthetic "key" field in a map entry. */
Field keyField,
/** A synthetic "value" field in a map entry. */
Field valueField,
// The rest of the fields below simply implement the Field interface:
boolean repeated,
int fieldNumber,
String name,
FieldType type,
String protobufFieldType,
String javaFieldTypeBase,
String methodNameType,
String parseCode,
String javaDefault,
String parserFieldsSetMethodCase,
String comment,
boolean deprecated
) implements Field {

/**
* Construct a MapField instance out of a MapFieldContext and a lookup helper.
*/
public MapField(Protobuf3Parser.MapFieldContext mapContext, final ContextualLookupHelper lookupHelper) {
this(
new SingleField(
false,
FieldType.of(mapContext.keyType(), lookupHelper),
1,
"___" + mapContext.mapName().getText() + "__key",
null,
null,
null,
null,
"An internal, private map entry key for " + mapContext.mapName().getText(),
false,
null),
new SingleField(
false,
FieldType.of(mapContext.type_(), lookupHelper),
2,
"___" + mapContext.mapName().getText() + "__value",
Field.extractMessageTypeName(mapContext.type_()),
Field.extractMessageTypePackage(mapContext.type_(), FileType.MODEL, lookupHelper),
Field.extractMessageTypePackage(mapContext.type_(), FileType.CODEC, lookupHelper),
Field.extractMessageTypePackage(mapContext.type_(), FileType.TEST, lookupHelper),
"An internal, private map entry value for " + mapContext.mapName().getText(),
false,
null),

false, // maps cannot be repeated
Integer.parseInt(mapContext.fieldNumber().getText()),
mapContext.mapName().getText(),
FieldType.MAP,
"",
"",
"",
null,
"PbjMap.EMPTY",
"",
Common.buildCleanFieldJavaDoc(Integer.parseInt(mapContext.fieldNumber().getText()), mapContext.docComment()),
getDeprecatedOption(mapContext.fieldOptions())
);
}

/**
* Composes the Java generic type of the map field, e.g. "&lt;Integer, String&gt;" for a Map&lt;Integer, String&gt;.
*/
public String javaGenericType() {
return "<" + keyField.type().boxedType + ", " +
(valueField().type() == FieldType.MESSAGE ? ((SingleField)valueField()).messageType() : valueField().type().boxedType)
+ ">";
}

/**
* {@inheritDoc}
*/
@Override
public String javaFieldType() {
return "Map" + javaGenericType();
}

private void composeFieldDef(StringBuilder sb, Field field) {
sb.append("""
/**
* $doc
*/
"""
.replace("$doc", field.comment().replaceAll("\n","\n * "))
);
sb.append(" public static final FieldDefinition %s = new FieldDefinition(\"%s\", FieldType.%s, %s, false, false, %d);\n"
.formatted(Common.camelToUpperSnake(field.name()), field.name(), field.type().fieldType(), field.repeated(), field.fieldNumber()));
}

/**
* {@inheritDoc}
*/
@Override
public String schemaFieldsDef() {
StringBuilder sb = new StringBuilder();
composeFieldDef(sb, this);
composeFieldDef(sb, keyField);
composeFieldDef(sb, valueField);
return sb.toString();
}

/**
* {@inheritDoc}
*/
@Override
public String schemaGetFieldsDefCase() {
return "case %d -> %s;".formatted(fieldNumber, Common.camelToUpperSnake(name));
}

/**
* {@inheritDoc}
*/
@Override
public void addAllNeededImports(
final Set<String> imports,
final boolean modelImports,
final boolean codecImports,
final boolean testImports) {
if (modelImports) {
imports.add("java.util");
}
if (codecImports) {
imports.add("java.util.stream");
imports.add("com.hedera.pbj.runtime.test");
}
}
}
Loading
Loading