diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8a9074a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,63 @@ +root = true + +[*] +charset = utf-8 +end_of_line = crlf +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{cs,csx}] +indent_size = 4 +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# C# code style +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_prefer_braces = true:suggestion +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_properties = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion + +# Naming conventions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +dotnet_naming_style.camel_case_style.capitalization = camel_case +dotnet_naming_style.underscore_camel_style.required_prefix = _ +dotnet_naming_style.underscore_camel_style.capitalization = camel_case + +dotnet_naming_rule.private_fields_should_be_underscore_camel.severity = suggestion +dotnet_naming_rule.private_fields_should_be_underscore_camel.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_underscore_camel.style = underscore_camel_style + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +[*.{xml,csproj,props,targets,config,nuspec}] +indent_size = 2 + +[*.{json,yaml,yml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{sh,bash}] +end_of_line = lf + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6ac8a2f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,45 @@ +# Auto detect text files and perform LF normalization +* text=auto eol=crlf + +# Source files +*.cs text eol=crlf diff=csharp +*.vb text eol=crlf +*.csproj text eol=crlf +*.sln text eol=crlf +*.props text eol=crlf +*.targets text eol=crlf +*.config text eol=crlf +*.json text eol=crlf +*.xml text eol=crlf +*.yaml text eol=crlf +*.yml text eol=crlf +*.md text eol=crlf +*.txt text eol=crlf +*.csv text eol=crlf +*.editorconfig text eol=crlf +*.gitattributes text eol=crlf +*.gitignore text eol=crlf + +# Shell scripts - keep LF +*.sh text eol=lf +*.bash text eol=lf + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.pdf binary +*.docx binary +*.doc binary +*.xls binary +*.xlsx binary +*.zip binary +*.7z binary +*.gz binary +*.nupkg binary +*.snupkg binary +*.dll binary +*.exe binary +*.pdb binary diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md new file mode 100644 index 0000000..a965634 --- /dev/null +++ b/.github/WORKFLOWS.md @@ -0,0 +1,83 @@ +# GitHub Actions Workflows + +This repository uses GitHub Actions for CI/CD automation. + +## Workflows + +### 1. **CI** (`ci.yml`) +Runs on every push and pull request to `develop` and `master` branches. +- ✅ Builds the solution (.NET 9) +- ✅ Runs all unit tests +- ✅ Collects code coverage +- ✅ Uploads test results as artifacts + +### 2. **CodeQL Security Scan** (`codeql.yml`) +Runs on push, pull request, and weekly schedule (Monday 03:00 UTC). +- 🔒 Analyzes C# code for security vulnerabilities +- 🔍 Detects code quality issues +- 📊 Uploads results to GitHub Security tab + +### 3. **Publish to NuGet** (`nuget-publish.yml`) +Automatically publishes to NuGet.org when a new release is created. +- 📦 Extracts version from release tag (e.g., `v1.8.0`) +- 🔨 Builds and tests the library +- 📤 Publishes to NuGet.org +- 💾 Saves package as artifact + +## Setup Instructions + +### Required Secrets + +To enable NuGet publishing, add the following secret to your repository: + +1. Go to **Settings** → **Secrets and variables** → **Actions** +2. Click **New repository secret** +3. Add: + - **Name:** `NUGET_API_KEY` + - **Value:** Your NuGet API key from https://www.nuget.org/account/apikeys + +### Creating a Release + +To publish a new version to NuGet: + +1. Update version in `src/FileConversionLibrary/FileConversionLibrary.csproj` +2. Commit and push changes +3. Create a new release on GitHub: + ```bash + git tag v1.8.0 + git push origin v1.8.0 + ``` +4. Go to GitHub → **Releases** → **Draft a new release** +5. Choose the tag `v1.8.0` +6. Add release notes +7. Click **Publish release** + +The `nuget-publish.yml` workflow will automatically: +- Extract version from the tag +- Build and test the project +- Publish to NuGet.org + +### Manual Publishing + +You can also trigger publishing manually: + +1. Go to **Actions** → **Publish to NuGet** +2. Click **Run workflow** +3. Enter version (e.g., `1.8.0`) +4. Click **Run workflow** + +## Status Badges + +Add these to your README.md: + +```markdown +[![CI](https://github.com/TheMysteriousStranger90/FileConversionLibrary/actions/workflows/ci.yml/badge.svg)](https://github.com/TheMysteriousStranger90/FileConversionLibrary/actions/workflows/ci.yml) +[![CodeQL](https://github.com/TheMysteriousStranger90/FileConversionLibrary/actions/workflows/codeql.yml/badge.svg)](https://github.com/TheMysteriousStranger90/FileConversionLibrary/actions/workflows/codeql.yml) +[![NuGet](https://img.shields.io/nuget/v/FileConversionLibrary.svg)](https://www.nuget.org/packages/FileConversionLibrary/) +``` + +## Requirements + +- **.NET 9.0** SDK +- **Ubuntu latest** runner +- **NuGet API Key** (for publishing) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f75d58a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: [ develop, master ] + pull_request: + branches: [ develop, master ] + +jobs: + build-and-test: + name: Build & Test (.NET 9) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET 9 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Restore dependencies + run: dotnet restore FileConversionLibrary.sln + + - name: Build (Release) + run: dotnet build FileConversionLibrary.sln -c Release --no-restore + + - name: Run unit tests + run: | + dotnet test FileConversionLibrary.sln \ + -c Release \ + --no-build \ + --logger "trx;LogFileName=test-results.trx" \ + --results-directory ./TestResults \ + --collect:"XPlat Code Coverage" \ + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: ./TestResults/*.trx + retention-days: 30 + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: ./TestResults/**/coverage.opencover.xml + retention-days: 30 + + - name: Build summary + if: always() + run: | + echo "### Build Summary :rocket:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **.NET Version:** 9.0" >> $GITHUB_STEP_SUMMARY + echo "- **Status:** ${{ job.status }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..c2505b6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,57 @@ +name: CodeQL Security Scan + +on: + push: + branches: [ develop, master ] + pull_request: + branches: [ develop, master ] + schedule: + # Run weekly on Monday at 03:00 UTC + - cron: '0 3 * * 1' + +jobs: + analyze: + name: Analyze (C#) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + security-events: write + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET 9 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: csharp + queries: security-and-quality + + - name: Restore dependencies + run: dotnet restore FileConversionLibrary.sln + + - name: Build for CodeQL analysis + run: dotnet build FileConversionLibrary.sln -c Release --no-restore + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:csharp" + + - name: Security scan summary + if: always() + run: | + echo "### Security Scan Summary :shield:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Language:** C#" >> $GITHUB_STEP_SUMMARY + echo "- **Status:** ${{ job.status }}" >> $GITHUB_STEP_SUMMARY + echo "- **Scan Date:** $(date -u +''%Y-%m-%d %H:%M UTC'')" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index a1ad72b..0000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,52 +0,0 @@ -# This workflow will build a .NET project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net - -name: .NET - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - build: - runs-on: ubuntu-latest - outputs: - Version: ${{ steps.gitversion.outputs.SemVer }} - CommitsSinceVersionSource: ${{ steps.gitversion.outputs.CommitsSinceVersionSource }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # fetch-depth is needed for GitVersion - - # Install and calculate the new version with GitVersion - - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.7 - with: - versionSpec: 5.x - - name: Determine Version - uses: gittools/actions/gitversion/execute@v0.9.7 - id: gitversion # step id used as reference for output values - - name: Display GitVersion outputs - run: | - echo "Version: ${{ steps.gitversion.outputs.SemVer }}" - echo "CommitsSinceVersionSource: ${{ steps.gitversion.outputs.CommitsSinceVersionSource }}" - - # Build/pack the project - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Restore dependencies - run: dotnet restore FileConversionLibrary/FileConversionLibrary.csproj - - name: Build the project - run: dotnet build FileConversionLibrary/FileConversionLibrary.csproj -c Release - - name: Pack the NuGet package - run: dotnet pack FileConversionLibrary/FileConversionLibrary.csproj -p:Version='${{ steps.gitversion.outputs.SemVer }}' -c Release - - name: Upload NuGet package as artifact - uses: actions/upload-artifact@v4 - with: - name: nugetPackage - path: FileConversionLibrary/FileConversionLibrary/bin/Release/ diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml new file mode 100644 index 0000000..9299114 --- /dev/null +++ b/.github/workflows/nuget-publish.yml @@ -0,0 +1,86 @@ +name: Publish to NuGet + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 1.8.0)' + required: true + type: string + +jobs: + publish: + name: Build and Publish to NuGet + runs-on: ubuntu-latest + environment: production + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET 9 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Extract version from tag + id: get_version + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + else + VERSION="${{ github.event.inputs.version }}" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Publishing version: $VERSION" + + - name: Update project version + run: | + VERSION="${{ steps.get_version.outputs.version }}" + sed -i "s|.*|$VERSION|" src/FileConversionLibrary/FileConversionLibrary.csproj + sed -i "s|.*|$VERSION.0|" src/FileConversionLibrary/FileConversionLibrary.csproj + sed -i "s|.*|$VERSION.0|" src/FileConversionLibrary/FileConversionLibrary.csproj + echo "Updated version to $VERSION" + + - name: Restore dependencies + run: dotnet restore FileConversionLibrary.sln + + - name: Build (Release) + run: dotnet build FileConversionLibrary.sln -c Release --no-restore + + - name: Run tests + run: dotnet test FileConversionLibrary.sln -c Release --no-build --verbosity normal + + - name: Pack NuGet package + run: dotnet pack src/FileConversionLibrary/FileConversionLibrary.csproj -c Release --no-build -o ./artifacts + + - name: Publish to NuGet.org + run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + + - name: Upload NuGet package artifact + uses: actions/upload-artifact@v4 + with: + name: nuget-package-${{ steps.get_version.outputs.version }} + path: ./artifacts/*.nupkg + retention-days: 90 + + - name: Create release summary + run: | + echo "### NuGet Package Published :package:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Package:** FileConversionLibrary" >> $GITHUB_STEP_SUMMARY + echo "- **Version:** ${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **NuGet:** https://www.nuget.org/packages/FileConversionLibrary/${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### Installation" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "dotnet add package FileConversionLibrary --version ${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 0c5ecd5..16549de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,49 @@ +## Build output bin/ obj/ +out/ + +## NuGet /packages/ +*.nupkg +*.snupkg +nupkg/ +!**/assets/ + +## IDE - Visual Studio +.vs/ +*.user +*.suo +*.userosscache +*.sln.docstates +*.DotSettings.user + +## IDE - JetBrains Rider +.idea/ +*.iml riderModule.iml /_ReSharper.Caches/ +*.DotSettings.user -.idea/ -nupkg/ \ No newline at end of file +## IDE - VS Code +.vscode/ +!.vscode/extensions.json + +## OS +.DS_Store +Thumbs.db +desktop.ini + +## Logs +*.log + +## Test results +TestResults/ +coverage/ +*.coverage +*.coveragexml + +## Temp / scripts +fix_*.py +update_*.py +*.bak diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..c8a2488 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + latest + true + true + latest-recommended + true + false + + + + + $(NoWarn);CA1303;CA1848;CA1002;NU1507 + + + + + false + $(NoWarn);CA1303;CA1848;CA1062;CA2007;CA1707 + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..e897852 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,25 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + diff --git a/FileConversionLibrary.Tests/CsvToJsonConverterTests.cs b/FileConversionLibrary.Tests/CsvToJsonConverterTests.cs deleted file mode 100644 index b481090..0000000 --- a/FileConversionLibrary.Tests/CsvToJsonConverterTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; -using Newtonsoft.Json.Linq; - -namespace FileConversionLibrary.Tests -{ - public class CsvToJsonConverterTests - { - private CsvToJsonConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new CsvToJsonConverter(); - } - - [Test] - public void Convert_GivenValidCsvData_ReturnsValidJson() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(csvData); - - // Assert - Assert.IsNotNull(result); - - // Verify the JSON can be parsed - var jsonArray = JArray.Parse(result); - Assert.AreEqual(2, jsonArray.Count); - - // Verify the structure of the first object - var firstObj = jsonArray[0]; - Assert.AreEqual("John", firstObj["Name"].ToString()); - Assert.AreEqual(30, firstObj["Age"].Value()); - - // Verify the structure of the second object - var secondObj = jsonArray[1]; - Assert.AreEqual("Jane", secondObj["Name"].ToString()); - Assert.AreEqual(25, secondObj["Age"].Value()); - } - - [Test] - public void Convert_WithConvertValuesFalse_KeepsStrings() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["convertValues"] = false - }; - - // Act - var result = _converter.Convert(csvData, options); - - // Assert - var jsonObj = JArray.Parse(result)[0]; - Assert.AreEqual("30", jsonObj["Age"].ToString()); // Should remain a string - } - - [Test] - public void Convert_WithDifferentDataTypes_ConvertsCorrectly() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "String", "Integer", "Decimal", "Boolean" }, - Rows = new List - { - new[] { "text", "42", "3.14", "true" } - } - }; - - // Act - var result = _converter.Convert(csvData); - - // Assert - var jsonObj = JArray.Parse(result)[0]; - Assert.AreEqual("text", jsonObj["String"].ToString()); - Assert.AreEqual(42, jsonObj["Integer"].Value()); - Assert.AreEqual(3.14, jsonObj["Decimal"].Value()); - Assert.IsTrue(jsonObj["Boolean"].Value()); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/CsvToPdfConverterTests.cs b/FileConversionLibrary.Tests/CsvToPdfConverterTests.cs deleted file mode 100644 index b86246f..0000000 --- a/FileConversionLibrary.Tests/CsvToPdfConverterTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; - -namespace FileConversionLibrary.Tests -{ - public class CsvToPdfConverterTests - { - private CsvToPdfConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new CsvToPdfConverter(); - } - - [Test] - public void Convert_GivenValidCsvData_ReturnsPdfBytes() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(csvData); - - // Assert - Assert.IsNotNull(result); - Assert.IsInstanceOf(result); - Assert.Greater(result.Length, 0); - - // Verify the PDF signature (%PDF-) - byte[] pdfSignature = { 0x25, 0x50, 0x44, 0x46, 0x2D }; - Assert.IsTrue(StartsWithSequence(result, pdfSignature)); - } - - [Test] - public void Convert_WithCustomOptions_ReturnsPdfBytes() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["fontSize"] = 12f, - ["addBorders"] = false - }; - - // Act - var result = _converter.Convert(csvData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - - // Helper method to check if a byte array starts with a sequence - private bool StartsWithSequence(byte[] data, byte[] sequence) - { - if (data.Length < sequence.Length) - return false; - - for (int i = 0; i < sequence.Length; i++) - { - if (data[i] != sequence[i]) - return false; - } - - return true; - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/CsvToWordConverterTests.cs b/FileConversionLibrary.Tests/CsvToWordConverterTests.cs deleted file mode 100644 index 54aee64..0000000 --- a/FileConversionLibrary.Tests/CsvToWordConverterTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; - -namespace FileConversionLibrary.Tests -{ - public class CsvToWordConverterTests - { - private CsvToWordConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new CsvToWordConverter(); - } - - [Test] - public void Convert_GivenValidCsvData_ReturnsWordBytes() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(csvData); - - // Assert - Assert.IsNotNull(result); - Assert.IsInstanceOf(result); - Assert.Greater(result.Length, 0); - - // Check for DOCX file signature (PK ZIP signature) - byte[] docxSignature = { 0x50, 0x4B, 0x03, 0x04 }; - Assert.IsTrue(StartsWithSequence(result, docxSignature)); - } - - [Test] - public void Convert_WithCustomOptions_ReturnsWordBytes() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["useTable"] = false, - ["fontFamily"] = "Arial" - }; - - // Act - var result = _converter.Convert(csvData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - - // Helper method to check if a byte array starts with a sequence - private bool StartsWithSequence(byte[] data, byte[] sequence) - { - if (data.Length < sequence.Length) - return false; - - for (int i = 0; i < sequence.Length; i++) - { - if (data[i] != sequence[i]) - return false; - } - - return true; - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/CsvToXmlConverterTests.cs b/FileConversionLibrary.Tests/CsvToXmlConverterTests.cs deleted file mode 100644 index 0cbae17..0000000 --- a/FileConversionLibrary.Tests/CsvToXmlConverterTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Xml.Linq; -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; - -namespace FileConversionLibrary.Tests -{ - public class CsvToXmlConverterTests - { - private CsvToXmlConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new CsvToXmlConverter(); - } - - [Test] - public void Convert_GivenValidCsvData_ReturnsValidXml() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(csvData); - - // Assert - Assert.IsNotNull(result); - - // Verify the XML can be parsed - var doc = XDocument.Parse(result); - Assert.IsNotNull(doc.Root); - - // Verify structure - var rows = doc.Root.Elements(); - Assert.AreEqual(2, rows.Count()); - - // Check first row content - var firstRow = rows.First(); - Assert.AreEqual("30", firstRow.Element("Age").Value); - Assert.AreEqual("John", firstRow.Element("Name").Value); - } - - [Test] - public void Convert_WithElementsFormat_CreatesCorrectStructure() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["format"] = CsvToXmlConverter.XmlOutputFormat.Elements, - ["useCData"] = false - }; - - // Act - var result = _converter.Convert(csvData, options); - - // Assert - var doc = XDocument.Parse(result); - var firstRow = doc.Root.Elements().First(); - - // Verify elements format (nested elements) - Assert.IsNull(firstRow.Attribute("Name")); - Assert.IsNotNull(firstRow.Element("Name")); - } - - [Test] - public void Convert_WithAttributesFormat_CreatesCorrectStructure() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["format"] = CsvToXmlConverter.XmlOutputFormat.Attributes - }; - - // Act - var result = _converter.Convert(csvData, options); - - // Assert - var doc = XDocument.Parse(result); - var firstRow = doc.Root.Elements().First(); - - // Verify attributes format - Assert.IsNotNull(firstRow.Attribute("Name")); - Assert.AreEqual("John", firstRow.Attribute("Name").Value); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/CsvToYamlConverterTests.cs b/FileConversionLibrary.Tests/CsvToYamlConverterTests.cs deleted file mode 100644 index 2830333..0000000 --- a/FileConversionLibrary.Tests/CsvToYamlConverterTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; -using YamlDotNet.RepresentationModel; - -namespace FileConversionLibrary.Tests -{ - public class CsvToYamlConverterTests - { - private CsvToYamlConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new CsvToYamlConverter(); - } - - [Test] - public void Convert_GivenValidCsvData_ReturnsValidYaml() - { - // Arrange - var csvData = new CsvData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(csvData); - - // Assert - Assert.IsNotNull(result); - - // Verify the YAML can be parsed - using var stringReader = new StringReader(result); - var yamlStream = new YamlStream(); - yamlStream.Load(stringReader); - - var document = yamlStream.Documents[0]; - var rootNode = document.RootNode as YamlSequenceNode; - - Assert.IsNotNull(rootNode); - Assert.AreEqual(2, rootNode.Children.Count); - - // Verify first item structure - var firstItem = rootNode.Children[0] as YamlMappingNode; - Assert.IsTrue(firstItem.Children.ContainsKey(new YamlScalarNode("Name"))); - Assert.AreEqual("John", - ((YamlScalarNode)firstItem.Children[new YamlScalarNode("Name")]).Value); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/FileConversionLibrary.Tests.csproj b/FileConversionLibrary.Tests/FileConversionLibrary.Tests.csproj deleted file mode 100644 index 0674e4d..0000000 --- a/FileConversionLibrary.Tests/FileConversionLibrary.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - - - - - diff --git a/FileConversionLibrary.Tests/GlobalUsings.cs b/FileConversionLibrary.Tests/GlobalUsings.cs deleted file mode 100644 index cefced4..0000000 --- a/FileConversionLibrary.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file diff --git a/FileConversionLibrary.Tests/XmlToCsvConverterTests.cs b/FileConversionLibrary.Tests/XmlToCsvConverterTests.cs deleted file mode 100644 index 6a458fa..0000000 --- a/FileConversionLibrary.Tests/XmlToCsvConverterTests.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.Xml.Linq; -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; - -namespace FileConversionLibrary.Tests -{ - [TestFixture] - public class XmlToCsvConverterTests - { - private XmlToCsvConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new XmlToCsvConverter(); - } - - [Test] - public void Convert_GivenValidXmlData_ReturnsValidCsv() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - - Jane - 25 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - - Assert.IsTrue(lines[0].Contains("Age") && lines[0].Contains("Name")); - - Assert.IsTrue(result.Contains("John") && result.Contains("30")); - Assert.IsTrue(result.Contains("Jane") && result.Contains("25")); - } - - [Test] - public void Convert_WithSemicolonDelimiter_UsesSemicolon() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - var options = new Dictionary - { - ["delimiter"] = ';' - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsTrue(result.Contains("Age;Name")); - Assert.IsTrue(result.Contains("30;John")); - } - - [Test] - public void Convert_WithQuoteValues_AddsQuotes() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John, Doe - 30 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - var options = new Dictionary - { - ["quoteValues"] = true - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsTrue(result.Contains("\"John, Doe\"")); - } - - [Test] - public void Convert_WithDataContainingSpecialChars_EscapesCorrectly() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - Line 1 -Line 2""quoted"" - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - var options = new Dictionary - { - ["quoteValues"] = true - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsTrue(result.Contains("\"Line 1\nLine 2\"\"quoted\"\"\"")); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - - [Test] - public void Convert_WithEmptyDocument_ThrowsArgumentException() - { - // Arrange - var xmlData = new XmlData - { - Document = new XDocument() - }; - - // Act & Assert - Assert.Throws(() => _converter.Convert(xmlData)); - } - - [Test] - public void Convert_WithTabularDataOnly_UsesTabularData() - { - // Arrange - var xmlData = new XmlData - { - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("Name,Age")); - Assert.IsTrue(result.Contains("John,30")); - Assert.IsTrue(result.Contains("Jane,25")); - } - - [Test] - public void Convert_WithAttributes_IncludesAttributes() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("@id") && result.Contains("@status")); - Assert.IsTrue(result.Contains("1") && result.Contains("active")); - } - - [Test] - public void Convert_WithFlattenHierarchyDisabled_DoesNotFlatten() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - - John - 30 - - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - var options = new Dictionary - { - ["flattenHierarchy"] = false - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("profile")); - } - - public void Convert_WithCustomNullValue_UsesCustomNull() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - - 30 - - - Jane - 25 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - var options = new Dictionary - { - ["customNullValue"] = "N/A" - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Console.WriteLine("Actual result:"); - Console.WriteLine(result); - - Assert.IsNotNull(result); - - var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - bool foundCustomNull = lines.Any(line => line.Contains("N/A")); - - Assert.IsTrue(foundCustomNull, $"Expected to find 'N/A' in result. Actual result:\n{result}"); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/XmlToJsonConverterTests.cs b/FileConversionLibrary.Tests/XmlToJsonConverterTests.cs deleted file mode 100644 index c1d8069..0000000 --- a/FileConversionLibrary.Tests/XmlToJsonConverterTests.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System.Xml.Linq; -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; -using Newtonsoft.Json.Linq; - -namespace FileConversionLibrary.Tests -{ - [TestFixture] - public class XmlToJsonConverterTests - { - private XmlToJsonConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new XmlToJsonConverter(); - } - - [Test] - public void Convert_GivenValidXmlData_ReturnsValidJson() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - - Jane - 25 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - - // Verify the JSON can be parsed - var jsonObj = JObject.Parse(result); - Assert.IsNotNull(jsonObj["root"]); - - var elements = jsonObj["root"]["element"]; - Assert.AreEqual(2, elements.Count()); - - Assert.AreEqual("John", elements[0]["Name"].ToString()); - Assert.AreEqual("30", elements[0]["Age"].ToString()); - } - - [Test] - public void Convert_WithNestedElements_PreservesStructure() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John -
- 30 -
-
-
"); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - var jsonObj = JObject.Parse(result); - var details = jsonObj["root"]["element"]["Details"]; - Assert.IsNotNull(details); - Assert.AreEqual("30", details["Age"].ToString()); - } - - [Test] - public void Convert_WithAttributes_IncludesAttributes() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - var jsonObj = JObject.Parse(result); - var elements = jsonObj["root"]["element"]; - - Assert.AreEqual("John", elements[0]["@Name"].ToString()); - Assert.AreEqual("30", elements[0]["@Age"].ToString()); - } - - [Test] - public void Convert_WithConvertValuesOption_ConvertsTypes() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - true - 9.5 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - var options = new Dictionary - { - ["convertValues"] = true - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - var jsonObj = JObject.Parse(result); - var element = jsonObj["root"]["element"]; - - // Check that Age is a number, not string - Assert.IsInstanceOf(element["Age"]); - Assert.AreEqual(JTokenType.Integer, element["Age"].Type); - Assert.AreEqual(30, element["Age"].Value()); - - // Check that Active is a boolean - Assert.IsInstanceOf(element["Active"]); - Assert.AreEqual(JTokenType.Boolean, element["Active"].Type); - Assert.AreEqual(true, element["Active"].Value()); - - // Check that Score is a decimal - Assert.IsInstanceOf(element["Score"]); - Assert.AreEqual(JTokenType.Float, element["Score"].Type); - Assert.AreEqual(9.5, element["Score"].Value()); - } - - [Test] - public void Convert_WithRemoveWhitespaceOption_RemovesEmptyTextNodes() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - - - - "); - - var xmlData = new XmlData - { - Document = xmlDoc - }; - - var options = new Dictionary - { - ["removeWhitespace"] = true - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - var jsonObj = JObject.Parse(result); - var element = jsonObj["root"]["element"]; - - // Description should still exist but not have #text node - Assert.IsNotNull(element["Description"]); - Assert.IsFalse(element["Description"].ToString().Contains("#text")); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/XmlToPdfConverterTests.cs b/FileConversionLibrary.Tests/XmlToPdfConverterTests.cs deleted file mode 100644 index 4c0dfef..0000000 --- a/FileConversionLibrary.Tests/XmlToPdfConverterTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System.Xml.Linq; -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; -using iTextSharp.text; - -namespace FileConversionLibrary.Tests -{ - [TestFixture] - public class XmlToPdfConverterTests - { - private XmlToPdfConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new XmlToPdfConverter(); - } - - [Test] - public void Convert_GivenValidXmlData_ReturnsPdfBytes() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - - Jane - 25 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - Assert.IsInstanceOf(result); - Assert.Greater(result.Length, 0); - - // Verify PDF header signature - byte[] pdfSignature = { 0x25, 0x50, 0x44, 0x46 }; // %PDF - for (int i = 0; i < pdfSignature.Length; i++) - { - Assert.AreEqual(pdfSignature[i], result[i]); - } - } - - [Test] - public void Convert_WithCustomFontSize_ReturnsPdf() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["fontSize"] = 12f - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithHierarchicalView_ReturnsPdf() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John -
- 30 -
-
-
"); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Details.Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["hierarchicalView"] = true - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithAlternateRowColors_ReturnsPdf() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - - Jane - 25 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - var options = new Dictionary - { - ["alternateRowColors"] = true, - ["headerBackgroundColor"] = new BaseColor(200, 200, 200) - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/XmlToWordConverterTests.cs b/FileConversionLibrary.Tests/XmlToWordConverterTests.cs deleted file mode 100644 index 00375cf..0000000 --- a/FileConversionLibrary.Tests/XmlToWordConverterTests.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Xml.Linq; -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; - -namespace FileConversionLibrary.Tests -{ - [TestFixture] - public class XmlToWordConverterTests - { - private XmlToWordConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new XmlToWordConverter(); - } - - [Test] - public void Convert_GivenValidXmlData_ReturnsWordBytes() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - - Jane - 25 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - Assert.IsInstanceOf(result); - Assert.Greater(result.Length, 0); - - // Check for DOCX file signature (PK ZIP signature) - byte[] docxSignature = { 0x50, 0x4B, 0x03, 0x04 }; - for (int i = 0; i < docxSignature.Length; i++) - { - Assert.AreEqual(docxSignature[i], result[i]); - } - } - - [Test] - public void Convert_WithoutTable_ReturnsWordDocument() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["useTable"] = false - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithHierarchicalFormat_ReturnsWordDocument() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John -
- 30 -
-
-
"); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Details.Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["formatAsHierarchy"] = true - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithCustomFontSettings_ReturnsWordDocument() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" } - } - }; - - var options = new Dictionary - { - ["fontFamily"] = "Arial", - ["fontSize"] = 14 - }; - - // Act - var result = _converter.Convert(xmlData, options); - - // Assert - Assert.IsNotNull(result); - Assert.Greater(result.Length, 0); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.Tests/XmlToYamlConverterTests.cs b/FileConversionLibrary.Tests/XmlToYamlConverterTests.cs deleted file mode 100644 index 9d31b68..0000000 --- a/FileConversionLibrary.Tests/XmlToYamlConverterTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Xml.Linq; -using FileConversionLibrary.Converters; -using FileConversionLibrary.Models; -using YamlDotNet.RepresentationModel; - -namespace FileConversionLibrary.Tests -{ - [TestFixture] - public class XmlToYamlConverterTests - { - private XmlToYamlConverter _converter; - - [SetUp] - public void SetUp() - { - _converter = new XmlToYamlConverter(); - } - - [Test] - public void Convert_GivenValidXmlData_ReturnsValidYaml() - { - // Arrange - var xmlDoc = XDocument.Parse(@" - - John - 30 - - - Jane - 25 - - "); - - var xmlData = new XmlData - { - Document = xmlDoc, - Headers = new[] { "Name", "Age" }, - Rows = new List - { - new[] { "John", "30" }, - new[] { "Jane", "25" } - } - }; - - // Act - var result = _converter.Convert(xmlData); - - // Assert - Assert.IsNotNull(result); - - // Verify the YAML can be parsed - using var reader = new StringReader(result); - var yamlStream = new YamlStream(); - yamlStream.Load(reader); - - // At least one document should be present - Assert.GreaterOrEqual(yamlStream.Documents.Count, 1); - Assert.IsNotNull(yamlStream.Documents[0].RootNode); - } - - [Test] - public void Convert_WithNullInput_ThrowsArgumentException() - { - // Arrange & Act & Assert - Assert.Throws(() => _converter.Convert(null)); - } - } -} \ No newline at end of file diff --git a/FileConversionLibrary.sln b/FileConversionLibrary.sln index d690967..09da685 100644 --- a/FileConversionLibrary.sln +++ b/FileConversionLibrary.sln @@ -1,28 +1,78 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConversionLibrary", "FileConversionLibrary\FileConversionLibrary.csproj", "{834D918D-1FE6-4954-B29E-E578A3D75850}" +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A1A1A1A1-A1A1-A1A1-A1A1-A1A1A1A1A1A1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConversionLibrary.Tests", "FileConversionLibrary.Tests\FileConversionLibrary.Tests.csproj", "{AD889C7E-9909-4DF4-82F0-F6741B16F043}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C3C3C3C3-C3C3-C3C3-C3C3-C3C3C3C3C3C3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConversionLibraryConsoleExample", "FileConversionLibraryConsoleExample\FileConversionLibraryConsoleExample.csproj", "{40D4EEC9-D867-40B8-AD80-69B2C085B5CB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConversionLibraryConsoleExample", "samples\FileConversionLibraryConsoleExample\FileConversionLibraryConsoleExample.csproj", "{40D4EEC9-D867-40B8-AD80-69B2C085B5CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConversionLibrary", "src\FileConversionLibrary\FileConversionLibrary.csproj", "{E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D9A44480-71ED-4501-A3C3-C75DE2082D66}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileConversionLibrary.Tests", "tests\FileConversionLibrary.Tests\FileConversionLibrary.Tests.csproj", "{E973AD6D-7910-45B0-8516-5DB2A6C239F5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {834D918D-1FE6-4954-B29E-E578A3D75850}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {834D918D-1FE6-4954-B29E-E578A3D75850}.Debug|Any CPU.Build.0 = Debug|Any CPU - {834D918D-1FE6-4954-B29E-E578A3D75850}.Release|Any CPU.ActiveCfg = Release|Any CPU - {834D918D-1FE6-4954-B29E-E578A3D75850}.Release|Any CPU.Build.0 = Release|Any CPU - {AD889C7E-9909-4DF4-82F0-F6741B16F043}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD889C7E-9909-4DF4-82F0-F6741B16F043}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD889C7E-9909-4DF4-82F0-F6741B16F043}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD889C7E-9909-4DF4-82F0-F6741B16F043}.Release|Any CPU.Build.0 = Release|Any CPU {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Debug|x64.Build.0 = Debug|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Debug|x86.Build.0 = Debug|Any CPU {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Release|Any CPU.Build.0 = Release|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Release|x64.ActiveCfg = Release|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Release|x64.Build.0 = Release|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Release|x86.ActiveCfg = Release|Any CPU + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB}.Release|x86.Build.0 = Release|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Debug|x64.Build.0 = Debug|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Debug|x86.Build.0 = Debug|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Release|Any CPU.Build.0 = Release|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Release|x64.ActiveCfg = Release|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Release|x64.Build.0 = Release|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Release|x86.ActiveCfg = Release|Any CPU + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5}.Release|x86.Build.0 = Release|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Debug|x64.Build.0 = Debug|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Debug|x86.Build.0 = Debug|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Release|Any CPU.Build.0 = Release|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Release|x64.ActiveCfg = Release|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Release|x64.Build.0 = Release|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Release|x86.ActiveCfg = Release|Any CPU + {E973AD6D-7910-45B0-8516-5DB2A6C239F5}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {40D4EEC9-D867-40B8-AD80-69B2C085B5CB} = {C3C3C3C3-C3C3-C3C3-C3C3-C3C3C3C3C3C3} + {E5E5E5E5-E5E5-E5E5-E5E5-E5E5E5E5E5E5} = {A1A1A1A1-A1A1-A1A1-A1A1-A1A1A1A1A1A1} + {E973AD6D-7910-45B0-8516-5DB2A6C239F5} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/FileConversionLibrary.sln.DotSettings.user b/FileConversionLibrary.sln.DotSettings.user index fb29774..b786381 100644 --- a/FileConversionLibrary.sln.DotSettings.user +++ b/FileConversionLibrary.sln.DotSettings.user @@ -1,4 +1,4 @@  <SessionState ContinuousTestingMode="0" IsActive="True" Name="CsvToXmlConverterTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Project Location="D:\MyProjects\FileConversionLibrary\FileConversionLibrary.Tests" Presentation="&lt;FileConversionLibrary.Tests&gt;" /> + <Project Location="D:\MyProjects\FileConversionLibrary" Presentation="&lt;tests&gt;" /> </SessionState> \ No newline at end of file diff --git a/FileConversionLibrary/FileConversionLibrary.csproj b/FileConversionLibrary/FileConversionLibrary.csproj deleted file mode 100644 index e6c443a..0000000 --- a/FileConversionLibrary/FileConversionLibrary.csproj +++ /dev/null @@ -1,93 +0,0 @@ - - - - net8.0 - enable - enable - true - FileConversionLibrary - 1.7.0 - FileConversionLibrary - Bohdan Harabadzhyu - FileConversionLibrary is a .NET library for converting files between different formats. - It supports converting CSV files to XML, PDF, Word, JSON, and YAML formats, as well as converting XML files to CSV, PDF, Word, JSON, and YAML formats. - The library provides asynchronous methods for all conversions and includes comprehensive error handling. - https://github.com/TheMysteriousStranger90/FileConversionLibrary - git - https://github.com/TheMysteriousStranger90/FileConversionLibrary - file-conversion, csv, xml - Version 1.7.0: - - (Released: 2 September 2025) - Bug fixes and improved XML parsing reliability - - Fixed a critical bug in XML parsing logic that incorrectly identified data rows in complex, nested XML files, significantly improving accuracy. - - Enhanced the heuristic for identifying repeating data records in XML to make it more robust for various document structures. - - Corrected an issue in the XML-to-CSV converter to ensure it uses the correctly parsed data, preventing inconsistent output. - - Fixed a data loss bug in the XML-to-JSON converter where child elements were being deleted when a CDATA section was present. - - General stability and reliability improvements for all XML conversion pathways. - - Simplified the `IFileReader` interface to a single, more flexible `ReadAsync` method. - - Refactored `CsvFileReader` and `XmlFileReader` to align with the new unified API, improving internal consistency and maintainability. - - **API Unification:** Standardized all file-based conversion methods in `FileConverter` to accept strongly-typed options classes (e.g., `JsonConversionOptions`, `PdfConversionOptions`) instead of long parameter lists. This creates a consistent, predictable, and extensible API across all conversion types (file, stream, and in-memory). - - Version 1.6.0: - - (Released: 6 August 2025) - Enhanced XML processing capabilities - - Added robust support for complex XML structures with nested elements - - Implemented proper handling of CDATA sections across all XML converters - - Added support for XML comments in output formats - - Enhanced handling of XML attributes and hierarchical structures - - Improved error handling for malformed XML documents - - Optimized performance for large XML documents with complex structures - - Added new configuration options for XML conversion customization - - Version 1.5.0: - - (Released: 4 August 2025) - Major update with new APIs and enhanced functionality. - - Added Stream API for direct stream-to-stream conversions without temporary files (Currently In Testing Mode). - - Introduced In-Memory conversion API for working with data objects directly (Currently In Testing Mode). - - Added comprehensive strongly-typed options classes for all conversion types. - - Enhanced performance and memory efficiency for large dataset processing. - - Added support for web applications and microservices scenarios. - - Implemented advanced configuration options for all output formats. - - true - MIT - README.md - Cover.png - - FileConversionLibrary - Advanced file format conversion library for .NET - Bohdan Harabadzhyu - FileConversionLibrary - Copyright © 2025 Bohdan Harabadzhyu - 1.7.0.0 - 1.7.0.0 - - true - latest - false - - - true - portable - true - - latest - strict - - - - - - - - - - - - - True - - - - - - - - \ No newline at end of file diff --git a/FileConversionLibrary/Models/CsvData.cs b/FileConversionLibrary/Models/CsvData.cs deleted file mode 100644 index 1880633..0000000 --- a/FileConversionLibrary/Models/CsvData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace FileConversionLibrary.Models; - -public class CsvData -{ - [Required] - public string[] Headers { get; set; } = Array.Empty(); - - [Required] - public List Rows { get; set; } = new List(); -} \ No newline at end of file diff --git a/FileConversionLibrary/Models/XmlData.cs b/FileConversionLibrary/Models/XmlData.cs deleted file mode 100644 index 33008eb..0000000 --- a/FileConversionLibrary/Models/XmlData.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Xml.Linq; - -namespace FileConversionLibrary.Models; - -public class XmlData -{ - public XDocument Document { get; set; } = new XDocument(); - - [Obsolete("Consider using Document property for better XML handling. This property is maintained for backward compatibility.")] - public string[] Headers { get; set; } = Array.Empty(); - - [Obsolete("Consider using Document property for better XML handling. This property is maintained for backward compatibility.")] - public List Rows { get; set; } = new List(); - - [Required] - public string RootElementName { get; set; } = string.Empty; - - [Required] - public string XmlVersion { get; set; } = "1.0"; - - [Required] - public string Encoding { get; set; } = "UTF-8"; -} \ No newline at end of file diff --git a/FileConversionLibraryConsoleExample/FileConversionLibraryConsoleExample.csproj b/FileConversionLibraryConsoleExample/FileConversionLibraryConsoleExample.csproj deleted file mode 100644 index 5f8ba1f..0000000 --- a/FileConversionLibraryConsoleExample/FileConversionLibraryConsoleExample.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net8.0 - enable - enable - false - false - - - - - - - diff --git a/FileConversionLibraryConsoleExample/Program.cs b/FileConversionLibraryConsoleExample/Program.cs deleted file mode 100644 index 5187320..0000000 --- a/FileConversionLibraryConsoleExample/Program.cs +++ /dev/null @@ -1,331 +0,0 @@ -using System.Xml.Linq; -using FileConversionLibrary; -using FileConversionLibrary.Models; -using FileConversionLibrary.Models.Options; - -namespace FileConversionLibraryConsoleExample; - -/// -/// This console application serves as a demonstration and testing ground -/// for the FileConversionLibrary, showcasing its various conversion capabilities. -/// -class Program -{ - /// - /// The main entry point for the application. - /// Initializes the FileConverter and runs a series of conversion tests. - /// - static async Task Main(string[] args) - { - var fileConverter = new FileConverter(); - - Console.WriteLine("=== FileConversionLibrary Test Console ===\n"); - - try - { - // Run tests for the file-based conversion API. - await TestFileBasedAPI(fileConverter); - - // Uncomment to run tests for the Stream-based conversion API. - //await TestStreamAPI(fileConverter); - - // Uncomment to run tests for the In-Memory conversion API. - //TestInMemoryAPI(fileConverter); - - Console.WriteLine("\n🎉 All tests completed successfully!"); - } - catch (Exception ex) - { - Console.WriteLine($"\n❌ An error occurred: {ex.Message}"); - Console.WriteLine($"Details: {ex}"); - } - - Console.WriteLine("\nPress any key to exit..."); - Console.ReadKey(); - } - - /// - /// Tests the primary file-to-file conversion methods of the library. - /// This demonstrates reading from a source file and writing to a destination file - /// using the new unified API with strongly-typed options classes. - /// - static async Task TestFileBasedAPI(FileConverter fileConverter) - { - Console.WriteLine("📁 Testing File-based API:"); - Console.WriteLine("==========================="); - - // --- CSV to Other Formats --- - await fileConverter.ConvertCsvToPdfAsync( - @"C:\Users\User\Desktop\csv_input.csv", @"C:\Users\User\Desktop\output1.pdf"); - Console.WriteLine("✅ CSV to PDF conversion completed."); - - await fileConverter.ConvertCsvToJsonAsync( - @"C:\Users\User\Desktop\csv_input.csv", - @"C:\Users\User\Desktop\output1.json" - ); - Console.WriteLine("✅ CSV to JSON conversion completed."); - - await fileConverter.ConvertCsvToWordAsync( - @"C:\Users\User\Desktop\csv_input.csv", - @"C:\Users\User\Desktop\output1.docx" - ); - Console.WriteLine("✅ CSV to Word conversion completed."); - - await fileConverter.ConvertCsvToXmlAsync( - @"C:\Users\User\Desktop\csv_input.csv", - @"C:\Users\User\Desktop\output1.xml" - ); - Console.WriteLine("✅ CSV to XML conversion completed."); - - await fileConverter.ConvertCsvToYamlAsync( - @"C:\Users\User\Desktop\csv_input.csv", - @"C:\Users\User\Desktop\output1.yaml" - ); - Console.WriteLine("✅ CSV to YAML conversion completed."); - - // --- XML to Other Formats --- - await fileConverter.ConvertXmlToCsvAsync( - @"C:\Users\User\Desktop\xml_input.xml", - @"C:\Users\User\Desktop\output2.csv" - ); - Console.WriteLine("✅ XML to CSV conversion completed."); - - await fileConverter.ConvertXmlToJsonAsync( - @"C:\Users\User\Desktop\xml_input.xml", - @"C:\Users\User\Desktop\output2.json" - ); - Console.WriteLine("✅ XML to JSON conversion completed."); - - await fileConverter.ConvertXmlToPdfAsync( - @"C:\Users\User\Desktop\xml_input.xml", - @"C:\Users\User\Desktop\output2.pdf" - ); - Console.WriteLine("✅ XML to PDF conversion completed."); - - await fileConverter.ConvertXmlToWordAsync( - @"C:\Users\User\Desktop\xml_input.xml", - @"C:\Users\User\Desktop\output2.docx"); - Console.WriteLine("✅ XML to Word conversion completed."); - - await fileConverter.ConvertXmlToYamlAsync( - @"C:\Users\User\Desktop\xml_input.xml", - @"C:\Users\User\Desktop\output2.yaml"); - Console.WriteLine("✅ XML to YAML conversion completed."); - - Console.WriteLine(); - } - - /// - /// Tests the stream-based conversion API, which is ideal for web applications - /// or scenarios where data is processed without being saved to disk. - /// - static async Task TestStreamAPI(FileConverter fileConverter) - { - Console.WriteLine("🌊 Testing New Stream API:"); - Console.WriteLine("=========================="); - - // Test Stream API with CSV to JSON - if (File.Exists(@"C:\Users\User\Desktop\csv_input.csv")) - { - using var inputStream = File.OpenRead(@"C:\Users\User\Desktop\csv_input.csv"); - - // Test ConvertStreamToStringAsync: Reads a stream and returns the converted content as a string. - var jsonOptions = new JsonConversionOptions - { - SourceFormat = "csv", - TargetFormat = "json", - UseIndentation = true - }; - - var jsonResult = await fileConverter.ConvertStreamToStringAsync(inputStream, jsonOptions); - await File.WriteAllTextAsync(@"C:\Users\User\Desktop\stream_output.json", jsonResult); - Console.WriteLine("✅ Stream to JSON string conversion completed."); - - // Reset stream position for the next test - inputStream.Position = 0; - - // Test ConvertStreamToBytesAsync: Reads a stream and returns the converted content as a byte array (e.g., for PDF/Word). - var pdfOptions = new PdfConversionOptions - { - SourceFormat = "csv", - TargetFormat = "pdf", - Title = "Streamed PDF Report" - }; - - var pdfBytes = await fileConverter.ConvertStreamToBytesAsync(inputStream, pdfOptions); - await File.WriteAllBytesAsync(@"C:\Users\User\Desktop\stream_output.pdf", pdfBytes); - Console.WriteLine("✅ Stream to PDF bytes conversion completed."); - - // Reset stream position for the next test - inputStream.Position = 0; - - // Test ConvertStreamAsync (Stream-to-Stream): Reads an input stream and returns a new output stream. - var xmlOptions = new XmlConversionOptions - { - SourceFormat = "csv", - TargetFormat = "xml" - }; - - using var outputStream = await fileConverter.ConvertStreamAsync(inputStream, xmlOptions); - using var fileStream = File.Create(@"C:\Users\User\Desktop\stream_output.xml"); - await outputStream.CopyToAsync(fileStream); - Console.WriteLine("✅ Stream to Stream conversion completed."); - } - else - { - Console.WriteLine("⚠️ CSV input file not found, skipping Stream API tests."); - } - - Console.WriteLine(); - } - - /// - /// Tests the in-memory conversion API, where data is represented by C# objects - /// (like CsvData or XmlData) instead of files. - /// - static void TestInMemoryAPI(FileConverter fileConverter) - { - Console.WriteLine("💾 Testing New In-Memory API:"); - Console.WriteLine("============================="); - - // Create sample CsvData object in memory. - var csvData = new CsvData - { - Headers = new[] { "ID", "Name", "Age", "City", "Salary", "Department" }, - Rows = new List - { - new[] { "1", "John Doe", "28", "New York", "75000", "Engineering" }, - new[] { "2", "Jane Smith", "32", "London", "82000", "Marketing" }, - new[] { "3", "Bob Johnson", "45", "Tokyo", "95000", "Sales" }, - new[] { "4", "Alice Brown", "29", "Berlin", "68000", "HR" }, - new[] { "5", "Charlie Wilson", "35", "Sydney", "71000", "Finance" } - } - }; - - // Test advanced JSON conversion with options - var jsonOptions = new JsonConversionOptions - { - ConvertValues = true, - UseIndentation = true - }; - var json = fileConverter.ConvertCsvToJson(csvData, jsonOptions); - File.WriteAllText(@"C:\Users\User\Desktop\inmemory_output.json", json); - Console.WriteLine("✅ In-Memory CSV to JSON conversion completed."); - - // Test advanced PDF conversion with options - var pdfOptions = new PdfConversionOptions - { - FontSize = 11f, - Title = "Employee Report", - AlternateRowColors = true - }; - var pdfBytes = fileConverter.ConvertCsvToPdf(csvData, pdfOptions); - File.WriteAllBytes(@"C:\Users\User\Desktop\inmemory_output.pdf", pdfBytes); - Console.WriteLine("✅ In-Memory CSV to PDF conversion completed."); - - // Test advanced Word conversion with options - var wordOptions = new WordConversionOptions - { - UseTable = true, - FontFamily = "Calibri", - FontSize = 11, - AlternateRowColors = true - }; - var wordBytes = fileConverter.ConvertCsvToWord(csvData, wordOptions); - File.WriteAllBytes(@"C:\Users\User\Desktop\inmemory_output.docx", wordBytes); - Console.WriteLine("✅ In-Memory CSV to Word conversion completed."); - - // Test advanced XML conversion with options - var xmlOptions = new XmlConversionOptions - { - OutputFormat = "Elements", - UseCData = false, - AddComments = true - }; - var xml = fileConverter.ConvertCsvToXml(csvData, xmlOptions); - File.WriteAllText(@"C:\Users\User\Desktop\inmemory_output.xml", xml); - Console.WriteLine("✅ In-Memory CSV to XML conversion completed."); - - // Test advanced YAML conversion with options - var yamlOptions = new YamlConversionOptions - { - Structure = "Array", - NamingConvention = "CamelCase", - ConvertDataTypes = true - }; - var yaml = fileConverter.ConvertCsvToYaml(csvData, yamlOptions); - File.WriteAllText(@"C:\Users\User\Desktop\inmemory_output.yaml", yaml); - Console.WriteLine("✅ In-Memory CSV to YAML conversion completed."); - - // Test in-memory conversions starting from XML data. - TestXmlInMemoryConversions(fileConverter); - - Console.WriteLine(); - } - - /// - /// Tests in-memory conversions that start with XML data. - /// It demonstrates the two ways to provide XML data: as a pre-parsed table (Headers/Rows) - /// or as a full XML document (XDocument). - /// - static void TestXmlInMemoryConversions(FileConverter fileConverter) - { - Console.WriteLine("\n🔄 Testing XML In-Memory Conversions:"); - - try - { - // For in-memory conversions to table-based formats (PDF, Word, CSV), - // we manually provide the headers and rows, as if they were already parsed from an XML file. - // This is the expected input for these converters. - var xmlDataForTables = new XmlData - { - Headers = new[] { "ProductID", "ProductName", "Price", "Category", "InStock" }, - Rows = new List - { - new[] { "1", "Laptop Pro", "1299.99", "Electronics", "true" }, - new[] { "2", "Office Chair", "249.50", "Furniture", "false" }, - new[] { "3", "Programming Book", "45.99", "Books", "true" }, - new[] { "4", "Wireless Mouse", "29.99", "Electronics", "true" } - }, - RootElementName = "Products" - }; - - // For conversions that work with the full XML structure (JSON, YAML), - // we provide the XDocument object directly. - var xmlContent = @"1Laptop Pro1299.99Electronicstrue2Office Chair249.50Furniturefalse"; - var xmlDataForTree = new XmlData - { - Document = XDocument.Parse(xmlContent) - }; - - // XML to CSV (uses the table-based data) - var csv = fileConverter.ConvertXmlToCsv(xmlDataForTables, new CsvConversionOptions { Delimiter = ',' }); - File.WriteAllText(@"C:\Users\User\Desktop\xml_inmemory_output.csv", csv); - Console.WriteLine("✅ In-Memory XML to CSV conversion completed."); - - // XML to JSON (uses the tree-based data) - var json = fileConverter.ConvertXmlToJson(xmlDataForTree, new JsonConversionOptions { ConvertValues = true, UseIndentation = true }); - File.WriteAllText(@"C:\Users\User\Desktop\xml_inmemory_output.json", json); - Console.WriteLine("✅ In-Memory XML to JSON conversion completed."); - - // XML to PDF (uses the table-based data) - var pdfBytes = fileConverter.ConvertXmlToPdf(xmlDataForTables, new PdfConversionOptions { Title = "Product Catalog", AlternateRowColors = true }); - File.WriteAllBytes(@"C:\Users\User\Desktop\xml_inmemory_output.pdf", pdfBytes); - Console.WriteLine("✅ In-Memory XML to PDF conversion completed."); - - // XML to Word (uses the table-based data) - var wordBytes = fileConverter.ConvertXmlToWord(xmlDataForTables, new WordConversionOptions { UseTable = true, FontFamily = "Arial" }); - File.WriteAllBytes(@"C:\Users\User\Desktop\xml_inmemory_output.docx", wordBytes); - Console.WriteLine("✅ In-Memory XML to Word conversion completed."); - - // XML to YAML (uses the tree-based data) - var yaml = fileConverter.ConvertXmlToYaml(xmlDataForTree, new YamlConversionOptions { ConvertDataTypes = true }); - File.WriteAllText(@"C:\Users\User\Desktop\xml_inmemory_output.yaml", yaml); - Console.WriteLine("✅ In-Memory XML to YAML conversion completed."); - } - catch (Exception ex) - { - Console.WriteLine($"❌ Error in XML In-Memory conversions: {ex.Message}"); - } - } -} \ No newline at end of file diff --git a/README.md b/README.md index fe32493..1d5ea33 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,26 @@ ![File Conversion Library Showcase](Screenshots/Screen1.png) -A .NET library for converting CSV and XML files to various formats including XML, PDF, Word, JSON, and YAML. Now with enhanced Stream API and In-Memory conversion capabilities! +A .NET library for converting CSV and XML files to various formats including XML, PDF, Word, JSON, and YAML. Now with +enhanced Stream API and In-Memory conversion capabilities! ## Key Features -- **Unified API**: A consistent and predictable API across file, stream, and in-memory conversions using strongly-typed options classes. -- **Robust Parsing**: Advanced heuristics to reliably parse complex and real-world CSV and XML files. -- **Multiple Conversion Modes**: - - **File-Based**: Convert files directly from disk. - - **Stream-Based**: For web applications, microservices, and data pipelines. - - **In-Memory**: High-performance, low-overhead conversions on data objects. -- **Extensive Customization**: Fine-tune every aspect of the output with detailed, format-specific options. +- **Unified API**: A consistent and predictable API across file, stream, and in-memory conversions using strongly-typed + options classes. +- **Robust Parsing**: Advanced heuristics to reliably parse complex and real-world CSV and XML files. +- **Multiple Conversion Modes**: + - **File-Based**: Convert files directly from disk. + - **Stream-Based**: For web applications, microservices, and data pipelines. + - **In-Memory**: High-performance, low-overhead conversions on data objects. +- **Extensive Customization**: Fine-tune every aspect of the output with detailed, format-specific options. ## Usage ### 1. File-Based Conversion -The most straightforward way to use the library. All methods now accept a dedicated options class for easy configuration. +The most straightforward way to use the library. All methods now accept a dedicated options class for easy +configuration. ```csharp // Create a single instance of the converter @@ -94,8 +97,8 @@ File.WriteAllBytes("in_memory_report.docx", wordBytes); The library supports two ways to provide in-memory XML data: -- **For Table-Based Formats (CSV, PDF, Word):** Provide pre-parsed `Headers` and `Rows`. -- **For Tree-Based Formats (JSON, YAML):** Provide the full `XDocument`. +- **For Table-Based Formats (CSV, PDF, Word):** Provide pre-parsed `Headers` and `Rows`. +- **For Tree-Based Formats (JSON, YAML):** Provide the full `XDocument`. ```csharp // --- For Table-Based output (e.g., PDF) --- @@ -122,12 +125,12 @@ var json = fileConverter.ConvertXmlToJson(xmlDataForTree, new JsonConversionOpti Customize your output by passing an options object to any conversion method. -- **`JsonConversionOptions`**: Control indentation, data type conversion, object nesting, and more. -- **`PdfConversionOptions`**: Set titles, font sizes, page orientation, and row styling. -- **`WordConversionOptions`**: Generate a table or hierarchical text, set fonts, and style rows. -- **`XmlConversionOptions`**: Define output structure (elements vs. attributes), naming conventions, and metadata. -- **`YamlConversionOptions`**: Choose data structure, naming conventions, and data type handling. -- **`CsvConversionOptions`**: Specify delimiters, quoting behavior, and attribute handling for XML sources. +- **`JsonConversionOptions`**: Control indentation, data type conversion, object nesting, and more. +- **`PdfConversionOptions`**: Set titles, font sizes, page orientation, and row styling. +- **`WordConversionOptions`**: Generate a table or hierarchical text, set fonts, and style rows. +- **`XmlConversionOptions`**: Define output structure (elements vs. attributes), naming conventions, and metadata. +- **`YamlConversionOptions`**: Choose data structure, naming conventions, and data type handling. +- **`CsvConversionOptions`**: Specify delimiters, quoting behavior, and attribute handling for XML sources. ## Contributing @@ -139,4 +142,4 @@ Bohdan Harabadzhyu ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/FileConversionLibrary/Cover.png b/assets/Cover.png similarity index 100% rename from FileConversionLibrary/Cover.png rename to assets/Cover.png diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..4e800bf --- /dev/null +++ b/nuget.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/samples/FileConversionLibraryConsoleExample/FileConversionLibraryConsoleExample.csproj b/samples/FileConversionLibraryConsoleExample/FileConversionLibraryConsoleExample.csproj new file mode 100644 index 0000000..8034395 --- /dev/null +++ b/samples/FileConversionLibraryConsoleExample/FileConversionLibraryConsoleExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + false + + $(NoWarn);CS0618;CA1861 + + + + + + + diff --git a/samples/FileConversionLibraryConsoleExample/Program.cs b/samples/FileConversionLibraryConsoleExample/Program.cs new file mode 100644 index 0000000..7ba6232 --- /dev/null +++ b/samples/FileConversionLibraryConsoleExample/Program.cs @@ -0,0 +1,353 @@ +using System.Text; +using System.Xml.Linq; +using FileConversionLibrary; +using FileConversionLibrary.Models; +using FileConversionLibrary.Models.Options; + +namespace FileConversionLibraryConsoleExample; + +/// +/// Demonstrates the capabilities of FileConversionLibrary with comprehensive examples +/// of file, stream, and in-memory conversion APIs. +/// +sealed class Program +{ + private static readonly FileConverter _converter = new(); + private static readonly string _desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + + static async Task Main(string[] args) + { + Console.OutputEncoding = Encoding.UTF8; + Console.WriteLine("╔═══════════════════════════════════════════════════════════╗"); + Console.WriteLine("║ FileConversionLibrary - Demonstration Console ║"); + Console.WriteLine("║ .NET 9 | v1.8.0 ║"); + Console.WriteLine("╚═══════════════════════════════════════════════════════════╝\n"); + + try + { + await RunInteractiveDemo(); + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("\n✓ All demonstrations completed successfully!"); + Console.ResetColor(); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n✗ Error: {ex.Message}"); + Console.ResetColor(); + + if (args.Contains("--verbose")) + { + Console.WriteLine($"\nStack trace:\n{ex.StackTrace}"); + } + } + + Console.WriteLine("\nPress any key to exit..."); + Console.ReadKey(); + } + + static async Task RunInteractiveDemo() + { + while (true) + { + DisplayMenu(); + var choice = Console.ReadLine()?.Trim(); + + switch (choice) + { + case "1": + await DemoFileConversions(); + break; + case "2": + await DemoStreamConversions(); + break; + case "3": + DemoInMemoryConversions(); + break; + case "4": + await DemoAdvancedOptions(); + break; + case "5": + return; + default: + Console.WriteLine("Invalid choice. Please try again.\n"); + break; + } + } + } + + static void DisplayMenu() + { + Console.WriteLine("\n┌─────────────────────────────────────────┐"); + Console.WriteLine("│ Select Demo Type │"); + Console.WriteLine("├─────────────────────────────────────────┤"); + Console.WriteLine("│ 1. File-to-File Conversions │"); + Console.WriteLine("│ 2. Stream-based Conversions │"); + Console.WriteLine("│ 3. In-Memory Conversions │"); + Console.WriteLine("│ 4. Advanced Options Examples │"); + Console.WriteLine("│ 5. Exit │"); + Console.WriteLine("└─────────────────────────────────────────┘"); + Console.Write("\nYour choice: "); + } + + static async Task DemoFileConversions() + { + Console.WriteLine("\n📁 File-to-File Conversions\n" + new string('─', 50)); + + var csvPath = Path.Combine(_desktopPath, "sample.csv"); + var xmlPath = Path.Combine(_desktopPath, "sample.xml"); + + // Create sample files if they don't exist + await EnsureSampleFiles(csvPath, xmlPath); + + // CSV Conversions + await ConvertWithLog("CSV → JSON", () => + _converter.ConvertCsvToJsonAsync(csvPath, Path.Combine(_desktopPath, "output-from-csv.json"))); + + await ConvertWithLog("CSV → PDF", () => + _converter.ConvertCsvToPdfAsync(csvPath, Path.Combine(_desktopPath, "output-from-csv.pdf"))); + + await ConvertWithLog("CSV → Word", () => + _converter.ConvertCsvToWordAsync(csvPath, Path.Combine(_desktopPath, "output-from-csv.docx"))); + + await ConvertWithLog("CSV → XML", () => + _converter.ConvertCsvToXmlAsync(csvPath, Path.Combine(_desktopPath, "output-from-csv.xml"))); + + await ConvertWithLog("CSV → YAML", () => + _converter.ConvertCsvToYamlAsync(csvPath, Path.Combine(_desktopPath, "output-from-csv.yaml"))); + + // XML Conversions + await ConvertWithLog("XML → CSV", () => + _converter.ConvertXmlToCsvAsync(xmlPath, Path.Combine(_desktopPath, "output-from-xml.csv"))); + + await ConvertWithLog("XML → JSON", () => + _converter.ConvertXmlToJsonAsync(xmlPath, Path.Combine(_desktopPath, "output-from-xml.json"))); + + await ConvertWithLog("XML → PDF", () => + _converter.ConvertXmlToPdfAsync(xmlPath, Path.Combine(_desktopPath, "output-from-xml.pdf"))); + + await ConvertWithLog("XML → Word", () => + _converter.ConvertXmlToWordAsync(xmlPath, Path.Combine(_desktopPath, "output-from-xml.docx"))); + + await ConvertWithLog("XML → YAML", () => + _converter.ConvertXmlToYamlAsync(xmlPath, Path.Combine(_desktopPath, "output-from-xml.yaml"))); + + Console.WriteLine($"\n✓ All files saved to: {_desktopPath}"); + } + + static async Task DemoStreamConversions() + { + Console.WriteLine("\n🌊 Stream-based Conversions\n" + new string('─', 50)); + + var csvPath = Path.Combine(_desktopPath, "sample.csv"); + await EnsureSampleFiles(csvPath, ""); + + // Stream to String (for text formats: JSON, YAML, XML) + using (var inputStream = File.OpenRead(csvPath)) + { + var jsonOptions = new JsonConversionOptions + { + SourceFormat = "csv", + TargetFormat = "json", + UseIndentation = true + }; + + var jsonResult = await _converter.ConvertStreamToStringAsync(inputStream, jsonOptions); + await File.WriteAllTextAsync(Path.Combine(_desktopPath, "stream-output.json"), jsonResult); + Console.WriteLine("✓ CSV → JSON (via stream)"); + } + + // Stream to Bytes (for binary formats: PDF, Word) + using (var inputStream = File.OpenRead(csvPath)) + { + var pdfOptions = new PdfConversionOptions + { + SourceFormat = "csv", + TargetFormat = "pdf", + Title = "Stream Demo Report" + }; + + var pdfBytes = await _converter.ConvertStreamToBytesAsync(inputStream, pdfOptions); + await File.WriteAllBytesAsync(Path.Combine(_desktopPath, "stream-output.pdf"), pdfBytes); + Console.WriteLine("✓ CSV → PDF (via stream)"); + } + + // Stream to Stream + using (var inputStream = File.OpenRead(csvPath)) + { + var xmlOptions = new XmlConversionOptions + { + SourceFormat = "csv", + TargetFormat = "xml", + UseCData = true + }; + + using var outputStream = await _converter.ConvertStreamAsync(inputStream, xmlOptions); + using var fileStream = File.Create(Path.Combine(_desktopPath, "stream-output.xml")); + await outputStream.CopyToAsync(fileStream); + Console.WriteLine("✓ CSV → XML (stream-to-stream)"); + } + } + + static void DemoInMemoryConversions() + { + Console.WriteLine("\n💾 In-Memory Conversions\n" + new string('─', 50)); + + // Create sample data in memory + var csvData = new CsvData + { + Headers = ["ID", "Product", "Price", "Stock"], + Rows = + [ + ["1", "Laptop", "1299.99", "15"], + ["2", "Mouse", "29.99", "150"], + ["3", "Keyboard", "79.99", "85"] + ] + }; + + // Convert CsvData to JSON + var jsonResult = _converter.ConvertCsvToJson(csvData); + File.WriteAllText(Path.Combine(_desktopPath, "memory-output.json"), jsonResult); + Console.WriteLine("✓ CsvData → JSON"); + + // Convert CsvData to XML + var xmlResult = _converter.ConvertCsvToXml(csvData); + File.WriteAllText(Path.Combine(_desktopPath, "memory-output.xml"), xmlResult); + Console.WriteLine("✓ CsvData → XML"); + + // Create XmlData in memory + var xmlDoc = XDocument.Parse(@" + + + Monitor + 299.99 + + + Webcam + 89.99 + + "); + + var xmlData = new XmlData { Document = xmlDoc }; + + // Convert XmlData to JSON + var xmlToJsonResult = _converter.ConvertXmlToJson(xmlData); + File.WriteAllText(Path.Combine(_desktopPath, "memory-xml-output.json"), xmlToJsonResult); + Console.WriteLine("✓ XmlData → JSON"); + + // Convert XmlData to CSV + var xmlToCsvResult = _converter.ConvertXmlToCsv(xmlData); + File.WriteAllText(Path.Combine(_desktopPath, "memory-xml-output.csv"), xmlToCsvResult); + Console.WriteLine("✓ XmlData → CSV"); + } + + static async Task DemoAdvancedOptions() + { + Console.WriteLine("\n⚙️ Advanced Options Examples\n" + new string('─', 50)); + + var csvPath = Path.Combine(_desktopPath, "sample.csv"); + await EnsureSampleFiles(csvPath, ""); + + // JSON with custom options + var jsonOptions = new JsonConversionOptions + { + SourceFormat = "csv", + TargetFormat = "json", + UseIndentation = true, + ConvertValues = true + }; + using (var input = File.OpenRead(csvPath)) + { + var result = await _converter.ConvertStreamToStringAsync(input, jsonOptions); + await File.WriteAllTextAsync(Path.Combine(_desktopPath, "advanced-json.json"), result); + } + + Console.WriteLine("✓ JSON with type conversion and indentation"); + + // PDF with custom options + var pdfOptions = new PdfConversionOptions + { + SourceFormat = "csv", + TargetFormat = "pdf", + Title = "Advanced PDF Report", + FontSize = 12, + LandscapeOrientation = true + }; + using (var input = File.OpenRead(csvPath)) + { + input.Position = 0; + var result = await _converter.ConvertStreamToBytesAsync(input, pdfOptions); + await File.WriteAllBytesAsync(Path.Combine(_desktopPath, "advanced.pdf"), result); + } + + Console.WriteLine("✓ PDF with landscape orientation and custom font"); + + // Word with custom options + var wordOptions = new WordConversionOptions + { + SourceFormat = "csv", + TargetFormat = "word", + UseTable = true, + AddHeaderRow = true + }; + using (var input = File.OpenRead(csvPath)) + { + input.Position = 0; + var result = await _converter.ConvertStreamToBytesAsync(input, wordOptions); + await File.WriteAllBytesAsync(Path.Combine(_desktopPath, "advanced.docx"), result); + } + + Console.WriteLine("✓ Word document with table format"); + } + + static async Task EnsureSampleFiles(string csvPath, string xmlPath) + { + // Create sample CSV + if (!File.Exists(csvPath)) + { + var csvContent = @"ID,Name,Age,City,Department,Salary +1,John Doe,28,New York,Engineering,75000 +2,Jane Smith,32,London,Marketing,82000 +3,Bob Johnson,45,Tokyo,Sales,95000 +4,Alice Brown,29,Berlin,HR,68000"; + await File.WriteAllTextAsync(csvPath, csvContent); + } + + // Create sample XML + if (!string.IsNullOrEmpty(xmlPath) && !File.Exists(xmlPath)) + { + var xmlContent = @" + + + John Doe + 28 + New York + Engineering + 75000 + + + Jane Smith + 32 + London + Marketing + 82000 + +"; + await File.WriteAllTextAsync(xmlPath, xmlContent); + } + } + + static async Task ConvertWithLog(string description, Func conversionAction) + { + try + { + await conversionAction(); + Console.WriteLine($"✓ {description}"); + } + catch (Exception ex) + { + Console.WriteLine($"✗ {description} - Error: {ex.Message}"); + } + } +} diff --git a/FileConversionLibrary/Converters/CsvToJsonConverter.cs b/src/FileConversionLibrary/Converters/CsvToJsonConverter.cs similarity index 85% rename from FileConversionLibrary/Converters/CsvToJsonConverter.cs rename to src/FileConversionLibrary/Converters/CsvToJsonConverter.cs index 993b966..073dfd9 100644 --- a/FileConversionLibrary/Converters/CsvToJsonConverter.cs +++ b/src/FileConversionLibrary/Converters/CsvToJsonConverter.cs @@ -1,4 +1,4 @@ -using FileConversionLibrary.Interfaces; +using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; using Newtonsoft.Json; using System.Globalization; @@ -73,7 +73,8 @@ public string Convert(CsvData input, object? options = null) preserveEmptyValues = preserveValue; } - if (optionsDict.TryGetValue("dateFormat", out var dateFormatObj) && dateFormatObj is string dateFormatValue) + if (optionsDict.TryGetValue("dateFormat", out var dateFormatObj) && + dateFormatObj is string dateFormatValue) { dateFormat = dateFormatValue; } @@ -91,8 +92,9 @@ public string Convert(CsvData input, object? options = null) if (groupByColumn && !string.IsNullOrEmpty(groupByColumnName)) { - return ConvertGrouped(input, groupByColumnName, convertValues, useIndentation, - preserveEmptyValues, dateFormat, convertArrays, arrayDelimiter, createNestedObjects, nestedSeparator); + return ConvertGrouped(input, groupByColumnName, convertValues, useIndentation, + preserveEmptyValues, dateFormat, convertArrays, arrayDelimiter, createNestedObjects, + nestedSeparator); } var resultList = new List>(); @@ -119,7 +121,8 @@ public string Convert(CsvData input, object? options = null) var processedValue = ProcessValue(value, convertValues, dateFormat, convertArrays, arrayDelimiter); - if (createNestedObjects && header.Contains(nestedSeparator)) + if (createNestedObjects && nestedSeparator != null && + header.Contains(nestedSeparator, StringComparison.Ordinal)) { CreateNestedObject(rowDict, header, processedValue, nestedSeparator); } @@ -142,8 +145,8 @@ public string Convert(CsvData input, object? options = null) return JsonConvert.SerializeObject(resultList, jsonSettings); } - private string ConvertGrouped(CsvData input, string groupByColumnName, bool convertValues, - bool useIndentation, bool preserveEmptyValues, string? dateFormat, bool convertArrays, + private static string ConvertGrouped(CsvData input, string groupByColumnName, bool convertValues, + bool useIndentation, bool preserveEmptyValues, string? dateFormat, bool convertArrays, string? arrayDelimiter, bool createNestedObjects, string? nestedSeparator) { var groupColumnIndex = Array.IndexOf(input.Headers, groupByColumnName); @@ -159,9 +162,10 @@ private string ConvertGrouped(CsvData input, string groupByColumnName, bool conv if (groupColumnIndex >= row.Length) continue; var groupValue = row[groupColumnIndex] ?? "null"; - if (!groups.ContainsKey(groupValue)) + if (!groups.TryGetValue(groupValue, out var groupList)) { - groups[groupValue] = new List>(); + groupList = new List>(); + groups[groupValue] = groupList; } var rowDict = new Dictionary(); @@ -179,7 +183,8 @@ private string ConvertGrouped(CsvData input, string groupByColumnName, bool conv var processedValue = ProcessValue(value, convertValues, dateFormat, convertArrays, arrayDelimiter); - if (createNestedObjects && header.Contains(nestedSeparator)) + if (createNestedObjects && nestedSeparator != null && + header.Contains(nestedSeparator, StringComparison.Ordinal)) { CreateNestedObject(rowDict, header, processedValue, nestedSeparator); } @@ -189,7 +194,7 @@ private string ConvertGrouped(CsvData input, string groupByColumnName, bool conv } } - groups[groupValue].Add(rowDict); + groupList.Add(rowDict); } var jsonSettings = new JsonSerializerSettings @@ -201,7 +206,7 @@ private string ConvertGrouped(CsvData input, string groupByColumnName, bool conv return JsonConvert.SerializeObject(groups, jsonSettings); } - private object ProcessValue(string? value, bool convertValues, string? dateFormat, + private static object ProcessValue(string? value, bool convertValues, string? dateFormat, bool convertArrays, string? arrayDelimiter) { if (string.IsNullOrEmpty(value)) @@ -218,15 +223,15 @@ private object ProcessValue(string? value, bool convertValues, string? dateForma return ProcessSingleValue(value, convertValues, dateFormat); } - private object ProcessSingleValue(string value, bool convertValues, string? dateFormat) + private static object ProcessSingleValue(string value, bool convertValues, string? dateFormat) { if (!convertValues) return value; if (!string.IsNullOrEmpty(dateFormat)) { - if (DateTime.TryParseExact(value, dateFormat, CultureInfo.InvariantCulture, - DateTimeStyles.None, out var dateValue)) + if (DateTime.TryParseExact(value, dateFormat, CultureInfo.InvariantCulture, + DateTimeStyles.None, out var dateValue)) { return dateValue; } @@ -265,7 +270,8 @@ private object ProcessSingleValue(string value, bool convertValues, string? date return value; } - private void CreateNestedObject(Dictionary target, string key, object value, string separator) + private static void CreateNestedObject(Dictionary target, string key, object value, + string separator) { var parts = key.Split(separator, StringSplitOptions.RemoveEmptyEntries); var current = target; @@ -273,12 +279,13 @@ private void CreateNestedObject(Dictionary target, string key, o for (int i = 0; i < parts.Length - 1; i++) { var part = parts[i]; - if (!current.ContainsKey(part)) + if (!current.TryGetValue(part, out var existingValue)) { - current[part] = new Dictionary(); + existingValue = new Dictionary(); + current[part] = existingValue; } - if (current[part] is Dictionary dict) + if (existingValue is Dictionary dict) { current = dict; } @@ -292,4 +299,4 @@ private void CreateNestedObject(Dictionary target, string key, o current[parts[^1]] = value; } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/CsvToPdfConverter.cs b/src/FileConversionLibrary/Converters/CsvToPdfConverter.cs similarity index 92% rename from FileConversionLibrary/Converters/CsvToPdfConverter.cs rename to src/FileConversionLibrary/Converters/CsvToPdfConverter.cs index 7a5bbce..ea221cd 100644 --- a/FileConversionLibrary/Converters/CsvToPdfConverter.cs +++ b/src/FileConversionLibrary/Converters/CsvToPdfConverter.cs @@ -1,4 +1,4 @@ -using FileConversionLibrary.Interfaces; +using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; using iTextSharp.text; using iTextSharp.text.pdf; @@ -194,7 +194,9 @@ public byte[] Convert(CsvData input, object? options = null) if (includeRowNumbers) { - var rowNumberCell = new PdfPCell(new Phrase((i + 1).ToString(), cellFont)); + var rowNumberCell = + new PdfPCell(new Phrase((i + 1).ToString(System.Globalization.CultureInfo.InvariantCulture), + cellFont)); if (rowBackground != null) { rowNumberCell.BackgroundColor = rowBackground; @@ -218,7 +220,7 @@ public byte[] Convert(CsvData input, object? options = null) if (cellText.Length > 200) { - cellText = cellText.Substring(0, 197) + "..."; + cellText = string.Concat(cellText.AsSpan(0, 197), "..."); } var cell = new PdfPCell(new Phrase(cellText, cellFont)); @@ -242,13 +244,22 @@ public byte[] Convert(CsvData input, object? options = null) } document.Add(table); + + if (input.Rows.Count == 0) + { + var noDataParagraph = new Paragraph("No data available.", cellFont); + noDataParagraph.Alignment = Element.ALIGN_CENTER; + noDataParagraph.SpacingBefore = 10f; + document.Add(noDataParagraph); + } + document.Close(); return memoryStream.ToArray(); } } - private float[] CalculateColumnWidths(CsvData input, bool includeRowNumbers) + private static float[] CalculateColumnWidths(CsvData input, bool includeRowNumbers) { var columnCount = input.Headers.Length + (includeRowNumbers ? 1 : 0); var widths = new float[columnCount]; @@ -278,4 +289,4 @@ private float[] CalculateColumnWidths(CsvData input, bool includeRowNumbers) return widths; } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/CsvToWordConverter.cs b/src/FileConversionLibrary/Converters/CsvToWordConverter.cs similarity index 84% rename from FileConversionLibrary/Converters/CsvToWordConverter.cs rename to src/FileConversionLibrary/Converters/CsvToWordConverter.cs index ba5d1e1..d50f7f3 100644 --- a/FileConversionLibrary/Converters/CsvToWordConverter.cs +++ b/src/FileConversionLibrary/Converters/CsvToWordConverter.cs @@ -110,7 +110,8 @@ public byte[] Convert(CsvData input, object? options = null) includeStatistics = statsValue; } - if (optionsDict.TryGetValue("pageOrientation", out var orientation) && orientation is string orientationValue) + if (optionsDict.TryGetValue("pageOrientation", out var orientation) && + orientation is string orientationValue) { pageOrientation = orientationValue; } @@ -154,8 +155,8 @@ public byte[] Convert(CsvData input, object? options = null) } else { - AddTableContent(body, input, fontFamily, fontSize, addHeaderRow, - includeRowNumbers, alternateRowColors, addBorders, autoFitTable, + AddTableContent(body, input, fontFamily, fontSize, addHeaderRow, + includeRowNumbers, alternateRowColors, addBorders, autoFitTable, headerStyle, tableWidth, wrapText); } } @@ -176,12 +177,12 @@ public byte[] Convert(CsvData input, object? options = null) } } - private void SetupPageSettings(Body body, string orientation) + private static void SetupPageSettings(Body body, string orientation) { var sectionProperties = new SectionProperties(); var pageSize = new PageSize(); - if (orientation.ToLower() == "landscape") + if (orientation.Equals("landscape", StringComparison.OrdinalIgnoreCase)) { pageSize.Width = 15840U; pageSize.Height = 12240U; @@ -198,14 +199,14 @@ private void SetupPageSettings(Body body, string orientation) body.Append(sectionProperties); } - private void AddTitle(Body body, string title, string fontFamily, int titleFontSize) + private static void AddTitle(Body body, string title, string fontFamily, int titleFontSize) { var titleParagraph = new Paragraph(); var titleRun = new Run(); var titleText = new Text(title); var titleRunProperties = new RunProperties( - new FontSize { Val = (titleFontSize * 2).ToString() }, + new FontSize { Val = (titleFontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Bold(), new RunFonts { Ascii = fontFamily } ); @@ -223,14 +224,14 @@ private void AddTitle(Body body, string title, string fontFamily, int titleFontS body.AppendChild(titleParagraph); } - private void AddTimestamp(Body body, string fontFamily, int fontSize) + private static void AddTimestamp(Body body, string fontFamily, int fontSize) { var timestampParagraph = new Paragraph(); var timestampRun = new Run(); var timestampText = new Text($"Generated on: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); var timestampRunProperties = new RunProperties( - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Italic(), new RunFonts { Ascii = fontFamily } ); @@ -248,8 +249,8 @@ private void AddTimestamp(Body body, string fontFamily, int fontSize) body.AppendChild(timestampParagraph); } - private void AddTableContent(Body body, CsvData input, string fontFamily, int fontSize, - bool addHeaderRow, bool includeRowNumbers, bool alternateRowColors, bool addBorders, + private static void AddTableContent(Body body, CsvData input, string fontFamily, int fontSize, + bool addHeaderRow, bool includeRowNumbers, bool alternateRowColors, bool addBorders, bool autoFitTable, string headerStyle, double tableWidth, bool wrapText) { var table = new Table(); @@ -269,7 +270,11 @@ private void AddTableContent(Body body, CsvData input, string fontFamily, int fo tableProperties.AppendChild(tableBorders); } - var tableWidth100 = new TableWidth { Width = (tableWidth * 50).ToString(), Type = TableWidthUnitValues.Pct }; + var tableWidth100 = new TableWidth + { + Width = (tableWidth * 50).ToString(System.Globalization.CultureInfo.InvariantCulture), + Type = TableWidthUnitValues.Pct + }; tableProperties.AppendChild(tableWidth100); if (autoFitTable) @@ -289,7 +294,7 @@ private void AddTableContent(Body body, CsvData input, string fontFamily, int fo if (addHeaderRow) { var headerRow = new TableRow(); - + foreach (var header in headers) { var headerCell = new TableCell(); @@ -329,7 +334,8 @@ private void AddTableContent(Body body, CsvData input, string fontFamily, int fo if (includeRowNumbers) { - var numberCell = CreateDataCell((i + 1).ToString(), fontFamily, fontSize, + var numberCell = CreateDataCell((i + 1).ToString(System.Globalization.CultureInfo.InvariantCulture), + fontFamily, fontSize, alternateRowColors && i % 2 == 1, wrapText); tableRow.AppendChild(numberCell); } @@ -337,7 +343,7 @@ private void AddTableContent(Body body, CsvData input, string fontFamily, int fo for (int j = 0; j < input.Headers.Length; j++) { var cellData = j < row.Length ? (row[j] ?? string.Empty) : string.Empty; - var cell = CreateDataCell(cellData, fontFamily, fontSize, + var cell = CreateDataCell(cellData, fontFamily, fontSize, alternateRowColors && i % 2 == 1, wrapText); tableRow.AppendChild(cell); } @@ -346,16 +352,35 @@ private void AddTableContent(Body body, CsvData input, string fontFamily, int fo } body.AppendChild(table); + + if (input.Rows.Count == 0) + { + var noDataParagraph = new Paragraph(); + var noDataRun = new Run(); + var noDataRunProperties = new RunProperties( + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, + new Italic(), + new RunFonts { Ascii = fontFamily } + ); + noDataRun.AppendChild(noDataRunProperties); + noDataRun.AppendChild(new Text("No data available.")); + noDataParagraph.AppendChild(new ParagraphProperties( + new Justification { Val = JustificationValues.Center }, + new SpacingBetweenLines { Before = "120" } + )); + noDataParagraph.AppendChild(noDataRun); + body.AppendChild(noDataParagraph); + } } - private RunProperties CreateHeaderRunProperties(string fontFamily, int fontSize, string headerStyle) + private static RunProperties CreateHeaderRunProperties(string fontFamily, int fontSize, string headerStyle) { var runProperties = new RunProperties( - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new RunFonts { Ascii = fontFamily } ); - switch (headerStyle.ToLower()) + switch (headerStyle.ToLowerInvariant()) { case "strong": runProperties.AppendChild(new Bold()); @@ -365,14 +390,15 @@ private RunProperties CreateHeaderRunProperties(string fontFamily, int fontSize, break; case "heading": runProperties.AppendChild(new Bold()); - runProperties.AppendChild(new FontSize { Val = ((fontSize + 2) * 2).ToString() }); + runProperties.AppendChild(new FontSize + { Val = ((fontSize + 2) * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }); break; } return runProperties; } - private TableCell CreateDataCell(string content, string fontFamily, int fontSize, + private static TableCell CreateDataCell(string content, string fontFamily, int fontSize, bool useAlternateColor, bool wrapText) { var cell = new TableCell(); @@ -381,7 +407,7 @@ private TableCell CreateDataCell(string content, string fontFamily, int fontSize var text = new Text(content); var runProperties = new RunProperties( - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new RunFonts { Ascii = fontFamily } ); @@ -390,7 +416,7 @@ private TableCell CreateDataCell(string content, string fontFamily, int fontSize paragraph.AppendChild(run); var cellProperties = new TableCellProperties(); - + if (useAlternateColor) { var shading = new Shading @@ -414,7 +440,7 @@ private TableCell CreateDataCell(string content, string fontFamily, int fontSize return cell; } - private void AddHierarchicalContent(Body body, CsvData input, string fontFamily, + private static void AddHierarchicalContent(Body body, CsvData input, string fontFamily, int fontSize, bool includeRowNumbers) { for (int i = 0; i < input.Rows.Count; i++) @@ -426,7 +452,7 @@ private void AddHierarchicalContent(Body body, CsvData input, string fontFamily, var titleText = new Text($"Record {i + 1}"); var titleRunProperties = new RunProperties( - new FontSize { Val = ((fontSize + 2) * 2).ToString() }, + new FontSize { Val = ((fontSize + 2) * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Bold(), new RunFonts { Ascii = fontFamily } ); @@ -451,7 +477,7 @@ private void AddHierarchicalContent(Body body, CsvData input, string fontFamily, var labelText = new Text($"{input.Headers[j]}: "); var labelRunProperties = new RunProperties( - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Bold(), new RunFonts { Ascii = fontFamily } ); @@ -464,7 +490,7 @@ private void AddHierarchicalContent(Body body, CsvData input, string fontFamily, var valueText = new Text(row[j] ?? string.Empty); var valueRunProperties = new RunProperties( - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new RunFonts { Ascii = fontFamily } ); @@ -482,7 +508,7 @@ private void AddHierarchicalContent(Body body, CsvData input, string fontFamily, } } - private void AddParagraphContent(Body body, CsvData input, string fontFamily, + private static void AddParagraphContent(Body body, CsvData input, string fontFamily, int fontSize, bool addHeaderRow) { if (addHeaderRow) @@ -492,7 +518,7 @@ private void AddParagraphContent(Body body, CsvData input, string fontFamily, var headerText = new Text(string.Join(" | ", input.Headers)); var headerRunProperties = new RunProperties( - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Bold(), new RunFonts { Ascii = fontFamily } ); @@ -511,7 +537,7 @@ private void AddParagraphContent(Body body, CsvData input, string fontFamily, var rowText = new Text(string.Join(" | ", row)); var rowRunProperties = new RunProperties( - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new RunFonts { Ascii = fontFamily } ); @@ -530,7 +556,7 @@ private void AddStatistics(Body body, CsvData input, string fontFamily, int font var statsText = new Text($"\nStatistics:\nTotal rows: {input.Rows.Count}\nTotal columns: {input.Headers.Length}\nGenerated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); var statsRunProperties = new RunProperties( - new FontSize { Val = ((fontSize - 1) * 2).ToString() }, + new FontSize { Val = ((fontSize - 1) * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Italic(), new RunFonts { Ascii = fontFamily } ); @@ -548,4 +574,4 @@ private void AddStatistics(Body body, CsvData input, string fontFamily, int font body.AppendChild(statsParagraph); } */ -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/CsvToXmlConverter.cs b/src/FileConversionLibrary/Converters/CsvToXmlConverter.cs similarity index 89% rename from FileConversionLibrary/Converters/CsvToXmlConverter.cs rename to src/FileConversionLibrary/Converters/CsvToXmlConverter.cs index bb169e1..b9619b8 100644 --- a/FileConversionLibrary/Converters/CsvToXmlConverter.cs +++ b/src/FileConversionLibrary/Converters/CsvToXmlConverter.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Xml.Linq; using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; @@ -186,6 +186,7 @@ public string Convert(CsvData input, object? options = null) rootElement.Add(new XAttribute(XNamespace.Xmlns + ns.Key, ns.Value)); } } + /* if (includeTimestamp) { @@ -217,7 +218,7 @@ public string Convert(CsvData input, object? options = null) return GetXmlString(doc, preserveWhitespace); } - private void ProcessGroupedData(XElement rootElement, CsvData input, string groupByColumnName, + private static void ProcessGroupedData(XElement rootElement, CsvData input, string groupByColumnName, XmlOutputFormat outputFormat, bool useCData, string rowElementName, bool includeRowNumbers, bool validateXmlNames, XmlNamingConvention namingConvention, bool convertDataTypes) { @@ -234,12 +235,13 @@ private void ProcessGroupedData(XElement rootElement, CsvData input, string grou if (groupColumnIndex >= row.Length) continue; var groupValue = row[groupColumnIndex] ?? "null"; - if (!groups.ContainsKey(groupValue)) + if (!groups.TryGetValue(groupValue, out var groupRows)) { - groups[groupValue] = new List(); + groupRows = new List(); + groups[groupValue] = groupRows; } - groups[groupValue].Add(row); + groupRows.Add(row); } foreach (var group in groups) @@ -256,7 +258,7 @@ private void ProcessGroupedData(XElement rootElement, CsvData input, string grou } } - private void ProcessRowsInGroup(XElement groupElement, CsvData input, List rows, + private static void ProcessRowsInGroup(XElement groupElement, CsvData input, List rows, XmlOutputFormat outputFormat, bool useCData, string rowElementName, bool includeRowNumbers, bool validateXmlNames, XmlNamingConvention namingConvention, bool convertDataTypes, int skipColumnIndex) { @@ -279,7 +281,7 @@ private void ProcessRowsInGroup(XElement groupElement, CsvData input, List GetSortedRows(CsvData input, bool sortRows, string? sortByColumn, bool ascending) + private static List GetSortedRows(CsvData input, bool sortRows, string? sortByColumn, bool ascending) { if (!sortRows || string.IsNullOrEmpty(sortByColumn)) { @@ -372,7 +374,8 @@ private List GetSortedRows(CsvData input, bool sortRows, string? sortB .ToList(); } - private void ProcessRowData(XElement rowElement, string[] headers, string[] row, XmlOutputFormat outputFormat, + private static void ProcessRowData(XElement rowElement, string[] headers, string[] row, + XmlOutputFormat outputFormat, bool useCData, HashSet attributeHeaders, bool validateXmlNames, XmlNamingConvention namingConvention, bool convertDataTypes, int skipColumnIndex = -1) { @@ -438,13 +441,14 @@ private void ProcessRowData(XElement rowElement, string[] headers, string[] row, } } - private XElement CreateRowElement(string rowElementName, bool validateXmlNames, + private static XElement CreateRowElement(string rowElementName, bool validateXmlNames, XmlNamingConvention namingConvention) { return new XElement(SanitizeXmlElementName(rowElementName, validateXmlNames, namingConvention)); } - private HashSet DetermineAttributeFields(CsvData input, XmlOutputFormat format, int skipColumnIndex = -1) + private static HashSet DetermineAttributeFields(CsvData input, XmlOutputFormat format, + int skipColumnIndex = -1) { var result = new HashSet(); @@ -493,7 +497,7 @@ private HashSet DetermineAttributeFields(CsvData input, XmlOutputFormat for return result; } - private object ProcessFieldValue(string value, bool convertDataTypes) + private static object ProcessFieldValue(string value, bool convertDataTypes) { if (!convertDataTypes || string.IsNullOrEmpty(value)) return value; @@ -516,7 +520,7 @@ private object ProcessFieldValue(string value, bool convertDataTypes) return value; } - private string GetDataType(object value) + private static string GetDataType(object value) { return value switch { @@ -529,14 +533,14 @@ private string GetDataType(object value) }; } - private bool NeedsCData(string value) + private static bool NeedsCData(string value) { return value.Contains('<') || value.Contains('>') || value.Contains('&') || value.Contains('\n') || value.Contains("]]>"); } - private string SanitizeXmlElementName(string name, bool validate, XmlNamingConvention convention) + private static string SanitizeXmlElementName(string name, bool validate, XmlNamingConvention convention) { if (string.IsNullOrEmpty(name)) return "Field"; @@ -563,7 +567,7 @@ private string SanitizeXmlElementName(string name, bool validate, XmlNamingConve return sanitized.ToString(); } - private string ApplyNamingConvention(string name, XmlNamingConvention convention) + private static string ApplyNamingConvention(string name, XmlNamingConvention convention) { return convention switch { @@ -575,43 +579,43 @@ private string ApplyNamingConvention(string name, XmlNamingConvention convention }; } - private string ToCamelCase(string input) + private static string ToCamelCase(string input) { if (string.IsNullOrEmpty(input)) return input; var words = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); - var result = words[0].ToLower(); + var result = words[0].ToLowerInvariant(); for (int i = 1; i < words.Length; i++) { - result += char.ToUpper(words[i][0]) + words[i].Substring(1).ToLower(); + result += char.ToUpperInvariant(words[i][0]) + words[i].Substring(1).ToLowerInvariant(); } return result; } - private string ToPascalCase(string input) + private static string ToPascalCase(string input) { if (string.IsNullOrEmpty(input)) return input; var words = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); var result = string.Empty; foreach (var word in words) { - result += char.ToUpper(word[0]) + word.Substring(1).ToLower(); + result += char.ToUpperInvariant(word[0]) + word.Substring(1).ToLowerInvariant(); } return result; } - private string ToKebabCase(string input) + private static string ToKebabCase(string input) { - return input.ToLower().Replace(' ', '-'); + return input.ToLowerInvariant().Replace(' ', '-'); } - private string ToSnakeCase(string input) + private static string ToSnakeCase(string input) { - return input.ToLower().Replace(' ', '_'); + return input.ToLowerInvariant().Replace(' ', '_'); } - private string EscapeXmlAttributeValue(string value) + private static string EscapeXmlAttributeValue(string value) { if (string.IsNullOrEmpty(value)) return value; @@ -623,18 +627,18 @@ private string EscapeXmlAttributeValue(string value) .Replace("\"", """) .Replace("'", "'"); } - - private string GetXmlString(XDocument doc, bool preserveWhitespace) + + private static string GetXmlString(XDocument doc, bool preserveWhitespace) { var sb = new StringBuilder(); - + var version = doc.Declaration?.Version ?? "1.0"; var encoding = doc.Declaration?.Encoding ?? "UTF-8"; - sb.AppendLine($""); - + sb.Append(FormattableString.Invariant($"")).AppendLine(); + var saveOptions = preserveWhitespace ? SaveOptions.None : SaveOptions.DisableFormatting; sb.Append(doc.ToString(saveOptions)); - + return sb.ToString(); } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/CsvToYamlConverter.cs b/src/FileConversionLibrary/Converters/CsvToYamlConverter.cs similarity index 91% rename from FileConversionLibrary/Converters/CsvToYamlConverter.cs rename to src/FileConversionLibrary/Converters/CsvToYamlConverter.cs index 3b5e4aa..2b2a654 100644 --- a/FileConversionLibrary/Converters/CsvToYamlConverter.cs +++ b/src/FileConversionLibrary/Converters/CsvToYamlConverter.cs @@ -155,7 +155,7 @@ public string Convert(CsvData input, object? options = null) } var sb = new StringBuilder(); - + if (includeMetadata) { //AddMetadata(sb, input, addTimestamp, customMetadata, indentSize); @@ -188,6 +188,7 @@ public string Convert(CsvData input, object? options = null) return sb.ToString(); } + /* private void AddMetadata(StringBuilder sb, CsvData input, bool addTimestamp, Dictionary? customMetadata, int indentSize) @@ -224,7 +225,7 @@ private void AddMetadata(StringBuilder sb, CsvData input, bool addTimestamp, sb.AppendLine(); } */ - private void ConvertToArray(StringBuilder sb, CsvData input, YamlNamingConvention namingConvention, + private static void ConvertToArray(StringBuilder sb, CsvData input, YamlNamingConvention namingConvention, bool convertDataTypes, int indentSize, bool useFlowStyle, bool sortKeys, bool includeNullValues, bool compactArrays, string? dateFormat, bool escapeSpecialChars, bool preserveQuotes) { @@ -299,7 +300,7 @@ private void ConvertToArray(StringBuilder sb, CsvData input, YamlNamingConventio } } - private void ConvertToDictionary(StringBuilder sb, CsvData input, YamlNamingConvention namingConvention, + private static void ConvertToDictionary(StringBuilder sb, CsvData input, YamlNamingConvention namingConvention, bool convertDataTypes, int indentSize, bool useFlowStyle, bool sortKeys, bool includeNullValues, string? dateFormat, bool escapeSpecialChars, bool preserveQuotes) { @@ -356,7 +357,7 @@ private void ConvertToDictionary(StringBuilder sb, CsvData input, YamlNamingConv } } - private void ConvertToHierarchical(StringBuilder sb, CsvData input, YamlNamingConvention namingConvention, + private static void ConvertToHierarchical(StringBuilder sb, CsvData input, YamlNamingConvention namingConvention, bool convertDataTypes, int indentSize, bool sortKeys, bool includeNullValues, string? customRootKey, string? dateFormat, bool escapeSpecialChars, bool preserveQuotes) { @@ -415,7 +416,7 @@ private void ConvertToHierarchical(StringBuilder sb, CsvData input, YamlNamingCo } } - private void ConvertToGrouped(StringBuilder sb, CsvData input, string? groupByColumn, + private static void ConvertToGrouped(StringBuilder sb, CsvData input, string? groupByColumn, YamlNamingConvention namingConvention, bool convertDataTypes, int indentSize, bool sortKeys, bool includeNullValues, string? dateFormat, bool escapeSpecialChars, bool preserveQuotes) { @@ -438,12 +439,13 @@ private void ConvertToGrouped(StringBuilder sb, CsvData input, string? groupByCo if (groupColumnIndex >= row.Length) continue; var groupValue = row[groupColumnIndex] ?? "null"; - if (!groups.ContainsKey(groupValue)) + if (!groups.TryGetValue(groupValue, out var groupList)) { - groups[groupValue] = new List(); + groupList = new List(); + groups[groupValue] = groupList; } - groups[groupValue].Add(row); + groupList.Add(row); } sb.AppendLine("groups:"); @@ -511,7 +513,7 @@ private void ConvertToGrouped(StringBuilder sb, CsvData input, string? groupByCo } } - private HashSet DetectMultilineColumns(CsvData input) + private static HashSet DetectMultilineColumns(CsvData input) { var result = new HashSet(); @@ -530,7 +532,7 @@ private HashSet DetectMultilineColumns(CsvData input) return result; } - private string DetectColumnDataType(CsvData input, string headerName) + private static string DetectColumnDataType(CsvData input, string headerName) { var headerIndex = Array.IndexOf(input.Headers, headerName); if (headerIndex == -1) return "string"; @@ -541,7 +543,7 @@ private string DetectColumnDataType(CsvData input, string headerName) .Take(10) .ToList(); - if (!sampleValues.Any()) return "string"; + if (sampleValues.Count == 0) return "string"; if (sampleValues.All(v => int.TryParse(v, out _))) return "integer"; if (sampleValues.All(v => double.TryParse(v, out _))) return "number"; @@ -551,7 +553,8 @@ private string DetectColumnDataType(CsvData input, string headerName) return "string"; } - private object? ProcessValue(string? value, bool convertDataTypes, string? dateFormat, bool escapeSpecialChars) + private static object? ProcessValue(string? value, bool convertDataTypes, string? dateFormat, + bool escapeSpecialChars) { if (string.IsNullOrEmpty(value)) return null; @@ -593,7 +596,7 @@ private string DetectColumnDataType(CsvData input, string headerName) return escapeSpecialChars ? EscapeYamlString(value) : value; } - private YamlStyle DetermineYamlStyle(string? value, bool isMultiline) + private static YamlStyle DetermineYamlStyle(string? value, bool isMultiline) { if (string.IsNullOrEmpty(value)) return YamlStyle.Default; @@ -610,7 +613,7 @@ private YamlStyle DetermineYamlStyle(string? value, bool isMultiline) return YamlStyle.Default; } - private string FormatYamlKey(string key, YamlNamingConvention convention) + private static string FormatYamlKey(string key, YamlNamingConvention convention) { if (string.IsNullOrEmpty(key)) return "field"; @@ -620,12 +623,12 @@ private string FormatYamlKey(string key, YamlNamingConvention convention) YamlNamingConvention.CamelCase => ToCamelCase(key), YamlNamingConvention.SnakeCase => ToSnakeCase(key), YamlNamingConvention.KebabCase => ToKebabCase(key), - YamlNamingConvention.LowerCase => key.ToLower(), + YamlNamingConvention.LowerCase => key.ToLowerInvariant(), _ => key }; } - private string FormatYamlValue(object? value, bool isMultiline, string? dateFormat, + private static string FormatYamlValue(object? value, bool isMultiline, string? dateFormat, bool escapeSpecialChars, bool preserveQuotes) { if (value == null) @@ -639,7 +642,7 @@ private string FormatYamlValue(object? value, bool isMultiline, string? dateForm } if (value is bool boolValue) - return boolValue.ToString().ToLower(); + return boolValue.ToString().ToLowerInvariant(); if (value is int || value is double || value is decimal) return value.ToString() ?? "0"; @@ -658,46 +661,46 @@ private string FormatYamlValue(object? value, bool isMultiline, string? dateForm return stringValue; } - private string ToCamelCase(string input) + private static string ToCamelCase(string input) { if (string.IsNullOrEmpty(input)) return input; - var words = input.Split(new[] { ' ', '_', '-' }, StringSplitOptions.RemoveEmptyEntries); + var words = input.Split([' ', '_', '-'], StringSplitOptions.RemoveEmptyEntries); if (words.Length == 0) return input; - var result = words[0].ToLower(); + var result = words[0].ToLowerInvariant(); for (int i = 1; i < words.Length; i++) { if (words[i].Length > 0) { - result += char.ToUpper(words[i][0]) + words[i].Substring(1).ToLower(); + result += char.ToUpperInvariant(words[i][0]) + words[i].Substring(1).ToLowerInvariant(); } } return result; } - private string ToSnakeCase(string input) + private static string ToSnakeCase(string input) { - return input.ToLower().Replace(" ", "_").Replace("-", "_"); + return input.ToLowerInvariant().Replace(" ", "_").Replace("-", "_"); } - private string ToKebabCase(string input) + private static string ToKebabCase(string input) { - return input.ToLower().Replace(" ", "-").Replace("_", "-"); + return input.ToLowerInvariant().Replace(" ", "-").Replace("_", "-"); } - private bool NeedsQuoting(string value) + private static bool NeedsQuoting(string value) { if (string.IsNullOrEmpty(value)) return false; - return value.Contains(":") || - value.Contains("#") || - value.Contains("'") || - value.Contains("\"") || - value.StartsWith("-") || - value.StartsWith("[") || - value.StartsWith("{") || + return value.Contains(':') || + value.Contains('#') || + value.Contains('\'') || + value.Contains('"') || + value.StartsWith('-') || + value.StartsWith('[') || + value.StartsWith('{') || value == "null" || value == "true" || value == "false" || @@ -709,7 +712,7 @@ private bool NeedsQuoting(string value) char.IsWhiteSpace(value[^1]); } - private string EscapeYamlString(string value) + private static string EscapeYamlString(string value) { if (string.IsNullOrEmpty(value)) return value; @@ -722,8 +725,8 @@ private string EscapeYamlString(string value) .Replace("\n", "\\n"); } - private string GetIndent(int level) + private static string GetIndent(int level) { return new string(' ', level); } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/InMemoryConverter.cs b/src/FileConversionLibrary/Converters/InMemoryConverter.cs similarity index 95% rename from FileConversionLibrary/Converters/InMemoryConverter.cs rename to src/FileConversionLibrary/Converters/InMemoryConverter.cs index 7bc41e5..026e820 100644 --- a/FileConversionLibrary/Converters/InMemoryConverter.cs +++ b/src/FileConversionLibrary/Converters/InMemoryConverter.cs @@ -1,4 +1,4 @@ -using FileConversionLibrary.Interfaces; +using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; using FileConversionLibrary.Factories; using FileConversionLibrary.Enums; @@ -167,7 +167,7 @@ public string ConvertXmlToYaml(XmlData data, YamlConversionOptions? options = nu } } - private Dictionary? ConvertToOptionsDict(ConversionOptions? options) + private static Dictionary? ConvertToOptionsDict(ConversionOptions? options) { if (options == null) return null; @@ -177,7 +177,7 @@ public string ConvertXmlToYaml(XmlData data, YamlConversionOptions? options = nu foreach (var prop in properties) { if (prop.Name == nameof(ConversionOptions.CustomProperties)) continue; - + var value = prop.GetValue(options); if (value != null) { @@ -198,6 +198,6 @@ private static string ToCamelCase(string input) if (string.IsNullOrEmpty(input) || char.IsLower(input[0])) return input; - return char.ToLower(input[0]) + input.Substring(1); + return char.ToLower(input[0], System.Globalization.CultureInfo.InvariantCulture) + input.Substring(1); } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/StreamConverter.cs b/src/FileConversionLibrary/Converters/StreamConverter.cs similarity index 96% rename from FileConversionLibrary/Converters/StreamConverter.cs rename to src/FileConversionLibrary/Converters/StreamConverter.cs index 833da01..d14040b 100644 --- a/FileConversionLibrary/Converters/StreamConverter.cs +++ b/src/FileConversionLibrary/Converters/StreamConverter.cs @@ -1,4 +1,4 @@ -using FileConversionLibrary.Interfaces; +using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; using FileConversionLibrary.Readers; using System.Text; @@ -37,7 +37,7 @@ public async Task ConvertToBytesAsync(Stream input, ConversionOptions op { var data = await ReadDataFromStream(input, options.SourceFormat); - return options.TargetFormat.ToLower() switch + return options.TargetFormat.ToLowerInvariant() switch { "pdf" => ConvertToPdfBytes(data, options), "word" or "docx" => ConvertToWordBytes(data, options), @@ -57,7 +57,7 @@ public async Task ConvertToStringAsync(Stream input, ConversionOptions o { var data = await ReadDataFromStream(input, options.SourceFormat); - return options.TargetFormat.ToLower() switch + return options.TargetFormat.ToLowerInvariant() switch { "json" => ConvertToJsonString(data, options), "xml" => ConvertToXmlString(data, options), @@ -78,7 +78,7 @@ private async Task ReadDataFromStream(Stream input, string sourceFormat) { input.Position = 0; - return sourceFormat.ToLower() switch + return sourceFormat.ToLowerInvariant() switch { "csv" => await ReadCsvFromStream(input), "xml" => await ReadXmlFromStream(input), @@ -181,4 +181,4 @@ private byte[] ConvertToWordBytes(object data, ConversionOptions options) _ => throw new ArgumentException("Unsupported data type for Word conversion") }; } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/XmlToCsvConverter.cs b/src/FileConversionLibrary/Converters/XmlToCsvConverter.cs similarity index 79% rename from FileConversionLibrary/Converters/XmlToCsvConverter.cs rename to src/FileConversionLibrary/Converters/XmlToCsvConverter.cs index b438cc9..4155ded 100644 --- a/FileConversionLibrary/Converters/XmlToCsvConverter.cs +++ b/src/FileConversionLibrary/Converters/XmlToCsvConverter.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Xml.Linq; using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; @@ -31,9 +31,9 @@ private enum XmlDataFormat TabularData } - private XmlDataFormat DetermineDataFormat(XmlData input) + private static XmlDataFormat DetermineDataFormat(XmlData input) { - bool hasTabularData = input.Headers?.Length > 0 && input.Rows?.Any() == true; + bool hasTabularData = input.Headers?.Length > 0 && input.Rows?.Count > 0; if (hasTabularData) { return XmlDataFormat.TabularData; @@ -48,7 +48,7 @@ private XmlDataFormat DetermineDataFormat(XmlData input) return XmlDataFormat.None; } - private string ConvertFromDocument(XmlData input, object? options) + private static string ConvertFromDocument(XmlData input, object? options) { if (input.Document?.Root == null) { @@ -70,9 +70,10 @@ private string ConvertFromDocument(XmlData input, object? options) recordElements = root.Elements().ToList(); - if (!recordElements.Any()) + if (recordElements.Count == 0) { - throw new ArgumentException("No record elements found in XML data"); + // Return empty CSV instead of throwing exception + return string.Empty; } if (recordElements.Any(e => e.HasElements) && @@ -82,7 +83,7 @@ private string ConvertFromDocument(XmlData input, object? options) recordElements = new List { root }; } - bool isSingleRecord = recordElements.Select(e => e.Name.LocalName).Distinct().Count() == recordElements.Count(); + bool isSingleRecord = recordElements.Select(e => e.Name.LocalName).Distinct().Count() == recordElements.Count; List headers; if (isComplexStructure || isSingleRecord) @@ -94,9 +95,10 @@ private string ConvertFromDocument(XmlData input, object? options) headers = ExtractHeaders(recordElements, flattenHierarchy); } - if (!headers.Any()) + if (headers.Count == 0) { - throw new ArgumentException("No headers found in XML data"); + // Return empty CSV instead of throwing exception + return string.Empty; } var sb = new StringBuilder(); @@ -118,12 +120,12 @@ private string ConvertFromDocument(XmlData input, object? options) { string? value; - if (header.StartsWith("@")) + if (header.StartsWith('@')) { var attrName = header.Substring(1); value = record.Attribute(attrName)?.Value; } - else if (header.Contains(".")) + else if (header.Contains('.')) { value = ExtractValueFromPath(record, header); } @@ -184,7 +186,7 @@ private string ConvertFromDocument(XmlData input, object? options) return sb.ToString(); } - private List ExtractFlattenedHeaders(List elements, bool flattenHierarchy) + private static List ExtractFlattenedHeaders(List elements, bool flattenHierarchy) { var headers = new HashSet(); @@ -201,7 +203,7 @@ private List ExtractFlattenedHeaders(List elements, bool flatt return headers.OrderBy(h => h).ToList(); } - private void ExtractHeadersFromElement(XElement element, HashSet headers, string prefix, + private static void ExtractHeadersFromElement(XElement element, HashSet headers, string prefix, bool flattenHierarchy) { foreach (var child in element.Elements()) @@ -230,7 +232,7 @@ private void ExtractHeadersFromElement(XElement element, HashSet headers } } - private string? ExtractValueFromPath(XElement element, string path) + private static string? ExtractValueFromPath(XElement element, string path) { string[] parts = path.Split('.'); XElement? current = element; @@ -239,7 +241,7 @@ private void ExtractHeadersFromElement(XElement element, HashSet headers { string part = parts[i]; - if (part.StartsWith("@")) + if (part.StartsWith('@')) { return null; } @@ -254,7 +256,7 @@ private void ExtractHeadersFromElement(XElement element, HashSet headers string lastPart = parts[parts.Length - 1]; - if (lastPart.StartsWith("@")) + if (lastPart.StartsWith('@')) { string attrName = lastPart.Substring(1); return current?.Attribute(attrName)?.Value; @@ -277,7 +279,7 @@ private void ExtractHeadersFromElement(XElement element, HashSet headers } } - private string ConvertFromTabularData(XmlData input, object? options) + private static string ConvertFromTabularData(XmlData input, object? options) { if (input.Headers == null || input.Rows == null) { @@ -298,18 +300,48 @@ private string ConvertFromTabularData(XmlData input, object? options) ParseOptions(options, ref delimiter, ref quoteValues, ref includeHeaders, ref flattenHierarchy, ref customNullValue); + var rootName = input.Document?.Root?.Name.LocalName ?? string.Empty; + var rootAttrs = input.Document?.Root?.Attributes().ToList() ?? + new System.Collections.Generic.List(); + + var baseHeaders = + System.Linq.Enumerable.ToList(System.Linq.Enumerable.Select(input.Headers, NormalizeTabularHeader)); + + var finalHeaders = new System.Collections.Generic.List(); + foreach (var attr in rootAttrs) + { + if (!string.IsNullOrEmpty(rootName)) + finalHeaders.Add($"{rootName}_{attr.Name.LocalName}"); + else + finalHeaders.Add(attr.Name.LocalName); + } + + finalHeaders.AddRange(baseHeaders); + var sb = new StringBuilder(); if (includeHeaders) { var headerLine = string.Join(delimiter.ToString(), - input.Headers.Select(h => quoteValues ? QuoteValue(h, delimiter) : h)); + System.Linq.Enumerable.Select(finalHeaders, h => quoteValues ? QuoteValue(h, delimiter) : h)); sb.AppendLine(headerLine); } foreach (var row in input.Rows) { - var values = new string[input.Headers.Length]; + var values = new string[finalHeaders.Count]; + var vIdx = 0; + + foreach (var attr in rootAttrs) + { + var val = attr.Value ?? string.Empty; + if (string.IsNullOrEmpty(val) && customNullValue != null) + { + val = customNullValue; + } + + values[vIdx++] = quoteValues ? QuoteValue(val, delimiter) : val; + } for (int i = 0; i < input.Headers.Length; i++) { @@ -327,7 +359,7 @@ private string ConvertFromTabularData(XmlData input, object? options) cellValue = customNullValue ?? string.Empty; } - values[i] = quoteValues ? QuoteValue(cellValue, delimiter) : cellValue; + values[vIdx++] = quoteValues ? QuoteValue(cellValue, delimiter) : cellValue; } sb.AppendLine(string.Join(delimiter.ToString(), values)); @@ -336,7 +368,19 @@ private string ConvertFromTabularData(XmlData input, object? options) return sb.ToString(); } - private void ParseOptions(object? options, ref char delimiter, ref bool quoteValues, + private static string NormalizeTabularHeader(string header) + { + if (header.StartsWith("attr_", StringComparison.Ordinal)) + return header[5..]; + + var attrIdx = header.IndexOf("_attr_", StringComparison.Ordinal); + if (attrIdx >= 0) + return string.Concat(header.AsSpan(0, attrIdx), "_", header.AsSpan(attrIdx + 6)); + + return header; + } + + private static void ParseOptions(object? options, ref char delimiter, ref bool quoteValues, ref bool includeHeaders, ref bool flattenHierarchy, ref string? customNullValue) { if (options is char delimiterChar) @@ -372,7 +416,7 @@ private void ParseOptions(object? options, ref char delimiter, ref bool quoteVal } } - private List ExtractHeaders(List recordElements, bool flattenHierarchy) + private static List ExtractHeaders(List recordElements, bool flattenHierarchy) { var allHeaders = new HashSet(); @@ -399,7 +443,7 @@ private List ExtractHeaders(List recordElements, bool flattenH return allHeaders.OrderBy(h => h).ToList(); } - private void ExtractHeadersRecursive(XElement element, HashSet headers, string prefix) + private static void ExtractHeadersRecursive(XElement element, HashSet headers, string prefix) { foreach (var child in element.Elements()) { @@ -428,15 +472,15 @@ private void ExtractHeadersRecursive(XElement element, HashSet headers, } } - private string? ExtractValue(XElement record, string path, bool flattenHierarchy) + private static string? ExtractValue(XElement record, string path, bool flattenHierarchy) { - if (path.StartsWith("@")) + if (path.StartsWith('@')) { var attrName = path.Substring(1); return record.Attribute(attrName)?.Value; } - if (!flattenHierarchy || !path.Contains(".")) + if (!flattenHierarchy || !path.Contains('.')) { var element = record.Element(path); @@ -459,7 +503,7 @@ private void ExtractHeadersRecursive(XElement element, HashSet headers, { var part = parts[i]; - if (part.StartsWith("@")) + if (part.StartsWith('@')) { var attrName = part.Substring(1); return current?.Attribute(attrName)?.Value; @@ -483,7 +527,7 @@ private void ExtractHeadersRecursive(XElement element, HashSet headers, return current?.Value; } - private string QuoteValue(string value, char delimiter) + private static string QuoteValue(string value, char delimiter) { if (string.IsNullOrEmpty(value)) return "\"\""; @@ -496,4 +540,4 @@ private string QuoteValue(string value, char delimiter) return value; } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/XmlToJsonConverter.cs b/src/FileConversionLibrary/Converters/XmlToJsonConverter.cs similarity index 96% rename from FileConversionLibrary/Converters/XmlToJsonConverter.cs rename to src/FileConversionLibrary/Converters/XmlToJsonConverter.cs index bdce2fa..d13cd4f 100644 --- a/FileConversionLibrary/Converters/XmlToJsonConverter.cs +++ b/src/FileConversionLibrary/Converters/XmlToJsonConverter.cs @@ -80,7 +80,7 @@ public string Convert(XmlData input, object? options = null) return json; } - private void PreserveCDataSections(XElement? element) + private static void PreserveCDataSections(XElement? element) { if (element == null) return; @@ -95,7 +95,7 @@ private void PreserveCDataSections(XElement? element) } } - private void RemoveWhitespaceNodes(XElement element) + private static void RemoveWhitespaceNodes(XElement element) { var nodesToRemove = element.Nodes() .OfType() @@ -113,7 +113,7 @@ private void RemoveWhitespaceNodes(XElement element) } } - private void RemoveWhitespaceTextNodes(JToken token) + private static void RemoveWhitespaceTextNodes(JToken token) { if (token is JObject obj) { @@ -174,7 +174,7 @@ private void RemoveWhitespaceTextNodes(JToken token) } } - private void ConvertStringValues(JToken token) + private static void ConvertStringValues(JToken token) { if (token is JObject obj) { @@ -220,4 +220,4 @@ private void ConvertStringValues(JToken token) } } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/XmlToPdfConverter.cs b/src/FileConversionLibrary/Converters/XmlToPdfConverter.cs similarity index 96% rename from FileConversionLibrary/Converters/XmlToPdfConverter.cs rename to src/FileConversionLibrary/Converters/XmlToPdfConverter.cs index 55f7b30..43f6c77 100644 --- a/FileConversionLibrary/Converters/XmlToPdfConverter.cs +++ b/src/FileConversionLibrary/Converters/XmlToPdfConverter.cs @@ -62,12 +62,12 @@ public byte[] Convert(XmlData input, object? options = null) { columnWidths = widthsArray; } - + if (optionsDict.TryGetValue("includeCData", out var cdata) && cdata is bool cdataValue) { includeCData = cdataValue; } - + if (optionsDict.TryGetValue("includeComments", out var comments) && comments is bool commentsValue) { includeComments = commentsValue; @@ -101,7 +101,7 @@ public byte[] Convert(XmlData input, object? options = null) title.SpacingAfter = 20f; document.Add(title); - AddHierarchicalContent(document, input.Document.Root, font, cdataFont, commentFont, + AddHierarchicalContent(document, input.Document.Root, font, cdataFont, commentFont, includeCData, includeComments); } else if (input.Headers != null && input.Rows != null && input.Headers.Length > 0) @@ -175,10 +175,10 @@ public byte[] Convert(XmlData input, object? options = null) { document.Add(new Paragraph("XML Structure (Hierarchical View):", headerFont)); document.Add(new Paragraph(" ")); - + if (input.Document?.Root != null) { - AddHierarchicalContent(document, input.Document.Root, font, cdataFont, commentFont, + AddHierarchicalContent(document, input.Document.Root, font, cdataFont, commentFont, includeCData, includeComments); } else @@ -193,7 +193,7 @@ public byte[] Convert(XmlData input, object? options = null) } } - private void AddHierarchicalContent(Document document, XElement element, Font font, Font cdataFont, + private static void AddHierarchicalContent(Document document, XElement element, Font font, Font cdataFont, Font commentFont, bool includeCData, bool includeComments, int level = 0) { string indent = new string(' ', level * 4); @@ -230,7 +230,7 @@ private void AddHierarchicalContent(Document document, XElement element, Font fo { if (textValue.Length > 100) { - textValue = textValue.Substring(0, 97) + "..."; + textValue = string.Concat(textValue.AsSpan(0, 97), "..."); } document.Add(new Paragraph($"{indent} Value: {textValue}", font)); @@ -246,7 +246,7 @@ private void AddHierarchicalContent(Document document, XElement element, Font fo { var cdataText = $"{indent} CDATA:"; document.Add(new Paragraph(cdataText, font)); - + var cdataLines = cdataValue.Split('\n'); foreach (var line in cdataLines) { @@ -258,8 +258,8 @@ private void AddHierarchicalContent(Document document, XElement element, Font fo foreach (var child in element.Elements()) { - AddHierarchicalContent(document, child, font, cdataFont, commentFont, + AddHierarchicalContent(document, child, font, cdataFont, commentFont, includeCData, includeComments, level + 1); } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/XmlToWordConverter.cs b/src/FileConversionLibrary/Converters/XmlToWordConverter.cs similarity index 84% rename from FileConversionLibrary/Converters/XmlToWordConverter.cs rename to src/FileConversionLibrary/Converters/XmlToWordConverter.cs index fe30540..4cf0e3a 100644 --- a/FileConversionLibrary/Converters/XmlToWordConverter.cs +++ b/src/FileConversionLibrary/Converters/XmlToWordConverter.cs @@ -14,7 +14,7 @@ public byte[] Convert(XmlData input, object? options = null) { throw new ArgumentException("Invalid XML data"); } - + bool useTable = true; bool addHeaderRow = true; bool formatAsHierarchy = false; @@ -49,12 +49,12 @@ public byte[] Convert(XmlData input, object? options = null) { formatAsHierarchy = hierarchyValue; } - + if (optionsDict.TryGetValue("includeCData", out var cdata) && cdata is bool cdataValue) { includeCData = cdataValue; } - + if (optionsDict.TryGetValue("includeComments", out var comments) && comments is bool commentsValue) { includeComments = commentsValue; @@ -67,22 +67,22 @@ public byte[] Convert(XmlData input, object? options = null) { var mainPart = wordDocument.AddMainDocumentPart(); mainPart.Document = new Document(); - + var stylesPart = mainPart.AddNewPart(); GenerateStyleDefinitions(stylesPart); var body = mainPart.Document.AppendChild(new Body()); - + var titleParagraph = body.AppendChild(new Paragraph( new ParagraphProperties(new ParagraphStyleId { Val = "Title" }), new Run( new Text("XML Data Export") ) )); - + if (formatAsHierarchy && input.Document?.Root != null) { - AddHierarchicalContent(body, input.Document.Root, fontFamily, fontSize, + AddHierarchicalContent(body, input.Document.Root, fontFamily, fontSize, includeCData, includeComments); } else if (useTable && input.Headers != null && input.Rows != null && input.Headers.Length > 0) @@ -102,19 +102,19 @@ public byte[] Convert(XmlData input, object? options = null) new Text("XML Structure (Hierarchical View)") ) )); - - AddHierarchicalContent(body, input.Document.Root, fontFamily, fontSize, + + AddHierarchicalContent(body, input.Document.Root, fontFamily, fontSize, includeCData, includeComments); } - + mainPart.Document.Save(); } return ms.ToArray(); } } - - private Table CreateTable(XmlData input, string fontFamily, int fontSize, bool addHeaderRow) + + private static Table CreateTable(XmlData input, string fontFamily, int fontSize, bool addHeaderRow) { var table = new Table( new TableProperties( @@ -129,7 +129,7 @@ private Table CreateTable(XmlData input, string fontFamily, int fontSize, bool a ) ) ); - + if (addHeaderRow && input.Headers != null) { var headerRow = new TableRow(); @@ -148,7 +148,7 @@ private Table CreateTable(XmlData input, string fontFamily, int fontSize, bool a new RunFonts { Ascii = fontFamily }, new FontSize { - Val = (fontSize * 2).ToString() + Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) } ), new Text(header ?? string.Empty) @@ -160,7 +160,7 @@ private Table CreateTable(XmlData input, string fontFamily, int fontSize, bool a table.Append(headerRow); } - + if (input.Rows != null) { foreach (var rowData in input.Rows) @@ -170,7 +170,7 @@ private Table CreateTable(XmlData input, string fontFamily, int fontSize, bool a for (int i = 0; i < rowData.Length && i < (input.Headers?.Length ?? 0); i++) { var cellText = rowData[i] ?? string.Empty; - + cellText = new string(cellText.Where(c => !char.IsControl(c)).ToArray()); var cell = new TableCell( @@ -178,7 +178,10 @@ private Table CreateTable(XmlData input, string fontFamily, int fontSize, bool a new Run( new RunProperties( new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() } + new FontSize + { + Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) + } ), new Text(cellText) ) @@ -193,8 +196,8 @@ private Table CreateTable(XmlData input, string fontFamily, int fontSize, bool a return table; } - - private void AddSimpleContent(Body body, XmlData input, string fontFamily, int fontSize) + + private static void AddSimpleContent(Body body, XmlData input, string fontFamily, int fontSize) { var headers = string.Join(", ", input.Headers?.Select(h => h ?? string.Empty) ?? Array.Empty()); @@ -203,11 +206,11 @@ private void AddSimpleContent(Body body, XmlData input, string fontFamily, int f new RunProperties( new Bold(), new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() } + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) } ) )); headerRun.AppendChild(new Text("Headers: " + headers)); - + if (input.Rows != null) { foreach (var row in input.Rows) @@ -221,15 +224,17 @@ private void AddSimpleContent(Body body, XmlData input, string fontFamily, int f var run = para.AppendChild(new Run( new RunProperties( new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() } + new FontSize + { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) } ) )); run.AppendChild(new Text(rowText)); } } } - - private void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, string fontFamily, int fontSize, + + private static void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, string fontFamily, + int fontSize, bool includeCData, bool includeComments, int level = 0) { string headingStyle = level == 0 ? "Heading1" : "Heading" + Math.Min(level + 1, 9); @@ -239,29 +244,31 @@ private void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, new Run( new RunProperties( new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() } + new FontSize { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) } ), new Text(element.Name.LocalName) ) )); - + foreach (var attr in element.Attributes().Where(a => !a.IsNamespaceDeclaration)) { var attrPara = body.AppendChild(new Paragraph( new ParagraphProperties( - new Indentation { Left = ((level + 1) * 360).ToString() } + new Indentation + { Left = ((level + 1) * 360).ToString(System.Globalization.CultureInfo.InvariantCulture) } ), new Run( new RunProperties( new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize + { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Italic() ), new Text($"@{attr.Name.LocalName}: {attr.Value}") ) )); } - + if (includeComments) { var comments = element.Nodes().OfType().ToList(); @@ -269,13 +276,15 @@ private void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, { var commentPara = body.AppendChild(new Paragraph( new ParagraphProperties( - new Indentation { Left = ((level + 1) * 360).ToString() }, + new Indentation + { Left = ((level + 1) * 360).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new ParagraphStyleId { Val = "Comment" } ), new Run( new RunProperties( new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize + { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Italic(), new Color { Val = "808080" } ), @@ -284,7 +293,7 @@ private void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, )); } } - + string? textContent = element.Nodes() .OfType() .Where(t => !string.IsNullOrWhiteSpace(t.Value)) @@ -297,41 +306,45 @@ private void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, var contentPara = body.AppendChild(new Paragraph( new ParagraphProperties( - new Indentation { Left = ((level + 1) * 360).ToString() } + new Indentation + { Left = ((level + 1) * 360).ToString(System.Globalization.CultureInfo.InvariantCulture) } ), new Run( new RunProperties( new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() } + new FontSize + { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) } ), new Text($"Value: {textContent}") ) )); } - + if (includeCData) { var cdataContent = element.Nodes() .OfType() .Select(c => c.Value) .FirstOrDefault(); - + if (!string.IsNullOrEmpty(cdataContent)) { var cdataHeaderPara = body.AppendChild(new Paragraph( new ParagraphProperties( - new Indentation { Left = ((level + 1) * 360).ToString() } + new Indentation + { Left = ((level + 1) * 360).ToString(System.Globalization.CultureInfo.InvariantCulture) } ), new Run( new RunProperties( new RunFonts { Ascii = fontFamily }, - new FontSize { Val = (fontSize * 2).ToString() }, + new FontSize + { Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }, new Bold() ), new Text("CDATA:") ) )); - + var cdataLines = cdataContent.Split('\n'); foreach (var line in cdataLines) { @@ -340,13 +353,20 @@ private void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, { var cdataLinePara = body.AppendChild(new Paragraph( new ParagraphProperties( - new Indentation { Left = ((level + 1) * 480).ToString() }, + new Indentation + { + Left = ((level + 1) * 480).ToString(System.Globalization.CultureInfo + .InvariantCulture) + }, new ParagraphStyleId { Val = "Code" } ), new Run( new RunProperties( new RunFonts { Ascii = "Courier New" }, - new FontSize { Val = (fontSize * 2).ToString() } + new FontSize + { + Val = (fontSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) + } ), new Text(trimmedLine) ) @@ -355,17 +375,17 @@ private void AddHierarchicalContent(Body body, System.Xml.Linq.XElement element, } } } - + foreach (var child in element.Elements()) { AddHierarchicalContent(body, child, fontFamily, fontSize, includeCData, includeComments, level + 1); } } - private void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) + private static void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) { var styles = new Styles(); - + var normalStyle = new Style { Type = StyleValues.Paragraph, @@ -381,7 +401,7 @@ private void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) normalStyle.Append(normalRunProps); styles.Append(normalStyle); - + var titleStyle = new Style { Type = StyleValues.Paragraph, @@ -401,7 +421,7 @@ private void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) titleStyle.Append(titleRunProps); styles.Append(titleStyle); - + var codeStyle = new Style { Type = StyleValues.Paragraph, @@ -409,17 +429,17 @@ private void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) }; codeStyle.Append(new StyleName { Val = "Code" }); codeStyle.Append(new BasedOn { Val = "Normal" }); - + var codeRunProps = new StyleRunProperties(); codeRunProps.Append(new RunFonts { Ascii = "Courier New" }); codeStyle.Append(codeRunProps); - + var codeParaProps = new StyleParagraphProperties(); codeParaProps.Append(new SpacingBetweenLines { Before = "120", After = "120" }); codeStyle.Append(codeParaProps); - + styles.Append(codeStyle); - + var commentStyle = new Style { Type = StyleValues.Paragraph, @@ -427,14 +447,14 @@ private void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) }; commentStyle.Append(new StyleName { Val = "Comment" }); commentStyle.Append(new BasedOn { Val = "Normal" }); - + var commentRunProps = new StyleRunProperties(); commentRunProps.Append(new Italic()); commentRunProps.Append(new Color { Val = "808080" }); commentStyle.Append(commentRunProps); - + styles.Append(commentStyle); - + for (int i = 1; i <= 9; i++) { var headingStyle = new Style @@ -449,17 +469,18 @@ private void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) headingStyle.Append(headingParaProps); var headingRunProps = new StyleRunProperties(); - + int headingSize = 28 - (i * 2); if (headingSize < 22) headingSize = 22; - headingRunProps.Append(new FontSize { Val = (headingSize * 2).ToString() }); + headingRunProps.Append(new FontSize + { Val = (headingSize * 2).ToString(System.Globalization.CultureInfo.InvariantCulture) }); if (i <= 3) { headingRunProps.Append(new Bold()); } - + if (i == 1) headingRunProps.Append(new Color { Val = "2E74B5" }); @@ -469,4 +490,4 @@ private void GenerateStyleDefinitions(StyleDefinitionsPart stylesPart) stylesPart.Styles = styles; } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Converters/XmlToYamlConverter.cs b/src/FileConversionLibrary/Converters/XmlToYamlConverter.cs similarity index 80% rename from FileConversionLibrary/Converters/XmlToYamlConverter.cs rename to src/FileConversionLibrary/Converters/XmlToYamlConverter.cs index f0f0142..8e98cd0 100644 --- a/FileConversionLibrary/Converters/XmlToYamlConverter.cs +++ b/src/FileConversionLibrary/Converters/XmlToYamlConverter.cs @@ -1,4 +1,4 @@ -using System.Xml.Linq; +using System.Xml.Linq; using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; using YamlDotNet.Serialization; @@ -14,124 +14,129 @@ public string Convert(XmlData input, object? options = null) { throw new ArgumentException("Invalid XML data or missing XDocument"); } - + bool useCamelCase = false; bool convertValues = true; bool keepStringsForNumbers = false; bool includeRootElement = true; bool preserveCData = true; bool includeComments = false; - + if (options is Dictionary optionsDict) { if (optionsDict.TryGetValue("useCamelCase", out var camel) && camel is bool camelValue) { useCamelCase = camelValue; } - + if (optionsDict.TryGetValue("convertValues", out var convert) && convert is bool convertValue) { convertValues = convertValue; } - - if (optionsDict.TryGetValue("keepStringsForNumbers", out var keepStrings) && keepStrings is bool keepStringsValue) + + if (optionsDict.TryGetValue("keepStringsForNumbers", out var keepStrings) && + keepStrings is bool keepStringsValue) { keepStringsForNumbers = keepStringsValue; } - - if (optionsDict.TryGetValue("includeRootElement", out var includeRoot) && includeRoot is bool includeRootValue) + + if (optionsDict.TryGetValue("includeRootElement", out var includeRoot) && + includeRoot is bool includeRootValue) { includeRootElement = includeRootValue; } - - if (optionsDict.TryGetValue("preserveCData", out var preserveCdataOption) && preserveCdataOption is bool preserveCdataValue) + + if (optionsDict.TryGetValue("preserveCData", out var preserveCdataOption) && + preserveCdataOption is bool preserveCdataValue) { preserveCData = preserveCdataValue; } - - if (optionsDict.TryGetValue("includeComments", out var includeCommentsOption) && includeCommentsOption is bool includeCommentsValue) + + if (optionsDict.TryGetValue("includeComments", out var includeCommentsOption) && + includeCommentsOption is bool includeCommentsValue) { includeComments = includeCommentsValue; } } - - INamingConvention namingConvention = useCamelCase - ? CamelCaseNamingConvention.Instance + + INamingConvention namingConvention = useCamelCase + ? CamelCaseNamingConvention.Instance : NullNamingConvention.Instance; - + var serializerBuilder = new SerializerBuilder() .WithNamingConvention(namingConvention) .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) .WithMaximumRecursion(100); - + var serializer = serializerBuilder.Build(); - + object yamlObject; - + if (includeRootElement) { - yamlObject = ConvertXmlElementWithRoot(input.Document.Root, convertValues, keepStringsForNumbers, + yamlObject = ConvertXmlElementWithRoot(input.Document.Root, convertValues, keepStringsForNumbers, preserveCData, includeComments); } else { - yamlObject = ConvertXmlToObject(input.Document.Root, convertValues, keepStringsForNumbers, + yamlObject = ConvertXmlToObject(input.Document.Root, convertValues, keepStringsForNumbers, preserveCData, includeComments); } - + return serializer.Serialize(yamlObject); } - - private object ConvertXmlElementWithRoot(XElement rootElement, bool convertValues, bool keepStringsForNumbers, + + private static Dictionary ConvertXmlElementWithRoot(XElement rootElement, bool convertValues, + bool keepStringsForNumbers, bool preserveCData, bool includeComments) { var rootName = rootElement.Name.LocalName; - var rootContent = ConvertXmlToObject(rootElement, convertValues, keepStringsForNumbers, + var rootContent = ConvertXmlToObject(rootElement, convertValues, keepStringsForNumbers, preserveCData, includeComments); - + return new Dictionary { [rootName] = rootContent }; } - - private object ConvertXmlToObject(XElement element, bool convertValues, bool keepStringsForNumbers, + + private static object ConvertXmlToObject(XElement element, bool convertValues, bool keepStringsForNumbers, bool preserveCData, bool includeComments) { var result = new Dictionary(); - + foreach (var attr in element.Attributes().Where(a => !a.IsNamespaceDeclaration)) { var attrName = $"@{attr.Name.LocalName}"; result[attrName] = ConvertValue(attr.Value, convertValues, keepStringsForNumbers); } - + if (includeComments) { var comments = element.Nodes().OfType().ToList(); - if (comments.Any()) + if (comments.Count > 0) { var commentsList = comments.Select(c => c.Value.Trim()).ToList(); result["#comments"] = commentsList; } } - + var cdataNode = element.Nodes().OfType().FirstOrDefault(); if (cdataNode != null && preserveCData) { result["#cdata"] = cdataNode.Value; - - if (!element.Elements().Any() && + + if (!element.Elements().Any() && !element.Nodes().OfType().Any(t => !string.IsNullOrWhiteSpace(t.Value))) { return result; } } - + if (!element.HasElements) { string? textValue = null; - + if (cdataNode == null || !preserveCData) { textValue = element.Nodes() @@ -140,7 +145,7 @@ private object ConvertXmlToObject(XElement element, bool convertValues, bool kee .Select(t => t.Value.Trim()) .FirstOrDefault(); } - + if (!string.IsNullOrEmpty(textValue)) { if (result.Count > 0) @@ -152,75 +157,75 @@ private object ConvertXmlToObject(XElement element, bool convertValues, bool kee return ConvertValue(textValue, convertValues, keepStringsForNumbers); } } - + return result; } - + var childGroups = element.Elements() .GroupBy(e => e.Name.LocalName) .ToDictionary(g => g.Key, g => g.ToList()); - + foreach (var group in childGroups) { string name = group.Key; List elements = group.Value; - + if (elements.Count == 1) { var childElement = elements[0]; - result[name] = ConvertXmlToObject(childElement, convertValues, keepStringsForNumbers, + result[name] = ConvertXmlToObject(childElement, convertValues, keepStringsForNumbers, preserveCData, includeComments); } else { var array = new List(); - + foreach (var childElement in elements) { - array.Add(ConvertXmlToObject(childElement, convertValues, keepStringsForNumbers, + array.Add(ConvertXmlToObject(childElement, convertValues, keepStringsForNumbers, preserveCData, includeComments)); } - + result[name] = array; } } - + return result; } - - private object ConvertValue(string value, bool convertValues, bool keepStringsForNumbers) + + private static object ConvertValue(string value, bool convertValues, bool keepStringsForNumbers) { if (string.IsNullOrEmpty(value)) { return string.Empty; } - + value = value.Trim(); - + if (!convertValues || keepStringsForNumbers) { return value; } - - if (!value.StartsWith("$") && !value.StartsWith("€") && !value.StartsWith("£")) + + if (!value.StartsWith('$') && !value.StartsWith('\u20ac') && !value.StartsWith('\u00a3')) { if (int.TryParse(value, out int intValue)) { return intValue; } - - if (double.TryParse(value, System.Globalization.NumberStyles.Any, + + if (double.TryParse(value, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double doubleValue)) { return doubleValue; } } - + if (bool.TryParse(value, out bool boolValue)) { return boolValue; } - + return value; } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Enums/OutputFormat.cs b/src/FileConversionLibrary/Enums/OutputFormat.cs similarity index 98% rename from FileConversionLibrary/Enums/OutputFormat.cs rename to src/FileConversionLibrary/Enums/OutputFormat.cs index c0771eb..18777c6 100644 --- a/FileConversionLibrary/Enums/OutputFormat.cs +++ b/src/FileConversionLibrary/Enums/OutputFormat.cs @@ -8,4 +8,4 @@ public enum OutputFormat Xml, Yaml, Csv -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Exceptions/ConsoleExceptionHandler.cs b/src/FileConversionLibrary/Exceptions/ConsoleExceptionHandler.cs similarity index 99% rename from FileConversionLibrary/Exceptions/ConsoleExceptionHandler.cs rename to src/FileConversionLibrary/Exceptions/ConsoleExceptionHandler.cs index 52d9572..d961c32 100644 --- a/FileConversionLibrary/Exceptions/ConsoleExceptionHandler.cs +++ b/src/FileConversionLibrary/Exceptions/ConsoleExceptionHandler.cs @@ -12,4 +12,4 @@ public void Handle(Exception exception) Console.WriteLine($"Inner error: {exception.InnerException.Message}"); } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Exceptions/FileConversionException.cs b/src/FileConversionLibrary/Exceptions/FileConversionException.cs similarity index 75% rename from FileConversionLibrary/Exceptions/FileConversionException.cs rename to src/FileConversionLibrary/Exceptions/FileConversionException.cs index d39edf3..6d7ff7a 100644 --- a/FileConversionLibrary/Exceptions/FileConversionException.cs +++ b/src/FileConversionLibrary/Exceptions/FileConversionException.cs @@ -2,6 +2,11 @@ public class FileConversionException : Exception { - public FileConversionException(string message) : base(message) { } - public FileConversionException(string message, Exception innerException) : base(message, innerException) { } -} \ No newline at end of file + public FileConversionException(string message) : base(message) + { + } + + public FileConversionException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/FileConversionLibrary/Factories/ConverterFactory.cs b/src/FileConversionLibrary/Factories/ConverterFactory.cs similarity index 91% rename from FileConversionLibrary/Factories/ConverterFactory.cs rename to src/FileConversionLibrary/Factories/ConverterFactory.cs index d76f2a4..00ccef7 100644 --- a/FileConversionLibrary/Factories/ConverterFactory.cs +++ b/src/FileConversionLibrary/Factories/ConverterFactory.cs @@ -1,4 +1,5 @@ -using FileConversionLibrary.Converters; +using System.Diagnostics.CodeAnalysis; +using FileConversionLibrary.Converters; using FileConversionLibrary.Enums; using FileConversionLibrary.Interfaces; using FileConversionLibrary.Models; @@ -7,6 +8,8 @@ namespace FileConversionLibrary.Factories; public class ConverterFactory { + [SuppressMessage("Performance", "CA1822:MarkMembersAsStatic", + Justification = "Factory method may have instance state in future versions")] public IConverter GetConverter(OutputFormat format = OutputFormat.Json) { if (typeof(TInput) == typeof(CsvData) && typeof(TOutput) == typeof(string)) @@ -49,12 +52,12 @@ public IConverter GetConverter(OutputFormat fo { return (IConverter)(object)new XmlToPdfConverter(); } - + if (typeof(TInput) == typeof(XmlData) && typeof(TOutput) == typeof(byte[]) && format == OutputFormat.Word) { return (IConverter)(object)new XmlToWordConverter(); } - + if (typeof(TInput) == typeof(XmlData) && typeof(TOutput) == typeof(string) && format == OutputFormat.Yaml) { return (IConverter)(object)new XmlToYamlConverter(); @@ -63,4 +66,4 @@ public IConverter GetConverter(OutputFormat fo throw new NotSupportedException( $"Converter from {typeof(TInput)} to {typeof(TOutput)} with format {format} is not supported"); } -} \ No newline at end of file +} diff --git a/src/FileConversionLibrary/FileConversionLibrary.csproj b/src/FileConversionLibrary/FileConversionLibrary.csproj new file mode 100644 index 0000000..be6efef --- /dev/null +++ b/src/FileConversionLibrary/FileConversionLibrary.csproj @@ -0,0 +1,54 @@ + + + + true + FileConversionLibrary + 1.8.0 + FileConversionLibrary + Bohdan Harabadzhyu + FileConversionLibrary is a .NET library for converting files between different formats. + It supports converting CSV files to XML, PDF, Word, JSON, and YAML formats, as well as converting XML files to CSV, PDF, Word, JSON, and YAML formats. + The library provides asynchronous methods for all conversions and includes comprehensive error handling. + + https://github.com/TheMysteriousStranger90/FileConversionLibrary + git + https://github.com/TheMysteriousStranger90/FileConversionLibrary + file-conversion;csv;xml;json;pdf;word;yaml;dotnet + Version 1.8.0: Migrated to .NET 9. Restructured into src/tests/samples layout. Central Package Management. Latest code analysis and code style enforcement. Improved null safety and ConfigureAwait correctness. + true + MIT + README.md + Cover.png + FileConversionLibrary + Advanced file format conversion library for .NET + Bohdan Harabadzhyu + FileConversionLibrary + Copyright 2025 Bohdan Harabadzhyu + 1.8.0.0 + 1.8.0.0 + + + + + + $(NoWarn);NU1507;NU1701;CS0618;CA1805;CA1305 + + + + + + + + + + + + + True + \ + Cover.png + + + + + diff --git a/FileConversionLibrary/FileConverter.cs b/src/FileConversionLibrary/FileConverter.cs similarity index 84% rename from FileConversionLibrary/FileConverter.cs rename to src/FileConversionLibrary/FileConverter.cs index 62ef9a9..1b898da 100644 --- a/FileConversionLibrary/FileConverter.cs +++ b/src/FileConversionLibrary/FileConverter.cs @@ -1,4 +1,4 @@ -using FileConversionLibrary.Converters; +using FileConversionLibrary.Converters; using FileConversionLibrary.Enums; using FileConversionLibrary.Exceptions; using FileConversionLibrary.Factories; @@ -22,9 +22,9 @@ public class FileConverter private readonly IFileWriter _pdfWriter; private readonly IFileWriter _wordWriter; private readonly ConverterFactory _converterFactory; - private readonly IStreamConverter _streamConverter; - private readonly IInMemoryConverter _inMemoryConverter; - + private readonly StreamConverter _streamConverter; // CA1859: concrete type for performance + private readonly InMemoryConverter _inMemoryConverter; // CA1859: concrete type for performance + private static FileConverter? _instance; public FileConverter() @@ -39,7 +39,7 @@ public FileConverter() _xmlReader = new XmlFileReader(_exceptionHandler); _csvWriter = new CsvFileWriter(_exceptionHandler); _converterFactory = new ConverterFactory(); - + _inMemoryConverter = new InMemoryConverter(_converterFactory, _exceptionHandler); _streamConverter = new StreamConverter(_inMemoryConverter, _exceptionHandler); } @@ -66,6 +66,8 @@ public FileConverter( _csvWriter = csvWriter; _converterFactory = converterFactory; _exceptionHandler = exceptionHandler; + _inMemoryConverter = new InMemoryConverter(_converterFactory, _exceptionHandler); + _streamConverter = new StreamConverter(_inMemoryConverter, _exceptionHandler); } public static FileConverter GetInstance() @@ -73,7 +75,8 @@ public static FileConverter GetInstance() return _instance ??= new FileConverter(); } - public async Task ConvertCsvToJsonAsync(string csvFilePath, string jsonOutputPath, JsonConversionOptions? options = null) + public async Task ConvertCsvToJsonAsync(string csvFilePath, string jsonOutputPath, + JsonConversionOptions? options = null) { try { @@ -87,11 +90,10 @@ public async Task ConvertCsvToJsonAsync(string csvFilePath, string jsonOutputPat { Directory.CreateDirectory(outputDirectory); } - + var csvData = await _csvReader.ReadAsync(csvFilePath); - var converter = _converterFactory.GetConverter(OutputFormat.Json); - var json = converter.Convert(csvData, options); + var json = _inMemoryConverter.ConvertCsvToJson(csvData, options); await _jsonWriter.WriteAsync(jsonOutputPath, json, options); } @@ -102,7 +104,8 @@ public async Task ConvertCsvToJsonAsync(string csvFilePath, string jsonOutputPat } } - public async Task ConvertCsvToPdfAsync(string csvFilePath, string pdfOutputPath, PdfConversionOptions? options = null) + public async Task ConvertCsvToPdfAsync(string csvFilePath, string pdfOutputPath, + PdfConversionOptions? options = null) { try { @@ -116,11 +119,10 @@ public async Task ConvertCsvToPdfAsync(string csvFilePath, string pdfOutputPath, { Directory.CreateDirectory(outputDirectory); } - + var csvData = await _csvReader.ReadAsync(csvFilePath); - var converter = _converterFactory.GetConverter(OutputFormat.Pdf); - var pdfData = converter.Convert(csvData, options); + var pdfData = _inMemoryConverter.ConvertCsvToPdf(csvData, options); await _pdfWriter.WriteAsync(pdfOutputPath, pdfData); } @@ -131,7 +133,8 @@ public async Task ConvertCsvToPdfAsync(string csvFilePath, string pdfOutputPath, } } - public async Task ConvertCsvToWordAsync(string csvFilePath, string wordOutputPath, WordConversionOptions? options = null) + public async Task ConvertCsvToWordAsync(string csvFilePath, string wordOutputPath, + WordConversionOptions? options = null) { try { @@ -145,11 +148,10 @@ public async Task ConvertCsvToWordAsync(string csvFilePath, string wordOutputPat { Directory.CreateDirectory(outputDirectory); } - + var csvData = await _csvReader.ReadAsync(csvFilePath); - var converter = _converterFactory.GetConverter(OutputFormat.Word); - var wordData = converter.Convert(csvData, options); + var wordData = _inMemoryConverter.ConvertCsvToWord(csvData, options); await _wordWriter.WriteAsync(wordOutputPath, wordData); } @@ -160,7 +162,8 @@ public async Task ConvertCsvToWordAsync(string csvFilePath, string wordOutputPat } } - public async Task ConvertCsvToXmlAsync(string csvFilePath, string xmlOutputPath, XmlConversionOptions? options = null) + public async Task ConvertCsvToXmlAsync(string csvFilePath, string xmlOutputPath, + XmlConversionOptions? options = null) { try { @@ -174,11 +177,10 @@ public async Task ConvertCsvToXmlAsync(string csvFilePath, string xmlOutputPath, { Directory.CreateDirectory(outputDirectory); } - + var csvData = await _csvReader.ReadAsync(csvFilePath); - var converter = _converterFactory.GetConverter(OutputFormat.Xml); - var xml = converter.Convert(csvData, options); + var xml = _inMemoryConverter.ConvertCsvToXml(csvData, options); await _xmlWriter.WriteAsync(xmlOutputPath, xml, options); } @@ -189,7 +191,8 @@ public async Task ConvertCsvToXmlAsync(string csvFilePath, string xmlOutputPath, } } - public async Task ConvertCsvToYamlAsync(string csvFilePath, string yamlOutputPath, YamlConversionOptions? options = null) + public async Task ConvertCsvToYamlAsync(string csvFilePath, string yamlOutputPath, + YamlConversionOptions? options = null) { try { @@ -203,11 +206,10 @@ public async Task ConvertCsvToYamlAsync(string csvFilePath, string yamlOutputPat { Directory.CreateDirectory(outputDirectory); } - + var csvData = await _csvReader.ReadAsync(csvFilePath); - var converter = _converterFactory.GetConverter(OutputFormat.Yaml); - var yaml = converter.Convert(csvData, options); + var yaml = _inMemoryConverter.ConvertCsvToYaml(csvData, options); await _yamlWriter.WriteAsync(yamlOutputPath, yaml); } @@ -218,7 +220,8 @@ public async Task ConvertCsvToYamlAsync(string csvFilePath, string yamlOutputPat } } - public async Task ConvertXmlToCsvAsync(string xmlFilePath, string csvOutputPath, CsvConversionOptions? options = null) + public async Task ConvertXmlToCsvAsync(string xmlFilePath, string csvOutputPath, + CsvConversionOptions? options = null) { try { @@ -235,8 +238,7 @@ public async Task ConvertXmlToCsvAsync(string xmlFilePath, string csvOutputPath, var xmlData = await _xmlReader.ReadAsync(xmlFilePath, options); - var converter = _converterFactory.GetConverter(OutputFormat.Csv); - var csv = converter.Convert(xmlData, options); + var csv = _inMemoryConverter.ConvertXmlToCsv(xmlData, options); await _csvWriter.WriteAsync(csvOutputPath, csv); } @@ -247,7 +249,8 @@ public async Task ConvertXmlToCsvAsync(string xmlFilePath, string csvOutputPath, } } - public async Task ConvertXmlToJsonAsync(string xmlFilePath, string jsonOutputPath, JsonConversionOptions? options = null) + public async Task ConvertXmlToJsonAsync(string xmlFilePath, string jsonOutputPath, + JsonConversionOptions? options = null) { try { @@ -261,11 +264,10 @@ public async Task ConvertXmlToJsonAsync(string xmlFilePath, string jsonOutputPat { Directory.CreateDirectory(outputDirectory); } - + var xmlData = await _xmlReader.ReadAsync(xmlFilePath, options); - var converter = _converterFactory.GetConverter(OutputFormat.Json); - var json = converter.Convert(xmlData, options); + var json = _inMemoryConverter.ConvertXmlToJson(xmlData, options); await _jsonWriter.WriteAsync(jsonOutputPath, json, options); } @@ -276,7 +278,8 @@ public async Task ConvertXmlToJsonAsync(string xmlFilePath, string jsonOutputPat } } - public async Task ConvertXmlToPdfAsync(string xmlFilePath, string pdfOutputPath, PdfConversionOptions? options = null) + public async Task ConvertXmlToPdfAsync(string xmlFilePath, string pdfOutputPath, + PdfConversionOptions? options = null) { try { @@ -290,11 +293,10 @@ public async Task ConvertXmlToPdfAsync(string xmlFilePath, string pdfOutputPath, { Directory.CreateDirectory(outputDirectory); } - + var xmlData = await _xmlReader.ReadAsync(xmlFilePath, options); - var converter = _converterFactory.GetConverter(OutputFormat.Pdf); - var pdfData = converter.Convert(xmlData, options); + var pdfData = _inMemoryConverter.ConvertXmlToPdf(xmlData, options); await _pdfWriter.WriteAsync(pdfOutputPath, pdfData); } @@ -305,7 +307,8 @@ public async Task ConvertXmlToPdfAsync(string xmlFilePath, string pdfOutputPath, } } - public async Task ConvertXmlToWordAsync(string xmlFilePath, string wordOutputPath, WordConversionOptions? options = null) + public async Task ConvertXmlToWordAsync(string xmlFilePath, string wordOutputPath, + WordConversionOptions? options = null) { try { @@ -319,11 +322,10 @@ public async Task ConvertXmlToWordAsync(string xmlFilePath, string wordOutputPat { Directory.CreateDirectory(outputDirectory); } - + var xmlData = await _xmlReader.ReadAsync(xmlFilePath, options); - var converter = _converterFactory.GetConverter(OutputFormat.Word); - var wordData = converter.Convert(xmlData, options); + var wordData = _inMemoryConverter.ConvertXmlToWord(xmlData, options); await _wordWriter.WriteAsync(wordOutputPath, wordData); } @@ -334,7 +336,8 @@ public async Task ConvertXmlToWordAsync(string xmlFilePath, string wordOutputPat } } - public async Task ConvertXmlToYamlAsync(string xmlFilePath, string yamlOutputPath, YamlConversionOptions? options = null) + public async Task ConvertXmlToYamlAsync(string xmlFilePath, string yamlOutputPath, + YamlConversionOptions? options = null) { try { @@ -348,11 +351,10 @@ public async Task ConvertXmlToYamlAsync(string xmlFilePath, string yamlOutputPat { Directory.CreateDirectory(outputDirectory); } - + var xmlData = await _xmlReader.ReadAsync(xmlFilePath, options); - var converter = _converterFactory.GetConverter(OutputFormat.Yaml); - var yaml = converter.Convert(xmlData, options); + var yaml = _inMemoryConverter.ConvertXmlToYaml(xmlData, options); await _yamlWriter.WriteAsync(yamlOutputPath, yaml); } @@ -362,7 +364,7 @@ public async Task ConvertXmlToYamlAsync(string xmlFilePath, string yamlOutputPat throw new FileConversionException($"Failed to convert {xmlFilePath} to {yamlOutputPath}", ex); } } - + public async Task ConvertStreamAsync(Stream input, ConversionOptions options) { return await _streamConverter.ConvertAsync(input, options); @@ -402,7 +404,7 @@ public string ConvertCsvToYaml(CsvData data, YamlConversionOptions? options = nu { return _inMemoryConverter.ConvertCsvToYaml(data, options); } - + public string ConvertXmlToCsv(XmlData data, CsvConversionOptions? options = null) { return _inMemoryConverter.ConvertXmlToCsv(data, options); @@ -427,4 +429,4 @@ public string ConvertXmlToYaml(XmlData data, YamlConversionOptions? options = nu { return _inMemoryConverter.ConvertXmlToYaml(data, options); } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Interfaces/IConverter.cs b/src/FileConversionLibrary/Interfaces/IConverter.cs similarity index 98% rename from FileConversionLibrary/Interfaces/IConverter.cs rename to src/FileConversionLibrary/Interfaces/IConverter.cs index b9569dc..77692fe 100644 --- a/FileConversionLibrary/Interfaces/IConverter.cs +++ b/src/FileConversionLibrary/Interfaces/IConverter.cs @@ -3,4 +3,4 @@ public interface IConverter { TOutput Convert(TInput input, object? options = null); -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Interfaces/IExceptionHandler.cs b/src/FileConversionLibrary/Interfaces/IExceptionHandler.cs similarity index 98% rename from FileConversionLibrary/Interfaces/IExceptionHandler.cs rename to src/FileConversionLibrary/Interfaces/IExceptionHandler.cs index 1d3c8b7..bfe55f7 100644 --- a/FileConversionLibrary/Interfaces/IExceptionHandler.cs +++ b/src/FileConversionLibrary/Interfaces/IExceptionHandler.cs @@ -3,4 +3,4 @@ public interface IExceptionHandler { void Handle(Exception exception); -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Interfaces/IFileReader.cs b/src/FileConversionLibrary/Interfaces/IFileReader.cs similarity index 98% rename from FileConversionLibrary/Interfaces/IFileReader.cs rename to src/FileConversionLibrary/Interfaces/IFileReader.cs index 262bab6..5facfac 100644 --- a/FileConversionLibrary/Interfaces/IFileReader.cs +++ b/src/FileConversionLibrary/Interfaces/IFileReader.cs @@ -3,4 +3,4 @@ public interface IFileReader { Task ReadAsync(string filePath, object? options = null); -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Interfaces/IFileWriter.cs b/src/FileConversionLibrary/Interfaces/IFileWriter.cs similarity index 98% rename from FileConversionLibrary/Interfaces/IFileWriter.cs rename to src/FileConversionLibrary/Interfaces/IFileWriter.cs index c4c9a82..a45f985 100644 --- a/FileConversionLibrary/Interfaces/IFileWriter.cs +++ b/src/FileConversionLibrary/Interfaces/IFileWriter.cs @@ -3,4 +3,4 @@ public interface IFileWriter { Task WriteAsync(string filePath, T data, object? options = null); -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Interfaces/IInMemoryConverter.cs b/src/FileConversionLibrary/Interfaces/IInMemoryConverter.cs similarity index 99% rename from FileConversionLibrary/Interfaces/IInMemoryConverter.cs rename to src/FileConversionLibrary/Interfaces/IInMemoryConverter.cs index f8e1f46..0e7a1dc 100644 --- a/FileConversionLibrary/Interfaces/IInMemoryConverter.cs +++ b/src/FileConversionLibrary/Interfaces/IInMemoryConverter.cs @@ -10,10 +10,10 @@ public interface IInMemoryConverter byte[] ConvertCsvToWord(CsvData data, WordConversionOptions? options = null); string ConvertCsvToXml(CsvData data, XmlConversionOptions? options = null); string ConvertCsvToYaml(CsvData data, YamlConversionOptions? options = null); - + string ConvertXmlToCsv(XmlData data, CsvConversionOptions? options = null); string ConvertXmlToJson(XmlData data, JsonConversionOptions? options = null); byte[] ConvertXmlToPdf(XmlData data, PdfConversionOptions? options = null); byte[] ConvertXmlToWord(XmlData data, WordConversionOptions? options = null); string ConvertXmlToYaml(XmlData data, YamlConversionOptions? options = null); -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Interfaces/IStreamConverter.cs b/src/FileConversionLibrary/Interfaces/IStreamConverter.cs similarity index 99% rename from FileConversionLibrary/Interfaces/IStreamConverter.cs rename to src/FileConversionLibrary/Interfaces/IStreamConverter.cs index 5f9fb51..6b717e0 100644 --- a/FileConversionLibrary/Interfaces/IStreamConverter.cs +++ b/src/FileConversionLibrary/Interfaces/IStreamConverter.cs @@ -7,4 +7,4 @@ public interface IStreamConverter Task ConvertAsync(Stream input, ConversionOptions options); Task ConvertToBytesAsync(Stream input, ConversionOptions options); Task ConvertToStringAsync(Stream input, ConversionOptions options); -} \ No newline at end of file +} diff --git a/src/FileConversionLibrary/Models/CsvData.cs b/src/FileConversionLibrary/Models/CsvData.cs new file mode 100644 index 0000000..06686fa --- /dev/null +++ b/src/FileConversionLibrary/Models/CsvData.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace FileConversionLibrary.Models; + +public class CsvData +{ + [Required] public string[] Headers { get; set; } = Array.Empty(); + + [Required] public List Rows { get; set; } = new List(); +} diff --git a/FileConversionLibrary/Models/Options/ConversionOptions.cs b/src/FileConversionLibrary/Models/Options/ConversionOptions.cs similarity index 99% rename from FileConversionLibrary/Models/Options/ConversionOptions.cs rename to src/FileConversionLibrary/Models/Options/ConversionOptions.cs index 1156ff9..b79691e 100644 --- a/FileConversionLibrary/Models/Options/ConversionOptions.cs +++ b/src/FileConversionLibrary/Models/Options/ConversionOptions.cs @@ -5,4 +5,4 @@ public class ConversionOptions public string SourceFormat { get; set; } = string.Empty; public string TargetFormat { get; set; } = string.Empty; public Dictionary CustomProperties { get; set; } = new(); -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Models/Options/CsvConversionOptions.cs b/src/FileConversionLibrary/Models/Options/CsvConversionOptions.cs similarity index 99% rename from FileConversionLibrary/Models/Options/CsvConversionOptions.cs rename to src/FileConversionLibrary/Models/Options/CsvConversionOptions.cs index 3b00edd..44cc77c 100644 --- a/FileConversionLibrary/Models/Options/CsvConversionOptions.cs +++ b/src/FileConversionLibrary/Models/Options/CsvConversionOptions.cs @@ -4,4 +4,4 @@ public class CsvConversionOptions : ConversionOptions { public char Delimiter { get; set; } = ','; public bool QuoteValues { get; set; } = true; -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Models/Options/JsonConversionOptions.cs b/src/FileConversionLibrary/Models/Options/JsonConversionOptions.cs similarity index 99% rename from FileConversionLibrary/Models/Options/JsonConversionOptions.cs rename to src/FileConversionLibrary/Models/Options/JsonConversionOptions.cs index d375e00..2a847b9 100644 --- a/FileConversionLibrary/Models/Options/JsonConversionOptions.cs +++ b/src/FileConversionLibrary/Models/Options/JsonConversionOptions.cs @@ -13,4 +13,4 @@ public class JsonConversionOptions : ConversionOptions public string? DateFormat { get; set; } public bool ConvertArrays { get; set; } = false; public string ArrayDelimiter { get; set; } = ";"; -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Models/Options/PdfConversionOptions.cs b/src/FileConversionLibrary/Models/Options/PdfConversionOptions.cs similarity index 99% rename from FileConversionLibrary/Models/Options/PdfConversionOptions.cs rename to src/FileConversionLibrary/Models/Options/PdfConversionOptions.cs index 2db8133..cb4e68a 100644 --- a/FileConversionLibrary/Models/Options/PdfConversionOptions.cs +++ b/src/FileConversionLibrary/Models/Options/PdfConversionOptions.cs @@ -10,4 +10,4 @@ public class PdfConversionOptions : ConversionOptions public bool IncludeRowNumbers { get; set; } = false; public bool LandscapeOrientation { get; set; } = false; public string FontFamily { get; set; } = "Helvetica"; -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Models/Options/WordConversionOptions.cs b/src/FileConversionLibrary/Models/Options/WordConversionOptions.cs similarity index 56% rename from FileConversionLibrary/Models/Options/WordConversionOptions.cs rename to src/FileConversionLibrary/Models/Options/WordConversionOptions.cs index b8ac241..10580eb 100644 --- a/FileConversionLibrary/Models/Options/WordConversionOptions.cs +++ b/src/FileConversionLibrary/Models/Options/WordConversionOptions.cs @@ -9,4 +9,11 @@ public class WordConversionOptions : ConversionOptions public int FontSize { get; set; } = 11; public bool AlternateRowColors { get; set; } = false; public string PageOrientation { get; set; } = "Portrait"; -} \ No newline at end of file + public string Title { get; set; } = "CSV Data Export"; + public bool IncludeTimestamp { get; set; } = true; + public bool IncludeRowNumbers { get; set; } = false; + public bool AddBorders { get; set; } = true; + public bool AutoFitTable { get; set; } = true; + public bool WrapText { get; set; } = true; + public bool IncludeStatistics { get; set; } = true; +} diff --git a/FileConversionLibrary/Models/Options/XmlConversionOptions.cs b/src/FileConversionLibrary/Models/Options/XmlConversionOptions.cs similarity index 99% rename from FileConversionLibrary/Models/Options/XmlConversionOptions.cs rename to src/FileConversionLibrary/Models/Options/XmlConversionOptions.cs index 89cda71..a3799f9 100644 --- a/FileConversionLibrary/Models/Options/XmlConversionOptions.cs +++ b/src/FileConversionLibrary/Models/Options/XmlConversionOptions.cs @@ -9,4 +9,4 @@ public class XmlConversionOptions : ConversionOptions public string NamingConvention { get; set; } = "Original"; public bool AddComments { get; set; } = false; public bool IncludeMetadata { get; set; } = false; -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Models/Options/YamlConversionOptions.cs b/src/FileConversionLibrary/Models/Options/YamlConversionOptions.cs similarity index 99% rename from FileConversionLibrary/Models/Options/YamlConversionOptions.cs rename to src/FileConversionLibrary/Models/Options/YamlConversionOptions.cs index 7114ebd..f5739c4 100644 --- a/FileConversionLibrary/Models/Options/YamlConversionOptions.cs +++ b/src/FileConversionLibrary/Models/Options/YamlConversionOptions.cs @@ -9,4 +9,4 @@ public class YamlConversionOptions : ConversionOptions public bool IncludeMetadata { get; set; } = true; public int IndentSize { get; set; } = 2; public bool SortKeys { get; set; } = false; -} \ No newline at end of file +} diff --git a/src/FileConversionLibrary/Models/XmlData.cs b/src/FileConversionLibrary/Models/XmlData.cs new file mode 100644 index 0000000..4ec0343 --- /dev/null +++ b/src/FileConversionLibrary/Models/XmlData.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.Xml.Linq; + +namespace FileConversionLibrary.Models; + +public class XmlData +{ + public XDocument Document { get; set; } = new XDocument(); + + [Obsolete( + "Consider using Document property for better XML handling. This property is maintained for backward compatibility.")] + public string[] Headers { get; set; } = Array.Empty(); + + [Obsolete( + "Consider using Document property for better XML handling. This property is maintained for backward compatibility.")] + public List Rows { get; set; } = new List(); + + [Required] public string RootElementName { get; set; } = string.Empty; + + [Required] public string XmlVersion { get; set; } = "1.0"; + + [Required] public string Encoding { get; set; } = "UTF-8"; +} diff --git a/FileConversionLibrary/Readers/CsvFileReader.cs b/src/FileConversionLibrary/Readers/CsvFileReader.cs similarity index 92% rename from FileConversionLibrary/Readers/CsvFileReader.cs rename to src/FileConversionLibrary/Readers/CsvFileReader.cs index 49f6317..b706948 100644 --- a/FileConversionLibrary/Readers/CsvFileReader.cs +++ b/src/FileConversionLibrary/Readers/CsvFileReader.cs @@ -44,11 +44,11 @@ public async Task ReadAsync(string filePath, object? options = null) } } - private async Task AutoDetectDelimiterAsync(string filePath) + private static async Task AutoDetectDelimiterAsync(string filePath) { var firstLines = (await File.ReadAllLinesAsync(filePath)).Take(5).ToList(); if (firstLines.Count == 0) - throw new Exception("Empty CSV file"); + throw new InvalidDataException("Empty CSV file"); var possibleDelimiters = new[] { ',', ';', '\t', '|' }; @@ -98,7 +98,7 @@ private async Task ReadInternalAsync(string filePath, char delimiter) if (headers == null || headers.Length == 0) { - throw new Exception("Failed to read CSV headers."); + throw new InvalidDataException("Failed to read CSV headers."); } while (await csv.ReadAsync()) @@ -135,7 +135,7 @@ private CsvData ReadWithCustomParser(string filePath, char delimiter) var lines = contents.Split('\n'); if (lines.Length == 0) - throw new Exception("Empty CSV file"); + throw new InvalidDataException("Empty CSV file"); var headerLine = lines[0].TrimEnd('\r'); headers.AddRange(ParseCsvLine(headerLine, delimiter)); @@ -150,7 +150,7 @@ private CsvData ReadWithCustomParser(string filePath, char delimiter) rows.Add(fields.ToArray()); } - _exceptionHandler?.Handle(new Exception($"Used custom CSV parser for {filePath}")); + _exceptionHandler?.Handle(new InvalidOperationException($"Used custom CSV parser for {filePath}")); return new CsvData { Headers = headers.ToArray(), Rows = rows }; } catch (Exception ex) @@ -160,7 +160,7 @@ private CsvData ReadWithCustomParser(string filePath, char delimiter) } } - private List ParseCsvLine(string line, char delimiter) + private static List ParseCsvLine(string line, char delimiter) { var fields = new List(); var fieldBuilder = new StringBuilder(); @@ -205,4 +205,4 @@ private List ParseCsvLine(string line, char delimiter) return fields; } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Readers/XmlFileReader.cs b/src/FileConversionLibrary/Readers/XmlFileReader.cs similarity index 90% rename from FileConversionLibrary/Readers/XmlFileReader.cs rename to src/FileConversionLibrary/Readers/XmlFileReader.cs index 58a0c22..764d19c 100644 --- a/FileConversionLibrary/Readers/XmlFileReader.cs +++ b/src/FileConversionLibrary/Readers/XmlFileReader.cs @@ -58,7 +58,8 @@ public async Task ReadAsync(string filePath, object? options = null) } catch (XmlException ex) { - _exceptionHandler?.Handle(new Exception("Standard XML parser failed, trying manual parsing", ex)); + _exceptionHandler?.Handle(new InvalidOperationException("Standard XML parser failed, trying manual parsing", + ex)); return await ManualParseXmlAsync(filePath); } catch (Exception ex) @@ -68,7 +69,7 @@ public async Task ReadAsync(string filePath, object? options = null) } } - private string[] ExtractHeaders(XDocument doc) + private static string[] ExtractHeaders(XDocument doc) { if (doc.Root == null) return Array.Empty(); @@ -104,7 +105,7 @@ private string[] ExtractHeaders(XDocument doc) } } - if (!finalHeaders.Any()) + if (finalHeaders.Count == 0) { return doc.Descendants().Where(e => !e.HasElements && !string.IsNullOrWhiteSpace(e.Value)) .Select(e => e.Name.LocalName).Distinct().ToArray(); @@ -113,7 +114,7 @@ private string[] ExtractHeaders(XDocument doc) return finalHeaders.OrderBy(h => h).ToArray(); } - private List ExtractRows(XDocument doc, string[] headers) + private static List ExtractRows(XDocument doc, string[] headers) { var rows = new List(); if (doc.Root == null) return rows; @@ -147,7 +148,7 @@ private List ExtractRows(XDocument doc, string[] headers) foreach (var child in element.Elements()) { var repeatingChildren = child.Elements().ToList(); - if (repeatingChildren.Any() && repeatingChildren.All(e => e.Name == repeatingChildren.First().Name)) + if (repeatingChildren.Count > 0 && repeatingChildren.All(e => e.Name == repeatingChildren.First().Name)) { rowValues[child.Name.LocalName] = string.Join("; ", repeatingChildren.Select(c => c.Value.Trim())); } @@ -168,7 +169,7 @@ private List ExtractRows(XDocument doc, string[] headers) return rows; } - private string? FindRowElement(XDocument doc) + private static string? FindRowElement(XDocument doc) { if (doc.Root == null) return null; @@ -230,7 +231,8 @@ private async Task ManualParseXmlAsync(string filePath) catch (Exception parseEx) { _exceptionHandler?.Handle( - new Exception("XDocument.Parse failed during manual parse, falling back to regex", parseEx)); + new InvalidOperationException("XDocument.Parse failed during manual parse, falling back to regex", + parseEx)); var rows = new List>(); var allHeaders = new HashSet(); @@ -251,7 +253,8 @@ private async Task ManualParseXmlAsync(string filePath) var elementName = elementMatch.Groups[1].Value; var elementContent = elementMatch.Groups[2].Value; - if (elementContent.StartsWith("")) + if (elementContent.StartsWith("", StringComparison.Ordinal)) { elementContent = elementContent.Substring(9, elementContent.Length - 12); } @@ -292,7 +295,8 @@ private async Task ManualParseXmlAsync(string filePath) dataRows.Add(dataRow); } - _exceptionHandler?.Handle(new Exception("Successfully parsed XML using manual parser (fallback)")); + _exceptionHandler?.Handle( + new InvalidOperationException("Successfully parsed XML using manual parser (fallback)")); return new XmlData { Headers = headerArray, @@ -306,8 +310,8 @@ private async Task ManualParseXmlAsync(string filePath) } catch (Exception ex) { - _exceptionHandler?.Handle(new Exception("Manual XML parsing failed", ex)); + _exceptionHandler?.Handle(new InvalidOperationException("Manual XML parsing failed", ex)); throw; } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Writers/CsvFileWriter.cs b/src/FileConversionLibrary/Writers/CsvFileWriter.cs similarity index 98% rename from FileConversionLibrary/Writers/CsvFileWriter.cs rename to src/FileConversionLibrary/Writers/CsvFileWriter.cs index c660f21..f56c934 100644 --- a/FileConversionLibrary/Writers/CsvFileWriter.cs +++ b/src/FileConversionLibrary/Writers/CsvFileWriter.cs @@ -19,7 +19,7 @@ public async Task WriteAsync(string filePath, string data, object? options = nul { filePath = Path.ChangeExtension(filePath, ".csv"); } - + await File.WriteAllTextAsync(filePath, data); } catch (Exception ex) @@ -28,4 +28,4 @@ public async Task WriteAsync(string filePath, string data, object? options = nul throw; } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Writers/JsonFileWriter.cs b/src/FileConversionLibrary/Writers/JsonFileWriter.cs similarity index 99% rename from FileConversionLibrary/Writers/JsonFileWriter.cs rename to src/FileConversionLibrary/Writers/JsonFileWriter.cs index 60c5910..0a80956 100644 --- a/FileConversionLibrary/Writers/JsonFileWriter.cs +++ b/src/FileConversionLibrary/Writers/JsonFileWriter.cs @@ -23,4 +23,4 @@ public async Task WriteAsync(string filePath, string data, object? options = nul throw; } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Writers/PdfFileWriter.cs b/src/FileConversionLibrary/Writers/PdfFileWriter.cs similarity index 99% rename from FileConversionLibrary/Writers/PdfFileWriter.cs rename to src/FileConversionLibrary/Writers/PdfFileWriter.cs index ee51985..474ee05 100644 --- a/FileConversionLibrary/Writers/PdfFileWriter.cs +++ b/src/FileConversionLibrary/Writers/PdfFileWriter.cs @@ -23,4 +23,4 @@ public async Task WriteAsync(string filePath, byte[] data, object? options = nul throw; } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Writers/WordFileWriter.cs b/src/FileConversionLibrary/Writers/WordFileWriter.cs similarity index 99% rename from FileConversionLibrary/Writers/WordFileWriter.cs rename to src/FileConversionLibrary/Writers/WordFileWriter.cs index cecb7f2..8231517 100644 --- a/FileConversionLibrary/Writers/WordFileWriter.cs +++ b/src/FileConversionLibrary/Writers/WordFileWriter.cs @@ -29,4 +29,4 @@ public async Task WriteAsync(string filePath, byte[] data, object? options = nul throw; } } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Writers/XmlFileWriter.cs b/src/FileConversionLibrary/Writers/XmlFileWriter.cs similarity index 93% rename from FileConversionLibrary/Writers/XmlFileWriter.cs rename to src/FileConversionLibrary/Writers/XmlFileWriter.cs index 33bf239..472747d 100644 --- a/FileConversionLibrary/Writers/XmlFileWriter.cs +++ b/src/FileConversionLibrary/Writers/XmlFileWriter.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Xml; using System.Xml.Linq; using FileConversionLibrary.Interfaces; @@ -86,17 +86,17 @@ public async Task WriteAsync(string filePath, string data, object? options = nul private async Task WriteRawXmlAsync(string filePath, string xmlData, bool useIndent, string indentChars, bool expandEntities) { - if (!xmlData.TrimStart().StartsWith("\n" + xmlData; } await File.WriteAllTextAsync(filePath, xmlData, Encoding.UTF8); - _exceptionHandler?.Handle(new Exception("Used raw XML writing due to parsing issues")); + _exceptionHandler?.Handle(new InvalidOperationException("Used raw XML writing due to parsing issues")); } - private string EscapeXML(string content) + private static string EscapeXML(string content) { if (string.IsNullOrEmpty(content)) { @@ -131,4 +131,4 @@ private string EscapeXML(string content) return sb.ToString(); } -} \ No newline at end of file +} diff --git a/FileConversionLibrary/Writers/YamlFileWriter.cs b/src/FileConversionLibrary/Writers/YamlFileWriter.cs similarity index 96% rename from FileConversionLibrary/Writers/YamlFileWriter.cs rename to src/FileConversionLibrary/Writers/YamlFileWriter.cs index 82d7fd7..b0557b2 100644 --- a/FileConversionLibrary/Writers/YamlFileWriter.cs +++ b/src/FileConversionLibrary/Writers/YamlFileWriter.cs @@ -33,7 +33,7 @@ public async Task WriteAsync(string filePath, string data, object? options = nul } } - private string CleanupYamlOutput(string yamlContent) + private static string CleanupYamlOutput(string yamlContent) { if (string.IsNullOrEmpty(yamlContent)) return yamlContent; @@ -61,4 +61,4 @@ private string CleanupYamlOutput(string yamlContent) return result.ToString(); } -} \ No newline at end of file +} diff --git a/tests/FileConversionLibrary.Tests/CsvToJsonConverterTests.cs b/tests/FileConversionLibrary.Tests/CsvToJsonConverterTests.cs new file mode 100644 index 0000000..673b8cf --- /dev/null +++ b/tests/FileConversionLibrary.Tests/CsvToJsonConverterTests.cs @@ -0,0 +1,267 @@ +using System.Text.Json; +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; +using FileConversionLibrary.Models.Options; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class CsvToJsonConverterTests +{ + private CsvToJsonConverter _converter = null!; + private CsvData _simpleCsvData = null!; + private CsvData _typedCsvData = null!; + + [SetUp] + public void SetUp() + { + _converter = new CsvToJsonConverter(); + + _simpleCsvData = new CsvData + { + Headers = ["Name", "Age", "City"], + Rows = + [ + ["Alice", "28", "London"], + ["Bob", "35", "Paris"], + ["Carol", "22", "Berlin"] + ] + }; + + _typedCsvData = new CsvData + { + Headers = ["ID", "Price", "InStock", "Notes"], + Rows = + [ + ["1", "19.99", "true", "good"], + ["2", "5.50", "false", ""] + ] + }; + } + + // ---- Null / argument guard ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_EmptyHeaders_ThrowsArgumentException() + { + var data = new CsvData { Headers = [], Rows = [] }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ProducesValidJson() + { + var result = _converter.Convert(_simpleCsvData); + Assert.DoesNotThrow(() => JsonDocument.Parse(result)); + } + + [Test] + public void Convert_DefaultOptions_ReturnsArray() + { + var result = _converter.Convert(_simpleCsvData); + using var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Array)); + } + + [Test] + public void Convert_DefaultOptions_CorrectRowCount() + { + var result = _converter.Convert(_simpleCsvData); + using var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement.GetArrayLength(), Is.EqualTo(3)); + } + + [Test] + public void Convert_DefaultOptions_StringValuesPreserved() + { + var result = _converter.Convert(_simpleCsvData); + using var doc = JsonDocument.Parse(result); + var name = doc.RootElement[0].GetProperty("Name").GetString(); + Assert.That(name, Is.EqualTo("Alice")); + } + + // ---- ConvertValues ---- + + [Test] + public void Convert_ConvertValuesTrue_IntegerParsed() + { + var opts = new Dictionary { ["convertValues"] = true }; + var result = _converter.Convert(_typedCsvData, opts); + using var doc = JsonDocument.Parse(result); + var id = doc.RootElement[0].GetProperty("ID"); + Assert.That(id.ValueKind, Is.EqualTo(JsonValueKind.Number)); + Assert.That(id.GetInt32(), Is.EqualTo(1)); + } + + [Test] + public void Convert_ConvertValuesTrue_BooleanParsed() + { + var opts = new Dictionary { ["convertValues"] = true }; + var result = _converter.Convert(_typedCsvData, opts); + using var doc = JsonDocument.Parse(result); + var inStock = doc.RootElement[0].GetProperty("InStock"); + Assert.That(inStock.ValueKind, Is.EqualTo(JsonValueKind.True)); + } + + [Test] + public void Convert_ConvertValuesFalse_AllStrings() + { + var opts = new Dictionary { ["convertValues"] = false }; + var result = _converter.Convert(_typedCsvData, opts); + using var doc = JsonDocument.Parse(result); + var id = doc.RootElement[0].GetProperty("ID"); + Assert.That(id.ValueKind, Is.EqualTo(JsonValueKind.String)); + } + + // ---- UseIndentation ---- + + [Test] + public void Convert_UseIndentationFalse_ProducesMinifiedJson() + { + var opts = new Dictionary { ["useIndentation"] = false }; + var result = _converter.Convert(_simpleCsvData, opts); + Assert.That(result, Does.Not.Contain("\n")); + } + + [Test] + public void Convert_UseIndentationTrue_ContainsNewlines() + { + var opts = new Dictionary { ["useIndentation"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + Assert.That(result, Does.Contain("\n").Or.Contain(Environment.NewLine)); + } + + // ---- IncludeRowNumbers ---- + + [Test] + public void Convert_IncludeRowNumbersTrue_AddsRowNumberField() + { + var opts = new Dictionary { ["includeRowNumbers"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + using var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement[0].TryGetProperty("_rowNumber", out var rn), Is.True); + Assert.That(rn.GetInt32(), Is.EqualTo(1)); + } + + // ---- PreserveEmptyValues ---- + + [Test] + public void Convert_PreserveEmptyValuesFalse_OmitsEmptyFields() + { + var opts = new Dictionary { ["preserveEmptyValues"] = false }; + var result = _converter.Convert(_typedCsvData, opts); + using var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement[1].TryGetProperty("Notes", out _), Is.False); + } + + [Test] + public void Convert_PreserveEmptyValuesTrue_KeepsEmptyFields() + { + var opts = new Dictionary { ["preserveEmptyValues"] = true }; + var result = _converter.Convert(_typedCsvData, opts); + using var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement[1].TryGetProperty("Notes", out _), Is.True); + } + + // ---- GroupByColumn ---- + + [Test] + public void Convert_GroupByColumn_ProducesObjectWithGroups() + { + var data = new CsvData + { + Headers = ["Department", "Name", "Salary"], + Rows = + [ + ["Engineering", "Alice", "90000"], + ["Marketing", "Bob", "70000"], + ["Engineering", "Carol", "85000"] + ] + }; + var opts = new Dictionary + { + ["groupByColumn"] = true, + ["groupByColumnName"] = "Department" + }; + var result = _converter.Convert(data, opts); + using var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object)); + Assert.That(doc.RootElement.TryGetProperty("Engineering", out _), Is.True); + Assert.That(doc.RootElement.TryGetProperty("Marketing", out _), Is.True); + } + + // ---- Nested objects ---- + + [Test] + public void Convert_CreateNestedObjects_NestedPropertyCreated() + { + var data = new CsvData + { + Headers = ["Address.City", "Address.Zip", "Name"], + Rows = [["London", "EC1", "Alice"]] + }; + var opts = new Dictionary + { + ["createNestedObjects"] = true, + ["nestedSeparator"] = "." + }; + var result = _converter.Convert(data, opts); + using var doc = JsonDocument.Parse(result); + var row = doc.RootElement[0]; + Assert.That(row.TryGetProperty("Address", out var addr), Is.True); + Assert.That(addr.TryGetProperty("City", out var city), Is.True); + Assert.That(city.GetString(), Is.EqualTo("London")); + } + + // ---- ConvertArrays ---- + + [Test] + public void Convert_ConvertArraysTrue_SemicolonDelimitedBecomesArray() + { + var data = new CsvData + { + Headers = ["Name", "Tags"], + Rows = [["Alice", "red;green;blue"]] + }; + var opts = new Dictionary + { + ["convertArrays"] = true, + ["arrayDelimiter"] = ";" + }; + var result = _converter.Convert(data, opts); + using var doc = JsonDocument.Parse(result); + var tags = doc.RootElement[0].GetProperty("Tags"); + Assert.That(tags.ValueKind, Is.EqualTo(JsonValueKind.Array)); + Assert.That(tags.GetArrayLength(), Is.EqualTo(3)); + } + + // ---- Options object overload ---- + + [Test] + public void Convert_WithJsonConversionOptions_UseIndentationFalse() + { + var result = _converter.Convert(_simpleCsvData, + new Dictionary { ["useIndentation"] = false }); + Assert.That(result, Does.Not.Contain("\n")); + } + + // ---- Empty rows ---- + + [Test] + public void Convert_NoRows_ProducesEmptyJsonArray() + { + var data = new CsvData { Headers = ["A", "B"], Rows = [] }; + var result = _converter.Convert(data); + using var doc = JsonDocument.Parse(result); + Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Array)); + Assert.That(doc.RootElement.GetArrayLength(), Is.EqualTo(0)); + } +} diff --git a/tests/FileConversionLibrary.Tests/CsvToPdfConverterTests.cs b/tests/FileConversionLibrary.Tests/CsvToPdfConverterTests.cs new file mode 100644 index 0000000..5f88234 --- /dev/null +++ b/tests/FileConversionLibrary.Tests/CsvToPdfConverterTests.cs @@ -0,0 +1,193 @@ +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class CsvToPdfConverterTests +{ + private CsvToPdfConverter _converter = null!; + private CsvData _simpleCsvData = null!; + + [SetUp] + public void SetUp() + { + _converter = new CsvToPdfConverter(); + + _simpleCsvData = new CsvData + { + Headers = ["Name", "Age", "City"], + Rows = + [ + ["Alice", "28", "London"], + ["Bob", "35", "Paris"], + ["Carol", "22", "Berlin"] + ] + }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_EmptyHeaders_ThrowsArgumentException() + { + var data = new CsvData { Headers = [], Rows = [] }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ReturnsByteArray() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); + } + + [Test] + public void Convert_DefaultOptions_StartsWithPdfMagicBytes() + { + var result = _converter.Convert(_simpleCsvData); + // PDF magic bytes: %PDF (0x25 0x50 0x44 0x46) + Assert.That(result[0], Is.EqualTo(0x25)); + Assert.That(result[1], Is.EqualTo(0x50)); + Assert.That(result[2], Is.EqualTo(0x44)); + Assert.That(result[3], Is.EqualTo(0x46)); + } + + [Test] + public void Convert_DefaultOptions_ProducesValidPdf() + { + var result = _converter.Convert(_simpleCsvData); + Assert.DoesNotThrow(() => + { + var reader = new iTextSharp.text.pdf.PdfReader(result); + reader.Close(); + }); + } + + [Test] + public void Convert_DefaultOptions_HasAtLeastOnePage() + { + var result = _converter.Convert(_simpleCsvData); + var reader = new iTextSharp.text.pdf.PdfReader(result); + try + { + Assert.That(reader.NumberOfPages, Is.GreaterThanOrEqualTo(1)); + } + finally + { + reader.Close(); + } + } + + // ---- Orientation ---- + + [Test] + public void Convert_LandscapeOrientationTrue_PageIsWiderThanTall() + { + var opts = new Dictionary { ["landscapeOrientation"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + var reader = new iTextSharp.text.pdf.PdfReader(result); + try + { + var pageSize = reader.GetPageSizeWithRotation(1); + Assert.That(pageSize.Width, Is.GreaterThan(pageSize.Height)); + } + finally + { + reader.Close(); + } + } + + [Test] + public void Convert_DefaultOrientation_PageIsTallerThanWide() + { + var result = _converter.Convert(_simpleCsvData); + var reader = new iTextSharp.text.pdf.PdfReader(result); + try + { + var pageSize = reader.GetPageSizeWithRotation(1); + Assert.That(pageSize.Height, Is.GreaterThan(pageSize.Width)); + } + finally + { + reader.Close(); + } + } + + // ---- Options ---- + + [Test] + public void Convert_CustomTitle_ProducesValidPdf() + { + var opts = new Dictionary { ["title"] = "My Report" }; + var result = _converter.Convert(_simpleCsvData, opts); + var reader = new iTextSharp.text.pdf.PdfReader(result); + try + { + Assert.That(reader.NumberOfPages, Is.GreaterThanOrEqualTo(1)); + } + finally + { + reader.Close(); + } + } + + [Test] + public void Convert_AddBordersFalse_ProducesValidPdf() + { + var opts = new Dictionary { ["addBorders"] = false }; + var result = _converter.Convert(_simpleCsvData, opts); + Assert.DoesNotThrow(() => + { + var reader = new iTextSharp.text.pdf.PdfReader(result); + reader.Close(); + }); + } + + [Test] + public void Convert_AlternateRowColorsTrue_ProducesValidPdf() + { + var opts = new Dictionary { ["alternateRowColors"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + Assert.DoesNotThrow(() => + { + var reader = new iTextSharp.text.pdf.PdfReader(result); + reader.Close(); + }); + } + + [Test] + public void Convert_IncludeRowNumbersTrue_ProducesValidPdf() + { + var opts = new Dictionary { ["includeRowNumbers"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + Assert.DoesNotThrow(() => + { + var reader = new iTextSharp.text.pdf.PdfReader(result); + reader.Close(); + }); + } + + // ---- Empty rows ---- + + [Test] + public void Convert_NoRows_ProducesValidPdf() + { + var data = new CsvData { Headers = ["A", "B"], Rows = [] }; + var result = _converter.Convert(data); + Assert.DoesNotThrow(() => + { + var reader = new iTextSharp.text.pdf.PdfReader(result); + reader.Close(); + }); + } +} diff --git a/tests/FileConversionLibrary.Tests/CsvToWordConverterTests.cs b/tests/FileConversionLibrary.Tests/CsvToWordConverterTests.cs new file mode 100644 index 0000000..112eef2 --- /dev/null +++ b/tests/FileConversionLibrary.Tests/CsvToWordConverterTests.cs @@ -0,0 +1,172 @@ +using System.IO.Compression; +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class CsvToWordConverterTests +{ + private CsvToWordConverter _converter = null!; + private CsvData _simpleCsvData = null!; + + [SetUp] + public void SetUp() + { + _converter = new CsvToWordConverter(); + + _simpleCsvData = new CsvData + { + Headers = ["Name", "Age", "City"], + Rows = + [ + ["Alice", "28", "London"], + ["Bob", "35", "Paris"], + ["Carol", "22", "Berlin"] + ] + }; + } + + private static string ReadDocumentXml(byte[] docxBytes) + { + using var ms = new MemoryStream(docxBytes); + using var zip = new ZipArchive(ms, ZipArchiveMode.Read); + var entry = zip.GetEntry("word/document.xml") + ?? throw new InvalidDataException("word/document.xml not found"); + using var reader = new StreamReader(entry.Open()); + return reader.ReadToEnd(); + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_EmptyHeaders_ThrowsArgumentException() + { + var data = new CsvData { Headers = [], Rows = [] }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ReturnsByteArray() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); + } + + [Test] + public void Convert_DefaultOptions_StartsWithZipMagicBytes() + { + var result = _converter.Convert(_simpleCsvData); + // ZIP/OOXML magic bytes: PK (0x50 0x4B) + Assert.That(result[0], Is.EqualTo(0x50)); + Assert.That(result[1], Is.EqualTo(0x4B)); + } + + [Test] + public void Convert_DefaultOptions_ContainsWordDocumentXml() + { + var result = _converter.Convert(_simpleCsvData); + Assert.DoesNotThrow(() => ReadDocumentXml(result)); + } + + [Test] + public void Convert_DefaultOptions_ContainsTable() + { + var result = _converter.Convert(_simpleCsvData); + var xml = ReadDocumentXml(result); + Assert.That(xml, Does.Contain(" { ["useTable"] = false }; + var result = _converter.Convert(_simpleCsvData, opts); + var xml = ReadDocumentXml(result); + Assert.That(xml, Does.Not.Contain(" { ["pageOrientation"] = "landscape" }; + var result = _converter.Convert(_simpleCsvData, opts); + var xml = ReadDocumentXml(result); + Assert.That(xml, Does.Contain("landscape")); + } + + // ---- IncludeRowNumbers ---- + + [Test] + public void Convert_IncludeRowNumbersTrue_DocumentContainsRowNumber() + { + var opts = new Dictionary { ["includeRowNumbers"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + var xml = ReadDocumentXml(result); + Assert.That(xml, Does.Contain("1")); + } + + // ---- FormatAsHierarchy ---- + + [Test] + public void Convert_FormatAsHierarchyTrue_DocumentContainsRecordText() + { + var opts = new Dictionary { ["formatAsHierarchy"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + var xml = ReadDocumentXml(result); + Assert.That(xml, Does.Contain("Record")); + } + + // ---- Title ---- + + [Test] + public void Convert_CustomTitle_DocumentContainsTitle() + { + var opts = new Dictionary { ["title"] = "My Custom Report" }; + var result = _converter.Convert(_simpleCsvData, opts); + var xml = ReadDocumentXml(result); + Assert.That(xml, Does.Contain("My Custom Report")); + } + + // ---- Empty rows ---- + + [Test] + public void Convert_NoRows_ProducesValidDocx() + { + var data = new CsvData { Headers = ["A", "B"], Rows = [] }; + var result = _converter.Convert(data); + Assert.DoesNotThrow(() => ReadDocumentXml(result)); + } + + [Test] + public void Convert_NoRows_ContainsNoDataMessage() + { + var data = new CsvData { Headers = ["A", "B"], Rows = [] }; + var result = _converter.Convert(data); + var xml = ReadDocumentXml(result); + Assert.That(xml, Does.Contain("No data available")); + } +} diff --git a/tests/FileConversionLibrary.Tests/CsvToXmlConverterTests.cs b/tests/FileConversionLibrary.Tests/CsvToXmlConverterTests.cs new file mode 100644 index 0000000..be91a66 --- /dev/null +++ b/tests/FileConversionLibrary.Tests/CsvToXmlConverterTests.cs @@ -0,0 +1,162 @@ +using System.Xml.Linq; +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class CsvToXmlConverterTests +{ + private CsvToXmlConverter _converter = null!; + private CsvData _simpleCsvData = null!; + + [SetUp] + public void SetUp() + { + _converter = new CsvToXmlConverter(); + + _simpleCsvData = new CsvData + { + Headers = ["Name", "Age", "City"], + Rows = + [ + ["Alice", "28", "London"], + ["Bob", "35", "Paris"], + ["Carol", "22", "Berlin"] + ] + }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_EmptyHeaders_ThrowsArgumentException() + { + var data = new CsvData { Headers = [], Rows = [] }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ReturnsNonEmptyString() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result, Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Convert_DefaultOptions_ProducesValidXml() + { + var result = _converter.Convert(_simpleCsvData); + Assert.DoesNotThrow(() => XDocument.Parse(result)); + } + + [Test] + public void Convert_DefaultOptions_HasRootElement() + { + var result = _converter.Convert(_simpleCsvData); + var doc = XDocument.Parse(result); + Assert.That(doc.Root, Is.Not.Null); + Assert.That(doc.Root!.Name.LocalName, Is.EqualTo("root")); + } + + [Test] + public void Convert_DefaultOptions_CorrectRowCount() + { + var result = _converter.Convert(_simpleCsvData); + var doc = XDocument.Parse(result); + var rows = doc.Root!.Descendants("row").ToList(); + Assert.That(rows.Count, Is.EqualTo(3)); + } + + [Test] + public void Convert_DefaultOptions_DataValuesPresent() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result, Does.Contain("Alice")); + Assert.That(result, Does.Contain("London")); + } + + // ---- CData ---- + + [Test] + public void Convert_UseCDataTrue_OutputContainsCData() + { + // CData is only added when value contains special XML chars (<, >, &, \n) + var data = new CsvData + { + Headers = ["Name", "Description"], + Rows = [["Alice", "A & B < C > D"]] + }; + var opts = new Dictionary { ["useCData"] = true }; + var result = _converter.Convert(data, opts); + Assert.That(result, Does.Contain(" D"]] + }; + var opts = new Dictionary { ["useCData"] = false }; + var result = _converter.Convert(data, opts); + Assert.That(result, Does.Not.Contain(" { ["includeRowNumbers"] = true }; + var result = _converter.Convert(_simpleCsvData, opts); + var doc = XDocument.Parse(result); + var firstRow = doc.Root!.Descendants("row").First(); + var numberAttr = firstRow.Attribute("number"); + Assert.That(numberAttr, Is.Not.Null); + Assert.That(numberAttr!.Value, Is.EqualTo("1")); + } + + // ---- Attributes output format ---- + // The option key is "format" (not "outputFormat") + + [Test] + public void Convert_AttributesFormat_ValuesAsAttributes() + { + var opts = new Dictionary { ["format"] = CsvToXmlConverter.XmlOutputFormat.Attributes }; + var result = _converter.Convert(_simpleCsvData, opts); + var doc = XDocument.Parse(result); + var firstRow = doc.Root!.Descendants("row").First(); + Assert.That(firstRow.Attribute("Name")?.Value, Is.EqualTo("Alice")); + } + + // ---- Empty rows ---- + + [Test] + public void Convert_NoRows_ProducesValidXml() + { + var data = new CsvData { Headers = ["A", "B"], Rows = [] }; + var result = _converter.Convert(data); + Assert.DoesNotThrow(() => XDocument.Parse(result)); + } + + [Test] + public void Convert_NoRows_RootHasNoRowChildren() + { + var data = new CsvData { Headers = ["A", "B"], Rows = [] }; + var result = _converter.Convert(data); + var doc = XDocument.Parse(result); + var rows = doc.Root!.Descendants("row").ToList(); + Assert.That(rows.Count, Is.EqualTo(0)); + } +} diff --git a/tests/FileConversionLibrary.Tests/CsvToYamlConverterTests.cs b/tests/FileConversionLibrary.Tests/CsvToYamlConverterTests.cs new file mode 100644 index 0000000..b1ac8e1 --- /dev/null +++ b/tests/FileConversionLibrary.Tests/CsvToYamlConverterTests.cs @@ -0,0 +1,151 @@ +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class CsvToYamlConverterTests +{ + private CsvToYamlConverter _converter = null!; + private CsvData _simpleCsvData = null!; + + [SetUp] + public void SetUp() + { + _converter = new CsvToYamlConverter(); + + _simpleCsvData = new CsvData + { + Headers = ["Name", "Age", "City"], + Rows = + [ + ["Alice", "28", "London"], + ["Bob", "35", "Paris"], + ["Carol", "22", "Berlin"] + ] + }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_EmptyHeaders_ThrowsArgumentException() + { + var data = new CsvData { Headers = [], Rows = [] }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ReturnsNonEmptyString() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result, Is.Not.Null.And.Not.Empty); + } + + [Test] + public void Convert_DefaultOptions_StartsWithYamlDocumentMarker() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result.TrimStart(), Does.StartWith("---")); + } + + [Test] + public void Convert_DefaultOptions_DataValuesPresent() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result, Does.Contain("Alice")); + Assert.That(result, Does.Contain("London")); + } + + [Test] + public void Convert_DefaultOptions_HeaderKeysPresent() + { + var result = _converter.Convert(_simpleCsvData); + Assert.That(result, Does.Contain("Name")); + Assert.That(result, Does.Contain("Age")); + Assert.That(result, Does.Contain("City")); + } + + // ---- SortKeys ---- + + [Test] + public void Convert_SortKeysTrue_KeysInAlphaOrder() + { + var data = new CsvData + { + Headers = ["Zebra", "Apple", "Mango"], + Rows = [["z", "a", "m"]] + }; + var opts = new Dictionary { ["sortKeys"] = true }; + var result = _converter.Convert(data, opts); + var appleIdx = result.IndexOf("Apple", StringComparison.Ordinal); + var mangoIdx = result.IndexOf("Mango", StringComparison.Ordinal); + var zebraIdx = result.IndexOf("Zebra", StringComparison.Ordinal); + Assert.That(appleIdx, Is.LessThan(mangoIdx)); + Assert.That(mangoIdx, Is.LessThan(zebraIdx)); + } + + // ---- ConvertDataTypes ---- + + [Test] + public void Convert_ConvertDataTypesTrue_IntegerNotQuoted() + { + var result = _converter.Convert(_simpleCsvData); + // Default is convertDataTypes=true; "28" should appear as plain number + Assert.That(result, Does.Contain("28")); + Assert.That(result, Does.Not.Contain("\"28\"")); + } + + [Test] + public void Convert_ConvertDataTypesFalse_ValuesAreStrings() + { + var opts = new Dictionary { ["convertDataTypes"] = false }; + var result = _converter.Convert(_simpleCsvData, opts); + // When not converting, numeric values should be quoted + Assert.That(result, Does.Contain("\"28\"").Or.Contain("'28'").Or.Contain("28")); + // At minimum, the value should be present + Assert.That(result, Does.Contain("28")); + } + + // ---- IncludeComments ---- + + [Test] + public void Convert_IncludeCommentsFalse_NoHashLines() + { + var opts = new Dictionary { ["includeComments"] = false }; + var result = _converter.Convert(_simpleCsvData, opts); + var lines = result.Split('\n'); + var commentLines = lines.Where(l => l.TrimStart().StartsWith('#')).ToList(); + Assert.That(commentLines, Is.Empty); + } + + // ---- Empty rows ---- + + [Test] + public void Convert_NoRows_ReturnsYamlDocumentMarker() + { + var data = new CsvData { Headers = ["A", "B"], Rows = [] }; + var result = _converter.Convert(data); + Assert.That(result.TrimStart(), Does.StartWith("---")); + } + + [Test] + public void Convert_NoRows_NoDataEntries() + { + var data = new CsvData { Headers = ["Name", "Age"], Rows = [] }; + var result = _converter.Convert(data); + // Should not contain any actual row data values (just the marker line) + var lines = result.Split('\n') + .Where(l => !string.IsNullOrWhiteSpace(l) && l.Trim() != "---") + .ToList(); + Assert.That(lines, Is.Empty); + } +} diff --git a/tests/FileConversionLibrary.Tests/FileConversionLibrary.Tests.csproj b/tests/FileConversionLibrary.Tests/FileConversionLibrary.Tests.csproj new file mode 100644 index 0000000..a36974f --- /dev/null +++ b/tests/FileConversionLibrary.Tests/FileConversionLibrary.Tests.csproj @@ -0,0 +1,25 @@ + + + + false + true + $(NoWarn);CA1707;CA1062;CA2007;CA1310;CA1847;CA1861 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/tests/FileConversionLibrary.Tests/GlobalUsings.cs b/tests/FileConversionLibrary.Tests/GlobalUsings.cs new file mode 100644 index 0000000..3244567 --- /dev/null +++ b/tests/FileConversionLibrary.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; diff --git a/tests/FileConversionLibrary.Tests/XmlToCsvConverterTests.cs b/tests/FileConversionLibrary.Tests/XmlToCsvConverterTests.cs new file mode 100644 index 0000000..19c09ff --- /dev/null +++ b/tests/FileConversionLibrary.Tests/XmlToCsvConverterTests.cs @@ -0,0 +1,171 @@ +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; +using System.Xml.Linq; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class XmlToCsvConverterTests +{ + private XmlToCsvConverter _converter = null!; + private XmlData _simpleXmlData = null!; + private XmlData _nestedXmlData = null!; + private XmlData _emptyXmlData = null!; + + [SetUp] + public void SetUp() + { + _converter = new XmlToCsvConverter(); + + // Simple tabular XML + var simpleXml = @" + + + Widget + 19.99 + true + + + Gadget + 29.99 + false + +"; + _simpleXmlData = new XmlData { Document = XDocument.Parse(simpleXml) }; + + // Nested XML + var nestedXml = @" + + + + John + Doe + + 30 + +"; + _nestedXmlData = new XmlData { Document = XDocument.Parse(nestedXml) }; + + // Empty XML + var emptyXml = @""; + _emptyXmlData = new XmlData { Document = XDocument.Parse(emptyXml) }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_NullDocument_ThrowsArgumentException() + { + var data = new XmlData { Document = null! }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ProducesValidCsv() + { + var result = _converter.Convert(_simpleXmlData); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); + Assert.That(result, Does.Contain(",")); + } + + [Test] + public void Convert_DefaultOptions_IncludesHeaders() + { + var result = _converter.Convert(_simpleXmlData); + var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.That(lines.Length, Is.GreaterThanOrEqualTo(2)); // At least header + 1 row + Assert.That(lines[0], Does.Contain("id")); // Attribute with @ prefix + Assert.That(lines[0], Does.Contain("name")); + Assert.That(lines[0], Does.Contain("price")); + } + + [Test] + public void Convert_DefaultOptions_CorrectRowCount() + { + var result = _converter.Convert(_simpleXmlData); + var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.That(lines.Length, Is.EqualTo(3)); // Header + 2 data rows + } + + [Test] + public void Convert_NestedXml_FlattensStructure() + { + var result = _converter.Convert(_nestedXmlData); + Assert.That(result, Does.Contain("first")); // Dot notation for nested + } + + // ---- Options testing ---- + + [Test] + public void Convert_CustomDelimiter_UsesSemicolon() + { + var options = new Dictionary + { + ["delimiter"] = ';' + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result, Does.Contain(";")); + } + + [Test] + public void Convert_NoHeaders_OmitsHeaderRow() + { + var options = new Dictionary + { + ["includeHeaders"] = false + }; + var result = _converter.Convert(_simpleXmlData, options); + var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.That(lines.Length, Is.EqualTo(2)); // Only data rows + } + + [Test] + public void Convert_QuoteValues_AddsQuotes() + { + var options = new Dictionary + { + ["quoteValues"] = true + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); // Should have quotes + } + + // ---- Edge cases ---- + + [Test] + public void Convert_EmptyXml_ReturnsEmptyString() + { + var result = _converter.Convert(_emptyXmlData); + Assert.That(result, Is.Empty); + } + + [Test] + public void Convert_SingleElement_ProducesValidCsv() + { + var singleXml = @"123"; + var data = new XmlData { Document = XDocument.Parse(singleXml) }; + var result = _converter.Convert(data); + var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.That(lines.Length, Is.EqualTo(2)); // Header + 1 row + } + + [Test] + public void Convert_AttributesOnly_IncludesAttributes() + { + var attrXml = @""; + var data = new XmlData { Document = XDocument.Parse(attrXml) }; + var result = _converter.Convert(data); + Assert.That(result, Does.Contain("id")); + Assert.That(result, Does.Contain("code")); + } +} diff --git a/tests/FileConversionLibrary.Tests/XmlToJsonConverterTests.cs b/tests/FileConversionLibrary.Tests/XmlToJsonConverterTests.cs new file mode 100644 index 0000000..af7293d --- /dev/null +++ b/tests/FileConversionLibrary.Tests/XmlToJsonConverterTests.cs @@ -0,0 +1,186 @@ +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; +using System.Text.Json; +using System.Xml.Linq; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class XmlToJsonConverterTests +{ + private XmlToJsonConverter _converter = null!; + private XmlData _simpleXmlData = null!; + private XmlData _nestedXmlData = null!; + private XmlData _cdataXmlData = null!; + + [SetUp] + public void SetUp() + { + _converter = new XmlToJsonConverter(); + + // Simple XML + var simpleXml = @" + + + Widget + 19.99 + +"; + _simpleXmlData = new XmlData { Document = XDocument.Parse(simpleXml) }; + + // Nested XML + var nestedXml = @" + + + + John Doe + Developer + + +"; + _nestedXmlData = new XmlData { Document = XDocument.Parse(nestedXml) }; + + // CData XML + var cdataXml = @" + + + HTML content]]> + +"; + _cdataXmlData = new XmlData { Document = XDocument.Parse(cdataXml) }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_NullDocument_ThrowsArgumentException() + { + var data = new XmlData { Document = null! }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ProducesValidJson() + { + var result = _converter.Convert(_simpleXmlData); + Assert.DoesNotThrow(() => JsonDocument.Parse(result)); + } + + [Test] + public void Convert_DefaultOptions_PreservesStructure() + { + var result = _converter.Convert(_simpleXmlData); + using var doc = JsonDocument.Parse(result); + var root = doc.RootElement; + Assert.That(root.ValueKind, Is.EqualTo(JsonValueKind.Object)); + Assert.That(root.TryGetProperty("products", out _), Is.True); + } + + [Test] + public void Convert_NestedXml_PreservesHierarchy() + { + var result = _converter.Convert(_nestedXmlData); + using var doc = JsonDocument.Parse(result); + Assert.That(result, Does.Contain("company")); + Assert.That(result, Does.Contain("department")); + Assert.That(result, Does.Contain("employee")); + } + + [Test] + public void Convert_AttributesIncluded_PrefixedWithAt() + { + var result = _converter.Convert(_simpleXmlData); + Assert.That(result, Does.Contain("@id")); // Attribute with @ prefix + } + + // ---- Options testing ---- + + [Test] + public void Convert_NoIndentation_MinifiedJson() + { + var options = new Dictionary + { + ["useIndentation"] = false + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result, Is.Not.Null); + // Minified JSON should not have newlines (or very few) + var lineCount = result.Split('\n').Length; + Assert.That(lineCount, Is.LessThanOrEqualTo(3)); // Allow for minimal breaks + } + + [Test] + public void Convert_ConvertValuesFalse_KeepsStrings() + { + var options = new Dictionary + { + ["convertValues"] = false + }; + var result = _converter.Convert(_simpleXmlData, options); + using var doc = JsonDocument.Parse(result); + // Values should be strings, not numbers + Assert.That(result, Does.Contain("\"19.99\"")); // Quoted price + } + + [Test] + public void Convert_PreserveCDataTrue_KeepsCDataContent() + { + var options = new Dictionary + { + ["preserveCData"] = true + }; + var result = _converter.Convert(_cdataXmlData, options); + Assert.That(result, Does.Contain("HTML")); // CData content preserved + } + + // ---- Edge cases ---- + + [Test] + public void Convert_EmptyXml_ProducesValidJson() + { + var emptyXml = @""; + var data = new XmlData { Document = XDocument.Parse(emptyXml) }; + var result = _converter.Convert(data); + Assert.DoesNotThrow(() => JsonDocument.Parse(result)); + Assert.That(result, Does.Contain("root")); + } + + [Test] + public void Convert_SingleElement_ProducesValidJson() + { + var singleXml = @"123"; + var data = new XmlData { Document = XDocument.Parse(singleXml) }; + var result = _converter.Convert(data); + Assert.DoesNotThrow(() => JsonDocument.Parse(result)); + Assert.That(result, Does.Contain("123")); + } + + [Test] + public void Convert_ComplexNesting_ProducesValidJson() + { + var complexXml = @" + + + + Book One + + Author A + Author B + + + +"; + var data = new XmlData { Document = XDocument.Parse(complexXml) }; + var result = _converter.Convert(data); + Assert.DoesNotThrow(() => JsonDocument.Parse(result)); + Assert.That(result, Does.Contain("authors")); + } +} diff --git a/tests/FileConversionLibrary.Tests/XmlToPdfConverterTests.cs b/tests/FileConversionLibrary.Tests/XmlToPdfConverterTests.cs new file mode 100644 index 0000000..bcb6151 --- /dev/null +++ b/tests/FileConversionLibrary.Tests/XmlToPdfConverterTests.cs @@ -0,0 +1,170 @@ +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; +using System.Xml.Linq; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class XmlToPdfConverterTests +{ + private XmlToPdfConverter _converter = null!; + private XmlData _simpleXmlData = null!; + private XmlData _complexXmlData = null!; + + [SetUp] + public void SetUp() + { + _converter = new XmlToPdfConverter(); + + // Simple XML + var simpleXml = @" + + + Widget + 19.99 + + + Gadget + 29.99 + +"; + _simpleXmlData = new XmlData { Document = XDocument.Parse(simpleXml) }; + + // Complex nested XML + var complexXml = @" + + + + John Doe + Developer + + + Jane Smith + Manager + + +"; + _complexXmlData = new XmlData { Document = XDocument.Parse(complexXml) }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_NullDocument_ThrowsArgumentException() + { + var data = new XmlData { Document = null! }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ReturnsByteArray() + { + var result = _converter.Convert(_simpleXmlData); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); + } + + [Test] + public void Convert_DefaultOptions_StartsWithPdfMagicBytes() + { + var result = _converter.Convert(_simpleXmlData); + // PDF magic bytes: %PDF (0x25 0x50 0x44 0x46) + Assert.That(result[0], Is.EqualTo(0x25)); // % + Assert.That(result[1], Is.EqualTo(0x50)); // P + Assert.That(result[2], Is.EqualTo(0x44)); // D + Assert.That(result[3], Is.EqualTo(0x46)); // F + } + + [Test] + public void Convert_DefaultOptions_ValidPdfStructure() + { + var result = _converter.Convert(_simpleXmlData); + var pdfString = System.Text.Encoding.ASCII.GetString(result); + Assert.That(pdfString, Does.Contain("%PDF")); + Assert.That(pdfString, Does.Contain("%%EOF")); + } + + // ---- Options testing ---- + + [Test] + public void Convert_LandscapeOrientation_ProducesValidPdf() + { + var options = new Dictionary + { + ["orientation"] = "landscape" + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result[0], Is.EqualTo(0x25)); // Still valid PDF + Assert.That(result.Length, Is.GreaterThan(0)); + } + + [Test] + public void Convert_CustomPageSize_ProducesValidPdf() + { + var options = new Dictionary + { + ["pageSize"] = "A3" + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result[0], Is.EqualTo(0x25)); // Still valid PDF + } + + [Test] + public void Convert_CustomFontSize_ProducesValidPdf() + { + var options = new Dictionary + { + ["fontSize"] = 14 + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result[0], Is.EqualTo(0x25)); // Still valid PDF + } + + [Test] + public void Convert_HierarchicalView_ProducesValidPdf() + { + var options = new Dictionary + { + ["hierarchicalView"] = true + }; + var result = _converter.Convert(_complexXmlData, options); + Assert.That(result[0], Is.EqualTo(0x25)); // Still valid PDF + Assert.That(result.Length, Is.GreaterThan(0)); + } + + // ---- Edge cases ---- + + [Test] + public void Convert_EmptyXml_ProducesValidPdf() + { + var emptyXml = @""; + var data = new XmlData { Document = XDocument.Parse(emptyXml) }; + var result = _converter.Convert(data); + Assert.That(result[0], Is.EqualTo(0x25)); // Still valid PDF + } + + [Test] + public void Convert_SingleElement_ProducesValidPdf() + { + var singleXml = @"Test"; + var data = new XmlData { Document = XDocument.Parse(singleXml) }; + var result = _converter.Convert(data); + Assert.That(result[0], Is.EqualTo(0x25)); // Still valid PDF + } + + [Test] + public void Convert_DeepNesting_ProducesValidPdf() + { + var result = _converter.Convert(_complexXmlData); + var pdfString = System.Text.Encoding.ASCII.GetString(result); + Assert.That(pdfString, Does.Contain("%%EOF")); // Valid PDF end + } +} diff --git a/tests/FileConversionLibrary.Tests/XmlToWordConverterTests.cs b/tests/FileConversionLibrary.Tests/XmlToWordConverterTests.cs new file mode 100644 index 0000000..a124306 --- /dev/null +++ b/tests/FileConversionLibrary.Tests/XmlToWordConverterTests.cs @@ -0,0 +1,187 @@ +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; +using System.IO.Compression; +using System.Xml.Linq; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class XmlToWordConverterTests +{ + private XmlToWordConverter _converter = null!; + private XmlData _simpleXmlData = null!; + private XmlData _complexXmlData = null!; + + [SetUp] + public void SetUp() + { + _converter = new XmlToWordConverter(); + + // Simple XML + var simpleXml = @" + + + Widget + 19.99 + + + Gadget + 29.99 + +"; + _simpleXmlData = new XmlData { Document = XDocument.Parse(simpleXml) }; + + // Complex nested XML + var complexXml = @" + + + + John Doe + Developer + + +"; + _complexXmlData = new XmlData { Document = XDocument.Parse(complexXml) }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_NullDocument_ThrowsArgumentException() + { + var data = new XmlData { Document = null! }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ReturnsByteArray() + { + var result = _converter.Convert(_simpleXmlData); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); + } + + [Test] + public void Convert_DefaultOptions_IsValidZipFile() + { + var result = _converter.Convert(_simpleXmlData); + // ZIP/DOCX magic bytes: PK (0x50 0x4B) + Assert.That(result[0], Is.EqualTo(0x50)); // P + Assert.That(result[1], Is.EqualTo(0x4B)); // K + } + + [Test] + public void Convert_DefaultOptions_ContainsWordDocumentXml() + { + var result = _converter.Convert(_simpleXmlData); + using var stream = new MemoryStream(result); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read); + + var documentEntry = archive.GetEntry("word/document.xml"); + Assert.That(documentEntry, Is.Not.Null, "word/document.xml should exist"); + + using var reader = new StreamReader(documentEntry!.Open()); + var content = reader.ReadToEnd(); + Assert.That(content, Does.Contain(" + { + ["useTable"] = true + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result[0], Is.EqualTo(0x50)); // Still valid ZIP/DOCX + } + + [Test] + public void Convert_LandscapeOrientation_ProducesValidDocx() + { + var options = new Dictionary + { + ["orientation"] = "landscape" + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result[0], Is.EqualTo(0x50)); // Still valid ZIP/DOCX + } + + [Test] + public void Convert_CustomFontSize_ProducesValidDocx() + { + var options = new Dictionary + { + ["fontSize"] = 14 + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result[0], Is.EqualTo(0x50)); // Still valid ZIP/DOCX + } + + [Test] + public void Convert_HierarchicalView_ProducesValidDocx() + { + var options = new Dictionary + { + ["hierarchicalView"] = true + }; + var result = _converter.Convert(_complexXmlData, options); + Assert.That(result[0], Is.EqualTo(0x50)); // Still valid ZIP/DOCX + } + + // ---- Edge cases ---- + + [Test] + public void Convert_EmptyXml_ProducesValidDocx() + { + var emptyXml = @""; + var data = new XmlData { Document = XDocument.Parse(emptyXml) }; + var result = _converter.Convert(data); + Assert.That(result[0], Is.EqualTo(0x50)); // Still valid ZIP/DOCX + } + + [Test] + public void Convert_SingleElement_ProducesValidDocx() + { + var singleXml = @"Test"; + var data = new XmlData { Document = XDocument.Parse(singleXml) }; + var result = _converter.Convert(data); + Assert.That(result[0], Is.EqualTo(0x50)); // Still valid ZIP/DOCX + } + + [Test] + public void Convert_ComplexNesting_ProducesValidDocx() + { + var result = _converter.Convert(_complexXmlData); + using var stream = new MemoryStream(result); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read); + + var documentEntry = archive.GetEntry("word/document.xml"); + Assert.That(documentEntry, Is.Not.Null); + } +} diff --git a/tests/FileConversionLibrary.Tests/XmlToYamlConverterTests.cs b/tests/FileConversionLibrary.Tests/XmlToYamlConverterTests.cs new file mode 100644 index 0000000..16085fd --- /dev/null +++ b/tests/FileConversionLibrary.Tests/XmlToYamlConverterTests.cs @@ -0,0 +1,195 @@ +using FileConversionLibrary.Converters; +using FileConversionLibrary.Models; +using System.Xml.Linq; + +namespace FileConversionLibrary.Tests; + +[TestFixture] +public class XmlToYamlConverterTests +{ + private XmlToYamlConverter _converter = null!; + private XmlData _simpleXmlData = null!; + private XmlData _nestedXmlData = null!; + private XmlData _attributesXmlData = null!; + + [SetUp] + public void SetUp() + { + _converter = new XmlToYamlConverter(); + + // Simple XML + var simpleXml = @" + + + Widget + 19.99 + +"; + _simpleXmlData = new XmlData { Document = XDocument.Parse(simpleXml) }; + + // Nested XML + var nestedXml = @" + + + + John Doe + Developer + + +"; + _nestedXmlData = new XmlData { Document = XDocument.Parse(nestedXml) }; + + // Attributes XML + var attributesXml = @" + + + Test + +"; + _attributesXmlData = new XmlData { Document = XDocument.Parse(attributesXml) }; + } + + // ---- Argument guards ---- + + [Test] + public void Convert_NullInput_ThrowsArgumentException() + { + Assert.Throws(() => _converter.Convert(null!)); + } + + [Test] + public void Convert_NullDocument_ThrowsArgumentException() + { + var data = new XmlData { Document = null! }; + Assert.Throws(() => _converter.Convert(data)); + } + + // ---- Default behaviour ---- + + [Test] + public void Convert_DefaultOptions_ProducesValidYaml() + { + var result = _converter.Convert(_simpleXmlData); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); + // YAML should have colons or dashes + Assert.That(result.Contains(':') || result.StartsWith("---", StringComparison.Ordinal), Is.True); + } + + [Test] + public void Convert_DefaultOptions_StartsWithDocumentMarker() + { + var result = _converter.Convert(_simpleXmlData); + // Many YAML files start with --- + var trimmed = result.TrimStart(); + Assert.That(trimmed.StartsWith("---", StringComparison.Ordinal) || trimmed.Contains(':'), Is.True); + } + + [Test] + public void Convert_DefaultOptions_PreservesStructure() + { + var result = _converter.Convert(_simpleXmlData); + Assert.That(result, Does.Contain("products")); + Assert.That(result, Does.Contain("product")); + Assert.That(result, Does.Contain("name")); + } + + [Test] + public void Convert_NestedXml_PreservesHierarchy() + { + var result = _converter.Convert(_nestedXmlData); + Assert.That(result, Does.Contain("company")); + Assert.That(result, Does.Contain("department")); + Assert.That(result, Does.Contain("employee")); + } + + [Test] + public void Convert_AttributesIncluded_MarkedWithPrefix() + { + var result = _converter.Convert(_attributesXmlData); + // Attributes should be included (typically with @ prefix) + Assert.That(result.Contains("@id") || result.Contains("id:"), Is.True); + } + + // ---- Options testing ---- + + [Test] + public void Convert_NoIndentation_MinimalWhitespace() + { + var options = new Dictionary + { + ["useIndentation"] = false + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result, Is.Not.Null); + // Minified YAML should have fewer spaces + var spaceCount = result.Count(c => c == ' '); + + var defaultResult = _converter.Convert(_simpleXmlData); + var defaultSpaceCount = defaultResult.Count(c => c == ' '); + + Assert.That(spaceCount, Is.LessThanOrEqualTo(defaultSpaceCount)); + } + + [Test] + public void Convert_ConvertValuesFalse_KeepsStrings() + { + var options = new Dictionary + { + ["convertValues"] = false + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result, Is.Not.Null); + Assert.That(result.Length, Is.GreaterThan(0)); // Should have quotes + } + + [Test] + public void Convert_SortKeysTrue_SortsAlphabetically() + { + var options = new Dictionary + { + ["sortKeys"] = true + }; + var result = _converter.Convert(_simpleXmlData, options); + Assert.That(result, Is.Not.Null); + // Just verify it doesn't throw + } + + // ---- Edge cases ---- + + [Test] + public void Convert_EmptyXml_ProducesValidYaml() + { + var emptyXml = @""; + var data = new XmlData { Document = XDocument.Parse(emptyXml) }; + var result = _converter.Convert(data); + Assert.That(result, Is.Not.Null); + Assert.That(result, Does.Contain("root")); + } + + [Test] + public void Convert_SingleElement_ProducesValidYaml() + { + var singleXml = @"123"; + var data = new XmlData { Document = XDocument.Parse(singleXml) }; + var result = _converter.Convert(data); + Assert.That(result, Does.Contain("123")); + } + + [Test] + public void Convert_ComplexNesting_ProducesValidYaml() + { + var result = _converter.Convert(_nestedXmlData); + // Check for proper indentation and structure + var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.That(lines.Length, Is.GreaterThan(1)); + } + + [Test] + public void Convert_MultipleAttributes_AllIncluded() + { + var result = _converter.Convert(_attributesXmlData); + // All attributes should appear + Assert.That(result.Contains("id") && result.Contains("code") && result.Contains("status"), Is.True); + } +}