diff --git a/csharp/Platform.Converters.CLI.Tests/ConverterTests.cs b/csharp/Platform.Converters.CLI.Tests/ConverterTests.cs new file mode 100644 index 0000000..7e21c6c --- /dev/null +++ b/csharp/Platform.Converters.CLI.Tests/ConverterTests.cs @@ -0,0 +1,164 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Platform.Converters.Plugins.Examples; +using Xunit; + +namespace Platform.Converters.CLI.Tests +{ + /// + /// Tests for converter implementations. + /// Тесты для реализаций конвертеров. + /// + public class ConverterTests + { + /// + /// Tests JSON to XML conversion. + /// Тестирует конверсию JSON в XML. + /// + [Fact] + public async Task JsonToXmlConverter_ShouldConvertJsonToXml() + { + // Arrange + var converter = new JsonToXmlConverter(); + var jsonContent = """{"name": "test", "value": 42, "active": true}"""; + var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + var targetStream = new MemoryStream(); + + // Act + await converter.ConvertAsync(sourceStream, targetStream, ".json", ".xml"); + + // Assert + targetStream.Position = 0; + var result = Encoding.UTF8.GetString(targetStream.ToArray()); + + Assert.Contains("test", result); + Assert.Contains("42", result); + Assert.Contains("true", result); + } + + /// + /// Tests XML to JSON conversion. + /// Тестирует конверсию XML в JSON. + /// + [Fact] + public async Task XmlToJsonConverter_ShouldConvertXmlToJson() + { + // Arrange + var converter = new XmlToJsonConverter(); + var xmlContent = """ + + + test + 42 + true + + """; + var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlContent)); + var targetStream = new MemoryStream(); + + // Act + await converter.ConvertAsync(sourceStream, targetStream, ".xml", ".json"); + + // Assert + targetStream.Position = 0; + var result = Encoding.UTF8.GetString(targetStream.ToArray()); + + Assert.Contains("\"name\": \"test\"", result); + Assert.Contains("\"value\": 42", result); + Assert.Contains("\"active\": true", result); + } + + /// + /// Tests text case conversion to uppercase. + /// Тестирует конверсию регистра текста в верхний. + /// + [Fact] + public async Task TextToUppercaseConverter_ShouldConvertToUppercase() + { + // Arrange + var converter = new TextToUppercaseConverter(); + var textContent = "Hello World!\nThis is a test."; + var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(textContent)); + var targetStream = new MemoryStream(); + + // Act + await converter.ConvertAsync(sourceStream, targetStream, ".txt", ".txt"); + + // Assert + targetStream.Position = 0; + var result = Encoding.UTF8.GetString(targetStream.ToArray()); + + Assert.Contains("HELLO WORLD!", result); + Assert.Contains("THIS IS A TEST.", result); + } + + /// + /// Tests text case conversion to lowercase. + /// Тестирует конверсию регистра текста в нижний. + /// + [Fact] + public async Task TextToLowercaseConverter_ShouldConvertToLowercase() + { + // Arrange + var converter = new TextToLowercaseConverter(); + var textContent = "HELLO WORLD!\nTHIS IS A TEST."; + var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(textContent)); + var targetStream = new MemoryStream(); + + // Act + await converter.ConvertAsync(sourceStream, targetStream, ".txt", ".txt"); + + // Assert + targetStream.Position = 0; + var result = Encoding.UTF8.GetString(targetStream.ToArray()); + + Assert.Contains("hello world!", result); + Assert.Contains("this is a test.", result); + } + + /// + /// Tests converter capability checking. + /// Тестирует проверку возможностей конвертера. + /// + [Fact] + public void Converters_CanConvert_ShouldReturnCorrectCapabilities() + { + // Arrange + var jsonToXml = new JsonToXmlConverter(); + var xmlToJson = new XmlToJsonConverter(); + var textToUpper = new TextToUppercaseConverter(); + + // Act & Assert + Assert.True(jsonToXml.CanConvert(".json", ".xml")); + Assert.False(jsonToXml.CanConvert(".xml", ".json")); + Assert.False(jsonToXml.CanConvert(".txt", ".xml")); + + Assert.True(xmlToJson.CanConvert(".xml", ".json")); + Assert.False(xmlToJson.CanConvert(".json", ".xml")); + Assert.False(xmlToJson.CanConvert(".txt", ".json")); + + Assert.True(textToUpper.CanConvert(".txt", ".txt")); + Assert.True(textToUpper.CanConvert(".text", ".txt")); + Assert.False(textToUpper.CanConvert(".json", ".txt")); + } + + /// + /// Tests that converters throw exceptions for unsupported conversions. + /// Тестирует что конвертеры выбрасывают исключения для неподдерживаемых конверсий. + /// + [Fact] + public async Task Converters_UnsupportedConversion_ShouldThrowException() + { + // Arrange + var converter = new JsonToXmlConverter(); + var sourceStream = new MemoryStream(); + var targetStream = new MemoryStream(); + + // Act & Assert + await Assert.ThrowsAsync( + () => converter.ConvertAsync(sourceStream, targetStream, ".xml", ".json")); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters.CLI.Tests/Platform.Converters.CLI.Tests.csproj b/csharp/Platform.Converters.CLI.Tests/Platform.Converters.CLI.Tests.csproj new file mode 100644 index 0000000..53ce556 --- /dev/null +++ b/csharp/Platform.Converters.CLI.Tests/Platform.Converters.CLI.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8 + false + true + latest + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + \ No newline at end of file diff --git a/csharp/Platform.Converters.CLI.Tests/PluginLoaderTests.cs b/csharp/Platform.Converters.CLI.Tests/PluginLoaderTests.cs new file mode 100644 index 0000000..1205256 --- /dev/null +++ b/csharp/Platform.Converters.CLI.Tests/PluginLoaderTests.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Linq; +using Platform.Converters.CLI; +using Platform.Converters.Plugins.Examples; +using Xunit; + +namespace Platform.Converters.CLI.Tests +{ + /// + /// Tests for the PluginLoader class. + /// Тесты для класса PluginLoader. + /// + public class PluginLoaderTests + { + /// + /// Tests that converters can be found by file extensions. + /// Тестирует что конвертеры могут быть найдены по расширениям файлов. + /// + [Fact] + public void FindConverters_ShouldReturnCompatibleConverters() + { + // Arrange + var loader = new PluginLoader(); + var jsonToXmlConverter = new JsonToXmlConverter(); + var xmlToJsonConverter = new XmlToJsonConverter(); + var textConverter = new TextToUppercaseConverter(); + + // Simulate loaded converters by using reflection to add them + var loadedConvertersField = typeof(PluginLoader) + .GetField("_loadedConverters", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var loadedConverters = (System.Collections.Generic.List)loadedConvertersField!.GetValue(loader)!; + + loadedConverters.Add(jsonToXmlConverter); + loadedConverters.Add(xmlToJsonConverter); + loadedConverters.Add(textConverter); + + // Act + var jsonToXmlConverters = loader.FindConverters(".json", ".xml"); + var xmlToJsonConverters = loader.FindConverters(".xml", ".json"); + var textConverters = loader.FindConverters(".txt", ".txt"); + var noConverters = loader.FindConverters(".pdf", ".doc"); + + // Assert + Assert.Single(jsonToXmlConverters); + Assert.Contains(jsonToXmlConverter, jsonToXmlConverters); + + Assert.Single(xmlToJsonConverters); + Assert.Contains(xmlToJsonConverter, xmlToJsonConverters); + + Assert.Single(textConverters); + Assert.Contains(textConverter, textConverters); + + Assert.Empty(noConverters); + } + + /// + /// Tests that plugin loading handles non-existent directories gracefully. + /// Тестирует что загрузка плагинов корректно обрабатывает несуществующие директории. + /// + [Fact] + public void LoadPlugins_WithNonExistentDirectory_ShouldNotThrow() + { + // Arrange + var loader = new PluginLoader(); + var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + // Act & Assert + var exception = Record.Exception(() => loader.LoadPlugins(nonExistentPath)); + Assert.Null(exception); + } + + /// + /// Tests that the loader starts with no converters loaded. + /// Тестирует что загрузчик начинает без загруженных конвертеров. + /// + [Fact] + public void LoadedConverters_InitialState_ShouldBeEmpty() + { + // Arrange & Act + var loader = new PluginLoader(); + + // Assert + Assert.Empty(loader.LoadedConverters); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters.CLI/Platform.Converters.CLI.csproj b/csharp/Platform.Converters.CLI/Platform.Converters.CLI.csproj new file mode 100644 index 0000000..ed366d9 --- /dev/null +++ b/csharp/Platform.Converters.CLI/Platform.Converters.CLI.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8 + Platform.Converters.CLI + Platform.Converters.CLI + CLI tool for file conversion with plugin support + Konstantin Diachenko + Konstantin Diachenko + 0.1.0 + latest + enable + + + + + + + + \ No newline at end of file diff --git a/csharp/Platform.Converters.CLI/PluginLoader.cs b/csharp/Platform.Converters.CLI/PluginLoader.cs new file mode 100644 index 0000000..40fe1ae --- /dev/null +++ b/csharp/Platform.Converters.CLI/PluginLoader.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +namespace Platform.Converters.CLI +{ + /// + /// Loads file converter plugins from DLL files. + /// Загружает плагины конвертеров файлов из DLL файлов. + /// + public class PluginLoader + { + private readonly List _loadedConverters = new(); + + /// + /// Gets all loaded file converters. + /// Получает все загруженные конвертеры файлов. + /// + public IReadOnlyList LoadedConverters => _loadedConverters.AsReadOnly(); + + /// + /// Loads plugins from the specified directory. + /// Загружает плагины из указанной директории. + /// + /// The directory containing plugin DLL files.Директория, содержащая DLL файлы плагинов. + public void LoadPlugins(string pluginDirectory) + { + if (!Directory.Exists(pluginDirectory)) + { + Console.WriteLine($"Plugin directory '{pluginDirectory}' does not exist."); + return; + } + + var dllFiles = Directory.GetFiles(pluginDirectory, "*.dll"); + + foreach (var dllFile in dllFiles) + { + try + { + LoadPlugin(dllFile); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to load plugin from '{dllFile}': {ex.Message}"); + } + } + + Console.WriteLine($"Loaded {_loadedConverters.Count} converter(s) from {dllFiles.Length} plugin file(s)."); + } + + /// + /// Loads a single plugin from the specified DLL file. + /// Загружает один плагин из указанного DLL файла. + /// + /// The path to the plugin DLL file.Путь к DLL файлу плагина. + public void LoadPlugin(string pluginPath) + { + if (!File.Exists(pluginPath)) + { + throw new FileNotFoundException($"Plugin file '{pluginPath}' not found."); + } + + var loadContext = new AssemblyLoadContext(Path.GetFileNameWithoutExtension(pluginPath), true); + var assembly = loadContext.LoadFromAssemblyPath(pluginPath); + + var converterTypes = assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && typeof(IFileConverter).IsAssignableFrom(t)) + .ToList(); + + foreach (var converterType in converterTypes) + { + try + { + if (Activator.CreateInstance(converterType) is IFileConverter converter) + { + _loadedConverters.Add(converter); + Console.WriteLine($"Loaded converter: {converter.Name} - {converter.Description}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to instantiate converter '{converterType.Name}': {ex.Message}"); + } + } + } + + /// + /// Finds converters that can handle the specified file extension conversion. + /// Находит конвертеры, которые могут выполнить преобразование указанных расширений файлов. + /// + /// The source file extension.Расширение исходного файла. + /// The target file extension.Расширение целевого файла. + /// A list of compatible converters.Список совместимых конвертеров. + public List FindConverters(string sourceExtension, string targetExtension) + { + return _loadedConverters + .Where(c => c.CanConvert(sourceExtension, targetExtension)) + .ToList(); + } + + /// + /// Lists all available converters with their supported formats. + /// Перечисляет все доступные конвертеры с их поддерживаемыми форматами. + /// + public void ListConverters() + { + if (_loadedConverters.Count == 0) + { + Console.WriteLine("No converters loaded."); + return; + } + + Console.WriteLine("Available converters:"); + Console.WriteLine(); + + foreach (var converter in _loadedConverters) + { + Console.WriteLine($"Name: {converter.Name}"); + Console.WriteLine($"Description: {converter.Description}"); + Console.WriteLine($"Source formats: {string.Join(", ", converter.SupportedSourceExtensions)}"); + Console.WriteLine($"Target formats: {string.Join(", ", converter.SupportedTargetExtensions)}"); + Console.WriteLine(); + } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters.CLI/Program.cs b/csharp/Platform.Converters.CLI/Program.cs new file mode 100644 index 0000000..1171f1f --- /dev/null +++ b/csharp/Platform.Converters.CLI/Program.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Platform.Converters.CLI +{ + /// + /// Main program class for the CLI tool. + /// Основной класс программы для CLI инструмента. + /// + public class Program + { + /// + /// Entry point for the CLI application. + /// Точка входа для CLI приложения. + /// + /// Command line arguments.Аргументы командной строки. + /// Exit code.Код завершения. + public static async Task Main(string[] args) + { + var pluginLoader = new PluginLoader(); + + // Load plugins from default directory or environment variable + var pluginDirectory = Environment.GetEnvironmentVariable("CONVERTERS_PLUGIN_DIR") + ?? Path.Combine(AppContext.BaseDirectory, "plugins"); + + if (Directory.Exists(pluginDirectory)) + { + pluginLoader.LoadPlugins(pluginDirectory); + } + + if (args.Length == 0) + { + ShowHelp(); + return 0; + } + + var command = args[0].ToLowerInvariant(); + + try + { + return command switch + { + "convert" => await HandleConvertCommand(args, pluginLoader), + "list" => HandleListCommand(pluginLoader), + "load" => HandleLoadCommand(args, pluginLoader), + "help" or "--help" or "-h" => ShowHelp(), + _ => ShowUnknownCommandError(command) + }; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static async Task HandleConvertCommand(string[] args, PluginLoader pluginLoader) + { + if (args.Length < 3) + { + Console.WriteLine("Usage: convert [--converter ]"); + return 1; + } + + var source = args[1]; + var target = args[2]; + string? converterName = null; + + // Parse optional converter argument + for (int i = 3; i < args.Length - 1; i++) + { + if (args[i] == "--converter" || args[i] == "-c") + { + converterName = args[i + 1]; + break; + } + } + + if (!File.Exists(source)) + { + Console.WriteLine($"Source file '{source}' not found."); + return 1; + } + + var sourceExtension = Path.GetExtension(source).ToLowerInvariant(); + var targetExtension = Path.GetExtension(target).ToLowerInvariant(); + + var availableConverters = pluginLoader.FindConverters(sourceExtension, targetExtension); + + if (availableConverters.Count == 0) + { + Console.WriteLine($"No converter found for {sourceExtension} -> {targetExtension}"); + Console.WriteLine("Use 'list' command to see available converters."); + return 1; + } + + var selectedConverter = availableConverters.First(); + + if (!string.IsNullOrEmpty(converterName)) + { + var namedConverter = availableConverters.FirstOrDefault(c => + c.Name.Equals(converterName, StringComparison.OrdinalIgnoreCase)); + + if (namedConverter == null) + { + Console.WriteLine($"Converter '{converterName}' not found or not compatible."); + Console.WriteLine("Available converters for this conversion:"); + foreach (var conv in availableConverters) + { + Console.WriteLine($" - {conv.Name}: {conv.Description}"); + } + return 1; + } + + selectedConverter = namedConverter; + } + + Console.WriteLine($"Converting '{source}' to '{target}' using '{selectedConverter.Name}'..."); + + try + { + // Ensure target directory exists + var targetDir = Path.GetDirectoryName(target); + if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + await selectedConverter.ConvertAsync(source, target); + Console.WriteLine("Conversion completed successfully."); + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Conversion failed: {ex.Message}"); + return 1; + } + } + + private static int HandleListCommand(PluginLoader pluginLoader) + { + pluginLoader.ListConverters(); + return 0; + } + + private static int HandleLoadCommand(string[] args, PluginLoader pluginLoader) + { + if (args.Length < 2) + { + Console.WriteLine("Usage: load "); + return 1; + } + + var pluginFile = args[1]; + + if (!File.Exists(pluginFile)) + { + Console.WriteLine($"Plugin file '{pluginFile}' not found."); + return 1; + } + + pluginLoader.LoadPlugin(pluginFile); + Console.WriteLine($"Plugin loaded successfully from '{Path.GetFileName(pluginFile)}'"); + return 0; + } + + private static int ShowHelp() + { + Console.WriteLine("Platform Converters CLI - File conversion tool with plugin support"); + Console.WriteLine(); + Console.WriteLine("Usage:"); + Console.WriteLine(" convert [--converter ] Convert a file"); + Console.WriteLine(" list List available converters"); + Console.WriteLine(" load Load a plugin DLL"); + Console.WriteLine(" help Show this help"); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine(" convert input.json output.xml"); + Console.WriteLine(" convert input.json output.xml --converter \"JSON to XML Converter\""); + Console.WriteLine(" list"); + Console.WriteLine(" load MyConverter.dll"); + Console.WriteLine(); + Console.WriteLine("Environment Variables:"); + Console.WriteLine(" CONVERTERS_PLUGIN_DIR Directory to load plugins from (default: ./plugins)"); + + return 0; + } + + private static int ShowUnknownCommandError(string command) + { + Console.WriteLine($"Unknown command: {command}"); + Console.WriteLine("Use 'help' to see available commands."); + return 1; + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters.CLI/README.md b/csharp/Platform.Converters.CLI/README.md new file mode 100644 index 0000000..d47539f --- /dev/null +++ b/csharp/Platform.Converters.CLI/README.md @@ -0,0 +1,137 @@ +# Platform Converters CLI + +A command-line tool for file conversion with plugin support, implementing the solution for [issue #67](https://github.com/linksplatform/Converters/issues/67). + +## Features + +- **Plugin System**: Dynamically load converter DLL plugins +- **Multiple Converters**: Support for various file format conversions +- **CLI Interface**: Easy-to-use command-line interface +- **Extensible**: Add new converters by implementing the `IFileConverter` interface + +## Installation + +Build the project using .NET 8: + +```bash +dotnet build Platform.Converters.CLI.csproj +``` + +## Usage + +### Basic Commands + +```bash +# Show help +dotnet Platform.Converters.CLI.dll help + +# List available converters +dotnet Platform.Converters.CLI.dll list + +# Convert a file +dotnet Platform.Converters.CLI.dll convert input.json output.xml + +# Convert with specific converter +dotnet Platform.Converters.CLI.dll convert input.json output.xml --converter "JSON to XML Converter" + +# Load a plugin +dotnet Platform.Converters.CLI.dll load MyPlugin.dll +``` + +### Environment Variables + +- `CONVERTERS_PLUGIN_DIR`: Directory to load plugins from (default: `./plugins`) + +## Plugin Development + +### Creating a Converter Plugin + +1. Create a new .NET library project +2. Reference the `Platform.Converters` project +3. Implement the `IFileConverter` interface: + +```csharp +using Platform.Converters; +using System.IO; +using System.Threading.Tasks; + +public class MyConverter : IFileConverter +{ + public string Name => "My Custom Converter"; + public string Description => "Converts format A to format B"; + public string[] SupportedSourceExtensions => new[] { ".a" }; + public string[] SupportedTargetExtensions => new[] { ".b" }; + + public bool CanConvert(string sourceExtension, string targetExtension) + { + return sourceExtension == ".a" && targetExtension == ".b"; + } + + public async Task ConvertAsync(string sourcePath, string targetPath) + { + // File-based conversion implementation + } + + public async Task ConvertAsync(Stream sourceStream, Stream targetStream, + string sourceExtension, string targetExtension) + { + // Stream-based conversion implementation + } +} +``` + +4. Build the plugin DLL +5. Copy to the plugins directory or load using the `load` command + +## Built-in Converters + +The example plugins project includes: + +- **JSON to XML Converter**: Converts JSON files to XML format +- **XML to JSON Converter**: Converts XML files to JSON format +- **Text to Uppercase Converter**: Converts text files to uppercase +- **Text to Lowercase Converter**: Converts text files to lowercase + +## Architecture + +``` +Platform.Converters.CLI/ +├── Program.cs # Main CLI entry point +├── PluginLoader.cs # Plugin loading system +└── IFileConverter.cs # Converter interface (in Platform.Converters) + +Platform.Converters.Plugins.Examples/ +├── JsonToXmlConverter.cs # JSON ↔ XML conversion +├── XmlToJsonConverter.cs +├── TextCaseConverter.cs # Text case conversion +└── ... + +Platform.Converters.CLI.Tests/ +├── PluginLoaderTests.cs # Unit tests for plugin loader +├── ConverterTests.cs # Unit tests for converters +└── ... +``` + +## Examples + +See the `examples/` directory for sample files and usage scenarios. + +## Testing + +Run the test suite: + +```bash +dotnet test Platform.Converters.CLI.Tests/Platform.Converters.CLI.Tests.csproj +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Add your converter implementation +4. Add tests for your converter +5. Submit a pull request + +## License + +This project follows the same license as the Platform.Converters project. \ No newline at end of file diff --git a/csharp/Platform.Converters.Plugins.Examples/JsonToXmlConverter.cs b/csharp/Platform.Converters.Plugins.Examples/JsonToXmlConverter.cs new file mode 100644 index 0000000..e74723d --- /dev/null +++ b/csharp/Platform.Converters.Plugins.Examples/JsonToXmlConverter.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +namespace Platform.Converters.Plugins.Examples +{ + /// + /// Converts JSON files to XML format. + /// Конвертирует JSON файлы в XML формат. + /// + public class JsonToXmlConverter : IFileConverter + { + /// + public string Name => "JSON to XML Converter"; + + /// + public string Description => "Converts JSON files to XML format"; + + /// + public string[] SupportedSourceExtensions => new[] { ".json" }; + + /// + public string[] SupportedTargetExtensions => new[] { ".xml" }; + + /// + public bool CanConvert(string sourceExtension, string targetExtension) + { + return sourceExtension.Equals(".json", StringComparison.OrdinalIgnoreCase) && + targetExtension.Equals(".xml", StringComparison.OrdinalIgnoreCase); + } + + /// + public async Task ConvertAsync(string sourcePath, string targetPath) + { + using var sourceStream = File.OpenRead(sourcePath); + using var targetStream = File.Create(targetPath); + + await ConvertAsync(sourceStream, targetStream, ".json", ".xml"); + } + + /// + public async Task ConvertAsync(Stream sourceStream, Stream targetStream, string sourceExtension, string targetExtension) + { + if (!CanConvert(sourceExtension, targetExtension)) + { + throw new NotSupportedException($"Conversion from {sourceExtension} to {targetExtension} is not supported."); + } + + var jsonDocument = await JsonDocument.ParseAsync(sourceStream); + var xmlDocument = ConvertJsonToXml(jsonDocument.RootElement, "root"); + + await using var writer = XmlWriter.Create(targetStream, new XmlWriterSettings + { + Async = true, + Indent = true, + IndentChars = " ", + CloseOutput = false + }); + + await xmlDocument.WriteToAsync(writer, default); + } + + private static XDocument ConvertJsonToXml(JsonElement element, string elementName) + { + var xmlDoc = new XDocument(); + xmlDoc.Add(CreateXElement(element, elementName)); + return xmlDoc; + } + + private static XElement CreateXElement(JsonElement element, string name) + { + var xElement = new XElement(SanitizeXmlName(name)); + + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + xElement.Add(CreateXElement(property.Value, property.Name)); + } + break; + + case JsonValueKind.Array: + var index = 0; + foreach (var item in element.EnumerateArray()) + { + xElement.Add(CreateXElement(item, $"item_{index++}")); + } + break; + + case JsonValueKind.String: + xElement.Value = element.GetString() ?? ""; + break; + + case JsonValueKind.Number: + xElement.Value = element.ToString(); + break; + + case JsonValueKind.True: + case JsonValueKind.False: + xElement.Value = element.GetBoolean().ToString().ToLowerInvariant(); + break; + + case JsonValueKind.Null: + xElement.SetAttributeValue("null", "true"); + break; + + default: + xElement.Value = element.ToString(); + break; + } + + return xElement; + } + + private static string SanitizeXmlName(string name) + { + // Replace invalid XML name characters with underscores + var sanitized = System.Text.RegularExpressions.Regex.Replace(name, @"[^\w\-_.]", "_"); + + // Ensure name starts with letter or underscore + if (!char.IsLetter(sanitized[0]) && sanitized[0] != '_') + { + sanitized = "_" + sanitized; + } + + return sanitized; + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters.Plugins.Examples/Platform.Converters.Plugins.Examples.csproj b/csharp/Platform.Converters.Plugins.Examples/Platform.Converters.Plugins.Examples.csproj new file mode 100644 index 0000000..3ab2a26 --- /dev/null +++ b/csharp/Platform.Converters.Plugins.Examples/Platform.Converters.Plugins.Examples.csproj @@ -0,0 +1,23 @@ + + + + net8 + Platform.Converters.Plugins.Examples + Platform.Converters.Plugins.Examples + Example file converter plugins + Konstantin Diachenko + Konstantin Diachenko + 0.1.0 + latest + enable + + + + + + + + + + + \ No newline at end of file diff --git a/csharp/Platform.Converters.Plugins.Examples/TextCaseConverter.cs b/csharp/Platform.Converters.Plugins.Examples/TextCaseConverter.cs new file mode 100644 index 0000000..0ab6ff2 --- /dev/null +++ b/csharp/Platform.Converters.Plugins.Examples/TextCaseConverter.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Platform.Converters.Plugins.Examples +{ + /// + /// Converts text files between different case formats (uppercase, lowercase). + /// Конвертирует текстовые файлы между различными форматами регистра (верхний, нижний). + /// + public class TextCaseConverter : IFileConverter + { + private readonly bool _toUpperCase; + + /// + /// Initializes a new instance of the TextCaseConverter class. + /// Инициализирует новый экземпляр класса TextCaseConverter. + /// + /// If true, converts to uppercase; otherwise, converts to lowercase.Если true, конвертирует в верхний регистр; иначе в нижний регистр. + public TextCaseConverter(bool toUpperCase = false) + { + _toUpperCase = toUpperCase; + } + + /// + public string Name => _toUpperCase ? "Text to Uppercase Converter" : "Text to Lowercase Converter"; + + /// + public string Description => _toUpperCase + ? "Converts text files to uppercase" + : "Converts text files to lowercase"; + + /// + public string[] SupportedSourceExtensions => new[] { ".txt", ".text", ".log" }; + + /// + public string[] SupportedTargetExtensions => new[] { ".txt", ".text", ".log" }; + + /// + public bool CanConvert(string sourceExtension, string targetExtension) + { + var supportedExtensions = new[] { ".txt", ".text", ".log" }; + + return Array.Exists(supportedExtensions, ext => + ext.Equals(sourceExtension, StringComparison.OrdinalIgnoreCase)) && + Array.Exists(supportedExtensions, ext => + ext.Equals(targetExtension, StringComparison.OrdinalIgnoreCase)); + } + + /// + public async Task ConvertAsync(string sourcePath, string targetPath) + { + using var sourceStream = File.OpenRead(sourcePath); + using var targetStream = File.Create(targetPath); + + await ConvertAsync(sourceStream, targetStream, + Path.GetExtension(sourcePath), + Path.GetExtension(targetPath)); + } + + /// + public async Task ConvertAsync(Stream sourceStream, Stream targetStream, string sourceExtension, string targetExtension) + { + if (!CanConvert(sourceExtension, targetExtension)) + { + throw new NotSupportedException($"Conversion from {sourceExtension} to {targetExtension} is not supported."); + } + + using var reader = new StreamReader(sourceStream, leaveOpen: true); + await using var writer = new StreamWriter(targetStream, leaveOpen: true); + + string? line; + while ((line = await reader.ReadLineAsync()) != null) + { + var convertedLine = _toUpperCase ? line.ToUpperInvariant() : line.ToLowerInvariant(); + await writer.WriteLineAsync(convertedLine); + } + + await writer.FlushAsync(); + } + } + + /// + /// Converts text files to uppercase. + /// Конвертирует текстовые файлы в верхний регистр. + /// + public class TextToUppercaseConverter : TextCaseConverter + { + /// + /// Initializes a new instance of the TextToUppercaseConverter class. + /// Инициализирует новый экземпляр класса TextToUppercaseConverter. + /// + public TextToUppercaseConverter() : base(true) { } + } + + /// + /// Converts text files to lowercase. + /// Конвертирует текстовые файлы в нижний регистр. + /// + public class TextToLowercaseConverter : TextCaseConverter + { + /// + /// Initializes a new instance of the TextToLowercaseConverter class. + /// Инициализирует новый экземпляр класса TextToLowercaseConverter. + /// + public TextToLowercaseConverter() : base(false) { } + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters.Plugins.Examples/XmlToJsonConverter.cs b/csharp/Platform.Converters.Plugins.Examples/XmlToJsonConverter.cs new file mode 100644 index 0000000..234ff3e --- /dev/null +++ b/csharp/Platform.Converters.Plugins.Examples/XmlToJsonConverter.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Platform.Converters.Plugins.Examples +{ + /// + /// Converts XML files to JSON format. + /// Конвертирует XML файлы в JSON формат. + /// + public class XmlToJsonConverter : IFileConverter + { + /// + public string Name => "XML to JSON Converter"; + + /// + public string Description => "Converts XML files to JSON format"; + + /// + public string[] SupportedSourceExtensions => new[] { ".xml" }; + + /// + public string[] SupportedTargetExtensions => new[] { ".json" }; + + /// + public bool CanConvert(string sourceExtension, string targetExtension) + { + return sourceExtension.Equals(".xml", StringComparison.OrdinalIgnoreCase) && + targetExtension.Equals(".json", StringComparison.OrdinalIgnoreCase); + } + + /// + public async Task ConvertAsync(string sourcePath, string targetPath) + { + using var sourceStream = File.OpenRead(sourcePath); + using var targetStream = File.Create(targetPath); + + await ConvertAsync(sourceStream, targetStream, ".xml", ".json"); + } + + /// + public async Task ConvertAsync(Stream sourceStream, Stream targetStream, string sourceExtension, string targetExtension) + { + if (!CanConvert(sourceExtension, targetExtension)) + { + throw new NotSupportedException($"Conversion from {sourceExtension} to {targetExtension} is not supported."); + } + + var xmlDocument = await XDocument.LoadAsync(sourceStream, LoadOptions.None, default); + var jsonObject = ConvertXmlToJson(xmlDocument.Root!); + + await using var writer = new Utf8JsonWriter(targetStream, new JsonWriterOptions + { + Indented = true + }); + + JsonSerializer.Serialize(writer, jsonObject); + } + + private static object ConvertXmlToJson(XElement element) + { + var result = new Dictionary(); + + // Add attributes as properties + foreach (var attribute in element.Attributes()) + { + result[$"@{attribute.Name}"] = attribute.Value; + } + + // Group child elements by name + var childGroups = new Dictionary>(); + foreach (var child in element.Elements()) + { + var name = child.Name.LocalName; + if (!childGroups.ContainsKey(name)) + { + childGroups[name] = new List(); + } + childGroups[name].Add(child); + } + + // Convert child elements + foreach (var group in childGroups) + { + if (group.Value.Count == 1) + { + // Single element + var child = group.Value[0]; + if (child.HasElements || child.Attributes().Any()) + { + result[group.Key] = ConvertXmlToJson(child); + } + else + { + result[group.Key] = ConvertValue(child.Value); + } + } + else + { + // Multiple elements with same name - create array + var array = new List(); + foreach (var child in group.Value) + { + if (child.HasElements || child.Attributes().Any()) + { + array.Add(ConvertXmlToJson(child)); + } + else + { + array.Add(ConvertValue(child.Value)); + } + } + result[group.Key] = array; + } + } + + // If element has no children or attributes, return the text value + if (!element.HasElements && !element.Attributes().Any()) + { + return ConvertValue(element.Value); + } + + // If element has text content along with children/attributes + if (!string.IsNullOrWhiteSpace(element.Value) && (element.HasElements || element.Attributes().Any())) + { + result["#text"] = ConvertValue(element.Value.Trim()); + } + + return result; + } + + private static object ConvertValue(string value) + { + if (string.IsNullOrEmpty(value)) + return value; + + // Try to parse as number + if (long.TryParse(value, out var longValue)) + return longValue; + + if (double.TryParse(value, out var doubleValue)) + return doubleValue; + + // Try to parse as boolean + if (bool.TryParse(value, out var boolValue)) + return boolValue; + + // Return as string + return value; + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters/IFileConverter.cs b/csharp/Platform.Converters/IFileConverter.cs new file mode 100644 index 0000000..adc0200 --- /dev/null +++ b/csharp/Platform.Converters/IFileConverter.cs @@ -0,0 +1,65 @@ +using System.IO; +using System.Threading.Tasks; + +namespace Platform.Converters +{ + /// + /// Defines a file converter interface for converting files from one format to another. + /// Определяет интерфейс конвертера файлов для преобразования файлов из одного формата в другой. + /// + public interface IFileConverter + { + /// + /// Gets the name of the converter. + /// Получает имя конвертера. + /// + string Name { get; } + + /// + /// Gets the description of the converter. + /// Получает описание конвертера. + /// + string Description { get; } + + /// + /// Gets the source file extensions supported by this converter. + /// Получает расширения исходных файлов, поддерживаемые этим конвертером. + /// + string[] SupportedSourceExtensions { get; } + + /// + /// Gets the target file extensions this converter can produce. + /// Получает расширения целевых файлов, которые может создать этот конвертер. + /// + string[] SupportedTargetExtensions { get; } + + /// + /// Determines whether this converter can convert from the source extension to the target extension. + /// Определяет, может ли этот конвертер выполнить преобразование из исходного расширения в целевое расширение. + /// + /// The source file extension.Расширение исходного файла. + /// The target file extension.Расширение целевого файла. + /// True if the conversion is supported, false otherwise.True, если преобразование поддерживается, иначе false. + bool CanConvert(string sourceExtension, string targetExtension); + + /// + /// Converts a file from the source path to the target path. + /// Конвертирует файл из исходного пути в целевой путь. + /// + /// The source file path.Путь к исходному файлу. + /// The target file path.Путь к целевому файлу. + /// A task representing the asynchronous conversion operation.Задача, представляющая асинхронную операцию преобразования. + Task ConvertAsync(string sourcePath, string targetPath); + + /// + /// Converts a file stream to another stream. + /// Конвертирует поток файла в другой поток. + /// + /// The source stream.Исходный поток. + /// The target stream.Целевой поток. + /// The source file extension.Расширение исходного файла. + /// The target file extension.Расширение целевого файла. + /// A task representing the asynchronous conversion operation.Задача, представляющая асинхронную операцию преобразования. + Task ConvertAsync(Stream sourceStream, Stream targetStream, string sourceExtension, string targetExtension); + } +} \ No newline at end of file diff --git a/csharp/Platform.Converters/Platform.Converters.csproj b/csharp/Platform.Converters/Platform.Converters.csproj index 3a85c4b..b1cf7d5 100644 --- a/csharp/Platform.Converters/Platform.Converters.csproj +++ b/csharp/Platform.Converters/Platform.Converters.csproj @@ -4,7 +4,7 @@ LinksPlatform's Platform.Converters Class Library Konstantin Diachenko Platform.Converters - 0.4.0 + 0.5.0 Konstantin Diachenko net8 Platform.Converters @@ -23,7 +23,7 @@ true snupkg latest - Update target framework from net7 to net8. + Add CLI tool with plugin system for file conversions. Implement IFileConverter interface for DLL plugins. enable diff --git a/examples/converted.json b/examples/converted.json new file mode 100644 index 0000000..66bc3d4 --- /dev/null +++ b/examples/converted.json @@ -0,0 +1,13 @@ +{ + "name": "Platform Converters", + "version": "1.0.0", + "features": { + "item_0": "Plugin System", + "item_1": "CLI Tool", + "item_2": "File Conversion", + "#text": "Plugin SystemCLI ToolFile Conversion" + }, + "active": true, + "count": 42, + "#text": "Platform Converters1.0.0Plugin SystemCLI ToolFile Conversiontrue42" +} \ No newline at end of file diff --git a/examples/sample.json b/examples/sample.json new file mode 100644 index 0000000..675b586 --- /dev/null +++ b/examples/sample.json @@ -0,0 +1,11 @@ +{ + "name": "Platform Converters", + "version": "1.0.0", + "features": [ + "Plugin System", + "CLI Tool", + "File Conversion" + ], + "active": true, + "count": 42 +} \ No newline at end of file diff --git a/examples/sample.txt b/examples/sample.txt new file mode 100644 index 0000000..2e01bcc --- /dev/null +++ b/examples/sample.txt @@ -0,0 +1,4 @@ +Hello World! +This is a sample text file. +It contains multiple lines. +We can convert it to uppercase or lowercase. \ No newline at end of file diff --git a/examples/sample.xml b/examples/sample.xml new file mode 100644 index 0000000..1afac22 --- /dev/null +++ b/examples/sample.xml @@ -0,0 +1,12 @@ + + + Platform Converters + 1.0.0 + + Plugin System + CLI Tool + File Conversion + + true + 42 + \ No newline at end of file diff --git a/examples/upper.txt b/examples/upper.txt new file mode 100644 index 0000000..3f746dc --- /dev/null +++ b/examples/upper.txt @@ -0,0 +1,4 @@ +HELLO WORLD! +THIS IS A SAMPLE TEXT FILE. +IT CONTAINS MULTIPLE LINES. +WE CAN CONVERT IT TO UPPERCASE OR LOWERCASE.