Skip to content
Merged
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
33 changes: 15 additions & 18 deletions eng/packages/http-client-csharp-mgmt/docs/resource-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,24 +303,21 @@ For each remaining operation `O`:
2. If `O`'s response is not a collection of some item model `T`,
skip — it cannot be a `List`.
3. Otherwise, look for a resource `R` in the **detected resource set**
such that `R.model == T` and `O.path` identifies a collection of
`R`. A path identifies a collection of `R` when either:
- `O.path` is exactly `R.instancePath` with the trailing name segment
removed. This is the normal list-by-parent/list-by-resource-group
shape. Singleton resources are handled the same way: the collection
path is the singleton instance path with its constant singleton name
removed.
- `O.path` has the same ARM resource type as `R` and ends on that
resource type. This covers scope-level list operations whose path
intentionally omits part of the instance path, such as a
subscription-level list of resource-group-scoped resources.

Exact collection-path matches take precedence over scope-level
resource-type matches. If multiple resources still match, attach `O`
to the resource with the **shortest** matching `instancePath` as
`R.List`. (The shortest match is the closest containing resource
whose model is `T` — i.e. `R` itself, not some deeper resource that
happens to also be of model `T`.)
such that `R.model == T` and `O.path` is a prefix of
`R.instancePath`. This covers normal list-by-parent/list-by-resource-group
operations, singleton resources, and tuple-resource list operations at
intermediate collection paths.

If no prefix match is found, a scope-level list can still match when
`O.path` has the same ARM resource type as `R`, has compatible scope
nesting, and ends on that resource type. This covers scope-level list
operations whose path intentionally omits part of the instance path, such as
a subscription-level list of resource-group-scoped resources.

If multiple resources match, attach `O` to the resource with the
**shortest** matching `instancePath` as `R.List`. (The shortest match is
the closest containing resource whose model is `T` — i.e. `R` itself,
not some deeper resource that happens to also be of model `T`.)
4. If no such `R` exists, `O` falls through to Pass 2.

#### Pass 2 — Everything else is an `Action`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,12 +555,11 @@ function findListTargetResource(
(resource) => resource.resourceModelId === itemModelId
);

const exactCollectionMatches = candidates.filter(
(resource) =>
resource.metadata.resourceIdPattern.trimLastSegment?.equals(operationPath)
const collectionMatches = candidates.filter((resource) =>
operationPath.isPrefixOf(resource.metadata.resourceIdPattern)
);
if (exactCollectionMatches.length > 0) {
return shortestResourcePath(exactCollectionMatches);
if (collectionMatches.length > 0) {
return shortestResourcePath(collectionMatches);
}

const operationType = operationPath.resourceType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2509,6 +2509,105 @@ interface ProfileRevisions {
);
});

it("tuple-resource intermediate collection path is assigned as list", async () => {
const program = await typeSpecCompile(
`
/** Virtual machine extension image resource */
model VirtualMachineExtensionImage is TrackedResource<VirtualMachineExtensionImageProperties> {
...ResourceNameParameter<
Resource = VirtualMachineExtensionImage,
KeyName = "type",
SegmentName = "types",
NamePattern = ""
>;
}

/** Virtual machine extension image properties */
model VirtualMachineExtensionImageProperties {
/** Display name */
displayName?: string;
}

alias ImageTypePath = {
...ApiVersionParameter;
...SubscriptionIdParameter;
...LocationResourceParameter;
/** Publisher name */
@path
@segment("publishers")
publisherName: string;
};

#suppress "@azure-tools/typespec-azure-resource-manager/arm-resource-interface-requires-decorator" "Testing static tuple resource paths"
interface VirtualMachineExtensionImages {
@get
@route("/subscriptions/{subscriptionId}/providers/Microsoft.Compute/locations/{location}/publishers/{publisherName}/artifacttypes/vmextension/types/{type}/versions/{version}")
get(
...ImageTypePath,
...KeysOf<VirtualMachineExtensionImage>,
/** Version name */
@path
@segment("versions")
version: string
): ArmResponse<VirtualMachineExtensionImage> | ErrorResponse;

@get
@route("/subscriptions/{subscriptionId}/providers/Microsoft.Compute/locations/{location}/publishers/{publisherName}/artifacttypes/vmextension/types")
listTypes(...ImageTypePath): ArmResponse<VirtualMachineExtensionImage[]> | ErrorResponse;

@get
@route("/subscriptions/{subscriptionId}/providers/Microsoft.Compute/locations/{location}/publishers/{publisherName}/artifacttypes/vmextension/types/{type}/versions")
listVersions(
...ImageTypePath,
...KeysOf<VirtualMachineExtensionImage>
): ArmResponse<VirtualMachineExtensionImage[]> | ErrorResponse;
}
`,
runner,
{ providerNamespace: "Microsoft.Compute" }
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);

const armProviderSchema = buildArmProviderSchema(sdkContext, root);
ok(armProviderSchema);

const imageModel = root.models.find(
(m) => m.name === "VirtualMachineExtensionImage"
);
ok(imageModel, "VirtualMachineExtensionImage model should exist");

const imageResource = armProviderSchema.resources.find(
(r) =>
r.resourceModelId === imageModel.crossLanguageDefinitionId &&
r.metadata.resourceIdPattern.path.endsWith(
"/types/{type}/versions/{version}"
)
);
ok(
imageResource,
"VirtualMachineExtensionImage resource should be detected"
);

const listPaths = imageResource.metadata.methods
.filter((m) => m.kind === "List")
.map((m) => m.operationPath.path)
.sort();

deepStrictEqual(listPaths, [
"/subscriptions/{subscriptionId}/providers/Microsoft.Compute/locations/{location}/publishers/{publisherName}/artifacttypes/vmextension/types",
"/subscriptions/{subscriptionId}/providers/Microsoft.Compute/locations/{location}/publishers/{publisherName}/artifacttypes/vmextension/types/{type}/versions"
]);
strictEqual(
armProviderSchema.nonResourceMethods.some((m) =>
m.operationPath.path.includes("/artifacttypes/vmextension/types")
),
false,
"Tuple-resource list operations should not remain non-resource methods"
);
});

it("custom Azure resource with @customAzureResource decorator (TrafficManager pattern)", async () => {
const program = await typeSpecCompile(
`
Expand Down Expand Up @@ -2658,8 +2757,8 @@ interface TrafficEndpoints {
);
strictEqual(
trafficProfileResource.metadata.methods.length,
3,
"TrafficProfile should have 3 methods present in the code model (get, createOrUpdate, delete)"
4,
"TrafficProfile should have 4 methods present in the code model (get, createOrUpdate, delete, list)"
);

// Find the TrafficEndpoint resource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal sealed class ResourceCollectionClientProvider : TypeProvider
private readonly IReadOnlyList<ParameterProvider> _extraCtorParameters;
private readonly IReadOnlyList<FieldProvider> _extraFields;
private readonly ResourceClientProvider _resource;
private readonly ResourceMethod? _getAll;
private readonly IReadOnlyList<ResourceMethod> _getAlls;
private readonly ResourceMethod? _create;
private readonly ResourceMethod? _get;

Expand All @@ -60,35 +60,33 @@ internal ResourceCollectionClientProvider(ResourceClientProvider resource, Input

_resourceTypeExpression = Static(_resource.Type).As<ArmResource>().ResourceType();

InitializeMethods(resourceMethods, ref _get, ref _create, ref _getAll);
_operationContext = InitializeContext(this, resourceMetadata, _getAll);
var contextualPath = GetContextualPath(resourceMetadata);
(_get, _create, _getAlls) = InitializeMethods(resourceMethods, contextualPath);
_operationContext = InitializeContext(this, contextualPath, _getAlls.Count > 0 ? _getAlls[0] : null);

// this depends on _getAll being initialized
// this depends on _getAlls being initialized
(_extraCtorParameters, _extraFields) = BuildExtraConstructorParametersAndFields();
}

private static OperationContext InitializeContext(ResourceCollectionClientProvider enclosingType, ArmResourceMetadata resourceMetadata, ResourceMethod? getAll)
private static OperationContext InitializeContext(ResourceCollectionClientProvider enclosingType, RequestPathPattern contextualPath, ResourceMethod? canonicalGetAll)
{
var contextualPath = GetContextualPath(resourceMetadata);
if (getAll is not null)
if (canonicalGetAll is null)
{
var secondaryContextualPath = getAll.OperationPath;
// validate the contextualPath should be an ancestor of the secondaryContextualPath, otherwise report diagnostic.
if (!contextualPath.IsAncestorOf(secondaryContextualPath))
{
// Report diagnostic
ManagementClientGenerator.Instance.Emitter.ReportDiagnostic(
code: "malformed-resource-detected",
message: $"The contextual path '{contextualPath}' is not an ancestor of the secondary contextual path '{secondaryContextualPath}'.",
targetCrossLanguageDefinitionId: getAll.InputMethod.CrossLanguageDefinitionId
);
}
return OperationContext.Create(contextualPath, secondaryContextualPath, enclosingType.FindField);
return OperationContext.Create(contextualPath);
}
else

var secondaryContextualPath = canonicalGetAll.OperationPath;
// validate the contextualPath should be an ancestor of the secondaryContextualPath, otherwise report diagnostic.
if (!contextualPath.IsAncestorOf(secondaryContextualPath))
{
return OperationContext.Create(contextualPath);
// Report diagnostic
ManagementClientGenerator.Instance.Emitter.ReportDiagnostic(
code: "malformed-resource-detected",
message: $"The contextual path '{contextualPath}' is not an ancestor of the secondary contextual path '{secondaryContextualPath}'.",
targetCrossLanguageDefinitionId: canonicalGetAll.InputMethod.CrossLanguageDefinitionId
);
}
return OperationContext.Create(contextualPath, secondaryContextualPath, enclosingType.FindField);
}

private FieldProvider FindField(string variableName)
Expand Down Expand Up @@ -128,7 +126,7 @@ private static RequestPathPattern GetContextualPath(ArmResourceMetadata resource
var parameter = new ParameterProvider(
contextualParameter.VariableName,
$"The {contextualParameter.VariableName} for the resource.",
ResourceHelpers.GetRequestPathParameterType(contextualParameter.VariableName, _getAll!.InputMethod));
ResourceHelpers.GetRequestPathParameterType(contextualParameter.VariableName, _getAlls[0].InputMethod));
var field = new FieldProvider(FieldModifiers.Private | FieldModifiers.ReadOnly, parameter.Type, $"_{contextualParameter.VariableName}", this, description: $"The {contextualParameter.VariableName}.");
parameter.Field = field;
extraParameters.Add(parameter);
Expand All @@ -137,32 +135,64 @@ private static RequestPathPattern GetContextualPath(ArmResourceMetadata resource
return (extraParameters, extraFields);
}

private static void InitializeMethods(
private static (ResourceMethod? Get, ResourceMethod? Create, IReadOnlyList<ResourceMethod> GetAlls) InitializeMethods(
IReadOnlyList<ResourceMethod> resourceMethods,
ref ResourceMethod? getMethod,
ref ResourceMethod? createMethod,
ref ResourceMethod? getAllMethod)
RequestPathPattern contextualPath)
{
ResourceMethod? getMethod = null;
ResourceMethod? createMethod = null;
var listMethods = new List<ResourceMethod>();

foreach (var method in resourceMethods)
{
if (getAllMethod is not null && createMethod is not null && getMethod is not null)
{
break; // we already have all methods we need
}

switch (method.Kind)
{
case ResourceOperationKind.Read:
getMethod = method;
getMethod ??= method;
break;
case ResourceOperationKind.List:
getAllMethod = method;
listMethods.Add(method);
break;
case ResourceOperationKind.Create:
createMethod = method;
createMethod ??= method;
break;
}
}

return (getMethod, createMethod, SortGetAllMethodsByScopeBreadth(listMethods, contextualPath));
}

private static IReadOnlyList<ResourceMethod> SortGetAllMethodsByScopeBreadth(
IReadOnlyList<ResourceMethod> listMethods,
RequestPathPattern contextualPath)
{
if (listMethods.Count <= 1)
{
return listMethods;
}

return [.. listMethods
.OrderBy(m => CountExtraVariableSegments(contextualPath, m.OperationPath))
.ThenBy(m => m.OperationPath.Count)];
}

private static int CountExtraVariableSegments(RequestPathPattern contextualPath, RequestPathPattern operationPath)
{
if (!contextualPath.IsAncestorOf(operationPath))
{
return int.MaxValue;
}

var extra = contextualPath.TrimAncestorFrom(operationPath);
int count = 0;
foreach (var segment in extra)
{
if (!segment.IsConstant)
{
count++;
}
}
return count;
}

public ResourceClientProvider Resource => _resource;
Expand Down Expand Up @@ -340,16 +370,28 @@ protected override MethodProvider[] BuildMethods()

private MethodProvider[] BuildGetAllMethods()
{
if (_getAll is null)
if (_getAlls.Count == 0)
{
return [];
}

// implement paging method GetAll
_getAllSyncMethodProvider = BuildGetAllMethod(_getAll, false);
var getAllAsync = BuildGetAllMethod(_getAll, true);
var methods = new List<MethodProvider>(_getAlls.Count * 2);
for (int i = 0; i < _getAlls.Count; i++)
{
var listMethod = _getAlls[i];
var sync = BuildGetAllMethod(listMethod, false);
var async = BuildGetAllMethod(listMethod, true);

if (i == 0)
{
_getAllSyncMethodProvider = sync;
}

methods.Add(async);
methods.Add(sync);
}

return [getAllAsync, _getAllSyncMethodProvider];
return [.. methods];
}

private MethodProvider[] BuildEnumeratorMethods()
Expand Down