Skip to content
Open
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
72 changes: 72 additions & 0 deletions protoc_plugin/lib/names.dart
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,75 @@ int countRealOneofs(DescriptorProto descriptor) {

String lowerCaseFirstLetter(String input) =>
input[0].toLowerCase() + input.substring(1);

/// Converts a TitleCase or camelCase enum name to UPPER_SNAKE_CASE.
/// Examples: 'PhoneType' -> 'PHONE_TYPE', 'HTTPStatus' -> 'HTTP_STATUS'
String titleCaseToUpperSnakeCase(String input) {
if (input.isEmpty) return input;

final buffer = StringBuffer();
bool previousWasLower = false;

for (var i = 0; i < input.length; i++) {
final char = input[i];
final isUpper = char == char.toUpperCase();
final isLower = char == char.toLowerCase();

if (i > 0 &&
isUpper &&
(previousWasLower ||
(i + 1 < input.length &&
input[i + 1] == input[i + 1].toLowerCase()))) {
buffer.write('_');
}

buffer.write(char.toUpperCase());
previousWasLower = isLower;
}

return buffer.toString();
}

/// Strips the enum name prefix from a protobuf-style enum value name and converts
/// the result to camelCase following Dart enum naming conventions.
/// Examples:
/// - enumName='PhoneType', valueName='PHONE_TYPE_MOBILE' -> 'mobile'
/// - enumName='PhoneType', valueName='PHONE_TYPE_UNSPECIFIED' -> 'unspecified'
/// - enumName='HTTPStatus', valueName='HTTP_STATUS_NOT_FOUND' -> 'notFound'
/// - enumName='HTTPStatus', valueName='HTTP_STATUS_OK' -> 'ok'
String stripEnumPrefix(String enumName, String valueName) {
final enumPrefix = titleCaseToUpperSnakeCase(enumName);
final prefixWithUnderscore = '${enumPrefix}_';

String strippedName;
if (valueName.startsWith(prefixWithUnderscore)) {
strippedName = valueName.substring(prefixWithUnderscore.length);
} else {
// If the value name doesn't start with the expected prefix, use as-is
strippedName = valueName;
}

// Convert UPPER_SNAKE_CASE to camelCase
return upperSnakeCaseToCamelCase(strippedName);
}

/// Converts UPPER_SNAKE_CASE to camelCase.
/// Examples:
/// - 'MOBILE' -> 'mobile'
/// - 'NOT_FOUND' -> 'notFound'
/// - 'UNSPECIFIED' -> 'unspecified'
String upperSnakeCaseToCamelCase(String input) {
if (input.isEmpty) return input;

final parts = input.toLowerCase().split('_');
if (parts.isEmpty) return input.toLowerCase();

final buffer = StringBuffer(parts[0]);
for (var i = 1; i < parts.length; i++) {
if (parts[i].isNotEmpty) {
buffer.write(parts[i][0].toUpperCase() + parts[i].substring(1));
}
}

return buffer.toString();
}
15 changes: 14 additions & 1 deletion protoc_plugin/lib/src/enum_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,21 @@ class EnumGenerator extends ProtobufContainer {
_aliases.add(EnumAlias(value, canonicalValue));
_originalAliasIndices.add(i);
}
// Generate Dart name for the enum value
String dartName;
final isProtobufEnumStyle =
parent.fileGen?.options.protobufEnumStyle ?? false;
if (isProtobufEnumStyle) {
// Strip enum prefix from protobuf-style enum values
final strippedName = stripEnumPrefix(descriptor.name, value.name);
dartName = avoidInitialUnderscore(strippedName);
} else {
// Use original style
dartName = avoidInitialUnderscore(value.name);
}

dartNames[value.name] = disambiguateName(
avoidInitialUnderscore(value.name),
dartName,
Comment on lines +67 to +81
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the ProtobufEnumStyle is not defined, I keep the orignal way of doing to not incorporate breaking change

usedNames,
enumSuffixes(),
);
Expand Down
19 changes: 19 additions & 0 deletions protoc_plugin/lib/src/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ class GenerationOptions {
final bool useGrpc;
final bool generateMetadata;
final bool disableConstructorArgs;
final bool protobufEnumStyle;

GenerationOptions({
this.useGrpc = false,
this.generateMetadata = false,
this.disableConstructorArgs = false,
this.protobufEnumStyle = false,
});
}

Expand Down Expand Up @@ -112,6 +114,19 @@ class DisableConstructorArgsParser implements SingleOptionParser {
}
}

class ProtobufEnumStyleParser implements SingleOptionParser {
bool protobufEnumStyleEnabled = false;

@override
void parse(String name, String? value, OnError onError) {
if (value != null) {
onError('Invalid protobuf-enum-style option. No value expected.');
return;
}
protobufEnumStyleEnabled = true;
}
}

Comment on lines +117 to +129
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New argument to enable protobuf enum style parsing. By default, the value is false to not incorporate breaking change!

/// Parser used by the compiler, which supports the `rpc` option (see
/// [GrpcOptionParser]) and any additional option added in [parsers]. If
/// [parsers] has a key for `rpc`, it will be ignored.
Expand All @@ -132,11 +147,15 @@ GenerationOptions? parseGenerationOptions(
final disableConstructorArgsParser = DisableConstructorArgsParser();
newParsers['disable_constructor_args'] = disableConstructorArgsParser;

final protobufEnumStyleParser = ProtobufEnumStyleParser();
newParsers['protobuf-enum-style'] = protobufEnumStyleParser;

if (genericOptionsParser(request, response, newParsers)) {
return GenerationOptions(
useGrpc: grpcOptionParser.grpcEnabled,
generateMetadata: generateMetadataParser.generateKytheInfo,
disableConstructorArgs: disableConstructorArgsParser.value,
protobufEnumStyle: protobufEnumStyleParser.protobufEnumStyleEnabled,
);
}
return null;
Expand Down
122 changes: 122 additions & 0 deletions protoc_plugin/test/enum_generator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,126 @@ void main() {
expectGolden(writer.emitSource(format: false), 'enum.pbenum.dart');
expectGolden(writer.sourceLocationInfo.toString(), 'enum.pbenum.dart.meta');
});

test('testEnumGeneratorWithProtobufEnumStyle', () {
// Create an enum with protobuf-style names (enum name prefixed values)
final ed =
EnumDescriptorProto()
..name = 'PhoneType'
..value.addAll([
EnumValueDescriptorProto()
..name = 'PHONE_TYPE_UNSPECIFIED'
..number = 0,
EnumValueDescriptorProto()
..name = 'PHONE_TYPE_MOBILE'
..number = 1,
EnumValueDescriptorProto()
..name = 'PHONE_TYPE_HOME'
..number = 2,
EnumValueDescriptorProto()
..name = 'PHONE_TYPE_WORK'
..number = 3,
]);

final writer = IndentingWriter(
generateMetadata: true,
fileName: 'sample.proto',
);

// Test with protobuf enum style enabled - this should strip prefixes
final optionsWithProtobufStyle = GenerationOptions(protobufEnumStyle: true);
final fg = FileGenerator(FileDescriptorProto(), optionsWithProtobufStyle);
final eg = EnumGenerator.topLevel(ed, fg, <String>{}, 0);
eg.generate(writer);

expectGolden(
writer.emitSource(format: false),
'enum_protobuf_style.pbenum.dart',
);
expectGolden(
writer.sourceLocationInfo.toString(),
'enum_protobuf_style.pbenum.dart.meta',
);
});

test('testEnumGeneratorWithoutProtobufEnumStyle', () {
// Same enum with protobuf-style names but without the flag
final ed =
EnumDescriptorProto()
..name = 'PhoneType'
..value.addAll([
EnumValueDescriptorProto()
..name = 'PHONE_TYPE_UNSPECIFIED'
..number = 0,
EnumValueDescriptorProto()
..name = 'PHONE_TYPE_MOBILE'
..number = 1,
]);

final writer = IndentingWriter(
generateMetadata: true,
fileName: 'sample.proto',
);

// Test without protobuf enum style - should use names as-is
final optionsWithoutProtobufStyle = GenerationOptions(
protobufEnumStyle: false,
);
final fg = FileGenerator(
FileDescriptorProto(),
optionsWithoutProtobufStyle,
);
final eg = EnumGenerator.topLevel(ed, fg, <String>{}, 0);
eg.generate(writer);

expectGolden(
writer.emitSource(format: false),
'enum_without_protobuf_style.pbenum.dart',
);
expectGolden(
writer.sourceLocationInfo.toString(),
'enum_without_protobuf_style.pbenum.dart.meta',
);
});

test('testEnumGeneratorWithComplexProtobufEnumStyle', () {
// Test with more complex enum names to verify camelCase conversion
final ed =
EnumDescriptorProto()
..name = 'HTTPStatusCode'
..value.addAll([
EnumValueDescriptorProto()
..name = 'HTTP_STATUS_CODE_UNSPECIFIED'
..number = 0,
EnumValueDescriptorProto()
..name = 'HTTP_STATUS_CODE_NOT_FOUND'
..number = 1,
EnumValueDescriptorProto()
..name = 'HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR'
..number = 2,
EnumValueDescriptorProto()
..name = 'HTTP_STATUS_CODE_OK'
..number = 3,
]);

final writer = IndentingWriter(
generateMetadata: true,
fileName: 'sample.proto',
);

// Test with protobuf enum style enabled
final optionsWithProtobufStyle = GenerationOptions(protobufEnumStyle: true);
final fg = FileGenerator(FileDescriptorProto(), optionsWithProtobufStyle);
final eg = EnumGenerator.topLevel(ed, fg, <String>{}, 0);
eg.generate(writer);

expectGolden(
writer.emitSource(format: false),
'enum_complex_protobuf_style.pbenum.dart',
);
expectGolden(
writer.sourceLocationInfo.toString(),
'enum_complex_protobuf_style.pbenum.dart.meta',
);
});
}
21 changes: 21 additions & 0 deletions protoc_plugin/test/goldens/enum_complex_protobuf_style.pbenum.dart
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a new golden test to validate protobuf enum style

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class HTTPStatusCode extends $pb.ProtobufEnum {
static const HTTPStatusCode unspecified = HTTPStatusCode._(0, _omitEnumNames ? '' : 'HTTP_STATUS_CODE_UNSPECIFIED');
static const HTTPStatusCode notFound = HTTPStatusCode._(1, _omitEnumNames ? '' : 'HTTP_STATUS_CODE_NOT_FOUND');
static const HTTPStatusCode internalServerError = HTTPStatusCode._(2, _omitEnumNames ? '' : 'HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR');
static const HTTPStatusCode ok = HTTPStatusCode._(3, _omitEnumNames ? '' : 'HTTP_STATUS_CODE_OK');

static const $core.List<HTTPStatusCode> values = <HTTPStatusCode> [
unspecified,
notFound,
internalServerError,
ok,
];

static final $core.List<HTTPStatusCode?> _byValue = $pb.ProtobufEnum.$_initByValueList(values, 3);
static HTTPStatusCode? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value];

const HTTPStatusCode._(super.value, super.name);
}


const $core.bool _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a new golden test to validate protobuf enum style

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
annotation: {
path: 5
path: 0
sourceFile: sample.proto
begin: 6
end: 20
}
annotation: {
path: 5
path: 0
path: 2
path: 0
sourceFile: sample.proto
begin: 78
end: 89
}
annotation: {
path: 5
path: 0
path: 2
path: 1
sourceFile: sample.proto
begin: 197
end: 205
}
annotation: {
path: 5
path: 0
path: 2
path: 2
sourceFile: sample.proto
begin: 311
end: 330
}
annotation: {
path: 5
path: 0
path: 2
path: 3
sourceFile: sample.proto
begin: 448
end: 450
}
21 changes: 21 additions & 0 deletions protoc_plugin/test/goldens/enum_protobuf_style.pbenum.dart
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a new golden test to validate protobuf enum style

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class PhoneType extends $pb.ProtobufEnum {
static const PhoneType unspecified = PhoneType._(0, _omitEnumNames ? '' : 'PHONE_TYPE_UNSPECIFIED');
static const PhoneType mobile = PhoneType._(1, _omitEnumNames ? '' : 'PHONE_TYPE_MOBILE');
static const PhoneType home = PhoneType._(2, _omitEnumNames ? '' : 'PHONE_TYPE_HOME');
static const PhoneType work = PhoneType._(3, _omitEnumNames ? '' : 'PHONE_TYPE_WORK');

static const $core.List<PhoneType> values = <PhoneType> [
unspecified,
mobile,
home,
work,
];

static final $core.List<PhoneType?> _byValue = $pb.ProtobufEnum.$_initByValueList(values, 3);
static PhoneType? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value];

const PhoneType._(super.value, super.name);
}


const $core.bool _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
Loading