// Copyright 2015-2017 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 { isPlainObject } from 'lodash'; import { info, warn, error } from './helpers/log'; import { Dummy } from '../src/jsonrpc/helpers'; import interfaces from '../src/jsonrpc'; import rustMethods from './helpers/parsed-rpc-traits'; const ROOT_DIR = path.join(__dirname, '../docs'); if (!fs.existsSync(ROOT_DIR)) { fs.mkdirSync(ROOT_DIR); } Object.keys(rustMethods).forEach((group) => { Object.keys(rustMethods[group]).forEach((method) => { if (interfaces[group] == null || interfaces[group][method] == null) { error(`${group}_${method} is defined in Rust traits, but not in js/src/jsonrpc/interfaces`); } }); }); function printType (type, obj) { if (!type) { throw new Error(`Invalid type in ${JSON.stringify(obj)}`); } return type.print || `\`${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, obj)} - ${optional}${defaults}${obj.desc}`; } function formatType (obj) { if (obj == null || obj.type == null) { return obj; } const details = obj.details || obj.type.details; if (details) { const sub = Object.keys(details).map((key) => { return formatDescription(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' }; const { isDummy } = Dummy; const { isArray } = Array; // 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 (isDummy(example)) { return example.toString(); } 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(d => d.toString()); return `[${dummies.join(', ')}]`; } // For arrays containing just one object or string, don't unwind the array to multiline if (last === 0 && (isPlainObject(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 (isPlainObject(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); } 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 = Dummy.stringifyJSON(Object.assign({}, rpcReqTemplate, { method: name, params })); 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) && !isDummy(params[0].example)) { 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 = {}; // Comparator that will sort by sections first, names second function methodComparator (a, b) { const sectionA = spec[a].section || ''; const sectionB = spec[b].section || ''; return sectionA.localeCompare(sectionB) || a.localeCompare(b); } Object.keys(spec).sort(methodComparator).forEach((iname) => { const method = spec[iname]; const groupName = group.replace(/_.*$/, ''); const name = `${groupName}_${iname}`; if (method.nodoc || method.deprecated) { info(`Skipping ${name}: ${method.nodoc || 'Deprecated'}`); return; } if (rustMethods[groupName] == null || rustMethods[groupName][iname] == null) { error(`${name} is defined in js/src/jsonrpc/interfaces, but not in Rust traits`); } 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'); });