diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 22456b0..71edae3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -46,21 +46,32 @@ jobs:
$VERSION = "0.0.1-$COMMIT_HASH"
}
- Write-Output "::set-output name=VERSION::$VERSION"
+ Write-Output "Version is $VERSION"
+
+ echo "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8
+ echo "VERSION=$VERSION" >> $env:GITHUB_OUTPUT
- name: Build AggregateConfigBuildTask solution in Release mode
- run: dotnet build src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ steps.get_version.outputs.VERSION }}
+ run: dotnet build src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ env.VERSION }}
- name: Run tests for AggregateConfigBuildTask solution
- run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ steps.get_version.outputs.VERSION }} -p:CollectCoverage=true
+ id: run_tests
+ run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ env.VERSION }} -p:CollectCoverage=true
+
+ - name: Upload TestResults artifact
+ if: ${{ always() && steps.run_tests.conclusion != 'skipped' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: TestResults
+ path: src\UnitTests\TestResults
- name: Upload NuGetPackage artifact
uses: actions/upload-artifact@v4
with:
name: NuGetPackage
path: |
- src/Task/bin/Release/AggregateConfigBuildTask.${{ steps.get_version.outputs.VERSION }}.nupkg
- src/Task/bin/Release/AggregateConfigBuildTask.${{ steps.get_version.outputs.VERSION }}.snupkg
+ src/Task/bin/Release/AggregateConfigBuildTask.${{ env.VERSION }}.nupkg
+ src/Task/bin/Release/AggregateConfigBuildTask.${{ env.VERSION }}.snupkg
integration_tests:
needs: build
diff --git a/.gitignore b/.gitignore
index dda79a5..cad43c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ output*.json
_site/
docs/
src/.manifest
+src/UnitTests/TestResults
+.vscode
diff --git a/README.md b/README.md
index 34476e6..b31cd6c 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@
[](https://www.nuget.org/packages/AggregateConfigBuildTask) [](https://github.com/richardsondev/AggregateConfigBuildTask/actions/workflows/build.yml?query=branch%3Amain)
-**AggregateConfigBuildTask** is a cross-platform MSBuild task that aggregates and transforms configuration files into more consumable formats like JSON, Azure ARM template parameters, YAML during the build process.
+**AggregateConfigBuildTask** is a cross-platform MSBuild task that aggregates and transforms configuration files into more consumable formats like JSON, Azure ARM template parameters, YAML, and TOML during the build process.
## Features
-- Merge multiple 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, YAML, or TOML).
- 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.
@@ -39,9 +39,9 @@ Alternatively, add the following line to your `.csproj` file:
| Parameter | Description | Supported Values | Default |
|----------|----------|----------|----------|
| **OutputFile**
*(Required)* | The file path to write output to. Should include the extension. | | |
-| **OutputType**
*(Required)* | Specifies the format of the output file. | `Json`, `Arm`, `Yaml` | |
+| **OutputType**
*(Required)* | Specifies the format of the output file. | `Json`, `Arm`, `Yaml`, `Toml` | |
| **InputDirectory**
*(Required)* | The directory containing the files that need to be aggregated. | | |
-| **InputType** | Specifies the format of the input files. Refer to the [File Types](#file-types) table below for the corresponding file extensions that will be searched for. | `Json`, `Arm`, `Yaml` | `Yaml` |
+| **InputType** | Specifies the format of the input files. Refer to the [File Types](#file-types) table below for the corresponding file extensions that will be searched for. | `Json`, `Arm`, `Yaml`, `Toml` | `Yaml` |
| **AddSourceProperty** | Adds a `source` property to each object in the output, specifying the filename from which the object originated. | `true`, `false` | `false` |
| **AdditionalProperties** | A set of custom top-level properties to include in the final output. Use `ItemGroup` syntax to define key-value pairs. See [below](#additional-properties) for usage details. | | |
| **IsQuietMode** | When true, only warning and error logs are generated by the task, suppressing standard informational output. | `true`, `false` | `false` |
@@ -54,6 +54,7 @@ The `InputDirectory` will be scanned for files based on the specified `InputType
|---------------|------------------------|
| `Json` | `.json` |
| `Arm` | `.json` |
+| `Toml` | `.toml` |
| `Yaml` | `.yml`, `.yaml` |
## Usage
@@ -367,13 +368,17 @@ This project is licensed under the MIT License. See the [LICENSE](https://github
This project leverages the following third-party libraries that are bundled with the package:
-- **[YamlDotNet](https://github.com/aaubry/YamlDotNet)**\
- __Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors__\
- 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](https://github.com/aaubry/YamlDotNet)**
+ _Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors_
+ Used for YAML serialization and deserialization. YamlDotNet is distributed under the MIT License. For details, 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)**\
- __Copyright (c) 2022 Ivan Josipovic__\
- 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).
+- **[YamlDotNet.System.Text.Json](https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json)**
+ _Copyright (c) 2022 Ivan Josipovic_
+ Facilitates type handling for YAML serialization and deserialization with System.Text.Json. 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).
+
+- **[Tommy](https://github.com/dezhidki/Tommy)**
+ _Copyright (c) 2020 Denis Zhidkikh_
+ Tommy provides support for working with TOML files in .NET applications. Distributed under the MIT License. See the full license at the [Tommy License](https://github.com/dezhidki/Tommy/blob/master/LICENSE).
## Contributing
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 87f6689..3bb58aa 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -4,30 +4,31 @@
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/src/Task/AggregateConfig.cs b/src/Task/AggregateConfig.cs
index 0e9f9e1..5912672 100644
--- a/src/Task/AggregateConfig.cs
+++ b/src/Task/AggregateConfig.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
using AggregateConfigBuildTask.FileHandlers;
using Microsoft.Build.Framework;
using Task = Microsoft.Build.Utilities.Task;
@@ -109,30 +110,7 @@ public override bool Execute()
return false;
}
- logger.LogMessage(MessageImportance.High, "Aggregating {0} to {1} in folder {2}", inputType, outputType, InputDirectory);
-
- string directoryPath = Path.GetDirectoryName(OutputFile);
- if (!fileSystem.DirectoryExists(directoryPath))
- {
- fileSystem.CreateDirectory(directoryPath);
- }
-
- var finalResult = ObjectManager.MergeFileObjects(InputDirectory, inputType, AddSourceProperty, fileSystem, logger).GetAwaiter().GetResult();
-
- if (finalResult == null)
- {
- logger.LogError("No input was found! Check the input directory.");
- return false;
- }
-
- var additionalPropertiesDictionary = JsonHelper.ParseAdditionalProperties(AdditionalProperties);
- finalResult = ObjectManager.InjectAdditionalProperties(finalResult, additionalPropertiesDictionary, logger).GetAwaiter().GetResult();
-
- var writer = FileHandlerFactory.GetFileHandlerForType(fileSystem, outputType);
- writer.WriteOutput(finalResult, OutputFile);
- logger.LogMessage(MessageImportance.High, "Wrote aggregated configuration file to {0}", OutputFile);
-
- return true;
+ return Process(inputType, outputType).GetAwaiter().GetResult();
}
catch (Exception ex)
{
@@ -142,6 +120,34 @@ public override bool Execute()
}
}
+ private async Task Process(FileType inputType, FileType outputType)
+ {
+ logger.LogMessage(MessageImportance.High, "Aggregating {0} to {1} in folder {2}", inputType, outputType, InputDirectory);
+
+ string directoryPath = Path.GetDirectoryName(OutputFile);
+ if (!fileSystem.DirectoryExists(directoryPath))
+ {
+ fileSystem.CreateDirectory(directoryPath);
+ }
+
+ var finalResult = await ObjectManager.MergeFileObjects(InputDirectory, inputType, AddSourceProperty, fileSystem, logger).ConfigureAwait(false);
+
+ if (finalResult == null)
+ {
+ logger.LogError("No input was found! Check the input directory.");
+ return false;
+ }
+
+ var additionalPropertiesDictionary = JsonHelper.ParseAdditionalProperties(AdditionalProperties);
+ finalResult = await ObjectManager.InjectAdditionalProperties(finalResult, additionalPropertiesDictionary, logger).ConfigureAwait(false);
+
+ var writer = FileHandlerFactory.GetFileHandlerForType(fileSystem, outputType);
+ await writer.WriteOutput(finalResult, OutputFile).ConfigureAwait(false);
+ logger.LogMessage(MessageImportance.High, "Wrote aggregated configuration file to {0}", OutputFile);
+
+ return true;
+ }
+
private void EmitHeader()
{
var assembly = Assembly.GetExecutingAssembly();
diff --git a/src/Task/AggregateConfigBuildTask.csproj b/src/Task/AggregateConfigBuildTask.csproj
index 78079ea..e4f5ee1 100644
--- a/src/Task/AggregateConfigBuildTask.csproj
+++ b/src/Task/AggregateConfigBuildTask.csproj
@@ -45,6 +45,7 @@
+
@@ -52,6 +53,7 @@
+
diff --git a/src/Task/FileHandlers/ArmParametersFileHandler.cs b/src/Task/FileHandlers/ArmParametersFileHandler.cs
index 48b2077..a565b19 100644
--- a/src/Task/FileHandlers/ArmParametersFileHandler.cs
+++ b/src/Task/FileHandlers/ArmParametersFileHandler.cs
@@ -6,7 +6,9 @@
namespace AggregateConfigBuildTask.FileHandlers
{
- ///
+ ///
+ /// Handles reading and writing ARM template parameter JSON files.
+ ///
public class ArmParametersFileHandler : IFileHandler
{
private readonly IFileSystem fileSystem;
@@ -51,7 +53,7 @@ public async ValueTask ReadInput(string inputPath)
}
///
- public void WriteOutput(JsonElement? mergedData, string outputPath)
+ public async Task WriteOutput(JsonElement? mergedData, string outputPath)
{
if (mergedData.HasValue && mergedData.Value.ValueKind == JsonValueKind.Object)
{
@@ -76,7 +78,7 @@ public void WriteOutput(JsonElement? mergedData, string outputPath)
["parameters"] = parameters
};
var jsonContent = JsonSerializer.Serialize(armTemplate, jsonOptions);
- fileSystem.WriteAllText(outputPath, jsonContent);
+ await fileSystem.WriteAllTextAsync(outputPath, jsonContent).ConfigureAwait(false);
}
else
{
diff --git a/src/Task/FileHandlers/FileHandlerFactory.cs b/src/Task/FileHandlers/FileHandlerFactory.cs
index bdc9d4c..e47ae52 100644
--- a/src/Task/FileHandlers/FileHandlerFactory.cs
+++ b/src/Task/FileHandlers/FileHandlerFactory.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
namespace AggregateConfigBuildTask.FileHandlers
@@ -25,6 +25,8 @@ internal static IFileHandler GetFileHandlerForType(IFileSystem fileSystem, FileT
return new YamlFileHandler(fileSystem);
case FileType.Arm:
return new ArmParametersFileHandler(fileSystem);
+ case FileType.Toml:
+ return new TomlFileHandler(fileSystem);
default:
throw new ArgumentException("Unsupported format");
}
@@ -46,6 +48,8 @@ internal static List GetExpectedFileExtensions(FileType inputType)
return new List { ".yml", ".yaml" };
case FileType.Arm:
return new List { ".json" };
+ case FileType.Toml:
+ return new List { ".toml" };
default:
throw new ArgumentException("Unsupported input type");
}
diff --git a/src/Task/FileHandlers/FileTypeEnum.cs b/src/Task/FileHandlers/FileTypeEnum.cs
index 993eb81..c9bd451 100644
--- a/src/Task/FileHandlers/FileTypeEnum.cs
+++ b/src/Task/FileHandlers/FileTypeEnum.cs
@@ -1,4 +1,4 @@
-namespace AggregateConfigBuildTask
+namespace AggregateConfigBuildTask
{
///
/// Enum representing different file types supported for merging and processing.
@@ -29,5 +29,10 @@ public enum FileType
/// Alias for the file type, for files with the .yaml extension.
///
Yaml = Yml,
+
+ ///
+ /// Represents a TOML (Tom's Obvious, Minimal Language) file type.
+ ///
+ Toml = 3,
}
}
diff --git a/src/Task/FileHandlers/IFileHandler.cs b/src/Task/FileHandlers/IFileHandler.cs
index 8a43edf..8200b61 100644
--- a/src/Task/FileHandlers/IFileHandler.cs
+++ b/src/Task/FileHandlers/IFileHandler.cs
@@ -21,6 +21,6 @@ public interface IFileHandler
///
/// The intermediate data in format. Can be null.
/// The path to the output file where the data will be written.
- void WriteOutput(JsonElement? mergedData, string outputPath);
+ Task WriteOutput(JsonElement? mergedData, string outputPath);
}
}
diff --git a/src/Task/FileHandlers/JsonFileHandler.cs b/src/Task/FileHandlers/JsonFileHandler.cs
index 6ceb2b3..cef4584 100644
--- a/src/Task/FileHandlers/JsonFileHandler.cs
+++ b/src/Task/FileHandlers/JsonFileHandler.cs
@@ -3,7 +3,9 @@
namespace AggregateConfigBuildTask.FileHandlers
{
- ///
+ ///
+ /// Handles reading and writing JSON files.
+ ///
public class JsonFileHandler : IFileHandler
{
readonly IFileSystem fileSystem;
@@ -25,10 +27,10 @@ public async ValueTask ReadInput(string inputPath)
}
///
- public void WriteOutput(JsonElement? mergedData, string outputPath)
+ public Task WriteOutput(JsonElement? mergedData, string outputPath)
{
var jsonContent = JsonSerializer.Serialize(mergedData, jsonOptions);
- fileSystem.WriteAllText(outputPath, jsonContent);
+ return fileSystem.WriteAllTextAsync(outputPath, jsonContent);
}
}
}
diff --git a/src/Task/FileHandlers/TomlFileHandler.cs b/src/Task/FileHandlers/TomlFileHandler.cs
new file mode 100644
index 0000000..d0c5bf4
--- /dev/null
+++ b/src/Task/FileHandlers/TomlFileHandler.cs
@@ -0,0 +1,315 @@
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Tommy;
+using System.IO;
+using System.Collections.Generic;
+
+namespace AggregateConfigBuildTask.FileHandlers
+{
+ ///
+ /// Handles reading and writing TOML files by converting between TOML and JSON structures.
+ ///
+ public class TomlFileHandler : IFileHandler
+ {
+ private readonly IFileSystem fileSystem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system abstraction used for reading and writing files.
+ internal TomlFileHandler(IFileSystem fileSystem)
+ {
+ this.fileSystem = fileSystem;
+ }
+
+ ///
+ /// Reads the input TOML file, converts its contents to a .
+ ///
+ /// The path to the TOML file.
+ /// A representing the JSON data as a .
+ /// Thrown when the input file is not found.
+ public async ValueTask ReadInput(string inputPath)
+ {
+ string tomlContent = await fileSystem.ReadAllTextAsync(inputPath).ConfigureAwait(false);
+
+ // Parse the TOML content using Tommy
+ using (var reader = new StringReader(tomlContent))
+ {
+ TomlTable tomlTable = TOML.Parse(reader);
+ return ConvertTomlToJsonElement(tomlTable);
+ }
+ }
+
+ ///
+ /// Writes the given to a TOML file at the specified output path.
+ ///
+ /// The JSON data to be written as TOML.
+ /// The path where the TOML file should be written.
+ /// Thrown when the provided JSON data is null.
+ public async Task WriteOutput(JsonElement? mergedData, string outputPath)
+ {
+ if (mergedData == null)
+ {
+ throw new ArgumentNullException(nameof(mergedData), "The merged data cannot be null.");
+ }
+
+ // Convert the JsonElement to a TOML table using Tommy
+ TomlTable tomlTable = ConvertJsonElementToToml(mergedData.Value);
+
+ string tomlString;
+ using (var writer = new StringWriter())
+ {
+ tomlTable.WriteTo(writer);
+ await writer.FlushAsync().ConfigureAwait(false);
+ tomlString = writer.ToString();
+ }
+
+ await fileSystem.WriteAllTextAsync(outputPath, tomlString).ConfigureAwait(false);
+ }
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The TOML table to be converted.
+ /// A representing the TOML data.
+ private static JsonElement ConvertTomlToJsonElement(TomlTable tomlTable)
+ {
+ using (var stream = new MemoryStream())
+ {
+ using (var writer = new Utf8JsonWriter(stream))
+ {
+ writer.WriteStartObject();
+
+ foreach (KeyValuePair kvp in tomlTable.RawTable)
+ {
+ WriteTomlNodeToJson(writer, kvp.Key, kvp.Value);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ stream.Position = 0;
+ using (var jsonDoc = JsonDocument.Parse(stream))
+ {
+ return jsonDoc.RootElement.Clone();
+ }
+ }
+ }
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The JSON element to be converted to TOML.
+ /// A representing the JSON data.
+ private TomlTable ConvertJsonElementToToml(JsonElement jsonElement)
+ {
+ TomlTable tomlTable = new TomlTable();
+
+ // Check if the JsonElement is an object
+ if (jsonElement.ValueKind == JsonValueKind.Object)
+ {
+ foreach (JsonProperty property in jsonElement.EnumerateObject())
+ {
+ AddJsonPropertyToTomlTable(tomlTable, property);
+ }
+ }
+ // Handle array case
+ else if (jsonElement.ValueKind == JsonValueKind.Array)
+ {
+ var tomlArray = new TomlArray();
+ foreach (JsonElement element in jsonElement.EnumerateArray())
+ {
+ tomlArray.Add(ConvertJsonElementToTomlValue(element));
+ }
+ return new TomlTable { ["array"] = tomlArray };
+ }
+ else
+ {
+ // For simple types, add them as a direct value
+ tomlTable["value"] = ConvertJsonElementToTomlValue(jsonElement);
+ }
+
+ return tomlTable;
+ }
+
+ ///
+ /// Writes a TOML node to a .
+ ///
+ /// The JSON writer to write the TOML node to.
+ /// The key of the TOML node.
+ /// The TOML node to be written.
+ /// Thrown when a TOML types cannot be converted to a JSON type.
+ private static void WriteTomlNodeToJson(Utf8JsonWriter writer, string key, TomlNode node)
+ {
+ switch (node)
+ {
+ case TomlTable tableNode:
+ writer.WriteStartObject(key);
+ foreach (KeyValuePair kvp in tableNode.RawTable)
+ {
+ WriteTomlNodeToJson(writer, kvp.Key, kvp.Value);
+ }
+ writer.WriteEndObject();
+ break;
+ case TomlArray arrayNode:
+ writer.WriteStartArray(key);
+ foreach (TomlNode item in arrayNode)
+ {
+ WriteTomlNodeToJson(writer, item);
+ }
+ writer.WriteEndArray();
+ break;
+ case TomlString stringNode:
+ writer.WriteString(key, stringNode.Value);
+ break;
+ case TomlInteger integerNode:
+ writer.WriteNumber(key, integerNode.Value);
+ break;
+ case TomlFloat floatNode:
+ writer.WriteNumber(key, floatNode.Value);
+ break;
+ case TomlBoolean boolNode:
+ writer.WriteBoolean(key, boolNode.Value);
+ break;
+ case TomlDateTime dateTimeNode:
+ writer.WriteString(key, dateTimeNode.ToString());
+ break;
+ default:
+ throw new InvalidOperationException($"Unsupported TOML node type: {node.GetType().Name}");
+ }
+ }
+
+ private static void WriteTomlNodeToJson(Utf8JsonWriter writer, TomlNode node)
+ {
+ switch (node)
+ {
+ case TomlTable tableNode:
+ writer.WriteStartObject();
+ foreach (KeyValuePair kvp in tableNode.RawTable)
+ {
+ WriteTomlNodeToJson(writer, kvp.Key, kvp.Value);
+ }
+ writer.WriteEndObject();
+ break;
+ case TomlArray arrayNode:
+ writer.WriteStartArray();
+ foreach (TomlNode item in arrayNode)
+ {
+ WriteTomlNodeToJson(writer, item);
+ }
+ writer.WriteEndArray();
+ break;
+ case TomlString stringNode:
+ writer.WriteStringValue(stringNode.Value);
+ break;
+ case TomlInteger integerNode:
+ writer.WriteNumberValue(integerNode.Value);
+ break;
+ case TomlFloat floatNode:
+ writer.WriteNumberValue(floatNode.Value);
+ break;
+ case TomlBoolean boolNode:
+ writer.WriteBooleanValue(boolNode.Value);
+ break;
+ case TomlDateTime dateTimeNode:
+ writer.WriteStringValue(dateTimeNode.ToString());
+ break;
+ default:
+ throw new InvalidOperationException($"Unsupported TOML node type: {node.GetType().Name}");
+ }
+ }
+
+ ///
+ /// Recursively adds JSON properties to the TOML table.
+ ///
+ /// The TOML table to add the JSON properties to.
+ /// The JSON property being processed.
+ /// Thrown when a JSON type cannot be converted to TOML.
+ private void AddJsonPropertyToTomlTable(TomlTable table, JsonProperty property)
+ {
+ switch (property.Value.ValueKind)
+ {
+ case JsonValueKind.Object:
+ var subTable = new TomlTable();
+ foreach (JsonProperty subProperty in property.Value.EnumerateObject())
+ {
+ AddJsonPropertyToTomlTable(subTable, subProperty);
+ }
+ table[property.Name] = subTable;
+ break;
+ case JsonValueKind.Array:
+ var tomlArray = new TomlArray
+ {
+ IsTableArray = true
+ };
+
+ foreach (JsonElement item in property.Value.EnumerateArray())
+ {
+ tomlArray.Add(ConvertJsonElementToToml(item));
+ }
+ table[property.Name] = tomlArray;
+ break;
+ case JsonValueKind.String:
+ table[property.Name] = property.Value.GetString();
+ break;
+ case JsonValueKind.Number:
+ if (property.Value.TryGetInt64(out long intValue))
+ {
+ table[property.Name] = intValue;
+ }
+ else
+ {
+ table[property.Name] = property.Value.GetDouble();
+ }
+ break;
+ case JsonValueKind.True:
+ case JsonValueKind.False:
+ table[property.Name] = property.Value.GetBoolean();
+ break;
+ default:
+ throw new InvalidOperationException($"Unsupported JSON value type: {property.Value.ValueKind}");
+ }
+ }
+
+ ///
+ /// Converts a value to the appropriate TOML node.
+ ///
+ /// The JSON element to be converted.
+ /// A TOML node.
+ /// Thrown when the JSON element contains unsupported number format or type.
+ private TomlNode ConvertJsonElementToTomlValue(JsonElement jsonElement)
+ {
+ switch (jsonElement.ValueKind)
+ {
+ case JsonValueKind.String:
+ return new TomlString { Value = jsonElement.GetString() };
+ case JsonValueKind.Number:
+ if (jsonElement.TryGetInt32(out int intValue))
+ {
+ return new TomlInteger { Value = intValue };
+ }
+ if (jsonElement.TryGetDouble(out double doubleValue))
+ {
+ return new TomlFloat { Value = doubleValue };
+ }
+ throw new InvalidOperationException("Unsupported number format.");
+ case JsonValueKind.True:
+ return new TomlBoolean { Value = true };
+ case JsonValueKind.False:
+ return new TomlBoolean { Value = false };
+ case JsonValueKind.Object:
+ return ConvertJsonElementToToml(jsonElement);
+ case JsonValueKind.Array:
+ var tomlArray = new TomlArray();
+ foreach (JsonElement element in jsonElement.EnumerateArray())
+ {
+ tomlArray.Add(ConvertJsonElementToTomlValue(element));
+ }
+ return tomlArray;
+ default:
+ throw new InvalidOperationException($"Unsupported JsonElement type: {jsonElement.ValueKind}");
+ }
+ }
+ }
+}
diff --git a/src/Task/FileHandlers/YamlFileHandler.cs b/src/Task/FileHandlers/YamlFileHandler.cs
index 06bf3aa..a140d01 100644
--- a/src/Task/FileHandlers/YamlFileHandler.cs
+++ b/src/Task/FileHandlers/YamlFileHandler.cs
@@ -7,7 +7,9 @@
namespace AggregateConfigBuildTask.FileHandlers
{
- ///
+ ///
+ /// Handles reading and writing YAML files by converting between YAML and JSON structures.
+ ///
public class YamlFileHandler : IFileHandler
{
readonly IFileSystem fileSystem;
@@ -34,7 +36,7 @@ public async ValueTask ReadInput(string inputPath)
}
///
- public void WriteOutput(JsonElement? mergedData, string outputPath)
+ public Task WriteOutput(JsonElement? mergedData, string outputPath)
{
var serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
@@ -42,7 +44,7 @@ public void WriteOutput(JsonElement? mergedData, string outputPath)
.WithTypeInspector(x => new SystemTextJsonTypeInspector(x))
.Build();
var yamlContent = serializer.Serialize(mergedData);
- fileSystem.WriteAllText(outputPath, yamlContent);
+ return fileSystem.WriteAllTextAsync(outputPath, yamlContent);
}
}
}
diff --git a/src/Task/FileSystem/FileSystem.cs b/src/Task/FileSystem/FileSystem.cs
index 3fd4c99..8d98bed 100644
--- a/src/Task/FileSystem/FileSystem.cs
+++ b/src/Task/FileSystem/FileSystem.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Threading.Tasks;
namespace AggregateConfigBuildTask
{
@@ -11,21 +12,21 @@ public string[] GetFiles(string path, string searchPattern)
}
///
- public string[] ReadAllLines(string path)
+ public async Task ReadAllTextAsync(string path)
{
- return File.ReadAllLines(path);
+ using (var reader = new StreamReader(path))
+ {
+ return await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
}
///
- public string ReadAllText(string path)
+ public async Task WriteAllTextAsync(string path, string text)
{
- return File.ReadAllText(path);
- }
-
- ///
- public void WriteAllText(string path, string text)
- {
- File.WriteAllText(path, text);
+ using (var writer = new StreamWriter(path))
+ {
+ await writer.WriteAsync(text).ConfigureAwait(false);
+ }
}
///
diff --git a/src/Task/FileSystem/IFileSystem.cs b/src/Task/FileSystem/IFileSystem.cs
index da2e91b..60fd2c7 100644
--- a/src/Task/FileSystem/IFileSystem.cs
+++ b/src/Task/FileSystem/IFileSystem.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Threading.Tasks;
namespace AggregateConfigBuildTask
{
@@ -15,26 +16,19 @@ internal interface IFileSystem
/// An array of file paths that match the specified search pattern.
string[] GetFiles(string path, string searchPattern);
- ///
- /// Reads all lines from the specified file.
- ///
- /// The path of the file to read.
- /// A string containing all the lines from the file.
- string[] ReadAllLines(string path);
-
///
/// Reads all text from the specified file.
///
/// The path of the file to read.
/// A string containing all the text from the file.
- string ReadAllText(string path);
+ Task ReadAllTextAsync(string path);
///
/// Writes the specified text to the specified file, overwriting the file if it already exists.
///
/// The path of the file to write to.
/// The text to write to the file.
- void WriteAllText(string path, string text);
+ Task WriteAllTextAsync(string path, string text);
///
/// Checks if the specified file exists at the given path.
diff --git a/src/Task/Logger/ITaskLogger.cs b/src/Task/Logger/ITaskLogger.cs
index 733c19a..cafd9a1 100644
--- a/src/Task/Logger/ITaskLogger.cs
+++ b/src/Task/Logger/ITaskLogger.cs
@@ -16,31 +16,6 @@ public interface ITaskLogger
/// Optional arguments for formatting the message.
void LogError(string message = null, params object[] messageArgs);
- ///
- /// Logs an error message with additional parameters.
- ///
- /// The subcategory of the error.
- /// The error code.
- /// The help keyword associated with the error.
- /// The file in which the error occurred.
- /// The line number where the error occurred.
- /// The column number where the error occurred.
- /// The end line number of the error.
- /// The end column number of the error.
- /// The error message to log.
- /// Optional arguments for formatting the message.
- void LogError(
- string subcategory = null,
- string errorCode = null,
- string helpKeyword = null,
- string file = null,
- int lineNumber = 0,
- int columnNumber = 0,
- int endLineNumber = 0,
- int endColumnNumber = 0,
- string message = null,
- params object[] messageArgs);
-
///
/// Logs an error message from an exception.
///
@@ -60,31 +35,6 @@ void LogErrorFromException(Exception exception,
/// Optional arguments for formatting the message.
void LogWarning(string message = null, params object[] messageArgs);
- ///
- /// Logs a warning message with additional parameters.
- ///
- /// The subcategory of the warning.
- /// The warning code.
- /// The help keyword associated with the warning.
- /// The file in which the warning occurred.
- /// The line number where the warning occurred.
- /// The column number where the warning occurred.
- /// The end line number of the warning.
- /// The end column number of the warning.
- /// The warning message to log.
- /// Optional arguments for formatting the message.
- void LogWarning(
- string subcategory = null,
- string warningCode = null,
- string helpKeyword = null,
- string file = null,
- int lineNumber = 0,
- int columnNumber = 0,
- int endLineNumber = 0,
- int endColumnNumber = 0,
- string message = null,
- params object[] messageArgs);
-
///
/// Logs a message with specified importance.
///
diff --git a/src/Task/Logger/QuietTaskLogger.cs b/src/Task/Logger/QuietTaskLogger.cs
index b0b376f..1c531cd 100644
--- a/src/Task/Logger/QuietTaskLogger.cs
+++ b/src/Task/Logger/QuietTaskLogger.cs
@@ -29,32 +29,6 @@ public void LogError(string message = null, params object[] messageArgs)
Log.LogError(message, messageArgs);
}
- ///
- public void LogError(
- string subcategory = null,
- string errorCode = null,
- string helpKeyword = null,
- string file = null,
- int lineNumber = 0,
- int columnNumber = 0,
- int endLineNumber = 0,
- int endColumnNumber = 0,
- string message = null,
- params object[] messageArgs)
- {
- Log.LogError(
- subcategory,
- errorCode,
- helpKeyword,
- file,
- lineNumber,
- columnNumber,
- endLineNumber,
- endColumnNumber,
- message,
- messageArgs);
- }
-
///
public void LogErrorFromException(Exception exception,
bool showStackTrace = false,
@@ -70,32 +44,6 @@ public void LogWarning(string message = null, params object[] messageArgs)
Log.LogWarning(message, messageArgs);
}
- ///
- public void LogWarning(
- string subcategory = null,
- string warningCode = null,
- string helpKeyword = null,
- string file = null,
- int lineNumber = 0,
- int columnNumber = 0,
- int endLineNumber = 0,
- int endColumnNumber = 0,
- string message = null,
- params object[] messageArgs)
- {
- Log.LogWarning(
- subcategory,
- warningCode,
- helpKeyword,
- file,
- lineNumber,
- columnNumber,
- endLineNumber,
- endColumnNumber,
- message,
- messageArgs);
- }
-
///
public void LogMessage(MessageImportance importance = MessageImportance.Normal, string message = null, params object[] messageArgs)
{
diff --git a/src/Task/Logger/TaskLogger.cs b/src/Task/Logger/TaskLogger.cs
index b3e4c6d..29db22a 100644
--- a/src/Task/Logger/TaskLogger.cs
+++ b/src/Task/Logger/TaskLogger.cs
@@ -27,32 +27,6 @@ public void LogError(string message = null, params object[] messageArgs)
Log.LogError(message, messageArgs);
}
- ///
- public void LogError(
- string subcategory = null,
- string errorCode = null,
- string helpKeyword = null,
- string file = null,
- int lineNumber = 0,
- int columnNumber = 0,
- int endLineNumber = 0,
- int endColumnNumber = 0,
- string message = null,
- params object[] messageArgs)
- {
- Log.LogError(
- subcategory,
- errorCode,
- helpKeyword,
- file,
- lineNumber,
- columnNumber,
- endLineNumber,
- endColumnNumber,
- message,
- messageArgs);
- }
-
///
public void LogErrorFromException(Exception exception,
bool showStackTrace = false,
@@ -68,32 +42,6 @@ public void LogWarning(string message = null, params object[] messageArgs)
Log.LogWarning(message, messageArgs);
}
- ///
- public void LogWarning(
- string subcategory = null,
- string warningCode = null,
- string helpKeyword = null,
- string file = null,
- int lineNumber = 0,
- int columnNumber = 0,
- int endLineNumber = 0,
- int endColumnNumber = 0,
- string message = null,
- params object[] messageArgs)
- {
- Log.LogWarning(
- subcategory,
- warningCode,
- helpKeyword,
- file,
- lineNumber,
- columnNumber,
- endLineNumber,
- endColumnNumber,
- message,
- messageArgs);
- }
-
///
public void LogMessage(MessageImportance importance = MessageImportance.Normal, string message = null, params object[] messageArgs)
{
diff --git a/src/Task/ObjectManager.cs b/src/Task/ObjectManager.cs
index 5fb11cb..f41907f 100644
--- a/src/Task/ObjectManager.cs
+++ b/src/Task/ObjectManager.cs
@@ -1,12 +1,12 @@
-using AggregateConfigBuildTask.FileHandlers;
-using Microsoft.Build.Framework;
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
+using AggregateConfigBuildTask.FileHandlers;
+using Microsoft.Build.Framework;
namespace AggregateConfigBuildTask
{
@@ -29,14 +29,33 @@ internal static class ObjectManager
ITaskLogger log)
{
var finalResults = new ConcurrentBag();
+ IEnumerable> fileGroups = null;
JsonElement? finalResult = null;
bool hasError = false;
- var expectedExtensions = FileHandlerFactory.GetExpectedFileExtensions(inputType);
- var fileGroups = fileSystem.GetFiles(fileObjectDirectoryPath, "*.*")
- .Where(file => expectedExtensions.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase))
- .ToList()
- .Chunk(100);
+ if (fileSystem.DirectoryExists(fileObjectDirectoryPath))
+ {
+ var expectedExtensions = FileHandlerFactory.GetExpectedFileExtensions(inputType);
+ fileGroups = fileSystem.GetFiles(fileObjectDirectoryPath, "*.*")
+ .Where(file => expectedExtensions.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase))
+ .ToList()
+ .Chunk(100);
+ }
+ else if (fileSystem.FileExists(fileObjectDirectoryPath))
+ {
+ fileGroups = new List>
+ {
+ new List
+ {
+ fileObjectDirectoryPath
+ }
+ };
+ }
+ else
+ {
+ log.LogError("The provided path was not found as a directory or file: {0}", fileObjectDirectoryPath);
+ return null;
+ }
await fileGroups.ForEachAsync(Environment.ProcessorCount,
async (files) => {
diff --git a/src/ThirdPartyNotices.txt b/src/ThirdPartyNotices.txt
index b78dc9a..8b9f3c4 100644
--- a/src/ThirdPartyNotices.txt
+++ b/src/ThirdPartyNotices.txt
@@ -4,7 +4,8 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
AggregateConfigBuildTask is based on or incorporates material from the projects listed below.
1. YamlDotNet (https://github.com/aaubry/YamlDotNet)
-2. YamlDotNet.System.Text.Json (https://github.com/adamabdelhamed/PowerArgs)
+2. YamlDotNet.System.Text.Json (https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json)
+3. Tommy (https://github.com/dezhidki/Tommy)
%% YamlDotNet NOTICES AND INFORMATION BEGIN HERE
@@ -58,3 +59,29 @@ SOFTWARE.
=========================================
END OF YamlDotNet.System.Text.Json NOTICES AND INFORMATION
+
+%% Tommy NOTICES AND INFORMATION BEGIN HERE
+=========================================
+MIT License
+
+Copyright (c) 2020 Denis Zhidkikh
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+=========================================
+END OF Tommy NOTICES AND INFORMATION
diff --git a/src/UnitTests/Data/DemoData.cs b/src/UnitTests/Data/DemoData.cs
new file mode 100644
index 0000000..5904cb6
--- /dev/null
+++ b/src/UnitTests/Data/DemoData.cs
@@ -0,0 +1,101 @@
+using System;
+
+namespace AggregateConfigBuildTask.Tests.Unit
+{
+ internal static class DemoData
+ {
+ public static string GetSampleDataForType(string type)
+ {
+ return type switch
+ {
+ "JSON" => """
+{
+ "options": [
+ {
+ "name": "Option 1",
+ "description": "First option",
+ "isTrue": true,
+ "number": 100,
+ "nested": [
+ {
+ "name": "Nested option 1",
+ "description": "Nested first option",
+ "isTrue": true,
+ "number": 1001
+ },
+ {
+ "name": "Nested option 2",
+ "description": "Nested second option"
+ }
+ ]
+ }
+ ]
+}
+""",
+ "ARM" => """
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "options": {
+ "type": "array",
+ "value": [
+ {
+ "name": "Option 1",
+ "description": "First option",
+ "isTrue": true,
+ "number": 100,
+ "nested": [
+ {
+ "name": "Nested option 1",
+ "description": "Nested first option",
+ "isTrue": true,
+ "number": 1002
+ },
+ {
+ "name": "Nested option 2",
+ "description": "Nested second option"
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
+""",
+ "YML" => @"options:
+- name: Option 1
+ description: First option
+ isTrue: true
+ number: 100
+ nested:
+ - name: Nested option 1
+ description: Nested first option
+ isTrue: true
+ number: 1003
+ - name: Nested option 2
+ description: Nested second option
+",
+ "TOML" => """
+[[options]]
+name = "Option 1"
+description = "First option"
+isTrue = true
+number = 100
+
+[[options.nested]]
+name = "Nested option 1"
+description = "Nested first option"
+isTrue = true
+number = 1004
+
+[[options.nested]]
+name = "Nested option 2"
+description = "Nested second option"
+
+""",
+ _ => throw new InvalidOperationException($"Unknown type: {type}")
+ };
+ }
+ }
+}
diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs
index 6f42c64..70cbf8c 100644
--- a/src/UnitTests/TaskTestBase.cs
+++ b/src/UnitTests/TaskTestBase.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using System.Text;
+using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Moq;
using Newtonsoft.Json;
@@ -33,24 +35,24 @@ public void Cleanup()
foreach (var invocation in mockLogger.Invocations)
{
var methodName = invocation.Method.Name;
- var arguments = string.Join(", ", invocation.Arguments);
+ var arguments = string.Join(", ", JsonConvert.SerializeObject(invocation.Arguments));
TestContext.WriteLine($"Logger call: {methodName}({arguments})");
}
}
[TestMethod]
[Description("Test that YAML files are merged into correct JSON output.")]
- public void ShouldGenerateJsonOutput()
+ public async Task ShouldGenerateJsonOutput()
{
// Arrange: Prepare sample YAML data in the mock file system.
- virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @"
+ await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @"
options:
- name: 'Option 1'
- description: 'First option'");
- virtualFileSystem.WriteAllText($"{testPath}\\file2.yml", @"
+ description: 'First option'").ConfigureAwait(false);
+ await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file2.yml", @"
options:
- name: 'Option 2'
- description: 'Second option'");
+ description: 'Second option'").ConfigureAwait(false);
var task = new AggregateConfig(virtualFileSystem, mockLogger.Object)
{
@@ -66,7 +68,7 @@ public void ShouldGenerateJsonOutput()
// Assert: Check that output was generated correctly.
Assert.IsTrue(result);
- string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json");
+ string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false);
var json = JsonConvert.DeserializeObject>(output);
Assert.IsTrue(json.ContainsKey("options"));
Assert.AreEqual(2, ((IEnumerable