From 98c27a89bf7538516a8fa735fc664725f9afdea6 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 00:25:54 -0700 Subject: [PATCH 01/15] Add test for boolean values --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 901dd3e..244ff29 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ resources: ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License. See the [LICENSE](https://github.com/richardsondev/AggregateConfigBuildTask/blob/main/LICENSE) file for details. ## Contributing From a469a53ee30904292e1de5c2a74b6eb277ff7be3 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 00:28:12 -0700 Subject: [PATCH 02/15] Add test for boolean values --- src/UnitTests/TaskTestBase.cs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index 4f186c4..4782b64 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -283,5 +283,37 @@ public void ShouldHandleInvalidYamlFormat() // Assert: Verify the task fails due to invalid YAML Assert.IsFalse(result); } + + [TestMethod] + [Description("Test that boolean input values are correctly treated as booleans in the output.")] + public void ShouldCorrectlyParseBooleanValues() + { + // Arrange: Prepare sample YAML data. + mockFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + options: + - name: 'Option 1' + description: 'First option' + isEnabled: true"); + + var task = new AggregateConfig(mockFileSystem) + { + InputDirectory = testPath, + OutputFile = testPath + @"\output.json", + OutputType = OutputTypeEnum.Arm.ToString(), + AddSourceProperty = true + }; + task.BuildEngine = Mock.Of(); + + // Act: Execute the task + bool result = task.Execute(); + + // Assert: Verify additional properties are included in ARM output + Assert.IsTrue(result); + string output = mockFileSystem.ReadAllText($"{testPath}\\output.json"); + var armTemplate = JsonConvert.DeserializeObject>(output); + JObject parameters = (JObject)armTemplate["parameters"]; + Assert.AreEqual("Boolean", parameters.GetValue("options")["value"].First()["isEnabled"].Type.ToString()); + Assert.AreEqual(true, parameters.GetValue("options")["value"].First()["isEnabled"].Value()); + } } } From 3c502d24ae6ccccdf47ef2c7902bb8488042c61b Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 00:32:39 -0700 Subject: [PATCH 03/15] Add local validation script --- tools/localreleasevalidation.ps1 | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tools/localreleasevalidation.ps1 diff --git a/tools/localreleasevalidation.ps1 b/tools/localreleasevalidation.ps1 new file mode 100644 index 0000000..8a28624 --- /dev/null +++ b/tools/localreleasevalidation.ps1 @@ -0,0 +1,52 @@ +# Windows E2E local release validation + +# Step 1: Set up paths +$solutionPath = "src\AggregateConfigBuildTask.sln" +$testProjectPath = "test\IntegrationTests\IntegrationTests.csproj" +$nupkgPath = "src\Task\bin\Release\AggregateConfigBuildTask.1.0.1.nupkg" +$localNugetDir = ($env:APPDATA + "\Roaming\NuGet\nuget\local") +$nugetSourceName = "AggregateConfigBuildTask" + +# Step 2: Restore NuGet packages for AggregateConfigBuildTask.sln +Write-Host "Restoring NuGet packages for $solutionPath..." +dotnet restore $solutionPath + +# Step 3: Build the src/AggregateConfigBuildTask.sln project in Release mode +Write-Host "Building $solutionPath in Release mode..." +dotnet build $solutionPath --configuration Release -warnaserror + +# Step 4: Run tests for AggregateConfigBuildTask.sln +Write-Host "Running tests for $solutionPath..." +dotnet test $solutionPath --configuration Release + +# Step 5: Copy the nupkg to a common location +Write-Host "Copying .nupkg to the local NuGet folder..." +if (-Not (Test-Path $localNugetDir)) { + New-Item -Path $localNugetDir -ItemType Directory +} +Copy-Item $nupkgPath -Destination $localNugetDir -Force + +# Step 6: Remove existing AggregateConfigBuildTask NuGet source if it exists +$existingSource = dotnet nuget list source | Select-String -Pattern $nugetSourceName +if ($existingSource) { + Write-Host "Removing existing '$nugetSourceName' NuGet source..." + dotnet nuget remove source $nugetSourceName +} + +# Step 7: Add the local NuGet source for the integration tests +Write-Host "Adding the local NuGet source with the name '$nugetSourceName'..." +dotnet nuget add source $localNugetDir --name $nugetSourceName + +# Step 8: Restore NuGet packages for the integration tests project +Write-Host "Restoring NuGet packages for $testProjectPath..." +dotnet restore $testProjectPath + +# Step 9: Build the integration tests project in Release mode +Write-Host "Building $testProjectPath in Release mode..." +dotnet build $testProjectPath --configuration Release -warnaserror + +# Step 10: Run the integration tests +Write-Host "Running the integration tests for $testProjectPath..." +dotnet test $testProjectPath --configuration Release + +Write-Host "All steps completed successfully." From 720dfab8685643194b17ce16e9211e586cf91a31 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 01:46:39 -0700 Subject: [PATCH 04/15] WIP Input type and boolean parsing --- README.md | 6 ++- src/Contracts/OutputTypeEnum.cs | 7 +++ src/Task/AggregateConfig.cs | 52 +++++++++++++------ .../Converters/BooleanYamlTypeConverter.cs | 51 ++++++++++++++++++ src/Task/Writers/IInputReader.cs | 7 +++ src/Task/Writers/OutputWriterFactory.cs | 27 ++++++++++ src/Task/Writers/YamlOutputWriter.cs | 15 +++++- 7 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 src/Task/Converters/BooleanYamlTypeConverter.cs create mode 100644 src/Task/Writers/IInputReader.cs diff --git a/README.md b/README.md index 244ff29..a4e5b82 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## Features -- Merge multiple YAML configuration files into a single output format (JSON, Azure ARM parameters, or YAML). +- Merge multiple configuration files into a single output format (JSON, Azure ARM parameters, or YAML). - Support for injecting custom metadata (e.g., `ResourceGroup`, `Environment`) into the output. - Optionally include the source file name in each configuration entry. - Embed output files as resources in the assembly for easy inclusion in your project. @@ -54,6 +54,7 @@ In your `.csproj` file, use the task to aggregate YAML files and output them in InputDirectory="Configs" OutputFile="$(MSBuildProjectDirectory)\out\output.json" AddSourceProperty="true" + InputType="Yaml" OutputType="Json" AdditionalProperties="@(AdditionalProperty)" /> @@ -175,6 +176,9 @@ In this example: - `Json`: Outputs a regular JSON file. - `ArmParameter`: Outputs an Azure ARM template parameter file. - `Yaml`: Outputs a YAML file. +- **InputType** *(optional, default=YAML)*: Determines the input format. Supported values: + - `Json`: Inputs are JSON files with a `.json` extension. + - `Yaml`: Inputs are YAML files with a `.yml` or `.yaml` extension. - **AdditionalProperties** *(optional)*: A collection of custom top-level properties to inject into the final output. Use the `ItemGroup` syntax to pass key-value pairs. ## Example YAML Input diff --git a/src/Contracts/OutputTypeEnum.cs b/src/Contracts/OutputTypeEnum.cs index ff57805..ef784c5 100644 --- a/src/Contracts/OutputTypeEnum.cs +++ b/src/Contracts/OutputTypeEnum.cs @@ -7,4 +7,11 @@ public enum OutputTypeEnum Yml, Yaml = Yml } + + public enum InputTypeEnum + { + Json, + Yml, + Yaml = Yml + } } diff --git a/src/Task/AggregateConfig.cs b/src/Task/AggregateConfig.cs index 24831bf..f67f130 100644 --- a/src/Task/AggregateConfig.cs +++ b/src/Task/AggregateConfig.cs @@ -2,6 +2,7 @@ using System.IO; using System.Collections.Generic; using System.Linq; +using AggregateConfig.Converters; using AggregateConfig.Writers; using AggregateConfig.Contracts; using Microsoft.Build.Framework; @@ -21,6 +22,9 @@ public class AggregateConfig : Task [Required] public string InputDirectory { get; set; } + [Required] + public string InputType { get; set; } + [Required] public string OutputFile { get; set; } @@ -57,38 +61,56 @@ public override bool Execute() return false; } + if (string.IsNullOrEmpty(InputType) || !Enum.TryParse(InputType, out InputTypeEnum inputType)) + { + inputType = InputTypeEnum.Yaml; + } + + if (!Enum.IsDefined(typeof(InputTypeEnum), inputType)) + { + Console.Error.WriteLine("Invalid InputType."); + return false; + } + string directoryPath = Path.GetDirectoryName(OutputFile); if (!fileSystem.DirectoryExists(directoryPath)) { fileSystem.CreateDirectory(directoryPath); } - var yamlFiles = fileSystem.GetFiles(InputDirectory, "*.yml"); - foreach (var yamlFile in yamlFiles) - { - var yamlContent = fileSystem.ReadAllText(yamlFile); - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(yamlFile); + var expectedExtensions = OutputWriterFactory.GetExpectedFileExtensions(inputType); + var files = fileSystem.GetFiles(InputDirectory, "*.*") + .Where(file => expectedExtensions.Contains(Path.GetExtension(file).ToLower())) + .ToList(); - // Deserialize the YAML content - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - object yamlData = null; + foreach (var file in files) + { + IInputReader outputWriter; + try + { + outputWriter = OutputWriterFactory.GetInputReader(fileSystem, inputType); + } + catch (ArgumentException ex) + { + hasError = true; + Console.Error.WriteLine($"No reader found for file {file}: {ex.Message}"); + continue; + } + object fileData; try { - yamlData = deserializer.Deserialize(yamlContent); + fileData = outputWriter.ReadInput(file); } catch (Exception ex) { hasError = true; - Console.Error.WriteLine($"Could not parse {yamlFile}: {ex.Message}"); + Console.Error.WriteLine($"Could not parse {file}: {ex.Message}"); continue; } - // Merge the deserialized YAML object into the final result - finalResult = MergeYamlObjects(finalResult, yamlData, yamlFile, AddSourceProperty); + // Merge the deserialized object into the final result + finalResult = MergeYamlObjects(finalResult, fileData, file, AddSourceProperty); } if (hasError) diff --git a/src/Task/Converters/BooleanYamlTypeConverter.cs b/src/Task/Converters/BooleanYamlTypeConverter.cs new file mode 100644 index 0000000..91b5e05 --- /dev/null +++ b/src/Task/Converters/BooleanYamlTypeConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace AggregateConfig.Converters +{ + public class BooleanYamlTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return typeof(bool).IsAssignableFrom(type); + } + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + // Check if the current token is a scalar (simple value) and can be processed + if (parser.Current is Scalar scalar) + { + var value = scalar.Value.ToLower(); + + // Process boolean-like values + if (value == "true" || value == "yes" || value == "on") + { + return true; + } + else if (value == "false" || value == "no" || value == "off") + { + return false; + } + } + + // For all other cases, delegate to the default deserialization + return parser.MoveNext(); + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) + { + // Only handle boolean serialization, delegate other cases to serializer + if (value is bool boolValue) + { + emitter.Emit(new Scalar(boolValue ? "true" : "false")); + } + else + { + serializer(value, type); + } + } + } +} diff --git a/src/Task/Writers/IInputReader.cs b/src/Task/Writers/IInputReader.cs new file mode 100644 index 0000000..a85ad28 --- /dev/null +++ b/src/Task/Writers/IInputReader.cs @@ -0,0 +1,7 @@ +namespace AggregateConfig.Writers +{ + public interface IInputReader + { + object ReadInput(string inputPath); + } +} diff --git a/src/Task/Writers/OutputWriterFactory.cs b/src/Task/Writers/OutputWriterFactory.cs index 14e2b15..62dabe3 100644 --- a/src/Task/Writers/OutputWriterFactory.cs +++ b/src/Task/Writers/OutputWriterFactory.cs @@ -1,5 +1,6 @@ using AggregateConfig.Contracts; using System; +using System.Collections.Generic; namespace AggregateConfig.Writers { @@ -19,5 +20,31 @@ internal static IOutputWriter GetOutputWriter(IFileSystem fileSystem, OutputType throw new ArgumentException("Unsupported format"); } } + + internal static IInputReader GetInputReader(IFileSystem fileSystem, InputTypeEnum format) + { + switch (format) + { + case InputTypeEnum.Yaml: + return new YamlOutputWriter(fileSystem); + default: + throw new ArgumentException("Unsupported input format"); + } + } + + internal static List GetExpectedFileExtensions(InputTypeEnum inputType) + { + switch (inputType) + { + case InputTypeEnum.Json: + return new List { ".json" }; + + case InputTypeEnum.Yaml: + return new List { ".yml", ".yaml" }; + + default: + throw new ArgumentException("Unsupported input type"); + } + } } } diff --git a/src/Task/Writers/YamlOutputWriter.cs b/src/Task/Writers/YamlOutputWriter.cs index b13416b..54157e1 100644 --- a/src/Task/Writers/YamlOutputWriter.cs +++ b/src/Task/Writers/YamlOutputWriter.cs @@ -3,7 +3,7 @@ namespace AggregateConfig.Writers { - public class YamlOutputWriter : IOutputWriter + public class YamlOutputWriter : IOutputWriter, IInputReader { IFileSystem fileSystem; @@ -12,6 +12,19 @@ internal YamlOutputWriter(IFileSystem fileSystem) this.fileSystem = fileSystem; } + public object ReadInput(string inputPath) + { + var yamlContent = fileSystem.ReadAllText(inputPath); + + // Deserialize the YAML content + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + //.WithTypeConverter(new BooleanYamlTypeConverter()) + .Build(); + + return deserializer.Deserialize(yamlContent); + } + public void WriteOutput(object mergedData, string outputPath) { var serializer = new SerializerBuilder() From 38ea872931b5becf6b01f1e6ce2fee79dc4d6a00 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 01:53:47 -0700 Subject: [PATCH 05/15] Add samples to tests and fix doc issue --- README.md | 4 +- .../IntegrationTests/EmbeddedResourceTests.cs | 4 +- test/IntegrationTests/IntegrationTests.csproj | 66 +++++++++++++++++-- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a4e5b82..c0eb558 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ You can also generate Azure ARM template parameters. Here's how to modify the co @@ -174,7 +174,7 @@ In this example: - **AddSourceProperty** *(optional, default=false)*: Adds a `source` property to each object in the output, indicating the YAML file it originated from. - **OutputType** *(required)*: Determines the output format. Supported values: - `Json`: Outputs a regular JSON file. - - `ArmParameter`: Outputs an Azure ARM template parameter file. + - `Arm`: Outputs an Azure ARM template parameter file. - `Yaml`: Outputs a YAML file. - **InputType** *(optional, default=YAML)*: Determines the input format. Supported values: - `Json`: Inputs are JSON files with a `.json` extension. diff --git a/test/IntegrationTests/EmbeddedResourceTests.cs b/test/IntegrationTests/EmbeddedResourceTests.cs index 21b1300..c209828 100644 --- a/test/IntegrationTests/EmbeddedResourceTests.cs +++ b/test/IntegrationTests/EmbeddedResourceTests.cs @@ -9,8 +9,8 @@ namespace AggregateConfig.Tests.Integration public class EmbeddedResourceTests { [TestMethod] - [DataRow("IntegrationTests.out.output.json")] - [DataRow("IntegrationTests.out.output.parameters.json")] + [DataRow("IntegrationTests.out.test.json")] + [DataRow("IntegrationTests.out.test.parameters.json")] public void ReadEmbeddedResource_DeserializesJsonSuccessfully(string resourceName) { // Arrange diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj index e2c1f3e..3abff14 100644 --- a/test/IntegrationTests/IntegrationTests.csproj +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -34,14 +34,14 @@ @@ -53,16 +53,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - From 83d2e2fa997b105ffe0eea7e0ebaba8ed6206dc6 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 02:07:12 -0700 Subject: [PATCH 06/15] wip implement json input --- src/Task/AggregateConfig.cs | 51 +++++++++++++++++++++++++ src/Task/Writers/JsonOutputWriter.cs | 11 +++++- src/Task/Writers/OutputWriterFactory.cs | 2 + src/UnitTests/TaskTestBase.cs | 45 ++++++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/Task/AggregateConfig.cs b/src/Task/AggregateConfig.cs index f67f130..2afe437 100644 --- a/src/Task/AggregateConfig.cs +++ b/src/Task/AggregateConfig.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization; +using System.Text.Json; [assembly: InternalsVisibleTo("AggregateConfig.Tests.UnitTests")] @@ -135,6 +136,17 @@ public override bool Execute() finalDictionary.Add(property.Key, property.Value); } } + else if (finalResult is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object) + { + var jsonDictionary = JsonElementToDictionary(jsonElement); + + foreach (var property in additionalPropertiesDictionary) + { + jsonDictionary[property.Key] = property.Value; + } + + finalResult = jsonDictionary; + } else { Console.Error.WriteLine($"Additional properties could not be injected since the top-level is not a dictionary."); @@ -287,5 +299,44 @@ private Dictionary ParseAdditionalProperties(string[] properties } return additionalPropertiesDict; } + + private Dictionary JsonElementToDictionary(JsonElement element) + { + var dictionary = new Dictionary(); + + foreach (var property in element.EnumerateObject()) + { + dictionary[property.Name] = ConvertJsonElementToObject(property.Value); + } + + return dictionary; + } + + private object ConvertJsonElementToObject(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + return JsonElementToDictionary(element); + case JsonValueKind.Array: + var list = new List(); + foreach (var item in element.EnumerateArray()) + { + list.Add(ConvertJsonElementToObject(item)); + } + return list; + case JsonValueKind.String: + return element.GetString(); + case JsonValueKind.Number: + return element.TryGetInt64(out long l) ? l : element.GetDouble(); + case JsonValueKind.True: + case JsonValueKind.False: + return element.GetBoolean(); + case JsonValueKind.Null: + return null; + default: + throw new InvalidOperationException($"Unsupported JsonValueKind: {element.ValueKind}"); + } + } } } diff --git a/src/Task/Writers/JsonOutputWriter.cs b/src/Task/Writers/JsonOutputWriter.cs index 0643cb9..de06398 100644 --- a/src/Task/Writers/JsonOutputWriter.cs +++ b/src/Task/Writers/JsonOutputWriter.cs @@ -1,8 +1,9 @@ -using System.Text.Json; +using System; +using System.Text.Json; namespace AggregateConfig.Writers { - public class JsonOutputWriter : IOutputWriter + public class JsonOutputWriter : IOutputWriter, IInputReader { IFileSystem fileSystem; @@ -11,6 +12,12 @@ internal JsonOutputWriter(IFileSystem fileSystem) this.fileSystem = fileSystem; } + public object ReadInput(string inputPath) + { + var jsonContent = fileSystem.ReadAllText(inputPath); + return JsonSerializer.Deserialize(jsonContent); + } + public void WriteOutput(object mergedData, string outputPath) { var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; diff --git a/src/Task/Writers/OutputWriterFactory.cs b/src/Task/Writers/OutputWriterFactory.cs index 62dabe3..4b24486 100644 --- a/src/Task/Writers/OutputWriterFactory.cs +++ b/src/Task/Writers/OutputWriterFactory.cs @@ -27,6 +27,8 @@ internal static IInputReader GetInputReader(IFileSystem fileSystem, InputTypeEnu { case InputTypeEnum.Yaml: return new YamlOutputWriter(fileSystem); + case InputTypeEnum.Json: + return new JsonOutputWriter(fileSystem); default: throw new ArgumentException("Unsupported input format"); } diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index 4782b64..139b5c4 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -315,5 +315,50 @@ public void ShouldCorrectlyParseBooleanValues() Assert.AreEqual("Boolean", parameters.GetValue("options")["value"].First()["isEnabled"].Type.ToString()); Assert.AreEqual(true, parameters.GetValue("options")["value"].First()["isEnabled"].Value()); } + + [TestMethod] + [Description("Test that additional properties are correctly added to the ARM parameters output from JSON input.")] + public void ShouldIncludeAdditionalPropertiesInJsonInput() + { + // Arrange: Prepare sample JSON data. + mockFileSystem.WriteAllText($"{testPath}\\file1.json", @" + { + ""options"": [ + { + ""name"": ""Option 1"", + ""description"": ""First option"", + ""isEnabled"": true + } + ] + }"); + + var task = new AggregateConfig(mockFileSystem) + { + InputType = InputTypeEnum.Json.ToString(), + InputDirectory = testPath, + OutputFile = testPath + @"\output.json", + OutputType = OutputTypeEnum.Arm.ToString(), + AddSourceProperty = true, + AdditionalProperties = new Dictionary + { + { "Group", "TestRG" }, + { "Environment", "Prod" } + }.Select(q => $"{q.Key}={q.Value}").ToArray() + }; + task.BuildEngine = Mock.Of(); + + // Act: Execute the task + bool result = task.Execute(); + + // Assert: Verify additional properties are included in ARM output + Assert.IsTrue(result); + string output = mockFileSystem.ReadAllText($"{testPath}\\output.json"); + var armTemplate = JsonConvert.DeserializeObject>(output); + JObject parameters = (JObject)armTemplate["parameters"]; + Assert.AreEqual("TestRG", parameters.GetValue("Group")["value"].Value()); + Assert.AreEqual("Prod", parameters.GetValue("Environment")["value"].Value()); + Assert.AreEqual("Boolean", parameters.GetValue("options")["value"].First()["isEnabled"].Type.ToString()); + Assert.AreEqual(true, parameters.GetValue("options")["value"].First()["isEnabled"].Value()); + } } } From 9a7723563ab2914696b54343a222662654d0f348 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 02:10:35 -0700 Subject: [PATCH 07/15] Check for source in output --- src/UnitTests/TaskTestBase.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index 139b5c4..0d55c3f 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -299,8 +299,7 @@ public void ShouldCorrectlyParseBooleanValues() { InputDirectory = testPath, OutputFile = testPath + @"\output.json", - OutputType = OutputTypeEnum.Arm.ToString(), - AddSourceProperty = true + OutputType = OutputTypeEnum.Arm.ToString() }; task.BuildEngine = Mock.Of(); @@ -357,6 +356,8 @@ public void ShouldIncludeAdditionalPropertiesInJsonInput() JObject parameters = (JObject)armTemplate["parameters"]; Assert.AreEqual("TestRG", parameters.GetValue("Group")["value"].Value()); Assert.AreEqual("Prod", parameters.GetValue("Environment")["value"].Value()); + Assert.AreEqual("String", parameters.GetValue("options")["value"].First()["source"].Type.ToString()); + Assert.AreEqual("file1", parameters.GetValue("options")["value"].First()["source"].Value()); Assert.AreEqual("Boolean", parameters.GetValue("options")["value"].First()["isEnabled"].Type.ToString()); Assert.AreEqual(true, parameters.GetValue("options")["value"].First()["isEnabled"].Value()); } From f83122009c4fd27187ef9055068d0e4d31271ebb Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 02:21:28 -0700 Subject: [PATCH 08/15] Support ArmParameter --- src/Contracts/OutputTypeEnum.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Contracts/OutputTypeEnum.cs b/src/Contracts/OutputTypeEnum.cs index ef784c5..b951c56 100644 --- a/src/Contracts/OutputTypeEnum.cs +++ b/src/Contracts/OutputTypeEnum.cs @@ -4,6 +4,7 @@ public enum OutputTypeEnum { Json, Arm, + ArmParameter = Arm, Yml, Yaml = Yml } From 192e2c357a82d16bd95bbbf21e5808bebfeee10e Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 12:21:11 -0700 Subject: [PATCH 09/15] Code cleanup --- src/Task/AggregateConfig.cs | 20 ++++++++----------- .../Converters/BooleanYamlTypeConverter.cs | 1 - src/Task/Writers/JsonOutputWriter.cs | 3 +-- src/UnitTests/TaskTestBase.cs | 2 +- src/UnitTests/VirtualFileSystem.cs | 4 ++-- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Task/AggregateConfig.cs b/src/Task/AggregateConfig.cs index 2afe437..e64788c 100644 --- a/src/Task/AggregateConfig.cs +++ b/src/Task/AggregateConfig.cs @@ -1,16 +1,13 @@ -using System; -using System.IO; -using System.Collections.Generic; -using System.Linq; -using AggregateConfig.Converters; +using AggregateConfig.Contracts; using AggregateConfig.Writers; -using AggregateConfig.Contracts; using Microsoft.Build.Framework; -using Task = Microsoft.Build.Utilities.Task; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Runtime.CompilerServices; -using YamlDotNet.Serialization.NamingConventions; -using YamlDotNet.Serialization; using System.Text.Json; +using Task = Microsoft.Build.Utilities.Task; [assembly: InternalsVisibleTo("AggregateConfig.Tests.UnitTests")] @@ -23,17 +20,16 @@ public class AggregateConfig : Task [Required] public string InputDirectory { get; set; } - [Required] public string InputType { get; set; } [Required] public string OutputFile { get; set; } - public bool AddSourceProperty { get; set; } = false; - [Required] public string OutputType { get; set; } + public bool AddSourceProperty { get; set; } = false; + public string[] AdditionalProperties { get; set; } public AggregateConfig() diff --git a/src/Task/Converters/BooleanYamlTypeConverter.cs b/src/Task/Converters/BooleanYamlTypeConverter.cs index 91b5e05..a00715b 100644 --- a/src/Task/Converters/BooleanYamlTypeConverter.cs +++ b/src/Task/Converters/BooleanYamlTypeConverter.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; diff --git a/src/Task/Writers/JsonOutputWriter.cs b/src/Task/Writers/JsonOutputWriter.cs index de06398..7c964bc 100644 --- a/src/Task/Writers/JsonOutputWriter.cs +++ b/src/Task/Writers/JsonOutputWriter.cs @@ -1,5 +1,4 @@ -using System; -using System.Text.Json; +using System.Text.Json; namespace AggregateConfig.Writers { diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index 0d55c3f..916dbcc 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -1,8 +1,8 @@ using AggregateConfig.Contracts; using Microsoft.Build.Framework; using Moq; -using Newtonsoft.Json.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/UnitTests/VirtualFileSystem.cs b/src/UnitTests/VirtualFileSystem.cs index 623f843..371b219 100644 --- a/src/UnitTests/VirtualFileSystem.cs +++ b/src/UnitTests/VirtualFileSystem.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; From 565e2846a2878a3bec42016839aa1bd7e4c4ca55 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 12:54:09 -0700 Subject: [PATCH 10/15] Add third party notice --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c0eb558..b5fadbf 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,11 @@ resources: This project is licensed under the MIT License. See the [LICENSE](https://github.com/richardsondev/AggregateConfigBuildTask/blob/main/LICENSE) file for details. +## Third-Party Libraries + +This tool makes use of [YamlDotNet](https://github.com/aaubry/YamlDotNet) for YAML serialization and deserialization. +YamlDotNet is licensed under the MIT License. For more details, please refer to the [YamlDotNet License](https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt). + ## Contributing Contributions are welcome! Feel free to submit issues or pull requests on [GitHub](https://github.com/richardsondev/AggregateConfigBuildTask). From 459ce388d118d35cbffcdc7e7337c16a83eaa504 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 13:23:27 -0700 Subject: [PATCH 11/15] Prepare for 1.0.1 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b5fadbf..2db526d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ dotnet add package AggregateConfigBuildTask Alternatively, add the following line to your `.csproj` file: ```xml - + ``` ## Usage @@ -38,7 +38,7 @@ In your `.csproj` file, use the task to aggregate YAML files and output them in - + all native;contentFiles;analyzers;runtime @@ -76,7 +76,7 @@ You can also generate Azure ARM template parameters. Here's how to modify the co - + all native;contentFiles;analyzers;runtime @@ -106,7 +106,7 @@ You can also output the aggregated configuration back into YAML format: - + all native;contentFiles;analyzers;runtime @@ -136,7 +136,7 @@ You can embed the output files (such as the generated JSON) as resources in the - + all native;contentFiles;analyzers;runtime From 785a43f5566571ee66a32b20bdafe050929c3dd8 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 13:36:50 -0700 Subject: [PATCH 12/15] Rename output writers to file handlers --- src/Task/AggregateConfig.cs | 8 ++++---- .../ArmParametersFileHandler.cs} | 6 +++--- .../FileHandlerFactory.cs} | 14 +++++++------- src/Task/{Writers => FileHandlers}/IInputReader.cs | 2 +- .../{Writers => FileHandlers}/IOutputWriter.cs | 2 +- .../JsonFileHandler.cs} | 6 +++--- .../YamlFileHandler.cs} | 6 +++--- 7 files changed, 22 insertions(+), 22 deletions(-) rename src/Task/{Writers/ArmParametersOutputWriter.cs => FileHandlers/ArmParametersFileHandler.cs} (89%) rename src/Task/{Writers/OutputWriterFactory.cs => FileHandlers/FileHandlerFactory.cs} (75%) rename src/Task/{Writers => FileHandlers}/IInputReader.cs (65%) rename src/Task/{Writers => FileHandlers}/IOutputWriter.cs (70%) rename src/Task/{Writers/JsonOutputWriter.cs => FileHandlers/JsonFileHandler.cs} (77%) rename src/Task/{Writers/YamlOutputWriter.cs => FileHandlers/YamlFileHandler.cs} (84%) diff --git a/src/Task/AggregateConfig.cs b/src/Task/AggregateConfig.cs index e64788c..4a00bda 100644 --- a/src/Task/AggregateConfig.cs +++ b/src/Task/AggregateConfig.cs @@ -1,5 +1,5 @@ using AggregateConfig.Contracts; -using AggregateConfig.Writers; +using AggregateConfig.FileHandlers; using Microsoft.Build.Framework; using System; using System.Collections.Generic; @@ -75,7 +75,7 @@ public override bool Execute() fileSystem.CreateDirectory(directoryPath); } - var expectedExtensions = OutputWriterFactory.GetExpectedFileExtensions(inputType); + var expectedExtensions = FileHandlerFactory.GetExpectedFileExtensions(inputType); var files = fileSystem.GetFiles(InputDirectory, "*.*") .Where(file => expectedExtensions.Contains(Path.GetExtension(file).ToLower())) .ToList(); @@ -85,7 +85,7 @@ public override bool Execute() IInputReader outputWriter; try { - outputWriter = OutputWriterFactory.GetInputReader(fileSystem, inputType); + outputWriter = FileHandlerFactory.GetInputReader(fileSystem, inputType); } catch (ArgumentException ex) { @@ -150,7 +150,7 @@ public override bool Execute() } } - var writer = OutputWriterFactory.GetOutputWriter(fileSystem, outputType); + var writer = FileHandlerFactory.GetOutputWriter(fileSystem, outputType); writer.WriteOutput(finalResult, OutputFile); return true; diff --git a/src/Task/Writers/ArmParametersOutputWriter.cs b/src/Task/FileHandlers/ArmParametersFileHandler.cs similarity index 89% rename from src/Task/Writers/ArmParametersOutputWriter.cs rename to src/Task/FileHandlers/ArmParametersFileHandler.cs index 86fa311..6072c96 100644 --- a/src/Task/Writers/ArmParametersOutputWriter.cs +++ b/src/Task/FileHandlers/ArmParametersFileHandler.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Text.Json; -namespace AggregateConfig.Writers +namespace AggregateConfig.FileHandlers { - public class ArmParametersOutputWriter : IOutputWriter + public class ArmParametersFileHandler : IOutputWriter { IFileSystem fileSystem; - internal ArmParametersOutputWriter(IFileSystem fileSystem) + internal ArmParametersFileHandler(IFileSystem fileSystem) { this.fileSystem = fileSystem; } diff --git a/src/Task/Writers/OutputWriterFactory.cs b/src/Task/FileHandlers/FileHandlerFactory.cs similarity index 75% rename from src/Task/Writers/OutputWriterFactory.cs rename to src/Task/FileHandlers/FileHandlerFactory.cs index 4b24486..7175446 100644 --- a/src/Task/Writers/OutputWriterFactory.cs +++ b/src/Task/FileHandlers/FileHandlerFactory.cs @@ -2,20 +2,20 @@ using System; using System.Collections.Generic; -namespace AggregateConfig.Writers +namespace AggregateConfig.FileHandlers { - public static class OutputWriterFactory + public static class FileHandlerFactory { internal static IOutputWriter GetOutputWriter(IFileSystem fileSystem, OutputTypeEnum format) { switch (format) { case OutputTypeEnum.Json: - return new JsonOutputWriter(fileSystem); + return new JsonFileHandler(fileSystem); case OutputTypeEnum.Yaml: - return new YamlOutputWriter(fileSystem); + return new YamlFileHandler(fileSystem); case OutputTypeEnum.Arm: - return new ArmParametersOutputWriter(fileSystem); + return new ArmParametersFileHandler(fileSystem); default: throw new ArgumentException("Unsupported format"); } @@ -26,9 +26,9 @@ internal static IInputReader GetInputReader(IFileSystem fileSystem, InputTypeEnu switch (format) { case InputTypeEnum.Yaml: - return new YamlOutputWriter(fileSystem); + return new YamlFileHandler(fileSystem); case InputTypeEnum.Json: - return new JsonOutputWriter(fileSystem); + return new JsonFileHandler(fileSystem); default: throw new ArgumentException("Unsupported input format"); } diff --git a/src/Task/Writers/IInputReader.cs b/src/Task/FileHandlers/IInputReader.cs similarity index 65% rename from src/Task/Writers/IInputReader.cs rename to src/Task/FileHandlers/IInputReader.cs index a85ad28..affc305 100644 --- a/src/Task/Writers/IInputReader.cs +++ b/src/Task/FileHandlers/IInputReader.cs @@ -1,4 +1,4 @@ -namespace AggregateConfig.Writers +namespace AggregateConfig.FileHandlers { public interface IInputReader { diff --git a/src/Task/Writers/IOutputWriter.cs b/src/Task/FileHandlers/IOutputWriter.cs similarity index 70% rename from src/Task/Writers/IOutputWriter.cs rename to src/Task/FileHandlers/IOutputWriter.cs index c9f86bd..2c7c409 100644 --- a/src/Task/Writers/IOutputWriter.cs +++ b/src/Task/FileHandlers/IOutputWriter.cs @@ -1,4 +1,4 @@ -namespace AggregateConfig.Writers +namespace AggregateConfig.FileHandlers { public interface IOutputWriter { diff --git a/src/Task/Writers/JsonOutputWriter.cs b/src/Task/FileHandlers/JsonFileHandler.cs similarity index 77% rename from src/Task/Writers/JsonOutputWriter.cs rename to src/Task/FileHandlers/JsonFileHandler.cs index 7c964bc..c71462e 100644 --- a/src/Task/Writers/JsonOutputWriter.cs +++ b/src/Task/FileHandlers/JsonFileHandler.cs @@ -1,12 +1,12 @@ using System.Text.Json; -namespace AggregateConfig.Writers +namespace AggregateConfig.FileHandlers { - public class JsonOutputWriter : IOutputWriter, IInputReader + public class JsonFileHandler : IOutputWriter, IInputReader { IFileSystem fileSystem; - internal JsonOutputWriter(IFileSystem fileSystem) + internal JsonFileHandler(IFileSystem fileSystem) { this.fileSystem = fileSystem; } diff --git a/src/Task/Writers/YamlOutputWriter.cs b/src/Task/FileHandlers/YamlFileHandler.cs similarity index 84% rename from src/Task/Writers/YamlOutputWriter.cs rename to src/Task/FileHandlers/YamlFileHandler.cs index 54157e1..cf853e2 100644 --- a/src/Task/Writers/YamlOutputWriter.cs +++ b/src/Task/FileHandlers/YamlFileHandler.cs @@ -1,13 +1,13 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace AggregateConfig.Writers +namespace AggregateConfig.FileHandlers { - public class YamlOutputWriter : IOutputWriter, IInputReader + public class YamlFileHandler : IOutputWriter, IInputReader { IFileSystem fileSystem; - internal YamlOutputWriter(IFileSystem fileSystem) + internal YamlFileHandler(IFileSystem fileSystem) { this.fileSystem = fileSystem; } From f6fe4bc66d782a2a4156b06fe9ce23b5eb297813 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 13:38:38 -0700 Subject: [PATCH 13/15] Cleanup readme --- README.md | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 2db526d..1591999 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,10 @@ dotnet add package AggregateConfigBuildTask Alternatively, add the following line to your `.csproj` file: ```xml - + + all + native;contentFiles;analyzers;runtime + ``` ## Usage @@ -37,13 +40,6 @@ In your `.csproj` file, use the task to aggregate YAML files and output them in ```xml - - - all - native;contentFiles;analyzers;runtime - - - @@ -75,13 +71,6 @@ You can also generate Azure ARM template parameters. Here's how to modify the co ```xml - - - all - native;contentFiles;analyzers;runtime - - - @@ -105,13 +94,6 @@ You can also output the aggregated configuration back into YAML format: ```xml - - - all - native;contentFiles;analyzers;runtime - - - @@ -135,13 +117,6 @@ You can embed the output files (such as the generated JSON) as resources in the ```xml - - - all - native;contentFiles;analyzers;runtime - - - From fc4cecab603faf36100b54b227c72985d889ad32 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 20:46:49 -0700 Subject: [PATCH 14/15] Add boolean support --- README.md | 9 +- ...{OutputTypeEnum.cs => InputOutputEnums.cs} | 0 src/Task/AggregateConfig.cs | 211 +----------------- src/Task/AggregateConfigBuildTask.csproj | 2 + .../Converters/BooleanYamlTypeConverter.cs | 50 ----- .../FileHandlers/ArmParametersFileHandler.cs | 95 ++++---- src/Task/FileHandlers/IInputReader.cs | 6 +- src/Task/FileHandlers/IOutputWriter.cs | 6 +- src/Task/FileHandlers/JsonFileHandler.cs | 10 +- src/Task/FileHandlers/YamlFileHandler.cs | 29 ++- src/Task/FileSystem/FileSystem.cs | 8 +- src/Task/FileSystem/IFileSystem.cs | 11 +- src/Task/JsonHelper.cs | 151 +++++++++++++ src/Task/ObjectManager.cs | 125 +++++++++++ src/UnitTests/TaskTestBase.cs | 3 + src/UnitTests/VirtualFileSystem.cs | 7 + .../IntegrationTests/EmbeddedResourceTests.cs | 1 - test/IntegrationTests/IntegrationTests.csproj | 2 + 18 files changed, 408 insertions(+), 318 deletions(-) rename src/Contracts/{OutputTypeEnum.cs => InputOutputEnums.cs} (100%) delete mode 100644 src/Task/Converters/BooleanYamlTypeConverter.cs create mode 100644 src/Task/JsonHelper.cs create mode 100644 src/Task/ObjectManager.cs diff --git a/README.md b/README.md index 1591999..08760fa 100644 --- a/README.md +++ b/README.md @@ -239,8 +239,13 @@ This project is licensed under the MIT License. See the [LICENSE](https://github ## Third-Party Libraries -This tool makes use of [YamlDotNet](https://github.com/aaubry/YamlDotNet) for YAML serialization and deserialization. -YamlDotNet is licensed under the MIT License. For more details, please refer to the [YamlDotNet License](https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt). +This project leverages the following third-party libraries: + +- **[YamlDotNet](https://github.com/aaubry/YamlDotNet)** + Used for YAML serialization and deserialization. YamlDotNet is distributed under the MIT License. For detailed information, refer to the [YamlDotNet License](https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt). + +- **[YamlDotNet.System.Text.Json](https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json)** + Facilitates type handling for YAML serialization and deserialization, enhancing compatibility with System.Text.Json. This library is also distributed under the MIT License. For more details, see the [YamlDotNet.System.Text.Json License](https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json/blob/main/LICENSE). ## Contributing diff --git a/src/Contracts/OutputTypeEnum.cs b/src/Contracts/InputOutputEnums.cs similarity index 100% rename from src/Contracts/OutputTypeEnum.cs rename to src/Contracts/InputOutputEnums.cs diff --git a/src/Task/AggregateConfig.cs b/src/Task/AggregateConfig.cs index 4a00bda..de348f7 100644 --- a/src/Task/AggregateConfig.cs +++ b/src/Task/AggregateConfig.cs @@ -1,8 +1,8 @@ using AggregateConfig.Contracts; using AggregateConfig.FileHandlers; +using AggregateConfigBuildTask; using Microsoft.Build.Framework; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -47,7 +47,7 @@ public override bool Execute() try { bool hasError = false; - object finalResult = null; + JsonElement? finalResult = null; OutputFile = Path.GetFullPath(OutputFile); @@ -94,7 +94,7 @@ public override bool Execute() continue; } - object fileData; + JsonElement fileData; try { fileData = outputWriter.ReadInput(file); @@ -107,7 +107,7 @@ public override bool Execute() } // Merge the deserialized object into the final result - finalResult = MergeYamlObjects(finalResult, fileData, file, AddSourceProperty); + finalResult = ObjectManager.MergeObjects(finalResult, fileData, file, AddSourceProperty); } if (hasError) @@ -121,33 +121,11 @@ public override bool Execute() return false; } - var additionalPropertiesDictionary = ParseAdditionalProperties(AdditionalProperties); - - if (additionalPropertiesDictionary?.Count > 0) + var additionalPropertiesDictionary = JsonHelper.ParseAdditionalProperties(AdditionalProperties); + if (!ObjectManager.InjectAdditionalProperties(ref finalResult, additionalPropertiesDictionary)) { - if (finalResult is IDictionary finalDictionary) - { - foreach (var property in additionalPropertiesDictionary) - { - finalDictionary.Add(property.Key, property.Value); - } - } - else if (finalResult is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object) - { - var jsonDictionary = JsonElementToDictionary(jsonElement); - - foreach (var property in additionalPropertiesDictionary) - { - jsonDictionary[property.Key] = property.Value; - } - - finalResult = jsonDictionary; - } - else - { - Console.Error.WriteLine($"Additional properties could not be injected since the top-level is not a dictionary."); - return false; - } + Console.Error.WriteLine("Additional properties could not be injected since the top-level is not a JSON object."); + return false; } var writer = FileHandlerFactory.GetOutputWriter(fileSystem, outputType); @@ -161,178 +139,5 @@ public override bool Execute() return false; } } - - // Recursively merge two YAML objects - private object MergeYamlObjects(object obj1, object obj2, string source2, bool injectSourceProperty) - { - // If injectSourceProperty is true, inject the source property into the second YAML object - if (injectSourceProperty && obj2 is IDictionary obj2Dict) - { - var firstObj2Value = obj2Dict.FirstOrDefault().Value; - if (firstObj2Value is IList obj2NestedList) - { - foreach (var currentObj2Nested in obj2NestedList) - { - if (currentObj2Nested is IDictionary obj2NestedDict) - { - // Inject the "source" property - obj2NestedDict["source"] = Path.GetFileNameWithoutExtension(source2); - } - } - } - } - - if (obj1 == null) return obj2; - if (obj2 == null) return obj1; - - // Handle merging of dictionaries with string keys (the normal case after conversion) - if (obj1 is IDictionary dict1 && obj2 is IDictionary dict2) - { - foreach (var key in dict2.Keys) - { - if (dict1.ContainsKey(key)) - { - dict1[key] = MergeYamlObjects(dict1[key], dict2[key], source2, injectSourceProperty); - } - else - { - dict1[key] = dict2[key]; - } - } - - return dict1; - } - // Handle merging of dictionaries where keys are of type object (e.g., Dictionary) - else if (obj1 is IDictionary objDict1 && obj2 is IDictionary objDict2) - { - var mergedDict = new Dictionary(objDict1); // Start with obj1's dictionary - - foreach (var key in objDict2.Keys) - { - if (mergedDict.ContainsKey(key)) - { - mergedDict[key] = MergeYamlObjects(mergedDict[key], objDict2[key], source2, injectSourceProperty); - } - else - { - mergedDict[key] = objDict2[key]; - } - } - - return mergedDict; - } - // Handle lists by concatenating them - else if (obj1 is IList list1 && obj2 is IList list2) - { - foreach (var item in list2) - { - list1.Add(item); - } - - return list1; - } - // For scalar values, obj2 overwrites obj1 - else - { - return obj2; - } - } - - // Helper method to convert dictionary keys to strings - private object ConvertKeysToString(object data) - { - if (data is IDictionary dict) - { - var convertedDict = new Dictionary(); - - foreach (var key in dict.Keys) - { - var stringKey = key.ToString(); // Ensure the key is a string - convertedDict[stringKey] = ConvertKeysToString(dict[key]); // Recursively convert values - } - - return convertedDict; - } - else if (data is IList list) - { - var convertedList = new List(); - - foreach (var item in list) - { - convertedList.Add(ConvertKeysToString(item)); // Recursively convert list items - } - - return convertedList; - } - - return data; // Return the item as-is if it's not a dictionary or list - } - - /// - /// Parses the additional properties provided as a string array in the format "key=value". - /// Supports escaping of the '=' sign using '\='. - /// - /// An array of key-value pairs in the form "key=value". - /// A dictionary containing the parsed key-value pairs. - private Dictionary ParseAdditionalProperties(string[] properties) - { - var additionalPropertiesDict = new Dictionary(); - const string unicodeEscape = "\u001F"; - - if (properties != null) - { - foreach (var property in properties) - { - var sanitizedProperty = property.Replace(@"\=", unicodeEscape); - - var keyValue = sanitizedProperty.Split(new[] { '=' }, 2); - - if (keyValue.Length == 2) - { - additionalPropertiesDict[keyValue[0].Replace(unicodeEscape, "=")] = keyValue[1].Replace(unicodeEscape, "="); - } - } - } - return additionalPropertiesDict; - } - - private Dictionary JsonElementToDictionary(JsonElement element) - { - var dictionary = new Dictionary(); - - foreach (var property in element.EnumerateObject()) - { - dictionary[property.Name] = ConvertJsonElementToObject(property.Value); - } - - return dictionary; - } - - private object ConvertJsonElementToObject(JsonElement element) - { - switch (element.ValueKind) - { - case JsonValueKind.Object: - return JsonElementToDictionary(element); - case JsonValueKind.Array: - var list = new List(); - foreach (var item in element.EnumerateArray()) - { - list.Add(ConvertJsonElementToObject(item)); - } - return list; - case JsonValueKind.String: - return element.GetString(); - case JsonValueKind.Number: - return element.TryGetInt64(out long l) ? l : element.GetDouble(); - case JsonValueKind.True: - case JsonValueKind.False: - return element.GetBoolean(); - case JsonValueKind.Null: - return null; - default: - throw new InvalidOperationException($"Unsupported JsonValueKind: {element.ValueKind}"); - } - } } } diff --git a/src/Task/AggregateConfigBuildTask.csproj b/src/Task/AggregateConfigBuildTask.csproj index e783e72..da5dd78 100644 --- a/src/Task/AggregateConfigBuildTask.csproj +++ b/src/Task/AggregateConfigBuildTask.csproj @@ -29,6 +29,7 @@ + @@ -43,6 +44,7 @@ licenses/YamlDotNet/LICENSE.txt + licenses/LICENSE diff --git a/src/Task/Converters/BooleanYamlTypeConverter.cs b/src/Task/Converters/BooleanYamlTypeConverter.cs deleted file mode 100644 index a00715b..0000000 --- a/src/Task/Converters/BooleanYamlTypeConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace AggregateConfig.Converters -{ - public class BooleanYamlTypeConverter : IYamlTypeConverter - { - public bool Accepts(Type type) - { - return typeof(bool).IsAssignableFrom(type); - } - - public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - // Check if the current token is a scalar (simple value) and can be processed - if (parser.Current is Scalar scalar) - { - var value = scalar.Value.ToLower(); - - // Process boolean-like values - if (value == "true" || value == "yes" || value == "on") - { - return true; - } - else if (value == "false" || value == "no" || value == "off") - { - return false; - } - } - - // For all other cases, delegate to the default deserialization - return parser.MoveNext(); - } - - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) - { - // Only handle boolean serialization, delegate other cases to serializer - if (value is bool boolValue) - { - emitter.Emit(new Scalar(boolValue ? "true" : "false")); - } - else - { - serializer(value, type); - } - } - } -} diff --git a/src/Task/FileHandlers/ArmParametersFileHandler.cs b/src/Task/FileHandlers/ArmParametersFileHandler.cs index 6072c96..8514482 100644 --- a/src/Task/FileHandlers/ArmParametersFileHandler.cs +++ b/src/Task/FileHandlers/ArmParametersFileHandler.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json; namespace AggregateConfig.FileHandlers @@ -12,55 +13,69 @@ internal ArmParametersFileHandler(IFileSystem fileSystem) this.fileSystem = fileSystem; } - public void WriteOutput(object mergedData, string outputPath) + /// + public void WriteOutput(JsonElement? mergedData, string outputPath) { - var dataDict = mergedData as Dictionary; - - var parameters = new Dictionary(); - foreach (var kvp in dataDict) + if (mergedData.HasValue && mergedData.Value.ValueKind == JsonValueKind.Object) { - string type = GetParameterType(kvp.Value); - parameters[kvp.Key] = new Dictionary + var parameters = new Dictionary(); + + foreach (var kvp in mergedData.Value.EnumerateObject()) { - ["type"] = type, - ["value"] = kvp.Value - }; - } + string type = GetParameterType(kvp.Value); - // ARM template structure - var armTemplate = new Dictionary - { - ["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - ["contentVersion"] = "1.0.0.0", - ["parameters"] = parameters - }; + parameters[kvp.Name] = new Dictionary + { + ["type"] = type, + ["value"] = kvp.Value + }; + } - var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; - var jsonContent = JsonSerializer.Serialize(armTemplate, jsonOptions); - fileSystem.WriteAllText(outputPath, jsonContent); - } + // ARM template structure + var armTemplate = new Dictionary + { + ["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + ["contentVersion"] = "1.0.0.0", + ["parameters"] = parameters + }; - private string GetParameterType(object value) - { - if (value is string) - { - return "string"; - } - else if (value is int || value is long || value is double || value is float) - { - return "int"; + var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + var jsonContent = JsonSerializer.Serialize(armTemplate, jsonOptions); + fileSystem.WriteAllText(outputPath, jsonContent); } - else if (value is bool) - { - return "bool"; - } - else if (value is IEnumerable) + else { - return "array"; + throw new InvalidOperationException("mergedData is either null or not a valid JSON object."); } - else + } + + /// + /// Determines the parameter type for a given JsonElement value, based on Azure ARM template supported types. + /// + /// The JsonElement value to evaluate. + /// A string representing the ARM template parameter type. + private string GetParameterType(JsonElement value) + { + switch (value.ValueKind) { - return "object"; + case JsonValueKind.String: + return "string"; + + case JsonValueKind.Number: + return "int"; + + case JsonValueKind.True: + case JsonValueKind.False: + return "bool"; + + case JsonValueKind.Array: + return "array"; + + case JsonValueKind.Object: + return "object"; + + default: + throw new ArgumentException("Unsupported type for ARM template parameters."); } } } diff --git a/src/Task/FileHandlers/IInputReader.cs b/src/Task/FileHandlers/IInputReader.cs index affc305..e28ca15 100644 --- a/src/Task/FileHandlers/IInputReader.cs +++ b/src/Task/FileHandlers/IInputReader.cs @@ -1,7 +1,9 @@ -namespace AggregateConfig.FileHandlers +using System.Text.Json; + +namespace AggregateConfig.FileHandlers { public interface IInputReader { - object ReadInput(string inputPath); + JsonElement ReadInput(string inputPath); } } diff --git a/src/Task/FileHandlers/IOutputWriter.cs b/src/Task/FileHandlers/IOutputWriter.cs index 2c7c409..48d5d52 100644 --- a/src/Task/FileHandlers/IOutputWriter.cs +++ b/src/Task/FileHandlers/IOutputWriter.cs @@ -1,7 +1,9 @@ -namespace AggregateConfig.FileHandlers +using System.Text.Json; + +namespace AggregateConfig.FileHandlers { public interface IOutputWriter { - void WriteOutput(object mergedData, string outputPath); + void WriteOutput(JsonElement? mergedData, string outputPath); } } diff --git a/src/Task/FileHandlers/JsonFileHandler.cs b/src/Task/FileHandlers/JsonFileHandler.cs index c71462e..2161e90 100644 --- a/src/Task/FileHandlers/JsonFileHandler.cs +++ b/src/Task/FileHandlers/JsonFileHandler.cs @@ -11,13 +11,15 @@ internal JsonFileHandler(IFileSystem fileSystem) this.fileSystem = fileSystem; } - public object ReadInput(string inputPath) + /// + public JsonElement ReadInput(string inputPath) { - var jsonContent = fileSystem.ReadAllText(inputPath); - return JsonSerializer.Deserialize(jsonContent); + var json = fileSystem.ReadAllText(inputPath); + return JsonSerializer.Deserialize(json); } - public void WriteOutput(object mergedData, string outputPath) + /// + public void WriteOutput(JsonElement? mergedData, string outputPath) { var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; var jsonContent = JsonSerializer.Serialize(mergedData, jsonOptions); diff --git a/src/Task/FileHandlers/YamlFileHandler.cs b/src/Task/FileHandlers/YamlFileHandler.cs index cf853e2..c1109f4 100644 --- a/src/Task/FileHandlers/YamlFileHandler.cs +++ b/src/Task/FileHandlers/YamlFileHandler.cs @@ -1,5 +1,8 @@ -using YamlDotNet.Serialization; +using System.IO; +using System.Text.Json; +using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.System.Text.Json; namespace AggregateConfig.FileHandlers { @@ -12,20 +15,22 @@ internal YamlFileHandler(IFileSystem fileSystem) this.fileSystem = fileSystem; } - public object ReadInput(string inputPath) + /// + public JsonElement ReadInput(string inputPath) { - var yamlContent = fileSystem.ReadAllText(inputPath); - - // Deserialize the YAML content - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - //.WithTypeConverter(new BooleanYamlTypeConverter()) - .Build(); - - return deserializer.Deserialize(yamlContent); + using (TextReader reader = fileSystem.OpenText(inputPath)) + { + return new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new SystemTextJsonYamlTypeConverter()) + .WithTypeInspector(x => new SystemTextJsonTypeInspector(x)) + .Build() + .Deserialize(reader); + } } - public void WriteOutput(object mergedData, string outputPath) + /// + public void WriteOutput(JsonElement? mergedData, string outputPath) { var serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) diff --git a/src/Task/FileSystem/FileSystem.cs b/src/Task/FileSystem/FileSystem.cs index ea29e86..bac451d 100644 --- a/src/Task/FileSystem/FileSystem.cs +++ b/src/Task/FileSystem/FileSystem.cs @@ -39,11 +39,17 @@ public bool DirectoryExists(string path) { return Directory.Exists(path); } - + /// public void CreateDirectory(string directoryPath) { Directory.CreateDirectory(directoryPath); } + + /// + public TextReader OpenText(string path) + { + return File.OpenText(path); + } } } diff --git a/src/Task/FileSystem/IFileSystem.cs b/src/Task/FileSystem/IFileSystem.cs index 1fef996..cb63f9e 100644 --- a/src/Task/FileSystem/IFileSystem.cs +++ b/src/Task/FileSystem/IFileSystem.cs @@ -1,4 +1,6 @@ -namespace AggregateConfig +using System.IO; + +namespace AggregateConfig { /// /// Interface for a file system abstraction, allowing various implementations to handle file operations. @@ -66,5 +68,12 @@ internal interface IFileSystem /// all necessary subdirectories are created. Throws an exception if the directory cannot be created. /// void CreateDirectory(string directoryPath); + + /// + /// Opens a text file for reading and returns a TextReader. + /// + /// The file path to open for reading. + /// A TextReader for reading the file content. + TextReader OpenText(string path); } } diff --git a/src/Task/JsonHelper.cs b/src/Task/JsonHelper.cs new file mode 100644 index 0000000..8db4878 --- /dev/null +++ b/src/Task/JsonHelper.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace AggregateConfigBuildTask +{ + internal static class JsonHelper + { + /// + /// Converts a dictionary of string and JsonElement pairs to a JsonElement. + /// + /// A dictionary containing string keys and JsonElement values. + /// A JsonElement representing the dictionary. + public static JsonElement ConvertToJsonElement(Dictionary dictionary) + { + return ConvertObjectToJsonElement(dictionary); + } + + /// + /// Converts a list of JsonElements to a JsonElement. + /// + /// A list containing JsonElement objects. + /// A JsonElement representing the list. + public static JsonElement ConvertToJsonElement(List list) + { + return ConvertObjectToJsonElement(list); + } + + /// + /// Converts dictionary keys to strings recursively for any dictionary or list. + /// + /// The object to process, can be a dictionary or list. + /// An object where dictionary keys are converted to strings. + public static object ConvertKeysToString(object data) + { + if (data is IDictionary dict) + { + var convertedDict = new Dictionary(); + + foreach (var key in dict.Keys) + { + var stringKey = key.ToString(); // Ensure the key is a string + convertedDict[stringKey] = ConvertKeysToString(dict[key]); // Recursively convert values + } + + return convertedDict; + } + else if (data is IList list) + { + var convertedList = new List(); + + foreach (var item in list) + { + convertedList.Add(ConvertKeysToString(item)); // Recursively convert list items + } + + return convertedList; + } + + return data; // Return the item as-is if it's not a dictionary or list + } + + /// + /// Parses an array of key-value pairs provided as strings in the format "key=value". + /// Supports escaping of the '=' sign using '\='. + /// + /// An array of key-value pairs in the form "key=value". + /// A dictionary containing the parsed key-value pairs. + public static Dictionary ParseAdditionalProperties(string[] properties) + { + var additionalPropertiesDict = new Dictionary(); + const string unicodeEscape = "\u001F"; + + if (properties != null) + { + foreach (var property in properties) + { + var sanitizedProperty = property.Replace(@"\=", unicodeEscape); + + var keyValue = sanitizedProperty.Split(new[] { '=' }, 2); + + if (keyValue.Length == 2) + { + additionalPropertiesDict[keyValue[0].Replace(unicodeEscape, "=")] = keyValue[1].Replace(unicodeEscape, "="); + } + } + } + return additionalPropertiesDict; + } + + /// + /// Converts any object to a JsonElement. + /// + /// The object to convert to JsonElement. + /// A JsonElement representing the object. + public static JsonElement ConvertObjectToJsonElement(object value) + { + var json = JsonSerializer.Serialize(value); + return JsonDocument.Parse(json).RootElement; + } + + /// + /// Converts a JsonElement to a dictionary of string and JsonElement pairs. + /// + /// The JsonElement to convert. + /// A dictionary representing the JsonElement. + public static Dictionary JsonElementToDictionary(JsonElement jsonElement) + { + var dictionary = new Dictionary(); + + foreach (var property in jsonElement.EnumerateObject()) + { + dictionary.Add(property.Name, property.Value); + } + + return dictionary; + } + + /// + /// Converts a JsonElement to an appropriate object representation, such as a dictionary, list, or primitive. + /// + /// The JsonElement to convert. + /// An object representing the JsonElement. + public static object ConvertJsonElementToObject(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + return JsonElementToDictionary(element); + case JsonValueKind.Array: + var list = new List(); + foreach (var item in element.EnumerateArray()) + { + list.Add(ConvertJsonElementToObject(item)); + } + return list; + case JsonValueKind.String: + return element.GetString(); + case JsonValueKind.Number: + return element.TryGetInt64(out long l) ? l : element.GetDouble(); + case JsonValueKind.True: + case JsonValueKind.False: + return element.GetBoolean(); + case JsonValueKind.Null: + return null; + default: + throw new InvalidOperationException($"Unsupported JsonValueKind: {element.ValueKind}"); + } + } + } +} diff --git a/src/Task/ObjectManager.cs b/src/Task/ObjectManager.cs new file mode 100644 index 0000000..b777a74 --- /dev/null +++ b/src/Task/ObjectManager.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace AggregateConfigBuildTask +{ + internal static class ObjectManager + { + /// + /// Merges two JsonElements into a single JsonElement. Will merge nested objects and lists together. + /// + public static JsonElement MergeObjects(JsonElement? obj1, JsonElement? obj2, string source2, bool injectSourceProperty) + { + // If injectSourceProperty is true, inject the source property into the second JSON object + if (injectSourceProperty && obj2.HasValue && obj2.Value.ValueKind == JsonValueKind.Object) + { + var obj2Dict = obj2.Value; + var jsonObject = obj2Dict.EnumerateObject().ToDictionary(p => p.Name, p => p.Value); + + if (jsonObject.FirstOrDefault().Value.ValueKind == JsonValueKind.Array) + { + var firstObj2Value = jsonObject.FirstOrDefault().Value; + var obj2NestedList = firstObj2Value.EnumerateArray().ToList(); + + for (int index = 0; index < obj2NestedList.Count; index++) + { + var currentObj2Nested = obj2NestedList[index]; + + if (currentObj2Nested.ValueKind == JsonValueKind.Object) + { + var nestedObj = currentObj2Nested; + var nestedDict = nestedObj.EnumerateObject().ToDictionary(p => p.Name, p => p.Value); + + // Inject the "source" property + nestedDict["source"] = JsonDocument.Parse($"\"{Path.GetFileNameWithoutExtension(source2)}\"").RootElement; + + // Update the list at the correct index + obj2NestedList[index] = JsonHelper.ConvertToJsonElement(nestedDict); + } + } + + jsonObject[jsonObject.FirstOrDefault().Key] = JsonHelper.ConvertToJsonElement(obj2NestedList); + } + obj2 = JsonHelper.ConvertObjectToJsonElement(jsonObject); + } + + if (obj1 == null) return obj2.HasValue ? obj2.Value : default; + if (obj2 == null) return obj1.HasValue ? obj1.Value : default; + + // Handle merging of objects + if (obj1.Value.ValueKind == JsonValueKind.Object && obj2.Value.ValueKind == JsonValueKind.Object) + { + var dict1 = obj1.Value.EnumerateObject().ToDictionary(p => p.Name, p => p.Value); + var dict2 = obj2.Value.EnumerateObject().ToDictionary(p => p.Name, p => p.Value); + + foreach (var key in dict2.Keys) + { + if (dict1.ContainsKey(key)) + { + dict1[key] = MergeObjects(dict1[key], dict2[key], source2, injectSourceProperty); + } + else + { + dict1[key] = dict2[key]; + } + } + + return JsonHelper.ConvertToJsonElement(dict1); + } + // Handle merging of arrays + else if (obj1.Value.ValueKind == JsonValueKind.Array && obj2.Value.ValueKind == JsonValueKind.Array) + { + var list1 = obj1.Value.EnumerateArray().ToList(); + var list2 = obj2.Value.EnumerateArray().ToList(); + + foreach (var item in list2) + { + list1.Add(item); + } + + return JsonHelper.ConvertToJsonElement(list1); + } + // For scalar values, obj2 overwrites obj1 + else + { + return obj2.Value; + } + } + + /// + /// Injects additional properties into a JSON object if possible. The additional properties are provided as a dictionary and are added to the top-level JSON object. + /// + /// The object that is expected to be a JSON object (JsonElement) where additional properties will be injected. + /// A dictionary of additional properties to inject. + /// True if the properties were successfully injected, false otherwise. + public static bool InjectAdditionalProperties(ref JsonElement? finalResult, Dictionary additionalPropertiesDictionary) + { + if (additionalPropertiesDictionary?.Count > 0) + { + if (finalResult is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object) + { + var jsonDictionary = JsonHelper.JsonElementToDictionary(jsonElement); + + // Add the properties from additionalPropertiesDictionary, converting values to JsonElement + foreach (var property in additionalPropertiesDictionary) + { + jsonDictionary[property.Key] = JsonHelper.ConvertObjectToJsonElement(property.Value); + } + + finalResult = JsonHelper.ConvertToJsonElement(jsonDictionary); + return true; + } + else + { + Console.Error.WriteLine("Additional properties could not be injected since the top-level is not a JSON object."); + return false; + } + } + + return true; + } + } +} diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index 916dbcc..929eae0 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -92,6 +92,7 @@ public void ShouldGenerateArmParameterOutput() JObject parameters = (JObject)armTemplate["parameters"]; Assert.IsNotNull(parameters.GetValue("options")); + Assert.AreEqual("array", parameters.GetValue("options")["type"].ToString()); } [TestMethod] @@ -233,6 +234,7 @@ public void ShouldIncludeAdditionalPropertiesInArmParameters() string output = mockFileSystem.ReadAllText($"{testPath}\\output.json"); var armTemplate = JsonConvert.DeserializeObject>(output); JObject parameters = (JObject)armTemplate["parameters"]; + Assert.AreEqual("array", parameters.GetValue("options")["type"].ToString()); Assert.AreEqual("TestRG", parameters.GetValue("Group")["value"].Value()); Assert.AreEqual("Prod", parameters.GetValue("Environment")["value"].Value()); } @@ -311,6 +313,7 @@ public void ShouldCorrectlyParseBooleanValues() string output = mockFileSystem.ReadAllText($"{testPath}\\output.json"); var armTemplate = JsonConvert.DeserializeObject>(output); JObject parameters = (JObject)armTemplate["parameters"]; + Assert.AreEqual("array", parameters.GetValue("options")["type"].ToString()); Assert.AreEqual("Boolean", parameters.GetValue("options")["value"].First()["isEnabled"].Type.ToString()); Assert.AreEqual(true, parameters.GetValue("options")["value"].First()["isEnabled"].Value()); } diff --git a/src/UnitTests/VirtualFileSystem.cs b/src/UnitTests/VirtualFileSystem.cs index 371b219..0006a0d 100644 --- a/src/UnitTests/VirtualFileSystem.cs +++ b/src/UnitTests/VirtualFileSystem.cs @@ -64,6 +64,7 @@ public string ReadAllText(string path) { return content; } + throw new FileNotFoundException($"The file '{path}' was not found in the virtual file system."); } @@ -123,6 +124,12 @@ public void CreateDirectory(string path) fileSystem[path] = string.Empty; } + /// + public TextReader OpenText(string path) + { + return new StringReader(ReadAllText(path)); + } + /// /// Ensures that the provided directory path ends with a directory separator character. /// diff --git a/test/IntegrationTests/EmbeddedResourceTests.cs b/test/IntegrationTests/EmbeddedResourceTests.cs index c209828..4700e36 100644 --- a/test/IntegrationTests/EmbeddedResourceTests.cs +++ b/test/IntegrationTests/EmbeddedResourceTests.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Linq; using System.Reflection; using System.Text.Json; diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj index 3abff14..be1c58e 100644 --- a/test/IntegrationTests/IntegrationTests.csproj +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -7,6 +7,7 @@ disable false true + true @@ -20,6 +21,7 @@ + From a6e0115c4311c3ad6cb122d46b48e964210dd93d Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Mon, 9 Sep 2024 20:55:10 -0700 Subject: [PATCH 15/15] local validation fixes --- .github/workflows/build.yml | 6 ++--- test/IntegrationTests.sln | 25 +++++++++++++++++++ test/IntegrationTests/.editorconfig | 4 +++ test/IntegrationTests/IntegrationTests.csproj | 1 - tools/localreleasevalidation.ps1 | 4 ++- 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 test/IntegrationTests.sln create mode 100644 test/IntegrationTests/.editorconfig diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e76f7fe..65549ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,13 +62,13 @@ jobs: run: dotnet nuget add source ${{ github.workspace }}/nuget/local --name AggregateConfigBuildTask - name: Restore IntegrationTests with custom AggregateConfigBuildTask package - run: dotnet restore test/IntegrationTests/IntegrationTests.csproj + run: dotnet restore test/IntegrationTests.sln - name: Build IntegrationTests in Release mode - run: dotnet build test/IntegrationTests/IntegrationTests.csproj --configuration Release -warnaserror + run: dotnet build test/IntegrationTests.sln --configuration Release -warnaserror - name: Run IntegrationTests - run: dotnet test test/IntegrationTests/IntegrationTests.csproj --configuration Release -warnaserror + run: dotnet test test/IntegrationTests.sln --configuration Release -warnaserror - name: Upload integration results artifact uses: actions/upload-artifact@v4 diff --git a/test/IntegrationTests.sln b/test/IntegrationTests.sln new file mode 100644 index 0000000..03198ec --- /dev/null +++ b/test/IntegrationTests.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34607.119 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "IntegrationTests\IntegrationTests.csproj", "{C3186E8C-A01C-46A3-BB70-0433374822C2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C3186E8C-A01C-46A3-BB70-0433374822C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3186E8C-A01C-46A3-BB70-0433374822C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3186E8C-A01C-46A3-BB70-0433374822C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3186E8C-A01C-46A3-BB70-0433374822C2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F4C35D56-1D95-4EF7-ACF7-4319177A048E} + EndGlobalSection +EndGlobal diff --git a/test/IntegrationTests/.editorconfig b/test/IntegrationTests/.editorconfig new file mode 100644 index 0000000..1f62320 --- /dev/null +++ b/test/IntegrationTests/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = suggestion diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj index be1c58e..ffb7b65 100644 --- a/test/IntegrationTests/IntegrationTests.csproj +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -21,7 +21,6 @@ - diff --git a/tools/localreleasevalidation.ps1 b/tools/localreleasevalidation.ps1 index 8a28624..b2deb61 100644 --- a/tools/localreleasevalidation.ps1 +++ b/tools/localreleasevalidation.ps1 @@ -2,7 +2,7 @@ # Step 1: Set up paths $solutionPath = "src\AggregateConfigBuildTask.sln" -$testProjectPath = "test\IntegrationTests\IntegrationTests.csproj" +$testProjectPath = "test\IntegrationTests.sln" $nupkgPath = "src\Task\bin\Release\AggregateConfigBuildTask.1.0.1.nupkg" $localNugetDir = ($env:APPDATA + "\Roaming\NuGet\nuget\local") $nugetSourceName = "AggregateConfigBuildTask" @@ -13,6 +13,7 @@ dotnet restore $solutionPath # Step 3: Build the src/AggregateConfigBuildTask.sln project in Release mode Write-Host "Building $solutionPath in Release mode..." +dotnet clean $solutionPath dotnet build $solutionPath --configuration Release -warnaserror # Step 4: Run tests for AggregateConfigBuildTask.sln @@ -43,6 +44,7 @@ dotnet restore $testProjectPath # Step 9: Build the integration tests project in Release mode Write-Host "Building $testProjectPath in Release mode..." +dotnet clean $solutionPath dotnet build $testProjectPath --configuration Release -warnaserror # Step 10: Run the integration tests