diff --git a/js/.gitignore b/js/.gitignore index c1e496d91..555c4b4bb 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -1,6 +1,7 @@ node_modules npm-debug.log build +docs .build .coverage .dist diff --git a/js/package.json b/js/package.json index 46de0a0b7..37bd57266 100644 --- a/js/package.json +++ b/js/package.json @@ -29,6 +29,8 @@ "build:app": "webpack --config webpack/app", "build:lib": "webpack --config webpack/libraries", "build:dll": "webpack --config webpack/vendor", + "build:markdown": "babel-node ./scripts/build-rpc-markdown.js", + "build:json": "babel-node ./scripts/build-rpc-json.js", "ci:build": "npm run ci:build:lib && npm run ci:build:dll && npm run ci:build:app", "ci:build:app": "NODE_ENV=production webpack --config webpack/app", "ci:build:lib": "NODE_ENV=production webpack --config webpack/libraries", @@ -74,6 +76,7 @@ "chai": "3.5.0", "chai-as-promised": "6.0.0", "chai-enzyme": "0.6.1", + "chalk": "1.1.3", "circular-dependency-plugin": "2.0.0", "copy-webpack-plugin": "4.0.1", "core-js": "2.4.1", diff --git a/js/src/jsonrpc/generator/build-json.js b/js/scripts/build-rpc-json.js similarity index 95% rename from js/src/jsonrpc/generator/build-json.js rename to js/scripts/build-rpc-json.js index ddbd41c25..ff1026b83 100644 --- a/js/src/jsonrpc/generator/build-json.js +++ b/js/scripts/build-rpc-json.js @@ -17,9 +17,9 @@ import fs from 'fs'; import path from 'path'; -import interfaces from '../'; +import interfaces from '../src/jsonrpc'; -const INDEX_JSON = path.join(__dirname, '../../release/index.json'); +const INDEX_JSON = path.join(__dirname, '../release/index.json'); const methods = []; function formatDescription (obj) { diff --git a/js/scripts/build-rpc-markdown.js b/js/scripts/build-rpc-markdown.js new file mode 100644 index 000000000..97b282c97 --- /dev/null +++ b/js/scripts/build-rpc-markdown.js @@ -0,0 +1,328 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; + +import { DUMMY } from '../src/jsonrpc/helpers'; +import { BlockNumber } from '../src/jsonrpc/types'; +import interfaces from '../src/jsonrpc'; + +const ROOT_DIR = path.join(__dirname, '../docs'); + +if (!fs.existsSync(ROOT_DIR)) { + fs.mkdirSync(ROOT_DIR); +} + +const type2print = new WeakMap(); + +type2print.set(BlockNumber, 'Quantity|Tag'); + +// INFO Logging helper +function info (log) { + console.log(chalk.blue(`INFO:\t${log}`)); +} + +// WARN Logging helper +function warn (log) { + console.warn(chalk.yellow(`WARN:\t${log}`)); +} + +// ERROR Logging helper +function error (log) { + console.error(chalk.red(`ERROR:\t${log}`)); +} + +function printType (type) { + return type2print.get(type) || type.name; +} + +function formatDescription (obj, prefix = '', indent = '') { + const optional = obj.optional ? '(optional) ' : ''; + const defaults = obj.default ? `(default: \`${obj.default}\`) ` : ''; + + return `${indent}${prefix}\`${printType(obj.type)}\` - ${optional}${defaults}${obj.desc}`; +} + +function formatType (obj) { + if (obj == null) { + return obj; + } + + if (obj.type === Object && obj.details) { + const sub = Object.keys(obj.details).map((key) => { + return formatDescription(obj.details[key], `\`${key}\`: `, ' - '); + }).join('\n'); + + return `${formatDescription(obj)}\n${sub}`; + } else if (obj.type && obj.type.name) { + return formatDescription(obj); + } + + return obj; +} + +const rpcReqTemplate = { + method: 'web3_clientVersion', + params: [], + id: 1, + jsonrpc: '2.0' +}; + +// Checks if the value passed in is a DUMMY object placeholder for `{ ... }`` +function isDummy (val) { + return val === DUMMY; +} + +const { isArray } = Array; + +// Checks if the value passed is a plain old JS object +function isObject (val) { + return val != null && val.constructor === Object; +} + +// Checks if a field definition has an example, +// or describes an object with fields that recursively have examples of their own, +// or is optional. +function hasExample ({ optional, example, details } = {}) { + if (optional || example !== undefined) { + return true; + } + + if (details !== undefined) { + const values = Object.keys(details).map((key) => details[key]); + + return values.every(hasExample); + } + + return false; +} + +// Remove all optional (trailing) params without examples from an array +function removeOptionalWithoutExamples (arr) { + return arr.filter(({ optional, example, details }) => { + return !optional || example !== undefined || details !== undefined; + }); +} + +// Grabs JSON compatible +function getExample (obj) { + if (isArray(obj)) { + return removeOptionalWithoutExamples(obj).map(getExample); + } + + const { example, details } = obj; + + if (example === undefined && details !== undefined) { + const nested = {}; + + Object.keys(details).forEach((key) => { + nested[key] = getExample(details[key]); + }); + + return nested; + } + + return example; +} + +function stringifyExample (example, dent = '') { + const indent = `${dent} `; + + if (example === DUMMY) { + return '{ ... }'; + } + + if (isArray(example)) { + const last = example.length - 1; + + // If all elements are dummies, print out a single line. + // Also covers empty arrays. + if (example.every(isDummy)) { + const dummies = example.map(_ => '{ ... }'); + + return `[${dummies.join(', ')}]`; + } + + // For arrays containing just one object or string, don't unwind the array to multiline + if (last === 0 && (isObject(example[0]) || typeof example[0] === 'string')) { + return `[${stringifyExample(example[0], dent)}]`; + } + + const elements = example.map((value, index) => { + const comma = index !== last ? ',' : ''; + const comment = value != null && value._comment ? ` // ${value._comment}` : ''; + + return `${stringifyExample(value, indent)}${comma}${comment}`; + }); + + return `[\n${indent}${elements.join(`\n${indent}`)}\n${dent}]`; + } + + if (isObject(example)) { + const keys = Object.keys(example); + const last = keys.length - 1; + + // print out an empty object + if (last === -1) { + return '{}'; + } + + const elements = keys.map((key, index) => { + const value = example[key]; + const comma = index !== last ? ',' : ''; + const comment = value && value._comment ? ` // ${example[key]._comment}` : ''; + + return `${JSON.stringify(key)}: ${stringifyExample(value, indent)}${comma}${comment}`; + }); + + return `{\n${indent}${elements.join(`\n${indent}`)}\n${dent}}`; + } + + return JSON.stringify(example); // .replace(/"\$DUMMY\$"/g, '{ ... }'); +} + +function buildExample (name, method) { + // deprecated, don't care + if (method.deprecated) { + return ''; + } + + const logPostfix = method.subdoc ? ` (${method.subdoc})` : ''; + + const hasReqExample = method.params.every(hasExample); + const hasResExample = hasExample(method.returns); + + if (!hasReqExample && !hasResExample) { + error(`${name} has no examples${logPostfix}`); + + return ''; + } + + const examples = []; + + if (hasReqExample) { + const params = getExample(method.params); + const req = JSON.stringify(Object.assign({}, rpcReqTemplate, { method: name, params })).replace(/"\$DUMMY\$"/g, '{ ... }'); + + examples.push(`Request\n\`\`\`bash\ncurl --data '${req}' -H "Content-Type: application/json" -X POST localhost:8545\n\`\`\``); + } else { + warn(`${name} has a response example but not a request example${logPostfix}`); + } + + if (hasResExample) { + const res = stringifyExample({ + id: 1, + jsonrpc: '2.0', + result: getExample(method.returns) + }); + + examples.push(`Response\n\`\`\`js\n${res}\n\`\`\``); + } else { + if (typeof method.returns === 'string') { + info(`${name} has a request example and only text description for response${logPostfix}`); + } else { + warn(`${name} has a request example but not a response example${logPostfix}`); + } + } + + return `\n\n#### Example\n\n${examples.join('\n\n')}`; +} + +function buildParameters (params) { + if (params.length === 0) { + return ''; + } + + let md = `0. ${params.map(formatType).join('\n0. ')}`; + + if (params.length > 0 && params.every(hasExample) && params[0].example !== DUMMY) { + const example = getExample(params); + + md = `${md}\n\n\`\`\`js\nparams: ${stringifyExample(example)}\n\`\`\``; + } + + return md; +} + +Object.keys(interfaces).sort().forEach((group) => { + const spec = interfaces[group]; + + for (const key in spec) { + const method = spec[key]; + + if (!method || !method.subdoc) { + continue; + } + + const subgroup = `${group}_${method.subdoc}`; + + interfaces[subgroup] = interfaces[subgroup] || {}; + + interfaces[subgroup][key] = method; + delete spec[key]; + } +}); + +Object.keys(interfaces).sort().forEach((group) => { + let preamble = `# The \`${group}\` Module`; + let markdown = `## JSON-RPC methods\n`; + + const spec = interfaces[group]; + + if (spec._preamble) { + preamble = `${preamble}\n\n${spec._preamble}`; + } + + const content = []; + const tocMain = []; + const tocSections = {}; + + Object.keys(spec).sort().forEach((iname) => { + const method = spec[iname]; + const name = `${group.replace(/_.*$/, '')}_${iname}`; + + if (method.nodoc || method.deprecated) { + info(`Skipping ${name}: ${method.nodoc || 'Deprecated'}`); + + return; + } + + const desc = method.desc; + const params = buildParameters(method.params); + const returns = `- ${formatType(method.returns)}`; + const example = buildExample(name, method); + + const { section } = method; + const toc = section ? tocSections[section] = tocSections[section] || [] : tocMain; + + toc.push(`- [${name}](#${name.toLowerCase()})`); + content.push(`### ${name}\n\n${desc}\n\n#### Parameters\n\n${params || 'None'}\n\n#### Returns\n\n${returns || 'None'}${example}`); + }); + + markdown = `${markdown}\n${tocMain.join('\n')}`; + + Object.keys(tocSections).sort().forEach((section) => { + markdown = `${markdown}\n\n#### ${section}\n${tocSections[section].join('\n')}`; + }); + + markdown = `${markdown}\n\n## JSON-RPC API Reference\n\n${content.join('\n\n***\n\n')}\n\n`; + + const mdFile = path.join(ROOT_DIR, `${group}.md`); + + fs.writeFileSync(mdFile, `${preamble}\n\n${markdown}`, 'utf8'); +}); diff --git a/js/src/jsonrpc/README.md b/js/src/jsonrpc/README.md index 8f16ec3ca..6a871c68c 100644 --- a/js/src/jsonrpc/README.md +++ b/js/src/jsonrpc/README.md @@ -12,9 +12,10 @@ JSON file of all ethereum's rpc methods supported by parity 0. Branch 0. Add the missing interfaces only into `src/interfaces/*.js` 0. Parameters (array) & Returns take objects of type - - `{ type: [Array|Boolean|Object|String|...], desc: 'some description' }` + - `{ type: [Array|Boolean|Object|String|...], desc: 'some description', example: 100|'0xff'|{ ... } }` - Types are built-in JS types or those defined in `src/types.js` (e.g. `BlockNumber`, `Quantity`, etc.) - If a formatter is required, add it as `format: 'string-type'` -0. Run the lint & tests, `npm run lint && npm run testOnce` -0. Generate via `npm run build` which outputs `index.js`, `index.json` & `interfaces.md` (Only required until Travis is fully in-place) -0. Check-in and make a PR +0. Run the lint & tests, `npm run lint && npm run test` +0. Generate via `npm run build` which outputs `index.js` & `index.json`. +0. (optional) Generate docs via `npm run build:markdown` which outputs `md` files to `./docs`. +0. Check-in and make a PR. diff --git a/js/src/jsonrpc/generator/build-markdown.js b/js/src/jsonrpc/generator/build-markdown.js deleted file mode 100644 index 101c8f3cf..000000000 --- a/js/src/jsonrpc/generator/build-markdown.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -import fs from 'fs'; -import path from 'path'; - -import interfaces from '../'; - -const MARKDOWN = path.join(__dirname, '../../release/interfaces.md'); - -let preamble = '# interfaces\n'; -let markdown = ''; - -function formatDescription (obj, prefix = '', indent = '') { - const optional = obj.optional ? '(optional) ' : ''; - const defaults = obj.default ? `(default: ${obj.default}) ` : ''; - - return `${indent}- ${prefix}\`${obj.type.name}\` - ${optional}${defaults}${obj.desc}`; -} - -function formatType (obj) { - if (obj.type === Object && obj.details) { - const sub = Object.keys(obj.details).sort().map((key) => { - return formatDescription(obj.details[key], `\`${key}\`/`, ' '); - }).join('\n'); - - return `${formatDescription(obj)}\n${sub}`; - } else if (obj.type && obj.type.name) { - return formatDescription(obj); - } - - return obj; -} - -Object.keys(interfaces).sort().forEach((group) => { - let content = ''; - - preamble = `${preamble}\n- [${group}](#${group})`; - markdown = `${markdown}\n## ${group}\n`; - - Object.keys(interfaces[group]).sort().forEach((iname) => { - const method = interfaces[group][iname]; - const name = `${group}_${iname}`; - const deprecated = method.deprecated ? ' (Deprecated and not supported, to be removed in a future version)' : ''; - const desc = `${method.desc}${deprecated}`; - const params = method.params.map(formatType).join('\n'); - const returns = formatType(method.returns); - - markdown = `${markdown}\n- [${name}](#${name})`; - content = `${content}### ${name}\n\n${desc}\n\n#### parameters\n\n${params || 'none'}\n\n#### returns\n\n${returns || 'none'}\n\n`; - }); - - markdown = `${markdown}\n\n${content}`; -}); - -fs.writeFileSync(MARKDOWN, `${preamble}\n\n${markdown}`, 'utf8'); diff --git a/js/src/jsonrpc/helpers.js b/js/src/jsonrpc/helpers.js new file mode 100644 index 000000000..b5e363ef1 --- /dev/null +++ b/js/src/jsonrpc/helpers.js @@ -0,0 +1,62 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +// Placeholders for objects with undefined fields, will show up in docs as `{ ... }` +export const DUMMY = '$DUMMY$'; + +// Enrich the API spec by additional markdown-formatted preamble +export function withPreamble (preamble, spec) { + Object.defineProperty(spec, '_preamble', { + value: preamble.trim(), + enumerable: false + }); + + return spec; +} + +// Enrich any example value with a comment to print in the docs +export function withComment (example, comment) { + const constructor = example == null ? null : example.constructor; + + if (constructor === Object || constructor === Array) { + Object.defineProperty(example, '_comment', { + value: comment, + enumerable: false + }); + + return example; + } + + // Convert primitives + return new PrimitiveWithComment(example, comment); +} + +// Turn a decimal number into a hexadecimal string with comment to it's original value +export function fromDecimal (decimal) { + return withComment(`0x${decimal.toString(16)}`, decimal.toString()); +} + +// Internal helper +class PrimitiveWithComment { + constructor (primitive, comment) { + this._value = primitive; + this._comment = comment; + } + + toJSON () { + return this._value; + } +}