Skip to content

Commit

Permalink
feat: Generate properties for nested resource reference fields
Browse files Browse the repository at this point in the history
Implements googleapis#545.

Test changes are for the specific tests for this use case, and for
Showcase which also contained resource references in nested messages.
  • Loading branch information
jskeet committed May 16, 2024
1 parent e57b663 commit c4f2025
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1568,5 +1568,45 @@ public SinglePatternName TopRefAsSinglePatternName
get => string.IsNullOrEmpty(TopRef) ? null : SinglePatternName.Parse(TopRef, allowUnparsed: true);
set => TopRef = value?.ToString() ?? "";
}

public partial class Types
{
public partial class Nested
{
/// <summary>
/// <see cref="SinglePatternName"/>-typed view over the <see cref="NestedRef"/> resource name property.
/// </summary>
public SinglePatternName NestedRefAsSinglePatternName
{
get => string.IsNullOrEmpty(NestedRef) ? null : SinglePatternName.Parse(NestedRef, allowUnparsed: true);
set => NestedRef = value?.ToString() ?? "";
}
}
}
}

public partial class DeeplyNestedResourceReference
{
public partial class Types
{
public partial class Nested
{
public partial class Types
{
public partial class DeeplyNested
{
/// <summary>
/// <see cref="SinglePatternName"/>-typed view over the <see cref="Ref"/> resource name
/// property.
/// </summary>
public SinglePatternName RefAsSinglePatternName
{
get => string.IsNullOrEmpty(Ref) ? null : SinglePatternName.Parse(Ref, allowUnparsed: true);
set => Ref = value?.ToString() ?? "";
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -559,4 +559,22 @@ public partial class StreamBlurbsRequest
set => Name = value?.ToString() ?? "";
}
}

public partial class ConnectRequest
{
public partial class Types
{
public partial class ConnectConfig
{
/// <summary>
/// <see cref="RoomName"/>-typed view over the <see cref="Parent"/> resource name property.
/// </summary>
public RoomName ParentAsRoomName
{
get => string.IsNullOrEmpty(Parent) ? null : RoomName.Parse(Parent, allowUnparsed: true);
set => Parent = value?.ToString() ?? "";
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,21 @@ public partial class Test
get => string.IsNullOrEmpty(Name) ? null : gsv::TestName.Parse(Name, allowUnparsed: true);
set => Name = value?.ToString() ?? "";
}

public partial class Types
{
public partial class Blueprint
{
/// <summary>
/// <see cref="gsv::BlueprintName"/>-typed view over the <see cref="Name"/> resource name property.
/// </summary>
public gsv::BlueprintName BlueprintName
{
get => string.IsNullOrEmpty(Name) ? null : gsv::BlueprintName.Parse(Name, allowUnparsed: true);
set => Name = value?.ToString() ?? "";
}
}
}
}

public partial class ListTestsRequest
Expand Down
63 changes: 47 additions & 16 deletions Google.Api.Generator/Generation/ResourceNamesGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -524,23 +524,41 @@ private IEnumerable<ClassDeclarationSyntax> ProtoMessagePartials()
{
// Note: Whether a field is required or optional is purposefully ignored in this partial class.
// The optionalness of a field is only relevant within a method signature (flattening).
foreach (var msg in _fileDesc.MessageTypes)
foreach (var topLevelMessage in _fileDesc.MessageTypes)
{
var resources = msg.Fields.InFieldNumberOrder()
.Select(fieldDesc => (fieldDesc, resDetails: _catalog.GetResourceDetailsByField(fieldDesc)))
.Where(x => x.resDetails != null)
// First find all resource-referencing fields for all messages recursively.
// We then pick only the relevant ones to generate on a per-message basis.
var allResources = topLevelMessage.SelfAndNestedMessagesRecursively()
.SelectMany(m => m.Fields.InFieldNumberOrder())
.Select(_catalog.GetResourceDetailsByField)
.Where(x => x is not null)
.SelectMany(r => r)
.ToList();
if (resources.Any())

var messagesContainingResources = allResources
.Select(r => r.Descriptor.ContainingType)
.SelectMany(m => m.SelfAndAncestors())
.ToHashSet();

if (allResources.Any())
{
yield return GenerateClassWithResourceReferences(topLevelMessage);
}

ClassDeclarationSyntax GenerateClassWithResourceReferences(MessageDescriptor msg)
{
var cls = Class(Public | Partial, ProtoTyp.Of(msg));
var messageOnlyResources = allResources.Where(r => r.Descriptor.ContainingType == msg).ToList();
var msgTyp = ProtoTyp.Of(msg);
var cls = Class(Public | Partial, msgTyp);
using (_ctx.InClass(cls))
{
_ctx.RegisterClassMemberNames(resources
.SelectMany(res => res.resDetails.Select(x => x.ResourcePropertyName))
.Concat(msg.Fields.InDeclarationOrder().Select(f => f.CSharpPropertyName()))
.Append(msg.NestedTypes.Any() ? "Types" : null)
.Where(x => x != null));
var properties = resources.SelectMany(res => res.resDetails).Select(field =>
_ctx.RegisterClassMemberNames(
messageOnlyResources.Select(x => x.ResourcePropertyName)
.Concat(msg.Fields.InDeclarationOrder().Select(f => f.CSharpPropertyName()))
.Append(msg.NestedTypes.Any() ? "Types" : null)
.Where(x => x != null));
// First generate the properties of this message.
var properties = messageOnlyResources.Select(field =>
{
var def = field.ResourceDefinition;
var underlyingProperty = Property(DontCare, _ctx.TypeDontCare, field.UnderlyingPropertyName);
Expand All @@ -556,27 +574,40 @@ private IEnumerable<ClassDeclarationSyntax> ProtoMessagePartials()
.Append(Return(_ctx.Type<UnparsedResourceName>().Call(nameof(UnparsedResourceName.Parse))(p)))) :
p => Return(_ctx.Type<string>().Call(nameof(string.IsNullOrEmpty))(p).ConditionalOperator(
Null, _ctx.Type(def.ResourceParserTyp).Call("Parse")(p, def.IsUnparsed ? null : (object) ("allowUnparsed", true))));
if (field.IsRepeated)
if (field.Descriptor.IsRepeated)
{
var s = Parameter(null, "s");
var repeatedTyp = Typ.Generic(typeof(ResourceNameList<>), def.ResourceNameTyp);
return Property(Public, _ctx.Type(repeatedTyp), field.ResourcePropertyName)
.MaybeWithAttribute(field.IsDeprecated, () => _ctx.Type<ObsoleteAttribute>())()
.MaybeWithAttribute(field.Descriptor.IsDeprecated(), () => _ctx.Type<ObsoleteAttribute>())()
.WithGetBody(Return(New(_ctx.Type(repeatedTyp))(underlyingProperty, Lambda(s)(getBodyFn(s)))))
.WithXmlDoc(xmlDocSummary);
}
else
{
return Property(Public, _ctx.Type(def.ResourceNameTyp), field.ResourcePropertyName)
.MaybeWithAttribute(field.IsDeprecated, () => _ctx.Type<ObsoleteAttribute>())()
.MaybeWithAttribute(field.Descriptor.IsDeprecated(), () => _ctx.Type<ObsoleteAttribute>())()
.WithGetBody(getBodyFn(underlyingProperty))
.WithSetBody(underlyingProperty.Assign(Value.Call(nameof(object.ToString), conditional: true)().NullCoalesce("")))
.WithXmlDoc(xmlDocSummary);
}
});
cls = cls.AddMembers(properties.ToArray());

// Now recursively generate any nested types with messages which need properties.
var nestedMessagesWithResources = msg.NestedTypes.Where(messagesContainingResources.Contains).ToList();
if (nestedMessagesWithResources.Any())
{
var types = Class(Public | Partial, Typ.Nested(msgTyp, "Types"));
using var _ = _ctx.InClass(types);
foreach (var nestedMessage in nestedMessagesWithResources)
{
types = types.AddMembers(GenerateClassWithResourceReferences(nestedMessage));
}
cls = cls.AddMembers(types);
}
}
yield return cls;
return cls;
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions Google.Api.Generator/ProtoUtils/ProtoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ internal static string RemoveEnumPrefix(string enumName, string valueName)
return valueName;
}

/// <summary>
/// Returns a sequence of messages covering <paramref name="descriptor"/> and all nested messages within it,
/// recursively.
/// </summary>
internal static IEnumerable<MessageDescriptor> SelfAndNestedMessagesRecursively(this MessageDescriptor descriptor)
{
yield return descriptor;
foreach (var nestedType in descriptor.NestedTypes.SelectMany(SelfAndNestedMessagesRecursively))
{
yield return nestedType;
}
}

/// <summary>
/// Returns a sequence of messages covering <paramref name="descriptor"/> and all its ancestor message descriptors
/// (in terms of containing types).
/// </summary>
internal static IEnumerable<MessageDescriptor> SelfAndAncestors(this MessageDescriptor descriptor)
{
do
{
yield return descriptor;
descriptor = descriptor.ContainingType;
} while (descriptor is not null);
}

public static string CSharpName(this EnumValueDescriptor desc) => RemoveEnumPrefix(desc.EnumDescriptor.Name, desc.Name);

// Convenience methods for accessing extensions, where repeated extensions
Expand Down
13 changes: 6 additions & 7 deletions Google.Api.Generator/ProtoUtils/ResourceDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,7 @@ public class Field
public Field(FieldDescriptor fieldDesc, Definition resourceDef, IReadOnlyList<Definition> innerDefs = null, bool? containsWildcard = null)
{
// innerFields only non-null for the IResourceName property of child_type refs.
IsRepeated = fieldDesc.IsRepeated;
IsDeprecated = fieldDesc.IsDeprecated();
UnderlyingPropertyName = fieldDesc.CSharpPropertyName();
Descriptor = fieldDesc;
ResourceDefinition = resourceDef;
var requireIdentifier = !((fieldDesc.IsRepeated && fieldDesc.Name.ToLowerInvariant() == "names") ||
(!fieldDesc.IsRepeated && fieldDesc.Name.ToLowerInvariant() == "name"));
Expand All @@ -149,12 +147,13 @@ public Field(FieldDescriptor fieldDesc, Definition resourceDef, IReadOnlyList<De
ContainsWildcard = containsWildcard;
}

public bool IsRepeated { get; }

public bool IsDeprecated { get; }
/// <summary>
/// The descriptor for the field which refers to the resource.
/// </summary>
public FieldDescriptor Descriptor { get; }

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

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

0 comments on commit c4f2025

Please sign in to comment.