Skip to content

Commit 1b980a9

Browse files
Introduce partial resources for cross RP resource references (#2229)
* add configuration for virtual resources * add a new layer in inheritance hierarchy VirtualResource. Resource now stands for ConcreteResource and inherits from VirtualResource * add a new test project about virtual resources * reverse the inheritance between Resource and VirtualResource * now we could add virtual resource to resource list * implement virtual resources * update test swagger * update to virtual resource * fix broken test cases * add test cases * update test cases * rename virtual resource to partial resource * change the create resource identifier to internal in partial resource * fix * some refinement and documentation * now you can override the name of a partial resource like a normal resource * modify the description of partial resource accordingly * fix test cases * minor fix after merge * regenerate the new test project * regenerate * refactor and regenerate * add a comment to explain the override * refactor * regenerate test projects * fix a few issues
1 parent 1427d1c commit 1b980a9

File tree

65 files changed

+7685
-77
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+7685
-77
lines changed

docs/mgmt/readme.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- [Change Singleton Resources](#change-singleton-resources)
1313
- [Change the Name of Operations](#change-the-name-of-operations)
1414
- [List Exception](#list-exception)
15+
- [Partial Resources](#partial-resources)
1516
- [Scope Resources](#scope-resources)
1617
- [SDK Polishing Configurations](#sdk-polishing-configurations)
1718
- [Management Debug Options](#management-debug-options)
@@ -625,6 +626,42 @@ list-exception:
625626
- /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/availabilitySets/{availabilitySetName}
626627
```
627628

629+
### Partial resources
630+
631+
Some operations might be operations of a resource from another RP, or some resources might have a parent resource from another RP.
632+
633+
For instance, in the RP `compute`, we have a resource `VirtualMachine` with the path of `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}`, in the meantime, in the RP `network`, which is completely a different SDK, we have an operation with the path of `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}/publicIps` which is clearly listing something on the virtual machine. During the generation of network RP, the generator has no knowledge of the resources in another RP, therefore by default this operation will be generated like the following code as an operation on the `ResourceGroupResource` because this is the best parent the generator could find within this RP.
634+
```csharp
635+
public static partial class NetworkExtensions
636+
{
637+
public static Pageable<PublicIPAddress> GetPublicIPs(this ResourceGroupResource resourceGroup, string vmName)
638+
{
639+
/* the implementation */
640+
}
641+
}
642+
```
643+
644+
The following configuration
645+
```yaml
646+
partial-resources:
647+
/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}: VirtualMachine
648+
```
649+
will tell the generator that this path `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}` is also a valid resource, and the generator will put it into the list of its known resources, and generates a "partial resource" from it and the operation in above example will show up here:
650+
```csharp
651+
public partial class VirtualMachineNetworkResource
652+
{
653+
public virtual Pageable<PublicIPAddress> GetPublicIPs()
654+
{
655+
/* the implementation */
656+
}
657+
}
658+
```
659+
660+
The naming pattern of a partial resource is like `[ResourceName][RPName]Resource`, where `ResourceName` is the value you assigned in the configuration, `RPName` is the name of this RP (for instance, `Network`). The name of a partial resource is also configurable using the `request-path-to-resource-name` configuration, please see [change resource name](#change-resource-name) section for more details.
661+
662+
The difference between a normal resource and a partial resource is that the partial resource is not a concrete resource, therefore it does not have a corresponding collection, and you cannot get the list of it from its direct parent (ResourceGroupResource in the above example).
663+
But like normal resources, partial resources will have an extension method of `Get[PartialResourceName]` on the `ArmClient` to let you get it from its full Id.
664+
628665
### Scope resources
629666

630667
Some resources are scope resources, which means that these resources can be created under different scopes, like subscriptions, resource groups, management groups, etc. In the swagger, we currently do not have an extension which assigns which type of resources can be the scope of this resource, therefore we add a configuration in our generator `request-path-to-scope-resource-types` for this new information.

src/AutoRest.CSharp/Mgmt/AutoRest/MgmtConfiguration.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ public MgmtConfiguration(
120120
JsonElement? singletonRequiresKeyword = default,
121121
JsonElement? operationIdMappings = default,
122122
JsonElement? updateRequiredCopy = default,
123-
JsonElement? patchInitializerCustomization = default)
123+
JsonElement? patchInitializerCustomization = default,
124+
JsonElement? partialResources = default)
124125
{
125126
RequestPathToParent = DeserializeDictionary<string, string>(requestPathToParent);
126127
RequestPathToResourceName = DeserializeDictionary<string, string>(requestPathToResourceName);
@@ -135,6 +136,7 @@ public MgmtConfiguration(
135136
RenameMapping = DeserializeDictionary<string, string>(renameMapping);
136137
ParameterRenameMapping = DeserializeDictionary<string, IReadOnlyDictionary<string, string>>(parameterRenameMapping);
137138
IrregularPluralWords = DeserializeDictionary<string, string>(irregularPluralWords);
139+
PartialResources = DeserializeDictionary<string, string>(partialResources);
138140
try
139141
{
140142
OperationPositions = DeserializeDictionary<string, string[]>(operationPositions);
@@ -209,6 +211,7 @@ private static Dictionary<TKey, TValue> DeserializeDictionary<TKey, TValue>(Json
209211
public IReadOnlyDictionary<string, string[]> RequestPathToScopeResourceTypes { get; }
210212
public IReadOnlyDictionary<string, string[]> OperationPositions { get; }
211213
public IReadOnlyDictionary<string, string[]> MergeOperations { get; }
214+
public IReadOnlyDictionary<string, string> PartialResources { get; }
212215
public IReadOnlyList<string> RawParameterizedScopes { get; }
213216
private ImmutableHashSet<RequestPath>? _parameterizedScopes;
214217
internal ImmutableHashSet<RequestPath> ParameterizedScopes
@@ -268,7 +271,8 @@ internal static MgmtConfiguration GetConfiguration(IPluginCommunication autoRest
268271
singletonRequiresKeyword: autoRest.GetValue<JsonElement?>("singleton-resource-requires-keyword").GetAwaiter().GetResult(),
269272
operationIdMappings: autoRest.GetValue<JsonElement?>("operation-id-mappings").GetAwaiter().GetResult(),
270273
updateRequiredCopy: autoRest.GetValue<JsonElement?>("update-required-copy").GetAwaiter().GetResult(),
271-
patchInitializerCustomization: autoRest.GetValue<JsonElement?>("patch-initializer-customization").GetAwaiter().GetResult());
274+
patchInitializerCustomization: autoRest.GetValue<JsonElement?>("patch-initializer-customization").GetAwaiter().GetResult(),
275+
partialResources: autoRest.GetValue<JsonElement?>("partial-resources").GetAwaiter().GetResult());
272276
}
273277

274278
internal void SaveConfiguration(Utf8JsonWriter writer)
@@ -297,6 +301,7 @@ internal void SaveConfiguration(Utf8JsonWriter writer)
297301
WriteNonEmptySettings(writer, nameof(ParameterRenameMapping), ParameterRenameMapping);
298302
WriteNonEmptySettings(writer, nameof(IrregularPluralWords), IrregularPluralWords);
299303
WriteNonEmptySettings(writer, nameof(OverrideOperationName), OverrideOperationName);
304+
WriteNonEmptySettings(writer, nameof(PartialResources), PartialResources);
300305
MgmtDebug.Write(writer, nameof(MgmtDebug));
301306
if (IsArmCore)
302307
writer.WriteBoolean("ArmCore", IsArmCore);
@@ -340,6 +345,7 @@ internal static MgmtConfiguration LoadConfiguration(JsonElement root)
340345
root.TryGetProperty(nameof(OverrideOperationName), out var operationIdToName);
341346
root.TryGetProperty(nameof(MergeOperations), out var mergeOperations);
342347
root.TryGetProperty(nameof(PromptedEnumValues), out var promptedEnumValuesElement);
348+
root.TryGetProperty(nameof(PartialResources), out var virtualResources);
343349
root.TryGetProperty(nameof(RawParameterizedScopes), out var parameterizedScopesElement);
344350

345351
var operationGroupToOmit = Configuration.DeserializeArray(operationGroupsToOmitElement);
@@ -399,7 +405,8 @@ internal static MgmtConfiguration LoadConfiguration(JsonElement root)
399405
singletonRequiresKeyword: singletonRequiresKeyword,
400406
operationIdMappings: operationIdMappings,
401407
updateRequiredCopy: updateRequiredCopy,
402-
patchInitializerCustomization: patchInitializerCustomization);
408+
patchInitializerCustomization: patchInitializerCustomization,
409+
partialResources: virtualResources);
403410
}
404411

405412
private static void WriteNonEmptySettings(

src/AutoRest.CSharp/Mgmt/AutoRest/MgmtContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Linq;
56
using AutoRest.CSharp.Input;
67
using AutoRest.CSharp.Output.Models.Types;
78

@@ -16,6 +17,10 @@ internal static class MgmtContext
1617

1718
public static CodeModel CodeModel => Context.CodeModel;
1819

20+
public static string DefaultNamespace => Context.DefaultNamespace;
21+
22+
public static string RPName => DefaultNamespace.Split('.').Last();
23+
1924
public static bool IsInitialized => _context is not null;
2025

2126
public static void Initialize(BuildContext<MgmtOutputLibrary> context)

src/AutoRest.CSharp/Mgmt/AutoRest/MgmtOutputLibrary.cs

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics;
76
using System.Diagnostics.CodeAnalysis;
87
using System.Linq;
98
using AutoRest.CSharp.Common.Input;
@@ -23,6 +22,7 @@
2322
using Azure.ResourceManager.ManagementGroups;
2423
using Azure.ResourceManager.Resources;
2524
using Humanizer.Inflections;
25+
using static AutoRest.CSharp.Mgmt.Decorator.Transformer.PartialResourceResolver;
2626

2727
namespace AutoRest.CSharp.Mgmt.AutoRest
2828
{
@@ -39,7 +39,7 @@ internal class MgmtOutputLibrary : OutputLibrary
3939
/// This is a map from raw request path to their corresponding <see cref="OperationSet"/>,
4040
/// which is a collection of the operations with the same raw request path
4141
/// </summary>
42-
private Dictionary<string, OperationSet> RawRequestPathToOperationSets { get; }
42+
internal Dictionary<string, OperationSet> RawRequestPathToOperationSets { get; }
4343

4444
/// <summary>
4545
/// This is a map from operation to its corresponding operation group
@@ -200,7 +200,7 @@ private IEnumerable<Schema> UpdateBodyParameters()
200200
// get the request path and operation set
201201
RequestPath requestPath = RequestPath.FromOperation(operation, operationGroup);
202202
var operationSet = RawRequestPathToOperationSets[requestPath];
203-
if (operationSet.TryGetResourceDataSchema(out var resourceDataModel))
203+
if (operationSet.TryGetResourceDataSchema(out var resourceDataSchema))
204204
{
205205
// if this is a resource, we need to make sure its body parameter is required when the verb is put or patch
206206
BodyParameterNormalizer.MakeRequired(bodyParam, httpRequest.Method);
@@ -212,14 +212,14 @@ private IEnumerable<Schema> UpdateBodyParameters()
212212
BodyParameterNormalizer.UpdateParameterNameOnly(bodyParam, ResourceDataSchemaNameToOperationSets);
213213
continue;
214214
}
215-
if (resourceDataModel is not null)
215+
if (resourceDataSchema is not null)
216216
{
217217
//TODO handle expandable request paths. We assume that this is fine since if all of the expanded
218218
//types use the same model they should have a common name, but since this case doesn't exist yet
219219
//we don't know for sure
220220
if (requestPath.IsExpandable)
221221
throw new InvalidOperationException($"Found expandable path in UpdatePatchParameterNames for {operationGroup.Key}.{operation.CSharpName()} : {requestPath}");
222-
var name = GetResourceName(resourceDataModel.Name, operationSet, requestPath);
222+
var name = GetResourceName(resourceDataSchema.Name, operationSet, requestPath);
223223
updatedModels.Add(bodyParam.Schema);
224224
BodyParameterNormalizer.Update(httpRequest.Method, operation.CSharpName(), bodyParam, name);
225225
}
@@ -564,30 +564,45 @@ private Dictionary<RequestPath, ResourceObjectAssociation> EnsureRequestPathToRe
564564

565565
foreach ((var resourceDataSchemaName, var operationSets) in ResourceDataSchemaNameToOperationSets)
566566
{
567-
var resourceOperationsList = FindResourceToChildOperationsMap(operationSets);
568-
foreach ((var operationSet, var operations) in resourceOperationsList)
567+
foreach (var operationSet in operationSets)
569568
{
570-
var isSingleton = operationSet.IsSingletonResource();
569+
var operations = GetChildOperations(operationSet.RequestPath);
571570
// get the corresponding resource data
572571
var originalResourcePath = operationSet.GetRequestPath();
573572
var resourceData = GetResourceData(originalResourcePath);
574-
// we calculate the resource type of the resource
575-
var resourcePaths = originalResourcePath.Expand();
576-
foreach (var resourcePath in resourcePaths)
577-
{
578-
var resourceType = resourcePath.GetResourceType();
579-
var resource = new Resource(operationSet, operations, GetResourceName(resourceDataSchemaName, operationSet, resourcePath), resourceType, resourceData);
580-
var collection = isSingleton ? null : new ResourceCollection(operationSet, operations, resource);
581-
resource.ResourceCollection = collection;
582-
583-
requestPathToResources.Add(resourcePath, new ResourceObjectAssociation(resourceType, resourceData, resource, collection));
584-
}
573+
if (resourceData is EmptyResourceData emptyResourceData)
574+
BuildPartialResource(requestPathToResources, resourceDataSchemaName, operationSet, operations, originalResourcePath, emptyResourceData);
575+
else
576+
BuildResource(requestPathToResources, resourceDataSchemaName, operationSet, operations, originalResourcePath, resourceData);
585577
}
586578
}
587579

588580
return requestPathToResources;
589581
}
590582

583+
private void BuildResource(Dictionary<RequestPath, ResourceObjectAssociation> result, string resourceDataSchemaName, OperationSet operationSet, IEnumerable<Operation> operations, RequestPath originalResourcePath, ResourceData resourceData)
584+
{
585+
var isSingleton = operationSet.IsSingletonResource();
586+
// we calculate the resource type of the resource
587+
var resourcePaths = originalResourcePath.Expand();
588+
foreach (var resourcePath in resourcePaths)
589+
{
590+
var resourceType = resourcePath.GetResourceType();
591+
var resource = new Resource(operationSet, operations, GetResourceName(resourceDataSchemaName, operationSet, resourcePath), resourceType, resourceData);
592+
var collection = isSingleton ? null : new ResourceCollection(operationSet, operations, resource);
593+
resource.ResourceCollection = collection;
594+
595+
result.Add(resourcePath, new ResourceObjectAssociation(resourceType, resourceData, resource, collection));
596+
}
597+
}
598+
599+
private void BuildPartialResource(Dictionary<RequestPath, ResourceObjectAssociation> result, string resourceDataSchemaName, OperationSet operationSet, IEnumerable<Operation> operations, RequestPath originalResourcePath, EmptyResourceData emptyResourceData)
600+
{
601+
var resourceType = originalResourcePath.GetResourceType();
602+
var resource = new PartialResource(operationSet, operations, GetResourceName(resourceDataSchemaName, operationSet, originalResourcePath, isPartial: true), resourceDataSchemaName, resourceType, emptyResourceData);
603+
result.Add(originalResourcePath, new ResourceObjectAssociation(originalResourcePath.GetResourceType(), emptyResourceData, resource, null));
604+
}
605+
591606
private string? GetDefaultNameFromConfiguration(OperationSet operationSet, ResourceTypeSegment resourceType)
592607
{
593608
if (Configuration.MgmtConfiguration.RequestPathToResourceName.TryGetValue(operationSet.RequestPath, out var name))
@@ -598,14 +613,23 @@ private Dictionary<RequestPath, ResourceObjectAssociation> EnsureRequestPathToRe
598613
return null;
599614
}
600615

601-
private string GetResourceName(string candidateName, OperationSet operationSet, RequestPath requestPath)
616+
private string GetResourceName(string candidateName, OperationSet operationSet, RequestPath requestPath, bool isPartial = false)
602617
{
603618
// read configuration to see if we could get a configuration for this resource
604619
var resourceType = requestPath.GetResourceType();
605620
var defaultNameFromConfig = GetDefaultNameFromConfiguration(operationSet, resourceType);
606621
if (defaultNameFromConfig != null)
607622
return defaultNameFromConfig;
608623

624+
var resourceName = CalculateResourceName(candidateName, operationSet, requestPath, resourceType);
625+
626+
return isPartial ?
627+
$"{resourceName}{MgmtContext.RPName}" :
628+
resourceName;
629+
}
630+
631+
private string CalculateResourceName(string candidateName, OperationSet operationSet, RequestPath requestPath, ResourceTypeSegment resourceType)
632+
{
609633
// find all the expanded request paths of resources that are assiociated with the same resource data model
610634
var resourcesWithSameResourceData = ResourceDataSchemaNameToOperationSets[candidateName]
611635
.SelectMany(opSet => opSet.GetRequestPath().Expand()).ToList();
@@ -716,19 +740,12 @@ public int GetHashCode([DisallowNull] IEnumerable<RequestPath> obj)
716740
}
717741
}
718742

719-
private Dictionary<OperationSet, IEnumerable<Operation>> FindResourceToChildOperationsMap(IEnumerable<OperationSet> resourceOperationSets)
720-
{
721-
return resourceOperationSets.ToDictionary(
722-
operationSet => operationSet,
723-
operationSet => GetChildOperations(operationSet.RequestPath));
724-
}
725-
726743
public IEnumerable<Operation> GetChildOperations(string requestPath)
727744
{
728745
if (requestPath == RequestPath.Any)
729746
return Enumerable.Empty<Operation>();
730747

731-
if (EnsureResourceChildOperations().TryGetValue(requestPath, out var operations))
748+
if (ChildOperations.TryGetValue(requestPath, out var operations))
732749
return operations;
733750

734751
return Enumerable.Empty<Operation>();
@@ -828,6 +845,7 @@ public IEnumerable<Resource> FindResources(ResourceData resourceData)
828845

829846
private TypeProvider BuildResourceData(Schema schema) => schema switch
830847
{
848+
EmptyObjectSchema emptyObjectSchema => new EmptyResourceData(emptyObjectSchema),
831849
ObjectSchema objectSchema => new ResourceData(objectSchema),
832850
_ => throw new NotImplementedException()
833851
};
@@ -857,6 +875,7 @@ private Dictionary<string, HashSet<OperationSet>> DecorateOperationSets()
857875
result.Add(operationSet);
858876
}
859877
}
878+
860879
return resourceDataSchemaNameToOperationSets;
861880
}
862881

@@ -885,6 +904,13 @@ private Dictionary<string, OperationSet> CategorizeOperationGroups()
885904
}
886905
}
887906
}
907+
908+
// add operation set for the partial resources here
909+
foreach (var path in Configuration.MgmtConfiguration.PartialResources.Keys)
910+
{
911+
rawRequestPathToOperationSets.Add(path, new OperationSet(path));
912+
}
913+
888914
return rawRequestPathToOperationSets;
889915
}
890916

0 commit comments

Comments
 (0)