Skip to content

Commit

Permalink
add 'extends' property for extending ruleset
Browse files Browse the repository at this point in the history
  • Loading branch information
willnorris committed Apr 14, 2021
1 parent 67ea12f commit b3f16d3
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 1 deletion.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,56 @@ rules:
...
```

### Extending Rulesets

A ruleset can extend another ruleset, in which case the two files will be
recursively merged. Extended rulesets can themselves extend additional rulesets
up to 20 rulesets deep.

Extend a ruleset by including an "extends" top-level key which identifies a URL
or file path:

```JavaScript
{
"extends": "https://raw.githubusercontent.com/todogroup/repolinter/master/rulesets/default.json"
"rules": {
# disable CI check
"integrates-with-ci": {
"level": "off"
}
}
}
```

```YAML
extends: https://raw.githubusercontent.com/todogroup/repolinter/master/rulesets/default.json
rules:
# disable CI check
integrates-with-ci
level: off
...
```

Relative paths are resolved relative to the location used to access the
extending file. For example, if repolinter is invoked as:

```
repolinter -u http://example.com/custom-rules.yaml
```

And that ruleset includes `extends: "./default.yaml"`, the path will be resolved
relative to the original URL as `http://example.com/default.yaml`. If instead
repolinter is invoked as:

```
repolinter -r /etc/repolinter/custom-rules.yaml
```

And that ruleset includes `extends: "./default.yaml"`, the path will be resolved
relative to the original file path as `/etc/repolinter/default.yaml`.

YAML and JSON rulesets can be extended from either format.

## API

Repolinter also includes an extensible JavaScript API:
Expand Down
26 changes: 25 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const fetch = require('node-fetch')
const findFile = require('find-config')
const fs = require('fs')
const jsonfile = require('jsonfile')
const lodash = require('lodash')
const path = require('path')
const yaml = require('js-yaml')

Expand Down Expand Up @@ -56,10 +57,11 @@ function findConfig(directory) {
* Load a ruleset config from the specified location.
*
* @param {string} configLocation A URL or local file containing a repolinter config file
* @param {array} [processed] List of config files already processed, used to prevent loops
* @returns {Object} The loaded repolinter json config
* @throws Will throw an error if unable to parse config or if config is invalid
*/
async function loadConfig(configLocation) {
async function loadConfig(configLocation, processed = []) {
if (!configLocation) {
throw new Error('must specify config location')
}
Expand Down Expand Up @@ -91,6 +93,28 @@ async function loadConfig(configLocation) {
}
}

// merge extended rulesets
if (ruleset.extends) {
processed.push(configLocation)
if (processed.length > 20) {
// safeguard against infinite loops. expose as flag one day if needed
throw new Error('exceeded maximum 20 ruleset extensions')
}

let parent
if (isAbsoluteURL(ruleset.extends)) {
parent = ruleset.extends
} else if (isAbsoluteURL(configLocation)) {
parent = new URL(ruleset.extends, configLocation)
} else {
parent = path.resolve(path.dirname(configLocation), ruleset.extends)
}
if (!processed.includes(parent)) {
const parentRuleset = await loadConfig(parent, processed)
ruleset = lodash.merge({}, parentRuleset, ruleset)
}
}

return ruleset
}

Expand Down
4 changes: 4 additions & 0 deletions rulesets/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"properties": {
"$schema": { "type": "string" },
"version": { "const": 2 },
"extends": {
"type": "string",
"title": "URL or path of ruleset file this ruleset extends"
},
"axioms": {
"type": "object",
"title": "The axioms schema",
Expand Down
6 changes: 6 additions & 0 deletions tests/lib/absolute-override.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$schema: "../../rulesets/schema.json"
extends: "http://localhost:9000/default.json"
version: 2
rules:
test-file-exists:
level: off
40 changes: 40 additions & 0 deletions tests/lib/config_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@ describe('lib', () => {
expect(actual.rules['test-file-exists'].level).to.equals('error')
})

it('should handle relative file extends', async () => {
const actual = await Config.loadConfig(
path.join(__dirname, 'repolinter.yaml')
)
expect(actual.rules).to.have.property('test-file-exists')
expect(actual.rules['test-file-exists'].level).to.equals('error')
})

it('should handle relative URL extends', async () => {
server.on(serveDirectory(__dirname))
const actual = await Config.loadConfig(
'http://localhost:9000/repolinter.yaml'
)
expect(actual.rules).to.have.property('test-file-exists')
expect(actual.rules['test-file-exists'].level).to.equals('error')
})

it('should handle absolute URL extends', async () => {
server.on(serveDirectory(__dirname))
const actual = await Config.loadConfig(
path.join(__dirname, 'absolute-override.yaml')
)
expect(actual.rules).to.have.property('test-file-exists')
expect(actual.rules['test-file-exists'].level).to.equals('off')
})

it('should detect loops in extended rulesets', async () => {
const loopSelf = await Config.loadConfig(
path.join(__dirname, 'loop-self.yaml')
)
expect(loopSelf.rules).to.have.property('test-file-exists')
expect(loopSelf.rules['test-file-exists'].level).to.equals('error')

const loopB = await Config.loadConfig(
path.join(__dirname, 'loop-b.yaml')
)
expect(loopB.rules).to.have.property('test-file-exists')
expect(loopB.rules['test-file-exists'].level).to.equals('off')
})

it('should throw error on non existant file', async () => {
expect(Config.loadConfig('/does-not-exist')).to.eventually.throw(
'ENOENT'
Expand Down
6 changes: 6 additions & 0 deletions tests/lib/loop-a.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$schema: "../../rulesets/schema.json"
extends: "./loop-b.yaml"
version: 2
rules:
test-file-exists:
level: error
6 changes: 6 additions & 0 deletions tests/lib/loop-b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$schema: "../../rulesets/schema.json"
extends: "./loop-a.yaml"
version: 2
rules:
test-file-exists:
level: off
6 changes: 6 additions & 0 deletions tests/lib/loop-self.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$schema: "../../rulesets/schema.json"
extends: "./loop-self.yaml"
version: 2
rules:
test-file-exists:
level: error
1 change: 1 addition & 0 deletions tests/lib/repolinter.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
$schema: "../../rulesets/schema.json"
extends: "./default.json"
version: 2

0 comments on commit b3f16d3

Please sign in to comment.