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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
convertResourceMetadataToArguments,
NonResourceMethod,
ResourceMetadata,
ResourceMethod,
ResourceOperationKind,
ResourceScope
} from "./resource-metadata.js";
Expand Down Expand Up @@ -58,6 +59,9 @@ export async function updateClients(
sdkContext.sdkPackage.models.map((m) => [m.crossLanguageDefinitionId, m])
);
const resourceModels = getAllResourceModels(codeModel);
const resourceModelMap = new Map<string, InputModelType>(
resourceModels.map((m) => [m.crossLanguageDefinitionId, m])
);

const resourceModelToMetadataMap = new Map<string, ResourceMetadata>(
resourceModels.map((m) => [
Expand All @@ -68,7 +72,7 @@ export async function updateClients(
singletonResourceName: getSingletonResource(
m.decorators?.find((d) => d.name == singleton)
),
resourceScope: getResourceScope(m),
resourceScope: ResourceScope.Tenant, // temporary default to Tenant, will be properly set later after methods are populated
methods: [],
parentResourceId: undefined, // this will be populated later
resourceName: m.name
Expand Down Expand Up @@ -137,6 +141,12 @@ export async function updateClients(
resourceModelToMetadataMap.values()
);
}

// update the model's resourceScope based on resource scope decorator if it exists or based on the Get method's scope. If neither exist, it will be set to ResourceGroup by default
const model = resourceModelMap.get(modelId);
if (model) {
metadata.resourceScope = getResourceScope(model, metadata.methods);
}
}

// the last step, add the decorator to the resource model
Expand Down Expand Up @@ -290,7 +300,8 @@ function getSingletonResource(
return singletonResource ?? "default";
}

function getResourceScope(model: InputModelType): ResourceScope {
function getResourceScope(model: InputModelType, methods?: ResourceMethod[]): ResourceScope {
// First, check for explicit scope decorators
const decorators = model.decorators;
if (decorators?.some((d) => d.name == tenantResource)) {
return ResourceScope.Tenant;
Expand All @@ -299,6 +310,16 @@ function getResourceScope(model: InputModelType): ResourceScope {
} else if (decorators?.some((d) => d.name == resourceGroupResource)) {
return ResourceScope.ResourceGroup;
}

// Fall back to Get method's scope only if no scope decorators are found
if (methods) {
const getMethod = methods.find(m => m.kind === ResourceOperationKind.Get);
if (getMethod) {
return getMethod.operationScope;
}
}

// Final fallback to ResourceGroup
return ResourceScope.ResourceGroup; // all the templates work as if there is a resource group decorator when there is no such decorator
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { TestHost } from "@typespec/compiler/testing";
import { createModel } from "@typespec/http-client-csharp";
import { getAllClients, updateClients } from "../src/resource-detection.js";
import { ok, strictEqual } from "assert";
import { resourceMetadata } from "../src/sdk-context-options.js";
import {
resourceMetadata,
tenantResource,
subscriptionResource,
resourceGroupResource
} from "../src/sdk-context-options.js";
import { ResourceScope } from "../src/resource-metadata.js";

describe("Resource Detection", () => {
Expand Down Expand Up @@ -963,4 +968,73 @@ interface Employees {
);
strictEqual(employeeMetadataDecorator.arguments.resourceName, "Employee");
});

it("resource scope determined from Get method when no explicit decorator", async () => {
const program = await typeSpecCompile(
`
@parentResource(SubscriptionLocationResource)
model Employee is ProxyResource<EmployeeProperties> {
...ResourceNameParameter<Employee, Type = EmployeeType>;
}

model EmployeeProperties {
age?: int32;
}

union EmployeeType {
string,
}

interface Operations extends Azure.ResourceManager.Operations {}

@armResourceOperations
interface Employees {
get is ArmResourceRead<Employee>;
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);
updateClients(root, sdkContext);

const employeeClient = getAllClients(root).find(
(c) => c.name === "Employees"
);
ok(employeeClient);
const employeeModel = root.models.find((m) => m.name === "Employee");
ok(employeeModel);
const getMethod = employeeClient.methods.find((m) => m.name === "get");
ok(getMethod);

const resourceMetadataDecorator = employeeModel.decorators?.find(
(d) => d.name === resourceMetadata
);
ok(resourceMetadataDecorator);
ok(resourceMetadataDecorator.arguments);

// Verify that the model has NO scope-related decorators
const hasNoScopeDecorators = !employeeModel.decorators?.some((d) =>
d.name === tenantResource ||
d.name === subscriptionResource ||
d.name === resourceGroupResource
);
ok(hasNoScopeDecorators, "Model should have no scope-related decorators to test fallback logic");

// The model should inherit its resourceScope from the Get method's operationScope (Subscription)
// because the Get method operates at subscription scope and there are no explicit scope decorators
strictEqual(
resourceMetadataDecorator.arguments.resourceScope,
"Subscription"
);

// Verify the Get method itself has the correct scope
const getMethodEntry = resourceMetadataDecorator.arguments.methods.find(
(m: any) => m.methodId === getMethod.crossLanguageDefinitionId
);
ok(getMethodEntry);
strictEqual(getMethodEntry.kind, "Get");
strictEqual(getMethodEntry.operationScope, ResourceScope.Subscription);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,10 @@ private IEnumerable<MethodProvider> BuildMethodsForResource(ResourceClientProvid
// the first method is returning the collection
var collection = resource.ResourceCollection!;
var collectionMethodSignature = resource.FactoryMethodSignature;
var pathParameters = collection.PathParameters;
collectionMethodSignature.Update(parameters: [.. collectionMethodSignature.Parameters, .. pathParameters]);

var bodyStatement = Return(This.As<ArmResource>().GetCachedClient(new CodeWriterDeclaration("client"), client => New.Instance(collection.Type, client, This.As<ArmResource>().Id())));
var bodyStatement = Return(This.As<ArmResource>().GetCachedClient(new CodeWriterDeclaration("client"), client => New.Instance(collection.Type, [client, This.As<ArmResource>().Id(), .. pathParameters])));
yield return new MethodProvider(
collectionMethodSignature,
bodyStatement,
Expand All @@ -233,24 +235,24 @@ private IEnumerable<MethodProvider> BuildMethodsForResource(ResourceClientProvid
if (getAsyncMethod is not null)
{
// we should be sure that this would never be null, but this null check here is just ensuring that we never crash
yield return BuildGetMethod(this, getAsyncMethod, collectionMethodSignature, $"Get{resource.ResourceName}Async");
yield return BuildGetMethod(this, getAsyncMethod, collectionMethodSignature, pathParameters, $"Get{resource.ResourceName}Async");
}

if (getMethod is not null)
{
// we should be sure that this would never be null, but this null check here is just ensuring that we never crash
yield return BuildGetMethod(this, getMethod, collectionMethodSignature, $"Get{resource.ResourceName}");
yield return BuildGetMethod(this, getMethod, collectionMethodSignature, pathParameters, $"Get{resource.ResourceName}");
}

static MethodProvider BuildGetMethod(TypeProvider enclosingType, MethodProvider resourceGetMethod, MethodSignature collectionGetSignature, string methodName)
static MethodProvider BuildGetMethod(TypeProvider enclosingType, MethodProvider resourceGetMethod, MethodSignature collectionGetSignature, IReadOnlyList<ParameterProvider> pathParameters, string methodName)
{
var signature = new MethodSignature(
methodName,
resourceGetMethod.Signature.Description,
resourceGetMethod.Signature.Modifiers,
resourceGetMethod.Signature.ReturnType,
resourceGetMethod.Signature.ReturnDescription,
resourceGetMethod.Signature.Parameters,
[.. pathParameters, .. resourceGetMethod.Signature.Parameters],
Attributes: [new AttributeStatement(typeof(ForwardsClientCallsAttribute))]);

return new MethodProvider(
Expand All @@ -265,7 +267,6 @@ static MethodProvider BuildGetMethod(TypeProvider enclosingType, MethodProvider
private MethodProvider BuildResourceServiceMethod(ResourceClientProvider resource, ResourceMethod resourceMethod, bool isAsync)
{
var methodName = ResourceHelpers.GetExtensionOperationMethodName(resourceMethod.Kind, resource.ResourceName, isAsync);

return BuildServiceMethod(resourceMethod.InputMethod, resourceMethod.InputClient, isAsync, methodName);
}

Expand All @@ -274,8 +275,8 @@ private MethodProvider BuildServiceMethod(InputServiceMethod method, InputClient
var clientInfo = _clientInfos[inputClient];
return method switch
{
InputPagingServiceMethod pagingMethod => new PageableOperationMethodProvider(this, _contextualPath, clientInfo, pagingMethod, isAsync, methodName: methodName),
_ => new ResourceOperationMethodProvider(this, _contextualPath, clientInfo, method, isAsync, methodName: methodName)
InputPagingServiceMethod pagingMethod => new PageableOperationMethodProvider(this, _contextualPath, clientInfo, pagingMethod, isAsync, methodName),
_ => new ResourceOperationMethodProvider(this, _contextualPath, clientInfo, method, isAsync, methodName)
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ protected MethodSignature CreateSignature()
_convenienceMethod.Signature.Modifiers,
returnType,
returnDescription,
OperationMethodParameterHelper.GetOperationMethodParameters(_method, _contextualPath),
OperationMethodParameterHelper.GetOperationMethodParameters(_method, _contextualPath, _enclosingType),
_convenienceMethod.Signature.Attributes,
_convenienceMethod.Signature.GenericArguments,
_convenienceMethod.Signature.GenericParameterConstraints,
Expand All @@ -119,7 +119,8 @@ protected MethodBodyStatement[] BuildBodyStatements()
{
_restClientInfo.RestClient,
};
arguments.AddRange(_contextualPath.PopulateArguments(This.As<ArmResource>().Id(), requestMethod.Signature.Parameters, contextVariable, _signature.Parameters));

arguments.AddRange(_contextualPath.PopulateArguments(This.As<ArmResource>().Id(), requestMethod.Signature.Parameters, contextVariable, _signature.Parameters, _enclosingType));

// Handle ResourceData type conversion if needed
if (_itemResourceClient != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ protected virtual MethodBodyStatement[] BuildBodyStatements()

protected IReadOnlyList<ParameterProvider> GetOperationMethodParameters()
{
return OperationMethodParameterHelper.GetOperationMethodParameters(_serviceMethod, _contextualPath, _isFakeLongRunningOperation);
return OperationMethodParameterHelper.GetOperationMethodParameters(_serviceMethod, _contextualPath, _enclosingType, _isFakeLongRunningOperation);
}

protected virtual MethodSignature CreateSignature()
Expand Down Expand Up @@ -178,8 +178,9 @@ private TryExpression BuildTryExpression()
{
ResourceMethodSnippets.CreateRequestContext(cancellationTokenParameter, out var contextVariable)
};

// Populate arguments for the REST client method call
var arguments = _contextualPath.PopulateArguments(This.As<ArmResource>().Id(), requestMethod.Signature.Parameters, contextVariable, _signature.Parameters);
var arguments = _contextualPath.PopulateArguments(This.As<ArmResource>().Id(), requestMethod.Signature.Parameters, contextVariable, _signature.Parameters, _enclosingType);

tryStatements.Add(ResourceMethodSnippets.CreateHttpMessage(_restClientField, requestMethod.Signature.Name, arguments, out var messageVariable));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ private ResourceClientProvider(string resourceName, InputModelType model, IReadO

internal ResourceCollectionClientProvider? ResourceCollection { get; private set; }

public RequestPathPattern ContextualPath => _contextualPath;

protected override string BuildName() => ResourceName.EndsWith("Resource") ? ResourceName : $"{ResourceName}Resource";

protected override FormattableString BuildDescription() => $"A class representing a {ResourceName} along with the instance operations that can be performed on it.\nIf you have a {typeof(ResourceIdentifier):C} you can construct a {Type:C} from an instance of {typeof(ArmClient):C} using the GetResource method.\nOtherwise you can get one from its parent resource {TypeOfParentResource:C} using the {FactoryMethodSignature.Name} method.";
Expand Down Expand Up @@ -302,7 +304,7 @@ private ConstructorProvider BuildResourceIdentifierConstructor()
}

// TODO -- this is temporary. We should change this to find the corresponding parameters in ContextualParameters after it is refactored to consume parent resources.
private CSharpType GetPathParameterType(string parameterName)
public CSharpType GetPathParameterType(string parameterName)
{
foreach (var resourceMethod in _resourceServiceMethods)
{
Expand Down
Loading