diff --git a/Compile.ps1 b/Compile.ps1 index 6f7a031381..e63c4c17e0 100644 --- a/Compile.ps1 +++ b/Compile.ps1 @@ -65,7 +65,35 @@ Get-ChildItem "$workingdir\functions" -Recurse -File | ForEach-Object { Update-Progress "Adding: Config *.json" 40 Get-ChildItem "$workingdir\config" | Where-Object {$psitem.extension -eq ".json"} | ForEach-Object { $json = (Get-Content $psitem.FullName).replace("'","''") - $jsonAsObject = $json | convertfrom-json + + # Replace every XML Special Character so it'll render correctly in final build + # Only do so if json files has content to be displayed (for example the applications, tweaks, features json files) + # Make an Array List containing every name at first level of Json File + [PSCustomObject]$jsonAsObject = $json | convertfrom-json + + # Remove properties like $schema and such from the json object (we don't need it at this point) + @( + "`$schema" + ) | ForEach-Object { + $jsonAsObject.PSObject.Properties.Remove($_) | Out-Null + } + + $firstLevelJsonList = [System.Collections.ArrayList]::new() + $jsonAsObject.PSObject.Properties.Name | ForEach-Object {$null = $firstLevelJsonList.Add($_)} + # Note: + # Avoid using HTML Entity Codes, for example '”' (stands for "Right Double Quotation Mark"), + # Use **HTML decimal/hex codes instead**, as using HTML Entity Codes will result in XML parse Error when running the compiled script. + for ($i = 0; $i -lt $firstLevelJsonList.Count; $i += 1) { + $firstLevelName = $firstLevelJsonList[$i] + if ($jsonAsObject.$firstLevelName.content -ne $null) { + $jsonAsObject.$firstLevelName.content = $jsonAsObject.$firstLevelName.content.replace('&','&').replace('“','“').replace('”','”').replace("'",''').replace('<','<').replace('>','>').replace('—','—') + $jsonAsObject.$firstLevelName.content = $jsonAsObject.$firstLevelName.content.replace('''',"'") # resolves the Double Apostrophe caused by the first replace function in the main loop + } + if ($jsonAsObject.$firstLevelName.description -ne $null) { + $jsonAsObject.$firstLevelName.description = $jsonAsObject.$firstLevelName.description.replace('&','&').replace('“','“').replace('”','”').replace("'",''').replace('<','<').replace('>','>').replace('—','—') + $jsonAsObject.$firstLevelName.description = $jsonAsObject.$firstLevelName.description.replace('''',"'") # resolves the Double Apostrophe caused by the first replace function in the main loop + } + } # Add 'WPFInstall' as a prefix to every entry-name in 'applications.json' file if ($psitem.Name -eq "applications.json") { diff --git a/config/applications.json b/config/applications.json index c7ad5a7e9c..8f22a4a1b0 100644 --- a/config/applications.json +++ b/config/applications.json @@ -1,4 +1,5 @@ { + "$schema": "../schemas/config/applications.json", "1password": { "category": "Utilities", "choco": "1password", diff --git a/config/dns.json b/config/dns.json index 6c2ffbbd16..e31fa0bf75 100644 --- a/config/dns.json +++ b/config/dns.json @@ -1,4 +1,5 @@ { + "$schema": "../schemas/config/dns.json", "Google":{ "Primary": "8.8.8.8", "Secondary": "8.8.4.4", diff --git a/config/feature.json b/config/feature.json index f28a92ed64..e7e032e47b 100644 --- a/config/feature.json +++ b/config/feature.json @@ -1,4 +1,5 @@ { + "$schema": "../schemas/config/feature.json", "WPFFeaturesdotnet": { "Content": "All .Net Framework (2,3,4)", "Description": ".NET and .NET Framework is a developer platform made up of tools, programming languages, and libraries for building many different types of applications.", diff --git a/config/preset.json b/config/preset.json index 570c1f67e5..ef33a05b92 100644 --- a/config/preset.json +++ b/config/preset.json @@ -1,4 +1,5 @@ { + "$schema": "../schemas/config/preset.json", "Standard": [ "WPFTweaksAH", "WPFTweaksConsumerFeatures", diff --git a/config/themes.json b/config/themes.json index efada878a7..969491fb91 100644 --- a/config/themes.json +++ b/config/themes.json @@ -1,4 +1,5 @@ { + "$schema": "../schemas/config/themes.json", "_default": { "CustomDialogFontSize": "12", "CustomDialogFontSizeHeader": "14", diff --git a/config/tweaks.json b/config/tweaks.json index 104a70ce30..bb1967cdaa 100644 --- a/config/tweaks.json +++ b/config/tweaks.json @@ -1,4 +1,5 @@ { + "$schema": "../schemas/config/tweaks.json", "WPFTweaksAH": { "Content": "Disable Activity History", "Description": "This erases recent docs, clipboard, and run history.", diff --git a/pester/configs.Tests.ps1 b/pester/configs.Tests.ps1 index dd2db7f36d..4759b1f68d 100644 --- a/pester/configs.Tests.ps1 +++ b/pester/configs.Tests.ps1 @@ -1,81 +1,146 @@ -# Import Config Files -$global:importedconfigs = @{} -Get-ChildItem .\config | Where-Object {$_.Extension -eq ".json"} | ForEach-Object { - $global:importedconfigs[$psitem.BaseName] = Get-Content $psitem.FullName | ConvertFrom-Json -} +# Enable verbose output +$VerbosePreference = "Continue" +# Define Test-Schema function +function Test-Schema { + param ( + $Object, + $Schema + ) -#=========================================================================== -# Tests - Application Installs -#=========================================================================== + $errors = @() -Describe "Config Files" -ForEach @( - @{ - name = "applications" - config = $('{ - "winget": "value", - "choco": "value", - "category": "value", - "content": "value", - "description": "value", - "link": "value" - }' | ConvertFrom-Json) - }, - @{ - name = "tweaks" - undo = $true - } -) { - Context "$name config file" { - It "Imports with no errors" { - $global:importedconfigs.$name | should -Not -BeNullOrEmpty + $Object.PSObject.Properties | ForEach-Object { + $propName = $_.Name + $propValue = $_.Value + + $propSchema = $Schema.Properties[$propName] + if (-not $propSchema) { + $errors += "Property '$propName' is not defined in the schema" + return } - if ($config) { - It "Imports should be the correct structure" { - $applications = $global:importedconfigs.$name | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name - $template = $config | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name - $result = New-Object System.Collections.Generic.List[System.Object] - Foreach ($application in $applications) { - $compare = $global:importedconfigs.$name.$application | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name - if ($(Compare-Object $compare $template) -ne $null) { - $result.Add($application) - } - } - $result | Select-String "WPF*" | should -BeNullOrEmpty + switch ($propSchema.Type) { + "String" { + if ($propValue -isnot [string]) { + $errors += "Property '$propName' should be a string but is $($propValue.GetType())" + } + } + "Object" { + if ($propValue -isnot [PSCustomObject]) { + $errors += "Property '$propName' should be an object but is $($propValue.GetType())" + } else { + $errors += Test-Schema -Object $propValue -Schema $propSchema + } } } - if($undo) { - It "Tweaks should contain original Value" { - $tweaks = $global:importedconfigs.$name | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name - $result = New-Object System.Collections.Generic.List[System.Object] + } + + foreach ($requiredProp in $Schema.Required) { + if (-not $Object.PSObject.Properties.Name.Contains($requiredProp)) { + $errors += "Required property '$requiredProp' is missing" + } + } - foreach ($tweak in $tweaks) { - $Originals = @( - @{ - name = "registry" - value = "OriginalValue" - }, - @{ - name = "service" - value = "OriginalType" - }, - @{ - name = "ScheduledTask" - value = "OriginalState" + return $errors +} + +# Import Config Files +$global:importedConfigs = @{} +Get-ChildItem .\config -Filter *.json | ForEach-Object { + try { + $global:importedConfigs[$_.BaseName] = Get-Content $_.FullName | ConvertFrom-Json + Write-Verbose "Successfully imported config file: $($_.FullName)" + } catch { + Write-Error "Failed to import config file: $($_.FullName). Error: $_" + } +} + +Describe "Config Files Validation" { + BeforeAll { + $script:configSchemas = @{ + applications = @{ + Type = "Object" + Properties = @{ + winget = @{ Type = "String" } + choco = @{ Type = "String" } + category = @{ Type = "String" } + content = @{ Type = "String" } + description = @{ Type = "String" } + link = @{ Type = "String" } + } + Required = @("winget", "choco", "category", "content", "description", "link") + } + tweaks = @{ + Type = "Object" + Properties = @{ + registry = @{ + Type = "Object" + Properties = @{ + Path = @{ Type = "String" } + Name = @{ Type = "String" } + Type = @{ Type = "String" } + Value = @{ Type = "String" } + OriginalValue = @{ Type = "String" } } - ) - Foreach ($original in $Originals) { - $TotalCount = ($global:importedconfigs.$name.$tweak.$($original.name)).count - $OriginalCount = ($global:importedconfigs.$name.$tweak.$($original.name).$($original.value) | Where-Object {$_}).count - if($TotalCount -ne $OriginalCount) { - $result.Add("$Tweak,$($original.name)") + Required = @("Path", "Name", "Type", "Value", "OriginalValue") + } + service = @{ + Type = "Object" + Properties = @{ + Name = @{ Type = "String" } + StartupType = @{ Type = "String" } + OriginalType = @{ Type = "String" } + } + Required = @("Name", "StartupType", "OriginalType") + } + ScheduledTask = @{ + Type = "Object" + Properties = @{ + Name = @{ Type = "String" } + State = @{ Type = "String" } + OriginalState = @{ Type = "String" } } + Required = @("Name", "State", "OriginalState") } } - $result | Select-String "WPF*" | should -BeNullOrEmpty } } + } + + Context "Config File Structure" { + It "Should import all config files without errors" { + $global:importedConfigs | Should -Not -BeNullOrEmpty -Because "No config files were imported successfully" + } + + It "Should have the correct structure for all configs" { + $testSchemaScriptBlock = ${function:Test-Schema}.ToString() + + $results = $configSchemas.Keys | ForEach-Object -Parallel { + $configName = $_ + $importedConfigs = $using:global:importedConfigs + $configSchemas = $using:configSchemas + $config = $importedConfigs[$configName] + $schema = $configSchemas[$configName] + + if (-not $config) { + return "Config file '$configName' is missing or empty" + } + + $testSchemaFunction = [ScriptBlock]::Create($using:testSchemaScriptBlock) + & $testSchemaFunction -Object $config -Schema $schema + } -ThrottleLimit 4 + $results | Should -BeNullOrEmpty -Because "The following schema violations were found: $($results -join '; ')" + } } } + +# Summarize test results +$testResults = Invoke-Pester -PassThru +if ($testResults.FailedCount -gt 0) { + Write-Error "Tests failed. $($testResults.FailedCount) out of $($testResults.TotalCount) tests failed." + exit 1 +} else { + Write-Output "All tests passed successfully!" +} \ No newline at end of file diff --git a/schemas/config/applications.json b/schemas/config/applications.json new file mode 100644 index 0000000000..3e0d459cdf --- /dev/null +++ b/schemas/config/applications.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "definitions": { + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_]*$": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "category": { + "type": "string", + "enum": [ + "Utilities", + "Document", + "Pro Tools", + "Multimedia Tools", + "Development", + "Games", + "Microsoft Tools", + "Browsers", + "Communications" + ] + }, + "choco": { + "type": "string" + }, + "winget": { + "type": "string" + }, + "link": { + "$ref": "#/definitions/url" + } + }, + "required": [ + "content", + "description", + "category", + "link", + "choco", + "winget" + ], + "additionalProperties": false + } + } +} diff --git a/schemas/config/dns.json b/schemas/config/dns.json new file mode 100644 index 0000000000..fa77eed1a0 --- /dev/null +++ b/schemas/config/dns.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_]*$": { + "type": "object", + "properties": { + "Primary": { + "type": "string", + "format": "ipv4" + }, + "Secondary": { + "type": "string", + "format": "ipv4" + }, + "Primary6": { + "type": "string", + "format": "ipv6" + }, + "Secondary6": { + "type": "string", + "format": "ipv6" + } + }, + "required": ["Primary", "Secondary", "Primary6", "Secondary6"], + "additionalProperties": false + } + } +} diff --git a/schemas/config/feature.json b/schemas/config/feature.json new file mode 100644 index 0000000000..520aff40a9 --- /dev/null +++ b/schemas/config/feature.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_]*$": { + "type": "object", + "properties": { + "Content": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "category": { + "type": "string", + "enum": ["Features", "Fixes", "Legacy Windows Panels"] + }, + "panel": { + "type": "string", + "pattern": "^\\d+$" + }, + "Order": { + "type": "string", + "pattern": "^[0-9a-f]+_$" + }, + "feature": { + "type": "array", + "items": { + "type": "string" + } + }, + "InvokeScript": { + "type": "array", + "items": { + "type": "string" + } + }, + "UndoScript": { + "type": "array", + "items": { + "type": "string" + } + }, + "Type": { + "type": "string", + "enum": ["Button"] + }, + "ButtonWidth": { + "type": "string", + "pattern": "^\\d+$" + } + }, + "required": ["Content", "category", "panel"], + "additionalProperties": false + } + } +} diff --git a/schemas/config/preset.json b/schemas/config/preset.json new file mode 100644 index 0000000000..56347a2acb --- /dev/null +++ b/schemas/config/preset.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_]*$": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/schemas/config/themes.json b/schemas/config/themes.json new file mode 100644 index 0000000000..746e8040bd --- /dev/null +++ b/schemas/config/themes.json @@ -0,0 +1,297 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "color": { + "type": "string", + "pattern": "^Transparent|(#[0-9A-Fa-f]{6})$" + }, + "decimal": { + "type": "string", + "pattern": "^\\d+(\\.\\d+)?$" + } + }, + "type": "object", + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_]*$": { + "type": "object", + "properties": { + "CustomDialogFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "CustomDialogFontSizeHeader": { + "type": "string", + "pattern": "^\\d+$" + }, + "CustomDialogIconSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "CustomDialogWidth": { + "type": "string", + "pattern": "^\\d+$" + }, + "CustomDialogHeight": { + "type": "string", + "pattern": "^\\d+$" + }, + "FontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "FontFamily": { + "type": "string" + }, + "FontSizeHeading": { + "type": "string", + "pattern": "^\\d+$" + }, + "HeaderFontFamily": { + "type": "string" + }, + "CheckBoxBulletDecoratorFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "CheckBoxMargin": { + "type": "string" + }, + "TabButtonFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "TabButtonWidth": { + "type": "string", + "pattern": "^\\d+$" + }, + "TabButtonHeight": { + "type": "string", + "pattern": "^\\d+$" + }, + "TabRowHeightInPixels": { + "type": "string", + "pattern": "^\\d+$" + }, + "IconFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "IconButtonSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "WinUtilIconSize": { + "type": "string" + }, + "SettingsIconFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "MicroWinLogoSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "ProgressBarForegroundColor": { + "$ref": "#/definitions/color" + }, + "ProgressBarBackgroundColor": { + "$ref": "#/definitions/color" + }, + "ProgressBarTextColor": { + "$ref": "#/definitions/color" + }, + "ComboBoxBackgroundColor": { + "$ref": "#/definitions/color" + }, + "LabelboxForegroundColor": { + "$ref": "#/definitions/color" + }, + "MainForegroundColor": { + "$ref": "#/definitions/color" + }, + "MainBackgroundColor": { + "$ref": "#/definitions/color" + }, + "LabelBackgroundColor": { + "$ref": "#/definitions/color" + }, + "LinkForegroundColor": { + "$ref": "#/definitions/color" + }, + "LinkHoverForegroundColor": { + "$ref": "#/definitions/color" + }, + "GroupBorderBackgroundColor": { + "$ref": "#/definitions/color" + }, + "ComboBoxForegroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "ButtonFontFamily": { + "type": "string" + }, + "ButtonWidth": { + "type": "string", + "pattern": "^\\d+$" + }, + "ButtonHeight": { + "type": "string", + "pattern": "^\\d+$" + }, + "ConfigTabButtonFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "SearchBarWidth": { + "type": "string", + "pattern": "^\\d+$" + }, + "SearchBarHeight": { + "type": "string", + "pattern": "^\\d+$" + }, + "SearchBarTextBoxFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "SearchBarClearButtonFontSize": { + "type": "string", + "pattern": "^\\d+$" + }, + "ButtonInstallBackgroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonTweaksBackgroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonConfigBackgroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonUpdatesBackgroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonInstallForegroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonTweaksForegroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonConfigForegroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonUpdatesForegroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonBackgroundColor": { + "$ref": "#/definitions/color" + }, + "ButtonBackgroundPressedColor": { + "$ref": "#/definitions/color" + }, + "CheckboxMouseOverColor": { + "$ref": "#/definitions/color" + }, + "ButtonBackgroundMouseoverColor": { + "$ref": "#/definitions/color" + }, + "ButtonBackgroundSelectedColor": { + "$ref": "#/definitions/color" + }, + "ButtonForegroundColor": { + "$ref": "#/definitions/color" + }, + "ToggleButtonOnColor": { + "$ref": "#/definitions/color" + }, + "ButtonBorderThickness": { + "type": "string", + "pattern": "^\\d+$" + }, + "ButtonMargin": { + "type": "string", + "pattern": "^\\d+$" + }, + "ButtonCornerRadius": { + "type": "string", + "pattern": "^\\d+$" + }, + "BorderColor": { + "$ref": "#/definitions/color" + }, + "BorderOpacity": { + "$ref": "#/definitions/decimal" + }, + "ShadowPulse": { + "type": "string" + } + }, + "required": [ + "CustomDialogFontSize", + "CustomDialogFontSizeHeader", + "CustomDialogIconSize", + "CustomDialogWidth", + "CustomDialogHeight", + "FontSize", + "FontFamily", + "FontSizeHeading", + "HeaderFontFamily", + "CheckBoxBulletDecoratorFontSize", + "CheckBoxMargin", + "TabButtonFontSize", + "TabButtonWidth", + "TabButtonHeight", + "TabRowHeightInPixels", + "IconFontSize", + "IconButtonSize", + "WinUtilIconSize", + "SettingsIconFontSize", + "MicroWinLogoSize", + "ProgressBarForegroundColor", + "ProgressBarBackgroundColor", + "ProgressBarTextColor", + "ComboBoxBackgroundColor", + "LabelboxForegroundColor", + "MainForegroundColor", + "MainBackgroundColor", + "LabelBackgroundColor", + "LinkForegroundColor", + "LinkHoverForegroundColor", + "ComboBoxForegroundColor", + "ButtonFontSize", + "ButtonFontFamily", + "ButtonWidth", + "ButtonHeight", + "ConfigTabButtonFontSize", + "SearchBarWidth", + "SearchBarHeight", + "SearchBarTextBoxFontSize", + "SearchBarClearButtonFontSize", + "ButtonInstallBackgroundColor", + "ButtonTweaksBackgroundColor", + "ButtonConfigBackgroundColor", + "ButtonUpdatesBackgroundColor", + "ButtonInstallForegroundColor", + "ButtonTweaksForegroundColor", + "ButtonConfigForegroundColor", + "ButtonUpdatesForegroundColor", + "ButtonBackgroundColor", + "ButtonBackgroundPressedColor", + "ButtonBackgroundMouseoverColor", + "ButtonBackgroundSelectedColor", + "ButtonForegroundColor", + "ToggleButtonOnColor", + "ButtonBorderThickness", + "ButtonMargin", + "ButtonCornerRadius", + "BorderColor", + "BorderOpacity", + "ShadowPulse" + ], + "additionalProperties": false + } + } +} diff --git a/schemas/config/tweaks.json b/schemas/config/tweaks.json new file mode 100644 index 0000000000..2705dea234 --- /dev/null +++ b/schemas/config/tweaks.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "definitions": { + "toggle-state": { + "type": "string", + "enum": ["Enabled", "Disabled"] + } + }, + "patternProperties": { + "^[a-zA-Z_][a-zA-Z0-9_]*$": { + "type": "object", + "properties": { + "Content": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "category": { + "type": "string", + "enum": [ + "Essential Tweaks", + "Performance Plans", + "Customize Preferences", + "z__Advanced Tweaks - CAUTION", + "Shortcuts" + ] + }, + "panel": { + "type": "string", + "pattern": "^\\d+$" + }, + "Order": { + "type": "string", + "pattern": "^[0-9a-f]+_$" + }, + "registry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Path": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "Type": { + "type": "string" + }, + "Value": { + "type": "string" + }, + "OriginalValue": { + "type": "string" + } + }, + "required": ["Path", "Name", "Type", "Value", "OriginalValue"] + } + }, + "service": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "StartupType": { + "type": "string" + }, + "OriginalType": { + "type": "string" + } + }, + "required": ["Name", "StartupType", "OriginalType"] + } + }, + "appx": { + "type": "array", + "items": { + "type": "string" + } + }, + "ScheduledTask": { + "type":"array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "State": { + "$ref": "#/definitions/toggle-state" + }, + "OriginalState": { + "$ref": "#/definitions/toggle-state" + } + }, + "required": ["Name", "State", "OriginalState"] + } + }, + "InvokeScript": { + "type": "array", + "items": { + "type": "string" + } + }, + "UndoScript": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["Content", "Description", "category", "panel", "Order"], + "additionalProperties": false + } + } +}