├── logo ├── logo.jpg ├── logo.png └── logo64.png ├── assets └── title.png ├── samples ├── psake │ ├── .nuget │ │ ├── nuget.exe │ │ └── packages.config │ ├── build.cmd │ ├── _config.yml │ ├── config.json │ ├── build.ps1 │ └── Web.config ├── powershell │ ├── MagicChunks.dll │ ├── _config.yml │ ├── config.json │ ├── MagicChunks.psm1 │ ├── build.ps1 │ └── Web.config ├── msbuild │ ├── tools │ │ ├── MagicChunks.dll │ │ └── MagicChunks.targets │ ├── _config.yml │ ├── config.json │ ├── build.msbuild │ └── Web.config ├── netapp │ ├── packages.config │ ├── App.config │ ├── _config.yml │ ├── config.json │ ├── TransformSample.sln │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Program.cs │ ├── TransformSample.csproj │ └── Web.config └── cake │ ├── _config.yml │ ├── config.json │ ├── build.cake │ ├── build.ps1 │ └── Web.config ├── src ├── MagicChunks │ ├── VSTS │ │ ├── images │ │ │ ├── icon.png │ │ │ ├── icon-large.png │ │ │ ├── screenshot1.png │ │ │ ├── screenshot2.png │ │ │ ├── get-started-2.png │ │ │ ├── get-started-3.png │ │ │ ├── get-started-4.png │ │ │ └── get-started-5.png │ │ ├── MagicChunks │ │ │ ├── icon.png │ │ │ ├── transform.ps1 │ │ │ └── task.json │ │ ├── videos │ │ │ └── magic-chunks.gif │ │ ├── overview.md │ │ └── vss-extension.json │ ├── MSBuild │ │ ├── MagicChunks.targets │ │ └── TransformConfig.cs │ ├── Core │ │ ├── IDocument.cs │ │ ├── TransformationCollection.cs │ │ ├── ITransformer.cs │ │ └── Transformer.cs │ ├── Documents │ │ ├── IgnoreCaseComparer.cs │ │ ├── CustomObjectFactory.cs │ │ ├── YamlDocument.cs │ │ ├── JsonDocument.cs │ │ └── XmlDocument.cs │ ├── Helpers │ │ ├── ReflectionExtensions.cs │ │ ├── JsonExtensions.cs │ │ └── XmlExtensions.cs │ ├── Powershell │ │ └── MagicChunks.psm1 │ ├── MagicChunks.csproj │ └── TransformTask.cs ├── .editorconfig ├── MagicChunks.Cake │ ├── MagicChunks.Cake.csproj │ └── MagicChunksAliases.cs ├── MagicChunks.sln └── MagicChunks.Tests │ ├── MagicChunks.Tests.csproj │ ├── Documents │ ├── YamlDocumentTests.cs │ └── JsonDocumentTests.cs │ └── Core │ └── TransformerTests.cs ├── .vscode ├── settings.json └── markdown.css ├── .appveyor.yml ├── LICENSE ├── .gitignore ├── nuspecs └── MagicChunks.nuspec └── readme.md /logo/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/logo/logo.jpg -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/logo/logo.png -------------------------------------------------------------------------------- /logo/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/logo/logo64.png -------------------------------------------------------------------------------- /assets/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/assets/title.png -------------------------------------------------------------------------------- /samples/psake/.nuget/nuget.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/samples/psake/.nuget/nuget.exe -------------------------------------------------------------------------------- /samples/powershell/MagicChunks.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/samples/powershell/MagicChunks.dll -------------------------------------------------------------------------------- /samples/msbuild/tools/MagicChunks.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/samples/msbuild/tools/MagicChunks.dll -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/icon.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/MagicChunks/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/MagicChunks/icon.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/icon-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/icon-large.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/screenshot1.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/screenshot2.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/get-started-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/get-started-2.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/get-started-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/get-started-3.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/get-started-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/get-started-4.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/images/get-started-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/images/get-started-5.png -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/videos/magic-chunks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/HEAD/src/MagicChunks/VSTS/videos/magic-chunks.gif -------------------------------------------------------------------------------- /samples/netapp/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /samples/psake/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd "%~dp0" 3 | .nuget\NuGet.exe install .nuget\packages.config -OutputDirectory packages 4 | packages\psake.4.4.2\tools\psake.cmd .\build.ps1 %* -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | [*.cs] 7 | indent_style = space 8 | end_of_line = crlf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "markdown.styles": [ 4 | "file://D:/OpenSource/magic-chunks/.vscode/markdown.css" 5 | ] 6 | } -------------------------------------------------------------------------------- /samples/msbuild/tools/MagicChunks.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/netapp/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/MagicChunks/MSBuild/MagicChunks.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/psake/.nuget/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/MagicChunks/Core/IDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MagicChunks.Core 4 | { 5 | public interface IDocument : IDisposable 6 | { 7 | void AddElementToArray(string[] path, string value); 8 | void ReplaceKey(string[] path, string value); 9 | void RemoveKey(string[] path); 10 | } 11 | } -------------------------------------------------------------------------------- /samples/cake/_config.yml: -------------------------------------------------------------------------------- 1 | baseUrl: "http://localhost/" 2 | permalink: ":year/:title" 3 | frontend_version: 1.0.0 4 | only_frontmatter_categories: true 5 | exclude: 6 | - node_modules\ 7 | - styles\ 8 | - scripts\ 9 | - .vscode\ 10 | - package.json 11 | - gulpfile.js 12 | - jsconfig.json 13 | similar_posts: 14 | filter_threshold: 0.3 15 | related_count: 3 -------------------------------------------------------------------------------- /samples/msbuild/_config.yml: -------------------------------------------------------------------------------- 1 | baseUrl: "http://localhost/" 2 | permalink: ":year/:title" 3 | frontend_version: 1.0.0 4 | only_frontmatter_categories: true 5 | exclude: 6 | - node_modules\ 7 | - styles\ 8 | - scripts\ 9 | - .vscode\ 10 | - package.json 11 | - gulpfile.js 12 | - jsconfig.json 13 | similar_posts: 14 | filter_threshold: 0.3 15 | related_count: 3 -------------------------------------------------------------------------------- /samples/netapp/_config.yml: -------------------------------------------------------------------------------- 1 | baseUrl: "http://localhost/" 2 | permalink: ":year/:title" 3 | frontend_version: 1.0.0 4 | only_frontmatter_categories: true 5 | exclude: 6 | - node_modules\ 7 | - styles\ 8 | - scripts\ 9 | - .vscode\ 10 | - package.json 11 | - gulpfile.js 12 | - jsconfig.json 13 | similar_posts: 14 | filter_threshold: 0.3 15 | related_count: 3 -------------------------------------------------------------------------------- /samples/psake/_config.yml: -------------------------------------------------------------------------------- 1 | baseUrl: "http://localhost/" 2 | permalink: ":year/:title" 3 | frontend_version: 1.0.0 4 | only_frontmatter_categories: true 5 | exclude: 6 | - node_modules\ 7 | - styles\ 8 | - scripts\ 9 | - .vscode\ 10 | - package.json 11 | - gulpfile.js 12 | - jsconfig.json 13 | similar_posts: 14 | filter_threshold: 0.3 15 | related_count: 3 -------------------------------------------------------------------------------- /samples/powershell/_config.yml: -------------------------------------------------------------------------------- 1 | baseUrl: "http://localhost/" 2 | permalink: ":year/:title" 3 | frontend_version: 1.0.0 4 | only_frontmatter_categories: true 5 | exclude: 6 | - node_modules\ 7 | - styles\ 8 | - scripts\ 9 | - .vscode\ 10 | - package.json 11 | - gulpfile.js 12 | - jsconfig.json 13 | similar_posts: 14 | filter_threshold: 0.3 15 | related_count: 3 -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | branches: 3 | only: 4 | - master 5 | - net-standard-migration 6 | image: Visual Studio 2019 7 | build_script: 8 | - ps: >- 9 | cd build 10 | 11 | ./build.ps1 12 | test_script: 13 | - ps: >- 14 | cd ..\working\sources\src\MagicChunks.Tests 15 | 16 | 17 | dotnet xunit 18 | artifacts: 19 | - path: working/dotnet/**/*.zip 20 | name: dotnet 21 | - path: working/nuget/*.nupkg 22 | name: nuget 23 | - path: working/vsts/**/*.vsix 24 | name: vsts -------------------------------------------------------------------------------- /src/MagicChunks/Core/TransformationCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MagicChunks.Core 4 | { 5 | public class TransformationCollection : Dictionary 6 | { 7 | public TransformationCollection() 8 | { 9 | } 10 | 11 | public TransformationCollection(params string[] keysToRemove) 12 | { 13 | KeysToRemove = keysToRemove; 14 | } 15 | 16 | public IEnumerable KeysToRemove { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/MagicChunks/Documents/IgnoreCaseComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MagicChunks.Documents 4 | { 5 | public class IgnoreCaseComparer : IEqualityComparer 6 | { 7 | bool IEqualityComparer.Equals(object x, object y) 8 | { 9 | if (x is string && y is string) 10 | return ((string)x).ToLowerInvariant() == ((string)y).ToLowerInvariant(); 11 | 12 | return x == y; 13 | } 14 | 15 | int IEqualityComparer.GetHashCode(object obj) 16 | { 17 | var s = obj as string; 18 | return s != null ? s.ToLowerInvariant().GetHashCode() : obj.GetHashCode(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/MagicChunks/Helpers/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace MagicChunks.Helpers 7 | { 8 | public static class ReflectionExtensions 9 | { 10 | public static IEnumerable GetAllTypes(this Assembly source, Func predicate = null) 11 | { 12 | IEnumerable result; 13 | try 14 | { 15 | result = source.DefinedTypes.ToArray(); 16 | } 17 | catch (ReflectionTypeLoadException ex) 18 | { 19 | result = ex.Types.Where(t => t != null).Select(t => t.GetTypeInfo()).ToArray(); 20 | } 21 | 22 | if (predicate != null) 23 | result = result.Where(predicate); 24 | 25 | return result.Select(t => t.AsType()).ToArray(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/cake/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "DefaultConnection": { 4 | "ConnectionString": "mongodb://localhost", 5 | "DatabaseName": "TestDb" 6 | } 7 | }, 8 | 9 | "Logging": { 10 | "IncludeScopes": false, 11 | "LogLevel": { 12 | "Default": "Verbose", 13 | "System": "Information", 14 | "Microsoft": "Information", 15 | "Performance": "Information" 16 | } 17 | }, 18 | 19 | "Smtp": { 20 | "Method": "SpecifiedPickupDirectory", 21 | 22 | "From": { 23 | "Address": "noreply@gmail.com", 24 | "DisplayName": "TicketUp" 25 | }, 26 | 27 | "SpecifiedPickupDirectory": { 28 | "PickupDirectoryLocation": "myapp\\maildrop\\", 29 | "AbsolutePath": false 30 | }, 31 | 32 | "Network": { 33 | "Host": "smtp.gmail.com", 34 | "Port": 587, 35 | "Timeout": 3000, 36 | "EnableSsl": true, 37 | "Credentials": { 38 | "Username": "testuser@gmail.com", 39 | "Password": "123456" 40 | } 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /samples/msbuild/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "DefaultConnection": { 4 | "ConnectionString": "mongodb://localhost", 5 | "DatabaseName": "TestDb" 6 | } 7 | }, 8 | 9 | "Logging": { 10 | "IncludeScopes": false, 11 | "LogLevel": { 12 | "Default": "Verbose", 13 | "System": "Information", 14 | "Microsoft": "Information", 15 | "Performance": "Information" 16 | } 17 | }, 18 | 19 | "Smtp": { 20 | "Method": "SpecifiedPickupDirectory", 21 | 22 | "From": { 23 | "Address": "noreply@gmail.com", 24 | "DisplayName": "TicketUp" 25 | }, 26 | 27 | "SpecifiedPickupDirectory": { 28 | "PickupDirectoryLocation": "myapp\\maildrop\\", 29 | "AbsolutePath": false 30 | }, 31 | 32 | "Network": { 33 | "Host": "smtp.gmail.com", 34 | "Port": 587, 35 | "Timeout": 3000, 36 | "EnableSsl": true, 37 | "Credentials": { 38 | "Username": "testuser@gmail.com", 39 | "Password": "123456" 40 | } 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /samples/netapp/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "DefaultConnection": { 4 | "ConnectionString": "mongodb://localhost", 5 | "DatabaseName": "TestDb" 6 | } 7 | }, 8 | 9 | "Logging": { 10 | "IncludeScopes": false, 11 | "LogLevel": { 12 | "Default": "Verbose", 13 | "System": "Information", 14 | "Microsoft": "Information", 15 | "Performance": "Information" 16 | } 17 | }, 18 | 19 | "Smtp": { 20 | "Method": "SpecifiedPickupDirectory", 21 | 22 | "From": { 23 | "Address": "noreply@gmail.com", 24 | "DisplayName": "TicketUp" 25 | }, 26 | 27 | "SpecifiedPickupDirectory": { 28 | "PickupDirectoryLocation": "myapp\\maildrop\\", 29 | "AbsolutePath": false 30 | }, 31 | 32 | "Network": { 33 | "Host": "smtp.gmail.com", 34 | "Port": 587, 35 | "Timeout": 3000, 36 | "EnableSsl": true, 37 | "Credentials": { 38 | "Username": "testuser@gmail.com", 39 | "Password": "123456" 40 | } 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /samples/psake/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "DefaultConnection": { 4 | "ConnectionString": "mongodb://localhost", 5 | "DatabaseName": "TestDb" 6 | } 7 | }, 8 | 9 | "Logging": { 10 | "IncludeScopes": false, 11 | "LogLevel": { 12 | "Default": "Verbose", 13 | "System": "Information", 14 | "Microsoft": "Information", 15 | "Performance": "Information" 16 | } 17 | }, 18 | 19 | "Smtp": { 20 | "Method": "SpecifiedPickupDirectory", 21 | 22 | "From": { 23 | "Address": "noreply@gmail.com", 24 | "DisplayName": "TicketUp" 25 | }, 26 | 27 | "SpecifiedPickupDirectory": { 28 | "PickupDirectoryLocation": "myapp\\maildrop\\", 29 | "AbsolutePath": false 30 | }, 31 | 32 | "Network": { 33 | "Host": "smtp.gmail.com", 34 | "Port": 587, 35 | "Timeout": 3000, 36 | "EnableSsl": true, 37 | "Credentials": { 38 | "Username": "testuser@gmail.com", 39 | "Password": "123456" 40 | } 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /samples/powershell/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Data": { 3 | "DefaultConnection": { 4 | "ConnectionString": "mongodb://localhost", 5 | "DatabaseName": "TestDb" 6 | } 7 | }, 8 | 9 | "Logging": { 10 | "IncludeScopes": false, 11 | "LogLevel": { 12 | "Default": "Verbose", 13 | "System": "Information", 14 | "Microsoft": "Information", 15 | "Performance": "Information" 16 | } 17 | }, 18 | 19 | "Smtp": { 20 | "Method": "SpecifiedPickupDirectory", 21 | 22 | "From": { 23 | "Address": "noreply@gmail.com", 24 | "DisplayName": "TicketUp" 25 | }, 26 | 27 | "SpecifiedPickupDirectory": { 28 | "PickupDirectoryLocation": "myapp\\maildrop\\", 29 | "AbsolutePath": false 30 | }, 31 | 32 | "Network": { 33 | "Host": "smtp.gmail.com", 34 | "Port": 587, 35 | "Timeout": 3000, 36 | "EnableSsl": true, 37 | "Credentials": { 38 | "Username": "testuser@gmail.com", 39 | "Password": "123456" 40 | } 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /samples/netapp/TransformSample.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25123.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransformSample", "TransformSample.csproj", "{C1BF6ECD-1451-46E3-9CC4-1B9F0CB577A9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C1BF6ECD-1451-46E3-9CC4-1B9F0CB577A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C1BF6ECD-1451-46E3-9CC4-1B9F0CB577A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C1BF6ECD-1451-46E3-9CC4-1B9F0CB577A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C1BF6ECD-1451-46E3-9CC4-1B9F0CB577A9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /src/MagicChunks/Documents/CustomObjectFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using YamlDotNet.Serialization; 5 | using YamlDotNet.Serialization.ObjectFactories; 6 | 7 | namespace MagicChunks.Documents 8 | { 9 | public class CustomObjectFactory : IObjectFactory 10 | { 11 | protected readonly IObjectFactory BaseFactory; 12 | 13 | public CustomObjectFactory() 14 | : this(new DefaultObjectFactory()) 15 | { 16 | } 17 | 18 | public CustomObjectFactory(IObjectFactory baseFactory) 19 | { 20 | if (baseFactory == null) 21 | throw new ArgumentNullException(nameof(baseFactory)); 22 | 23 | BaseFactory = baseFactory; 24 | } 25 | 26 | public object Create(Type type) 27 | { 28 | if (typeof(Dictionary).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) 29 | { 30 | return Activator.CreateInstance(type, new IgnoreCaseComparer()); 31 | } 32 | 33 | return BaseFactory.Create(type); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sergey Zwezdin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/powershell/MagicChunks.psm1: -------------------------------------------------------------------------------- 1 | Function Format-MagicChunks() { 2 | <# 3 | .SYNOPSIS 4 | Transforms source document with specified transformations 5 | #> 6 | [CmdletBinding()] 7 | param( 8 | [Parameter(Mandatory=$True, ValueFromPipeline=$True)] 9 | [System.IO.FileInfo]$path, 10 | 11 | [Parameter()] 12 | [string]$target, 13 | 14 | [Parameter(Mandatory=$True)] 15 | [hashtable]$transformations, 16 | 17 | [Parameter()] 18 | [string]$type 19 | ) 20 | PROCESS { 21 | Add-Type -Path "$PSScriptRoot\MagicChunks.dll" 22 | 23 | Write-Host "Transforming file $($path)" 24 | 25 | try { 26 | $transforms = New-Object -TypeName MagicChunks.Core.TransformationCollection ` 27 | 28 | foreach($t in $transformations.GetEnumerator()) { 29 | $transforms.Add($t.Key, $t.Value) 30 | } 31 | 32 | [MagicChunks.TransformTask]::Transform($type, $path, ($target, $path)[[string]::IsNullOrWhiteSpace($target)], $transforms) 33 | 34 | Write-Host "File transformed to $(($target, $path)[[string]::IsNullOrWhiteSpace($target)])" 35 | } 36 | catch { 37 | Write-Error -Message "File transformation error: $($_.Exception.Message)" -Exception $_.Exception 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/MagicChunks/Powershell/MagicChunks.psm1: -------------------------------------------------------------------------------- 1 | Function Format-MagicChunks() { 2 | <# 3 | .SYNOPSIS 4 | Transforms source document with specified transformations 5 | #> 6 | [CmdletBinding()] 7 | param( 8 | [Parameter(Mandatory=$True, ValueFromPipeline=$True)] 9 | [System.IO.FileInfo]$path, 10 | 11 | [Parameter()] 12 | [string]$target, 13 | 14 | [Parameter(Mandatory=$True)] 15 | [hashtable]$transformations, 16 | 17 | [Parameter()] 18 | [string]$type 19 | ) 20 | PROCESS { 21 | Add-Type -Path "$PSScriptRoot\MagicChunks.dll" 22 | 23 | Write-Host "Transforming file $($path)" 24 | 25 | try { 26 | $transforms = New-Object -TypeName MagicChunks.Core.TransformationCollection ` 27 | 28 | foreach($t in $transformations.GetEnumerator()) { 29 | $transforms.Add($t.Key, $t.Value) 30 | } 31 | 32 | [MagicChunks.TransformTask]::Transform($type, $path, ($target, $path)[[string]::IsNullOrWhiteSpace($target)], $transforms) 33 | 34 | Write-Host "File transformed to $(($target, $path)[[string]::IsNullOrWhiteSpace($target)])" 35 | } 36 | catch { 37 | Write-Error -Message "File transformation error: $($_.Exception.Message)" -Exception $_.Exception 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/MagicChunks.Cake/MagicChunks.Cake.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Sergey Zwezdin 6 | 7 | Magic Chunks 8 | Easy to use tool to config transformations for JSON, XML and YAML. 9 | (c) Sergey Zwezdin, 2017 10 | MIT 11 | https://github.com/magic-chunks/magic-chunks-dotnetcore 12 | logo64.png 13 | https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/master/logo/logo64.png 14 | https://github.com/magic-chunks/magic-chunks-dotnetcore 15 | Git 16 | Configuration, Transform, JSON, XML, YAML, YML, web.config, app.config, appsetings.json 17 | true 18 | 19 | 20 | 21 | 22 | True 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /samples/netapp/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("TransformSample")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("TransformSample")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("c1bf6ecd-1451-46e3-9cc4-1b9f0cb577a9")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /samples/netapp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using MagicChunks.Core; 4 | 5 | namespace TransformSample 6 | { 7 | internal class Program 8 | { 9 | private static void Main(string[] args) 10 | { 11 | var transformer = new Transformer(); 12 | 13 | Console.WriteLine( 14 | transformer.Transform(File.ReadAllText(@"Web.config"), new TransformationCollection 15 | { 16 | {"configuration/system.web/authentication/@mode", "Forms"}, 17 | {"configuration/system.web/httpRuntime", "125"}, 18 | {"configuration/appSettings/add[@key='LoadBundledScripts']/@value", "true"}, 19 | {"configuration/appSettings/add[@key='SomethingNew']/@value", "NewValue"}, 20 | {"configuration/newKey", "12345"} 21 | })); 22 | Console.Read(); 23 | 24 | Console.WriteLine( 25 | transformer.Transform(File.ReadAllText(@"config.json"), new TransformationCollection 26 | { 27 | {"Data/DefaultConnection/ConnectionString", "mongodb://10.1.25.144/"}, 28 | {"Data/DefaultConnection/Production", "true"}, 29 | {"Smtp/Method", "Network"}, 30 | {"NewKey", "12345"} 31 | })); 32 | Console.Read(); 33 | 34 | Console.WriteLine( 35 | transformer.Transform(File.ReadAllText(@"_config.yml"), new TransformationCollection 36 | { 37 | {"baseUrl", "http://production.com/"}, 38 | {"frontend_version", "3.0.5"}, 39 | {"new_key", "23"}, 40 | {"another_key/val/t", "qwerty"} 41 | })); 42 | Console.Read(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/MagicChunks/MagicChunks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.3;netstandard1.6;netstandard2.0 5 | true 6 | Sergey Zwezdin 7 | 8 | Magic Chunks 9 | Easy to use tool to config transformations for JSON, XML and YAML. 10 | (c) Sergey Zwezdin, 2017 11 | MIT 12 | https://github.com/magic-chunks/magic-chunks-dotnetcore 13 | logo64.png 14 | https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/master/logo/logo64.png 15 | https://github.com/magic-chunks/magic-chunks-dotnetcore 16 | Git 17 | Configuration, Transform, JSON, XML, YAML, YML, web.config, app.config, appsetings.json 18 | true 19 | 20 | 21 | 22 | 23 | True 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/MagicChunks.Cake/MagicChunksAliases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cake.Core; 3 | using Cake.Core.Annotations; 4 | using Cake.Core.Diagnostics; 5 | using MagicChunks.Core; 6 | 7 | namespace MagicChunks.Cake 8 | { 9 | /// 10 | /// Cake build aliases for MagicChunks 11 | /// 12 | [CakeAliasCategory("Configuration")] 13 | [CakeNamespaceImport("MagicChunks")] 14 | [CakeNamespaceImport("MagicChunks.Core")] 15 | public static class MagicChunksAliases 16 | { 17 | [CakeMethodAlias] 18 | public static bool TransformConfig(this ICakeContext context, string path, 19 | TransformationCollection transformations) 20 | { 21 | return TransformConfig(context, null, path, path, transformations); 22 | } 23 | 24 | [CakeMethodAlias] 25 | public static bool TransformConfig(this ICakeContext context, string path, string target, 26 | TransformationCollection transformations) 27 | { 28 | return TransformConfig(context, null, path, target, transformations); 29 | 30 | } 31 | 32 | [CakeMethodAlias] 33 | public static bool TransformConfig(this ICakeContext context, string type, string path, string target, 34 | TransformationCollection transformations) 35 | { 36 | var result = true; 37 | 38 | context.Log.Write(Verbosity.Normal, LogLevel.Information, "Transforming file: {0}", path); 39 | 40 | try 41 | { 42 | TransformTask.Transform(type, path, target ?? path, transformations); 43 | 44 | context.Log.Write(Verbosity.Normal, LogLevel.Information, "File transformed to: {0}", target ?? path); 45 | } 46 | catch (Exception ex) 47 | { 48 | context.Log.Write(Verbosity.Normal, LogLevel.Error, "File transformation error: {0}", ex.Message); 49 | result = false; 50 | } 51 | 52 | return result; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/overview.md: -------------------------------------------------------------------------------- 1 | **Magic Chunks** is easy to use tool to config transformations for JSON, XML and YAML. 2 | 3 | Everyone remember [XML Document Transform](https://msdn.microsoft.com/en-us/library/dd465326.aspx) syntax to transform configuration files during the build process. But world is changing and now you can have different config types in your .NET projects. 4 | 5 | Magic Chunks allows you to transform you **JSON**, **XML** and **YAML** files. You can run it at MSBuild, Cake, PSake or Powershell script as well as use this Visual Studio Team Services build extension. 6 | 7 | 8 | # How it works 9 | 10 | The main idea is quite simple. Magic Chunks represents transformation as a *key-value* collection. 11 | 12 | The key contains path at source file which should be modified. And the value contains data for this path in modified file. 13 | 14 | Imagine you have following **JSON based configuration** file. 15 | 16 | 17 | ```json 18 | { 19 | "ConnectionStrings": { 20 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=webapp" 21 | } 22 | } 23 | ``` 24 | 25 | So following **transformations** could be applied to this config: 26 | 27 | 28 | ```json 29 | { 30 | "ConnectionStrings/DefaultConnection": "Data Source=10.0.0.5;Initial Catalog=Db1" 31 | } 32 | ``` 33 | 34 | As a **result** you will have: 35 | 36 | ```json 37 | { 38 | "ConnectionStrings": { 39 | "DefaultConnection": "Data Source=10.0.0.5;Initial Catalog=Db1" 40 | } 41 | } 42 | ``` 43 | 44 | # Getting started 45 | 46 | ### Step 1: Install this extension into your VSTS account. 47 | ### Step 2: In your build or release definition add new build step. 48 | ![Add build step](images/get-started-2.png) 49 | ### Step 3: Select "Config transformation" under "Utility" group and click "Add". 50 | ![Add build step](images/get-started-3.png) 51 | ### Step 4: Specify configuration file path and required transformations. 52 | ![Add build step](images/get-started-4.png) 53 | ### Alternately, specify a path to a file containing the transformations. 54 | ![Add build step](images/get-started-5.png) 55 | ### Step 5: Run build. Profit! 56 | 57 | # Setup in 30 seconds 58 | 59 | ![Add build step](videos/magic-chunks.gif) -------------------------------------------------------------------------------- /src/MagicChunks.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicChunks", "MagicChunks\MagicChunks.csproj", "{1A51168D-B3DA-4D8C-951B-AFF2DA6664FE}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicChunks.Cake", "MagicChunks.Cake\MagicChunks.Cake.csproj", "{D9FABFD0-10E2-401D-ADCA-172F74A92446}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicChunks.Tests", "MagicChunks.Tests\MagicChunks.Tests.csproj", "{7CB305A2-C9F2-4CBB-ABA1-F883E58D160A}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {1A51168D-B3DA-4D8C-951B-AFF2DA6664FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {1A51168D-B3DA-4D8C-951B-AFF2DA6664FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {1A51168D-B3DA-4D8C-951B-AFF2DA6664FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {1A51168D-B3DA-4D8C-951B-AFF2DA6664FE}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {D9FABFD0-10E2-401D-ADCA-172F74A92446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {D9FABFD0-10E2-401D-ADCA-172F74A92446}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {D9FABFD0-10E2-401D-ADCA-172F74A92446}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {D9FABFD0-10E2-401D-ADCA-172F74A92446}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {7CB305A2-C9F2-4CBB-ABA1-F883E58D160A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {7CB305A2-C9F2-4CBB-ABA1-F883E58D160A}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {7CB305A2-C9F2-4CBB-ABA1-F883E58D160A}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {7CB305A2-C9F2-4CBB-ABA1-F883E58D160A}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {E1118607-AA51-4A52-8D15-0DF7639627EF} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pgc 27 | *.pgd 28 | *.rsp 29 | *.sbr 30 | *.tlb 31 | *.tli 32 | *.tlh 33 | *.tmp 34 | *.log 35 | *.vspscc 36 | *.vssscc 37 | .builds 38 | 39 | # Visual C++ cache files 40 | ipch/ 41 | *.aps 42 | *.ncb 43 | *.opensdf 44 | 45 | # Visual Studio profiler 46 | *.psess 47 | *.vsp 48 | *.vspx 49 | 50 | # Guidance Automation Toolkit 51 | *.gpState 52 | 53 | # ReSharper is a .NET coding add-in 54 | _ReSharper* 55 | 56 | # NCrunch 57 | *.ncrunch* 58 | .*crunch*.local.xml 59 | 60 | # Installshield output folder 61 | [Ee]xpress 62 | 63 | # DocProject is a documentation generator add-in 64 | DocProject/buildhelp/ 65 | DocProject/Help/*.HxT 66 | DocProject/Help/*.HxC 67 | DocProject/Help/*.hhc 68 | DocProject/Help/*.hhk 69 | DocProject/Help/*.hhp 70 | DocProject/Help/Html2 71 | DocProject/Help/html 72 | 73 | # Click-Once directory 74 | publish 75 | 76 | # Publish Web Output 77 | *.Publish.xml 78 | 79 | # NuGet Packages Directory 80 | packages 81 | nuspecs/Net45 82 | 83 | # Windows Azure Build Output 84 | csx 85 | *.build.csdef 86 | 87 | # Windows Store app package directory 88 | AppPackages/ 89 | 90 | # Others 91 | [Bb]in 92 | [Oo]bj 93 | sql 94 | TestResults 95 | [Tt]est[Rr]esult* 96 | *.Cache 97 | ClientBin 98 | [Ss]tyle[Cc]op.* 99 | ~$* 100 | *.dbmdl 101 | Generated_Code #added for RIA/Silverlight projects 102 | 103 | # Backup & report files from converting an old project file to a newer 104 | # Visual Studio version. Backup files are not needed, because we have git ;-) 105 | _UpgradeReport_Files/ 106 | Backup*/ 107 | UpgradeLog*.XML 108 | 109 | ### Cake ### 110 | tools/* 111 | !tools/packages.config 112 | 113 | artifacts/ 114 | src/.vs 115 | working/ 116 | build/tools/ -------------------------------------------------------------------------------- /samples/powershell/build.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ".\MagicChunks.psm1"; 2 | 3 | Format-MagicChunks -path "$PSScriptRoot\Web.config" -target "$PSScriptRoot\Web.transformed.1.config" -transformations @{ 4 | "configuration/system.web/authentication/@mode" = "Forms"; 5 | "configuration/system.web/httpRuntime" = "125"; 6 | "configuration/appSettings/add[@key='LoadBundledScripts']/@value" = "true"; 7 | "configuration/appSettings/add[@key='SomethingNew']/@value" = "NewValue"; 8 | "configuration/newKey" = "12345" 9 | } 10 | 11 | Format-MagicChunks -path "$PSScriptRoot\Web.config" -target "$PSScriptRoot\Web.transformed.2.config" -transformations @{ 12 | "configuration/system.web/authentication/@mode" = "Forms"; 13 | "configuration/system.web/httpRuntime" = "125"; 14 | "configuration/appSettings/add[@key='LoadBundledScripts']/@value" = "true"; 15 | "configuration/appSettings/add[@key='SomethingNew']/@value" = "NewValue"; 16 | "configuration/newKey" = "12345" 17 | } -type Xml 18 | 19 | Format-MagicChunks -path "$PSScriptRoot\config.json" -target "$PSScriptRoot\config.transformed.1.json" -transformations @{ 20 | "Data/DefaultConnection/ConnectionString" = "mongodb://10.1.25.144/"; 21 | "Data/DefaultConnection/Production" = "true"; 22 | "Smtp/Method" = "Network"; 23 | "NewKey" = "12345" 24 | } 25 | 26 | Format-MagicChunks -path "$PSScriptRoot\config.json" -target "$PSScriptRoot\config.transformed.2.json" -transformations @{ 27 | "Data/DefaultConnection/ConnectionString" = "mongodb://10.1.25.144/"; 28 | "Data/DefaultConnection/Production" = "true"; 29 | "Smtp/Method" = "Network"; 30 | "NewKey" = "12345" 31 | } -type Json 32 | 33 | Format-MagicChunks -path "$PSScriptRoot\_config.yml" -target "$PSScriptRoot\_config.transformed.1.yml" -transformations @{ 34 | "baseUrl" = "http://production.com/"; 35 | "frontend_version" = "3.0.5"; 36 | "new_key" = "23"; 37 | "another_key/val/t" = "qwerty" 38 | } 39 | 40 | Format-MagicChunks -path "$PSScriptRoot\_config.yml" -target "$PSScriptRoot\_config.transformed.2.yml" -transformations @{ 41 | "baseUrl" = "http://production.com/"; 42 | "frontend_version" = "3.0.5"; 43 | "new_key" = "23"; 44 | "another_key/val/t" = "qwerty" 45 | } -type Yaml -------------------------------------------------------------------------------- /nuspecs/MagicChunks.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MagicChunks 5 | 1.0.0 6 | Magic Chunks 7 | Sergey Zwezdin 8 | Sergey Zwezdin 9 | MIT 10 | 11 | https://github.com/magic-chunks/magic-chunks-dotnetcore 12 | logo64.png 13 | https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/master/logo/logo64.png 14 | false 15 | Easy to use tool to config transformations for JSON, XML and YAML. 16 | 17 | Copyright © Sergey Zwezdin 18 | Configuration Transform JSON XML YAML YML web.config app.config appsetings.json 19 | https://github.com/magic-chunks/magic-chunks-dotnetcore/releases/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/MagicChunks/Core/ITransformer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MagicChunks.Core 5 | { 6 | /// 7 | /// Represents transformation object 8 | /// 9 | public interface ITransformer : IDisposable 10 | { 11 | /// 12 | /// List of possible document types 13 | /// 14 | IEnumerable DocumentTypes { get; } 15 | 16 | /// 17 | /// Transforms source document with specified transformations. 18 | /// It trying to detect document type automatically. 19 | /// 20 | /// Source document 21 | /// List of transformations 22 | /// Transformed document 23 | string Transform(string source, TransformationCollection transformations); 24 | 25 | /// 26 | /// Transforms source document with specified transformations 27 | /// 28 | /// Document type 29 | /// Source document 30 | /// List of transformations 31 | /// Transformed document 32 | string Transform(string source, TransformationCollection transformations) 33 | where TDocument: IDocument; 34 | 35 | /// 36 | /// Transforms source document with specified transformations 37 | /// 38 | /// Document handler type 39 | /// Source document 40 | /// List of transformations 41 | /// Transformed document 42 | string Transform(Type documentType, string source, TransformationCollection transformations); 43 | 44 | /// 45 | /// Transforms source document with specified transformations 46 | /// 47 | /// Source document 48 | /// List of transformations 49 | /// Transformed document 50 | string Transform(IDocument source, TransformationCollection transformations); 51 | } 52 | } -------------------------------------------------------------------------------- /src/MagicChunks.Tests/MagicChunks.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 2.0.9 6 | Sergey Zwezdin 7 | 8 | Magic Chunks 9 | Easy to use tool to config transformations for JSON, XML and YAML. 10 | (c) Sergey Zwezdin, 2017 11 | MIT 12 | https://github.com/magic-chunks/magic-chunks-dotnetcore 13 | logo64.png 14 | https://raw.githubusercontent.com/magic-chunks/magic-chunks-dotnetcore/master/logo/logo64.png 15 | https://github.com/magic-chunks/magic-chunks-dotnetcore 16 | Git 17 | Configuration, Transform, JSON, XML, YAML, YML, web.config, app.config, appsetings.json 18 | 19 | false 20 | 21 | 22 | 23 | 24 | True 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/MagicChunks/TransformTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using MagicChunks.Core; 5 | 6 | //https://stackoverflow.com/questions/43165788/merging-net-standard-assemblies 7 | namespace MagicChunks 8 | { 9 | public static class TransformTask 10 | { 11 | public static void Transform(string sourcePath, string targetPath, TransformationCollection transformation) 12 | { 13 | Transform(null, sourcePath, targetPath, transformation); 14 | } 15 | 16 | public static void Transform(string type, string sourcePath, string targetPath, TransformationCollection transformation) 17 | { 18 | if (File.Exists(sourcePath) == false) 19 | throw new ArgumentException($"File \"{sourcePath}\" does not exists.", nameof(sourcePath)); 20 | 21 | string target; 22 | if (String.IsNullOrWhiteSpace(targetPath) == false) 23 | { 24 | try 25 | { 26 | target = Path.GetFullPath(targetPath); 27 | } 28 | catch (ArgumentException ex) 29 | { 30 | throw new ArgumentException($"Path \"{targetPath}\" is incorrect.", nameof(targetPath), ex); 31 | } 32 | catch (IOException ex) 33 | { 34 | throw new ArgumentException($"Path \"{targetPath}\" is incorrect.", nameof(targetPath), ex); 35 | } 36 | } 37 | else 38 | { 39 | target = sourcePath; 40 | } 41 | 42 | var source = File.ReadAllText(sourcePath); 43 | string result; 44 | 45 | ITransformer transformer = new Transformer(); 46 | 47 | if (String.IsNullOrWhiteSpace(type) == false) 48 | { 49 | var document = transformer.DocumentTypes.FirstOrDefault(x => x.Name == type + "Document"); 50 | if (document == null) 51 | throw new ArgumentException($"Wrong document type: {type}", nameof(type)); 52 | 53 | result = transformer.Transform(document, source, transformation); 54 | } 55 | else 56 | { 57 | result = transformer.Transform(source, transformation); 58 | } 59 | 60 | File.WriteAllText(target, result); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /samples/psake/build.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ".\packages\MagicChunks.0.5.9.23\lib\net45\MagicChunks.psm1"; 2 | 3 | Task Build { 4 | Format-MagicChunks -path Web.config -target Web.transformed.1.config -transformations @{ 5 | "configuration/system.web/authentication/@mode" = "Forms"; 6 | "configuration/system.web/httpRuntime" = "125"; 7 | "configuration/appSettings/add[@key='LoadBundledScripts']/@value" = "true"; 8 | "configuration/appSettings/add[@key='SomethingNew']/@value" = "NewValue"; 9 | "configuration/newKey" = "12345" 10 | } 11 | 12 | Format-MagicChunks -path Web.config -target Web.transformed.2.config -transformations @{ 13 | "configuration/system.web/authentication/@mode" = "Forms"; 14 | "configuration/system.web/httpRuntime" = "125"; 15 | "configuration/appSettings/add[@key='LoadBundledScripts']/@value" = "true"; 16 | "configuration/appSettings/add[@key='SomethingNew']/@value" = "NewValue"; 17 | "configuration/newKey" = "12345" 18 | } -type Xml 19 | 20 | Format-MagicChunks -path config.json -target config.transformed.1.json -transformations @{ 21 | "Data/DefaultConnection/ConnectionString" = "mongodb://10.1.25.144/"; 22 | "Data/DefaultConnection/Production" = "true"; 23 | "Smtp/Method" = "Network"; 24 | "NewKey" = "12345" 25 | } 26 | 27 | Format-MagicChunks -path config.json -target config.transformed.2.json -transformations @{ 28 | "Data/DefaultConnection/ConnectionString" = "mongodb://10.1.25.144/"; 29 | "Data/DefaultConnection/Production" = "true"; 30 | "Smtp/Method" = "Network"; 31 | "NewKey" = "12345" 32 | } -type Json 33 | 34 | Format-MagicChunks -path _config.yml -target _config.transformed.1.yml -transformations @{ 35 | "baseUrl" = "http://production.com/"; 36 | "frontend_version" = "3.0.5"; 37 | "new_key" = "23"; 38 | "another_key/val/t" = "qwerty" 39 | } 40 | 41 | Format-MagicChunks -path _config.yml -target _config.transformed.2.yml -transformations @{ 42 | "baseUrl" = "http://production.com/"; 43 | "frontend_version" = "3.0.5"; 44 | "new_key" = "23"; 45 | "another_key/val/t" = "qwerty" 46 | } -type Yaml 47 | } 48 | 49 | Task Default -depends Build; -------------------------------------------------------------------------------- /samples/cake/build.cake: -------------------------------------------------------------------------------- 1 | #addin "MagicChunks" 2 | 3 | var target = Argument("target", "Default"); 4 | 5 | Task("Default") 6 | .Does(() => { 7 | 8 | TransformConfig(@"Web.config", "Web.transformed.1.config", new TransformationCollection { 9 | { "configuration/system.web/authentication/@mode", "Forms" }, 10 | { "configuration/system.web/httpRuntime", "125" }, 11 | { "configuration/appSettings/add[@key='LoadBundledScripts']/@value", "true" }, 12 | { "configuration/appSettings/add[@key='SomethingNew']/@value", "NewValue" }, 13 | { "configuration/newKey", "12345" } 14 | }); 15 | 16 | TransformConfig("Xml", @"Web.config", "Web.transformed.2.config", new TransformationCollection { 17 | { "configuration/system.web/authentication/@mode", "Forms" }, 18 | { "configuration/system.web/httpRuntime", "125" }, 19 | { "configuration/appSettings/add[@key='LoadBundledScripts']/@value", "true" }, 20 | { "configuration/appSettings/add[@key='SomethingNew']/@value", "NewValue" }, 21 | { "configuration/newKey", "12345" } 22 | }); 23 | 24 | TransformConfig(@"config.json", "config.transformed.1.json", new TransformationCollection { 25 | { "Data/DefaultConnection/ConnectionString", "mongodb://10.1.25.144/" }, 26 | { "Data/DefaultConnection/Production", "true" }, 27 | { "Smtp/Method", "Network" }, 28 | { "NewKey", "12345" } 29 | }); 30 | 31 | TransformConfig("Json", @"config.json", "config.transformed.2.json", new TransformationCollection { 32 | { "Data/DefaultConnection/ConnectionString", "mongodb://10.1.25.144/" }, 33 | { "Data/DefaultConnection/Production", "true" }, 34 | { "Smtp/Method", "Network" }, 35 | { "NewKey", "12345" } 36 | }); 37 | 38 | TransformConfig(@"_config.yml", "_config.transformed.1.yml", new TransformationCollection { 39 | { "baseUrl", "http://production.com/" }, 40 | { "frontend_version", "3.0.5" }, 41 | { "new_key", "23" }, 42 | { "another_key/val/t", "qwerty" } 43 | }); 44 | 45 | TransformConfig("Yaml", @"_config.yml", "_config.transformed.2.yml", new TransformationCollection { 46 | { "baseUrl", "http://production.com/" }, 47 | { "frontend_version", "3.0.5" }, 48 | { "new_key", "23" }, 49 | { "another_key/val/t", "qwerty" } 50 | }); 51 | 52 | }); 53 | 54 | RunTarget(target); -------------------------------------------------------------------------------- /src/MagicChunks/MSBuild/TransformConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MagicChunks.Core; 3 | using Microsoft.Build.Framework; 4 | 5 | namespace MagicChunks.MSBuild 6 | { 7 | public class TransformConfig : ITask 8 | { 9 | [Required] 10 | public string Path { get; set; } 11 | 12 | public string Target { get; set; } 13 | 14 | public string Type { get; set; } 15 | 16 | public ITaskItem[] Trasformations { get; set; } 17 | 18 | public IBuildEngine BuildEngine { get; set; } 19 | 20 | public ITaskHost HostObject { get; set; } 21 | 22 | public bool Execute() 23 | { 24 | bool result = true; 25 | 26 | BuildEngine.LogMessageEvent(new BuildMessageEventArgs($"Transforming file: {Path}", string.Empty, 27 | nameof(TransformConfig), MessageImportance.Normal)); 28 | 29 | try 30 | { 31 | TransformationCollection transforms = new TransformationCollection(); 32 | 33 | foreach (var item in Trasformations) 34 | { 35 | if (transforms.ContainsKey(item.ItemSpec) == false) 36 | transforms.Add(item.ItemSpec, item.GetMetadata("Value")); 37 | else 38 | { 39 | BuildEngine.LogWarningEvent(new BuildWarningEventArgs( 40 | null, 41 | null, 42 | null, 43 | 0, 0, 0, 0, 44 | $"Transform key duplicate: {item.ItemSpec}. Skipping.", null, 45 | nameof(TransformConfig), DateTime.Now)); 46 | result = false; 47 | } 48 | } 49 | 50 | TransformTask.Transform(Type, Path, Target ?? Path, transforms); 51 | 52 | BuildEngine.LogMessageEvent(new BuildMessageEventArgs($"File transformed to: {Target ?? Path}", string.Empty, 53 | nameof(TransformConfig), MessageImportance.Normal)); 54 | } 55 | catch (Exception ex) 56 | { 57 | BuildEngine.LogErrorEvent(new BuildErrorEventArgs( 58 | null, 59 | null, 60 | null, 61 | 0, 0, 0, 0, 62 | $"File transformation error: {ex.Message}", null, 63 | nameof(TransformConfig), DateTime.Now)); 64 | 65 | result = false; 66 | } 67 | 68 | return result; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/vss-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "magic-chunks", 4 | "version": "1.0.0", 5 | "name": "Magic Chunks", 6 | "description": "Easy to use tool to config transformations for JSON, XML and YAML.", 7 | "publisher": "sergeyzwezdin", 8 | "public": true, 9 | "icons": { 10 | "default": "images/icon.png", 11 | "large": "images/icon-large.png" 12 | }, 13 | "scopes": [ 14 | "vso.build" 15 | ], 16 | "tags": [ 17 | "Configuration", 18 | "Transform", 19 | "JSON", 20 | "XML", 21 | "YAML", 22 | "YML", 23 | "web.config", 24 | "app.config", 25 | "appsetings.json" 26 | ], 27 | "categories": [ 28 | "Build and release" 29 | ], 30 | "targets": [ 31 | { 32 | "id": "Microsoft.VisualStudio.Services" 33 | } 34 | ], 35 | "files": [ 36 | { 37 | "path": "MagicChunks" 38 | }, 39 | { 40 | "path": "images", 41 | "addressable": true 42 | }, 43 | { 44 | "path": "videos", 45 | "addressable": true 46 | } 47 | ], 48 | "contributions": [ 49 | { 50 | "id": "magic.chunks", 51 | "type": "ms.vss-distributed-task.task", 52 | "description": "Config transform with Magic Chunks", 53 | "targets": [ 54 | "ms.vss-distributed-task.tasks" 55 | ], 56 | "properties": { 57 | "name": "MagicChunks" 58 | } 59 | } 60 | ], 61 | "content": { 62 | "details": { 63 | "path": "overview.md" 64 | } 65 | }, 66 | "screenshots": [ 67 | { 68 | "path": "images/screenshot1.png" 69 | }, 70 | { 71 | "path": "images/screenshot2.png" 72 | } 73 | ], 74 | "links": { 75 | "getstarted": { 76 | "uri": "https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Visual-Studio-Team-System-extension" 77 | }, 78 | "learn": { 79 | "uri": "https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki" 80 | }, 81 | "support": { 82 | "uri": "https://github.com/magic-chunks/magic-chunks-dotnetcore/issues" 83 | }, 84 | "license": { 85 | "uri": "https://github.com/magic-chunks/magic-chunks-dotnetcore/blob/master/LICENSE" 86 | } 87 | }, 88 | "branding": { 89 | "color": "#00552e", 90 | "theme": "dark" 91 | } 92 | } -------------------------------------------------------------------------------- /samples/msbuild/build.msbuild: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Forms 8 | 9 | 10 | 125 11 | 12 | 13 | true 14 | 15 | 16 | NewValue 17 | 18 | 19 | 12345 20 | 21 | 22 | 23 | mongodb://10.1.25.144/ 24 | 25 | 26 | true 27 | 28 | 29 | Network 30 | 31 | 32 | 12345 33 | 34 | 35 | 36 | http://production.com/ 37 | 38 | 39 | 3.0.5 40 | 41 | 42 | 23 43 | 44 | 45 | qwerty 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/MagicChunks/transform.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(DefaultParameterSetName = 'None')] 2 | param( 3 | [String] [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] 4 | $sourcePath, 5 | 6 | [String] [Parameter(Mandatory = $true)] 7 | $sourcePathRecurse, 8 | 9 | [String] [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] 10 | $fileType, 11 | 12 | [String] [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] 13 | $targetPathType, 14 | 15 | [String] [Parameter(Mandatory = $false)] 16 | $targetPath, 17 | 18 | [String] [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] 19 | $transformationType, 20 | 21 | [String] [Parameter(Mandatory = $false)] 22 | $transformations, 23 | 24 | [String] [Parameter(Mandatory = $false)] 25 | $transformationsFile 26 | ) 27 | 28 | # Init Magic Chunks 29 | 30 | Add-Type -Path "$PSScriptRoot\MagicChunks.dll" 31 | 32 | if ($transformationType -eq "json" -And [String]::IsNullOrEmpty($transformations)) { 33 | Write-Error -Message "Inline transformations must be specified if Inline JSON transformation type is enabled!" 34 | Exit 35 | } 36 | 37 | if ($transformationType -eq "file" -And [String]::IsNullOrEmpty($transformationsFile)) { 38 | Write-Error -Message "Transformation file path must be specified if JSON File transformation type is enabled!" 39 | Exit 40 | } 41 | 42 | 43 | # Parse transformations 44 | 45 | try { 46 | $transforms = New-Object -TypeName MagicChunks.Core.TransformationCollection 47 | 48 | if ($transformationType -eq "file") { 49 | if (Test-Path $transformationsFile) { 50 | $transformations = Get-Content $transformationsFile 51 | } 52 | else { 53 | Write-Error -Message "Could not find transformation file at $transformationsFile" 54 | Exit 55 | } 56 | } 57 | 58 | foreach($t in ($transformations.Replace("\", "\\") | ConvertFrom-Json).psobject.properties) { 59 | Write-Host "Transformation found: $($t.name): $($t.value)" 60 | $transforms.Add($t.name, $t.value) 61 | } 62 | } 63 | catch { 64 | Write-Error -Message "Transforms parsing error: $($_.Exception.Message)" -Exception $_.Exception 65 | throw; 66 | } 67 | 68 | 69 | # Find files to transform 70 | 71 | if ([System.Convert]::ToBoolean($sourcePathRecurse)) { 72 | $files = Get-ChildItem $sourcePath -Recurse 73 | } 74 | else { 75 | $files = Get-ChildItem $sourcePath 76 | } 77 | 78 | 79 | # Transform files 80 | 81 | foreach ($file in $files) { 82 | Write-Host "Transforming file $($file)" 83 | 84 | try { 85 | if ($targetPathType -eq "source") { 86 | $target = $file; 87 | } 88 | elseif ($targetPathType -eq "specific") { 89 | $target = $targetPath; 90 | } 91 | 92 | [MagicChunks.TransformTask]::Transform(($fileType, $null)[[string]::IsNullOrWhitespace($fileType) -or ($fileType -eq "Auto")], $file, $target, $transforms) 93 | 94 | Write-Host "File $($file) transformed into $($target)" 95 | } 96 | catch { 97 | Write-Error -Message "File $($file) transformation error: $($_.Exception.Message)" -Exception $_.Exception 98 | } 99 | } -------------------------------------------------------------------------------- /src/MagicChunks.Tests/Documents/YamlDocumentTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MagicChunks.Documents; 3 | using Xunit; 4 | 5 | namespace MagicChunks.Tests.Documents 6 | { 7 | public class YamlDocumentTests 8 | { 9 | [Fact] 10 | public void Transform() 11 | { 12 | // Arrange 13 | 14 | var document = new YamlDocument(@"a: 15 | x: 1 16 | b: 2 17 | c: 3"); 18 | 19 | 20 | // Act 21 | 22 | document.ReplaceKey(new[] { "A", "y" }, "2"); 23 | document.ReplaceKey(new[] { "a", "z", "t", "w" }, "3"); 24 | document.ReplaceKey(new[] { "b" }, "5"); 25 | document.ReplaceKey(new[] { "c", "a" }, "1"); 26 | document.ReplaceKey(new[] { "c", "b" }, "2"); 27 | document.ReplaceKey(new[] { "c", "b", "t" }, "3"); 28 | document.ReplaceKey(new[] { "D" }, "4"); 29 | 30 | var result = document.ToString(); 31 | 32 | 33 | // Assert 34 | 35 | Assert.Equal(@"a: 36 | x: 1 37 | y: 2 38 | z: 39 | t: 40 | w: 3 41 | b: 5 42 | c: 43 | a: 1 44 | b: 45 | t: 3 46 | d: 4 47 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 48 | } 49 | 50 | [Fact] 51 | public void AddStringToArray() 52 | { 53 | // Arrange 54 | 55 | var document = new YamlDocument(@"a: 56 | x: 1 57 | b: 2 58 | c: 3"); 59 | 60 | 61 | // Act 62 | 63 | document.AddElementToArray(new[] { "d" }, "1"); 64 | document.AddElementToArray(new[] { "d" }, "2"); 65 | 66 | var result = document.ToString(); 67 | 68 | 69 | // Assert 70 | 71 | Assert.Equal(@"a: 72 | x: 1 73 | b: 2 74 | c: 3 75 | d: 76 | - 1 77 | - 2 78 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 79 | } 80 | 81 | [Fact] 82 | public void Remove() 83 | { 84 | // Arrange 85 | 86 | var document = new YamlDocument( 87 | @"a: 88 | x: 1 89 | b: 90 | x: 1 91 | c: 3"); 92 | 93 | 94 | // Act 95 | 96 | document.RemoveKey(new[] { "A"}); 97 | document.RemoveKey(new[] { "b", "X"}); 98 | 99 | var result = document.ToString(); 100 | 101 | 102 | // Assert 103 | 104 | Assert.Equal( 105 | @"b: {} 106 | c: 3 107 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 108 | } 109 | 110 | [Fact] 111 | public void ValidateEmptyPath() 112 | { 113 | // Assert 114 | YamlDocument document = new YamlDocument(""); 115 | 116 | // Act 117 | ArgumentException result = Assert.Throws(() => document.ReplaceKey(new[] { "a", "", "b" }, "")); 118 | 119 | // Arrange 120 | Assert.True(result.Message?.StartsWith("There is empty items in the path.")); 121 | } 122 | 123 | [Fact] 124 | public void ValidateWithespacePath() 125 | { 126 | // Assert 127 | YamlDocument document = new YamlDocument(""); 128 | 129 | // Act 130 | ArgumentException result = Assert.Throws(() => document.ReplaceKey(new[] { "a", " ", "b" }, "")); 131 | 132 | // Arrange 133 | Assert.True(result.Message?.StartsWith("There is empty items in the path.")); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /samples/netapp/TransformSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {C1BF6ECD-1451-46E3-9CC4-1B9F0CB577A9} 8 | Exe 9 | Properties 10 | TransformSample 11 | TransformSample 12 | v4.6.1 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | packages\MagicChunks.0.5.9.23\lib\net45\MagicChunks.dll 38 | True 39 | 40 | 41 | packages\MagicChunks.0.5.9.23\lib\net45\MagicChunks.Cake.dll 42 | True 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | PreserveNewest 61 | 62 | 63 | 64 | PreserveNewest 65 | 66 | 67 | PreserveNewest 68 | 69 | 70 | 71 | 78 | -------------------------------------------------------------------------------- /src/MagicChunks/VSTS/MagicChunks/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "985284E0-A7D2-4E4D-802C-0A516BFFAADF", 3 | "name": "MagicChunks", 4 | "friendlyName": "Config transformation", 5 | "description": "Transform config file with Magic Chunks", 6 | "helpMarkDown": "[More Information](https://github.com/magic-chunks/magic-chunks-dotnetcore)", 7 | "category": "Utility", 8 | "visibility": [ 9 | "Build", 10 | "Release" 11 | ], 12 | "author": "Sergey Zwezdin", 13 | "version": { 14 | "Major": 1, 15 | "Minor": 0, 16 | "Patch": 0 17 | }, 18 | "demands": [ 19 | "DotNetFramework" 20 | ], 21 | "minimumAgentVersion": "1.95.1", 22 | "groups": [ 23 | { 24 | "name": "sourcePath", 25 | "displayName": "Source", 26 | "isExpanded": true 27 | }, 28 | { 29 | "name": "targetPath", 30 | "displayName": "Target", 31 | "isExpanded": false 32 | }, 33 | { 34 | "name": "transformation", 35 | "displayName": "Transformation", 36 | "isExpanded": true 37 | } 38 | ], 39 | "inputs": [ 40 | { 41 | "name": "sourcePath", 42 | "type": "filePath", 43 | "label": "Path", 44 | "defaultValue": "", 45 | "required": true, 46 | "helpMarkDown": "Configuration file path which should be transformed.", 47 | "groupName": "sourcePath" 48 | }, 49 | { 50 | "name": "sourcePathRecurse", 51 | "type": "boolean", 52 | "label": "Recursive search", 53 | "defaultValue": "false", 54 | "required": false, 55 | "groupName": "sourcePath", 56 | "helpMarkDown": "Trying to serarch specified file in folder and sub-folders" 57 | }, 58 | { 59 | "name": "fileType", 60 | "type": "pickList", 61 | "label": "File type", 62 | "defaultValue": "Auto", 63 | "required": true, 64 | "helpMarkDown": "Type of the config file: JSON, XML or YAML.", 65 | "options": { 66 | "Auto": "Auto detect", 67 | "Json": "JSON", 68 | "Xml": "XML", 69 | "Yaml": "YAML" 70 | }, 71 | "groupName": "sourcePath" 72 | }, 73 | { 74 | "name": "targetPathType", 75 | "type": "pickList", 76 | "label": "Target", 77 | "defaultValue": "source", 78 | "required": true, 79 | "helpMarkDown": "Place where to put transformation result.", 80 | "options": { 81 | "source": "Source file", 82 | "specific": "Specific path" 83 | }, 84 | "groupName": "targetPath" 85 | }, 86 | { 87 | "name": "targetPath", 88 | "type": "filePath", 89 | "label": "Target path", 90 | "defaultValue": "", 91 | "required": false, 92 | "helpMarkDown": "File path where to put transformation result.", 93 | "groupName": "targetPath", 94 | "visibleRule": "targetPathType = specific" 95 | }, 96 | { 97 | "name": "transformationType", 98 | "type": "pickList", 99 | "label": "Type", 100 | "defaultValue": "json", 101 | "required": true, 102 | "helpMarkDown": "The you will use to define transformations.", 103 | "options": { 104 | "json": "Inline JSON", 105 | "file": "JSON File" 106 | }, 107 | "groupName": "transformation" 108 | }, 109 | { 110 | "name": "transformations", 111 | "type": "multiLine", 112 | "label": "Transformations", 113 | "defaultValue": "{\n \"key1\": \"value1\",\n \"key2\": \"value2\"\n}", 114 | "required": true, 115 | "helpMarkDown": "JSON representation of transformation.", 116 | "properties": { 117 | "resizable": "true", 118 | "rows": "10", 119 | "maxLength": "2500" 120 | }, 121 | "groupName": "transformation", 122 | "visibleRule": "transformationType = json" 123 | }, 124 | { 125 | "name": "transformationsFile", 126 | "type": "filePath", 127 | "label": "Transform File", 128 | "defaultValue": "", 129 | "required": true, 130 | "helpMarkDown": "JSON file containing the configuration transformations to apply.", 131 | "groupName": "transformation", 132 | "visibleRule": "transformationType = file" 133 | } 134 | ], 135 | "instanceNameFormat": "Config transform - $(sourcePath)", 136 | "execution": { 137 | "PowerShell": { 138 | "target": "$(currentDirectory)\\transform.ps1", 139 | "argumentFormat": "", 140 | "workingDirectory": "$(currentDirectory)" 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/MagicChunks/Core/Transformer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.RegularExpressions; 6 | using MagicChunks.Helpers; 7 | 8 | namespace MagicChunks.Core 9 | { 10 | public class Transformer : ITransformer 11 | { 12 | private static readonly Regex RemoveEndingRegex = new Regex(@"\`\d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant); 13 | 14 | private static readonly Regex RemoveArrayEndingRegex = new Regex(@"\[\]$", RegexOptions.Compiled | RegexOptions.CultureInvariant); 15 | 16 | private static readonly IEnumerable _documentTypes; 17 | 18 | static Transformer() 19 | { 20 | _documentTypes = typeof(Transformer) 21 | .GetTypeInfo() 22 | .Assembly 23 | .GetAllTypes(p => !p.IsAbstract && p.ImplementedInterfaces.Contains(typeof(IDocument))); 24 | } 25 | 26 | public IEnumerable DocumentTypes => _documentTypes; 27 | 28 | public string Transform(string source, TransformationCollection transformations) 29 | { 30 | foreach (var documentType in DocumentTypes) 31 | { 32 | IDocument document; 33 | try 34 | { 35 | document = (IDocument)Activator.CreateInstance(documentType, source); 36 | } 37 | catch (TargetInvocationException ex) 38 | { 39 | if (ex.InnerException.GetType() == typeof(ArgumentException)) 40 | continue; 41 | 42 | throw; 43 | } 44 | 45 | return Transform(document, transformations); 46 | } 47 | 48 | throw new ArgumentException("Unknown document type.", nameof(source)); 49 | } 50 | 51 | public string Transform(string source, TransformationCollection transformations) where TDocument : IDocument 52 | { 53 | var document = (TDocument)Activator.CreateInstance(typeof(TDocument), source); 54 | return Transform(document, transformations); 55 | } 56 | 57 | public string Transform(Type documentType, string source, TransformationCollection transformations) 58 | { 59 | IDocument document; 60 | try 61 | { 62 | document = (IDocument)Activator.CreateInstance(documentType, source); 63 | } 64 | catch (TargetInvocationException ex) 65 | { 66 | if (ex.InnerException.GetType() == typeof(ArgumentException)) 67 | throw new ArgumentException("Unknown document type.", nameof(source), ex); 68 | 69 | throw; 70 | } 71 | 72 | return Transform(document, transformations); 73 | } 74 | 75 | public string Transform(IDocument source, TransformationCollection transformations) 76 | { 77 | if (transformations.KeysToRemove != null) 78 | { 79 | foreach (var key in transformations.KeysToRemove) 80 | { 81 | if (String.IsNullOrWhiteSpace(key)) 82 | throw new ArgumentNullException("Transformation key is empty.", nameof(transformations)); 83 | 84 | source.RemoveKey(SplitKey(key)); 85 | } 86 | } 87 | 88 | foreach (var transformation in transformations) 89 | { 90 | if (String.IsNullOrWhiteSpace(transformation.Key)) 91 | throw new ArgumentException("Transformation key is empty.", nameof(transformations)); 92 | 93 | if (transformation.Key.StartsWith("#")) 94 | { 95 | source.RemoveKey(SplitKey(transformation.Key.TrimStart('#'))); 96 | } 97 | else if (RemoveEndingRegex.Replace(transformation.Key, String.Empty).EndsWith("[]")) 98 | { 99 | source.AddElementToArray(SplitKey(transformation.Key), transformation.Value); 100 | } 101 | else 102 | { 103 | source.ReplaceKey(SplitKey(transformation.Key), transformation.Value); 104 | } 105 | } 106 | 107 | return source.ToString(); 108 | } 109 | 110 | private static string[] SplitKey(string key) 111 | { 112 | return RemoveArrayEndingRegex.Replace(RemoveEndingRegex.Replace(key.Trim(), String.Empty), String.Empty).Split('/').Select(x => x.Trim()).ToArray(); 113 | } 114 | 115 | public void Dispose() 116 | { 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /samples/cake/build.ps1: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the Cake bootstrapper script for PowerShell. 3 | # This file was downloaded from https://github.com/cake-build/resources 4 | # Feel free to change this file to fit your needs. 5 | ########################################################################## 6 | 7 | <# 8 | 9 | .SYNOPSIS 10 | This is a Powershell script to bootstrap a Cake build. 11 | 12 | .DESCRIPTION 13 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 14 | and execute your Cake build script with the parameters you provide. 15 | 16 | .PARAMETER Script 17 | The build script to execute. 18 | .PARAMETER Target 19 | The build script target to run. 20 | .PARAMETER Configuration 21 | The build configuration to use. 22 | .PARAMETER Verbosity 23 | Specifies the amount of information to be displayed. 24 | .PARAMETER Experimental 25 | Tells Cake to use the latest Roslyn release. 26 | .PARAMETER WhatIf 27 | Performs a dry run of the build script. 28 | No tasks will be executed. 29 | .PARAMETER Mono 30 | Tells Cake to use the Mono scripting engine. 31 | .PARAMETER SkipToolPackageRestore 32 | Skips restoring of packages. 33 | .PARAMETER ScriptArgs 34 | Remaining arguments are added here. 35 | 36 | .LINK 37 | http://cakebuild.net 38 | 39 | #> 40 | 41 | [CmdletBinding()] 42 | Param( 43 | [string]$Script = "build.cake", 44 | [string]$Target = "Default", 45 | [string]$Configuration = "Release", 46 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 47 | [string]$Verbosity = "Verbose", 48 | [switch]$Experimental, 49 | [Alias("DryRun","Noop")] 50 | [switch]$WhatIf, 51 | [switch]$Mono, 52 | [switch]$SkipToolPackageRestore, 53 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 54 | [string[]]$ScriptArgs, 55 | [string]$Version = "0.0.0", 56 | [string]$Build = "0" 57 | ) 58 | 59 | Write-Host "Preparing to run build script..." 60 | 61 | $PSScriptRoot = split-path -parent $MyInvocation.MyCommand.Definition; 62 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 63 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 64 | $NUGET_URL = "http://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 65 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 66 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 67 | 68 | # Should we use mono? 69 | $UseMono = ""; 70 | if($Mono.IsPresent) { 71 | Write-Verbose -Message "Using the Mono based scripting engine." 72 | $UseMono = "-mono" 73 | } 74 | 75 | # Should we use the new Roslyn? 76 | $UseExperimental = ""; 77 | if($Experimental.IsPresent -and !($Mono.IsPresent)) { 78 | Write-Verbose -Message "Using experimental version of Roslyn." 79 | $UseExperimental = "-experimental" 80 | } 81 | 82 | # Is this a dry run? 83 | $UseDryRun = ""; 84 | if($WhatIf.IsPresent) { 85 | $UseDryRun = "-dryrun" 86 | } 87 | 88 | # Make sure tools folder exists 89 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 90 | Write-Verbose -Message "Creating tools directory..." 91 | New-Item -Path $TOOLS_DIR -Type directory | out-null 92 | } 93 | 94 | # Make sure that packages.config exist. 95 | if (!(Test-Path $PACKAGES_CONFIG)) { 96 | Write-Verbose -Message "Downloading packages.config..." 97 | try { Invoke-WebRequest -Uri http://cakebuild.net/download/bootstrapper/packages -OutFile $PACKAGES_CONFIG } catch { 98 | Throw "Could not download packages.config." 99 | } 100 | } 101 | 102 | # Try find NuGet.exe in path if not exists 103 | if (!(Test-Path $NUGET_EXE)) { 104 | Write-Verbose -Message "Trying to find nuget.exe in PATH..." 105 | $existingPaths = $Env:Path -Split ';' | Where-Object { Test-Path $_ } 106 | $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 107 | if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { 108 | Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." 109 | $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName 110 | } 111 | } 112 | 113 | # Try download NuGet.exe if not exists 114 | if (!(Test-Path $NUGET_EXE)) { 115 | Write-Verbose -Message "Downloading NuGet.exe..." 116 | try { 117 | (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) 118 | } catch { 119 | Throw "Could not download NuGet.exe." 120 | } 121 | } 122 | 123 | # Save nuget.exe path to environment to be available to child processed 124 | $ENV:NUGET_EXE = $NUGET_EXE 125 | 126 | # Restore tools from NuGet? 127 | if(-Not $SkipToolPackageRestore.IsPresent) { 128 | Push-Location 129 | Set-Location $TOOLS_DIR 130 | Write-Verbose -Message "Restoring tools from NuGet..." 131 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" 132 | if ($LASTEXITCODE -ne 0) { 133 | Throw "An error occured while restoring NuGet tools." 134 | } 135 | Write-Verbose -Message ($NuGetOutput | out-string) 136 | Pop-Location 137 | } 138 | 139 | # Make sure that Cake has been installed. 140 | if (!(Test-Path $CAKE_EXE)) { 141 | Throw "Could not find Cake.exe at $CAKE_EXE" 142 | } 143 | 144 | # Start Cake 145 | Write-Host "Running build script..." 146 | Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" 147 | exit $LASTEXITCODE -------------------------------------------------------------------------------- /src/MagicChunks/Documents/YamlDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using MagicChunks.Core; 6 | using YamlDotNet.Core; 7 | using YamlDotNet.Serialization; 8 | using YamlDotNet.Serialization.NamingConventions; 9 | 10 | namespace MagicChunks.Documents 11 | { 12 | public class YamlDocument : IDocument 13 | { 14 | protected readonly Dictionary Document; 15 | 16 | public YamlDocument(string source) 17 | { 18 | var deserializer = new DeserializerBuilder() 19 | .WithNamingConvention(new CamelCaseNamingConvention()) 20 | .WithObjectFactory(new CustomObjectFactory()) 21 | .Build(); 22 | 23 | using (var reader = new StringReader(source)) 24 | { 25 | try 26 | { 27 | Document = (Dictionary)deserializer.Deserialize(reader); 28 | } 29 | catch (YamlException ex) 30 | { 31 | throw new ArgumentException("Wrong document format", nameof(source), ex); 32 | } 33 | } 34 | } 35 | 36 | public void AddElementToArray(string[] path, string value) 37 | { 38 | if ((path == null) || (path.Any() == false)) 39 | throw new ArgumentException("Path is not specified.", nameof(path)); 40 | 41 | if (path.Any(String.IsNullOrWhiteSpace)) 42 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 43 | 44 | Dictionary current = Document; 45 | 46 | if (current == null) 47 | throw new ArgumentException("Root element is not present.", nameof(path)); 48 | 49 | current = FindPath(path.Take(path.Length - 1), current); 50 | 51 | UpdateTargetArrayElement(current, path.Last(), value); 52 | } 53 | 54 | public void ReplaceKey(string[] path, string value) 55 | { 56 | if ((path == null) || (path.Any() == false)) 57 | throw new ArgumentException("Path is not specified", nameof(path)); 58 | 59 | if (path.Any(String.IsNullOrWhiteSpace)) 60 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 61 | 62 | Dictionary current = Document; 63 | 64 | if (current == null) 65 | throw new ArgumentException("Root element is not present.", nameof(path)); 66 | 67 | current = FindPath(path.Take(path.Length - 1), current); 68 | 69 | UpdateTargetElement(current, path.Last(), value); 70 | } 71 | 72 | public void RemoveKey(string[] path) 73 | { 74 | if ((path == null) || (path.Any() == false)) 75 | throw new ArgumentException("Path is not specified.", nameof(path)); 76 | 77 | if (path.Any(String.IsNullOrWhiteSpace)) 78 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 79 | 80 | Dictionary current = Document; 81 | 82 | if (current == null) 83 | throw new ArgumentException("Root element is not present.", nameof(path)); 84 | 85 | current = FindPath(path.Take(path.Length - 1), current); 86 | current.Remove(path.Last()); 87 | } 88 | 89 | private static Dictionary FindPath(IEnumerable path, Dictionary current) 90 | { 91 | foreach (string pathElement in path) 92 | { 93 | object pathElementValue; 94 | if (current.TryGetValue(pathElement, out pathElementValue) && (pathElementValue is Dictionary)) 95 | { 96 | current = (Dictionary)pathElementValue; 97 | } 98 | else 99 | { 100 | var newElement = new Dictionary(); 101 | current[pathElement] = newElement; 102 | current = newElement; 103 | } 104 | } 105 | return current; 106 | } 107 | 108 | private static void UpdateTargetElement(Dictionary current, string targetElementName, string value) 109 | { 110 | current[targetElementName] = value; 111 | } 112 | 113 | private static void UpdateTargetArrayElement(Dictionary current, string targetElementName, string value) 114 | { 115 | 116 | if (current.ContainsKey(targetElementName) && (current[targetElementName] is List)) 117 | { 118 | ((List)current[targetElementName]).Add(value); 119 | } 120 | else if (!current.ContainsKey(targetElementName)) 121 | { 122 | current[targetElementName] = new List(); 123 | ((List)current[targetElementName]).Add(value); 124 | } 125 | else 126 | { 127 | throw new FormatException("Target element is not array."); 128 | } 129 | } 130 | 131 | public override string ToString() 132 | { 133 | using (var writer = new StringWriter()) 134 | { 135 | var serializer = new Serializer(); 136 | serializer.Serialize(writer, Document); 137 | 138 | return writer.ToString(); 139 | } 140 | } 141 | 142 | public void Dispose() 143 | { 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![AppVeyor](https://img.shields.io/appveyor/ci/magic-chunks/magic-chunks-dotnetcore/master.svg?maxAge=2592000)](https://ci.appveyor.com/project/magic-chunks/magic-chunks-dotnetcore) [![Github Releases](https://img.shields.io/github/downloads/magic-chunks/magic-chunks-dotnetcore/total.svg?maxAge=2592000)](https://github.com/magic-chunks/magic-chunks-dotnetcore/releases) [![NuGet](https://img.shields.io/nuget/v/MagicChunks.svg?maxAge=2592000)](https://www.nuget.org/packages/MagicChunks/) [![Visual Studio Marketplace](https://img.shields.io/badge/VS%20Marketplace-1.3.1-yellowgreen.svg)](https://marketplace.visualstudio.com/items?itemName=sergeyzwezdin.magic-chunks) 2 | 3 | ![Magic Chunks](assets/title.png) 4 | 5 | Easy to use tool to config transformations for JSON, XML and YAML. 6 | 7 | --- 8 | 9 | Everyone remember [XML Document Transform](https://msdn.microsoft.com/en-us/library/dd465326.aspx) syntax to transform configuration files during the build process. But world is changing and now you can have different config types in your .NET projects. 10 | 11 | **Magic Chunks** allows you to transform you JSON, XML and YAML files. You can run it at [MSBuild](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/MSBuild), [Cake](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Cake), [PSake](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/PSake) or [Powershell](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Powershell) script as well as use [Visual Studio Team Services build extension](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Visual-Studio-Team-System-extension). Also, it's possible to reference Magic Chunks from your .NET projects in more complicated cases. 12 | 13 | # How it works 14 | 15 | The main idea is quite simple. Magic Chunks represents transformation as a key-value collection. 16 | 17 | The key contains path in the source file which should be modified, and the value contains data for this path in modified file. 18 | 19 | If you are using Magic Chunks from .NET or Cake, you can also pass in any keys to be removed into the constructor. 20 | 21 | #### XML 22 | 23 | Imagine you have following XML based configuration file. 24 | 25 | ```xml 26 | 27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | 34 | So following transformations could be applied to this config: 35 | 36 | ```json 37 | { 38 | "configuration/system.web/compilation/@debug": "false", 39 | "configuration/system.web/authentication/@mode": "Forms" 40 | } 41 | ``` 42 | 43 | As a result you will have config like this: 44 | 45 | ```xml 46 | 47 | 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | #### JSON 55 | 56 | The same approach works if you have JSON based configuration: 57 | 58 | ```json 59 | { 60 | "ConnectionStrings": { 61 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=webapp" 62 | } 63 | } 64 | ``` 65 | 66 | Transformation for the config could be: 67 | 68 | ```json 69 | { 70 | "ConnectionStrings/DefaultConnection": "Data Source=10.0.0.5;Initial Catalog=Db1;Persist Security Info=True" 71 | } 72 | ``` 73 | 74 | After transformation you will have: 75 | 76 | ```json 77 | { 78 | "ConnectionStrings": { 79 | "DefaultConnection": "Data Source=10.0.0.5;Initial Catalog=Db1;Persist Security Info=True" 80 | } 81 | } 82 | ``` 83 | 84 | # Supported formats 85 | 86 | Magic Chunks supports following file formats: 87 | 88 | 1. [XML](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/XML) 89 | 2. [JSON](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/JSON) 90 | 3. [YAML](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/YAML) 91 | 92 | # Getting started 93 | 94 | Let's say you have `appsettings.json` file at `C:\sources\project1` folder: 95 | 96 | ```json 97 | { 98 | "ConnectionStrings": { 99 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=webapp" 100 | } 101 | } 102 | ``` 103 | 104 | To use Magic Chunks download [latest release](https://github.com/magic-chunks/magic-chunks-dotnetcore/releases) manually or get it directly from [Nuget](http://nuget.org). 105 | 106 | For example we will use Powershell to transform configuration file. To do this you have to write something like this: 107 | 108 | ```powershell 109 | Import-Module .\MagicChunks.psm1 110 | 111 | Format-MagicChunks -path C:\sources\project1\appsettings.json -transformations @{ 112 | "ConnectionStrings/DefaultConnection" = "Data Source=10.0.0.5;Initial Catalog=Db1;Persist Security Info=True" 113 | } 114 | ``` 115 | 116 | To transform config files you can use any approach you like: 117 | 118 | - [MSBuild](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/MSBuild) 119 | - [Cake](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Cake) 120 | - [Powershell](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Powershell) 121 | - [PSake](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/PSake) 122 | - [VSTS extension](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Visual-Studio-Team-System-extension) 123 | 124 | To learn more check [wiki page](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki). 125 | 126 | # Maintainers 127 | 128 | - Sergey Zwezdin ([@sergeyzwezdin](https://github.com/sergeyzwezdin)) 129 | - Gary Ewan Park ([@gep13](https://github.com/gep13)) 130 | - Pascal Berger ([@pascalberger](https://github.com/pascalberger)) 131 | 132 | # Contributions 133 | 134 | Any contributions are welcome. Most probably someone will want to extend it with additional formats. So feel free to make pull requests for your changes. Read [contribution guidelines](https://github.com/magic-chunks/magic-chunks-dotnetcore/wiki/Contribution-guidelines) to start. 135 | 136 | # License 137 | 138 | Magic Chunks is released under the [MIT License](https://github.com/magic-chunks/magic-chunks-dotnetcore/blob/master/LICENSE). 139 | -------------------------------------------------------------------------------- /samples/cake/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /samples/msbuild/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /samples/netapp/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /samples/psake/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /samples/powershell/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src/MagicChunks/Documents/JsonDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using MagicChunks.Core; 6 | using MagicChunks.Helpers; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace MagicChunks.Documents 11 | { 12 | public class JsonDocument : IDocument 13 | { 14 | private static readonly Regex JsonObjectRegex = new Regex(@"^{.+}$$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 15 | private static readonly Regex NodeIndexEndingRegex = new Regex(@"\[\d+\]$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 16 | private static readonly Regex NodeValueEndingRegex = new Regex(@"\[\@.+\=.+\]$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 17 | protected readonly JObject Document; 18 | 19 | public JsonDocument(string source) 20 | { 21 | try 22 | { 23 | Document = (JObject)JsonConvert.DeserializeObject(source); 24 | } 25 | catch (JsonReaderException ex) 26 | { 27 | throw new ArgumentException("Wrong document format", nameof(source), ex); 28 | } 29 | } 30 | 31 | public void AddElementToArray(string[] path, string value) 32 | { 33 | if ((path == null) || (path.Any() == false)) 34 | throw new ArgumentException("Path is not specified.", nameof(path)); 35 | 36 | if (path.Any(String.IsNullOrWhiteSpace)) 37 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 38 | 39 | if (Document.Root == null) 40 | throw new ArgumentException("Root element is not present.", nameof(path)); 41 | 42 | var targets = FindPath(path.Take(path.Length - 1), (JObject)Document.Root); 43 | var pathEnding = path.Last(); 44 | 45 | foreach (var target in targets) 46 | { 47 | UpdateTargetArrayElement(target, pathEnding, value); 48 | } 49 | } 50 | 51 | public void ReplaceKey(string[] path, string value) 52 | { 53 | if ((path == null) || (path.Any() == false)) 54 | throw new ArgumentException("Path is not specified.", nameof(path)); 55 | 56 | if (path.Any(String.IsNullOrWhiteSpace)) 57 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 58 | 59 | if (Document.Root == null) 60 | throw new ArgumentException("Root element is not present.", nameof(path)); 61 | 62 | var targets = FindPath(path.Take(path.Length - 1), (JObject)Document.Root); 63 | var pathEnding = path.Last(); 64 | 65 | foreach (var target in targets) 66 | { 67 | UpdateTargetElement(target, pathEnding, value); 68 | } 69 | } 70 | 71 | public void RemoveKey(string[] path) 72 | { 73 | if ((path == null) || (path.Any() == false)) 74 | throw new ArgumentException("Path is not specified.", nameof(path)); 75 | 76 | if (path.Any(String.IsNullOrWhiteSpace)) 77 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 78 | 79 | if (Document.Root == null) 80 | throw new ArgumentException("Root element is not present.", nameof(path)); 81 | 82 | var targets = FindPath(path.Take(path.Length - 1), (JObject)Document.Root); 83 | var pathEnding = path.Last(); 84 | 85 | if (NodeIndexEndingRegex.IsMatch(pathEnding) || NodeValueEndingRegex.IsMatch(pathEnding)) 86 | { 87 | // Remove item from array 88 | foreach (var target in targets) 89 | { 90 | foreach (var item in target.GetChildPropertyValue(pathEnding)) 91 | { 92 | item.Remove(); 93 | } 94 | } 95 | } 96 | else 97 | { 98 | // Remove property 99 | foreach (var target in targets) 100 | { 101 | target.Remove(pathEnding); 102 | } 103 | } 104 | } 105 | 106 | private static IEnumerable FindPath(IEnumerable path, JObject current) 107 | { 108 | var pathElements = new Queue(path); 109 | 110 | foreach (string pathElement in path) 111 | { 112 | pathElements.Dequeue(); 113 | 114 | var element = current.GetChildPropertyValue(pathElement).FirstOrDefault(); 115 | if (element is JObject) 116 | { 117 | current = (JObject)element; 118 | } 119 | else if (element is JArray) 120 | { 121 | return ((JArray)element).SelectMany(arrayElement => FindPath(pathElements.ToArray(), arrayElement as JObject)).ToArray(); 122 | } 123 | else 124 | { 125 | current[pathElement] = new JObject(); 126 | current = (JObject)current[pathElement]; 127 | } 128 | } 129 | return new JObject[] { current }; 130 | } 131 | 132 | private static void UpdateTargetArrayElement(JObject current, string targetElementName, string value) 133 | { 134 | var targetElement = current.GetChildProperty(targetElementName) as JProperty; 135 | if ((targetElement != null) && (targetElement is JProperty) && (targetElement.Value is JArray)) 136 | { 137 | if (JsonObjectRegex.IsMatch(value.Trim())) 138 | { 139 | ((JArray)targetElement.Value).Add(JsonConvert.DeserializeObject(value)); 140 | } 141 | else 142 | { 143 | ((JArray)targetElement.Value).Add(value); 144 | } 145 | } 146 | else if (targetElement != null) 147 | throw new FormatException("Target element is not array."); 148 | else 149 | { 150 | var array = new JArray(); 151 | if (JsonObjectRegex.IsMatch(value.Trim())) 152 | { 153 | array.Add(JsonConvert.DeserializeObject(value)); 154 | } 155 | else 156 | { 157 | array.Add(value); 158 | } 159 | current.Add(targetElementName, array); 160 | } 161 | } 162 | 163 | private static void UpdateTargetElement(JObject current, string targetElementName, string value) 164 | { 165 | var targetElement = current.GetChildProperty(targetElementName); 166 | if (targetElement is JProperty) 167 | ((JProperty)targetElement).Value = value; 168 | else if (targetElement is JObject) 169 | { 170 | var targetValue = JsonConvert.DeserializeObject(value) as JObject; 171 | if (targetValue != null) 172 | ((JObject)targetElement).Replace(targetValue); 173 | else 174 | throw new ArgumentException("Value is not valid JSON object.", nameof(value)); 175 | } 176 | else 177 | current.Add(targetElementName, value); 178 | } 179 | 180 | public override string ToString() 181 | { 182 | return Document?.ToString() ?? String.Empty; 183 | } 184 | 185 | public void Dispose() 186 | { 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /src/MagicChunks/Helpers/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace MagicChunks.Helpers 8 | { 9 | public static class JsonExtensions 10 | { 11 | private static readonly Regex NodeIndexEndingRegex = new Regex(@"\[\d+\]$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 12 | private static readonly Regex NodeArrayEndingRegex = new Regex(@"\[\]$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 13 | private static readonly Regex NodeValueEndingRegex = new Regex(@"\[\@.+\=.+\]$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 14 | private static readonly Regex NodeValueRegex = new Regex(@"^\@(.+)\=(.+)$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 15 | 16 | public static JToken GetChildProperty(this JContainer source, string name) 17 | { 18 | if (NodeIndexEndingRegex.IsMatch(name)) 19 | { 20 | string nodeName; 21 | int nodeIndex; 22 | 23 | try 24 | { 25 | nodeName = NodeIndexEndingRegex.Replace(name, String.Empty); 26 | nodeIndex = int.Parse(NodeIndexEndingRegex.Match(name).Value.Trim('[', ']')); 27 | 28 | if (nodeIndex < 0) 29 | throw new ArgumentException("Index should be greater than 0."); 30 | } 31 | catch (ArgumentException ex) 32 | { 33 | throw new ArgumentException($"Wrong element name: {name}", ex); 34 | } 35 | catch (FormatException ex) 36 | { 37 | throw new ArgumentException($"Wrong element name: {name}", ex); 38 | } 39 | catch (OverflowException ex) 40 | { 41 | throw new ArgumentException($"Wrong element name: {name}", ex); 42 | } 43 | 44 | var elements = source.Children() 45 | .OfType() 46 | .FirstOrDefault(e => String.Compare(e.Name, nodeName, StringComparison.OrdinalIgnoreCase) == 0)? 47 | .Children() 48 | .FirstOrDefault(); 49 | 50 | return elements.Skip(nodeIndex).FirstOrDefault(); 51 | } 52 | else if (NodeValueEndingRegex.IsMatch(name)) 53 | { 54 | string nodeName = NodeValueEndingRegex.Replace(name, String.Empty); 55 | var nodeSelector = ParseNodeValueFilter(NodeValueEndingRegex.Match(name).Value); 56 | 57 | var elements = source.Children() 58 | .OfType() 59 | .FirstOrDefault(e => String.Compare(e.Name, nodeName, StringComparison.OrdinalIgnoreCase) == 0)? 60 | .Children() 61 | .FirstOrDefault() 62 | .OfType(); 63 | 64 | return elements.FirstOrDefault(e => 65 | { 66 | if ((e.GetChildProperty(nodeSelector.paramName) as JProperty)?.Value is JValue) 67 | { 68 | return (e.GetChildProperty(nodeSelector.paramName) as JProperty)?.Value.ToString() == nodeSelector.paramValue; 69 | } 70 | else 71 | { 72 | return false; 73 | } 74 | }); 75 | } 76 | else 77 | { 78 | return source.Children() 79 | .OfType() 80 | .FirstOrDefault(e => String.Compare(e.Name, name, StringComparison.OrdinalIgnoreCase) == 0); 81 | } 82 | } 83 | 84 | public static IEnumerable GetChildPropertyValue(this JContainer source, string name) 85 | { 86 | if (NodeIndexEndingRegex.IsMatch(name)) 87 | { 88 | string nodeName; 89 | int nodeIndex; 90 | 91 | try 92 | { 93 | nodeName = NodeIndexEndingRegex.Replace(name, String.Empty); 94 | nodeIndex = int.Parse(NodeIndexEndingRegex.Match(name).Value.Trim('[', ']')); 95 | 96 | if (nodeIndex < 0) 97 | throw new ArgumentException("Index should be greater than 0."); 98 | } 99 | catch (ArgumentException ex) 100 | { 101 | throw new ArgumentException($"Wrong element name: {name}", ex); 102 | } 103 | catch (FormatException ex) 104 | { 105 | throw new ArgumentException($"Wrong element name: {name}", ex); 106 | } 107 | catch (OverflowException ex) 108 | { 109 | throw new ArgumentException($"Wrong element name: {name}", ex); 110 | } 111 | 112 | var elements = source.Children() 113 | .OfType() 114 | .Where(e => String.Compare(e.Name, nodeName, StringComparison.OrdinalIgnoreCase) == 0)? 115 | .Select(e => e.Children() 116 | .FirstOrDefault() 117 | .Skip(nodeIndex) 118 | .FirstOrDefault()) 119 | .Where(e => e != null); 120 | 121 | return elements.ToArray(); 122 | } 123 | else if (NodeArrayEndingRegex.IsMatch(name)) 124 | { 125 | string nodeName = NodeArrayEndingRegex.Replace(name, String.Empty); 126 | 127 | return source.Children() 128 | .OfType() 129 | .Where(e => String.Compare(e.Name, nodeName, StringComparison.OrdinalIgnoreCase) == 0)? 130 | .Select(e => e.Children() 131 | .FirstOrDefault()) 132 | .ToArray(); 133 | } 134 | else if (NodeValueEndingRegex.IsMatch(name)) 135 | { 136 | string nodeName = NodeValueEndingRegex.Replace(name, String.Empty); 137 | var nodeSelector = ParseNodeValueFilter(NodeValueEndingRegex.Match(name).Value); 138 | 139 | var elements = source.Children() 140 | .OfType() 141 | .FirstOrDefault(e => String.Compare(e.Name, nodeName, StringComparison.OrdinalIgnoreCase) == 0)? 142 | .Children() 143 | .FirstOrDefault() 144 | .OfType(); 145 | 146 | return elements.Where(e => 147 | { 148 | if ((e.GetChildProperty(nodeSelector.paramName) as JProperty)?.Value is JValue) 149 | { 150 | return (e.GetChildProperty(nodeSelector.paramName) as JProperty)?.Value.ToString() == nodeSelector.paramValue; 151 | } 152 | else 153 | { 154 | return false; 155 | } 156 | }).ToArray(); 157 | } 158 | else 159 | { 160 | return new[] { 161 | source.Children() 162 | .OfType() 163 | .FirstOrDefault(e => String.Compare(e.Name, name, StringComparison.OrdinalIgnoreCase) == 0)? 164 | .Value 165 | }; 166 | } 167 | } 168 | 169 | public static (string paramName, string paramValue) ParseNodeValueFilter(this string filter) 170 | { 171 | var match = NodeValueRegex.Match(filter.Trim(' ', '[', ']')); 172 | if (match.Success && (match.Groups.Count == 3)) 173 | { 174 | return (match.Groups[1].Value, match.Groups[2].Value.Trim('\"', '\'')); 175 | } 176 | else 177 | { 178 | throw new ArgumentException($"Wrong expression: {filter}", nameof(filter)); 179 | } 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/MagicChunks/Helpers/XmlExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using System.Xml.Linq; 7 | 8 | namespace MagicChunks.Helpers 9 | { 10 | public static class XmlExtensions 11 | { 12 | private static readonly Regex NodeIndexEndingRegex = new Regex(@"\[\d+\]$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 13 | private static readonly Regex ProcessingInstructionsPathElementRegex = new Regex(@"^\?.+", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline); 14 | private static readonly Regex AttributeNodeRegex = new Regex(@"(?\w+)\s*\=\s*\""(?.+?)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline); 15 | 16 | public static string ToStringWithDeclaration(this XDocument document) 17 | { 18 | if (document == null) 19 | { 20 | throw new ArgumentNullException(nameof(document)); 21 | } 22 | 23 | return document.ToStringWithDeclaration(SaveOptions.None); 24 | } 25 | 26 | public static string ToStringWithDeclaration(this XDocument document, SaveOptions options) 27 | { 28 | if (document == null) 29 | { 30 | throw new ArgumentNullException(nameof(document)); 31 | } 32 | 33 | var newLine = (options & SaveOptions.DisableFormatting) == SaveOptions.DisableFormatting ? string.Empty : Environment.NewLine; 34 | 35 | return 36 | document.Declaration == null ? 37 | document.ToString(options) : 38 | document.Declaration + newLine + document.ToString(options); 39 | } 40 | 41 | public static XName GetNameWithNamespace(this string name, XElement element, string defaultNamespace) 42 | { 43 | XName result; 44 | if (name.Contains(':') == false) 45 | result = XName.Get(name, defaultNamespace); 46 | else 47 | { 48 | var attributeNameParts = name.Split(':'); 49 | var attributeNamespace = element.GetNamespaceOfPrefix(attributeNameParts[0]); 50 | if (attributeNamespace != null) 51 | result = XName.Get(attributeNameParts[1], attributeNamespace.NamespaceName); 52 | else 53 | result = XName.Get(attributeNameParts[1], defaultNamespace); 54 | } 55 | 56 | return result; 57 | } 58 | 59 | public static XElement GetChildElementByName(this XElement source, string name) 60 | { 61 | if (!NodeIndexEndingRegex.IsMatch(name)) 62 | { 63 | bool isElementNameWithNamespace = name.IndexOf(':') > 0 && !(name.Split(':')[0].Contains("'") || (name.Split(':')[0].Contains(@""""))); 64 | 65 | return source?.Elements() 66 | .FirstOrDefault(e => !isElementNameWithNamespace ? 67 | String.Compare(e.Name.LocalName, name, StringComparison.OrdinalIgnoreCase) == 0 : 68 | e.Name == name.GetNameWithNamespace(source, String.Empty)); 69 | } 70 | else 71 | { 72 | string nodeName; 73 | int nodeIndex; 74 | 75 | try 76 | { 77 | nodeName = NodeIndexEndingRegex.Replace(name, String.Empty); 78 | nodeIndex = int.Parse(NodeIndexEndingRegex.Match(name).Value.Trim('[', ']')); 79 | 80 | if (nodeIndex < 0) 81 | throw new ArgumentException("Index should be greater than 0."); 82 | } 83 | catch (ArgumentException ex) 84 | { 85 | throw new ArgumentException($"Wrong element name: {name}", ex); 86 | } 87 | catch (FormatException ex) 88 | { 89 | throw new ArgumentException($"Wrong element name: {name}", ex); 90 | } 91 | catch (OverflowException ex) 92 | { 93 | throw new ArgumentException($"Wrong element name: {name}", ex); 94 | } 95 | 96 | var elements = source?.Elements() 97 | .Where(e => name.IndexOf(':') == -1 ? 98 | String.Compare(e.Name.LocalName, nodeName, StringComparison.OrdinalIgnoreCase) == 0 : 99 | e.Name == nodeName.GetNameWithNamespace(source, String.Empty)); 100 | 101 | return elements.Skip(nodeIndex).FirstOrDefault(); 102 | } 103 | } 104 | 105 | public static XProcessingInstruction GetChildProcessingInstructionByName(this XElement source, string name) 106 | { 107 | if (!NodeIndexEndingRegex.IsMatch(name)) 108 | { 109 | return source?.Nodes().OfType() 110 | .FirstOrDefault(e => String.Compare(e.Target, name, StringComparison.OrdinalIgnoreCase) == 0); 111 | } 112 | else 113 | { 114 | string nodeName; 115 | int nodeIndex; 116 | 117 | try 118 | { 119 | nodeName = NodeIndexEndingRegex.Replace(name, String.Empty); 120 | nodeIndex = int.Parse(NodeIndexEndingRegex.Match(name).Value.Trim('[', ']')); 121 | 122 | if (nodeIndex < 0) 123 | throw new ArgumentException("Index should be greater than 0."); 124 | } 125 | catch (ArgumentException ex) 126 | { 127 | throw new ArgumentException($"Wrong element name: {name}", ex); 128 | } 129 | catch (FormatException ex) 130 | { 131 | throw new ArgumentException($"Wrong element name: {name}", ex); 132 | } 133 | catch (OverflowException ex) 134 | { 135 | throw new ArgumentException($"Wrong element name: {name}", ex); 136 | } 137 | 138 | var elements = source?.Nodes().OfType() 139 | .Where(e => String.Compare(e.Target, nodeName, StringComparison.OrdinalIgnoreCase) == 0); 140 | 141 | return elements.Skip(nodeIndex).FirstOrDefault(); 142 | } 143 | } 144 | 145 | public static XElement GetChildElementByAttrValue(this XElement source, string name, string attr, string attrValue) 146 | { 147 | var elements = source.Elements() 148 | .Where(e => String.Compare(e.Name.LocalName, name, StringComparison.OrdinalIgnoreCase) == 0); 149 | 150 | return elements 151 | .FirstOrDefault(e => e.Attributes().Any(a => (a.Name == attr.GetNameWithNamespace(source, String.Empty)) && (a.Value == attrValue))); 152 | } 153 | 154 | public static XProcessingInstruction GetChildProcessingInstructionByAttrValue(this XElement source, string name, string attr, string attrValue) 155 | { 156 | var nodes = source.Nodes().OfType() 157 | .Where(e => String.Compare(e.Target, name, StringComparison.OrdinalIgnoreCase) == 0); 158 | 159 | return nodes.FirstOrDefault(e => AttributeNodeRegex.Matches(e.Data).OfType().Any(a => 160 | { 161 | var eAttrName = a.Groups["attrName"]?.Value; 162 | var eAttrValue = a.Groups["attrValue"]?.Value; 163 | 164 | return (String.Compare(eAttrName, attr, StringComparison.OrdinalIgnoreCase) == 0) && (eAttrValue == attrValue); 165 | })); 166 | } 167 | 168 | public static XElement CreateChildElement(this XElement source, string documentNamespace, string elementName, 169 | string attrName = null, string attrValue = null) 170 | { 171 | var item = new XElement(elementName.GetNameWithNamespace(source, documentNamespace)); 172 | 173 | if (!String.IsNullOrWhiteSpace(attrName) && !String.IsNullOrWhiteSpace(attrValue)) 174 | { 175 | item.SetAttributeValue(attrName.GetNameWithNamespace(source, documentNamespace), attrValue); 176 | } 177 | 178 | source.Add(item); 179 | return item; 180 | } 181 | 182 | public static XProcessingInstruction CreateChildProcessingInstruction(this XElement source, string documentNamespace, string elementName, 183 | string attrName = null, string attrValue = null) 184 | { 185 | var item = new XProcessingInstruction(elementName, (!string.IsNullOrWhiteSpace(attrName) && !string.IsNullOrWhiteSpace(attrValue)) ? $"{attrName}=\"{attrValue}\"" : string.Empty); 186 | source.Add(item); 187 | return item; 188 | } 189 | 190 | public static XElement FindChildByAttrFilterMatch(this XElement source, Match attributeFilterMatch, 191 | string documentNamespace) 192 | { 193 | var elementName = attributeFilterMatch.Groups["element"].Value; 194 | var attrName = attributeFilterMatch.Groups["key"].Value; 195 | var attrValue = attributeFilterMatch.Groups["value"].Value; 196 | 197 | var item = source?.GetChildElementByAttrValue(elementName, attrName, attrValue); 198 | return item ?? source.CreateChildElement(documentNamespace, elementName, attrName, attrValue); 199 | } 200 | 201 | public static XProcessingInstruction FindProcessingInstructionByAttrFilterMatch(this XElement source, Match attributeFilterMatch, 202 | string documentNamespace) 203 | { 204 | var elementName = attributeFilterMatch.Groups["element"].Value?.TrimStart('?'); 205 | var attrName = attributeFilterMatch.Groups["key"].Value; 206 | var attrValue = attributeFilterMatch.Groups["value"].Value; 207 | 208 | var item = source?.GetChildProcessingInstructionByAttrValue(elementName, attrName, attrValue); 209 | return item ?? source.CreateChildProcessingInstruction(documentNamespace, elementName, attrName, attrValue); 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /src/MagicChunks.Tests/Documents/JsonDocumentTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MagicChunks.Documents; 3 | using Xunit; 4 | 5 | namespace MagicChunks.Tests.Documents 6 | { 7 | public class JsonDocumentTests 8 | { 9 | [Fact] 10 | public void Transform() 11 | { 12 | // Arrange 13 | 14 | var document = new JsonDocument(@"{ 15 | 'a': { 16 | 'x': '1' 17 | }, 18 | 'b': '2', 19 | 'c': '3' 20 | }"); 21 | 22 | 23 | // Act 24 | 25 | document.ReplaceKey(new[] {"A", "y"}, "2"); 26 | document.ReplaceKey(new[] {"a", "z", "t", "w"}, "3"); 27 | document.ReplaceKey(new[] {"B"}, "5"); 28 | document.ReplaceKey(new[] {"c", "a"}, "1"); 29 | document.ReplaceKey(new[] {"c", "b"}, "2"); 30 | document.ReplaceKey(new[] {"c", "b", "t"}, "3"); 31 | document.ReplaceKey(new[] {"d"}, "4"); 32 | 33 | var result = document.ToString(); 34 | 35 | 36 | // Assert 37 | 38 | Assert.Equal(@"{ 39 | ""a"": { 40 | ""x"": ""1"", 41 | ""y"": ""2"", 42 | ""z"": { 43 | ""t"": { 44 | ""w"": ""3"" 45 | } 46 | } 47 | }, 48 | ""b"": ""5"", 49 | ""c"": { 50 | ""a"": ""1"", 51 | ""b"": { 52 | ""t"": ""3"" 53 | } 54 | }, 55 | ""d"": ""4"" 56 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 57 | } 58 | 59 | [Fact] 60 | public void Transform2() 61 | { 62 | // Arrange 63 | 64 | var document = new JsonDocument(@"{ 65 | ""Data"": { 66 | ""DefaultConnection"": { 67 | ""ConnectionString"": ""mongodb://"", 68 | ""DatabaseName"": ""test1"" 69 | } 70 | }, 71 | 72 | ""Logging"": { 73 | ""IncludeScopes"": false, 74 | ""LogLevel"": { 75 | ""Default"": ""Verbose"", 76 | ""System"": ""Information"", 77 | ""Microsoft"": ""Information"" 78 | } 79 | }, 80 | 81 | ""Smtp"": { 82 | ""Method"": ""SpecifiedPickupDirectory"", 83 | 84 | ""From"": { 85 | ""Address"": ""test1@gmail.com"", 86 | ""DisplayName"": ""test"" 87 | }, 88 | 89 | ""SpecifiedPickupDirectory"": { 90 | ""PickupDirectoryLocation"": ""test\\maildrop\\"", 91 | ""AbsolutePath"": false 92 | }, 93 | 94 | ""Network"": { 95 | ""Host"": ""smtp.gmail.com"", 96 | ""Port"": 587, 97 | ""Timeout"": 3000, 98 | ""EnableSsl"": true, 99 | ""Credentials"": { 100 | ""Username"": ""test@gmail.com"", 101 | ""Password"": ""asdasdasd"" 102 | } 103 | } 104 | 105 | } 106 | }"); 107 | 108 | 109 | // Act 110 | 111 | document.ReplaceKey(new[] { "Data", "DefaultConnection", "ConnectionString" }, "mongodb://server1.local.db1.test"); 112 | document.ReplaceKey(new[] { "Smtp", "Network", "Host" }, "test.ru"); 113 | 114 | var result = document.ToString(); 115 | 116 | 117 | // Assert 118 | 119 | Assert.Equal(@"{ 120 | ""Data"": { 121 | ""DefaultConnection"": { 122 | ""ConnectionString"": ""mongodb://server1.local.db1.test"", 123 | ""DatabaseName"": ""test1"" 124 | } 125 | }, 126 | ""Logging"": { 127 | ""IncludeScopes"": false, 128 | ""LogLevel"": { 129 | ""Default"": ""Verbose"", 130 | ""System"": ""Information"", 131 | ""Microsoft"": ""Information"" 132 | } 133 | }, 134 | ""Smtp"": { 135 | ""Method"": ""SpecifiedPickupDirectory"", 136 | ""From"": { 137 | ""Address"": ""test1@gmail.com"", 138 | ""DisplayName"": ""test"" 139 | }, 140 | ""SpecifiedPickupDirectory"": { 141 | ""PickupDirectoryLocation"": ""test\\maildrop\\"", 142 | ""AbsolutePath"": false 143 | }, 144 | ""Network"": { 145 | ""Host"": ""test.ru"", 146 | ""Port"": 587, 147 | ""Timeout"": 3000, 148 | ""EnableSsl"": true, 149 | ""Credentials"": { 150 | ""Username"": ""test@gmail.com"", 151 | ""Password"": ""asdasdasd"" 152 | } 153 | } 154 | } 155 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 156 | } 157 | 158 | [Fact] 159 | public void Transform3() 160 | { 161 | // Arrange 162 | 163 | var document = new JsonDocument(@"{ 164 | ""bindings"": [ 165 | { 166 | ""type"": ""httpTrigger"", 167 | ""direction"": ""in"", 168 | ""webHookType"": ""genericJson"", 169 | ""name"": ""req"" 170 | }, 171 | { 172 | ""type"": ""http"", 173 | ""direction"": ""out"", 174 | ""name"": ""res"" 175 | }, 176 | { 177 | ""type"": ""blob"", 178 | ""name"": ""name2"", 179 | ""path"": ""path2"", 180 | ""connection"": ""connection_storage"", 181 | ""direction"": ""in"" 182 | }, 183 | { 184 | ""type"": ""blob"", 185 | ""name"": ""name2"", 186 | ""path"": ""path2"", 187 | ""connection"": ""connection_storage"", 188 | ""direction"": ""in"" 189 | } 190 | ], 191 | ""disabled"": false 192 | }"); 193 | 194 | 195 | // Act 196 | 197 | document.ReplaceKey(new[] { "bindings[]", "connection" }, "test"); 198 | 199 | var result = document.ToString(); 200 | 201 | 202 | // Assert 203 | 204 | Assert.Equal(@"{ 205 | ""bindings"": [ 206 | { 207 | ""type"": ""httpTrigger"", 208 | ""direction"": ""in"", 209 | ""webHookType"": ""genericJson"", 210 | ""name"": ""req"", 211 | ""connection"": ""test"" 212 | }, 213 | { 214 | ""type"": ""http"", 215 | ""direction"": ""out"", 216 | ""name"": ""res"", 217 | ""connection"": ""test"" 218 | }, 219 | { 220 | ""type"": ""blob"", 221 | ""name"": ""name2"", 222 | ""path"": ""path2"", 223 | ""connection"": ""test"", 224 | ""direction"": ""in"" 225 | }, 226 | { 227 | ""type"": ""blob"", 228 | ""name"": ""name2"", 229 | ""path"": ""path2"", 230 | ""connection"": ""test"", 231 | ""direction"": ""in"" 232 | } 233 | ], 234 | ""disabled"": false 235 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 236 | } 237 | 238 | [Fact] 239 | public void TransformByIndex() 240 | { 241 | // Arrange 242 | 243 | var document = new JsonDocument(@"{ 244 | 'items': [ 245 | { 'i': 0, data: 'AA' }, 246 | { 'i': 1, data: 'BB' }, 247 | { 'i': 2, data: 'CC' }, 248 | { 'i': 3, data: 'DD' }, 249 | ], 250 | 'b': '2', 251 | 'c': '3' 252 | }"); 253 | 254 | 255 | // Act 256 | 257 | document.ReplaceKey(new[] { "items[2]", "data" }, "AA"); 258 | 259 | var result = document.ToString(); 260 | 261 | 262 | // Assert 263 | 264 | Assert.Equal(@"{ 265 | ""items"": [ 266 | { 267 | ""i"": 0, 268 | ""data"": ""AA"" 269 | }, 270 | { 271 | ""i"": 1, 272 | ""data"": ""BB"" 273 | }, 274 | { 275 | ""i"": 2, 276 | ""data"": ""AA"" 277 | }, 278 | { 279 | ""i"": 3, 280 | ""data"": ""DD"" 281 | } 282 | ], 283 | ""b"": ""2"", 284 | ""c"": ""3"" 285 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 286 | } 287 | 288 | [Fact] 289 | public void TransformByNodeValue() 290 | { 291 | // Arrange 292 | 293 | var document = new JsonDocument(@"{ 294 | 'items': [ 295 | { 'i': 0, data: 'AA' }, 296 | { 'i': 1, data: 'BB' }, 297 | { 'i': 2, data: 'CC' }, 298 | { 'i': 3, data: 'DD' }, 299 | ], 300 | 'b': '2', 301 | 'c': '3' 302 | }"); 303 | 304 | 305 | // Act 306 | 307 | document.ReplaceKey(new[] { "items[@i=1]", "data" }, "EE"); 308 | 309 | var result = document.ToString(); 310 | 311 | 312 | // Assert 313 | 314 | Assert.Equal(@"{ 315 | ""items"": [ 316 | { 317 | ""i"": 0, 318 | ""data"": ""AA"" 319 | }, 320 | { 321 | ""i"": 1, 322 | ""data"": ""EE"" 323 | }, 324 | { 325 | ""i"": 2, 326 | ""data"": ""CC"" 327 | }, 328 | { 329 | ""i"": 3, 330 | ""data"": ""DD"" 331 | } 332 | ], 333 | ""b"": ""2"", 334 | ""c"": ""3"" 335 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 336 | } 337 | 338 | [Fact] 339 | public void AddStringElementToArray() 340 | { 341 | // Arrange 342 | 343 | var document = new JsonDocument(@"{ 344 | 'a': { 345 | 'x': '1' 346 | }, 347 | 'b': '2', 348 | 'c': '3', 349 | 'd': [] 350 | }"); 351 | 352 | 353 | // Act 354 | 355 | document.AddElementToArray(new[] { "d" }, "1"); 356 | 357 | var result = document.ToString(); 358 | 359 | 360 | // Assert 361 | 362 | Assert.Equal(@"{ 363 | ""a"": { 364 | ""x"": ""1"" 365 | }, 366 | ""b"": ""2"", 367 | ""c"": ""3"", 368 | ""d"": [ 369 | ""1"" 370 | ] 371 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 372 | } 373 | 374 | [Fact] 375 | public void AddObjectElementToArray() 376 | { 377 | // Arrange 378 | 379 | var document = new JsonDocument(@"{ 380 | 'a': { 381 | 'x': '1' 382 | }, 383 | 'b': '2', 384 | 'c': '3', 385 | 'd': [] 386 | }"); 387 | 388 | 389 | // Act 390 | 391 | document.AddElementToArray(new[] { "d" }, "{ 'val1': '1', 'val2': '2' }"); 392 | 393 | var result = document.ToString(); 394 | 395 | 396 | // Assert 397 | 398 | Assert.Equal(@"{ 399 | ""a"": { 400 | ""x"": ""1"" 401 | }, 402 | ""b"": ""2"", 403 | ""c"": ""3"", 404 | ""d"": [ 405 | { 406 | ""val1"": ""1"", 407 | ""val2"": ""2"" 408 | } 409 | ] 410 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 411 | } 412 | 413 | [Fact] 414 | public void Remove() 415 | { 416 | // Arrange 417 | 418 | var document = new JsonDocument(@"{ 419 | 'a': { 420 | 'x': '1' 421 | }, 422 | 'b': { 423 | 'x': '1' 424 | }, 425 | 'c': '3' 426 | }"); 427 | 428 | 429 | // Act 430 | 431 | document.RemoveKey(new[] { "a"}); 432 | document.RemoveKey(new[] { "b", "x" }); 433 | 434 | var result = document.ToString(); 435 | 436 | 437 | // Assert 438 | 439 | Assert.Equal(@"{ 440 | ""b"": {}, 441 | ""c"": ""3"" 442 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 443 | } 444 | 445 | [Fact] 446 | public void RemoveArrayItem() 447 | { 448 | // Arrange 449 | 450 | var document = new JsonDocument(@"{ 451 | 'items': [ 452 | { 'i': 0, data: 'AA' }, 453 | { 'i': 1, data: 'BB' }, 454 | { 'i': 2, data: 'CC' }, 455 | { 'i': 3, data: 'DD' }, 456 | ], 457 | 'b': '2', 458 | 'c': '3' 459 | }"); 460 | 461 | 462 | // Act 463 | 464 | document.RemoveKey(new[] { "items[2]" }); 465 | document.RemoveKey(new[] { "items[@i=1]", "data" }); 466 | 467 | var result = document.ToString(); 468 | 469 | 470 | // Assert 471 | 472 | Assert.Equal(@"{ 473 | ""items"": [ 474 | { 475 | ""i"": 0, 476 | ""data"": ""AA"" 477 | }, 478 | { 479 | ""i"": 1 480 | }, 481 | { 482 | ""i"": 3, 483 | ""data"": ""DD"" 484 | } 485 | ], 486 | ""b"": ""2"", 487 | ""c"": ""3"" 488 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 489 | } 490 | 491 | [Fact] 492 | public void ValidateEmptyPath() 493 | { 494 | // Assert 495 | JsonDocument document = new JsonDocument("{}"); 496 | 497 | // Act 498 | ArgumentException result = Assert.Throws(() => document.ReplaceKey(new[] { "a", "", "b" }, "")); 499 | 500 | // Arrange 501 | Assert.True(result.Message?.StartsWith("There is empty items in the path.")); 502 | } 503 | 504 | [Fact] 505 | public void ValidateWithespacePath() 506 | { 507 | // Assert 508 | JsonDocument document = new JsonDocument("{}"); 509 | 510 | // Act 511 | ArgumentException result = Assert.Throws(() => document.ReplaceKey(new[] { "a", " ", "b" }, "")); 512 | 513 | // Arrange 514 | Assert.True(result.Message?.StartsWith("There is empty items in the path.")); 515 | } 516 | } 517 | } -------------------------------------------------------------------------------- /.vscode/markdown.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 35px 45px; 4 | background-color: #fff; 5 | } 6 | 7 | @font-face { 8 | font-family: octicons-link; 9 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); 10 | } 11 | 12 | body { 13 | -ms-text-size-adjust: 100%; 14 | -webkit-text-size-adjust: 100%; 15 | color: #333; 16 | font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 17 | font-size: 16px; 18 | line-height: 1.6; 19 | word-wrap: break-word; 20 | } 21 | 22 | a { 23 | background-color: transparent; 24 | -webkit-text-decoration-skip: objects; 25 | } 26 | 27 | a:active, 28 | a:hover { 29 | outline-width: 0; 30 | } 31 | 32 | strong { 33 | font-weight: inherit; 34 | } 35 | 36 | strong { 37 | font-weight: bolder; 38 | } 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | img { 46 | border-style: none; 47 | } 48 | 49 | svg:not(:root) { 50 | overflow: hidden; 51 | } 52 | 53 | code, 54 | kbd, 55 | pre { 56 | font-family: monospace, monospace; 57 | font-size: 1em; 58 | } 59 | 60 | hr { 61 | box-sizing: content-box; 62 | height: 0; 63 | overflow: visible; 64 | } 65 | 66 | input { 67 | font: inherit; 68 | margin: 0; 69 | } 70 | 71 | input { 72 | overflow: visible; 73 | } 74 | 75 | button:-moz-focusring, 76 | [type="button"]:-moz-focusring, 77 | [type="reset"]:-moz-focusring, 78 | [type="submit"]:-moz-focusring { 79 | outline: 1px dotted ButtonText; 80 | } 81 | 82 | [type="checkbox"] { 83 | box-sizing: border-box; 84 | padding: 0; 85 | } 86 | 87 | table { 88 | border-spacing: 0; 89 | border-collapse: collapse; 90 | } 91 | 92 | td, 93 | th { 94 | padding: 0; 95 | } 96 | 97 | * { 98 | box-sizing: border-box; 99 | } 100 | 101 | input { 102 | font: 13px/1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 103 | } 104 | 105 | a { 106 | color: #4078c0; 107 | text-decoration: none; 108 | } 109 | 110 | a:hover, 111 | a:active { 112 | text-decoration: underline; 113 | } 114 | 115 | hr { 116 | height: 0; 117 | margin: 15px 0; 118 | overflow: hidden; 119 | background: transparent; 120 | border: 0; 121 | border-bottom: 1px solid #ddd; 122 | } 123 | 124 | hr::before { 125 | display: table; 126 | content: ""; 127 | } 128 | 129 | hr::after { 130 | display: table; 131 | clear: both; 132 | content: ""; 133 | } 134 | 135 | h1, 136 | h2, 137 | h3, 138 | h4, 139 | h5, 140 | h6 { 141 | margin-top: 0; 142 | margin-bottom: 0; 143 | line-height: 1.5; 144 | } 145 | 146 | h1 { 147 | font-size: 30px; 148 | } 149 | 150 | h2 { 151 | font-size: 21px; 152 | } 153 | 154 | h3 { 155 | font-size: 16px; 156 | } 157 | 158 | h4 { 159 | font-size: 14px; 160 | } 161 | 162 | h5 { 163 | font-size: 12px; 164 | } 165 | 166 | h6 { 167 | font-size: 11px; 168 | } 169 | 170 | p { 171 | margin-top: 0; 172 | margin-bottom: 10px; 173 | } 174 | 175 | blockquote { 176 | margin: 0; 177 | } 178 | 179 | ul, 180 | ol { 181 | padding-left: 0; 182 | margin-top: 0; 183 | margin-bottom: 0; 184 | } 185 | 186 | ol ol, 187 | ul ol { 188 | list-style-type: lower-roman; 189 | } 190 | 191 | ul ul ol, 192 | ul ol ol, 193 | ol ul ol, 194 | ol ol ol { 195 | list-style-type: lower-alpha; 196 | } 197 | 198 | dd { 199 | margin-left: 0; 200 | } 201 | 202 | code { 203 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 204 | font-size: 12px; 205 | } 206 | 207 | pre { 208 | margin-top: 0; 209 | margin-bottom: 0; 210 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; 211 | } 212 | 213 | .pl-0 { 214 | padding-left: 0 !important; 215 | } 216 | 217 | .pl-1 { 218 | padding-left: 3px !important; 219 | } 220 | 221 | .pl-2 { 222 | padding-left: 6px !important; 223 | } 224 | 225 | .pl-3 { 226 | padding-left: 12px !important; 227 | } 228 | 229 | .pl-4 { 230 | padding-left: 24px !important; 231 | } 232 | 233 | .pl-5 { 234 | padding-left: 36px !important; 235 | } 236 | 237 | .pl-6 { 238 | padding-left: 48px !important; 239 | } 240 | 241 | .form-select::-ms-expand { 242 | opacity: 0; 243 | } 244 | 245 | .markdown-body:before { 246 | display: table; 247 | content: ""; 248 | } 249 | 250 | .markdown-body:after { 251 | display: table; 252 | clear: both; 253 | content: ""; 254 | } 255 | 256 | .markdown-body>*:first-child { 257 | margin-top: 0 !important; 258 | } 259 | 260 | .markdown-body>*:last-child { 261 | margin-bottom: 0 !important; 262 | } 263 | 264 | a:not([href]) { 265 | color: inherit; 266 | text-decoration: none; 267 | } 268 | 269 | .anchor { 270 | display: inline-block; 271 | padding-right: 2px; 272 | margin-left: -18px; 273 | } 274 | 275 | .anchor:focus { 276 | outline: none; 277 | } 278 | 279 | h1, 280 | h2, 281 | h3, 282 | h4, 283 | h5, 284 | h6 { 285 | margin-top: 1em; 286 | margin-bottom: 16px; 287 | font-weight: bold; 288 | line-height: 1.4; 289 | } 290 | 291 | h1 .octicon-link, 292 | h2 .octicon-link, 293 | h3 .octicon-link, 294 | h4 .octicon-link, 295 | h5 .octicon-link, 296 | h6 .octicon-link { 297 | color: #000; 298 | vertical-align: middle; 299 | visibility: hidden; 300 | } 301 | 302 | h1:hover .anchor, 303 | h2:hover .anchor, 304 | h3:hover .anchor, 305 | h4:hover .anchor, 306 | h5:hover .anchor, 307 | h6:hover .anchor { 308 | text-decoration: none; 309 | } 310 | 311 | h1:hover .anchor .octicon-link, 312 | h2:hover .anchor .octicon-link, 313 | h3:hover .anchor .octicon-link, 314 | h4:hover .anchor .octicon-link, 315 | h5:hover .anchor .octicon-link, 316 | h6:hover .anchor .octicon-link { 317 | visibility: visible; 318 | } 319 | 320 | h1 { 321 | padding-bottom: 0.3em; 322 | font-size: 2.25em; 323 | line-height: 1.2; 324 | border-bottom: 1px solid #eee; 325 | } 326 | 327 | h1 .anchor { 328 | line-height: 1; 329 | } 330 | 331 | h2 { 332 | padding-bottom: 0.3em; 333 | font-size: 1.75em; 334 | line-height: 1.225; 335 | border-bottom: 1px solid #eee; 336 | } 337 | 338 | h2 .anchor { 339 | line-height: 1; 340 | } 341 | 342 | h3 { 343 | font-size: 1.5em; 344 | line-height: 1.43; 345 | } 346 | 347 | h3 .anchor { 348 | line-height: 1.2; 349 | } 350 | 351 | h4 { 352 | font-size: 1.25em; 353 | } 354 | 355 | h4 .anchor { 356 | line-height: 1.2; 357 | } 358 | 359 | h5 { 360 | font-size: 1em; 361 | } 362 | 363 | h5 .anchor { 364 | line-height: 1.1; 365 | } 366 | 367 | h6 { 368 | font-size: 1em; 369 | color: #777; 370 | } 371 | 372 | h6 .anchor { 373 | line-height: 1.1; 374 | } 375 | 376 | p, 377 | blockquote, 378 | ul, 379 | ol, 380 | dl, 381 | table, 382 | pre { 383 | margin-top: 0; 384 | margin-bottom: 16px; 385 | } 386 | 387 | hr { 388 | height: 4px; 389 | padding: 0; 390 | margin: 16px 0; 391 | background-color: #e7e7e7; 392 | border: 0 none; 393 | } 394 | 395 | ul, 396 | ol { 397 | padding-left: 2em; 398 | } 399 | 400 | ul ul, 401 | ul ol, 402 | ol ol, 403 | ol ul { 404 | margin-top: 0; 405 | margin-bottom: 0; 406 | } 407 | 408 | li>p { 409 | margin-top: 16px; 410 | } 411 | 412 | dl { 413 | padding: 0; 414 | } 415 | 416 | dl dt { 417 | padding: 0; 418 | margin-top: 16px; 419 | font-size: 1em; 420 | font-style: italic; 421 | font-weight: bold; 422 | } 423 | 424 | dl dd { 425 | padding: 0 16px; 426 | margin-bottom: 16px; 427 | } 428 | 429 | blockquote { 430 | padding: 0 15px; 431 | color: #777; 432 | border-left: 4px solid #ddd; 433 | } 434 | 435 | blockquote>:first-child { 436 | margin-top: 0; 437 | } 438 | 439 | blockquote>:last-child { 440 | margin-bottom: 0; 441 | } 442 | 443 | table { 444 | display: block; 445 | width: 100%; 446 | overflow: auto; 447 | word-break: normal; 448 | word-break: keep-all; 449 | } 450 | 451 | table th { 452 | font-weight: bold; 453 | } 454 | 455 | table th, 456 | table td { 457 | padding: 6px 13px; 458 | border: 1px solid #ddd; 459 | } 460 | 461 | table tr { 462 | background-color: #fff; 463 | border-top: 1px solid #ccc; 464 | } 465 | 466 | table tr:nth-child(2n) { 467 | background-color: #f8f8f8; 468 | } 469 | 470 | img { 471 | max-width: 100%; 472 | box-sizing: content-box; 473 | background-color: #fff; 474 | } 475 | 476 | code { 477 | padding: 0; 478 | padding-top: 0.2em; 479 | padding-bottom: 0.2em; 480 | margin: 0; 481 | font-size: 85%; 482 | background-color: rgba(0,0,0,0.04); 483 | border-radius: 3px; 484 | } 485 | 486 | code:before, 487 | code:after { 488 | letter-spacing: -0.2em; 489 | content: "\00a0"; 490 | } 491 | 492 | pre>code { 493 | padding: 0; 494 | margin: 0; 495 | font-size: 100%; 496 | word-break: normal; 497 | white-space: pre; 498 | background: transparent; 499 | border: 0; 500 | } 501 | 502 | .highlight { 503 | margin-bottom: 16px; 504 | } 505 | 506 | .highlight pre, 507 | pre { 508 | padding: 16px; 509 | overflow: auto; 510 | font-size: 85%; 511 | line-height: 1.45; 512 | background-color: #f7f7f7; 513 | border-radius: 3px; 514 | } 515 | 516 | .highlight pre { 517 | margin-bottom: 0; 518 | word-break: normal; 519 | } 520 | 521 | pre { 522 | word-wrap: normal; 523 | } 524 | 525 | pre code { 526 | display: inline; 527 | max-width: initial; 528 | padding: 0; 529 | margin: 0; 530 | overflow: initial; 531 | line-height: inherit; 532 | word-wrap: normal; 533 | background-color: transparent; 534 | border: 0; 535 | } 536 | 537 | pre code:before, 538 | pre code:after { 539 | content: normal; 540 | } 541 | 542 | kbd { 543 | display: inline-block; 544 | padding: 3px 5px; 545 | font-size: 11px; 546 | line-height: 10px; 547 | color: #555; 548 | vertical-align: middle; 549 | background-color: #fcfcfc; 550 | border: solid 1px #ccc; 551 | border-bottom-color: #bbb; 552 | border-radius: 3px; 553 | box-shadow: inset 0 -1px 0 #bbb; 554 | } 555 | 556 | .pl-c { 557 | color: #969896; 558 | } 559 | 560 | .pl-c1, 561 | .pl-s .pl-v { 562 | color: #0086b3; 563 | } 564 | 565 | .pl-e, 566 | .pl-en { 567 | color: #795da3; 568 | } 569 | 570 | .pl-s .pl-s1, 571 | .pl-smi { 572 | color: #333; 573 | } 574 | 575 | .pl-ent { 576 | color: #63a35c; 577 | } 578 | 579 | .pl-k { 580 | color: #a71d5d; 581 | } 582 | 583 | .pl-pds, 584 | .pl-s, 585 | .pl-s .pl-pse .pl-s1, 586 | .pl-sr, 587 | .pl-sr .pl-cce, 588 | .pl-sr .pl-sra, 589 | .pl-sr .pl-sre { 590 | color: #183691; 591 | } 592 | 593 | .pl-v { 594 | color: #ed6a43; 595 | } 596 | 597 | .pl-id { 598 | color: #b52a1d; 599 | } 600 | 601 | .pl-ii { 602 | background-color: #b52a1d; 603 | color: #f8f8f8; 604 | } 605 | 606 | .pl-sr .pl-cce { 607 | color: #63a35c; 608 | font-weight: bold; 609 | } 610 | 611 | .pl-ml { 612 | color: #693a17; 613 | } 614 | 615 | .pl-mh, 616 | .pl-mh .pl-en, 617 | .pl-ms { 618 | color: #1d3e81; 619 | font-weight: bold; 620 | } 621 | 622 | .pl-mq { 623 | color: #008080; 624 | } 625 | 626 | .pl-mi { 627 | color: #333; 628 | font-style: italic; 629 | } 630 | 631 | .pl-mb { 632 | color: #333; 633 | font-weight: bold; 634 | } 635 | 636 | .pl-md { 637 | background-color: #ffecec; 638 | color: #bd2c00; 639 | } 640 | 641 | .pl-mi1 { 642 | background-color: #eaffea; 643 | color: #55a532; 644 | } 645 | 646 | .pl-mdr { 647 | color: #795da3; 648 | font-weight: bold; 649 | } 650 | 651 | .pl-mo { 652 | color: #1d3e81; 653 | } 654 | 655 | kbd { 656 | display: inline-block; 657 | padding: 3px 5px; 658 | font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; 659 | line-height: 10px; 660 | color: #555; 661 | vertical-align: middle; 662 | background-color: #fcfcfc; 663 | border: solid 1px #ccc; 664 | border-bottom-color: #bbb; 665 | border-radius: 3px; 666 | box-shadow: inset 0 -1px 0 #bbb; 667 | } 668 | 669 | .full-commit .btn-outline:not(:disabled):hover { 670 | color: #4078c0; 671 | border: 1px solid #4078c0; 672 | } 673 | 674 | :checked+.radio-label { 675 | position: relative; 676 | z-index: 1; 677 | border-color: #4078c0; 678 | } 679 | 680 | .octicon { 681 | display: inline-block; 682 | vertical-align: text-top; 683 | fill: currentColor; 684 | } 685 | 686 | .task-list-item { 687 | list-style-type: none; 688 | } 689 | 690 | .task-list-item+.task-list-item { 691 | margin-top: 3px; 692 | } 693 | 694 | .task-list-item input { 695 | margin: 0 0.2em 0.25em -1.6em; 696 | vertical-align: middle; 697 | } 698 | 699 | hr { 700 | border-bottom-color: #eee; 701 | } -------------------------------------------------------------------------------- /src/MagicChunks/Documents/XmlDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Xml; 6 | using System.Xml.Linq; 7 | using MagicChunks.Core; 8 | using MagicChunks.Helpers; 9 | 10 | namespace MagicChunks.Documents 11 | { 12 | public class XmlDocument : IDocument 13 | { 14 | private static readonly Regex AttributeFilterRegex = new Regex(@"(?.+?)\[\s*\@(?[\w\:]+)\s*\=\s*[\'\""]?(?.+?)[\'\""]?\s*\]$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline); 15 | private static readonly Regex ProcessingInstructionsPathElementRegex = new Regex(@"^\?.+", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline); 16 | private static readonly Regex AttributePathElementRegex = new Regex(@"^\@.+", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline); 17 | private static readonly Regex AttributeNodeRegex = new Regex(@"(?\w+)\s*\=\s*\""(?.+?)\""", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Singleline); 18 | 19 | protected readonly XDocument Document; 20 | 21 | public XmlDocument(string source) 22 | { 23 | try 24 | { 25 | Document = XDocument.Parse(source); 26 | } 27 | catch (XmlException ex) 28 | { 29 | throw new ArgumentException("Wrong document format", nameof(source), ex); 30 | } 31 | } 32 | 33 | public void AddElementToArray(string[] path, string value) 34 | { 35 | if ((path == null) || (path.Any() == false)) 36 | throw new ArgumentException("Path is not specified.", nameof(path)); 37 | 38 | if (path.Any(String.IsNullOrWhiteSpace)) 39 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 40 | 41 | XElement current = Document.Root; 42 | string documentNamespace = Document.Root?.Name.NamespaceName ?? String.Empty; 43 | 44 | if (current == null) 45 | throw new ArgumentException("Root element is not present.", nameof(path)); 46 | 47 | if (String.Compare(current.Name.LocalName, path.First(), StringComparison.OrdinalIgnoreCase) != 0) 48 | throw new ArgumentException("Root element name does not match path.", nameof(path)); 49 | 50 | if (!path.Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p))) 51 | { 52 | current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XElement; 53 | UpdateTargetArrayElement(value, path.Last(), current, documentNamespace); 54 | } 55 | else 56 | { 57 | throw new NotSupportedException("Arrays are not supported for XML processing instructions"); 58 | } 59 | } 60 | 61 | public void ReplaceKey(string[] path, string value) 62 | { 63 | if ((path == null) || (path.Any() == false)) 64 | throw new ArgumentException("Path is not specified", nameof(path)); 65 | 66 | if (path.Any(String.IsNullOrWhiteSpace)) 67 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 68 | 69 | XElement current = Document.Root; 70 | string documentNamespace = Document.Root?.Name.NamespaceName ?? String.Empty; 71 | 72 | if (current == null) 73 | throw new ArgumentException("Root element is not present.", nameof(path)); 74 | 75 | if (String.Compare(current.Name.LocalName, path.First(), StringComparison.OrdinalIgnoreCase) != 0) 76 | throw new ArgumentException("Root element name does not match path.", nameof(path)); 77 | 78 | if (!path.Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p))) 79 | { 80 | current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XElement; 81 | UpdateTargetElement(value, path.Last(), current, documentNamespace); 82 | } 83 | else 84 | { 85 | if (path.Take(path.Length - 2).Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p))) 86 | { 87 | throw new ArgumentException("Processing instruction could not contain nested elements.", nameof(path)); 88 | } 89 | 90 | if (!ProcessingInstructionsPathElementRegex.IsMatch(path.Skip(path.Length - 2).First())) 91 | { 92 | throw new ArgumentException("To update processing instruction you should point attribute name.", nameof(path)); 93 | } 94 | 95 | if (!AttributePathElementRegex.IsMatch(path.Last())) 96 | { 97 | throw new ArgumentException("To update processing instruction you should point attribute name.", nameof(path)); 98 | } 99 | 100 | var processingInstruction = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XProcessingInstruction; 101 | UpdateProcessingInstruction(value, path.Last().TrimStart('@'), processingInstruction, documentNamespace); 102 | } 103 | } 104 | 105 | public void RemoveKey(string[] path) 106 | { 107 | if ((path == null) || (path.Any() == false)) 108 | throw new ArgumentException("Path is not specified.", nameof(path)); 109 | 110 | if (path.Any(String.IsNullOrWhiteSpace)) 111 | throw new ArgumentException("There is empty items in the path.", nameof(path)); 112 | 113 | XElement current = Document.Root; 114 | string documentNamespace = Document.Root?.Name.NamespaceName ?? String.Empty; 115 | 116 | if (current == null) 117 | throw new ArgumentException("Root element is not present.", nameof(path)); 118 | 119 | if (String.Compare(current.Name.LocalName, path.First(), StringComparison.OrdinalIgnoreCase) != 0) 120 | throw new ArgumentException("Root element name does not match path.", nameof(path)); 121 | 122 | if (!path.Any(p => ProcessingInstructionsPathElementRegex.IsMatch(p))) 123 | { 124 | current = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XElement; 125 | RemoveTargetElement(path.Last(), current, documentNamespace); 126 | } 127 | else 128 | { 129 | var processingInstruction = FindPath(path.Skip(1).Take(path.Length - 2), current, documentNamespace) as XProcessingInstruction; 130 | 131 | // Remove whole processing instruction (not just single attribute) 132 | if (processingInstruction == null) 133 | processingInstruction = FindPath(path.Skip(1).Take(path.Length - 1), current, documentNamespace) as XProcessingInstruction; 134 | 135 | RemoveProcessingInstruction(path.Last(), processingInstruction, documentNamespace); 136 | } 137 | } 138 | 139 | private static XNode FindPath(IEnumerable path, XElement current, string documentNamespace) 140 | { 141 | XNode result = current; 142 | 143 | foreach (string pathElement in path) 144 | { 145 | if (pathElement.StartsWith("@")) 146 | throw new ArgumentException("Attribute element could be only at end of the path.", nameof(path)); 147 | 148 | var currentElement = !ProcessingInstructionsPathElementRegex.IsMatch(pathElement) ? (result as XElement)?.GetChildElementByName(pathElement) as XNode : (result as XElement)?.GetChildProcessingInstructionByName(pathElement.TrimStart('?')); 149 | 150 | var attributeFilterMatch = AttributeFilterRegex.Match(pathElement); 151 | if (attributeFilterMatch.Success) 152 | { 153 | if (!ProcessingInstructionsPathElementRegex.IsMatch(pathElement)) 154 | { 155 | result = (result as XElement).FindChildByAttrFilterMatch(attributeFilterMatch, documentNamespace); 156 | } 157 | else 158 | { 159 | result = (result as XElement).FindProcessingInstructionByAttrFilterMatch(attributeFilterMatch, documentNamespace); 160 | } 161 | } 162 | else if (currentElement != null) 163 | { 164 | result = currentElement; 165 | } 166 | else 167 | { 168 | if (!(result as XElement).HasElements) 169 | (result as XElement).SetValue(""); 170 | 171 | if (!ProcessingInstructionsPathElementRegex.IsMatch(pathElement)) 172 | result = (result as XElement).CreateChildElement(documentNamespace, pathElement); 173 | else 174 | result = (result as XElement).CreateChildProcessingInstruction(documentNamespace, pathElement.TrimStart('?')); 175 | } 176 | } 177 | 178 | return result; 179 | } 180 | 181 | private static void UpdateTargetElement(string value, string targetElement, XElement current, string documentNamespace) 182 | { 183 | var attributeFilterMatch = AttributeFilterRegex.Match(targetElement); 184 | 185 | if (targetElement.StartsWith("@") == true) 186 | { // Attribute update 187 | current.SetAttributeValue(targetElement.TrimStart('@').GetNameWithNamespace(current, String.Empty), value.Replace(""", @"""").Replace("<", @"<").Replace(">", @">")); 188 | } 189 | else if (!attributeFilterMatch.Success) 190 | { // Property update 191 | var elementToUpdate = current.GetChildElementByName(targetElement); 192 | 193 | if (elementToUpdate != null) 194 | { 195 | elementToUpdate.Value = value; 196 | } 197 | else 198 | { 199 | if (!current.HasElements) 200 | current.SetValue(""); 201 | 202 | current.Add(new XElement(targetElement.GetNameWithNamespace(current, documentNamespace)) { Value = value }); 203 | } 204 | } 205 | else 206 | { // Filtered element update 207 | current = current.FindChildByAttrFilterMatch(attributeFilterMatch, documentNamespace); 208 | current.Value = value; 209 | } 210 | } 211 | 212 | private static void UpdateProcessingInstruction(string value, string targetElement, XProcessingInstruction current, string documentNamespace) 213 | { 214 | var valuesToReplace = AttributeNodeRegex.Matches(current.Data).OfType().Where(a => 215 | { 216 | var eAttrName = a.Groups["attrName"]?.Value; 217 | var eAttrValue = a.Groups["attrValue"]?.Value; 218 | 219 | return String.Compare(eAttrName, targetElement, StringComparison.OrdinalIgnoreCase) == 0; 220 | }).Select(m => m.Value).ToArray(); 221 | 222 | if (valuesToReplace.Any()) 223 | { 224 | foreach (var replaceValue in valuesToReplace) 225 | { 226 | current.Data = current.Data.Replace(replaceValue, $"{targetElement}=\"{value}\""); 227 | } 228 | } 229 | else 230 | { 231 | current.Data += (!string.IsNullOrEmpty(current.Data) ? " " : string.Empty) + $"{targetElement}=\"{value}\""; 232 | } 233 | } 234 | 235 | private static void UpdateTargetArrayElement(string value, string targetElement, XElement current, string documentNamespace) 236 | { 237 | var attributeFilterMatch = AttributeFilterRegex.Match(targetElement); 238 | 239 | if (!attributeFilterMatch.Success) 240 | { // Property update 241 | var elementToUpdate = current.GetChildElementByName(targetElement); 242 | 243 | if (elementToUpdate != null) 244 | { 245 | elementToUpdate.Add(XElement.Parse(value)); 246 | } 247 | else 248 | { 249 | if (!current.HasElements) 250 | current.SetValue(""); 251 | 252 | elementToUpdate = new XElement(targetElement.GetNameWithNamespace(current, documentNamespace)); 253 | current.Add(elementToUpdate); 254 | 255 | elementToUpdate.Add(XElement.Parse(value)); 256 | } 257 | } 258 | else 259 | { // Filtered element update 260 | current = current.FindChildByAttrFilterMatch(attributeFilterMatch, documentNamespace); 261 | current.Add(XElement.Parse(value)); 262 | } 263 | } 264 | 265 | private static void RemoveTargetElement(string targetElement, XElement current, string documentNamespace) 266 | { 267 | var attributeFilterMatch = AttributeFilterRegex.Match(targetElement); 268 | 269 | if (targetElement.StartsWith("@") == true) 270 | { // Attribute update 271 | current.Attribute(XName.Get(targetElement.TrimStart('@'))) 272 | ?.Remove(); 273 | } 274 | else if (!attributeFilterMatch.Success) 275 | { // Property update 276 | var elementToRemove = current.GetChildElementByName(targetElement); 277 | elementToRemove.Remove(); 278 | } 279 | else 280 | { // Filtered element update 281 | current.FindChildByAttrFilterMatch(attributeFilterMatch, documentNamespace) 282 | .Remove(); 283 | } 284 | } 285 | 286 | private static void RemoveProcessingInstruction(string targetElement, XProcessingInstruction current, string documentNamespace) 287 | { 288 | if (ProcessingInstructionsPathElementRegex.IsMatch(targetElement)) 289 | { 290 | // remove whole processing restriction 291 | current.Remove(); 292 | } 293 | else if (AttributePathElementRegex.IsMatch(targetElement)) 294 | { 295 | // remove attribute from processing instruction 296 | var valuesToRemove = AttributeNodeRegex.Matches(current.Data).OfType().Where(a => 297 | { 298 | var eAttrName = a.Groups["attrName"]?.Value; 299 | 300 | return String.Compare(eAttrName, targetElement.TrimStart('@'), StringComparison.OrdinalIgnoreCase) == 0; 301 | }).Select(m => m.Value).ToArray(); 302 | 303 | if (valuesToRemove.Any()) 304 | { 305 | foreach (var value in valuesToRemove) 306 | { 307 | current.Data = current.Data.Replace(value, string.Empty); 308 | } 309 | } 310 | } 311 | } 312 | 313 | 314 | public override string ToString() 315 | { 316 | return Document?.ToStringWithDeclaration() ?? String.Empty; 317 | } 318 | 319 | public void Dispose() 320 | { 321 | } 322 | } 323 | } -------------------------------------------------------------------------------- /src/MagicChunks.Tests/Core/TransformerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MagicChunks.Core; 3 | using MagicChunks.Documents; 4 | using Xunit; 5 | 6 | namespace MagicChunks.Tests.Core 7 | { 8 | public class TransformerTests 9 | { 10 | [Fact] 11 | public void TransformJson() 12 | { 13 | // Arrange 14 | 15 | var transform = new TransformationCollection() 16 | { 17 | { "a/y", "2" }, 18 | { "a/z/t/w", "3" }, 19 | { "b", "5" }, 20 | { "c/a", "1" }, 21 | { "c/b", "2" }, 22 | { "c/b/t", "3" }, 23 | { "d", "4" }, 24 | { "#e", "" }, 25 | { "f/items[]`1", "1" }, 26 | { "f/items[]`2", "2" } 27 | }; 28 | 29 | 30 | // Act 31 | 32 | var transformer = new Transformer(); 33 | string result = transformer.Transform(new JsonDocument(@"{ 34 | 'a': { 35 | 'x': '1' 36 | }, 37 | 'b': '2', 38 | 'c': '3', 39 | 'e': '4' 40 | }"), transform); 41 | 42 | 43 | // Assert 44 | 45 | Assert.Equal(@"{ 46 | ""a"": { 47 | ""x"": ""1"", 48 | ""y"": ""2"", 49 | ""z"": { 50 | ""t"": { 51 | ""w"": ""3"" 52 | } 53 | } 54 | }, 55 | ""b"": ""5"", 56 | ""c"": { 57 | ""a"": ""1"", 58 | ""b"": { 59 | ""t"": ""3"" 60 | } 61 | }, 62 | ""d"": ""4"", 63 | ""f"": { 64 | ""items"": [ 65 | ""1"", 66 | ""2"" 67 | ] 68 | } 69 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 70 | } 71 | 72 | [Fact] 73 | public void TransformJsonImplicit() 74 | { 75 | // Arrange 76 | 77 | var transform = new TransformationCollection("e") 78 | { 79 | { "a/y", "2" }, 80 | { "a/z/t/w", "3" }, 81 | { "b", "5" }, 82 | { "c/a", "1" }, 83 | { "c/b", "2" }, 84 | { "c/b/t", "3" }, 85 | { "d", "4" }, 86 | }; 87 | 88 | 89 | // Act 90 | 91 | var transformer = new Transformer(); 92 | string result = transformer.Transform(@"{ 93 | 'a': { 94 | 'x': '1' 95 | }, 96 | 'b': '2', 97 | 'c': '3', 98 | 'e': '4' 99 | }", transform); 100 | 101 | 102 | // Assert 103 | 104 | Assert.Equal(@"{ 105 | ""a"": { 106 | ""x"": ""1"", 107 | ""y"": ""2"", 108 | ""z"": { 109 | ""t"": { 110 | ""w"": ""3"" 111 | } 112 | } 113 | }, 114 | ""b"": ""5"", 115 | ""c"": { 116 | ""a"": ""1"", 117 | ""b"": { 118 | ""t"": ""3"" 119 | } 120 | }, 121 | ""d"": ""4"" 122 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 123 | } 124 | 125 | [Fact] 126 | public void TransformJsonByIndex() 127 | { 128 | // Arrange 129 | 130 | var transform = new TransformationCollection() 131 | { 132 | { "items[2]/data", "EE" }, 133 | { "items[@i='1']/data", "FF" }, 134 | { "#items[0]", "" }, 135 | { "#items[@i='1']/x", "" }, 136 | { "items[@i='3']", "{ y: '20' }" }, 137 | { "items[0]", "{ x: '40' }" }, 138 | }; 139 | 140 | 141 | // Act 142 | 143 | var transformer = new Transformer(); 144 | string result = transformer.Transform(new JsonDocument(@"{ 145 | 'items': [ 146 | { 'i': 0, data: 'AA' }, 147 | { 'i': 1, data: 'BB', x: '25' }, 148 | { 'i': 2, data: 'CC' }, 149 | { 'i': 3, data: 'DD' }, 150 | ], 151 | 'b': '2', 152 | 'c': '3' 153 | }"), transform); 154 | 155 | 156 | // Assert 157 | 158 | Assert.Equal(@"{ 159 | ""items"": [ 160 | { 161 | ""x"": ""40"" 162 | }, 163 | { 164 | ""i"": 2, 165 | ""data"": ""EE"" 166 | }, 167 | { 168 | ""y"": ""20"" 169 | } 170 | ], 171 | ""b"": ""2"", 172 | ""c"": ""3"" 173 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 174 | } 175 | 176 | [Fact] 177 | public void TransformXml() 178 | { 179 | // Arrange 180 | 181 | var transform = new TransformationCollection() 182 | { 183 | { "xml/a/y", "2" }, 184 | { "xml/a/@y", "3" }, 185 | { "xml/a/x/@q", "9" }, 186 | { "xml/a/z/t/w", "3" }, 187 | { "xml/b", "5" }, 188 | { "xml/c/a", "1" }, 189 | { "xml/c/b", "2" }, 190 | { "xml/c/b/t", "3" }, 191 | { "xml/e/item[@key = 'item2']", "5" }, 192 | { "xml/e/item[@key=\"item3\"]", "6" }, 193 | { "xml/f/item[@key = 'item2']/val", "7" }, 194 | { "xml/f/item[@key=\"item3\"]/val", "8" }, 195 | { "xml/d", "4" }, 196 | { "#xml/g", "" }, 197 | { "xml/items[]`1", "1" }, 198 | { "xml/items[]`2", "2" }, 199 | { "xml/data[]`1", "1" }, 200 | { "xml/data[]`2", "2" }, 201 | }; 202 | 203 | 204 | // Act 205 | 206 | var transformer = new Transformer(); 207 | string result = transformer.Transform(new XmlDocument(@" 208 | 209 | 1 210 | 211 | 2 212 | 3 213 | 214 | 1 215 | 2 216 | 3 217 | 218 | 219 | 220 | 1 221 | 222 | 223 | 2 224 | 225 | 226 | 3 227 | 228 | 229 | 230 | 231 | 232 | 233 | "), transform); 234 | 235 | 236 | // Assert 237 | 238 | Assert.Equal(@" 239 | 240 | 1 241 | 2 242 | 243 | 244 | 3 245 | 246 | 247 | 248 | 5 249 | 250 | 1 251 | 252 | 3 253 | 254 | 255 | 256 | 1 257 | 5 258 | 6 259 | 260 | 261 | 262 | 1 263 | 264 | 265 | 7 266 | 267 | 268 | 8 269 | 270 | 271 | 272 | 1 273 | 2 274 | 275 | 4 276 | 277 | 1 278 | 2 279 | 280 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 281 | } 282 | 283 | [Fact] 284 | public void TransformXmlImplicit() 285 | { 286 | // Arrange 287 | 288 | var transform = new TransformationCollection("xml/g") 289 | { 290 | { "xml/a/y", "2" }, 291 | { "xml/a/@y", "3" }, 292 | { "xml/a/z/t/w", "3" }, 293 | { "xml/b", "5" }, 294 | { "xml/c/a", "1" }, 295 | { "xml/c/b", "2" }, 296 | { "xml/c/b/t", "3" }, 297 | { "xml/e/item[@key = 'item2']", "5" }, 298 | { "xml/e/item[@key=\"item3\"]", "6" }, 299 | { "xml/f/item[@key = 'item2']/val", "7" }, 300 | { "xml/f/item[@key=\"item3\"]/val", "8" }, 301 | { "xml/d", "4" }, 302 | }; 303 | 304 | 305 | // Act 306 | 307 | var transformer = new Transformer(); 308 | string result = transformer.Transform(@" 309 | 310 | 1 311 | 312 | 2 313 | 3 314 | 315 | 1 316 | 2 317 | 3 318 | 319 | 320 | 321 | 1 322 | 323 | 324 | 2 325 | 326 | 327 | 3 328 | 329 | 330 | 331 | 332 | 333 | ", transform); 334 | 335 | 336 | // Assert 337 | 338 | Assert.Equal(@" 339 | 340 | 1 341 | 2 342 | 343 | 344 | 3 345 | 346 | 347 | 348 | 5 349 | 350 | 1 351 | 352 | 3 353 | 354 | 355 | 356 | 1 357 | 5 358 | 6 359 | 360 | 361 | 362 | 1 363 | 364 | 365 | 7 366 | 367 | 368 | 8 369 | 370 | 371 | 4 372 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 373 | } 374 | 375 | [Fact] 376 | public void TransformXmlByIndex() 377 | { 378 | // Arrange 379 | 380 | var transform = new TransformationCollection() 381 | { 382 | { "info/param[1]/option", "DD" }, 383 | }; 384 | 385 | 386 | // Act 387 | 388 | var transformer = new Transformer(); 389 | string result = transformer.Transform(new XmlDocument(@" 390 | 391 | 392 | 393 | 394 | 395 | CC 396 | 397 | "), transform); 398 | 399 | 400 | // Assert 401 | 402 | Assert.Equal(@" 403 | 404 | 405 | 406 | 407 | 408 | CC 409 | 410 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 411 | } 412 | 413 | [Fact] 414 | public void TransformXmlWithNamespace() 415 | { 416 | // Arrange 417 | 418 | var transform = new TransformationCollection() 419 | { 420 | { "manifest/@android:versionCode", "10001" } 421 | }; 422 | 423 | 424 | // Act 425 | 426 | var transformer = new Transformer(); 427 | string result = transformer.Transform(new XmlDocument(@" 432 | 433 | 434 | 435 | 436 | "), transform); 437 | 438 | 439 | // Assert 440 | 441 | Assert.Equal(@" 442 | 443 | 444 | 445 | 446 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 447 | } 448 | 449 | [Fact] 450 | public void TransformYaml() 451 | { 452 | // Arrange 453 | 454 | var transform = new TransformationCollection("e") 455 | { 456 | { "a/y", "2" }, 457 | { "a/z/t/w", "3" }, 458 | { "b", "5" }, 459 | { "c/a", "1" }, 460 | { "c/b", "2" }, 461 | { "c/b/t", "3" }, 462 | { "d", "4" }, 463 | { "f[]`1", "1" }, 464 | { "f[]`2", "2" } 465 | }; 466 | 467 | 468 | // Act 469 | 470 | var transformer = new Transformer(); 471 | string result = transformer.Transform(new YamlDocument(@"a: 472 | x: 1 473 | b: 2 474 | c: 3 475 | e: 476 | x: 5"), transform); 477 | 478 | 479 | // Assert 480 | 481 | Assert.Equal(@"a: 482 | x: 1 483 | y: 2 484 | z: 485 | t: 486 | w: 3 487 | b: 5 488 | c: 489 | a: 1 490 | b: 491 | t: 3 492 | d: 4 493 | f: 494 | - 1 495 | - 2 496 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 497 | } 498 | 499 | [Fact] 500 | public void TransformYamlImplicit() 501 | { 502 | // Arrange 503 | 504 | var transform = new TransformationCollection("e") 505 | { 506 | { "a/y", "2" }, 507 | { "a/z/t/w", "3" }, 508 | { "b", "5" }, 509 | { "c/a", "1" }, 510 | { "c/b", "2" }, 511 | { "c/b/t", "3" }, 512 | { "d", "4" }, 513 | }; 514 | 515 | 516 | // Act 517 | 518 | var transformer = new Transformer(); 519 | string result = transformer.Transform(@"a: 520 | x: 1 521 | b: 2 522 | c: 3 523 | e: 524 | x: 5", transform); 525 | 526 | 527 | // Assert 528 | 529 | Assert.Equal(@"a: 530 | x: 1 531 | y: 2 532 | z: 533 | t: 534 | w: 3 535 | b: 5 536 | c: 537 | a: 1 538 | b: 539 | t: 3 540 | d: 4 541 | ", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 542 | } 543 | 544 | [Fact] 545 | public void RemoveNode() 546 | { 547 | // Arrange 548 | 549 | var transform = new TransformationCollection("e") 550 | { 551 | { "a/y", "2" }, 552 | { "a/z/t/w", "3" }, 553 | { "#b", "5" }, 554 | { "c/a", "1" }, 555 | { "c/b", "2" }, 556 | { "c/b/t", "3" }, 557 | { "d", "4" }, 558 | }; 559 | 560 | 561 | // Act 562 | 563 | var transformer = new Transformer(); 564 | string result = transformer.Transform(new JsonDocument(@"{ 565 | 'a': { 566 | 'x': '1' 567 | }, 568 | 'b': '2', 569 | 'c': '3', 570 | 'e': '4' 571 | }"), transform); 572 | 573 | 574 | // Assert 575 | 576 | Assert.Equal(@"{ 577 | ""a"": { 578 | ""x"": ""1"", 579 | ""y"": ""2"", 580 | ""z"": { 581 | ""t"": { 582 | ""w"": ""3"" 583 | } 584 | } 585 | }, 586 | ""c"": { 587 | ""a"": ""1"", 588 | ""b"": { 589 | ""t"": ""3"" 590 | } 591 | }, 592 | ""d"": ""4"" 593 | }", result, ignoreCase: true, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true); 594 | } 595 | 596 | [Fact] 597 | public void ValidateTransformationKey() 598 | {// Arrange 599 | 600 | var transform = new TransformationCollection() 601 | { 602 | { "", "1" } 603 | }; 604 | 605 | 606 | // Act 607 | 608 | var transformer = new Transformer(); 609 | ArgumentException result = Assert.Throws(() => transformer.Transform(new JsonDocument(@"{ }"), transform)); 610 | 611 | 612 | // Assert 613 | Assert.True(result.Message?.StartsWith("Transformation key is empty.")); 614 | } 615 | 616 | [Fact] 617 | public void ValidateWrongDocumentType() 618 | {// Arrange 619 | 620 | var transform = new TransformationCollection() 621 | { 622 | { "x", "1" } 623 | }; 624 | 625 | 626 | // Act 627 | 628 | var transformer = new Transformer(); 629 | ArgumentException result = Assert.Throws(() => transformer.Transform(@"{ x:y }", transform)); 630 | 631 | 632 | // Assert 633 | Assert.True(result.Message?.StartsWith("Unknown document type.")); 634 | } 635 | } 636 | } --------------------------------------------------------------------------------