rawmind/express-autodoc

View on GitHub
src/parser/EndpointDoc.js

Summary

Maintainability
A
0 mins
Test Coverage
A
98%
const doctrine = require("doctrine");
const { pathToRegexp } = require('path-to-regexp')

class EndpointDoc {

    constructor(comments = [], path) {
        this.ast = doctrine.parse(comments.join('\n'), { unwrap: true })
        const tags = this.ast.tags
        this.description = tags.find(t => t.title === 'description')?.description || ''
        const docParms = tags.filter(t => t.title === 'pathParam')
            .map(t => PathParam.parse(t.description))
            .reduce((acc, p) => {
                acc[p.name] = p
                return acc
            }, {})
        this.queryParams = tags.filter(t => t.title === 'queryParam')
            .map(t => QueryParam.parse(t.description))
        this.body = tags.find(t => t.title === 'body' || t.title == 'request')?.description
        this.response = tags.find(t => t.title === 'response')?.description
        this.pathParams = this.extractPathParams(path).map(paramName => `:${paramName}`).map(paramName => new PathParam(paramName).merge(docParms[paramName]))
        this.produces = tags.filter(t => t.title === 'produces').map(t => new ProducesTag(t.description))
    }

    extractPathParams(path = '') {
        const keys = []
        const normalized = this.#relaceWildchart(path)
        pathToRegexp(normalized, keys)
        return keys.map(k => k.name)
    }

    #relaceWildchart(str) {
        return str.split('*').reduce((acc, currentValue, index) => {
            if (index === 0) {
                return currentValue;
            } else {
                return acc + `{anyStringParam}${index}` + currentValue;
            }
        }, '');
    }

}

class PathParam {

    constructor(name = '', description = '') {
        this.name = name
        this.description = description
    }

    static parse(str) {
        const regex = /\((.*?)\)/g;
        const matches = [...str.matchAll(regex)];
        const tagName = matches.map(match => match[1]).find(() => true)
        return new PathParam(tagName, str.replace(`(${tagName})`, '').trim())
    }

    merge(param) {
        return new PathParam(this.name, param?.description || this.description)
    }
}

class ProducesTag {
    constructor(contentTypes) {
        this.produces = contentTypes?.split(',').map(c => c.trim())
    }
}

class QueryParam {

    constructor(name, type, required = false, description = '', defaultValue) {
        this.name = name?.trim()
        this.type = type?.trim() || 'string'
        this.required = required
        this.defaultValue = defaultValue?.trim()
        this.description = description?.trim()
    }

    static parse(str) {
        const regex = /\(([^)]+)\) ?(\{[^}]+\})? ?(.+)?/;
        const matches = str.match(regex);
        if (matches) {
            const [, paramName, options, paramDescription] = matches;
            if (options) {
                const safeOptions = options.replace('{', '')
                    .replace('}', '')
                    .split(',')
                    .reduce((acc, o) => {
                        const [key, value] = o.split(':')
                        acc[key.trim()] = value?.trim()
                        return acc
                    }, {})
                const required = safeOptions['required'] === 'true'
                const defaultValue = safeOptions['default']
                const type = safeOptions['type']
                return new QueryParam(paramName, type, required, paramDescription, defaultValue)
            }
            return new QueryParam(paramName, undefined, undefined, paramDescription)
        }
    }
}

class SwaggerPathParam {

    constructor(pathParam) {
        const pathTag = { name: pathParam.name.replace(':', ''), in: 'path', required: true, type: 'string' }
        if (pathParam.description) {
            pathTag.description = pathParam.description
        }
        this.value = pathTag
    }
}

class SwaggerBody {

    constructor(body) {
        if (body.startsWith('{')) {
            this.value = { in: 'body', name: 'body', required: true, schema: { type: 'object', example: body } }
        } else {
            this.value = { in: 'body', name: 'body', required: true, schema: { $ref: body } }
        }

    }
}

class SwaggerResponse {

    constructor(body, contentType = 'application/json') {
        if (body.startsWith('{')) {
            this.value = { content: contentType, schema: { type: 'object', example: body } }
        } else {
            this.value = { content: contentType, schema: { $ref: body } }
        }
    }
}

class SwaggerEndpointPath {

    constructor(path, pathParams) {
        this.value = this.#normalizePath(path, pathParams)
    }

    #normalizePath(path, pathParams = []) {
        // replace all path params with :param
        let normalized = path || ''
        pathParams.forEach(p => {
            normalized = normalized.replace(`:${p}`, `{${p}}`)
        })
        return normalized
    }
}

class SwaggerQueryParam {

    constructor(queryParam) {
        const pathTag = { name: queryParam.name, in: 'query', required: queryParam.required, type: queryParam.type }
        if (queryParam.description) {
            pathTag.description = queryParam.description
        }
        if (queryParam.defaultValue) {
            pathTag.default = queryParam.defaultValue
        }
        this.value = pathTag
    }
}

exports.PathParam = PathParam
exports.QueryParam = QueryParam
exports.EndpointDoc = EndpointDoc
exports.SwaggerPathParam = SwaggerPathParam
exports.SwaggerEndpointPath = SwaggerEndpointPath
exports.SwaggerQueryParam = SwaggerQueryParam
exports.ProducesTag = ProducesTag
exports.SwaggerBody = SwaggerBody
exports.SwaggerResponse = SwaggerResponse