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

JsonPropertyPostAction: allow to create file and path #41959

Merged
merged 4 commits into from
Jul 9, 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

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

53 changes: 28 additions & 25 deletions src/Cli/Microsoft.TemplateEngine.Cli/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@
</data>
<data name="TemplatePackageCoordinator_VulnerabilitySeverity_Moderate" xml:space="preserve">
<value>Moderate</value>
<comment>represents nuget api severity of level 1.</comment>
<comment>represents nuget api severity of level 1.</comment>
</data>
<data name="TemplatePackageCoordinator_VulnerabilitySeverity_High" xml:space="preserve">
<value>High</value>
Expand Down Expand Up @@ -859,67 +859,64 @@ The header is followed by the list of parameters and their errors (might be seve
<data name="Generic_CommandHints_List" xml:space="preserve">
<value>To list installed templates, run:</value>
</data>
<data name="DetailsCommand_Argument_PackageIdentifier">
<data name="DetailsCommand_Argument_PackageIdentifier" xml:space="preserve">
<value>Package identifier</value>
</data>
<data name="DetailsCommand_Option_Version">
<data name="DetailsCommand_Option_Version" xml:space="preserve">
<value>Specifies a concrete version for displaying details. If not specified the last is taken.</value>
</data>
<data name="DetailsCommand_Property_Authors">
<data name="DetailsCommand_Property_Authors" xml:space="preserve">
<value>Authors</value>
</data>
<data name="DetailsCommand_Property_Description">
<data name="DetailsCommand_Property_Description" xml:space="preserve">
<value>Details</value>
</data>
<data name="DetailsCommand_Property_SourceFeed">
<data name="DetailsCommand_Property_SourceFeed" xml:space="preserve">
<value>Source Feed</value>
</data>
<data name="DetailsCommand_Property_Version">
<data name="DetailsCommand_Property_Version" xml:space="preserve">
<value>Package version</value>
</data>
<data name="DetailsCommand_Property_PrefixReserved">
<data name="DetailsCommand_Property_PrefixReserved" xml:space="preserve">
<value>Reserved</value>
</data>
<data name="DetailsCommand_Property_Languages">
<data name="DetailsCommand_Property_Languages" xml:space="preserve">
<value>Languages</value>
<comment></comment>
</data>
<data name="DetailsCommand_Property_LicenseMetadata">
<data name="DetailsCommand_Property_LicenseMetadata" xml:space="preserve">
<value>License Metadata</value>
</data>
<data name="DetailsCommand_Property_LicenseExpression">
<data name="DetailsCommand_Property_LicenseExpression" xml:space="preserve">
<value>License Expression</value>
</data>
<data name="DetailsCommand_Property_License">
<data name="DetailsCommand_Property_License" xml:space="preserve">
<value>License</value>
</data>
<data name="DetailsCommand_Property_LicenseUrl">
<data name="DetailsCommand_Property_LicenseUrl" xml:space="preserve">
<value>License Url</value>
</data>
<data name="DetailsCommand_Property_Owners">
<data name="DetailsCommand_Property_Owners" xml:space="preserve">
<value>Owners</value>
</data>
<data name="DetailsCommand_Property_RepoUrl">
<data name="DetailsCommand_Property_RepoUrl" xml:space="preserve">
<value>Repository Url</value>
</data>
<data name="DetailsCommand_Property_ShortNames">
<data name="DetailsCommand_Property_ShortNames" xml:space="preserve">
<value>Short Names</value>
<comment></comment>
</data>
<data name="DetailsCommand_Property_Tags">
<data name="DetailsCommand_Property_Tags" xml:space="preserve">
<value>Tags</value>
<comment></comment>
</data>
<data name="DetailsCommand_Property_Templates">
<data name="DetailsCommand_Property_Templates" xml:space="preserve">
<value>Templates</value>
</data>
<data name="DetailsCommand_NoNuGetSources">
<data name="DetailsCommand_NoNuGetSources" xml:space="preserve">
<value>No NuGet sources are defined or enabled</value>
</data>
<data name="DetailsCommand_UnableToLoadResources">
<data name="DetailsCommand_UnableToLoadResources" xml:space="preserve">
<value>Failed to load NuGet sources configured for the folder {0}</value>
</data>
<data name="DetailsCommand_UnableToLoadResource">
<data name="DetailsCommand_UnableToLoadResource" xml:space="preserve">
<value>Could not parse NuGet source '{0}', so it was discarded</value>
</data>
<data name="OperationCancelled" xml:space="preserve">
Expand Down Expand Up @@ -947,4 +944,10 @@ The header is followed by the list of parameters and their errors (might be seve
<value>Trusted</value>
<comment>information about NuGet package origin; if a package has PrefixReserved indicator </comment>
</data>
</root>
<data name="PostAction_ModifyJson_Error_ArgumentNotBoolean" xml:space="preserve">
<value>Post action argument '{0}' is not a valid boolean value.</value>
</data>
<data name="PostAction_ModifyJson_Verbose_AttemptingToFindJsonFile" xml:space="preserve">
<value>Attempting to find json file '{0}' in '{1}'</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;

using Microsoft.DotNet.Cli.Utils;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem;
Expand All @@ -12,6 +13,8 @@ namespace Microsoft.TemplateEngine.Cli.PostActionProcessors
{
internal class AddJsonPropertyPostActionProcessor : PostActionProcessorBase
{
private const string AllowFileCreationArgument = "allowFileCreation";
private const string AllowPathCreationArgument = "allowPathCreation";
Comment on lines +16 to +17
Copy link
Member Author

Choose a reason for hiding this comment

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

Split into 2 properties to allow users to be able to add the missing property path without forcing requirement to allow file creation.

There is some potential discussion here because it's not really making sense to allow file creation and not allow path creation so we could maybe have some kind of enum instead (e.g. CreationMode with None being the default, Path and File or FileAndPath). We could also keep the boolean and say the allowPathCreation is assumed to be true when allowFileCreation is set to true.

private const string JsonFileNameArgument = "jsonFileName";
private const string ParentPropertyPathArgument = "parentPropertyPath";
private const string NewJsonPropertyNameArgument = "newJsonPropertyName";
Expand Down Expand Up @@ -46,12 +49,25 @@ protected override bool ProcessInternal(
return false;
}

IReadOnlyList<string> jsonFiles = FindFilesInCurrentProjectOrSolutionFolder(environment.Host.FileSystem, outputBasePath, matchPattern: jsonFileName, maxAllowedAboveDirectories: 1);
IReadOnlyList<string> jsonFiles = FindFilesInCurrentFolderOrParentFolder(environment.Host.FileSystem, outputBasePath, jsonFileName);

if (jsonFiles.Count == 0)
{
Reporter.Error.WriteLine(LocalizableStrings.PostAction_ModifyJson_Error_NoJsonFile);
return false;
if (!bool.TryParse(action.Args.GetValueOrDefault(AllowFileCreationArgument, "false"), out bool createFile))
{
Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_ArgumentNotBoolean, AllowFileCreationArgument));
return false;
}

if (!createFile)
{
Reporter.Error.WriteLine(LocalizableStrings.PostAction_ModifyJson_Error_NoJsonFile);
return false;
}

string newJsonFilePath = Path.Combine(outputBasePath, jsonFileName);
environment.Host.FileSystem.WriteAllText(newJsonFilePath, "{}");
jsonFiles = new List<string> { newJsonFilePath };
}

if (jsonFiles.Count > 1)
Expand All @@ -73,7 +89,8 @@ protected override bool ProcessInternal(
newJsonElementProperties!.ParentProperty,
":",
newJsonElementProperties.NewJsonPropertyName,
newJsonElementProperties.NewJsonPropertyValue);
newJsonElementProperties.NewJsonPropertyValue,
action);

if (newJsonContent == null)
{
Expand All @@ -87,7 +104,7 @@ protected override bool ProcessInternal(
return true;
}

private static JsonNode? AddElementToJson(IPhysicalFileSystem fileSystem, string targetJsonFile, string? propertyPath, string propertyPathSeparator, string newJsonPropertyName, string newJsonPropertyValue)
private static JsonNode? AddElementToJson(IPhysicalFileSystem fileSystem, string targetJsonFile, string? propertyPath, string propertyPathSeparator, string newJsonPropertyName, string newJsonPropertyValue, IPostAction action)
{
JsonNode? jsonContent = JsonNode.Parse(fileSystem.ReadAllText(targetJsonFile), nodeOptions: null, documentOptions: DeserializerOptions);

Expand All @@ -96,7 +113,13 @@ protected override bool ProcessInternal(
return null;
}

JsonNode? parentProperty = FindJsonNode(jsonContent, propertyPath, propertyPathSeparator);
if (!bool.TryParse(action.Args.GetValueOrDefault(AllowPathCreationArgument, "false"), out bool createPath))
{
Reporter.Error.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Error_ArgumentNotBoolean, AllowPathCreationArgument));
return false;
}

JsonNode? parentProperty = FindJsonNode(jsonContent, propertyPath, propertyPathSeparator, createPath);

if (parentProperty == null)
{
Expand All @@ -116,7 +139,7 @@ protected override bool ProcessInternal(
return jsonContent;
}

private static JsonNode? FindJsonNode(JsonNode content, string? nodePath, string pathSeparator)
private static JsonNode? FindJsonNode(JsonNode content, string? nodePath, string pathSeparator, bool createPath)
{
if (nodePath == null)
{
Expand All @@ -134,18 +157,22 @@ protected override bool ProcessInternal(
return null;
}

node = node[property];
JsonNode? childNode = node[property];
if (childNode is null && createPath)
{
node[property] = childNode = new JsonObject();
}

node = childNode;
}

return node;
}

private static IReadOnlyList<string> FindFilesInCurrentProjectOrSolutionFolder(
private static string[] FindFilesInCurrentFolderOrParentFolder(
Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed to better match actual logic in place.

IPhysicalFileSystem fileSystem,
string startPath,
string matchPattern,
Func<string, bool>? secondaryFilter = null,
Copy link
Member Author

Choose a reason for hiding this comment

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

Unused.

int maxAllowedAboveDirectories = 250)
Copy link
Member Author

Choose a reason for hiding this comment

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

Was forced to 1 on the only calling place.

string matchPattern)
{
string? directory = fileSystem.DirectoryExists(startPath) ? startPath : Path.GetDirectoryName(startPath);

Expand All @@ -158,22 +185,20 @@ private static IReadOnlyList<string> FindFilesInCurrentProjectOrSolutionFolder(

do
{
List<string> filesInDir = fileSystem.EnumerateFileSystemEntries(directory, matchPattern, SearchOption.AllDirectories).ToList();
List<string> matches = new();

matches = secondaryFilter == null ? filesInDir : filesInDir.Where(x => secondaryFilter(x)).ToList();
Reporter.Verbose.WriteLine(string.Format(LocalizableStrings.PostAction_ModifyJson_Verbose_AttemptingToFindJsonFile, matchPattern, directory));
string[] filesInDir = fileSystem.EnumerateFileSystemEntries(directory, matchPattern, SearchOption.AllDirectories).ToArray();

if (matches.Count > 0)
if (filesInDir.Length > 0)
{
return matches;
return filesInDir;
}

directory = Path.GetPathRoot(directory) != directory ? Directory.GetParent(directory)?.FullName : null;
numberOfUpLevels++;
}
while (directory != null && numberOfUpLevels <= maxAllowedAboveDirectories);
while (directory != null && numberOfUpLevels <= 1);
Copy link
Member Author

Choose a reason for hiding this comment

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

It'd be good to have a better walk-up logic but I couldn't think of anything that would work for all/most cases.

  1. Walking up to first project found is problematic for templates adding project as we would never walk-up.
  2. Walking up to first sln found can lead to walking up to root if no sln exists
  3. Walking up to repo root is hard because we have nothing to understand where to stop (is it git folder? is it something else?).


return new List<string>();
return Array.Empty<string>();
}

private class JsonContentParameters
Expand Down

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

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

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

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

Loading