Skip to content

Commit 0c722b8

Browse files
committed
feat: Generate enum strings for Compute
This moves code from google-cloud-dotnet to the generator, removing the post-processing. We can probably adopt this as-is for the moment (given that this is only for DIREGAPIC, and only Compute uses DIREGAPIC) but we should really have configuration within the service config publish settings to turn this feature on (and potentially configure the filename; that'll be easier to tell if we end up with more examples).
1 parent 38cb7dd commit 0c722b8

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

Google.Api.Generator/CodeGenerator.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
using Google.Protobuf;
2525
using Google.Protobuf.Reflection;
2626
using Grpc.ServiceConfig;
27+
using Microsoft.CodeAnalysis.CSharp.Syntax;
2728
using System;
2829
using System.Collections.Generic;
2930
using System.IO;
@@ -467,6 +468,13 @@ private static IEnumerable<ResultFile> GeneratePackage(string ns,
467468
hasContent = true;
468469
}
469470
}
471+
if (EnumStringGenerator.MaybeGenerate(SourceFileContext.CreateFullyAliased(clock, WellknownNamespaceAliases), ns, packageFileDescriptors) is CompilationUnitSyntax enumConstantsClass)
472+
{
473+
// TODO: Policy or config for filename.
474+
yield return new ResultFile($"{clientPathPrefix}{EnumStringGenerator.RootClassName}.g.cs", enumConstantsClass);
475+
hasContent = true;
476+
}
477+
470478
// Now we've processed all the files, check for duplicate resource names.
471479
if (duplicateResourceNameClasses.Count > 0)
472480
{
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Google.Api.Generator.ProtoUtils;
16+
using Google.Api.Generator.Utils;
17+
using Google.Api.Generator.Utils.Roslyn;
18+
using Google.Protobuf.Reflection;
19+
using Microsoft.CodeAnalysis.CSharp.Syntax;
20+
using System;
21+
using System.Collections.Generic;
22+
using System.Linq;
23+
using static Google.Api.Generator.Utils.Roslyn.Modifier;
24+
using static Google.Api.Generator.Utils.Roslyn.RoslynBuilder;
25+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
26+
27+
namespace Google.Api.Generator.Generation;
28+
29+
/// <summary>
30+
/// Generates a static class (with nested static classes) to provide
31+
/// string constants for the wire representations of enum values. This is only used
32+
/// for DIREGAPIC APIs (currently just Compute).
33+
/// </summary>
34+
internal class EnumStringGenerator
35+
{
36+
// TODO: Determine a policy or config for the class, file and API description.
37+
internal const string RootClassName = "ComputeEnumConstants";
38+
private const string ApiDescription = "Compute API";
39+
40+
public static CompilationUnitSyntax MaybeGenerate(SourceFileContext ctx, string ns, IEnumerable<FileDescriptor> packageFileDescriptors)
41+
{
42+
// TODO: Add an option in the service config for this... although this will be fine for now,
43+
// as only Compute needs this.
44+
if (!ns.StartsWith("Google.Cloud.Compute.", StringComparison.Ordinal))
45+
{
46+
return null;
47+
}
48+
49+
var container = new EnumContainer(null, packageFileDescriptors.SelectMany(d => d.EnumTypes), packageFileDescriptors.SelectMany(d => d.MessageTypes));
50+
if (!container.ShouldGenerate)
51+
{
52+
return null;
53+
}
54+
55+
var nsDeclaration = Namespace(ns);
56+
using (ctx.InNamespace(nsDeclaration))
57+
{
58+
nsDeclaration = nsDeclaration.AddMembers(new[] { container.Generate(ctx) });
59+
}
60+
return ctx.CreateCompilationUnit(nsDeclaration);
61+
}
62+
63+
/// <summary>
64+
/// A container for either enums or other nested enum containers (or both).
65+
/// </summary>
66+
public class EnumContainer
67+
{
68+
public MessageDescriptor Descriptor { get; }
69+
70+
public List<EnumContainer> NestedContainers { get; }
71+
72+
public List<EnumDescriptor> Enums { get; }
73+
74+
public bool ShouldGenerate => Enums.Count > 0 || NestedContainers.Any(nc => nc.ShouldGenerate);
75+
76+
public EnumContainer(MessageDescriptor descriptor, IEnumerable<EnumDescriptor> enums, IEnumerable<MessageDescriptor> messages)
77+
{
78+
Descriptor = descriptor;
79+
Enums = enums.ToList();
80+
NestedContainers = messages.Select(message => new EnumContainer(message, message.EnumTypes, message.NestedTypes)).ToList();
81+
}
82+
83+
public MemberDeclarationSyntax Generate(SourceFileContext ctx)
84+
{
85+
var cls = Descriptor is object
86+
? Class(Public | Static, Typ.Nested(ctx.CurrentTyp, Descriptor.Name))
87+
.WithXmlDoc(XmlDoc.Summary("Container class for enums within the ", GlobalTypeSyntax(ProtoTyp.Of(Descriptor)), " message."))
88+
: Class(Public | Static, Typ.Manual(ctx.Namespace, RootClassName))
89+
.WithXmlDoc(XmlDoc.Summary($"Helper constants with the wire representation for enums within the {ApiDescription}."))
90+
;
91+
92+
using (ctx.InClass(cls))
93+
{
94+
var enumClasses = Enums.OrderBy(d => d.Name, StringComparer.Ordinal).Select(GenerateEnumClass);
95+
var nestedClasses = NestedContainers.Where(nc => nc.ShouldGenerate).OrderBy(nc => nc.Descriptor.Name, StringComparer.Ordinal).Select(nested => nested.Generate(ctx));
96+
cls = cls.AddMembers(enumClasses.ToArray());
97+
cls = cls.AddMembers(nestedClasses.ToArray());
98+
}
99+
return cls;
100+
101+
MemberDeclarationSyntax GenerateEnumClass(EnumDescriptor descriptor)
102+
{
103+
var enumCls = Class(Public | Static, Typ.Nested(ctx.CurrentTyp, descriptor.Name))
104+
.WithXmlDoc(XmlDoc.Summary("Constants for wire representations of the ", GlobalTypeSyntax(ProtoTyp.Of(descriptor)), " enum."));
105+
106+
using (ctx.InClass(enumCls))
107+
{
108+
enumCls = enumCls.AddMembers(descriptor.Values.Select(GenerateEnumConstant).ToArray());
109+
}
110+
return enumCls;
111+
112+
MemberDeclarationSyntax GenerateEnumConstant(EnumValueDescriptor value)
113+
{
114+
string wireValue = value.Name;
115+
string enumValueName = value.CSharpName();
116+
string constantName = enumValueName;
117+
if (NeedsUnderscore(constantName))
118+
{
119+
constantName += "_";
120+
}
121+
var enumValueAccess = GlobalTypeSyntax(ProtoTyp.Of(value.EnumDescriptor)).Access(enumValueName);
122+
return Field(Public | Const, ctx.Type<string>(), constantName)
123+
.WithXmlDoc(XmlDoc.Summary("Wire representation of ", enumValueAccess, "."))
124+
.WithInitializer(wireValue);
125+
}
126+
127+
// Initially only Equals needs "escaping". It's unlikely that we'll get ToString or GetHashCode as field names,
128+
// but there may be other cases. Just add them here as needed.
129+
bool NeedsUnderscore(string name) => name == "Equals";
130+
}
131+
132+
// Due to the nesting, it's simplest to use names of the form global::X.Y.Z to refer to the real
133+
// message and enum types. That's tricky with our existing SourceFileContext code, so this method just builds it.
134+
TypeSyntax GlobalTypeSyntax(Typ typ)
135+
{
136+
var segments = typ.FullName.Replace('+', '.').Split('.');
137+
138+
var name = (NameSyntax) AliasQualifiedName(IdentifierName("global"), IdentifierName(segments.First()));
139+
foreach (var segment in segments.Skip(1))
140+
{
141+
name = QualifiedName(name, IdentifierName(segment));
142+
}
143+
return name;
144+
}
145+
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)