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
						
					
					
				| '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
 |