You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					230 lines
				
				5.2 KiB
			
		
		
			
		
	
	
					230 lines
				
				5.2 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const assert = require('assert') | ||
|  | const http = require('http') | ||
|  | const Ajv = require('ajv') | ||
|  | const Request = require('./lib/request') | ||
|  | const Response = require('./lib/response') | ||
|  | 
 | ||
|  | const errorMessage = 'The dispatch function has already been invoked' | ||
|  | const urlSchema = { | ||
|  |   oneOf: [ | ||
|  |     { type: 'string' }, | ||
|  |     { | ||
|  |       type: 'object', | ||
|  |       properties: { | ||
|  |         protocol: { type: 'string' }, | ||
|  |         hostname: { type: 'string' }, | ||
|  |         pathname: { type: 'string' } | ||
|  |         // port type => any
 | ||
|  |         // query type => any
 | ||
|  |       }, | ||
|  |       additionalProperties: true, | ||
|  |       required: ['pathname'] | ||
|  |     } | ||
|  |   ] | ||
|  | } | ||
|  | 
 | ||
|  | const ajv = new Ajv() | ||
|  | 
 | ||
|  | ajv.addKeyword({ | ||
|  |   keyword: 'prototypedType', | ||
|  |   validate: (_, data) => | ||
|  |     data && data.prototype && typeof data.prototype === 'object' | ||
|  | }) | ||
|  | 
 | ||
|  | const schema = { | ||
|  |   type: 'object', | ||
|  |   properties: { | ||
|  |     url: urlSchema, | ||
|  |     path: urlSchema, | ||
|  |     cookies: { | ||
|  |       type: 'object', | ||
|  |       additionalProperties: true | ||
|  |     }, | ||
|  |     headers: { | ||
|  |       type: 'object', | ||
|  |       additionalProperties: true | ||
|  |     }, | ||
|  |     query: { | ||
|  |       type: 'object', | ||
|  |       additionalProperties: true | ||
|  |     }, | ||
|  |     simulate: { | ||
|  |       type: 'object', | ||
|  |       properties: { | ||
|  |         end: { type: 'boolean' }, | ||
|  |         split: { type: 'boolean' }, | ||
|  |         error: { type: 'boolean' }, | ||
|  |         close: { type: 'boolean' } | ||
|  |       } | ||
|  |     }, | ||
|  |     authority: { type: 'string' }, | ||
|  |     remoteAddress: { type: 'string' }, | ||
|  |     method: { type: 'string', enum: http.METHODS.concat(http.METHODS.map(toLowerCase)) }, | ||
|  |     validate: { type: 'boolean' }, | ||
|  |     Request: { prototypedType: true } | ||
|  |     // payload type => any
 | ||
|  |   }, | ||
|  |   additionalProperties: true, | ||
|  |   oneOf: [ | ||
|  |     { required: ['url'] }, | ||
|  |     { required: ['path'] } | ||
|  |   ] | ||
|  | } | ||
|  | 
 | ||
|  | const optsValidator = ajv.compile(schema) | ||
|  | 
 | ||
|  | function inject (dispatchFunc, options, callback) { | ||
|  |   if (typeof callback === 'undefined') { | ||
|  |     return new Chain(dispatchFunc, options) | ||
|  |   } else { | ||
|  |     return doInject(dispatchFunc, options, callback) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function makeRequest (dispatchFunc, server, req, res) { | ||
|  |   req.once('error', function (err) { | ||
|  |     if (this.destroyed) res.destroy(err) | ||
|  |   }) | ||
|  | 
 | ||
|  |   req.once('close', function () { | ||
|  |     if (this.destroyed && !this._error) res.destroy() | ||
|  |   }) | ||
|  | 
 | ||
|  |   return req.prepare(() => dispatchFunc.call(server, req, res)) | ||
|  | } | ||
|  | 
 | ||
|  | function doInject (dispatchFunc, options, callback) { | ||
|  |   options = (typeof options === 'string' ? { url: options } : options) | ||
|  | 
 | ||
|  |   if (options.validate !== false) { | ||
|  |     assert(typeof dispatchFunc === 'function', 'dispatchFunc should be a function') | ||
|  |     const isOptionValid = optsValidator(options) | ||
|  |     if (!isOptionValid) { | ||
|  |       throw new Error(optsValidator.errors.map(e => e.message)) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   const server = options.server || {} | ||
|  | 
 | ||
|  |   const RequestConstructor = options.Request | ||
|  |     ? Request.CustomRequest | ||
|  |     : Request | ||
|  | 
 | ||
|  |   if (typeof callback === 'function') { | ||
|  |     const req = new RequestConstructor(options) | ||
|  |     const res = new Response(req, callback) | ||
|  | 
 | ||
|  |     return makeRequest(dispatchFunc, server, req, res) | ||
|  |   } else { | ||
|  |     return new Promise((resolve, reject) => { | ||
|  |       const req = new RequestConstructor(options) | ||
|  |       const res = new Response(req, resolve, reject) | ||
|  | 
 | ||
|  |       makeRequest(dispatchFunc, server, req, res) | ||
|  |     }) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function Chain (dispatch, option) { | ||
|  |   if (typeof option === 'string') { | ||
|  |     this.option = { url: option } | ||
|  |   } else { | ||
|  |     this.option = Object.assign({}, option) | ||
|  |   } | ||
|  | 
 | ||
|  |   this.dispatch = dispatch | ||
|  |   this._hasInvoked = false | ||
|  |   this._promise = null | ||
|  | 
 | ||
|  |   if (this.option.autoStart !== false) { | ||
|  |     process.nextTick(() => { | ||
|  |       if (!this._hasInvoked) { | ||
|  |         this.end() | ||
|  |       } | ||
|  |     }) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const httpMethods = [ | ||
|  |   'delete', | ||
|  |   'get', | ||
|  |   'head', | ||
|  |   'options', | ||
|  |   'patch', | ||
|  |   'post', | ||
|  |   'put', | ||
|  |   'trace' | ||
|  | ] | ||
|  | 
 | ||
|  | httpMethods.forEach(method => { | ||
|  |   Chain.prototype[method] = function (url) { | ||
|  |     if (this._hasInvoked === true || this._promise) { | ||
|  |       throw new Error(errorMessage) | ||
|  |     } | ||
|  |     this.option.url = url | ||
|  |     this.option.method = method.toUpperCase() | ||
|  |     return this | ||
|  |   } | ||
|  | }) | ||
|  | 
 | ||
|  | const chainMethods = [ | ||
|  |   'body', | ||
|  |   'cookies', | ||
|  |   'headers', | ||
|  |   'payload', | ||
|  |   'query' | ||
|  | ] | ||
|  | 
 | ||
|  | chainMethods.forEach(method => { | ||
|  |   Chain.prototype[method] = function (value) { | ||
|  |     if (this._hasInvoked === true || this._promise) { | ||
|  |       throw new Error(errorMessage) | ||
|  |     } | ||
|  |     this.option[method] = value | ||
|  |     return this | ||
|  |   } | ||
|  | }) | ||
|  | 
 | ||
|  | Chain.prototype.end = function (callback) { | ||
|  |   if (this._hasInvoked === true || this._promise) { | ||
|  |     throw new Error(errorMessage) | ||
|  |   } | ||
|  |   this._hasInvoked = true | ||
|  |   if (typeof callback === 'function') { | ||
|  |     doInject(this.dispatch, this.option, callback) | ||
|  |   } else { | ||
|  |     this._promise = doInject(this.dispatch, this.option) | ||
|  |     return this._promise | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | Object.getOwnPropertyNames(Promise.prototype).forEach(method => { | ||
|  |   if (method === 'constructor') return | ||
|  |   Chain.prototype[method] = function (...args) { | ||
|  |     if (!this._promise) { | ||
|  |       if (this._hasInvoked === true) { | ||
|  |         throw new Error(errorMessage) | ||
|  |       } | ||
|  |       this._hasInvoked = true | ||
|  |       this._promise = doInject(this.dispatch, this.option) | ||
|  |     } | ||
|  |     return this._promise[method](...args) | ||
|  |   } | ||
|  | }) | ||
|  | 
 | ||
|  | function isInjection (obj) { | ||
|  |   return ( | ||
|  |     obj instanceof Request || | ||
|  |     obj instanceof Response || | ||
|  |     (obj && obj.constructor && obj.constructor.name === '_CustomLMRRequest') | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function toLowerCase (m) { return m.toLowerCase() } | ||
|  | 
 | ||
|  | module.exports = inject | ||
|  | module.exports.inject = inject | ||
|  | module.exports.isInjection = isInjection |