Skip to content

Commit

Permalink
Implement resource detection and generate ReourceData (Azure#47730)
Browse files Browse the repository at this point in the history
  • Loading branch information
live1206 authored Jan 15, 2025
1 parent 68b6ecb commit 750b0a1
Show file tree
Hide file tree
Showing 19 changed files with 547 additions and 58 deletions.
2 changes: 1 addition & 1 deletion eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@
</ItemGroup>

<ItemGroup Condition="'$(IsGeneratorLibrary)' == 'true'">
<PackageReference Update="Microsoft.Generator.CSharp.ClientModel" Version="1.0.0-alpha.20250106.3" />
<PackageReference Update="Microsoft.Generator.CSharp.ClientModel" Version="1.0.0-alpha.20250114.2" />
</ItemGroup>

<!--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class AzureArmVisitor : ScmLibraryVisitor
// TODO: uncomment this once resources are generated
//if (type is RestClientProvider)
//{
// type.Update(modifiers: TransfromModifiers(type), relativeFilePath: TransformRelativeFilePathForRestClient(type));
// type.Update(modifiers: TransfromPublicModifiersToInternal(type), relativeFilePath: TransformRelativeFilePathForRestClient(type));
//}
return type;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public AzureClientPlugin(GeneratorContext context) : base(context)
public override void Configure()
{
base.Configure();
// Include Azure.Core
AddMetadataReference(MetadataReference.CreateFromFile(typeof(Response).Assembly.Location));
var sharedSourceDirectory = Path.Combine(Path.GetDirectoryName(typeof(AzureClientPlugin).Assembly.Location)!, "Shared", "Core");
AddSharedSourceDirectory(sharedSourceDirectory);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Generator.Mgmt.Models;
using Azure.Generator.Providers;
using Azure.Generator.Utilities;
using Microsoft.Generator.CSharp.ClientModel;
using Microsoft.Generator.CSharp.Providers;
using System.Collections.Generic;

namespace Azure.Generator
{
/// <inheritdoc/>
public class AzureOutputLibrary : ScmOutputLibrary
{
// TODO: categorize clients into operationSets, which contains operations sharing the same Path
private Dictionary<string, OperationSet> _pathToOperationSetMap;
private Dictionary<string, HashSet<OperationSet>> _resourceDataBySpecNameMap;

/// <inheritdoc/>
public AzureOutputLibrary()
{
_pathToOperationSetMap = CategorizeClients();
_resourceDataBySpecNameMap = EnsureResourceDataMap();
}

private Dictionary<string, HashSet<OperationSet>> EnsureResourceDataMap()
{
var result = new Dictionary<string, HashSet<OperationSet>>();
foreach (var operationSet in _pathToOperationSetMap.Values)
{
if (operationSet.TryGetResourceDataSchema(out var resourceSpecName, out var resourceSchema))
{
// if this operation set corresponds to a SDK resource, we add it to the map
if (!result.TryGetValue(resourceSpecName!, out HashSet<OperationSet>? value))
{
value = new HashSet<OperationSet>();
result.Add(resourceSpecName!, value);
}
value.Add(operationSet);
}
}

return result;
}

private Dictionary<string, OperationSet> CategorizeClients()
{
var result = new Dictionary<string, OperationSet>();
foreach (var inputClient in AzureClientPlugin.Instance.InputLibrary.InputNamespace.Clients)
{
var requestPathList = new HashSet<string>();
foreach (var operation in inputClient.Operations)
{
var path = operation.GetHttpPath();
requestPathList.Add(path);
if (result.TryGetValue(path, out var operationSet))
{
operationSet.Add(operation);
}
else
{
operationSet = new OperationSet(path, inputClient)
{
operation
};
result.Add(path, operationSet);
}
}
}

// TODO: add operation set for the partial resources here

return result;
}

/// <inheritdoc/>
// TODO: generate resources and collections
protected override TypeProvider[] BuildTypeProviders() => [.. base.BuildTypeProviders(), new RequestContextExtensionsDefinition()];

internal bool IsResource(string name) => _resourceDataBySpecNameMap.ContainsKey(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
using Microsoft.Generator.CSharp.Expressions;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Primitives;
using Microsoft.Generator.CSharp.Providers;
using Microsoft.Generator.CSharp.Snippets;
using Microsoft.Generator.CSharp.Statements;
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Text.Json;

namespace Azure.Generator
Expand Down Expand Up @@ -109,5 +111,29 @@ public override MethodBodyStatement SerializeJsonValue(Type valueType, ValueExpr
/// <inheritdoc/>
protected override ClientProvider CreateClientCore(InputClient inputClient)
=> AzureClientPlugin.Instance.IsAzureArm.Value ? base.CreateClientCore(InputClientTransformer.TransformInputClient(inputClient)) : base.CreateClientCore(inputClient);

/// <inheritdoc/>
protected override IReadOnlyList<TypeProvider> CreateSerializationsCore(InputType inputType, TypeProvider typeProvider)
{
if (inputType is InputModelType inputModel
&& typeProvider is ModelProvider modelProvider
&& AzureClientPlugin.Instance.OutputLibrary.IsResource(inputType.Name)
&& inputModel.Usage.HasFlag(InputModelTypeUsage.Json))
{
return [new ResourceDataSerializationProvider(inputModel, modelProvider)];
}

return base.CreateSerializationsCore(inputType, typeProvider);
}

/// <inheritdoc/>
protected override ModelProvider? CreateModelCore(InputModelType model)
{
if (AzureClientPlugin.Instance.OutputLibrary.IsResource(model.Name))
{
return new ResourceDataProvider(model);
}
return base.CreateModelCore(model);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Core;
using Azure.Generator.Utilities;
using Microsoft.Generator.CSharp.Input;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace Azure.Generator.Mgmt.Models
{
/// <summary>
/// An <see cref="OperationSet"/> represents a collection of <see cref="Operation"/> with the same request path.
/// </summary>
internal class OperationSet : IReadOnlyCollection<InputOperation>, IEquatable<OperationSet>
{
private readonly InputClient? _inputClient;

/// <summary>
/// The raw request path of string of the operations in this <see cref="OperationSet"/>
/// </summary>
public string RequestPath { get; }

/// <summary>
/// The operation set
/// </summary>
private HashSet<InputOperation> _operations;

public int Count => _operations.Count;

public OperationSet(string requestPath, InputClient? inputClient)
{
_inputClient = inputClient;
RequestPath = requestPath;
_operations = new HashSet<InputOperation>();
}

/// <summary>
/// Add a new operation to this <see cref="OperationSet"/>
/// </summary>
/// <param name="operation">The operation to be added</param>
/// <exception cref="InvalidOperationException">when trying to add an operation with a different path from <see cref="RequestPath"/></exception>
public void Add(InputOperation operation)
{
var path = operation.GetHttpPath();
if (path != RequestPath)
throw new InvalidOperationException($"Cannot add operation with path {path} to OperationSet with path {RequestPath}");
_operations.Add(operation);
}

public IEnumerator<InputOperation> GetEnumerator() => _operations.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => _operations.GetEnumerator();

public override int GetHashCode()
{
return RequestPath.GetHashCode();
}

public bool Equals([AllowNull] OperationSet other)
{
if (other is null)
return false;

return RequestPath == other.RequestPath;
}

/// <summary>
/// Get the operation with the given verb.
/// We cannot have two operations with the same verb under the same request path, therefore this method is only returning one operation or null
/// </summary>
/// <param name="method"></param>
/// <returns></returns>
public InputOperation? GetOperation(RequestMethod method)
{
foreach (var operation in _operations)
{
if (operation.HttpMethod == method.ToString())
return operation;
}

return null;
}



private InputOperation? FindBestOperation()
{
// first we try GET operation
var getOperation = FindOperation(RequestMethod.Get);
if (getOperation != null)
return getOperation;
// if no GET operation, we return PUT operation
var putOperation = FindOperation(RequestMethod.Put);
if (putOperation != null)
return putOperation;

// if no PUT or GET, we just return the first one
return _operations.FirstOrDefault();
}

public InputOperation? FindOperation(RequestMethod method)
{
return this.FirstOrDefault(operation => operation.HttpMethod == method.ToString());
}

public override string? ToString()
{
return RequestPath;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Providers;
using System.IO;

namespace Azure.Generator.Providers
{
internal class ResourceDataProvider : ModelProvider
{
private readonly InputModelType _inputModel;

public ResourceDataProvider(InputModelType inputModel) : base(inputModel)
{
_inputModel = inputModel;
}

protected override string BuildName() => $"{base.BuildName()}Data";

protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Generator.CSharp.ClientModel.Providers;
using Microsoft.Generator.CSharp.Input;
using Microsoft.Generator.CSharp.Providers;
using System.IO;

namespace Azure.Generator.Providers
{
internal class ResourceDataSerializationProvider : MrwSerializationTypeDefinition
{
public ResourceDataSerializationProvider(InputModelType inputModel, ModelProvider modelProvider) : base(inputModel, modelProvider)
{
}

protected override string BuildName() => $"{base.BuildName()}Data";

protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.Serialization.cs");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Generator.CSharp.Input;
using System.Collections.Generic;
using System.Linq;

namespace Azure.Generator.Utilities
{
internal static class InputExtensions
{
/// <summary>
/// Union all the properties on myself and all the properties from my parents
/// </summary>
/// <param name="inputModel"></param>
/// <returns></returns>
internal static IEnumerable<InputModelProperty> GetAllProperties(this InputModelType inputModel)
{
return inputModel.GetAllBaseModels().SelectMany(parentInputModelType => parentInputModelType.Properties).Concat(inputModel.Properties);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Generator.CSharp.Input;
using System;
using System.Linq;

namespace Azure.Generator.Utilities
{
internal static class OperationExtensions
{
public static string GetHttpPath(this InputOperation operation)
{
var path = operation.Path;
// Do not trim the tenant resource path '/'.
return (path?.Length == 1 ? path : path?.TrimEnd('/')) ??
throw new InvalidOperationException($"Cannot get HTTP path from operation {operation.Name}");
}

public static OperationResponse? GetServiceResponse(this InputOperation operation, int code = 200)
{
return operation.Responses.FirstOrDefault(r => r.StatusCodes.Contains(code));
}
}
}
Loading

0 comments on commit 750b0a1

Please sign in to comment.