diff --git a/config/rollup.js b/config/rollup.js index 421d6f7..0233932 100644 --- a/config/rollup.js +++ b/config/rollup.js @@ -27,6 +27,7 @@ const bundle = { external, input: { include: [ + // join(rootPath, 'rules', '*.yml'), join(rootPath, 'src', 'index.ts'), join(rootPath, 'test', 'harness.ts'), join(rootPath, 'test', '**', 'Test*.ts'), @@ -49,7 +50,7 @@ const bundle = { return 'index'; } - if (id.includes(`${sep}src${sep}`)) { + if (id.includes(`${sep}src${sep}`) || id.includes(`${sep}rules${sep}`)) { return 'main'; } diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..6ed0dc4 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,16 @@ +# Roadmap + +## v0.9 + +- architecture: split up main into commands +- architecture: wire up dependency injection with noicejs +- feature: visitor events (#21) +- feature: improve diff messages (#133) +- feature: interactive diffs (#9, requires #21 and #133) +- feature: instantiate rules based on type (#113, requires #?) +- fix: pass structured errors +- fix: type or eliminate flash data (#137) + +## v1.0 + +- fix: all the bugs diff --git a/rules/salty-dog.yml b/rules/salty-dog.yml index b1ea6f4..e51fb02 100644 --- a/rules/salty-dog.yml +++ b/rules/salty-dog.yml @@ -1,5 +1,13 @@ name: salty-dog-meta definitions: + name-safe: + type: string + pattern: "[-a-z0-9]+" + + name-tag: + type: string + pattern: "[-:a-z0-9]+" + rule: type: object additionalProperties: false @@ -13,8 +21,7 @@ definitions: - check properties: name: - type: string - pattern: "[-a-z0-9]+" + $ref: "salty-dog-meta#/definitions/name-safe" desc: type: string minLength: 8 @@ -29,8 +36,7 @@ definitions: tags: type: array items: - type: string - pattern: "[-:a-z0-9]+" + $ref: "salty-dog-meta#/definitions/name-tag" select: type: string default: '$' @@ -40,6 +46,25 @@ definitions: check: $ref: "http://json-schema.org/draft-07/schema#" + source: + type: object + required: + - name + - rules + properties: + name: + $ref: "salty-dog-meta#/definitions/name-safe" + definitions: + type: object + additionalProperties: false + patternProperties: + "[-a-z]+": + type: object + rules: + type: array + items: + $ref: "salty-dog-meta#/definitions/rule" + rules: - name: salty-dog-rule desc: rules must be complete @@ -60,21 +85,10 @@ rules: - salty-dog check: - type: object - additionalProperties: false - required: [name, rules] - properties: - definitions: - type: object - additionalProperties: false - patternProperties: - "[-a-z]+": - type: object - name: - type: string - pattern: "[-a-z]+" - rules: - type: array - minItems: 1 - items: - $ref: "salty-dog-meta#/definitions/rule" \ No newline at end of file + allOf: + - $ref: "salty-dog-meta#/definitions/source" + - type: object + properties: + rules: + type: array + minItems: 1 \ No newline at end of file diff --git a/src/rule/index.ts b/src/rule/index.ts index a9c9f11..8435101 100644 --- a/src/rule/index.ts +++ b/src/rule/index.ts @@ -4,6 +4,7 @@ import { Minimatch } from 'minimatch'; import { LogLevel } from 'noicejs'; import recursive from 'recursive-readdir'; +import ruleSchemaData from '../../rules/salty-dog.yml'; import { YamlParser } from '../parser/YamlParser'; import { readFile } from '../source'; import { ensureArray } from '../utils'; @@ -107,6 +108,14 @@ export async function loadRuleFiles(paths: Array, ctx: VisitorContext): const docs = parser.parse(contents) as Array; for (const data of docs) { + if (!validateRules(ctx, data)) { + ctx.logger.error({ + file: data, + path, + }, 'error loading rule file'); + continue; + } + if (!isNil(data.definitions)) { ctx.addSchema(data.name, data.definitions); } @@ -147,7 +156,12 @@ export async function loadRuleModules(modules: Array, ctx: VisitorContex try { /* eslint-disable-next-line @typescript-eslint/no-var-requires */ const module: RuleSourceModule = r(name); - // TODO: ensure module has definitions, name, and rules + if (!validateRules(ctx, module)) { + ctx.logger.error({ + module: name, + }, 'error loading rule module'); + continue; + } if (!isNil(module.definitions)) { ctx.addSchema(module.name, module.definitions); @@ -193,3 +207,18 @@ export async function resolveRules(rules: Array, selector: RuleSelector): return Array.from(activeRules); } + +export function validateRules(ctx: VisitorContext, root: any): boolean { + const { definitions, name } = ruleSchemaData as any; + + const validCtx = new VisitorContext(ctx); + validCtx.addSchema(name, definitions); + const ruleSchema = validCtx.compile(definitions.source); + + if (ruleSchema(root) === true) { + return true; + } else { + ctx.logger.error({ errors: ruleSchema.errors }, 'error validating rules'); + return false; + } +} diff --git a/test/TestApp.ts b/test/TestApp.ts index 1e7e0a9..c783694 100644 --- a/test/TestApp.ts +++ b/test/TestApp.ts @@ -18,7 +18,7 @@ const TEST_FILES = { name: test, rules: [{ name: test, - desc: test, + desc: test-rule, level: info, tags: [test], check: { diff --git a/test/rule/TestLoadRule.ts b/test/rule/TestLoadRule.ts index 773cbb5..01b9b6b 100644 --- a/test/rule/TestLoadRule.ts +++ b/test/rule/TestLoadRule.ts @@ -13,6 +13,7 @@ const EXAMPLE_RULES = `{ definitions: {}, rules: [{ name: test, + desc: test-rule, level: info, tags: [] }] diff --git a/test/rule/TestRule.ts b/test/rule/TestRule.ts index 238f66a..88c42b7 100644 --- a/test/rule/TestRule.ts +++ b/test/rule/TestRule.ts @@ -1,8 +1,9 @@ import { expect } from 'chai'; -import { LogLevel } from 'noicejs'; +import { ConsoleLogger, LogLevel, NullLogger } from 'noicejs'; -import { createRuleSelector, createRuleSources, resolveRules } from '../../src/rule'; +import { createRuleSelector, createRuleSources, resolveRules, validateRules } from '../../src/rule'; import { SchemaRule } from '../../src/rule/SchemaRule'; +import { VisitorContext } from '../../src/visitor/VisitorContext'; import { describeLeaks, itLeaks } from '../helpers/async'; const TEST_RULES = [new SchemaRule({ @@ -133,3 +134,37 @@ describe('create rule selector helper', () => { expect(sources).to.have.deep.property('includeTag', []); }); }); + +describeLeaks('validate rule helper', async () => { + itLeaks('should accept valid modules', async () => { + const ctx = new VisitorContext({ + logger: ConsoleLogger.global, + schemaOptions: { + coerce: false, + defaults: false, + mutate: false, + }, + }); + + expect(validateRules(ctx, { + name: 'test', + rules: [], + })).to.equal(true); + }); + + itLeaks('should reject partial modules', async () => { + const ctx = new VisitorContext({ + logger: NullLogger.global, + schemaOptions: { + coerce: false, + defaults: false, + mutate: false, + }, + }); + + expect(validateRules(ctx, {})).to.equal(false); + expect(validateRules(ctx, { + name: '', + })).to.equal(false); + }); +});