Best practices

Is important to write your rules in a way that can be automatically fixable by project-config itself without defining a fix query.

The next style file is documented showing best practices to follow when writing a style file and other helpful common patterns:

{
  rules: [
    {
      // The directory `src/` and the file `src/.keep` must exist
      // or will be created (FIXABLE):
      files: ["src/", "src/.keep"],
    },
    {
      // The file `data.json` is a file or will be created (FIXABLE):
      files: ["data.json"],

      // Using JMESPath we can query serialized files to expect
      // that certain conditions match or edit theirs contents:
      //
      JMESPathsMatch: [
        // note that you can fix a value automatically
        // querying the type of the root object `@` (FIXABLE):
        ["type(@)", "object"],

        // The manual version of this and what I need for this
        // example: initialize the file in `fix` with an empty object
        // to trigger all other assertions as errors (MANUAL FIX)
        ["null", true, "`{}`"],
        //
        // Note that I'm comparing ``null == true``, so this error
        // will be raised always. You can use this pattern to create
        // entire sections of files programatically.
        //
        // Take a look at how I'm adding a third item in the array to
        // make an edition of the file. The value returned by this
        // third JMESPath expression will be the new content of the file.

        // Specify that a value must be a certain constant (FIXABLE):
        ["string_constant", "my-string"],
        ["number_constant", 42],
        ["boolean_constant", true],
        ["null_constant", null],

        // If you need to create an intermediate object,
        // define it of type `array` or `object` (FIXABLE):
        ["type(an_object)", "object"],
        ["type(an_array)", "array"],

        // This also works lately in nested arrays of objects (FIXABLE):
        ["type(an_object.a_nested_array)", "array"],
        ["type(an_object.a_nested_object)", "object"],
        ["type(an_object.a_nested_empty_string)", "string"],
        ["type(an_object.a_nested_object.a_subnested_null)", "null"],

        // You can make your own editions manually by specifying a fix query.
        //
        // For example, here if `boolean_constant` is `true`, the value
        // of `conditional_constant` will be setted as 'enabled' (MANUAL FIX):
        [
          "boolean_constant",
          false,
          "set(@, 'conditional_constant', 'enabled')",
        ],
        // Here I'm using the function `set()` which returns the base
        // object with a field updated, 'conditional_constant' in this case
        // setted to 'enabled'.
        //
        // You can use `unset()` to delete keys from objects (MANUAL FIX):
        [
          "contains(keys(@), 'conditional_constant')",
          false,
          "unset(@, 'conditional_constant')",
          // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
          // Note that this expression will be always treated as an
          // error and fixed because the previous is setting
          // `conditional_constant` to `enabled`:
        ],

        [
          "contains(keys(@), 'conditional_constant')",
          true,
          "set(@, 'conditional_constant', 'enabled')",
        ],

        // But the previous `unset()` expression is autofixable, so is
        // not needed using `contains(keys(@), '<key>') -> false` (FIXABLE):
        ["contains(keys(@), 'conditional_constant')", false],

        // If you need to update the current object you can use `update()`:
        //                                                    (MANUAL FIX)
        [
          "boolean_constant",
          false,
          "update(@, {string_constant: 'my-string', other_string_constant: 'my-other-string', other_boolean_constant: `false`})",
        ],

        // The function `insert()` can be used to insert an element in an array.
        //
        // If `an_array` is empty, set `an_array` in the root object (`@`) to be
        // the previous `an_array` with the value `77` prepended at index 0 (MANUAL FIX):
        [
          "op(length(an_array), '>', `1`)",
          true,
          "set(@ , 'an_array', insert(an_array, `0`, `77`))",
        ],

        // With the function `deepmerge` we can make more complex updates of nested
        // nodes following different strategies, even by type:
        //
        //   - Update the root object and a the nested dictionary `an_object`
        //     using the default conservative merging strategy (MANUAL FIX):
        [
          "type(an_object.deepmerged_array)",
          "array",
          "deepmerge(@, {deepmerged_string: 'deep string', an_object: {deepmerged_array: [`1`, `2`, `3`, `4`, `[\"to-the-deep\"]`]}})",
        ],
        //
        //   - Update the root object and a the nested dictionary `an_object`
        //     using an 'always_merger' strategy, adding items to strings if the keys match
        //     and replacing previous fields like strings (MANUAL FIX):
        [
          "op(length(an_object.deepmerged_array), '<', `6`)",
          false,
          "deepmerge(@, {deepmerged_string: 'even deeper string', an_object: {deepmerged_array: [`6`, `7`, `8`]}}, 'always_merger')",
        ],
        //
        // The strategies can be defined for each type differently passing an array
        // to the third argument with an array of three arrays:
        //
        // `[ [["<type>", ["<strategy>", ...]]... ], ["<strategy>", ...], ["<strategy>", ...]]`
        //
        // For example:
        //
        // `[ [['array', ['prepend']], ['object', ['merge']]], ['override'], ['override'] ]`
        //     ^^^^^^^^^^^^^^^^^^^^^                           ^^^^^^^^^^^^^^^^^^^^^^^^^^
        //       |            |                                                     |
        // The arrays will insert new items at the beginning of previous arrays     |
        // and objects will merge the new values with the previous ones.            |
        //                                                                          |
        // For all the other types override the previous values and for types that enter
        // in conflict when merging override the previous values too.
      ],

      crossJMESPathsMatch: [
        //
        // Working with online sources
        // ---------------------------
        //
        // You can use the verb `crossJMESPathsMatch` to ensure that your data matches
        // with an online source.
        //
        // For example, if you want to ensure that the value of the key `license`
        // is OSI approved SPDX license identifier according to the official SPDX
        // repository you can use:
        [
          "license",
          [
            "gh://spdx/license-list-data@v3.25.0/json/licenses.json",
            "licenses[?isOsiApproved] | [?!isDeprecatedLicenseId].licenseId",
          ],
          "contains([1], [0])",
          true,
        ],
        // Here we are defining the file URL using a Github protocol,
        // which allows to query remote files located in Github repositories
        // with the syntax `gh://<owner>/<repository>(@v<version>)?/<path>`
        // (note that the `@v<version>` part is optional).
        //
        // So the previous expressions means that `license` from the 'data.json',
        // represented in the final `contains()` argument as `[0]`: must be inside
        // an array of OSI approved and non deprecated SPDX license identifiers
        // obtained from the online source (`[1]`), as is defined for the final
        // expected value `true`.
        //
      ],
    },
    {
      //
      // Working with text files
      // -----------------------
      //
      // Text files like `.gitignores` are serialized in an array of lines,
      // one string per line with line ends characters stripped.
      //
      files: [".gitignore"],
      //
      // You can use `includeLines` to force the inclusion of a line.
      // For example, if you want to ensure that the file `.gitignore`
      // has the line 'dist/' (FIXABLE):
      includeLines: ["dist/"],
      //
      // TIP: Use `hint` field of rules to show a better error message
      //      when the rule is not satisfied.
      hint: "Enforce the addition of the line 'dist/' in the .gitignore file.",
      //
      // Only execute this rule if the file `.project-config.toml` exists:
      ifFilesExist: [".project-config.toml"],
    },
    {
      // A line must preceed another
      // ---------------------------
      //
      files: ["data.json"],

      // If you want to ensure that a line is preceded by another in a text file,
      // you can use `crossJMESPathsMatch` with the `text` serializer.

      // For example, I want that the `dependencies` field in the 'data.json'
      // file to be defined before `devDependencies`:
      crossJMESPathsMatch: [
        [
          "@",
          [
            "data.json?text",
            "op(op(@, 'indexOf', '  \"dependencies\": {'), '<', op(@, 'indexOf', '  \"devDependencies\": {'))",
          ],
          "[1]",
          true,
        ],
      ],
      //
      // With the syntax `?<serializer>` we can use other serializer to open
      // the file, so in this example 'data.json' is opened as an array of lines
      // rather than a JSON object (`[0]` in the last pipe).
      //
      // Only execute this assertion if the file `data.json` contains
      // the lines '  "dependencies": {' and '  "devDependencies": {':
      ifIncludeLines: {
        "data.json": ['  "dependencies": {', '  "devDependencies": {'],
      },
      hint: "The field `dependencies` must preceed `devDependencies`",
    },
  ],
}
{
  "string_constant": "my-string",
  "number_constant": 42,
  "devDependencies": {
    "foo": "bar"
  },
  "dependencies": {
    "foo": "bar"
  },
  "boolean_constant": true,
  "an_object": {
    "a_nested_array": [],
    "a_nested_object": {},
    "a_nested_empty_string": "",
    "deepmerged_array": [1, 2, 3, 4, ["to-the-deep"], 6, 7, 8]
  },
  "an_array": [77, 77],
  "other_string_constant": "my-other-string",
  "other_boolean_constant": false,
  "deepmerged_string": "even deeper string",
  "conditional_constant": "enabled"
}
user@hostname:~/ rm -rf data.json src && project-config fix
data.json
  - (FIXED) JMESPath 'null' does not match. Expected True, returned None rules[1].JMESPathsMatch[0]
  - (FIXED) JMESPath 'string_constant' does not match. Expected 'my-string', returned None rules[1].JMESPathsMatch[1]
  - (FIXED) JMESPath 'number_constant' does not match. Expected 42, returned None rules[1].JMESPathsMatch[2]
  - (FIXED) JMESPath 'boolean_constant' does not match. Expected True, returned None rules[1].JMESPathsMatch[3]
  - (FIXED) JMESPath 'type(an_object)' does not match. Expected 'object', returned 'null' rules[1].JMESPathsMatch[5]
  - (FIXED) JMESPath 'type(an_array)' does not match. Expected 'array', returned 'null' rules[1].JMESPathsMatch[6]
  - (FIXED) JMESPath 'type(an_object.a_nested_array)' does not match. Expected 'array', returned 'null' rules[1].JMESPathsMatch[7]
  - (FIXED) JMESPath 'type(an_object.a_nested_object)' does not match. Expected 'object', returned 'null' rules[1].JMESPathsMatch[8]
  - (FIXED) JMESPath 'type(an_object.a_nested_empty_string)' does not match. Expected 'string', returned 'null' rules[1].JMESPathsMatch[9]
  - (FIXED) JMESPath 'boolean_constant' does not match. Expected False, returned True rules[1].JMESPathsMatch[11]
  - (FIXED) JMESPath 'contains(keys(@), 'conditional_constant')' does not match. Expected False, returned True rules[1].JMESPathsMatch[12]
  - (FIXED) JMESPath 'contains(keys(@), 'conditional_constant')' does not match. Expected True, returned False rules[1].JMESPathsMatch[13]
  - (FIXED) JMESPath 'boolean_constant' does not match. Expected False, returned True rules[1].JMESPathsMatch[14]
  - (FIXED) JMESPath 'op(length(an_array), '>', `0`)' does not match. Expected True, returned False rules[1].JMESPathsMatch[15]
  - (FIXED) JMESPath 'type(an_object.deepmerged_array)' does not match. Expected 'array', returned 'null' rules[1].JMESPathsMatch[16]
  - (FIXED) JMESPath 'op(length(an_object.deepmerged_array), '<', `6`)' does not match. Expected False, returned True rules[1].JMESPathsMatch[18]
  - JMESPath 'contains([1], [0])' does not match. Expected True, returned False rules[1].crossJMESPathsMatch[0]
  - JMESPath '[1]' does not match. Expected True, returned False rules[3].crossJMESPathsMatch[0] The field `dependencies` must preceed `devDependencies`