UNPKG

5.6 kBJavaScriptView Raw
1'use strict';
2
3// Module dependencies
4const assert = require('assert');
5const EventEmitter = require('events');
6const path = require('path');
7const utils = require('./utils');
8
9// Constants
10const SUPPORT_ADAPTER_NAMES = ['express', 'socketio', 'rpc'];
11const REQUIRED_PARAM_KEYS = ['name'];
12
13class Method extends EventEmitter {
14 static create(name, options, fn) {
15 return new Method(name, options, fn);
16 }
17
18 constructor(name, options, fn) {
19 super();
20
21 this.name = name || options.name || utils.getName(fn);
22 assert(typeof this.name === 'string', 'Method name must be a valid string');
23 assert(typeof fn === 'function', 'Method fn must be a valid function');
24
25 // Parent reference
26 this.parent = null;
27
28 // Stack to be executed, must be composed before server starts
29 this.stack = null;
30
31 // Main function
32 this.fn = fn;
33
34 // Method options
35 this.options = options || {};
36
37 // Parameters accpetance schema
38 this._validateAndSetParams(options.params || options.accepts);
39
40 // Set supported adapters
41 this._validateAndSetAdapters(options.adapters || options.adapter);
42
43 // Method route config
44 this._validateAndSetRoute(options.http || options.route);
45
46 // Method description, notes for debug info and documentation usage
47 this._validateAndSetDocumentation(options);
48
49 this._determineWhetherHooksShouldBeSkiped();
50
51 // TODO: Add built in multipart support
52 this.multipart = options.multipart || {};
53
54 // Extra attibutes that can be used by plugins to store cusmized data
55 this.extra = options.extra || {};
56 }
57
58 // Validate whether params schema is valid
59 _validateAndSetParams(params) {
60 params = params || [];
61 if (!Array.isArray(params)) params = [params];
62
63 let errorMessages = [];
64 // console.log(params);
65 params.forEach((param, i) => {
66 param = param || {};
67 REQUIRED_PARAM_KEYS.forEach((key) => {
68 if (!param[key]) errorMessages.push(`\`${key}\` is missing for params of \`${this.fullName()}\` method at ${i}`);
69 });
70 });
71
72 if (errorMessages.length) {
73 throw new Error(errorMessages.join('\n'));
74 }
75
76 this.params = params;
77 }
78
79 // Validate and set adapters
80 _validateAndSetAdapters(adapters) {
81 // Assign default adapters
82 if (!adapters) adapters = ['all'];
83
84 // Convert adapters into an array
85 if (!Array.isArray(adapters)) adapters = [adapters];
86
87 // If `all` is included, then add all supported adapters
88 if (~adapters.indexOf('all')) {
89 this.adapters = SUPPORT_ADAPTER_NAMES.slice();
90 }
91 // Else check whether adapter names are valid
92 else {
93 this.adapters = adapters;
94 // check adapter validation
95 this.adapters.forEach((adapter) => {
96 assert(
97 ~SUPPORT_ADAPTER_NAMES.indexOf(adapter),
98 `Invalid adapter name found: '${adapter}'`
99 );
100 });
101 }
102 }
103
104 // Validate and set route config
105 _validateAndSetRoute(route) {
106 this.route = route || {};
107 // Set default verb
108 if (!this.route.verb) this.route.verb = 'all';
109
110 // Lowercase verb
111 this.route.verb = String(this.route.verb).toLowerCase();
112
113 // Set default path
114 if (!this.route.path) this.route.path = '/';
115 }
116
117 // Validate and set documentation related options
118 _validateAndSetDocumentation(options) {
119 this.description = options.description || options.desc || name;
120 this.notes = options.notes;
121 this.documented = options.documented !== false;
122 }
123
124 // Determine whether hooks should be skiped, expecially for middlewarified method
125 _determineWhetherHooksShouldBeSkiped() {
126 // Skip beforeHooks and afterHooks or not, default is `true` when route.verb is `use` otherwise `false`
127 this.skipHooks = this.route.verb === 'use';
128 if (this.options.skipHooks === true) {
129 this.skipHooks = true;
130 } else if (this.options.skipHooks === false) {
131 this.skipHooks = false;
132 }
133 }
134
135 // Check whether a method is support a specific adapter
136 isSupport(adapterName) {
137 return !!~this.adapters.indexOf(adapterName);
138 }
139
140 fullPath() {
141 let segments = ['/'];
142 if (this.parent) segments.push(this.parent.fullPath());
143 segments.push(this.route.path);
144 return path.join.apply(path, segments);
145 }
146
147 clone() {
148 let method = new Method(
149 this.name,
150 Object.assign({}, this.options),
151 this.fn
152 );
153
154 method.stack = null;
155 method.adapters = this.adapters.slice();
156 method.skipHooks = !!this.skipHooks;
157 method.parent = this.parent;
158 method.params = Object.assign([], this.params);
159 method.description = this.description;
160 method.notes = this.notes;
161 method.documented = !!this.documented;
162 method.route = Object.assign({}, this.route);
163 method.multipart = Object.assign({}, this.multipart);
164 method.extra = Object.assign({}, this.extra);
165
166 return method;
167 }
168
169 // Method fullname, including parent's fullname
170 fullName() {
171 return this.parent ? `${this.parent.fullName()}.${this.name}` : this.name;
172 }
173
174 // Invoke method
175 invoke() {
176 assert(
177 this.stack,
178 `Method: '${this.fullName()}' must be composed before invoking`
179 );
180
181 return this.stack.apply(null, arguments);
182 }
183
184 // Compose hooks and method for stack manner
185 compose(beforeStack, afterStack, afterErrorStack) {
186 let stack = []
187 .concat(this.skipHooks ? [] : (beforeStack || []))
188 .concat(this.fn)
189 .concat(this.skipHooks ? [] : (afterStack || []));
190
191 let afterError = utils.compose(afterErrorStack || []);
192
193 this.stack = utils.compose(stack, afterError);
194 }
195}
196
197module.exports = Method;