Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/generate client #4286

Merged
merged 8 commits into from
Mar 6, 2024
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 @@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using Kiota.Builder.Extensions;
using Kiota.Builder.Lock;
using Microsoft.OpenApi.ApiManifest;

Expand Down Expand Up @@ -113,6 +114,10 @@ public StructuredMimeTypesCollection StructuredMimeTypes
};
public HashSet<string> IncludePatterns { get; set; } = new(0, StringComparer.OrdinalIgnoreCase);
public HashSet<string> ExcludePatterns { get; set; } = new(0, StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The overrides loaded from the api manifest when refreshing a client, as opposed to the user provided ones.
/// </summary>
public HashSet<string> PatternsOverride { get; set; } = new(0, StringComparer.OrdinalIgnoreCase);
public bool ClearCache
{
get; set;
Expand Down Expand Up @@ -144,6 +149,7 @@ public object Clone()
MaxDegreeOfParallelism = MaxDegreeOfParallelism,
SkipGeneration = SkipGeneration,
Operation = Operation,
PatternsOverride = new(PatternsOverride ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase),
};
}
private static readonly StringIEnumerableDeepComparer comparer = new();
Expand Down Expand Up @@ -175,7 +181,7 @@ public ApiDependency ToApiDependency(string configurationHash, Dictionary<string
Extensions = new() {
{ KiotaHashManifestExtensionKey, JsonValue.Create(configurationHash)}
},
Requests = templatesWithOperations.SelectMany(static x => x.Value.Select(y => new RequestInfo { Method = y.ToUpperInvariant(), UriTemplate = x.Key })).ToList(),
Requests = templatesWithOperations.SelectMany(static x => x.Value.Select(y => new RequestInfo { Method = y.ToUpperInvariant(), UriTemplate = x.Key.DeSanitizeUrlTemplateParameter() })).ToList(),
};
return dependency;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@ public static string SanitizeParameterNameForUrlTemplate(this string original)
.Replace(".", "%2E", StringComparison.OrdinalIgnoreCase)
.Replace("~", "%7E", StringComparison.OrdinalIgnoreCase);// - . ~ are invalid uri template character but don't get encoded by Uri.EscapeDataString
}
public static string DeSanitizeUrlTemplateParameter(this string original)
{
if (string.IsNullOrEmpty(original)) return original;
return Uri.UnescapeDataString(original.Replace("%2D", "-", StringComparison.OrdinalIgnoreCase)
.Replace("%2E", ".", StringComparison.OrdinalIgnoreCase)
.Replace("%7E", "~", StringComparison.OrdinalIgnoreCase));
}
[GeneratedRegex(@"%[0-9A-F]{2}", RegexOptions.Singleline, 500)]
private static partial Regex removePctEncodedCharacters();
public static string SanitizeParameterNameForCodeSymbols(this string original, string replaceEncodedCharactersWith = "")
Expand Down
5 changes: 5 additions & 0 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@
{
var includePatterns = GetFilterPatternsFromConfiguration(config.IncludePatterns);
var excludePatterns = GetFilterPatternsFromConfiguration(config.ExcludePatterns);
if (config.PatternsOverride.Count != 0)
{ // loading the patterns from the manifest as we don't want to take the user input one and have new operation creep in from the description being updated since last generation
includePatterns = GetFilterPatternsFromConfiguration(config.PatternsOverride);
excludePatterns = [];
}
if (includePatterns.Count == 0 && excludePatterns.Count == 0) return;

var nonOperationIncludePatterns = includePatterns.Where(static x => x.Value.Count == 0).Select(static x => x.Key).ToList();
Expand Down Expand Up @@ -970,7 +975,7 @@

if (!"string".Equals(parameter.Type.Name, StringComparison.OrdinalIgnoreCase) && config.IncludeBackwardCompatible)
{ // adding a second indexer for the string version of the parameter so we keep backward compatibility
//TODO remove for v2

Check warning on line 978 in src/Kiota.Builder/KiotaBuilder.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
var backCompatibleValue = (CodeIndexer)result[0].Clone();
backCompatibleValue.Name += "-string";
backCompatibleValue.IndexParameter.Type = DefaultIndexerParameterType;
Expand Down Expand Up @@ -1124,7 +1129,7 @@
var suffix = $"{operationType}Response";
var modelType = CreateModelDeclarations(currentNode, schema, operation, parentClass, suffix);
if (modelType is not null && config.IncludeBackwardCompatible && config.Language is GenerationLanguage.CSharp or GenerationLanguage.Go && modelType.Name.EndsWith(suffix, StringComparison.Ordinal))
{ //TODO remove for v2

Check warning on line 1132 in src/Kiota.Builder/KiotaBuilder.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
var obsoleteTypeName = modelType.Name[..^suffix.Length] + "Response";
if (modelType is CodeType codeType &&
codeType.TypeDefinition is CodeClass codeClass)
Expand Down Expand Up @@ -1257,7 +1262,7 @@
executorMethod.AddParameter(cancellationParam);// Add cancellation token parameter

if (returnTypes.Item2 is not null && config.IncludeBackwardCompatible)
{ //TODO remove for v2

Check warning on line 1265 in src/Kiota.Builder/KiotaBuilder.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
var additionalExecutorMethod = (CodeMethod)executorMethod.Clone();
additionalExecutorMethod.ReturnType = returnTypes.Item2;
additionalExecutorMethod.OriginalMethod = executorMethod;
Expand Down Expand Up @@ -2264,7 +2269,7 @@
if (!parameterClass.ContainsPropertyWithWireName(prop.WireName))
{
if (addBackwardCompatibleParameter && config.IncludeBackwardCompatible && config.Language is GenerationLanguage.CSharp or GenerationLanguage.Go)
{ //TODO remove for v2

Check warning on line 2272 in src/Kiota.Builder/KiotaBuilder.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
var modernProp = (CodeProperty)prop.Clone();
modernProp.Name = $"{prop.Name}As{modernProp.Type.Name.ToFirstCharacterUpperCase()}";
modernProp.SerializationName = prop.WireName;
Expand Down
2 changes: 1 addition & 1 deletion src/Kiota.Builder/OpenApiDocumentDownloadService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public OpenApiDocumentDownloadService(HttpClient httpClient, ILogger logger)
if (useKiotaConfig &&
config.Operation is ClientOperation.Edit or ClientOperation.Add &&
workspaceManagementService is not null &&
await workspaceManagementService.GetDescriptionCopyAsync(config.ClientClassName, inputPath, cancellationToken).ConfigureAwait(false) is { } descriptionStream)
await workspaceManagementService.GetDescriptionCopyAsync(config.ClientClassName, inputPath, config.CleanOutput, cancellationToken).ConfigureAwait(false) is { } descriptionStream)
{
Logger.LogInformation("loaded description from the workspace copy");
input = descriptionStream;
Expand Down
32 changes: 9 additions & 23 deletions src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using Kiota.Builder.Configuration;
using Kiota.Builder.Lock;
using Microsoft.OpenApi.ApiManifest;

namespace Kiota.Builder.WorkspaceManagement;

Expand Down Expand Up @@ -92,34 +93,13 @@ public ApiClientConfiguration(GenerationConfiguration config)
OutputPath = config.OutputPath;
}
/// <summary>
/// Initializes a new instance of the <see cref="ApiClientConfiguration"/> class from an existing <see cref="KiotaLock"/> to enable migrations.
/// </summary>
/// <param name="kiotaLock">The kiota lock to migrate.</param>
/// <param name="relativeOutputPath">The relative output path to output folder from the configuration file.</param>
public ApiClientConfiguration(KiotaLock kiotaLock, string relativeOutputPath)
{
ArgumentNullException.ThrowIfNull(kiotaLock);
ArgumentNullException.ThrowIfNull(relativeOutputPath);
Language = kiotaLock.Language;
ClientNamespaceName = kiotaLock.ClientNamespaceName;
UsesBackingStore = kiotaLock.UsesBackingStore;
ExcludeBackwardCompatible = kiotaLock.ExcludeBackwardCompatible;
IncludeAdditionalData = kiotaLock.IncludeAdditionalData;
StructuredMimeTypes = kiotaLock.StructuredMimeTypes.ToList();
IncludePatterns = kiotaLock.IncludePatterns;
ExcludePatterns = kiotaLock.ExcludePatterns;
DescriptionLocation = kiotaLock.DescriptionLocation;
DisabledValidationRules = kiotaLock.DisabledValidationRules;
OutputPath = relativeOutputPath;
}
/// <summary>
/// Updates the passed configuration with the values from the config file.
/// </summary>
/// <param name="config">Generation configuration to update.</param>
/// <param name="clientName">Client name serving as class name.</param>
public void UpdateGenerationConfigurationFromApiClientConfiguration(GenerationConfiguration config, string clientName)
/// <param name="requests">The requests to use when updating an existing client.</param>
public void UpdateGenerationConfigurationFromApiClientConfiguration(GenerationConfiguration config, string clientName, IList<RequestInfo>? requests = default)
{
//TODO lock the api manifest as well to have accurate path resolution
ArgumentNullException.ThrowIfNull(config);
ArgumentException.ThrowIfNullOrEmpty(clientName);
config.ClientNamespaceName = ClientNamespaceName;
Expand All @@ -137,6 +117,12 @@ public void UpdateGenerationConfigurationFromApiClientConfiguration(GenerationCo
config.ClientClassName = clientName;
config.Serializers.Clear();
config.Deserializers.Clear();
if (requests is { Count: > 0 })
{
config.PatternsOverride = requests.Where(static x => !x.Exclude && !string.IsNullOrEmpty(x.Method) && !string.IsNullOrEmpty(x.UriTemplate))
.Select(static x => $"/{x.UriTemplate}#{x.Method!.ToUpperInvariant()}")
.ToHashSet();
}
}

public object Clone()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ public async Task<bool> ShouldGenerateAsync(GenerationConfiguration inputConfig,
}

}
public async Task<Stream?> GetDescriptionCopyAsync(string clientName, string inputPath, CancellationToken cancellationToken = default)
public async Task<Stream?> GetDescriptionCopyAsync(string clientName, string inputPath, bool cleanOutput, CancellationToken cancellationToken = default)
{
if (!UseKiotaConfig)
if (!UseKiotaConfig || cleanOutput)
return null;
return await descriptionStorageService.GetDescriptionAsync(clientName, new Uri(inputPath).GetFileExtension(), cancellationToken).ConfigureAwait(false);
}
Expand Down
6 changes: 3 additions & 3 deletions src/kiota/Handlers/BaseKiotaCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,10 @@ protected void DisplayInstallHint(LanguageInformation languageInformation, List<
languageDependencies.Select(x => " " + string.Format(languageInformation.DependencyInstallCommand, x.Name, x.Version))).ToArray());
}
}
protected void DisplayCleanHint(string commandName)
protected void DisplayCleanHint(string commandName, string argumentName = "--clean-output")
{
DisplayHint("Hint: to force the generation to overwrite an existing client pass the --clean-output switch.",
$"Example: kiota {commandName} --clean-output");
DisplayHint($"Hint: to force the generation to overwrite an existing client pass the {argumentName} switch.",
$"Example: kiota {commandName} {argumentName}");
}
protected void DisplayInfoAdvancedHint()
{
Expand Down
92 changes: 92 additions & 0 deletions src/kiota/Handlers/Client/GenerateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Kiota.Builder;
using Kiota.Builder.Configuration;
using Kiota.Builder.WorkspaceManagement;
using Microsoft.Extensions.Logging;

namespace kiota.Handlers.Client;

internal class GenerateHandler : BaseKiotaCommandHandler
{
public required Option<string> ClassOption
{
get; init;
}
public required Option<bool> RefreshOption
{
get; init;
}
public override async Task<int> InvokeAsync(InvocationContext context)
{
string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty;
bool refresh = context.ParseResult.GetValueForOption(RefreshOption);
CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None;
var (loggerFactory, logger) = GetLoggerAndFactory<KiotaBuilder>(context, Configuration.Generation.OutputPath);
using (loggerFactory)
{
await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false);
logger.AppendInternalTracing();
logger.LogTrace("configuration: {configuration}", JsonSerializer.Serialize(Configuration, KiotaConfigurationJsonContext.Default.KiotaConfiguration));
try
{
var workspaceStorageService = new WorkspaceConfigurationStorageService(Directory.GetCurrentDirectory());
var (config, manifest) = await workspaceStorageService.GetWorkspaceConfigurationAsync(cancellationToken).ConfigureAwait(false);
if (config == null)
{
DisplayError("The workspace configuration is missing, please run the init command first.");
return 1;
}
var clientNameWasNotProvided = string.IsNullOrEmpty(className);
var clientEntries = config
.Clients
.Where(x => clientNameWasNotProvided || x.Key.Equals(className, StringComparison.OrdinalIgnoreCase))
.ToArray();
if (clientEntries.Length == 0 && !clientNameWasNotProvided)
{
DisplayError($"No client found with the provided name {className}");
return 1;
}
foreach (var clientEntry in clientEntries)
{
var generationConfiguration = new GenerationConfiguration();
var requests = !refresh && manifest is not null && manifest.ApiDependencies.TryGetValue(clientEntry.Key, out var value) ? value.Requests : [];
clientEntry.Value.UpdateGenerationConfigurationFromApiClientConfiguration(generationConfiguration, clientEntry.Key, requests);
generationConfiguration.ClearCache = refresh;
generationConfiguration.CleanOutput = refresh;
var builder = new KiotaBuilder(logger, generationConfiguration, httpClient, true);
var result = await builder.GenerateClientAsync(cancellationToken).ConfigureAwait(false);
if (result)
{
DisplaySuccess($"Update of {clientEntry.Key} client completed");
var manifestPath = $"{GetAbsolutePath(WorkspaceConfigurationStorageService.ManifestFileName)}#{clientEntry.Key}";
DisplayInfoHint(generationConfiguration.Language, string.Empty, manifestPath);
}
else
{
DisplayWarning($"Update of {clientEntry.Key} skipped, no changes detected");
DisplayCleanHint("client generate", "--refresh");
}
}
return 0;
}
catch (Exception ex)
{
#if DEBUG
logger.LogCritical(ex, "error adding the client: {exceptionMessage}", ex.Message);
throw; // so debug tools go straight to the source of the exception when attached
#else
logger.LogCritical("error adding the client: {exceptionMessage}", ex.Message);
return 1;
#endif
}
}
throw new System.NotImplementedException();
}
}
2 changes: 1 addition & 1 deletion src/kiota/Handlers/KiotaUpdateCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public override async Task<int> InvokeAsync(InvocationContext context)
DisplaySuccess($"Update of {locks.Length} clients completed successfully");
foreach (var configuration in configurations)
DisplayInfoHint(configuration.Language, configuration.OpenAPIFilePath, string.Empty);
if (results.Any(x => x))
if (Array.Exists(results, static x => x))
DisplayCleanHint("update");
return 0;
}
Expand Down
23 changes: 21 additions & 2 deletions src/kiota/KiotaClientCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,27 @@ public static Command GetEditCommand()
}
public static Command GetGenerateCommand()
{
var command = new Command("generate", "Generates one or all clients from the Kiota configuration");
//TODO map the handler
var clientNameOption = GetClientNameOption(false);
var logLevelOption = KiotaHost.GetLogLevelOption();
var refreshOption = GetRefreshOption();
var command = new Command("generate", "Generates one or all clients from the Kiota configuration")
{
clientNameOption,
logLevelOption,
refreshOption,
};
command.Handler = new GenerateHandler
{
ClassOption = clientNameOption,
LogLevelOption = logLevelOption,
RefreshOption = refreshOption,
};
return command;
}
private static Option<bool> GetRefreshOption()
{
var refresh = new Option<bool>("--refresh", "Refreshes the client OpenAPI description before generating the client");
refresh.AddAlias("--r");
return refresh;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Kiota.Builder.Configuration;
using Kiota.Builder.WorkspaceManagement;
using Microsoft.OpenApi.ApiManifest;
using Xunit;

namespace Kiota.Builder.Tests.WorkspaceManagement;
Expand Down Expand Up @@ -91,7 +92,18 @@ public void UpdatesGenerationConfigurationFromApiClientConfiguration()
UsesBackingStore = true,
};
var generationConfiguration = new GenerationConfiguration();
clientConfiguration.UpdateGenerationConfigurationFromApiClientConfiguration(generationConfiguration, "client");
clientConfiguration.UpdateGenerationConfigurationFromApiClientConfiguration(generationConfiguration, "client", [
new RequestInfo
{
Method = "GET",
UriTemplate = "path/bar",
},
new RequestInfo
{
Method = "PATH",
UriTemplate = "path/baz",
},
]);
Assert.Equal(clientConfiguration.ClientNamespaceName, generationConfiguration.ClientNamespaceName);
Assert.Equal(GenerationLanguage.CSharp, generationConfiguration.Language);
Assert.Equal(clientConfiguration.DescriptionLocation, generationConfiguration.OpenAPIFilePath);
Expand All @@ -104,6 +116,7 @@ public void UpdatesGenerationConfigurationFromApiClientConfiguration()
Assert.Equal(clientConfiguration.UsesBackingStore, generationConfiguration.UsesBackingStore);
Assert.Empty(generationConfiguration.Serializers);
Assert.Empty(generationConfiguration.Deserializers);
Assert.Equal(2, generationConfiguration.PatternsOverride.Count);
}

}
Loading
Loading