Skip to content

Commit c4f2025

Browse files
committed
feat: Generate properties for nested resource reference fields
Implements #545. Test changes are for the specific tests for this use case, and for Showcase which also contained resource references in nested messages.
1 parent e57b663 commit c4f2025

File tree

6 files changed

+152
-23
lines changed

6 files changed

+152
-23
lines changed

Google.Api.Generator.Tests/ProtoTests/ResourceNames/Testing.ResourceNames/ResourceNamesResourceNames.g.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,5 +1568,45 @@ public SinglePatternName TopRefAsSinglePatternName
15681568
get => string.IsNullOrEmpty(TopRef) ? null : SinglePatternName.Parse(TopRef, allowUnparsed: true);
15691569
set => TopRef = value?.ToString() ?? "";
15701570
}
1571+
1572+
public partial class Types
1573+
{
1574+
public partial class Nested
1575+
{
1576+
/// <summary>
1577+
/// <see cref="SinglePatternName"/>-typed view over the <see cref="NestedRef"/> resource name property.
1578+
/// </summary>
1579+
public SinglePatternName NestedRefAsSinglePatternName
1580+
{
1581+
get => string.IsNullOrEmpty(NestedRef) ? null : SinglePatternName.Parse(NestedRef, allowUnparsed: true);
1582+
set => NestedRef = value?.ToString() ?? "";
1583+
}
1584+
}
1585+
}
1586+
}
1587+
1588+
public partial class DeeplyNestedResourceReference
1589+
{
1590+
public partial class Types
1591+
{
1592+
public partial class Nested
1593+
{
1594+
public partial class Types
1595+
{
1596+
public partial class DeeplyNested
1597+
{
1598+
/// <summary>
1599+
/// <see cref="SinglePatternName"/>-typed view over the <see cref="Ref"/> resource name
1600+
/// property.
1601+
/// </summary>
1602+
public SinglePatternName RefAsSinglePatternName
1603+
{
1604+
get => string.IsNullOrEmpty(Ref) ? null : SinglePatternName.Parse(Ref, allowUnparsed: true);
1605+
set => Ref = value?.ToString() ?? "";
1606+
}
1607+
}
1608+
}
1609+
}
1610+
}
15711611
}
15721612
}

Google.Api.Generator.Tests/ProtoTests/Showcase/Google.Showcase.V1Beta1/MessagingResourceNames.g.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,4 +559,22 @@ public partial class StreamBlurbsRequest
559559
set => Name = value?.ToString() ?? "";
560560
}
561561
}
562+
563+
public partial class ConnectRequest
564+
{
565+
public partial class Types
566+
{
567+
public partial class ConnectConfig
568+
{
569+
/// <summary>
570+
/// <see cref="RoomName"/>-typed view over the <see cref="Parent"/> resource name property.
571+
/// </summary>
572+
public RoomName ParentAsRoomName
573+
{
574+
get => string.IsNullOrEmpty(Parent) ? null : RoomName.Parse(Parent, allowUnparsed: true);
575+
set => Parent = value?.ToString() ?? "";
576+
}
577+
}
578+
}
579+
}
562580
}

Google.Api.Generator.Tests/ProtoTests/Showcase/Google.Showcase.V1Beta1/TestingResourceNames.g.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,21 @@ public partial class Test
741741
get => string.IsNullOrEmpty(Name) ? null : gsv::TestName.Parse(Name, allowUnparsed: true);
742742
set => Name = value?.ToString() ?? "";
743743
}
744+
745+
public partial class Types
746+
{
747+
public partial class Blueprint
748+
{
749+
/// <summary>
750+
/// <see cref="gsv::BlueprintName"/>-typed view over the <see cref="Name"/> resource name property.
751+
/// </summary>
752+
public gsv::BlueprintName BlueprintName
753+
{
754+
get => string.IsNullOrEmpty(Name) ? null : gsv::BlueprintName.Parse(Name, allowUnparsed: true);
755+
set => Name = value?.ToString() ?? "";
756+
}
757+
}
758+
}
744759
}
745760

746761
public partial class ListTestsRequest

Google.Api.Generator/Generation/ResourceNamesGenerator.cs

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -524,23 +524,41 @@ private IEnumerable<ClassDeclarationSyntax> ProtoMessagePartials()
524524
{
525525
// Note: Whether a field is required or optional is purposefully ignored in this partial class.
526526
// The optionalness of a field is only relevant within a method signature (flattening).
527-
foreach (var msg in _fileDesc.MessageTypes)
527+
foreach (var topLevelMessage in _fileDesc.MessageTypes)
528528
{
529-
var resources = msg.Fields.InFieldNumberOrder()
530-
.Select(fieldDesc => (fieldDesc, resDetails: _catalog.GetResourceDetailsByField(fieldDesc)))
531-
.Where(x => x.resDetails != null)
529+
// First find all resource-referencing fields for all messages recursively.
530+
// We then pick only the relevant ones to generate on a per-message basis.
531+
var allResources = topLevelMessage.SelfAndNestedMessagesRecursively()
532+
.SelectMany(m => m.Fields.InFieldNumberOrder())
533+
.Select(_catalog.GetResourceDetailsByField)
534+
.Where(x => x is not null)
535+
.SelectMany(r => r)
532536
.ToList();
533-
if (resources.Any())
537+
538+
var messagesContainingResources = allResources
539+
.Select(r => r.Descriptor.ContainingType)
540+
.SelectMany(m => m.SelfAndAncestors())
541+
.ToHashSet();
542+
543+
if (allResources.Any())
544+
{
545+
yield return GenerateClassWithResourceReferences(topLevelMessage);
546+
}
547+
548+
ClassDeclarationSyntax GenerateClassWithResourceReferences(MessageDescriptor msg)
534549
{
535-
var cls = Class(Public | Partial, ProtoTyp.Of(msg));
550+
var messageOnlyResources = allResources.Where(r => r.Descriptor.ContainingType == msg).ToList();
551+
var msgTyp = ProtoTyp.Of(msg);
552+
var cls = Class(Public | Partial, msgTyp);
536553
using (_ctx.InClass(cls))
537554
{
538-
_ctx.RegisterClassMemberNames(resources
539-
.SelectMany(res => res.resDetails.Select(x => x.ResourcePropertyName))
540-
.Concat(msg.Fields.InDeclarationOrder().Select(f => f.CSharpPropertyName()))
541-
.Append(msg.NestedTypes.Any() ? "Types" : null)
542-
.Where(x => x != null));
543-
var properties = resources.SelectMany(res => res.resDetails).Select(field =>
555+
_ctx.RegisterClassMemberNames(
556+
messageOnlyResources.Select(x => x.ResourcePropertyName)
557+
.Concat(msg.Fields.InDeclarationOrder().Select(f => f.CSharpPropertyName()))
558+
.Append(msg.NestedTypes.Any() ? "Types" : null)
559+
.Where(x => x != null));
560+
// First generate the properties of this message.
561+
var properties = messageOnlyResources.Select(field =>
544562
{
545563
var def = field.ResourceDefinition;
546564
var underlyingProperty = Property(DontCare, _ctx.TypeDontCare, field.UnderlyingPropertyName);
@@ -556,27 +574,40 @@ private IEnumerable<ClassDeclarationSyntax> ProtoMessagePartials()
556574
.Append(Return(_ctx.Type<UnparsedResourceName>().Call(nameof(UnparsedResourceName.Parse))(p)))) :
557575
p => Return(_ctx.Type<string>().Call(nameof(string.IsNullOrEmpty))(p).ConditionalOperator(
558576
Null, _ctx.Type(def.ResourceParserTyp).Call("Parse")(p, def.IsUnparsed ? null : (object) ("allowUnparsed", true))));
559-
if (field.IsRepeated)
577+
if (field.Descriptor.IsRepeated)
560578
{
561579
var s = Parameter(null, "s");
562580
var repeatedTyp = Typ.Generic(typeof(ResourceNameList<>), def.ResourceNameTyp);
563581
return Property(Public, _ctx.Type(repeatedTyp), field.ResourcePropertyName)
564-
.MaybeWithAttribute(field.IsDeprecated, () => _ctx.Type<ObsoleteAttribute>())()
582+
.MaybeWithAttribute(field.Descriptor.IsDeprecated(), () => _ctx.Type<ObsoleteAttribute>())()
565583
.WithGetBody(Return(New(_ctx.Type(repeatedTyp))(underlyingProperty, Lambda(s)(getBodyFn(s)))))
566584
.WithXmlDoc(xmlDocSummary);
567585
}
568586
else
569587
{
570588
return Property(Public, _ctx.Type(def.ResourceNameTyp), field.ResourcePropertyName)
571-
.MaybeWithAttribute(field.IsDeprecated, () => _ctx.Type<ObsoleteAttribute>())()
589+
.MaybeWithAttribute(field.Descriptor.IsDeprecated(), () => _ctx.Type<ObsoleteAttribute>())()
572590
.WithGetBody(getBodyFn(underlyingProperty))
573591
.WithSetBody(underlyingProperty.Assign(Value.Call(nameof(object.ToString), conditional: true)().NullCoalesce("")))
574592
.WithXmlDoc(xmlDocSummary);
575593
}
576594
});
577595
cls = cls.AddMembers(properties.ToArray());
596+
597+
// Now recursively generate any nested types with messages which need properties.
598+
var nestedMessagesWithResources = msg.NestedTypes.Where(messagesContainingResources.Contains).ToList();
599+
if (nestedMessagesWithResources.Any())
600+
{
601+
var types = Class(Public | Partial, Typ.Nested(msgTyp, "Types"));
602+
using var _ = _ctx.InClass(types);
603+
foreach (var nestedMessage in nestedMessagesWithResources)
604+
{
605+
types = types.AddMembers(GenerateClassWithResourceReferences(nestedMessage));
606+
}
607+
cls = cls.AddMembers(types);
608+
}
578609
}
579-
yield return cls;
610+
return cls;
580611
}
581612
}
582613
}

Google.Api.Generator/ProtoUtils/ProtoExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@ internal static string RemoveEnumPrefix(string enumName, string valueName)
7878
return valueName;
7979
}
8080

81+
/// <summary>
82+
/// Returns a sequence of messages covering <paramref name="descriptor"/> and all nested messages within it,
83+
/// recursively.
84+
/// </summary>
85+
internal static IEnumerable<MessageDescriptor> SelfAndNestedMessagesRecursively(this MessageDescriptor descriptor)
86+
{
87+
yield return descriptor;
88+
foreach (var nestedType in descriptor.NestedTypes.SelectMany(SelfAndNestedMessagesRecursively))
89+
{
90+
yield return nestedType;
91+
}
92+
}
93+
94+
/// <summary>
95+
/// Returns a sequence of messages covering <paramref name="descriptor"/> and all its ancestor message descriptors
96+
/// (in terms of containing types).
97+
/// </summary>
98+
internal static IEnumerable<MessageDescriptor> SelfAndAncestors(this MessageDescriptor descriptor)
99+
{
100+
do
101+
{
102+
yield return descriptor;
103+
descriptor = descriptor.ContainingType;
104+
} while (descriptor is not null);
105+
}
106+
81107
public static string CSharpName(this EnumValueDescriptor desc) => RemoveEnumPrefix(desc.EnumDescriptor.Name, desc.Name);
82108

83109
// Convenience methods for accessing extensions, where repeated extensions

Google.Api.Generator/ProtoUtils/ResourceDetails.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,7 @@ public class Field
137137
public Field(FieldDescriptor fieldDesc, Definition resourceDef, IReadOnlyList<Definition> innerDefs = null, bool? containsWildcard = null)
138138
{
139139
// innerFields only non-null for the IResourceName property of child_type refs.
140-
IsRepeated = fieldDesc.IsRepeated;
141-
IsDeprecated = fieldDesc.IsDeprecated();
142-
UnderlyingPropertyName = fieldDesc.CSharpPropertyName();
140+
Descriptor = fieldDesc;
143141
ResourceDefinition = resourceDef;
144142
var requireIdentifier = !((fieldDesc.IsRepeated && fieldDesc.Name.ToLowerInvariant() == "names") ||
145143
(!fieldDesc.IsRepeated && fieldDesc.Name.ToLowerInvariant() == "name"));
@@ -149,12 +147,13 @@ public Field(FieldDescriptor fieldDesc, Definition resourceDef, IReadOnlyList<De
149147
ContainsWildcard = containsWildcard;
150148
}
151149

152-
public bool IsRepeated { get; }
153-
154-
public bool IsDeprecated { get; }
150+
/// <summary>
151+
/// The descriptor for the field which refers to the resource.
152+
/// </summary>
153+
public FieldDescriptor Descriptor { get; }
155154

156155
/// <summary>The C# name of the string-typed property underlying this resource.</summary>
157-
public string UnderlyingPropertyName { get; }
156+
public string UnderlyingPropertyName => Descriptor.CSharpPropertyName();
158157

159158
/// <summary>The C# name of the resource property.</summary>
160159
public string ResourcePropertyName { get; }

0 commit comments

Comments
 (0)