All files / src/plugins/validation/2and3/semantic-validators paths.js

84% Statements 42/50
72.22% Branches 26/36
90.91% Functions 10/11
84% Lines 42/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150                                29x 29x 29x 29x   29x   29x 100x   100x   100x   199x 199x 199x   199x     100x 199x       199x   199x 15x     15x 15x     199x 249x         146x   165x     165x 165x             199x 199x 2x               199x 15x                 15x                 199x 199x 35x       199x 180x                         199x 199x     35x   3x                                         100x    
// Assertation 1:
// Path parameters definition, either at the path-level or the operation-level, need matching paramater declarations
 
// Assertation 2:
// Path parameter declarations do not allow empty names (/path/{} is not valid)
 
// Assertation 3:
// Path strings must be (equivalently) different (Example: /pet/{petId} and /pet/{petId2} are equivalently the same and would generate an error)
 
// Assertation 4:
// Paths must have unique (name + in combination) parameters
 
// Assertation 5:
// Paths cannot have literal query strings in them.
// Handled by the Spectral rule, path-not-include-query
 
const each = require('lodash/each');
const findIndex = require('lodash/findIndex');
const isPlainObject = require('lodash/isPlainObject');
const MessageCarrier = require('../../../utils/messageCarrier');
 
const templateRegex = /\{(.*?)\}/g;
 
module.exports.validate = function({ resolvedSpec }) {
  const messages = new MessageCarrier();
 
  const seenRealPaths = {};
 
  const tallyRealPath = path => {
    // ~~ is a flag for a removed template string
    const realPath = path.replace(templateRegex, '~~');
    const prev = seenRealPaths[realPath];
    seenRealPaths[realPath] = true;
    // returns if it was previously seen
    return !!prev;
  };
 
  each(resolvedSpec.paths, (path, pathName) => {
    Iif (!path || !pathName) {
      return;
    }
 
    const parametersFromPath = path.parameters ? path.parameters.slice() : [];
 
    const availableParameters = parametersFromPath.map((param, i) => {
      Iif (!isPlainObject(param)) {
        return;
      }
      param.$$path = `paths.${pathName}.parameters[${i}]`;
      return param;
    });
 
    each(path, (operation, operationName) => {
      if (
        operation &&
        operation.parameters &&
        Array.isArray(operation.parameters)
      ) {
        availableParameters.push(
          ...operation.parameters.map((param, i) => {
            Iif (!isPlainObject(param)) {
              return;
            }
            param.$$path = `paths.${pathName}.${operationName}.parameters[${i}]`;
            return param;
          })
        );
      }
    });
 
    // Assertation 3
    const hasBeenSeen = tallyRealPath(pathName);
    if (hasBeenSeen) {
      messages.addMessage(
        `paths.${pathName}`,
        'Equivalent paths are not allowed.',
        'error'
      );
    }
 
    // Assertation 4
    each(parametersFromPath, (parameterDefinition, i) => {
      const nameAndInComboIndex = findIndex(parametersFromPath, {
        name: parameterDefinition.name,
        in: parameterDefinition.in
      });
      // comparing the current index against the first found index is good, because
      // it cuts down on error quantity when only two parameters are involved,
      // i.e. if param1 and param2 conflict, this will only complain about param2.
      // it also will favor complaining about parameters later in the spec, which
      // makes more sense to the user.
      Iif (i !== nameAndInComboIndex && parameterDefinition.in) {
        messages.addMessage(
          `paths.${pathName}.parameters[${i}]`,
          "Path parameters must have unique 'name' + 'in' properties",
          'error'
        );
      }
    });
 
    let pathTemplates = pathName.match(templateRegex) || [];
    pathTemplates = pathTemplates.map(str =>
      str.replace('{', '').replace('}', '')
    );
 
    // Assertation 1
    each(availableParameters, (parameterDefinition, i) => {
      Iif (
        isPlainObject(parameterDefinition) &&
        parameterDefinition.in === 'path' &&
        pathTemplates.indexOf(parameterDefinition.name) === -1
      ) {
        messages.addMessage(
          parameterDefinition.$$path || `paths.${pathName}.parameters[${i}]`,
          `Path parameter was defined but never used: ${parameterDefinition.name}`,
          'error'
        );
      }
    });
 
    Eif (pathTemplates) {
      pathTemplates.forEach(parameter => {
        // Assertation 2
 
        if (parameter === '') {
          // it was originally "{}"
          messages.addMessage(
            `paths.${pathName}`,
            'Empty path parameter declarations are not valid',
            'error'
          );
        }
      });
    } else {
      each(availableParameters, (parameterDefinition, i) => {
        // Assertation 1, for cases when no templating is present on the path
        if (parameterDefinition.in === 'path') {
          messages.addMessage(
            `paths.${pathName}.parameters[${i}]`,
            `Path parameter was defined but never used: ${parameterDefinition.name}`,
            'error'
          );
        }
      });
    }
  });
 
  return messages;
};