Skip to content

A PowerShell script to transform JSON settings files in a manner similar to XDT transformations of XML files.

Notifications You must be signed in to change notification settings

mckrex/azure-devops-pwsh

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 

Repository files navigation

A PowerShell Script to Transform JSON Documents

This script is intended to mimic the functionality of Microsoft's Xml Document Transformation (XDT) technology, but for settings files using JSON.

Problem

My organization had a requirement to transform JSON configurations in a manner similar to how XML configuration files are transformed using Microsoft's Xml Document Transformation (XDT) technology. In build and release pipelines running on Azure DevOps Server 2022, we had to be able to specify the values for an environment in unique settings files, include only the unique values for the given environment in that file, indicate how each setting should be changed (modify, add, or remove), and create complete settings files for each environment at build time.

Solution

The solution is a single PowerShell script file which uses JSON property names to indicate transformations. Where XDT adds attributes to indicate how to change a setting, my system uses case-sensitive verbs:

XDT

xdt:Transform="SetAttributes(value)" xdt:Locator="Match(key)"/>

Mine

"REPLACE|key": "value"

It's a simple system that can perform most of the operations performed by XDT using a single file.

The PowerShell script is intended for use in an automated build pipeline. It takes as parameters a path to a base file name (-BaseFileName) and a folder path (-FileSource), The assumption is that a settings file and its transformation files would share a common base name (e.g., “appsettings”) and be stored in a common location. There is a third parameter for an output directory (-OutputDirectory).

The most common scenario would include a directory structure that looks like this:

appsettings.json
appsettings.PROD.json
appsettings.QA.json

At build time, after the application is compiled and stored as an artifact, the PowerShell script creates a complete settings file for QA environments using the files appsettings.json and appsettings.QA.json, and a second complete settings file for production environments using appsettings.json and appsettings.PROD.json.

The use of a -BaseFileName parameter serves a special situation in our organization. Some of our applications use more than one settings file. These are applications that host multiple unique components. The main application has a settings file with common things shared by all components (like a database connection string), and additional settings files for each unique component that the main application hosts. In this case, application's settings directory has multiple groups of settings file and looks more like this:

appsettings.json
appsettings.PROD.json
appsettings.QA.json
appsettings.component1.json
appsettings.component1.PROD.json
appsettings.component1.QA.json
appsettings.component2.json
appsettings.component2.PROD.json
appsettings.component2.QA.json

The -BaseFileName parameter (“appsettings “, “appsettings.component1”, “appsettings.component2”) gives the pipeline control over what files are transformed.

The output directory is intended to hold the transformed configuration for each environment. In the above scenario, two folders – QA and PROD – would be created to hold transformed files and would be stored alongside compiled binaries in a deployment artifact.

At deployment, the configurations would be copied to a target environment from the appropriate folder. A possible improvement would be to store the transformed files together, keeping the target environment in the file name even after the completed transformation, then copying the appropriate file to the target location with a new name.

The Rabbit Hole

Each base file is altered according to the content of a transform file with a new file being saved in the output directory. The process repeats for as many transform files as are found in the source folder. The base and transform files are loaded as PSCustomObjects, then the Edit-SettingsObject function is called with both objects as parameters named -baseObject and -transformObject respectively.

In the Edit-SettingsObject function, each property of the base file is iterated. If the iterated property can be cast as a PSCustomObject (a complex object), and a complex property with a matching name is found in the transform object, Edit-SettingsObject is called recursively using those two properties.

When the iterated property is not a PSCustomObject, or a property with a matching name cannot be found, then the -transformObject property name is examined. The script looks for a pipe character in the property name. This character is used to supply information in the same way the XDT uses attributes like xdt:Transform="SetAttributes(value)". The property name is split into an array on the pipe character. The last element of the array is the property name to match to the base object; the previous elements are the transformation commands. As an example, this property in the transform object...

"REPLACE|enabled": true

...would cause the “enabled” property in the base object to be set to “true”. At this point, only one value is expected before the property name, for an array of length 2.

The allowed command values are REPLACE, MERGE, ADD, and REMOVE.

  • REPLACE: replace a property completely, including nested properties
  • MERGE: for arrays and objects; merges new values into an existing object
  • ADD: adds a property not present in the base object
  • REMOVE: removes a property from the base object

All values are case-sensitive

Consider the following JSON as a base object:

{
    "object_1": {
        "enabled": false,
        "id": "7f1ae96f-1769-4d58-b636-ca0800c6550b",
        "maximum": 1,
        "minimum": 0.125
    }
}

And the following as the transform object:

{
    "object_1": {
        "REPLACE|enabled": true,
        "id": "fc23819e-3bd1-436a-9458-c0ed46181c45",
        "REPLACE|maximum": 1.667,
        "REPLACE|minimum": 0.667,
        "ADD|allowOffCycle": true
    },
    "ADD|object_2": {
        "enabled": false,
        "id": "c18359fb-30d0-4c6d-a73f-69d1ffca73e1",
        "maximum": 2,
        "minimum": 0.5
    }
}

The function Edit-SettingsObject would iterate the base object and find the complex property called “object_1”, and it would also find a matching property in the transform object. Therefore, the function would call itself recursively with these objects as parameters.

In the recursion, the function would not find any complex objects in the base property, so to would turn to examine the transform object. It would find three properties with the REPLACE command, and one with the ADD command. The values in the base object would be overwritten by the values in the transform object, and one property would be added. Th “id” property would be ignored, even thought the value is different in both properties.

The recursion would return to the parent, and, not finding any more properties in the base object, the function will iterate the original transform object. The first property in the transform object, “object_1”, would be ignored since it does not contain pipe character. The second object, “object_2”, would be examined because the name does contain the pipe character. Since the command is ADD, the property and all it’s values would be added to the base object. The resulting output would look like this:

{
    "object_1": {
        "enabled": true,
        "id": "7f1ae96f-1769-4d58-b636-ca0800c6550b",
        "maximum": 1.667,
        "minimum": 0.667,
        "allowOffCycle": true
    },
    "object_2": {
        "enabled": false,
        "id": "c18359fb-30d0-4c6d-a73f-69d1ffca73e1",
        "maximum": 2,
        "minimum": 0.5
    }
}

About

A PowerShell script to transform JSON settings files in a manner similar to XDT transformations of XML files.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published