sway/lib/types/operation.js

328 строки
12 KiB
JavaScript

/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Apigee Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
'use strict';
var _ = require('lodash');
var cloneDeep = require('clone-deep');
var JsonRefs = require('json-refs');
var Parameter = require('./parameter');
var Response = require('./response');
var helpers = require('../helpers');
/**
* The Swagger Operation object.
*
* **Note:** Do not use directly.
*
* **Extra Properties:** Other than the documented properties, this object also exposes all properties of the definition
* object.
*
* @param {module:Sway~Path} pathObject - The Path object
* @param {string} method - The operation method
* @param {object} definition - The operation definition *(The raw operation definition __after__ remote references were
* resolved)*
* @param {object} definitionFullyResolved - The operation definition with all of its resolvable references resolved
* @param {string[]} pathToDefinition - The path segments to the operation definition
*
* @property {object} definition - The operation definition *(The raw operation definition __after__ remote references were
* resolved)*
* @property {object} definitionFullyResolved - The operation definition with all of its resolvable references resolved
* @property {string} method - The HTTP method for this operation
* @property {module:Sway~Path} pathObject - The `Path` object
* @property {string[]} pathToDefinition - The path segments to the operation definition
* @property {module:Sway~Parameter[]} parameterObjects - The `Parameter` objects
* @property {string} ptr - The JSON Pointer to the operation
* @property {object} securityDefinitions - The security definitions used by this operation
*
* @constructor
*/
function Operation (pathObject, method, definition, definitionFullyResolved, pathToDefinition) {
var seenParameters = [];
var that = this;
// Assign local properties
this.consumes = definitionFullyResolved.consumes || pathObject.api.consumes || [];
this.definition = cloneDeep(definition); // Clone so we do not alter the original
this.definitionFullyResolved = cloneDeep(definitionFullyResolved); // Clone so we do not alter the original
this.method = method;
this.parameterObjects = []; // Computed below
this.pathObject = pathObject;
this.pathToDefinition = pathToDefinition;
this.produces = definitionFullyResolved.produces || pathObject.api.produces || [];
this.ptr = JsonRefs.pathToPtr(pathToDefinition);
// Assign local properties from the Swagger definition properties
_.assign(this, definitionFullyResolved);
this._debug = this.pathObject.api._debug;
// Add the Parameter objects from the Path object that were not redefined in the operation definition
this.parameterObjects = _.map(pathObject.parameterObjects, function (parameterObject) {
seenParameters.push(parameterObject.in + ':' + parameterObject.name);
return parameterObject;
});
this._debug(' %s at %s', this.method.toUpperCase(), this.ptr);
this._debug(' Consumes:');
_.each(this.consumes, function (mimeType) {
that._debug(' %s', mimeType);
});
this._debug(' Parameters:');
// Create Parameter objects from parameters defined in the operation definition
_.each(definitionFullyResolved.parameters, function (paramDef, index) {
var key = paramDef.in + ':' + paramDef.name;
var seenIndex = seenParameters.indexOf(key);
var pPath = pathToDefinition.concat(['parameters', index.toString()]);
var parameterObject = new Parameter(that, _.get(pathObject.api.definitionRemotesResolved, pPath), paramDef, pPath);
if (seenIndex > -1) {
that.parameterObjects[seenIndex] = parameterObject;
} else {
that.parameterObjects.push(parameterObject);
seenParameters.push(key);
}
});
this._debug(' Produces:');
_.each(this.produces, function (mimeType) {
that._debug(' %s', mimeType);
});
this._debug(' Responses:');
// Create response objects from responses defined in the operation definition
this.responseObjects = _.map(this.definitionFullyResolved.responses, function (responseDef, code) {
var rPath = pathToDefinition.concat(['responses', code])
return new Response(that,
code,
_.get(that.pathObject.api.definitionRemotesResolved, rPath),
responseDef,
rPath);
});
this._debug(' Security:');
// Bring in the security definitions for easier access
// Override global security with locally defined
var security = this.security || pathObject.api.definitionFullyResolved.security;
this.securityDefinitions = _.reduce(security, function (defs, reqs) {
_.each(reqs, function (req, name) {
var def = pathObject.api.definitionFullyResolved.securityDefinitions ?
pathObject.api.definitionFullyResolved.securityDefinitions[name] :
undefined;
if (!_.isUndefined(def)) {
defs[name] = def;
}
that._debug(' %s (type: %s)', name, _.isUndefined(def) ? 'missing': def.type);
});
return defs;
}, {});
}
/**
* Returns the parameter with the provided name and location when provided.
*
* @param {string} name - The name of the parameter
* @param {string} [location] - The location *(`in`)* of the parameter *(Used for disambiguation)*
*
* @returns {module:Sway~Parameter} The `Parameter` matching the location and name combination or `undefined` if there is
* no match
*/
Operation.prototype.getParameter = function (name, location) {
return _.find(this.parameterObjects, function (parameterObject) {
return parameterObject.name === name && (_.isUndefined(location) ? true : parameterObject.in === location);
});
};
/**
* Returns all parameters for the operation.
*
* @returns {module:Sway~Parameter[]} All `Parameter` objects for the operation
*/
Operation.prototype.getParameters = function () {
return this.parameterObjects;
};
/**
* Returns the response for the requested status code or the default response *(if available)* if none is provided.
*
* @param {number|string} [statusCode='default'] - The status code
*
* @returns {module:Sway~Response} The `Response` or `undefined` if one cannot be found
*/
Operation.prototype.getResponse = function (statusCode) {
if (_.isUndefined(statusCode)) {
statusCode = 'default';
} else if (_.isNumber(statusCode)) {
statusCode = statusCode.toString();
}
return _.find(
this.getResponses(),
responseObject => responseObject.statusCode === statusCode
);
};
/**
* Returns all responses for the operation.
*
* @returns {module:Sway~Response[]} All `Response` objects for the operation
*/
Operation.prototype.getResponses = function () {
return this.responseObjects;
};
/**
* Returns the composite security definitions for this operation.
*
* The difference between this API and `this.security` is that `this.security` is the raw `security` value for the
* operation where as this API will return the global `security` value when available and this operation's security
* is undefined.
*
* @returns {object[]} The security for this operation
*/
Operation.prototype.getSecurity = function () {
return this.definitionFullyResolved.security || this.pathObject.api.definitionFullyResolved.security;
};
/**
* Validates the request.
*
* **Note:** Below is the list of `req` properties used *(req should be an `http.ClientRequest` or equivalent)*:
*
* * `body`: Used for `body` and `formData` parameters
* * `files`: Used for `formData` parameters whose `type` is `file`
* * `headers`: Used for `header` parameters and consumes
* * `originalUrl`: used for `path` parameters
* * `query`: Used for `query` parameters
* * `url`: used for `path` parameters
*
* For `path` parameters, we will use the operation's `regexp` property to parse out path parameters using the
* `originalUrl` or `url` property.
*
* *(See: {@link https://nodejs.org/api/http.html#http_class_http_clientrequest})*
*
* @param {object} req - The http client request *(or equivalent)*
*
* @param {object} options - The options passed for the validation
*
* @returns {module:Sway~ValidationResults} The validation results
*/
Operation.prototype.validateRequest = function (req, options) {
var results = {
errors: [],
warnings: []
};
// Validate the Content-Type if there is a set of expected consumes. Also we will validate only if there
// is a content-type set in the request headers as many times it happens that it is not set at all and
// the service works fine. Going by the RFC www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 mentioned above we would need to assume 'application/octet-stream'
// which in most cases would not be true so it would produce a lot of noise without adding much value.
if (this.consumes.length > 0 && helpers.getHeaderValue(req.headers, 'content-type')) {
helpers.validateContentType(helpers.getContentType(req.headers), this.consumes, results, options);
}
// Validate the parameters
_.each(this.getParameters(), function (param) {
var paramValue = param.getValue(req, options);
var vErr;
if (!paramValue.valid) {
vErr = {
code: 'INVALID_REQUEST_PARAMETER',
errors: paramValue.error.errors || [
{
code: paramValue.error.code,
message: paramValue.error.message,
path: paramValue.error.path,
schemaPath: paramValue.error.schemaPath || ''
}
],
in: paramValue.parameterObject.in,
// Report the actual error if there is only one error. Otherwise, report a JSON Schema validation error.
message: 'Invalid parameter (' + param.name + '): ' + ((paramValue.errors || []).length > 1 ?
'Value failed JSON Schema validation' :
paramValue.error.message),
name: paramValue.parameterObject.name,
path: (paramValue.error.path || []).length > 1 ? paramValue.error.path : ['parameters', param.name],
schemaPath: paramValue.error.schemaPath || ''
};
results.errors.push(vErr);
}
});
return results;
};
/**
* Validates the response.
*
* @param {module:Sway~ServerResponseWrapper} res - The response or response like object
* @param {module:Sway~ValidateOptions} options - The options passed for the validation
*
* @returns {module:Sway~ValidationResults} The validation results
*/
Operation.prototype.validateResponse = function (res, options) {
var results = {
errors: [],
warnings: []
};
var realStatusCode = res ? res.statusCode : 'default';
var response = this.getResponse(realStatusCode);
if (_.isUndefined(response)) {
const realCode = Number(realStatusCode)
// If there is no response for the requested status, use the default if there is one (This is Swagger's approach)
response = (400 <= realCode && realCode <= 599) ? this.getResponse('default') : undefined;
if (_.isUndefined(response)) {
results.errors.push({
code: 'INVALID_RESPONSE_CODE',
message: 'This operation does not have a defined \'' + realStatusCode + '\' response code',
path: []
});
return results;
}
}
results = response.validateResponse(res, options);
return results;
};
module.exports = Operation;