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

Add a recursive parameter to the dotnet-grpc add-files CLI command #2204

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 8 additions & 4 deletions src/dotnet-grpc/Commands/AddFileCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand All @@ -19,6 +19,7 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using Grpc.Dotnet.Cli.Internal;
using Grpc.Dotnet.Cli.Options;
using Grpc.Dotnet.Cli.Properties;
Expand All @@ -41,6 +42,7 @@ public static Command Create(HttpClient httpClient)
var serviceOption = CommonOptions.ServiceOption();
var additionalImportDirsOption = CommonOptions.AdditionalImportDirsOption();
var accessOption = CommonOptions.AccessOption();
var recursiveOption = CommonOptions.RecursiveOption();
var filesArgument = new Argument<string[]>
{
Name = "files",
Expand All @@ -52,6 +54,7 @@ public static Command Create(HttpClient httpClient)
command.AddOption(serviceOption);
command.AddOption(accessOption);
command.AddOption(additionalImportDirsOption);
command.AddOption(recursiveOption);
command.AddArgument(filesArgument);

command.SetHandler(
Expand All @@ -61,12 +64,13 @@ public static Command Create(HttpClient httpClient)
var services = context.ParseResult.GetValueForOption(serviceOption);
var access = context.ParseResult.GetValueForOption(accessOption);
var additionalImportDirs = context.ParseResult.GetValueForOption(additionalImportDirsOption);
var searchOption = context.ParseResult.HasOption(recursiveOption) ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var files = context.ParseResult.GetValueForArgument(filesArgument);

try
{
var command = new AddFileCommand(context.Console, project, httpClient);
await command.AddFileAsync(services, access, additionalImportDirs, files);
await command.AddFileAsync(services, access, additionalImportDirs, files, searchOption);

context.ExitCode = 0;
}
Expand All @@ -81,11 +85,11 @@ public static Command Create(HttpClient httpClient)
return command;
}

public async Task AddFileAsync(Services services, Access access, string? additionalImportDirs, string[] files)
public async Task AddFileAsync(Services services, Access access, string? additionalImportDirs, string[] files, SearchOption searchOption)
{
var resolvedServices = ResolveServices(services);
await EnsureNugetPackagesAsync(resolvedServices);
files = GlobReferences(files);
files = GlobReferences(files, searchOption);

foreach (var file in files)
{
Expand Down
10 changes: 5 additions & 5 deletions src/dotnet-grpc/Commands/CommandBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -174,7 +174,7 @@ public void AddProtobufReference(Services services, string? additionalImportDirs
}

var normalizedFile = NormalizePath(file);

var normalizedAdditionalImportDirs = string.Empty;

if (!string.IsNullOrWhiteSpace(additionalImportDirs))
Expand Down Expand Up @@ -298,7 +298,7 @@ public IEnumerable<ProjectItem> ResolveReferences(string[] references)
return resolvedReferences;
}

internal string[] GlobReferences(string[] references)
internal string[] GlobReferences(string[] references, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
var expandedReferences = new List<string>();

Expand All @@ -315,7 +315,7 @@ internal string[] GlobReferences(string[] references)
var directoryToSearch = Path.GetPathRoot(reference)!;
var searchPattern = reference.Substring(directoryToSearch.Length);

var resolvedFiles = Directory.GetFiles(directoryToSearch, searchPattern);
var resolvedFiles = Directory.GetFiles(directoryToSearch, searchPattern, searchOption);
Copy link

@Jecha1917 Jecha1917 Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a good idea to import all proto files, it's better to go through the proto imports recursively and import the specified files if they exist. It will look like this: we add only the securities.proto file, the definition of the service, the rest used will be pulled up recursively.
example: securities.proto

syntax = "proto3";
import "proto/fftapi/v1/security.proto";
package grpc.fftapi.v1;

message GetSecuritiesRequest {
}

message GetSecuritiesResult {
  repeated proto.fftapi.v1.Security securities = 1;
}

service Securities {
   rpc GetSecurities(grpc.fftapi.v1.GetSecuritiesRequest) returns (grpc.fftapi.v1.GetSecuritiesResult);
}

This example is from a real service. The approach that implements recursive directory search not only fails to complete successfully but also generates unnecessary services and types.


if (resolvedFiles.Length == 0)
{
Expand All @@ -328,7 +328,7 @@ internal string[] GlobReferences(string[] references)

if (Directory.Exists(Path.Combine(Project.DirectoryPath, Path.GetDirectoryName(reference)!)))
{
var resolvedFiles = Directory.GetFiles(Project.DirectoryPath, reference);
var resolvedFiles = Directory.GetFiles(Project.DirectoryPath, reference, searchOption);

if (resolvedFiles.Length == 0)
{
Expand Down
11 changes: 10 additions & 1 deletion src/dotnet-grpc/Options/CommonOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -54,4 +54,13 @@ public static Option<string> AdditionalImportDirsOption()
description: CoreStrings.AdditionalImportDirsOption);
return o;
}

public static Option<bool> RecursiveOption()
{
var o = new Option<bool>(
aliases: new[] { "-r", "--recursive" },
description: CoreStrings.RecursiveOptionDescription
);
return o;
}
}
11 changes: 10 additions & 1 deletion src/dotnet-grpc/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/dotnet-grpc/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,7 @@
<data name="LogNoReferences" xml:space="preserve">
<value>No protobuf references in the gRPC project.</value>
</data>
<data name="RecursiveOptionDescription" xml:space="preserve">
<value>Whether to add the protobuf file references recursively. Default value is false.</value>
</data>
</root>
48 changes: 43 additions & 5 deletions test/dotnet-grpc.Tests/AddFileCommandTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -41,7 +41,7 @@ public async Task Commandline_AddFileCommand_AddsPackagesAndReferences()
var parser = Program.BuildParser(CreateClient());

// Act
var result = await parser.InvokeAsync($"add-file -p {tempDir} -s Server --access Internal -i ImportDir {Path.Combine("Proto", "*.proto")}", testConsole);
var result = await parser.InvokeAsync($"add-file -p {tempDir} -s Server --access Internal -r -i ImportDir {Path.Combine("Proto", "*.proto")}", testConsole);

// Assert
Assert.AreEqual(0, result, testConsole.Error.ToString());
Expand All @@ -55,9 +55,10 @@ public async Task Commandline_AddFileCommand_AddsPackagesAndReferences()


var protoRefs = project.GetItems(CommandBase.ProtobufElement);
Assert.AreEqual(2, protoRefs.Count);
Assert.AreEqual(3, protoRefs.Count);
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\a.proto"));
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\b.proto"));
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\Subfolder\\c.proto"));
foreach (var protoRef in protoRefs)
{
Assert.AreEqual("Server", protoRef.GetMetadataValue(CommandBase.GrpcServicesElement));
Expand All @@ -81,19 +82,56 @@ public async Task AddFileCommand_AddsPackagesAndReferences()
// Act
Directory.SetCurrentDirectory(tempDir);
var command = new AddFileCommand(new TestConsole(), projectPath: null, CreateClient());
await command.AddFileAsync(Services.Server, Access.Internal, "ImportDir", new[] { Path.Combine("Proto", "*.proto") });
await command.AddFileAsync(Services.Server, Access.Internal, "ImportDir", new[] { Path.Combine("Proto", "*.proto") }, SearchOption.TopDirectoryOnly);
command.Project.ReevaluateIfNecessary();

// Assert
var packageRefs = command.Project.GetItems(CommandBase.PackageReferenceElement);
Assert.AreEqual(1, packageRefs.Count);
Assert.NotNull(packageRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Grpc.AspNetCore" && !r.HasMetadata(CommandBase.PrivateAssetsElement)));


var protoRefs = command.Project.GetItems(CommandBase.ProtobufElement);
Assert.AreEqual(2, protoRefs.Count);
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\a.proto"));
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\b.proto"));
Assert.Null(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\Subfolder\\c.proto"));
foreach (var protoRef in protoRefs)
{
Assert.AreEqual("Server", protoRef.GetMetadataValue(CommandBase.GrpcServicesElement));
Assert.AreEqual("ImportDir", protoRef.GetMetadataValue(CommandBase.AdditionalImportDirsElement));
Assert.AreEqual("Internal", protoRef.GetMetadataValue(CommandBase.AccessElement));
}

// Cleanup
Directory.SetCurrentDirectory(currentDir);
Directory.Delete(tempDir, true);
}

[Test]
[NonParallelizable]
public async Task AddFileCommand_AddsPackagesAndReferencesRecursively()
{
// Arrange
var currentDir = Directory.GetCurrentDirectory();
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
new DirectoryInfo(Path.Combine(currentDir, "TestAssets", "EmptyProject")).CopyTo(tempDir);

// Act
Directory.SetCurrentDirectory(tempDir);
var command = new AddFileCommand(new TestConsole(), projectPath: null, CreateClient());
await command.AddFileAsync(Services.Server, Access.Internal, "ImportDir", new[] { Path.Combine("Proto", "*.proto") }, SearchOption.AllDirectories);
command.Project.ReevaluateIfNecessary();

// Assert
var packageRefs = command.Project.GetItems(CommandBase.PackageReferenceElement);
Assert.AreEqual(1, packageRefs.Count);
Assert.NotNull(packageRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Grpc.AspNetCore" && !r.HasMetadata(CommandBase.PrivateAssetsElement)));

var protoRefs = command.Project.GetItems(CommandBase.ProtobufElement);
Assert.AreEqual(3, protoRefs.Count);
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\a.proto"));
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\b.proto"));
Assert.NotNull(protoRefs.SingleOrDefault(r => r.UnevaluatedInclude == "Proto\\Subfolder\\c.proto"));
foreach (var protoRef in protoRefs)
{
Assert.AreEqual("Server", protoRef.GetMetadataValue(CommandBase.GrpcServicesElement));
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions test/dotnet-grpc.Tests/dotnet-grpc.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,8 @@
</AssemblyAttribute>

</ItemGroup>
<ItemGroup>
<Folder Include="TestAssets\EmptyProject\Proto\Subfolder\" />
</ItemGroup>

</Project>