UNPKG

@zowe/imperative

Version:
626 lines 31.1 kB
"use strict"; /* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html * * SPDX-License-Identifier: EPL-2.0 * * Copyright Contributors to the Zowe Project. * */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandPreparer = void 0; const Constants_1 = require("../../constants/src/Constants"); const ImperativeError_1 = require("../../error/src/ImperativeError"); const ProfileUtils_1 = require("../../profiles/src/utils/ProfileUtils"); const TextUtils_1 = require("../../utilities/src/TextUtils"); const OptionConstants_1 = require("./constants/OptionConstants"); const DeepMerge = require("deepmerge"); /** * Command preparer provides static utilities to ensure that command definitions are suitable for Imperative definition. */ class CommandPreparer { /** * Prepare the command definition and apply any pass on traits to children. * After a definition has been prepared, it should be considered final. * @param {ICommandDefinition} original - The original command definition tree to "prepare" * @param {ICommandProfileTypeConfiguration} baseProfile - An optional base profile to add to command definitions * @return {ICommandDefinition} A copy of the original that has been prepared */ static prepare(original, baseProfile) { /** * Ensure it was specified */ if (original == null) { throw new ImperativeError_1.ImperativeError({ msg: `The command definition document must not be null/undefined.` }); } /** * Even before basic validation, ensure that we can stringify and create a copy of the original document. * The document MUST be capable of this (i.e. every value must be convertable to a canonical JSON format and * no circular references are allowable) */ let copy; try { copy = JSON.parse(JSON.stringify(original)); } catch (e) { throw new ImperativeError_1.ImperativeError({ msg: `An error occurred copying the original command document: ${e.message}` }); } /** * Set the default values for optional fields (if they were omitted) */ CommandPreparer.setDefaultValues(copy); /** * For nodes that wish to "pass on" attributes, ensure that the attribute value to pass on is populated from * the parent (if omitted). */ CommandPreparer.populatePassOnValueFromParent(copy); /** * Add global options that should be passed down. We cannot use `appendAutoOptions` because `passOn` * logic is done before these function is called. */ CommandPreparer.appendPassOnOptions(copy); /** * Pass on/down any attributes/traits from parents to children (as required) */ CommandPreparer.passOn(copy); /** * Perform basic validation on the document to ensure that all the necessary fields are present. */ CommandPreparer.validateDefinitionTree(copy); /** * Prepare the definition by populating with default values, applying global options, etc. */ const baseProfileOptions = []; if (baseProfile != null) { for (const propName of Object.keys(baseProfile.schema.properties)) { if (baseProfile.schema.properties[propName].optionDefinition != null) { baseProfileOptions.push(baseProfile.schema.properties[propName].optionDefinition); } if (baseProfile.schema.properties[propName].optionDefinitions != null) { baseProfileOptions.push(...baseProfile.schema.properties[propName].optionDefinitions); } } } const prepared = CommandPreparer.appendAutoOptions(copy, baseProfileOptions); /** * The document prepared for Imperative CLI usage/definition. This should be considered the final document. */ return prepared; } /** * Perform preliminary (or post-preparation) basic validation of the command definition tree. Checks to ensure * that absoultely necessary fields are populated and invalid combos are not present. * @param {ICommandDefinition} definitionTree - full tree of command definitions to validate */ static validateDefinitionTree(definitionTree) { CommandPreparer.perfomBasicValidation(definitionTree, []); /** * TODO: Advanced Validation * TODO: Consider protecting against re-used/overridden aliases, command collisions, etc. * TODO: Consider adding CLI implementation specific command structure validation */ } /** * Perform preliminary (or post prepared) basic validation of the command definition tree. Checks to ensure * that absoultely necessary fields are populated and invalid combos are not present. * * Note: The root command is a bit of a special case, and is immune to some validation - we can't have a * name associated because that would cause an extra segment added in yargs. * * @param {ICommandDefinition} definition - The current command definition in the tree * @param {ICommandDefinition[]} definitions - the current set of definitions we've traversed - for diagnostics */ static perfomBasicValidation(definition, definitions) { const definitionDetails = "The definition in error has been placed in the additional details field of this error object."; // Do a quick check for required properties. If none are present, assume that either the definition // is completely incorrect OR the user did NOT export the definition in a module glob const props = Object.keys(definition); if (props.indexOf("name") < 0 && props.indexOf("description") < 0 && props.indexOf("type") < 0) { throw new ImperativeError_1.ImperativeError({ msg: `The command definition node being validated does NOT contain any of the required fields (name, description, type). ` + `Either the definition supplied is completely incorrect (see ICommandDefinition interface for required fields) ` + `OR you did NOT export the definition in your command definition module (found via the command definition glob). ` + `Keys/properties present on the definition: ${props.join(",")}. ${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // All nodes must have a non-blank name if (!definition.isRoot && (definition.name == null || definition.name.trim().length === 0)) { throw new ImperativeError_1.ImperativeError({ msg: `A command definition node contains an undefined or empty name. ${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // A node cannot have both chained handlers and a single handler if (definition.handler != null && definition.chainedHandlers != null && definition.chainedHandlers.length > 0) { throw new ImperativeError_1.ImperativeError({ msg: `A command definition node (${definition.name}) contains both a handler and chained handler ` + `configuration. The two are mutually exclusive. ${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // verify chained handler configurations are correct if (definition.chainedHandlers != null) { for (let chainedHandlerIndex = 0; chainedHandlerIndex < definition.chainedHandlers.length; chainedHandlerIndex++) { const chainedHandler = definition.chainedHandlers[chainedHandlerIndex]; const mappings = chainedHandler.mapping == null ? [] : chainedHandler.mapping; for (const mapping of mappings) { if (mapping.to == null) { throw new ImperativeError_1.ImperativeError({ msg: "Property to argument mapping is invalid for chained handler: " + chainedHandler.handler, additionalDetails: "Argument mapping does not have a 'to' field. Unable to determine " + "where to place the arguments for this chained handler." }); } if (mapping.from != null && mapping.value != null) { throw new ImperativeError_1.ImperativeError({ msg: "Property to argument mapping is invalid for chained handler: " + chainedHandler.handler, additionalDetails: "Argument mapping has both a 'from' field and a 'value' field. " + "These two fields are mutually exclusive." }); } const indicesAhead = mapping.applyToHandlers == null ? [1] : mapping.applyToHandlers; // make sure they don't try to specify a handler out of bounds of the array for (const indexAhead of indicesAhead) { if (chainedHandlerIndex + indexAhead >= definition.chainedHandlers.length) { throw new ImperativeError_1.ImperativeError({ msg: "Property to argument mapping is invalid for chained handler: " + chainedHandler.handler, additionalDetails: TextUtils_1.TextUtils.formatMessage("The mapping refers to a relative index %s that when added to its " + "absolute index (%s) is greater than the total number of handlers (%s).", indexAhead, chainedHandlerIndex, definition.chainedHandlers.length) }); } } } } } // All nodes must have a type if (definition.type == null || definition.type.trim().length === 0) { throw new ImperativeError_1.ImperativeError({ msg: `A command definition node (${definition.name}) contains an undefined or empty type. ${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // All nodes must have a description if (!definition.isRoot && (definition.description == null || definition.description.trim().length === 0)) { throw new ImperativeError_1.ImperativeError({ msg: `A command definition node (${definition.name} of type ${definition.type}) contains an ` + `undefined or empty description. ${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // Options, if specified, must be an array if (definition.options != null) { if (!Array.isArray(definition.options)) { throw new ImperativeError_1.ImperativeError({ msg: `A command definition node (${definition.name} of type ${definition.type}) options are invalid (not an array). ` + `${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // If options are specified, perform validation CommandPreparer.performBasicOptionValidation(definition); } // Check positional arguments are an array if (definition.positionals != null) { if (!Array.isArray(definition.positionals)) { throw new ImperativeError_1.ImperativeError({ msg: `A command definition node (${definition.name} of type ${definition.type}) positionals are invalid (not an array). ` + `${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // If positionals are specified, perform validation CommandPreparer.performBasicPositionalValidation(definition); } // Children must be an array if (definition.children != null && !Array.isArray(definition.children)) { throw new ImperativeError_1.ImperativeError({ msg: `A command definition node (${definition.name} of type ${definition.type}) contains ill-formed children. ${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // A group must have children if (definition.type === "group" && (definition.children == null || definition.children.length === 0)) { throw new ImperativeError_1.ImperativeError({ msg: `A "group" command definition node (${definition.name}) contains no children. A group implies children. ${definitionDetails}`, additionalDetails: JSON.stringify(definition) }); } // Perform validation for each child if (definition.children != null) { for (const child of definition.children) { CommandPreparer.perfomBasicValidation(child, definitions.concat(definition)); } } } /** * Perform basic positional operand validation. Ensure that the positional operands are valid and well formed. * @private * @static * @param {ICommandDefinition} definition - The command definition containing positionals to be validated * @memberof CommandPreparer */ static performBasicPositionalValidation(definition) { for (const pos of definition.positionals) { /** * All positionals must have a name */ if (pos.name == null || pos.name.trim().length === 0) { throw new ImperativeError_1.ImperativeError({ msg: `A positional definition contains an undefined or empty name.`, additionalDetails: "POSITIONAL_DEFINITION:\n" + JSON.stringify(pos) + "\nCOMMAND_DEFINITION:\n" + JSON.stringify(definition) }); } /** * All positionals must have a type */ if (pos.type == null || pos.type.trim().length === 0) { throw new ImperativeError_1.ImperativeError({ msg: `A positional definition (${pos.name}) contains an undefined or empty type.`, additionalDetails: "POSITIONAL_DEFINITION:\n" + JSON.stringify(pos) + "\nCOMMAND_DEFINITION:\n" + JSON.stringify(definition) }); } /** * All positionals must have a non-blank description */ if (pos.description == null || pos.description.trim().length === 0) { throw new ImperativeError_1.ImperativeError({ msg: `A positional definition (${pos.name} of type ${pos.type}) contains an ` + `undefined or empty description.`, additionalDetails: "POSITIONAL_DEFINITION:\n" + JSON.stringify(pos) + "\nCOMMAND_DEFINITION:\n" + JSON.stringify(definition) }); } } } /** * Perform basic option operand validation. Ensure that the option operands are valid and well formed. * @private * @static * @param {ICommandDefinition} definition - The command definition containing options to be validated * @memberof CommandPreparer */ static performBasicOptionValidation(definition) { for (const opt of definition.options) { if (opt == null) { throw new ImperativeError_1.ImperativeError({ msg: `An option definition is null or undefined.`, additionalDetails: `COMMAND_DEFINITION:\n${JSON.stringify(definition)}` }); } /** * All options must have a name */ if (opt.name == null || opt.name.trim().length === 0) { throw new ImperativeError_1.ImperativeError({ msg: `An option definition contains an undefined or empty name.`, additionalDetails: "OPTION_DEFINITION:\n" + JSON.stringify(opt) + "\nCOMMAND_DEFINITION:\n" + JSON.stringify(definition) }); } /** * All options must have a type */ if (opt.type == null || opt.type.trim().length === 0) { throw new ImperativeError_1.ImperativeError({ msg: `An option definition (${opt.name}) contains an undefined or empty type.`, additionalDetails: "OPTION_DEFINITION:\n" + JSON.stringify(opt) + "\nCOMMAND_DEFINITION:\n" + JSON.stringify(definition) }); } /** * All options must have a non-blank description */ if (opt.description == null || opt.description.trim().length === 0) { throw new ImperativeError_1.ImperativeError({ msg: `An option definition (${opt.name} of type ${opt.type}) contains an ` + `undefined or empty description.`, additionalDetails: "OPTION_DEFINITION:\n" + JSON.stringify(opt) + "\nCOMMAND_DEFINITION:\n" + JSON.stringify(definition) }); } } } /** * If optional fields have not been populated in the original definition, ensure they are set to the appropriate defaults. * @private * @static * @param {ICommandDefinition} definition - the definition tree to set the default values * @memberof CommandPreparer */ static setDefaultValues(definition) { // make sure any array types are at least initialized to empty definition.options = definition.options || []; definition.aliases = definition.aliases || []; definition.positionals = definition.positionals || []; definition.passOn = definition.passOn || []; if (definition.children != null) { for (const child of definition.children) { CommandPreparer.setDefaultValues(child); } } } /** * If the "passOn" specification does not indicate a value, we will extract the value/trait from the parent and * populate the "passOn" value. This allows parents to pass on their own properties/traits. * @private * @static * @param {ICommandDefinition} definition - the full definition tree * @memberof CommandPreparer */ static populatePassOnValueFromParent(definition) { /** * If the pass on trait has no value, it is taken from the node in which it is defined (meaning * we are passing-on the trait as-is from the parent). */ for (const trait of definition.passOn) { if (trait.value == null) { const traitPropertyDef = definition[trait.property]; if (traitPropertyDef == null) { throw new ImperativeError_1.ImperativeError({ msg: `You cannot pass on a trait (${trait.property}) with a value of ` + `undefined (current command definition name: ${definition.name} of type ${definition.type}).` }); } trait.value = JSON.parse(JSON.stringify(traitPropertyDef)); if (trait.value == null) { throw new ImperativeError_1.ImperativeError({ msg: `You cannot pass on a trait (${trait.property}) with a value of ` + `undefined (current command definition name: ${definition.name} of type ${definition.type}).` }); } } } /** * Perform for every child */ if (definition.children != null) { for (const child of definition.children) { CommandPreparer.setDefaultValues(child); } } } /** * Appends items which should be passed on to later nodes * @param definition - The original command definition tree to "prepare" */ static appendPassOnOptions(definition) { // all groups have --help-examples definition.passOn.push({ property: "options", value: { name: Constants_1.Constants.HELP_EXAMPLES, group: Constants_1.Constants.GLOBAL_GROUP, description: "Display examples for all the commands in a group", type: "boolean" }, ignoreNodes: [ { type: "command", } ], merge: true }); // add show-inputs-only to all "command" nodes definition.passOn.push({ property: "options", value: { name: "show-inputs-only", group: Constants_1.Constants.GLOBAL_GROUP, description: "Show command inputs and do not run the command", type: "boolean", }, ignoreNodes: [ { type: "group", } ], merge: true }); } /** * Appends options (for profiles, global options like help, etc.) automatically * @param {ICommandDefinition} definition - The original command definition tree to "prepare" * @param {ICommandOptionDefinition[]} baseProfileOptions - Option definitions sourced from base profile * @return {ICommandDefinition} A copy of the original that has been prepared */ static appendAutoOptions(definition, baseProfileOptions) { // add the json option for all commands definition.options.push({ name: Constants_1.Constants.JSON_OPTION, aliases: [Constants_1.Constants.JSON_OPTION_ALIAS], group: Constants_1.Constants.GLOBAL_GROUP, description: "Produce JSON formatted data from a command", type: "boolean" }); // all commands and groups have --help definition.options.push({ name: Constants_1.Constants.HELP_OPTION, aliases: [Constants_1.Constants.HELP_OPTION_ALIAS], group: Constants_1.Constants.GLOBAL_GROUP, description: "Display help text", type: "boolean" }); // all commands and groups have --help-web definition.options.push({ name: Constants_1.Constants.HELP_WEB_OPTION, aliases: [Constants_1.Constants.HELP_WEB_OPTION_ALIAS], group: Constants_1.Constants.GLOBAL_GROUP, description: "Display HTML help in browser", type: "boolean" }); /** * Append any profile related options */ if (definition.profile != null) { let types = []; if (definition.profile.required) { types = types.concat(definition.profile.required); } if (definition.profile.optional) { types = types.concat(definition.profile.optional); } const profileOptions = types.filter((type) => !Array.isArray(definition.profile.suppressOptions) ? true : definition.profile.suppressOptions.indexOf(type) < 0); profileOptions.forEach((profOpt) => { const [profOptName, profOptAlias] = ProfileUtils_1.ProfileUtils.getProfileOptionAndAlias(profOpt); definition.options.push({ name: profOptName, aliases: [profOptAlias], group: "Profile Options", description: `The name of a (${profOpt}) profile to load for this command execution.`, type: "string" }); }); // Add any option definitions from base profile that are missing in service profile if (definition.options != null && baseProfileOptions.length > 0 && types.length > 1) { const optionNames = definition.options.map((cmdOpt) => cmdOpt.name); for (const profOpt of baseProfileOptions) { if (optionNames.indexOf(profOpt.name) === -1) { definition.options.push(profOpt); } } } } if (definition.children != null) { let allChildrenAreExperimental = true; for (const child of definition.children) { if (!child.experimental) { allChildrenAreExperimental = false; break; } } // hide any groups/actions where all the children are experimental but // the parent isn't explicitly marked experimental if (allChildrenAreExperimental) { definition.experimental = true; } } definition.children = definition.children ? definition.children.map((child) => { if (definition.experimental) { // propagate the experimental setting downwards if a parent is experimental child.experimental = true; } // prepare each child return CommandPreparer.appendAutoOptions(child, baseProfileOptions); }) : []; if (definition.enableStdin) { definition.options.push({ name: Constants_1.Constants.STDIN_OPTION, aliases: [Constants_1.Constants.STDIN_OPTION_ALIAS], type: "boolean", description: definition.stdinOptionDescription || Constants_1.Constants.STDIN_DEFAULT_DESCRIPTION }); } definition.options = definition.options.map((option) => { if (option.group == null) { if (option.required) { option.group = "Required Options"; } else { option.group = "Options"; } } if (option.aliases == null) { option.aliases = []; } return option; }); // Append the format options if requested if (definition.outputFormatOptions) { definition.options.push(OptionConstants_1.OptionConstants.RESPONSE_FORMAT_FILTER_OPTION); definition.options.push(OptionConstants_1.OptionConstants.RESPONSE_FORMAT_OPTION); definition.options.push(OptionConstants_1.OptionConstants.RESPONSE_FORMAT_HEADER_OPTION); } return definition; } /** * A command definition node can indicate any arbitrary field be "passed on" to it's children. The intention is * to provide convienence for the coder of definition document, when they want to apply the same attributes (such * as reading from stdin OR which profiles are required) to all of its decedents. * @param {ICommandDefinition} definition - the original command document * @param {ICommandDefinition} inherit - the current set of attributes/fields being "passed on" - if a "pass on" * specification is found in a child document, it overwrites the parents (takes precedence) * @return {ICommandDefinition} A copy of the original with all "passed on" fields. */ static passOn(definition, inherit) { /** * Apply the attributes to the current node - assuming the conditions are met */ if (inherit != null) { /** * Ensure this passOn specification wants this node to inherit the field */ for (const trait of inherit) { if (!CommandPreparer.ignoreNode(definition, trait.ignoreNodes)) { /** * Either merge/append or overwrite the field in the definition. */ const cloned = trait.value != null ? JSON.parse(JSON.stringify(trait.value)) : undefined; if (cloned == null) { throw new ImperativeError_1.ImperativeError({ msg: `The trait (${trait.property}) to pass on cannot have a ` + `value of undefined. (Current definition name: ${definition.name} ` + `of type: ${definition.type})` }); } if (trait.merge && Array.isArray(definition[trait.property])) { definition[trait.property] = definition[trait.property].concat(cloned); } else if (trait.merge && definition[trait.property] != null) { definition[trait.property] = DeepMerge(definition[trait.property], cloned); } else { definition[trait.property] = cloned; } } } } else { inherit = []; } /** * traits a cumulative - so we can pass down multiple from the same ancestor OR they may accumulate from * any number of ancestors. */ inherit = definition.passOn.concat(inherit); if (definition.children != null) { for (const child of definition.children) { CommandPreparer.passOn(child, inherit); } } } /** * Check if the current node should be ignored. The name of the node is checked agaisnt the specification in * the pass on parameters. * @param {ICommandDefinition} node - The command definition node * @param {ICommandDefinitionPassOnIgnore[]} ignore - The names to ignore * @returns {boolean} - True if we are to ignore passing on attributes to the passed definition node. */ static ignoreNode(node, ignore) { if (ignore == null) { return false; } for (const ig of ignore) { if (ig.name != null && ig.type != null) { if (ig.name === node.name && ig.type === node.type) { return true; } } if (ig.type == null) { if (ig.name != null && ig.name === node.name) { return true; } } if (ig.name == null) { if (ig.type != null && ig.type === node.type) { return true; } } } return false; } } exports.CommandPreparer = CommandPreparer; //# sourceMappingURL=CommandPreparer.js.map