Skip to content

Commit

Permalink
feat(AzureDevOps.WorkItemClone.ConsoleUI): add support for YAML confi…
Browse files Browse the repository at this point in the history
…guration files

refactor(AzureDevOps.WorkItemClone.ConsoleUI): improve file handling and error messages
fix(AzureDevOps.WorkItemClone.ConsoleUI): ensure file extension matches selected format

feat(WorkItemInitCommand.cs): add support for YAML config format
refactor(WorkItemInitCommand.cs): replace direct file operations with FileStore methods for better abstraction
chore(launchSettings.json): update commandLineArgs to use YAML config
chore(configuration.json): remove unnecessary newline at end of file
feat(configuration.yaml): add new YAML configuration file for testing YAML config support
  • Loading branch information
MrHinsh committed Jul 17, 2024
1 parent e1aa120 commit 72dc594
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="Spectre.Console.ImageSharp" Version="0.49.1" />
<PackageReference Include="YamlDotNet" Version="16.0.0" />
</ItemGroup>

<ItemGroup>
Expand All @@ -40,6 +41,9 @@
<None Update="configuration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="configuration.yaml">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Resources\ADO_TESTProjPipline_V03.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text;
using Newtonsoft.Json;
using System.Threading.Tasks;
using YamlDotNet.Serialization;

namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
Expand All @@ -14,7 +15,7 @@ internal class BaseCommandSettings : CommandSettings
[Description("Pre configure paramiters using this config file. Run `Init` to create it.")]
[CommandOption("--config|--configFile")]
[DefaultValue("configuration.json")]
[JsonIgnore]
[JsonIgnore, YamlIgnore]
public string? configFile { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@
using System.Threading.Tasks;
using System.Diagnostics.Eventing.Reader;
using AzureDevOps.WorkItemClone.Repositories;
using YamlDotNet.Serialization;

namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
internal class WorkItemCloneCommand : WorkItemCommandBase<WorkItemCloneCommandSettings>
{
public override async Task<int> ExecuteAsync(CommandContext context, WorkItemCloneCommandSettings settingsFromCmd)
{
if (!FileStoreCheckExtensionMatchesFormat(settingsFromCmd.configFile, settingsFromCmd.ConfigFormat))
{
AnsiConsole.MarkupLine($"[bold red]The file extension of {settingsFromCmd.configFile} does not match the format {settingsFromCmd.ConfigFormat.ToString()} selected! Please rerun with the correct format You can use --configFormat JSON or update your file to YAML[/]");
return -1;
}
WorkItemCloneCommandSettings config = null;
if (File.Exists(settingsFromCmd.configFile))
if (FileStoreExist(settingsFromCmd.configFile, settingsFromCmd.ConfigFormat))
{
config = LoadWorkItemCloneCommandSettingsFromFile(settingsFromCmd.configFile);
config = FileStoreLoad<WorkItemCloneCommandSettings>(settingsFromCmd.configFile, settingsFromCmd.ConfigFormat);
}
if (config == null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
using Spectre.Console.Cli;
using System.ComponentModel;
using Newtonsoft.Json;
using YamlDotNet.Serialization;

namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
public enum ConfigFormats
{
JSON,
YAML
}

internal class WorkItemCloneCommandSettings : BaseCommandSettings
{
//------------------------------------------------
[Description("Execute with no user interaction required.")]
[CommandOption("--NonInteractive")]
[JsonIgnore]
[JsonIgnore, YamlIgnore]
public bool NonInteractive { get; set; }
[Description("Clear any cache if there is any")]
[CommandOption("--ClearCache")]
[JsonIgnore]
[JsonIgnore, YamlIgnore]
public bool ClearCache { get; set; }
[Description("Use this run name to execute. This will create a unique folder under the CachePath for storing run specific data and status. Defaults to yyyyyMMddHHmmss.")]
[CommandOption("--RunName")]
[JsonIgnore]
[JsonIgnore, YamlIgnore]
public string? RunName { get; set; }
[Description("Use this run name to execute. This will create a unique folder under the CachePath for storing run specific data and status. Defaults to yyyyyMMddHHmmss.")]
[CommandOption("--configFormat")]
[DefaultValue(ConfigFormats.JSON)]
[JsonIgnore, YamlIgnore]
public ConfigFormats ConfigFormat { get; set; }
//------------------------------------------------
[CommandOption("--outputPath|--cachePath")]
[DefaultValue("./cache")]
Expand Down
88 changes: 69 additions & 19 deletions AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization;
using Microsoft.SqlServer.Server;

namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
Expand All @@ -23,7 +26,7 @@ internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings se
config.ClearCache = settings.ClearCache;
config.RunName = settings.RunName != null ? settings.RunName : DateTime.Now.ToString("yyyyyMMddHHmmss");
config.configFile = EnsureFileAskIfMissing(config.configFile = settings.configFile != null ? settings.configFile : config.configFile, "Where is the config file to load?");
config.controlFile = EnsureFileAskIfMissing(config.controlFile = settings.controlFile != null ? settings.controlFile : config.controlFile, "Where is the JSON File?");
config.controlFile = EnsureFileAskIfMissing(config.controlFile = settings.controlFile != null ? settings.controlFile : config.controlFile, "Where is the Control File?");
config.CachePath = EnsureFolderAskIfMissing(config.CachePath = settings.CachePath != null ? settings.CachePath : config.CachePath, "What is the cache path?");

config.templateOrganization = EnsureStringAskIfMissing(config.templateOrganization = settings.templateOrganization != null ? settings.templateOrganization : config.templateOrganization, "What is the template organisation?");
Expand Down Expand Up @@ -137,16 +140,73 @@ internal string EnsureStringAskIfMissing(string value, string message)
}


internal ConfigurationSettings LoadConfigFile(string? configFile)
public TypeToLoad FileStoreLoad<TypeToLoad>(string configFile, ConfigFormats format)
{
ConfigurationSettings configSettings = System.Text.Json.JsonSerializer.Deserialize<ConfigurationSettings>(System.IO.File.ReadAllText(configFile));
return configSettings;
TypeToLoad loadedFromFile;
configFile = FileStoreEnsureExtension(configFile, format);
if (!System.IO.File.Exists(configFile))
{
AnsiConsole.MarkupLine("[red]Error:[/] No file was found.");
throw new Exception(configFile + " not found.");
}
string content = System.IO.File.ReadAllText(configFile);
switch (format)
{
case ConfigFormats.JSON:
loadedFromFile = JsonConvert.DeserializeObject<TypeToLoad>(content);
break;
case ConfigFormats.YAML:
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
loadedFromFile = deserializer.Deserialize<TypeToLoad>(content); /* compile error */
break;
default:
throw new Exception("Unknown format");
}
return loadedFromFile;
}

public bool FileStoreExist(string? configFile, ConfigFormats format)
{
return System.IO.File.Exists(FileStoreEnsureExtension(configFile, format));
}
public bool FileStoreCheckExtensionMatchesFormat(string? configFile, ConfigFormats format)
{
if (Path.GetExtension(configFile).ToLower() != $".{format.ToString().ToLower()}")
{
return false;
}
return true;
}

public string FileStoreEnsureExtension(string? configFile, ConfigFormats format)
{
if (Path.GetExtension(configFile).ToLower() != $".{format.ToString().ToLower()}")
{
var original = configFile;
configFile = Path.ChangeExtension(configFile, format.ToString().ToLower());
AnsiConsole.MarkupLine($"[green]Info:[/] Changed name of {original} to {configFile} ");
}
return configFile;
}

internal WorkItemCloneCommandSettings LoadWorkItemCloneCommandSettingsFromFile(string? configFile)
public string FileStoreSave<TypeToSave>(string configFile, TypeToSave content , ConfigFormats format)
{
WorkItemCloneCommandSettings configSettings = System.Text.Json.JsonSerializer.Deserialize<WorkItemCloneCommandSettings>(System.IO.File.ReadAllText(configFile));
return configSettings;
string output;
switch (format)
{
case ConfigFormats.JSON:
output = JsonConvert.SerializeObject(content, Formatting.Indented);
break;
case ConfigFormats.YAML:
var serializer = new SerializerBuilder().Build();
output = serializer.Serialize(content);
break;
default:
throw new Exception("Unknown format");
}
configFile = FileStoreEnsureExtension(configFile, format);
System.IO.File.WriteAllText(configFile, output);
return output;
}

internal string EnsureFileAskIfMissing(string? filename, string message = "What file should we load?")
Expand All @@ -155,17 +215,12 @@ internal string EnsureFileAskIfMissing(string? filename, string message = "What
{

filename = AnsiConsole.Prompt(
new TextPrompt<string>("Where is the config File?")
new TextPrompt<string>(message)
.Validate(configFile
=> !string.IsNullOrWhiteSpace(configFile) && System.IO.File.Exists(configFile)
=> !string.IsNullOrWhiteSpace(configFile)
? ValidationResult.Success()
: ValidationResult.Error("[yellow]Invalid config file[/]")));
}
if (!System.IO.File.Exists(filename))
{
AnsiConsole.MarkupLine("[red]Error:[/] No file was found.");
throw new Exception(filename + " not found.");
}
return filename;
}

Expand Down Expand Up @@ -197,11 +252,6 @@ internal string EnsureConfigFileAskIfMissing(string? configFile)
? ValidationResult.Success()
: ValidationResult.Error("[yellow]Invalid config file[/]")));
}
if (!System.IO.File.Exists(configFile))
{
AnsiConsole.MarkupLine("[red]Error:[/] No JSON file was found.");
throw new Exception(configFile + " not found.");
}
return configFile;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,30 @@
using Spectre.Console;
using Spectre.Console.Cli;
using System.Linq;
using Microsoft.SqlServer.Server;

namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
internal class WorkItemInitCommand : WorkItemCommandBase<WorkItemCloneCommandSettings>
{
public override async Task<int> ExecuteAsync(CommandContext context, WorkItemCloneCommandSettings settings)
{

if (!FileStoreCheckExtensionMatchesFormat(settings.configFile, settings.ConfigFormat))
{
AnsiConsole.MarkupLine($"[bold red]The file extension of {settings.configFile} does not match the format {settings.ConfigFormat.ToString()} selected! Please rerun with the correct format You can use --configFormat JSON or update your file to YAML[/]");
return -1;
}
var configFile = EnsureConfigFileAskIfMissing(settings.configFile);
WorkItemCloneCommandSettings config = null;
if (File.Exists(configFile))
if (FileStoreExist(configFile, settings.ConfigFormat))
{
var proceedWithSettings = AnsiConsole.Prompt(
new SelectionPrompt<bool> { Converter = value => value ? "Yes" : "No" }
.Title("The config file name used exists would you like to load this one?")
.AddChoices(true, false));
if (proceedWithSettings)
{
config = LoadWorkItemCloneCommandSettingsFromFile(configFile);
config = FileStoreLoad<WorkItemCloneCommandSettings>(configFile, settings.ConfigFormat);
}
}
if (config == null)
Expand All @@ -34,12 +39,9 @@ public override async Task<int> ExecuteAsync(CommandContext context, WorkItemClo

WriteOutSettings(config);

FileStoreSave(configFile, config, settings.ConfigFormat);



System.IO.File.WriteAllText(configFile, JsonConvert.SerializeObject(config, Formatting.Indented));

AnsiConsole.WriteLine("Settings saved to {configFile}!");
AnsiConsole.WriteLine($"Settings saved to {configFile}!");

return 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"Clone": {
"commandName": "Project",
"commandLineArgs": "clone --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration-test.json --jsonFile ..\\..\\..\\..\\..\\TestData\\tst_jsonj_export_v20.json --NonInteractive"
"commandLineArgs": "clone --configFormat YAML --cachePath ..\\..\\..\\..\\..\\.cache\\ --configFile ..\\..\\..\\..\\..\\.cache\\configuration-test.yaml --jsonFile ..\\..\\..\\..\\..\\TestData\\tst_jsonj_export_v20.json --NonInteractive "
},
"empty": {
"commandName": "Project"
Expand Down
1 change: 0 additions & 1 deletion AzureDevOps.WorkItemClone.ConsoleUI/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@
"targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID",
"targetQueryTitle": "Project-@RunName - @projectTitle",
"targetQueryFolder": "Shared Queries"

}
40 changes: 40 additions & 0 deletions AzureDevOps.WorkItemClone.ConsoleUI/configuration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
CachePath: "./cache"
controlFile: "ADO_TESTProjPipline_V03.json"
targetAccessToken": null
targetOrganization": "nkdagility-preview"
targetProject": "ABB-Demo"
targetParentId": 540
templateAccessToken": null
templateOrganization": "ABB-MO-ATE"
templateProject": "ABB Traction Template"
templateParentId": 212315
targetQuery": |
SELECT
[Custom.Product],
[System.Title],
[System.Description],
[Custom.DeadlineDate],
[System.AreaPath],
[System.AssignedTo],
[System.State],
[Custom.Notes],
[System.WorkItemType],
[Custom.TRA_Milestone]
FROM workitemLinks
WHERE
(
[Source].[System.Id] = 000000
OR [Source].[System.Parent] = 000000
OR [Source].[System.Tags] CONTAINS 'xxxx xxxx'
)
AND (
[System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward'
)
AND (
[Target].[System.Parent] = 000000
OR [Target].[System.Tags] CONTAINS 'xxxx xxxx'
)
ORDER BY [Custom.DeadlineDate]
MODE (Recursive)
targetQueryTitle": "Project-@RunName - @projectTitle"
targetQueryFolder": "Shared Queries"

0 comments on commit 72dc594

Please sign in to comment.