From e0ae74d40ef448575588ce739afd7ee63a01928b Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 13 Mar 2018 22:24:47 +0000 Subject: [PATCH] build(aio): add checkContentRules processor (#22759) This processor will enable us to write rules about how the content should appear, such as: * no headings in markdown content * only one sentence per line * no single character parameter names * etc. PR Close #22759 --- .../transforms/angular-base-package/index.js | 1 + .../processors/checkContentRules.js | 69 +++++++++++ .../processors/checkContentRules.spec.js | 108 ++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 aio/tools/transforms/angular-base-package/processors/checkContentRules.js create mode 100644 aio/tools/transforms/angular-base-package/processors/checkContentRules.spec.js diff --git a/aio/tools/transforms/angular-base-package/index.js b/aio/tools/transforms/angular-base-package/index.js index 75e496ec3b..01314550a2 100644 --- a/aio/tools/transforms/angular-base-package/index.js +++ b/aio/tools/transforms/angular-base-package/index.js @@ -30,6 +30,7 @@ module.exports = new Package('angular-base', [ .processor(require('./processors/fixInternalDocumentLinks')) .processor(require('./processors/copyContentAssets')) .processor(require('./processors/renderLinkInfo')) + .processor(require('./processors/checkContentRules')) // overrides base packageInfo and returns the one for the 'angular/angular' repo. .factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); }) diff --git a/aio/tools/transforms/angular-base-package/processors/checkContentRules.js b/aio/tools/transforms/angular-base-package/processors/checkContentRules.js new file mode 100644 index 0000000000..ae6f569ad1 --- /dev/null +++ b/aio/tools/transforms/angular-base-package/processors/checkContentRules.js @@ -0,0 +1,69 @@ + +/** + * A processor that can run arbitrary checking rules against properties of documents + + * The configuration for the processor is via the `docTypeRules`. + * This is a hash of docTypes to rulesets. + * Each rules set is a hash of properties to rule functions. + * + * The processor will run each rule function against each matching property of each matching doc. + * + * An example rule might look like: + * + * ``` + * function noMarkdownHeadings(doc, prop, value) { + * const match = /^\s?#+\s+.*$/m.exec(value); + * if (match) { + * return `Headings not allowed in "${prop}" property. Found "${match[0]}"`; + * } + * } + * ``` + * + */ +module.exports = function checkContentRules(log, createDocMessage) { + return { + /** + * { + * [docType]: { + * [property]: Array<(doc: Document, property: string, value: any) => string|undefined> + * } + * } + */ + docTypeRules: {}, + failOnContentErrors: false, + $runAfter: ['tags-extracted'], + $runBefore: ['processing-docs'], + $process(docs) { + const errors = []; + docs.forEach(doc => { + const docErrors = []; + const rules = this.docTypeRules[doc.docType] || {}; + if (rules) { + Object.keys(rules).forEach(property => { + const ruleFns = rules[property]; + ruleFns.forEach(ruleFn => { + const error = ruleFn(doc, property, doc[property]); + if (error) { + docErrors.push(error); + } + }); + }); + } + if (docErrors.length) { + errors.push({ doc, errors: docErrors }); + } + }); + + if (errors.length) { + log.error('Content contains errors'); + errors.forEach(docError => { + const errors = docError.errors.join('\n '); + log.error(createDocMessage(errors + '\n ', docError.doc)); + }); + if (this.failOnContentErrors) { + throw new Error('Stopping due to content errors.'); + } + } + } + }; +}; diff --git a/aio/tools/transforms/angular-base-package/processors/checkContentRules.spec.js b/aio/tools/transforms/angular-base-package/processors/checkContentRules.spec.js new file mode 100644 index 0000000000..a4ec18a16a --- /dev/null +++ b/aio/tools/transforms/angular-base-package/processors/checkContentRules.spec.js @@ -0,0 +1,108 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +describe('checkContentRules processor', function() { + let processor, logger; + + beforeEach(function() { + const dgeni = new Dgeni([testPackage('angular-base-package')]); + const injector = dgeni.configureInjector(); + processor = injector.get('checkContentRules'); + logger = injector.get('log'); + }); + + it('should exist on the injector', () => { + expect(processor).toBeDefined(); + expect(processor.$process).toEqual(jasmine.any(Function)); + }); + + it('shpuld run at the right time', () => { + expect(processor.$runAfter).toEqual(['tags-extracted']); + expect(processor.$runBefore).toEqual(['processing-docs']); + }); + + it('should do nothing if not configured', () => { + const docs = [{ docType: 'test', description: '## heading 2' }]; + processor.$process(docs); + expect(docs).toEqual([{ docType: 'test', description: '## heading 2' }]); + + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('should run configured rules against matching docs', () => { + const nameSpy1 = jasmine.createSpy('name 1'); + const nameSpy2 = jasmine.createSpy('name 2'); + const nameSpy3 = jasmine.createSpy('name 3'); + const descriptionSpy1 = jasmine.createSpy('description 1'); + const descriptionSpy2 = jasmine.createSpy('description 2'); + const descriptionSpy3 = jasmine.createSpy('description 3'); + + processor.docTypeRules = { + 'test1': { + name: [nameSpy1, nameSpy3], + description: [descriptionSpy1, descriptionSpy3] + }, + 'test2': { + name: [nameSpy2], + description: [descriptionSpy2] + } + }; + + const docs = [ + { docType: 'test1', description: 'test doc 1', name: 'test-1' }, + { docType: 'test2', description: 'test doc 2', name: 'test-2' } + ]; + processor.$process(docs); + expect(nameSpy1).toHaveBeenCalledTimes(1); + expect(nameSpy1).toHaveBeenCalledWith(docs[0], 'name', 'test-1'); + expect(nameSpy2).toHaveBeenCalledTimes(1); + expect(nameSpy2).toHaveBeenCalledWith(docs[1], 'name', 'test-2'); + expect(nameSpy3).toHaveBeenCalledTimes(1); + expect(nameSpy3).toHaveBeenCalledWith(docs[0], 'name', 'test-1'); + expect(descriptionSpy1).toHaveBeenCalledTimes(1); + expect(descriptionSpy1).toHaveBeenCalledWith(docs[0], 'description', 'test doc 1'); + expect(descriptionSpy2).toHaveBeenCalledTimes(1); + expect(descriptionSpy2).toHaveBeenCalledWith(docs[1], 'description', 'test doc 2'); + expect(descriptionSpy3).toHaveBeenCalledTimes(1); + expect(descriptionSpy3).toHaveBeenCalledWith(docs[0], 'description', 'test doc 1'); + }); + + it('should log errors if the rule returns error messages', () => { + const nameSpy1 = jasmine.createSpy('name 1').and.returnValue('name error message'); + const descriptionSpy1 = jasmine.createSpy('description 1').and.returnValue('description error message'); + + processor.docTypeRules = { + 'test1': { + name: [nameSpy1], + description: [descriptionSpy1] + } + }; + + const docs = [ + { docType: 'test1', description: 'test doc 1', name: 'test-1' }, + { docType: 'test2', description: 'test doc 2', name: 'test-2' } + ]; + + processor.$process(docs); + + expect(logger.error).toHaveBeenCalledTimes(2); + expect(logger.error).toHaveBeenCalledWith('Content contains errors'); + expect(logger.error).toHaveBeenCalledWith(`name error message + description error message + - doc "test-1" (test1) `); + }); + + it('should throw an error if `failOnContentErrors` is true and errors are found', () => { + const errorRule = jasmine.createSpy('error rule').and.returnValue('some error'); + processor.docTypeRules = { + 'test': { description: [errorRule] } + }; + processor.failOnContentErrors = true; + + const docs = [ + { docType: 'test', description: 'test doc' }, + ]; + expect(() => processor.$process(docs)).toThrowError('Stopping due to content errors.'); + }); + +});