You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					300 lines
				
				9.5 KiB
			
		
		
			
		
	
	
					300 lines
				
				9.5 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | /* eslint-disable no-multi-spaces */ | ||
|  | const indent              = '    ' | ||
|  | const branchIndent        = '│   ' | ||
|  | const midBranchIndent     = '├── ' | ||
|  | const endBranchIndent     = '└── ' | ||
|  | const wildcardDelimiter   = '*' | ||
|  | const pathDelimiter       = '/' | ||
|  | const pathRegExp          = /(?=\/)/ | ||
|  | /* eslint-enable */ | ||
|  | 
 | ||
|  | function parseFunctionName (fn) { | ||
|  |   let fName = fn.name || '' | ||
|  | 
 | ||
|  |   fName = fName.replace('bound', '').trim() | ||
|  |   fName = (fName || 'anonymous') + '()' | ||
|  |   return fName | ||
|  | } | ||
|  | 
 | ||
|  | function parseMeta (meta) { | ||
|  |   if (Array.isArray(meta)) return meta.map(m => parseMeta(m)) | ||
|  |   if (typeof meta === 'symbol') return meta.toString() | ||
|  |   if (typeof meta === 'function') return parseFunctionName(meta) | ||
|  |   return meta | ||
|  | } | ||
|  | 
 | ||
|  | function buildMetaObject (route, metaArray) { | ||
|  |   const out = {} | ||
|  |   const cleanMeta = this.buildPrettyMeta(route) | ||
|  |   if (!Array.isArray(metaArray)) metaArray = cleanMeta ? Reflect.ownKeys(cleanMeta) : [] | ||
|  |   metaArray.forEach(m => { | ||
|  |     const metaKey = typeof m === 'symbol' ? m.toString() : m | ||
|  |     if (cleanMeta && cleanMeta[m]) { | ||
|  |       out[metaKey] = parseMeta(cleanMeta[m]) | ||
|  |     } | ||
|  |   }) | ||
|  |   return out | ||
|  | } | ||
|  | 
 | ||
|  | function prettyPrintRoutesArray (routeArray, opts = {}) { | ||
|  |   if (!this.buildPrettyMeta) throw new Error('buildPrettyMeta not defined') | ||
|  |   opts.includeMeta = opts.includeMeta || null // array of meta objects to display
 | ||
|  |   const mergedRouteArray = [] | ||
|  | 
 | ||
|  |   let tree = '' | ||
|  | 
 | ||
|  |   routeArray.sort((a, b) => { | ||
|  |     if (!a.path || !b.path) return 0 | ||
|  |     return a.path.localeCompare(b.path) | ||
|  |   }) | ||
|  | 
 | ||
|  |   // merge alike paths
 | ||
|  |   for (let i = 0; i < routeArray.length; i++) { | ||
|  |     const route = routeArray[i] | ||
|  |     const pathExists = mergedRouteArray.find(r => route.path === r.path) | ||
|  |     if (pathExists) { | ||
|  |       // path already declared, add new method and break out of loop
 | ||
|  |       pathExists.handlers.push({ | ||
|  |         method: route.method, | ||
|  |         opts: route.opts.constraints || undefined, | ||
|  |         meta: opts.includeMeta ? buildMetaObject.call(this, route, opts.includeMeta) : null | ||
|  |       }) | ||
|  |       continue | ||
|  |     } | ||
|  | 
 | ||
|  |     const routeHandler = { | ||
|  |       method: route.method, | ||
|  |       opts: route.opts.constraints || undefined, | ||
|  |       meta: opts.includeMeta ? buildMetaObject.call(this, route, opts.includeMeta) : null | ||
|  |     } | ||
|  |     mergedRouteArray.push({ | ||
|  |       path: route.path, | ||
|  |       methods: [route.method], | ||
|  |       opts: [route.opts], | ||
|  |       handlers: [routeHandler] | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   // insert root level path if none defined
 | ||
|  |   if (!mergedRouteArray.filter(r => r.path === pathDelimiter).length) { | ||
|  |     const rootPath = { | ||
|  |       path: pathDelimiter, | ||
|  |       truncatedPath: '', | ||
|  |       methods: [], | ||
|  |       opts: [], | ||
|  |       handlers: [{}] | ||
|  |     } | ||
|  | 
 | ||
|  |     // if wildcard route exists, insert root level after wildcard
 | ||
|  |     if (mergedRouteArray.filter(r => r.path === wildcardDelimiter).length) { | ||
|  |       mergedRouteArray.splice(1, 0, rootPath) | ||
|  |     } else { | ||
|  |       mergedRouteArray.unshift(rootPath) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // build tree
 | ||
|  |   const routeTree = buildRouteTree(mergedRouteArray) | ||
|  | 
 | ||
|  |   // draw tree
 | ||
|  |   routeTree.forEach((rootBranch, idx) => { | ||
|  |     tree += drawBranch(rootBranch, null, idx === routeTree.length - 1, false, true) | ||
|  |     tree += '\n' // newline characters inserted at beginning of drawing function to allow for nested paths
 | ||
|  |   }) | ||
|  | 
 | ||
|  |   return tree | ||
|  | } | ||
|  | 
 | ||
|  | function buildRouteTree (mergedRouteArray, rootPath) { | ||
|  |   rootPath = rootPath || pathDelimiter | ||
|  | 
 | ||
|  |   const result = [] | ||
|  |   const temp = { result } | ||
|  |   mergedRouteArray.forEach((route, idx) => { | ||
|  |     let splitPath = route.path.split(pathRegExp) | ||
|  | 
 | ||
|  |     // add preceding slash for proper nesting
 | ||
|  |     if (splitPath[0] !== pathDelimiter) { | ||
|  |       // handle wildcard route
 | ||
|  |       if (splitPath[0] !== wildcardDelimiter) splitPath = [pathDelimiter, splitPath[0].slice(1), ...splitPath.slice(1)] | ||
|  |     } | ||
|  | 
 | ||
|  |     // build tree
 | ||
|  |     splitPath.reduce((acc, path, pidx) => { | ||
|  |       if (!acc[path]) { | ||
|  |         acc[path] = { result: [] } | ||
|  |         const pathSeg = { path, children: acc[path].result } | ||
|  | 
 | ||
|  |         if (pidx === splitPath.length - 1) pathSeg.handlers = route.handlers | ||
|  |         acc.result.push(pathSeg) | ||
|  |       } | ||
|  |       return acc[path] | ||
|  |     }, temp) | ||
|  |   }) | ||
|  | 
 | ||
|  |   // unfold root object from array
 | ||
|  |   return result | ||
|  | } | ||
|  | 
 | ||
|  | function drawBranch (pathSeg, prefix, endBranch, noPrefix, rootBranch) { | ||
|  |   let branch = '' | ||
|  | 
 | ||
|  |   if (!noPrefix && !rootBranch) branch += '\n' | ||
|  |   if (!noPrefix) branch += `${prefix || ''}${endBranch ? endBranchIndent : midBranchIndent}` | ||
|  |   branch += `${pathSeg.path}` | ||
|  | 
 | ||
|  |   if (pathSeg.handlers) { | ||
|  |     const flatHandlers = pathSeg.handlers.reduce((acc, curr) => { | ||
|  |       const match = acc.findIndex(h => JSON.stringify(h.opts) === JSON.stringify(curr.opts)) | ||
|  |       if (match !== -1) { | ||
|  |         acc[match].method = [acc[match].method, curr.method].join(', ') | ||
|  |       } else { | ||
|  |         acc.push(curr) | ||
|  |       } | ||
|  |       return acc | ||
|  |     }, []) | ||
|  | 
 | ||
|  |     flatHandlers.forEach((handler, idx) => { | ||
|  |       if (idx > 0) branch += `${noPrefix ? '' : prefix || ''}${endBranch ? indent : branchIndent}${pathSeg.path}` | ||
|  |       branch += ` (${handler.method || '-'})` | ||
|  |       if (handler.opts && JSON.stringify(handler.opts) !== '{}') branch += ` ${JSON.stringify(handler.opts)}` | ||
|  |       if (handler.meta) { | ||
|  |         Reflect.ownKeys(handler.meta).forEach((m, hidx) => { | ||
|  |           branch += `\n${noPrefix ? '' : prefix || ''}${endBranch ? indent : branchIndent}` | ||
|  |           branch += `• (${m}) ${JSON.stringify(handler.meta[m])}` | ||
|  |         }) | ||
|  |       } | ||
|  |       if (flatHandlers.length > 1 && idx !== flatHandlers.length - 1) branch += '\n' | ||
|  |     }) | ||
|  |   } else { | ||
|  |     if (pathSeg.children.length > 1) branch += ' (-)' | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!noPrefix) prefix = `${prefix || ''}${endBranch ? indent : branchIndent}` | ||
|  | 
 | ||
|  |   pathSeg.children.forEach((child, idx) => { | ||
|  |     const endBranch = idx === pathSeg.children.length - 1 | ||
|  |     const skipPrefix = (!pathSeg.handlers && pathSeg.children.length === 1) | ||
|  |     branch += drawBranch(child, prefix, endBranch, skipPrefix) | ||
|  |   }) | ||
|  | 
 | ||
|  |   return branch | ||
|  | } | ||
|  | 
 | ||
|  | function prettyPrintFlattenedNode (flattenedNode, prefix, tail, opts) { | ||
|  |   if (!this.buildPrettyMeta) throw new Error('buildPrettyMeta not defined') | ||
|  |   opts.includeMeta = opts.includeMeta || null // array of meta items to display
 | ||
|  |   let paramName = '' | ||
|  |   const printHandlers = [] | ||
|  | 
 | ||
|  |   for (const node of flattenedNode.nodes) { | ||
|  |     for (const handler of node.handlers) { | ||
|  |       printHandlers.push({ method: node.method, ...handler }) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (printHandlers.length) { | ||
|  |     printHandlers.forEach((handler, index) => { | ||
|  |       let suffix = `(${handler.method || '-'})` | ||
|  |       if (Object.keys(handler.constraints).length > 0) { | ||
|  |         suffix += ' ' + JSON.stringify(handler.constraints) | ||
|  |       } | ||
|  | 
 | ||
|  |       let name = '' | ||
|  |       // find locations of parameters in prefix
 | ||
|  |       const paramIndices = flattenedNode.prefix.split('').map((ch, idx) => ch === ':' ? idx : null).filter(idx => idx !== null) | ||
|  |       if (paramIndices.length) { | ||
|  |         let prevLoc = 0 | ||
|  |         paramIndices.forEach((loc, idx) => { | ||
|  |           // find parameter in prefix
 | ||
|  |           name += flattenedNode.prefix.slice(prevLoc, loc + 1) | ||
|  |           // insert parameters
 | ||
|  |           name += handler.params[handler.params.length - paramIndices.length + idx] | ||
|  |           if (idx === paramIndices.length - 1) name += flattenedNode.prefix.slice(loc + 1) | ||
|  |           prevLoc = loc + 1 | ||
|  |         }) | ||
|  |       } else { | ||
|  |         // there are no parameters, return full object
 | ||
|  |         name = flattenedNode.prefix | ||
|  |       } | ||
|  | 
 | ||
|  |       if (index === 0) { | ||
|  |         paramName += `${name} ${suffix}` | ||
|  |       } else { | ||
|  |         paramName += `\n${prefix}${tail ? indent : branchIndent}${name} ${suffix}` | ||
|  |       } | ||
|  |       if (opts.includeMeta) { | ||
|  |         const meta = buildMetaObject.call(this, handler, opts.includeMeta) | ||
|  |         Object.keys(meta).forEach((m, hidx) => { | ||
|  |           paramName += `\n${prefix || ''}${tail ? indent : branchIndent}` | ||
|  |           paramName += `• (${m}) ${JSON.stringify(meta[m])}` | ||
|  |         }) | ||
|  |       } | ||
|  |     }) | ||
|  |   } else { | ||
|  |     paramName = flattenedNode.prefix | ||
|  |   } | ||
|  | 
 | ||
|  |   let tree = `${prefix}${tail ? endBranchIndent : midBranchIndent}${paramName}\n` | ||
|  | 
 | ||
|  |   prefix = `${prefix}${tail ? indent : branchIndent}` | ||
|  |   const labels = Object.keys(flattenedNode.children) | ||
|  |   for (let i = 0; i < labels.length; i++) { | ||
|  |     const child = flattenedNode.children[labels[i]] | ||
|  |     tree += prettyPrintFlattenedNode.call(this, child, prefix, i === (labels.length - 1), opts) | ||
|  |   } | ||
|  |   return tree | ||
|  | } | ||
|  | 
 | ||
|  | function flattenNode (flattened, node) { | ||
|  |   if (node.handlers.length > 0) { | ||
|  |     flattened.nodes.push(node) | ||
|  |   } | ||
|  | 
 | ||
|  |   if (node.children) { | ||
|  |     for (const child of Object.values(node.children)) { | ||
|  |       // split on the slash separator but use a regex to lookahead and not actually match it, preserving it in the returned string segments
 | ||
|  |       const childPrefixSegments = child.prefix.split(pathRegExp) | ||
|  |       let cursor = flattened | ||
|  |       let parent | ||
|  |       for (const segment of childPrefixSegments) { | ||
|  |         parent = cursor | ||
|  |         cursor = cursor.children[segment] | ||
|  |         if (!cursor) { | ||
|  |           cursor = { | ||
|  |             prefix: segment, | ||
|  |             nodes: [], | ||
|  |             children: {} | ||
|  |           } | ||
|  |           parent.children[segment] = cursor | ||
|  |         } | ||
|  |       } | ||
|  |       flattenNode(cursor, child) | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function compressFlattenedNode (flattenedNode) { | ||
|  |   const childKeys = Object.keys(flattenedNode.children) | ||
|  |   if (flattenedNode.nodes.length === 0 && childKeys.length === 1) { | ||
|  |     const child = flattenedNode.children[childKeys[0]] | ||
|  |     if (child.nodes.length <= 1) { | ||
|  |       compressFlattenedNode(child) | ||
|  |       flattenedNode.nodes = child.nodes | ||
|  |       flattenedNode.prefix += child.prefix | ||
|  |       flattenedNode.children = child.children | ||
|  |       return flattenedNode | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   for (const key of Object.keys(flattenedNode.children)) { | ||
|  |     compressFlattenedNode(flattenedNode.children[key]) | ||
|  |   } | ||
|  | 
 | ||
|  |   return flattenedNode | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode, prettyPrintRoutesArray } |