diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..433f656 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "env": { + "node": true + }, + "rules": { + "no-use-before-define": [2, "nofunc"], //latedef in jshint + "quotes": [2, "single"], + "no-underscore-dangle": 0, + "new-cap": [1, {"capIsNew": false}] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1d6efe --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.idea/ +coverage/ + + +npm-debug.log diff --git a/.istanbul.yml b/.istanbul.yml new file mode 100644 index 0000000..6feeb31 --- /dev/null +++ b/.istanbul.yml @@ -0,0 +1,36 @@ + +verbose: false +instrumentation: + root: . + default-excludes: true + embed-source: false + variable: __coverage__ + compact: true + preserve-comments: false + complete-copy: false + save-baseline: false + baseline-file: ./coverage/coverage-baseline.json + preload-sources: false +reporting: + print: summary + reports: + - lcov + dir: ./coverage + watermarks: + statements: [50, 80] + lines: [50, 80] + functions: [50, 80] + branches: [50, 80] + report-config: + clover: {file: clover.xml} + cobertura: {file: cobertura-coverage.xml} + json: {file: coverage-final.json} + json-summary: {file: coverage-summary.json} + lcovonly: {file: lcov.info} + teamcity: {file: null} + text: {file: null, maxCols: 0} + text-summary: {file: null} +hooks: + hook-run-in-context: false + post-require-hook: null + handle-sigint: false diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..b4c7a0b --- /dev/null +++ b/.jscsrc @@ -0,0 +1,10 @@ +{ + "preset": "google", + "maximumLineLength": 120, + "disallowSpacesInCallExpression": true, + "validateJSDoc": null, + "disallowMultipleVarDecl": null, + "requireSpacesInNamedFunctionExpression": { + "beforeOpeningCurlyBrace": true + } +} \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0c919ad --- /dev/null +++ b/.npmignore @@ -0,0 +1,12 @@ +node_modules/ +.idea/ +coverage/ +test/ +examples/ + +npm-debug.log +.eslintrc +.istanbul.yml +.jscsrc +.travis.yml +CONTRIBUTING.md diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..17932cf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - "0.10" + - "0.12" +script: "npm run travis" +notifications: + email: + - javier.mendiaracanardo@telefonica.com + - guido.garciabernardo@telefonica.com \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d946815 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing to therror-doc + +1. Make a fork + +2. Test the code! + ```sh + npm run test + ``` + +3. Check your coverage! + ```sh + npm run coverage + ``` + +4. Lint your code! + ```sh + npm run lint + ``` + +5. Try to squash your commits (optional) + +6. Make a PR to the master branch + +7. Thanks a lot! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..9ec1504 --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Copyright 2014,2015 Telefónica I+D + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ff7ead --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# therror-doc + +Documentation parser for [therror](https://github.com/therror/therror) + +[![npm version](https://badge.fury.io/js/therror-doc.svg)](http://badge.fury.io/js/therror-doc) +[![Build Status](https://travis-ci.org/therror/therror-doc.svg)](https://travis-ci.org/therror/therror-doc) +[![Coverage Status](https://coveralls.io/repos/therror/therror-doc/badge.svg?branch=master)](https://coveralls.io/r/therror/therror-doc?branch=master) + + +Creates a javascript object with all the documentation present in therror errors registrations. Once you have this, you will have the possibility to: +* Filter the errors based on your custom criterias, to make versions for developers, delivery teams, operation teams. +* Create Web pages with rich errors description +* Create Markdown files +* Anything you can imagine! + + +## Installation +```bash + npm install --save-dev therror-doc +``` + +You can also save globally and use the cli to generate docs +```bash + npm install -g therror-doc + therror-doc lib/*.js > errors.json +``` + + +## Examples + +### CLI +```bash +$ therror-doc --help + + Usage: therror-doc [options] + + Generate a json with the errors documentation for the provided files + + Options: + + -h, --help output usage information + -V, --version output the version number + + Examples: + + Get and generate error doc from js files in lib directory, output to stdout + $ therror-doc lib/*.js + Get and generate error doc from all node_module dependencies, output to file (bash) + $ therror-doc $(find node_modules -name *.js) > errors.json +``` + +Exit Codes: + * `0`: All went fine + * `1`: Something failed. Check your `stderr` for details. + +### API +Give your file paths to the parser, and it will produce an static js object ready for JSONification + +```js +var therrorDoc = require('therror-doc'); + +therrorDoc.parse([ + './server-errors.js', + './client-errors.js' + ], function onParse(err, data) { + console.log(JSON.stringify(data, null, 2)); + } +); +``` + +## Current limitations + +At this time, the parser only understands error registrations using _exactly_ an asignation of a `therror.register(...)` call. +```js +var something = therror.register('NAMESPACE', {...}); +module.exports = therror.register({...}); +therror.register({...}); +``` + +Knowing which variable holds the therror module on a file should not be difficult enought +(as the parser is using an [esprima AST](http://esprima.org/)) + + +## LICENSE + +Copyright 2014,2015 [Telefónica I+D](http://www.tid.es) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/bin/therror-doc b/bin/therror-doc new file mode 100755 index 0000000..4b1deb5 --- /dev/null +++ b/bin/therror-doc @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +/** + * @license + * Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var program = require('commander'), + pkg = require('../package.json'), + therrorDoc = require('../lib/therror-doc'), + therror = require('therror'); + +//Output errors to stderr +therror.on('create', function(err) { + console.error('%s\n %s', err.toString(), err.getURL()); +}); + +program + .version(pkg.version) + .description('Generate a json with the errors documentation for the provided files') + .usage(' [options]') + .on('--help', function() { + console.log(' Examples:'); + console.log(''); + console.log(' Get and generate error doc from js files in lib directory, output to stdout'); + console.log(' $ therror-doc lib/*.js'); + console.log(' Get and generate error doc from all node_module dependencies, output to file (bash)'); + console.log(' $ therror-doc $(find node_modules -name *.js) > errors.json'); + }); + +program.parse(process.argv); + +therrorDoc.parse(program.args, function(err, data) { + if (err) { + return fail(err); + } + + console.log(JSON.stringify(data, null, 2)); +}); + + +function fail() { + process.exit(1); +} + +function success() { + console.error.apply(console, arguments); + process.exit(0); +} + + + diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..95315fa --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var therror = require('therror'), + pkg = require('./../package.json'), + url = require('url'), + querystring = require('querystring'); + +//the Base URL for all the errors in the project, using this app version +var baseURL = url.format([ + pkg.homepage, + 'v' + pkg.version, + 'errors' + ].join('/') + '/#!/' +); + +/** + * Therror Doc generation errors + */ +module.exports = therror.register('TherrorDoc', { + /** + * The specified file cannot be opened + * + * therror-doc execution was not able to open the specified file for reading + * + * @solution check the path you provided to therror-doc is valid, the file + * exists and have the appropriate permissions to open it + * + */ + CannotRead: { + message: 'Cannot read file "{2}" for parsing errors: {1}' + }, + /** + * Unable to parse file + * + * therror-doc uses esprima project to get the error information and documentation + * directly from your source code. Maybe shipped esprima dependency is not able to + * understand your code, or we have a bug in our parsing routine + * + * @solution Check the specified file for syntax errors, and solve them. + * + */ + Parse: { + message: 'Unable to parse "{2}" while looking for errors: {1}' + } +}, { + getURL: function getURL() { + var qs = querystring.stringify(this._args + .map(function(arg) { + return this.stringify(arg); + }, this) + .reduce(function(memo, value, index) { + memo['p' + index] = value; + return memo; + }, {}) + ); + return baseURL + this._namespace + '/' + this._type + '?' + qs; + } +}); + diff --git a/lib/therror-doc.js b/lib/therror-doc.js new file mode 100644 index 0000000..631165a --- /dev/null +++ b/lib/therror-doc.js @@ -0,0 +1,330 @@ +/** + * @license + * Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var fs = require('fs'), + esprima = require('esprima'), + escodegen = require('escodegen'), + estraverse = require('estraverse'), + async = require('async'), + doctrine = require('doctrine'), + _ = require('underscore'), + errors = require('./errors'); + +function isJSDoc(comment) { + return comment.type === 'Block' && + comment.value.indexOf('*') === 0; +} + +function isTherrorRegister(node) { + return node && + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + node.callee.object.name === 'therror' && + node.callee.property.name === 'register'; +} + +function isNamespaced(node) { + return node && + node.arguments[0].type === 'Literal' && + node.arguments[1].type === 'ObjectExpression'; + +} + +/** + * Takes an AST and adds parsedComments + * Each node with comments will have 2 new properties + * - leadingComments: The raw comment + * - jsdoc: The comment analysed as a jsdoc + * + * @param {AST} syntax the original AST + * @return {AST} the new AST with comments + */ +function addComments(syntax) { + + //We attach the comments to the AST + syntax = escodegen.attachComments( + syntax, + syntax.comments, + syntax.tokens + ); + + //Taverse the AST with comments looking for comments! + estraverse.traverse(syntax, { + leave: function(node) { + if (node.leadingComments) { + //Comment found! + //We add the parsed comment to the node, + node.jsdoc = node.leadingComments + .filter(function(comment) { + //is a jsdoc comment? + return isJSDoc(comment); + }) + .map(function(comment) { + return doctrine.parse(comment.value, { + unwrap: true + }); + })[0]; //We use only the first jsdoc comment found + } + } + }); + + return syntax; +} + +function getTherrorDataFromNode(node) { + + var namespace = { + errors: [] + }, errorsProperties; + + if (isNamespaced(node)) { + namespace.namespace = node.arguments[0].value; + errorsProperties = node.arguments[1].properties; + } else { + namespace.namespace = null; + errorsProperties = node.arguments[0].properties; + } + + namespace.errors = errorsProperties.map(function(errorNode) { + var val; + + //Get the fields representation + //Cochinus maximus! + //dont wanna to traverse all possible values + //lets escodegen to do the dirty job + //skype:(devil) + eval('val = ' + escodegen.generate(errorNode.value)); // eslint-disable-line no-eval + var fields = _.map(val, function(value, key) { + return { + key: key, + value: value + }; + }); + + //attach comments to the fields + if (errorNode.value.properties) { + errorNode.value.properties.forEach(function(item, index) { + fields[index].doc = item.jsdoc || null; + }); + } + + var obj = { + type: errorNode.key.name, + fields: fields, + doc: errorNode.jsdoc || null + }; + return obj; + }); + + return namespace; +} + +function parseFileForErrors(filename, cb) { + fs.readFile(filename, {encoding: 'utf8'}, function(err, code) { + if (err) { + return cb(errors.CannotRead(err, err.path)); + } + var namespaces = []; + try { + + //We make the AST for the code + var syntax = addComments(esprima.parse(code, { + comment: true, + range: true, + tokens: true + })); + + //Traverse the AST with comments looking for comments! + estraverse.traverse(syntax, { + leave: function(node) { + var ns; + if (node.type === 'ExpressionStatement' && + isTherrorRegister(node.expression.right)) { + //module.exports = therror.register(); + + ns = getTherrorDataFromNode(node.expression.right); + ns.doc = node.jsdoc || null; + namespaces.push(ns); + } else if (node.type === 'VariableDeclaration' && + isTherrorRegister(node.declarations[0].init)) { + //var a = therror.register(); + ns = getTherrorDataFromNode(node.declarations[0].init); + ns.doc = node.jsdoc || null; + namespaces.push(ns); + } else if (node.type === 'ExpressionStatement' && + isTherrorRegister(node.expression)) { + //therror.register(); + ns = getTherrorDataFromNode(node.expression); + ns.doc = node.jsdoc || null; + namespaces.push(ns); + } + } + }); + } catch (e) { + return cb(errors.Parse(e, filename)); + } + cb(null, namespaces); + }); +} + +/** + * Sorts the errors by namespace + * + * @param {Array} data the data resulting for parsing all files + * @return {Array} the sorted namespaces + */ +function sortByNamespace(data) { + return data.sort(function compare(a, b) { + if (!b.namespace || a.namespace > b.namespace) { + return 1; + } else if (!a.namespace || a.namespace < b.namespace) { + return -1; + } else { + return 0; + } + }); +} + +/** + * Indexes a namespaces array by namespace + * + * @param {Array} data + * @return {Object} the namespaces indexed + */ +function indexNamespaces(data) { + return data.reduce(function(memo, item) { + var targetID = item.namespace || '__GLOBAL', + target = memo[targetID] || (memo[targetID] = []); + + target.push(item); + return memo; + }, {}); +} +/** + * Finds the dup namespaces in an object data + * + * @param {Object} data + * @return {Object} The duplicated namespaces indexed by namespace + */ +function findDuplicatedNamespaces(data) { + var obj = indexNamespaces(data); + + Object.keys(obj).forEach(function(key) { + if (obj[key].length <= 1) { + delete obj[key]; + } + }); + return obj; +} + +/** + * Merges the error in one namespace + * + * @param {Array} arr the array of namespaces to merge + * @return {Object} a namespace error definition + */ +function mergeNamespaces(arr) { + var obj = { + namespace: arr[0].namespace + }, + intermediateErrors = [], + intermediateTags = [], + intermediateDescription = []; + + arr.forEach(function(ns) { + if (ns.errors) { + intermediateErrors = intermediateErrors.concat(ns.errors); + } + if (ns.doc) { + if (ns.doc.tags) { + intermediateTags = intermediateTags.concat(ns.doc.tags); + } + if (ns.doc.description) { + intermediateDescription.push(ns.doc.description); + } + } + }); + + if (intermediateErrors.length) { + obj.errors = intermediateErrors; + } + + if (intermediateTags.length) { + obj.doc = {}; + obj.doc.tags = intermediateTags; + } + if (intermediateDescription.length) { + obj.doc = obj.doc || {}; + obj.doc.description = intermediateDescription.join(' '); + } + return obj; +} + +/** + * Merges namespaces removing duplicates and sorting the results + * + * @param {Array} data + * @param {Function} cb callback + */ +function merge(data, cb) { + var res = [], + ordered = sortByNamespace(data), + dups = findDuplicatedNamespaces(ordered), + indexed = indexNamespaces(ordered); + + Object.keys(dups).forEach(function(ns) { + indexed[ns] = [mergeNamespaces(dups[ns])]; + }); + + Object.keys(indexed).forEach(function(ns) { + res.push(indexed[ns][0]); + }); + + cb(null, res); +} +/** + * Analyzes the provided files searching for + * + * therror.register('namespace', { + * ERROR_ID: {} + * }); + * + * and returns in the callback an Array with all the found errors + * with their documentation + * + * @param {Array.} files + * @param {Function} cb + */ +var parse = function(files, cb) { + async.waterfall([ + async.apply(async.concat, files, parseFileForErrors), + merge + ], cb); +}; + +/** + * The exported API + * @type {Function} + */ +module.exports = { + parse: parse, + _sortByNamespace: sortByNamespace, + _findDuplicatedNamespaces: findDuplicatedNamespaces, + _mergeNamespaces: mergeNamespaces +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..a03263a --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "therror-doc", + "description": "Documentation parser for therror", + "version": "0.2.0-rc.0", + "homepage": "https://therror.github.io/therror-doc", + "author": { + "name": "Javier Mendiara Cañardo", + "email": "javier.mendiaracanardo@telefonica.com" + }, + "bin": { + "therror-doc": "./bin/therror-doc" + }, + "repository": { + "type": "git", + "url": "git://github.com/therror/therror-doc.git" + }, + "bugs": { + "url": "https://github.com/therror/therror-doc/issues" + }, + "main": "lib/therror-doc", + "engines": { + "node": ">=0.10.8" + }, + "scripts": { + "travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec test/environment.js test/*.spec.js && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", + "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha -- -R dot test/environment.js test/*.spec.js", + "lint": "jscs lib && eslint lib", + "test": "mocha -R spec test/environment.js test/*.spec.js", + "prepublish": "npm run test && npm run lint" + }, + "devDependencies": { + "chai": "^3.0.0", + "coveralls": "^2.11.2", + "eslint": "^0.23.0", + "istanbul": "^0.3.15", + "jscs": "^1.13.1", + "mocha": "^2.2.5", + "sinon": "^1.15.0", + "sinon-chai": "^2.8.0", + "therror": "^0.2.0" + }, + "keywords": [], + "dependencies": { + "async": "^1.2.1", + "commander": "^2.8.1", + "doctrine": "^0.6.4", + "escodegen": "^1.6.1", + "esprima": "^2.3.0", + "estraverse": "^4.1.0", + "therror": "^0.2.0", + "underscore": "^1.8.3" + } +} diff --git a/test/environment.js b/test/environment.js new file mode 100644 index 0000000..6c54e5c --- /dev/null +++ b/test/environment.js @@ -0,0 +1,15 @@ +var sinon = require ('sinon'), + chai = require ('chai'), + sinonChai = require('sinon-chai'); + +chai.use(sinonChai); + +global.expect = chai.expect; + +beforeEach(function(){ + global.sinon = sinon.sandbox.create(); +}); + +afterEach(function(){ + global.sinon.restore(); +}); diff --git a/test/expectations/merged.json b/test/expectations/merged.json new file mode 100644 index 0000000..e03f923 --- /dev/null +++ b/test/expectations/merged.json @@ -0,0 +1,130 @@ +[ + { + "namespace": "NS", + "errors": [ + { + "type": "ERR", + "fields": [ + { + "key": "key1", + "value": "value1", + "doc": { + "description": "The field documentation", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "key": "key2", + "value": "value2", + "doc": null + } + ], + "doc": { + "description": "ERR description", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "type": "ERR2", + "fields": [ + { + "key": "key2", + "value": "value2", + "doc": null + } + ], + "doc": null + }, + { + "type": "ERR", + "fields": [ + { + "key": "key1", + "value": "value1", + "doc": { + "description": "The field documentation", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "key": "key2", + "value": "value2", + "doc": null + } + ], + "doc": { + "description": "ERR description", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "type": "ERR3", + "fields": [ + { + "key": "key3", + "value": "value3", + "doc": null + } + ], + "doc": null + } + ], + "doc": { + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + }, + { + "title": "flag2", + "description": null + }, + { + "title": "field", + "description": "Documentation2" + } + ], + "description": "NS documentation NS documentation2" + } + } +] diff --git a/test/expectations/namespaced.json b/test/expectations/namespaced.json new file mode 100644 index 0000000..851f69f --- /dev/null +++ b/test/expectations/namespaced.json @@ -0,0 +1,71 @@ +[ + { + "errors": [ + { + "type": "ERR", + "fields": [ + { + "key": "key1", + "value": "value1", + "doc": { + "description": "The field documentation", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "key": "key2", + "value": "value2", + "doc": null + } + ], + "doc": { + "description": "ERR description", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "type": "ERR2", + "fields": [ + { + "key": "key2", + "value": "value2", + "doc": null + } + ], + "doc": null + } + ], + "namespace": "NS", + "doc": { + "description": "NS documentation", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + } +] diff --git a/test/expectations/nonamespaced.json b/test/expectations/nonamespaced.json new file mode 100644 index 0000000..c2e6859 --- /dev/null +++ b/test/expectations/nonamespaced.json @@ -0,0 +1,71 @@ +[ + { + "errors": [ + { + "type": "ERR", + "fields": [ + { + "key": "key1", + "value": "value1", + "doc": { + "description": "The field documentation", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "key": "key2", + "value": "value2", + "doc": null + } + ], + "doc": { + "description": "ERR description", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + }, + { + "type": "ERR2", + "fields": [ + { + "key": "key2", + "value": "value2", + "doc": null + } + ], + "doc": null + } + ], + "namespace": null, + "doc": { + "description": "NS documentation", + "tags": [ + { + "title": "flag", + "description": null + }, + { + "title": "field", + "description": "Documentation" + } + ] + } + } +] diff --git a/test/probes/multi.js b/test/probes/multi.js new file mode 100644 index 0000000..f123fd3 --- /dev/null +++ b/test/probes/multi.js @@ -0,0 +1,64 @@ +'use strict'; + +var therror = require('therror'); + +/** + * NS documentation + * + * @flag + * @field Documentation + */ +var a = therror.register('NS', { + /** + * ERR description + * + * @flag + * @field Documentation + */ + ERR: { + /** + * The field documentation + * + * @flag + * @field Documentation + */ + key1: 'value1', + key2: 'value2' + }, + ERR2: { + key2: 'value2' + } +}, { + method: function method() { + }, + field: 'field' +}); + + +/** + * NS documentation2 + * + * @flag2 + * @field Documentation2 + */ +var b = therror.register('NS', { + /** + * ERR description + * + * @flag + * @field Documentation + */ + ERR: { + /** + * The field documentation + * + * @flag + * @field Documentation + */ + key1: 'value1', + key2: 'value2' + }, + ERR3: { + key3: 'value3' + } +}); diff --git a/test/probes/namespaced.js b/test/probes/namespaced.js new file mode 100644 index 0000000..394341c --- /dev/null +++ b/test/probes/namespaced.js @@ -0,0 +1,43 @@ +'use strict'; + +var therror = require('therror'); + +/** + * NS documentation + * + * @flag + * @field Documentation + */ +module.exports = therror.register('NS', { + /** + * ERR description + * + * @flag + * @field Documentation + */ + ERR: { + /** + * The field documentation + * + * @flag + * @field Documentation + */ + key1: 'value1', + key2: 'value2' + }, + ERR2: { + key2: 'value2' + } +}, { + method: function method() { + }, + field: 'field' +}); + +function someOther(){ +} + +var plo = 'plo'; + +someOther(plo); + diff --git a/test/probes/namespaceddup.js b/test/probes/namespaceddup.js new file mode 100644 index 0000000..d8fa9d1 --- /dev/null +++ b/test/probes/namespaceddup.js @@ -0,0 +1,31 @@ +'use strict'; + +var therror = require('therror'); + +/** + * NS documentation2 + * + * @flag2 + * @field Documentation2 + */ +module.exports = therror.register('NS', { + /** + * ERR description + * + * @flag + * @field Documentation + */ + ERR: { + /** + * The field documentation + * + * @flag + * @field Documentation + */ + key1: 'value1', + key2: 'value2' + }, + ERR3: { + key3: 'value3' + } +}); diff --git a/test/probes/nonamespaced.js b/test/probes/nonamespaced.js new file mode 100644 index 0000000..1ee6cbb --- /dev/null +++ b/test/probes/nonamespaced.js @@ -0,0 +1,43 @@ +'use strict'; + +var therror = require('therror'); + +/** + * NS documentation + * + * @flag + * @field Documentation + */ +module.exports = therror.register({ + /** + * ERR description + * + * @flag + * @field Documentation + */ + ERR: { + /** + * The field documentation + * + * @flag + * @field Documentation + */ + key1: 'value1', + key2: 'value2' + }, + ERR2: { + key2: 'value2' + } +}, { + method: function method() { + }, + field: 'field' +}); + +function someOther(){ +} + +var plo = 'plo'; + +someOther(plo); + diff --git a/test/probes/singlecall.js b/test/probes/singlecall.js new file mode 100644 index 0000000..5c1e963 --- /dev/null +++ b/test/probes/singlecall.js @@ -0,0 +1,43 @@ +'use strict'; + +var therror = require('therror'); + +/** + * NS documentation + * + * @flag + * @field Documentation + */ +therror.register('NS', { + /** + * ERR description + * + * @flag + * @field Documentation + */ + ERR: { + /** + * The field documentation + * + * @flag + * @field Documentation + */ + key1: 'value1', + key2: 'value2' + }, + ERR2: { + key2: 'value2' + } +}, { + method: function method() { + }, + field: 'field' +}); + +function someOther(){ +} + +var plo = 'plo'; + +someOther(plo); + diff --git a/test/therror-doc.spec.js b/test/therror-doc.spec.js new file mode 100644 index 0000000..9bb68ca --- /dev/null +++ b/test/therror-doc.spec.js @@ -0,0 +1,180 @@ +'use strict'; + +var therrordoc = require('../lib/therror-doc'), + path = require('path'); + +describe('Therror Documentation parser', function() { + var probes = {}, expectations = {}; + before(function() { + probes.namespace = path.resolve(__dirname, './probes/namespaced.js'); + probes.nonamespace = path.resolve(__dirname, './probes/nonamespaced.js'); + probes.namespacedup = path.resolve(__dirname, './probes/namespaceddup.js'); + probes.multi = path.resolve(__dirname, './probes/multi.js'); + probes.singlecall = path.resolve(__dirname, './probes/singlecall.js'); + + expectations.namespace = require('./expectations/namespaced.json'); + expectations.nonamespace = require('./expectations/nonamespaced.json'); + expectations.merged = require('./expectations/merged.json'); + }); + describe('Namespace Parsing', function() { + it('should be able to parse error registrations with namespaces', function(done) { + therrordoc.parse([ + probes.namespace + ], function onParse(err, data) { + expect(data).to.be.deep.equal(expectations.namespace); + done(err); + //done(); + }); + }); + + it('should be able to parse error registrations without namespaces', function(done) { + therrordoc.parse([ + probes.nonamespace + ], function onParse(err, data) { + expect(data).to.be.deep.equal(expectations.nonamespace); + done(err); + }); + }); + + it('should be able to parse several files', function(done) { + therrordoc.parse([ + probes.namespace, + probes.nonamespace + ], function onParse(err, data) { + expect(data).to.be.deep.equal( + expectations.nonamespace.concat( + expectations.namespace + ) + ); + done(err); + }); + }); + + it('should be able to merge multiple namespaces', function(done) { + therrordoc.parse([ + probes.namespace, + probes.namespacedup + ], function(err, data) { + expect(data).to.be.deep.equal(expectations.merged); + done(err); + }); + }); + + it('should be able to identify multiple namespaces in one file', function(done) { + therrordoc.parse([ + probes.multi + ], function(err, data) { + expect(data).to.be.deep.equal(expectations.merged); + done(err); + }); + }); + + it('should be able to parse error registrations when it\'s only a call', function(done) { + therrordoc.parse([ + probes.singlecall + ], function onParse(err, data) { + expect(data).to.be.deep.equal(expectations.namespace); + done(err); + }); + }); + }); + + describe('Helper methods', function() { + it('should sort all the namespaces, putting first the null ones', function() { + var probe = [ + {namespace: 'A'}, + {namespace: null}, + {namespace: 'C'}, + {namespace: 'B'}, + {namespace: 'Aa'}, + {namespace: null}, + {namespace: 'B'}, + {namespace: 'AA'} + ]; + + expect(therrordoc._sortByNamespace(probe)).to.be.deep.equal([ + {namespace: null}, + {namespace: null}, + {namespace: 'A'}, + {namespace: 'AA'}, + {namespace: 'Aa'}, + {namespace: 'B'}, + {namespace: 'B'}, + {namespace: 'C'} + ]); + }); + + it('should find the duplicated namespaces', function() { + var probe = [ + {namespace: 'A'}, + {namespace: null}, + {namespace: 'C'}, + {namespace: 'B'}, + {namespace: 'Aa'}, + {namespace: null}, + {namespace: 'B'}, + {namespace: 'AA'} + ]; + + expect(therrordoc._findDuplicatedNamespaces(probe)).to.be.deep.equal({ + '__GLOBAL': [ + {namespace: null}, + {namespace: null} + ], + 'B': [ + {namespace: 'B'}, + {namespace: 'B'} + ] + }); + }); + + it('should merge the errors in namespaces', function() { + var probe = [ + { + namespace: 'A', + errors: [{type: 'ERR'}, {type: 'ERR2'}] + }, { + namespace: 'A', + errors: [{type: 'ERR2'}, {type: 'ERR4'}] + } + ]; + expect(therrordoc._mergeNamespaces(probe)).to.be.deep.equal({ + namespace: 'A', + errors: [ + {type: 'ERR'}, + {type: 'ERR2'}, + {type: 'ERR2'}, + {type: 'ERR4'} + ] + }); + }); + + it('should merge the documentation in namespaces', function() { + var probe = [ + { + namespace: 'A', + doc: { + description: 'foo description', + tags: [{title: 'foo'}] + } + }, { + namespace: 'A', + doc: { + description: 'bar description', + tags: [{title: 'foo'}] + } + }, { + namespace: 'A', + doc: null + } + ]; + expect(therrordoc._mergeNamespaces(probe)).to.be.deep.equal({ + namespace: 'A', + doc: { + description: 'foo description bar description', + tags: [{title: 'foo'}, {title: 'foo'}] + } + }); + }); + }); +});