Scoop icon indicating copy to clipboard operation
Scoop copied to clipboard

`formatjson.ps1` - Sort manifest root keys by `schema.json` and child keys alphabetically

Open o-l-a-v opened this issue 3 months ago β€’ 10 comments

Description

  1. Reordered root properties in schema.json so it matches the wanted order.
  2. Created new functions in manifest.ps1
    • Sort-ScoopManifestProperties:
      • Sorts root properties according to schema.json properties.
      • Sorts one level deep child properties alphabetically if the parent property is of type PSCustomObject.
      • Called by formatjson.ps1.
    • Sort-PSCustomObjectKeysRecursively:
      • Uses recursion to sort all child keys alphabetically, does not touch other values.
      • Called by Sort-ScoopManifestProperties for manifest 1 level child properties that are of type [PSCustomObject].
  3. Made formatjson.ps1 use new function Sort-ScoopManifestProperties.

Motivation and Context

Closes #6491.

How Has This Been Tested?

# Path to formatjson.ps1 in branch "formatjson-sort-root-properties"
$FunctionPath = [string] 'D:\git\ScoopInstaller--Scoop\bin\formatjson.ps1'

# Path to main bucket in branch "master" pulled 2025-09-19T12:25:00+02:00
$Directory = [string] 'D:\git\ScoopInstaller--Main\bucket'

# Run format json, look at git afterwards
$ErrorActionPreference = 'Stop'
. $FunctionPath -Dir $Directory -App '*'
image

Checklist:

Summary by CodeRabbit

  • New Features

    • Manifest formatter now deterministically orders root and first-level properties; added two manifest utilities to support recursive and root-level ordering.
    • Schema expanded with new top-level fields and updated required fields (version, homepage, license).
  • Refactor

    • More robust manifest parsing and safer formatting pipeline that writes back only when output is valid.
  • Tests

    • Added test to ensure empty-path JSON parsing does not throw.
  • Documentation

    • CHANGELOG updated.

o-l-a-v avatar Sep 19 '25 09:09 o-l-a-v

@deevus @HUMORCE @rashil2000 @z-Fng: What do you guys think?

o-l-a-v avatar Sep 19 '25 10:09 o-l-a-v

Walkthrough

Adds deterministic manifest root-property ordering and recursive PSCustomObject key sorting; updates parse_json to a Param signature and integrates sorting into bin/formatjson.ps1; annotates ConvertToPrettyJson with an output type; and updates schema.json top-level properties and required fields.

Changes

Cohort / File(s) Summary
Formatting pipeline
bin/formatjson.ps1
Reworks pipeline to parse JSON via parse_json -path, cast to PSCustomObject, apply Sort-ScoopManifestProperties (orders root and first-level properties), pretty-print via ConvertToPrettyJson, normalize tabs to four spaces, and write file only if non-empty; minor style tweaks (-not, explicit [string] cast).
Manifest utilities & parsing
lib/manifest.ps1
Converts parse_json to a Param-based signature with explicit path validation and file checks; adds Sort-PSCustomObjectKeysRecursively (recursive alphabetical sorting of PSCustomObject keys) and Sort-ScoopManifestProperties (orders root keys per schema.json, validates/filtering keys, and recursively sorts nested PSCustomObject values).
JSON helper metadata
lib/json.ps1
Adds [OutputType([string])] attribute to ConvertToPrettyJson declaration; no logic changes.
Schema reordering and updates
schema.json
Reorders and adds many top-level properties (e.g., version, description, homepage, license, notes, depends, suggest, url, hash, installer-related fields), updates references/definitions, and sets top-level required fields to version, homepage, and license.
Tests
test/Scoop-Manifest.Tests.ps1
Adds test asserting parse_json '' does not throw.
Changelog
CHANGELOG.md
Documents unreleased features: schema reorder, Sort-ScoopManifestProperties, Sort-PSCustomObjectKeysRecursively, and integration with formatjson.ps1.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant Fmt as bin/formatjson.ps1
  participant Parse as lib/manifest.ps1::parse_json
  participant SortRoot as lib/manifest.ps1::Sort-ScoopManifestProperties
  participant SortRec as lib/manifest.ps1::Sort-PSCustomObjectKeysRecursively
  participant Pretty as lib/json.ps1::ConvertToPrettyJson
  participant FS as Filesystem

  Dev->>Fmt: run formatjson with manifest path(s)
  Fmt->>Parse: parse_json -path <file>
  Parse-->>Fmt: PSCustomObject (manifest)
  Fmt->>SortRoot: Sort-ScoopManifestProperties -JsonAsObject
  SortRoot->>SortRec: recursively sort nested PSCustomObject keys
  SortRec-->>SortRoot: nested objects sorted
  SortRoot-->>Fmt: PSCustomObject (root ordered per schema)
  Fmt->>Pretty: ConvertToPrettyJson -data
  Pretty-->>Fmt: string (formatted JSON)
  Fmt->>FS: Write-Content (only if non-empty)
  FS-->>Dev: File updated if changed
  note right of SortRoot: Root order is derived from `schema.json` and validated against available schema keys

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hopped through keys and gave them names,
I nudged the roots into tidy lanes.
With whisker-sorts and carrot cheer,
I made each manifest appear clear.
JSON crumbs and tidy dreams β€” neat seams. πŸ₯•

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The pull request includes additional modifications unrelated to the feature request, such as changing the parse_json function signature and behavior, adding an output type attribute in ConvertToPrettyJson, minor stylistic refinements in formatjson.ps1, and a new test for empty-path parsing, which fall outside the specified objectives of sorting manifest properties. Consider isolating or justifying the parse_json signature update, OutputType annotation, test addition, and stylistic changes in separate pull requests so that this PR remains focused solely on sorting manifest properties.
βœ… Passed checks (4 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check βœ… Passed The title succinctly names the primary script being modified (formatjson.ps1) and clearly describes the main feature added (sorting manifest root keys according to schema.json and child keys alphabetically), matching the core changes in the pull request.
Linked Issues Check βœ… Passed This pull request fully implements the requirements from issue #6491 by establishing the desired property order in schema.json, adding Sort-ScoopManifestProperties and Sort-PSCustomObjectKeysRecursively functions in lib/manifest.ps1, and integrating the sorting logic into bin/formatjson.ps1 to enforce the canonical root-level and child-level ordering during formatting.
Docstring Coverage βœ… Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
πŸ§ͺ Generate unit tests
  • [ ] Create PR with unit tests
  • [ ] Post copyable unit tests in a comment

πŸ“œ Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 931183897b0a07969dafbb150a94d5b7167167ae and 3fef8035d92b29ff9426e75b6fd48f475b62acb9.

πŸ“’ Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • lib/manifest.ps1 (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/manifest.ps1
  • CHANGELOG.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot] avatar Sep 19 '25 10:09 coderabbitai[bot]

@coderabbitai review

o-l-a-v avatar Sep 19 '25 18:09 o-l-a-v

@coderabbitai review

z-Fng avatar Sep 22 '25 06:09 z-Fng

βœ… Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot] avatar Sep 22 '25 06:09 coderabbitai[bot]

I implemented some of the nitpicks from CodeRabbit.

  • CHANGELOG: Fixed.

bin/formatjson.ps1 (2): Use WriteAllText for a single string payload

I tested this, this does not write the final newline. So I kept it to use WriteAllLines.

lib/json.ps1: Cache wanted order; avoid hidden dependency on parse_json

Scoop does this many other places, uses it's own functions. I can change this if maintainers want it, but that would not fit with what existing code already does.

Avoid [byte] cast; use int to prevent overflow and past -1 issues

It's highly unlikely that the Scoop manifest will ever get more than 255 unique root properties. But I changed it to uint16 just in case, which will do 65535.

o-l-a-v avatar Sep 22 '25 08:09 o-l-a-v

I thought that when sorting keys like architecture.64bit/32bit , local order are used because the configuration file contains architecture.64bit/32bit. However, when sorting keys like architecture.64bit.url/hash, the configuration file lacks this entry, causing it to fall back to the global order.

As I mentioned earlier, this is the code implementation of my previous idea. Can this help us go further in a simpler way?

Just a thought.

function Get-RootPropertiesObject {
    [OutputType([PSCustomObject])]
    param()

    if (-not $Script:RootPropertiesObject) {
        $Script:RootPropertiesObject = [PSCustomObject](
            (
                parse_json -path (
                    '{0}\..\schema.json' -f $PSScriptRoot
                )
            ).'properties'
        )
    }

    return $Script:RootPropertiesObject
}

function Sort-Properties {
    param(
        [Parameter(Mandatory)]
        $Properties,
        [Parameter(Mandatory = $false)]
        [PSCustomObject] $PropertiesObjectDefault = (Get-RootPropertiesObject),
        [Parameter(Mandatory = $false)]
        [PSCustomObject] $PropertiesObjectOverride = $null
    )

    if ($Properties -is [PSCustomObject]) {

        $OrderDefault = $PropertiesObjectDefault.'PSObject'.'Properties'.'Name'
        $OrderOverride = $PropertiesObjectOverride.'PSObject'.'Properties'.'Name'

        [PSCustomObject] $PropertiesObject = if ($PropertiesObjectOverride) {
            $PropertiesObjectOverride
        } else {
            $PropertiesObjectDefault
        }

        $PropertyNames = $Properties.'PSObject'.'Properties'.'Name'

        $SortedProperties = [PSCustomObject]::new()

        $SortedPropertyNames = @($PropertyNames.Where{ $_ -in $OrderOverride } | Sort-Object { $OrderOverride.IndexOf($_) }) +
                               @($PropertyNames.Where{ ($_ -notin $OrderOverride) -and ($_ -in $OrderDefault)} | Sort-Object { $OrderDefault.IndexOf($_) }) +
                               @($PropertyNames.Where{ ($_ -notin $OrderOverride) -and ($_ -notin $OrderDefault)})

        $SortedPropertyNames | ForEach-Object {
            $SortedProperty = Sort-Properties -Properties $Properties.$_ -PropertiesObjectDefault $PropertiesObjectDefault -PropertiesObjectOverride $PropertiesObject.$_.'properties'

            if ($SortedProperty) {
                Add-Member -InputObject $SortedProperties -NotePropertyName $_ -NotePropertyValue $SortedProperty | Out-Null
            }
        }

        if ((-not $SortedProperties.PSObject.Properties) -or ($SortedProperties.PSObject.Properties.Count -eq 0)) {
            $SortedProperties = $null
        }

        return $SortedProperties
    }

    if ($Properties -is [array]) {
        $SortedProperties = @()

        for($i = 0; $i -lt $Properties.Count; ++$i) {
            $SortedProperties += @(Sort-Properties -Properties $Properties[$i] -PropertiesObjectDefault $PropertiesObjectDefault)
        }

        return ,$SortedProperties
    }

    return $Properties
}

function Get-SortedManifest {
    [OutputType([PSCustomObject])]

    param(
        [Parameter(Mandatory)]
        [ValidateScript({ $null -ne $_ -and $_ -ne [PSCustomObject]::new() })]
        [PSCustomObject] $Manifest
    )

    Sort-Properties -Properties $Manifest -PropertiesObjectDefault (Get-RootPropertiesObject)
}

z-Fng avatar Sep 24 '25 15:09 z-Fng

https://github.com/ScoopInstaller/Scoop/pull/6494#issuecomment-3329194281

I don't see how that is simpler. Also the first function won't, as PSCustomObject.properties won't output anything. Is it LLM generated?

o-l-a-v avatar Sep 24 '25 17:09 o-l-a-v

I don't see how that is simpler.

Possibly more comprehensive.

PSCustomObject.properties won't output anything

It's actually 'properties' in schema.json.

Is it LLM generated?

Yes, after editing and modifying it, it runs fine on my local machine.

z-Fng avatar Sep 24 '25 17:09 z-Fng

I don't see how that is simpler.

Possibly more comprehensive.

That's subjective. I think not using aliases, using explicit types, and using named parameters with functions are more readable, maintainable. But I'm down with whatever Scoop maintainers want, as long as we can get the functionality we want here. 😊

PSCustomObject.properties won't output anything

It's actually 'properties' in schema.json.

Isn't it <output_from_parse_json>.PSObject.Properties.Name you want?

Is it LLM generated?

Yes, after editing and modifying it, it runs fine on my local machine.

Ok.

o-l-a-v avatar Sep 25 '25 16:09 o-l-a-v