doc-enforcer

0.5.4 • Public • Published

doc enforcer

This is a way to guarantee plain, human-readable, deep-linkable descriptions exist in github-compatible READMEs for arrays of strings you provide in one of 3 patterns:

  • by globbing for js files in your codebase, applying AST selectors to them, and extracting specific properties
  • by executing arbitrary code to return a string array
  • by hardcoding a string array directly

Attach these to tests to guarantee documentation, as the check fails until all items are documented.

See examples section below for ideas.

Table of Contents

Why

  • eliminating folklore and oral tradition surrounding code
  • guaranteeing support for the next dev (not you or anyone else in your current team)
  • minimizing the bus factor
  • keeping things written down
  • Unlike Socrates, you think the relative benefit of writing something down outweighs the costs

How

require('doc-enforcer')([ //a single function exported
    //which you supply an array of objects:
{ // each object often called a "pattern"
    name:""         ,// what to title this doc section
    markdownPath:"" ,// which md file this doc should go in
        // note these sections are <!--delimited-->, so N patterns
        // may exist in one file, in any order.
        // Just don't add anything except the requested
        // descriptions inside the delimiters, 
        // or they will disappear on rebuild.
    verbose:true    ,// (default: true)
        // true = log out success & fail; false = only fails
    optional:false  ,// (default: false)
        // false = throw on missing doc; true = don't
 
    // extract a string [] 1 of 3 ways:
        // 1) either from js files by globbing, ASTing, & selecting:
    ast:{
        fileGlob:"",// string passed to npmjs.org/glob to fetch js files
        selector:"",// string passed to npmjs.com/esquery to target js
            // this is how eslint works; plug your code into
            // astexplorer.net or estools.github.io/esquery
            // to understand how to make these
        property:"",// string passed to npmjs.com/lodash-getpath
            // to extract string []
            // from the AST nodes selected above
            // this way you can select a prop
            // from all items in an [] (in an [], in an [],...)
    },
        // 2) or dynamically if what you want to document isn't in js:
    customInput({glob,fs}){
            // note you're given glob & fs
            // since you'll likely be manipulating files
        return anArrayOfStrings // or a promise for one
    },
        // 3) or directly:
    customInput:['a','b','c',...],
    
    // if you want to customize how to format the doc
    // there are some presets:
    customOutput:'ul|ol|table', //default is ul
    // or a fully custom option:
    customOutput(matchedDescriptions/* = {
            [name]:{
                name, - plain string of item supplied
                formattedName, - .md form for safe anchoring & extraction
                description, - plain content of description
                formattedDescription, - .md form for safe extraction
                slug, - pattern + name for canonical referencing in a file
                linkSafeSlug, - formatted slug for github-safe anchoring
                isBlank, - whether doc is empty
                [ast context if available] - (if you gathered items this way)
            }
            ,...
        }
        */){
        // in case you prefer the doc to look a very specific way,
        // return 1 string here to write to the markdown file.
        // Log out the given param to see what you have to work with.
        // Some form of given formattedName and formattedDescription
        // per item needs to exist in the returned string
        // to preserve data across rebuilds.
        // It should let you know if it cannot find them.
    },
},
//...add as many pattern objects as you'd like
])

In case that wasn't clear, check the examples.

Examples

module.exports

Say you wanted to require readme info for keys on all module.exported objects, like:

module.exports = {
    something,
    somethingElse,
    etcetera,
    soManyThings,
}

That could look like:

require('doc-enforcer')([{
    name:"Module exports",
    markdownPath:"./readme.md",
    ast:{
        fileGlob:`${__dirname}/**/wherever/they/are/**.js`,
        selector:"AssignmentExpression[left.object.name='module'][left.property.name='exports'][right.type='ObjectExpression']",
        property:"right.properties[].key.name",
    },
}])

A Folder Structure

Say you wanted to guarantee new top level folders had mutually exclusive reasons for existing. That might look like:

require('doc-enforcer')([{
    name:"Top Level Folders",
    markdownPath:"./readme.md",
    customInput:({glob,fs})=>glob.sync(`${__dirname}/../*/`),
}])

A CSV

Say your codebase makes CSVs for others to use and you wanted to document one's header. Parse the file, extract your points of interest, return an array.

require('doc-enforcer')([{
    name:"That CSV",
    markdownPath:"./test.md",
    customInput:({glob,fs})=>
        fs.readFileSync(`${__dirname}/test.md`).toString()
            .split('\n').shift().split(/\W/)
}])

A Database Schema

Say you wanted a simple text explanation for each column added to the db. That could simply look like this:

 
require('doc-enforcer')([{
    name:"db-schema",
    markdownPath:"./readme.md",
    async customInput({glob,fs}){
        return new Promise((good,bad)=>{
            var mysql      = require('mysql');
            var connection = mysql.createConnection({
              user     : 'x',
              password : 'x',
            });
            connection.connect();
            connection.query(`
                SELECT column_name,table_name
                from information_schema.columns
                where table_schema='test'
            `,
            function (error, results, fields) {
                if (error) throw error;
                good(results.map(x=>`${x.table_name}.${x.column_name}`))
                connection.end();
            });
        })
    },
    customOutput:'table',
}])

If you also wanted each table to have a description, you'd need to alter the query to return a row per table. Or, if you wanted it to look fancier with type info & other things, see below.

A Fancier Database Schema

If you wanted to customize the output, like grouping columns by table or adding type info, you might make customInput & customOutput functions, and store some info for reuse:

var mysql=require('mysql')
var _=require('lodash')
var remember //store here things from customInput for customOutput
require('doc-enforcer')([{
    name:"db-schema",
    markdownPath:"./test.md",
    async customInput({glob,fs}){
        return new Promise((good,bad)=>{
            var mysql      = require('mysql');
            var connection = mysql.createConnection({
              user     : 'x',
              password : 'x',
            });
            connection.connect();
            connection.query(`
                select table_name,column_name,column_type
                from information_schema.columns
                where table_schema='test'
                /*
                 note you'll need the same # of rows returned
                 as descriptions to keep
                */
                union
                select table_name,'',''
                from information_schema.tables
                where table_schema='test'
            `,
            function (error, results, fields) {
                if (error) throw error;
                var tokenize=x=>`${x.table_name}.${x.column_name}`
                remember=_.keyBy(results,tokenize)//for customOutput
                good(results.map(tokenize))
                connection.end();
            });
        })
    },
    customOutput(matches){
        return `<table>${_(matches)
            .map(x=>{
                var [table_name,column_name]=x.name.split('.')
                return {...x
                    ,table_name
                    ,column_name
                    ,...remember[x.name]
                }
            })
            .groupBy('table_name')
            .map((items,tableName)=>{
                var {tables:[table],columns} = _.groupBy(items,x=>x.column_name?'columns':'tables')
                return `<tr alt="item:${table.slug}"><td>
                    ${table.formattedName
                        .replace(`.<`,'<')
                    } - ${table.formattedDescription}
                    <ul>${
                        columns.map(col=>`
                        <li alt="item:${col.slug}">${col.formattedName
                            .replace(`<code>${table.name}`,'<code>')
                        } <i>[${col.column_type}]</i> - ${col.formattedDescription}</li>`
                            ).join('')
                    }
                    </ul>
                </td></tr>`
            })
            .join('\n')}</table>`
    },
}])

(Or use something like db-linter!)

Contra

  • "just read the code, it's the best documentation"
    • Code is the excruciatingly exact explanation, but often not phrased abstractly/high level enough to be useful as an overview.
    • Also, not every consumer of it will have the skill to read it or time for the deep dive necessary.
  • "all documentation inevitably becomes out of date"
    • Documentation pointing to code that no longer exists is automatically removed in this process.
    • But ultimately that is true, especially if no one reads it because they don't know about it. Hopefully as people are forced to update readmes because of these checks, they will glance over neighboring notes for accuracy. And since it's not in some other system, devs will be near it every day.
    • Alternatively, maybe your environment has people depending on this now-easily-accessible documentation who will be more than happy to tell you first
  • "code comments are enough"
    • In-code comments have it worst: they are obscured in code files but unexecutable, and are stereotyped as "code that gets stale", given neighboring executable code doesn't. One could consider IDEs/linting which parse docblocks as "execution", but that's secondary at best.
    • This doesn't work for the same reason "just reading the code" isn't always an acceptable answer
    • Good luck relying on humans to always remember what to document
  • "why not just use jsdoc/docblocks and extract them?"
    • eslint jsdoc / doc block rules do not have a fine grain option. You could have an .eslint file in each folder that has all the files that rule should apply to, but then your folder structure is bending to a lint rule, which seems backwards, and that still will not cover cases for things not literally in your codebase.
    • copying code is bad. Even comments, from one place to another.

References

Package Sidebar

Install

npm i doc-enforcer

Weekly Downloads

3

Version

0.5.4

License

MIT

Unpacked Size

18.6 kB

Total Files

3

Last publish

Collaborators

  • jnvm