normalize-argins.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /**
  2. * Module dependencies
  3. */
  4. var util = require('util');
  5. var _ = require('@sailshq/lodash');
  6. var anchor = require('anchor');
  7. var ANCHOR_RULES = require('anchor/accessible/rules');
  8. var flaverr = require('flaverr');
  9. var rttc = require('rttc');
  10. var getIsProductionWithoutDebug = require('./get-is-production-without-debug');
  11. var GENERIC_HELP_SUFFIX = require('./GENERIC_HELP_SUFFIX.string');
  12. var getMethodName = require('../get-method-name');
  13. /**
  14. * normalizeArgins()
  15. *
  16. * If argin validation is enabled, validate a dictionary of argins, potentially coercing them a bit.
  17. *
  18. * > Note that this modifies properties in-place, as a direct reference!
  19. *
  20. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  21. * @param {Dictionary} argins [argins from userland]
  22. * @param {String} arginValidationTactic [build-time usage option (see help-build-machine for more info)]
  23. * @param {String} extraArginsTactic [build-time usage option (see help-build-machine for more info)]
  24. * @param {Dictionary?} defaultArgins [a dictionary of defaults to automatically include as they were passed in as argins, but every time the machine function is executed. These take precedent over `defaultsTo`. If any of them don't apply to this machine (i.e. no matching input), they will be ignored..]
  25. * @param {Dictionary} nmDef [machine definition from implementor-land]
  26. * @param {Error?} omenForWarnings [optional omen-- if provided, logged warning messages will include a shortened stack trace from it]
  27. *
  28. * @returns {Array} [argin problems]
  29. * @of {String}
  30. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  31. */
  32. module.exports = function normalizeArgins(argins, arginValidationTactic, extraArginsTactic, defaultArgins, nmDef, omenForWarnings){
  33. // Assert valid usage of this utility.
  34. // > (in production, we skip this stuff to save cycles. We can get away w/ that
  35. // > because we only include this here to provide better error msgs in development
  36. // > anyway)
  37. if (!getIsProductionWithoutDebug()) {
  38. if (!_.isObject(argins) || _.isArray(argins) || _.isFunction(argins)) {
  39. throw new Error('Consistency violation: `argins` must be a dictionary. But instead got: '+util.inspect(argins, {depth: 5}));
  40. }
  41. if (!_.isString(arginValidationTactic) || !_.isString(extraArginsTactic)) {
  42. throw new Error('Consistency violation: `arginValidationTactic` and `extraArginsTactic` should have both been specified as strings! But instead, for `arginValidationTactic`, got: '+util.inspect(arginValidationTactic, {depth: 5})+' ...and for `extraArginsTactic`, got: '+util.inspect(extraArginsTactic, {depth: 5}));
  43. }
  44. if (!_.isObject(nmDef) || _.isArray(nmDef) || _.isFunction(nmDef)) {
  45. throw new Error('Consistency violation: `nmDef` must be a dictionary. But instead got: '+util.inspect(nmDef, {depth: 5}));
  46. }
  47. }//fi
  48. var arginProblems = [];
  49. // Strip any argins with an undefined value on the right-hand side.
  50. _.each(_.keys(argins), function (supposedInputCodeName) {
  51. if (argins[supposedInputCodeName] === undefined) {
  52. delete argins[supposedInputCodeName];
  53. }
  54. });//∞
  55. // If any argins exist for undeclared inputs, handle them accordingly.
  56. if (extraArginsTactic !== 'doNotCheck') {
  57. var unrecognizedInputCodeNames = _.difference(_.keys(argins), _.keys(nmDef.inputs));
  58. if (unrecognizedInputCodeNames.length > 0) {
  59. var extraArginsErrorMsg =
  60. // 'Unrecognized argin'+(unrecognizedInputCodeNames.length===1?'':'s')+' were passed in.\n'+
  61. unrecognizedInputCodeNames.length+' of the provided values ('+unrecognizedInputCodeNames.join(', ')+') '+
  62. (unrecognizedInputCodeNames.length === 1?'does':'do')+' not correspond with any recognized input.\n'+
  63. 'Please try again without the extra value'+(unrecognizedInputCodeNames.length===1?'':'s')+', or '+
  64. 'check your usage and adjust accordingly.\n'+
  65. GENERIC_HELP_SUFFIX;
  66. var potentialWarningSuffix = '';
  67. if (omenForWarnings && _.contains(['warnAndOmit', 'warn'], extraArginsTactic)) {
  68. potentialWarningSuffix = '\n'+
  69. flaverr.getBareTrace(omenForWarnings, 3);
  70. }
  71. if (extraArginsTactic === 'warnAndOmit') {
  72. _.each(unrecognizedInputCodeNames, function(extraArginName){
  73. delete argins[extraArginName];
  74. });//∞
  75. console.warn(
  76. '- - - - - - - - - - - - - - - - - - - - - - - -\n'+
  77. 'WARNING: Automatically trimmed extraneous values!\n'+
  78. 'In call to '+getMethodName(nmDef.identity)+'(), '+extraArginsErrorMsg+
  79. potentialWarningSuffix+'\n'+
  80. '- - - - - - - - - - - - - - - - - - - - - - - -'
  81. );
  82. }
  83. else if (extraArginsTactic === 'warn') {
  84. console.warn(
  85. '- - - - - - - - - - - - - - - - - - - - - - - -\n'+
  86. 'WARNING: In call to '+getMethodName(nmDef.identity)+'(), '+extraArginsErrorMsg+
  87. potentialWarningSuffix+'\n'+
  88. '- - - - - - - - - - - - - - - - - - - - - - - -'
  89. );
  90. }
  91. else if (extraArginsTactic === 'error') {
  92. arginProblems.push(extraArginsErrorMsg);
  93. }//fi
  94. }//fi
  95. }//fi
  96. // ┌─┐┌─┐┌─┐┬ ┬ ┬ ┌─┐┌┐┌┬ ┬ ┬─┐┌─┐┬ ┌─┐┬ ┬┌─┐┌┐┌┌┬┐ ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗ ╔═╗╦═╗╔═╗╦╔╗╔╔═╗
  97. // ├─┤├─┘├─┘│ └┬┘ ├─┤│││└┬┘ ├┬┘├┤ │ ├┤ └┐┌┘├─┤│││ │ ║║║╣ ╠╣ ╠═╣║ ║║ ║ ╠═╣╠╦╝║ ╦║║║║╚═╗
  98. // ┴ ┴┴ ┴ ┴─┘┴ ┴ ┴┘└┘ ┴ ┴└─└─┘┴─┘└─┘ └┘ ┴ ┴┘└┘ ┴ ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩ ╩ ╩╩╚═╚═╝╩╝╚╝╚═╝
  99. if (defaultArgins) {
  100. _.each(defaultArgins, function(defaultArgin, inputCodeName) {
  101. var inputDef = nmDef.inputs[inputCodeName];
  102. // Ignore/skip any default argins that were actually configured.
  103. if (argins[inputCodeName] !== undefined) { return; }
  104. // Ignore/skip any default argins that don't apply to the declared interface of this
  105. // machine (i.e. if there is no matching input).
  106. else if (!inputDef){ return; }
  107. // But otherwise, treat this default argin just like it was passed in by hand.
  108. else {
  109. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  110. // Note that, in most cases, this is passed in as a direct reference.
  111. // But sometimes, we deep-clone it. (For more context, see the very similar handling of input defs'
  112. // `defaultsTo` declarations further down in this file.)
  113. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  114. var isDefaultArginDeepMutableAtRuntime = defaultArgin && typeof defaultArgin === 'object';
  115. if (arginValidationTactic === 'coerceAndCloneOrError' && isDefaultArginDeepMutableAtRuntime && !inputDef.readOnly) {
  116. argins[inputCodeName] = _.cloneDeep(defaultArgin);
  117. }
  118. else {
  119. argins[inputCodeName] = defaultArgin;
  120. }
  121. }
  122. });//∞
  123. }//fi
  124. _.each(nmDef.inputs, function (inputDef, inputCodeName){
  125. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  126. // FUTURE: Check for extraneous properties (nested AND top-level)
  127. // -how to handle this might fall into a slightly different category
  128. // than the `arginValidationTactic`-- or it might not. Not sure yet
  129. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  130. // ┌─┐┬ ┬┌─┐┬─┐┌─┐┌┐┌┌┬┐┌─┐┌─┐ ╔╦╗╦ ╦╔═╗╔═╗ ╔═╗╔═╗╔═╗╔═╗╔╦╗╦ ╦
  131. // │ ┬│ │├─┤├┬┘├─┤│││ │ ├┤ ├┤ ║ ╚╦╝╠═╝║╣ ╚═╗╠═╣╠╣ ║╣ ║ ╚╦╝
  132. // └─┘└─┘┴ ┴┴└─┴ ┴┘└┘ ┴ └─┘└─┘ ╩ ╩ ╩ ╚═╝ ╚═╝╩ ╩╚ ╚═╝ ╩ ╩
  133. // Perform type safety checks, if configured to do so.
  134. if (arginValidationTactic !== 'doNotCheck') {
  135. if (argins[inputCodeName] === undefined && !inputDef.required) {
  136. // NOTE:
  137. // This check in the machine runner is the only way to allow undefined values
  138. // when there is an explicit type; even when that type is `ref`.
  139. //
  140. // `rttc` normally treats everything as if it is required.
  141. // So if you are validating against a nested `===` in the example, for instance,
  142. // if the actual value is `undefined`, the validation will fail.
  143. //
  144. // That said, `undefined` _can_ make it past validation if it is attached
  145. // to a key in a nested dictionary, or as an item in a nested array within
  146. // a dict/array that is passed through `example: '==='`.
  147. //
  148. // In the same situation, but with `example: {}` or `example: []`, `undefined` items
  149. // will be removed from arrays, and if there are any keys with `undefined` attached as
  150. // a value, those keys will be stripped.
  151. }
  152. else if (argins[inputCodeName] === undefined && inputDef.required) {
  153. arginProblems.push('"' + inputCodeName + '" is required, but it was not defined.');
  154. }
  155. else if (argins[inputCodeName] === '' && inputDef.required) {
  156. arginProblems.push('Invalid "' + inputCodeName + '"'+': Cannot use \'\' (empty string) for a required input.');
  157. }
  158. else if (_.isNull(argins[inputCodeName]) && inputDef.allowNull === true) {
  159. // If a null value is provided and `allowNull` is `true`, there's no need to
  160. // validate it further. We're all good!
  161. }
  162. else if (_.isNull(argins[inputCodeName]) && inputDef.required) {
  163. arginProblems.push('Invalid "' + inputCodeName + '"'+': Cannot use `null` for this required input.');
  164. }
  165. else if (_.isNull(argins[inputCodeName]) && !inputDef.allowNull && !inputDef.required && inputDef.type !== 'json' && inputDef.type !== 'ref') {
  166. // If `null` is provided for an incompatible optional input, handle with a specific error message.
  167. arginProblems.push(
  168. 'Invalid "' + inputCodeName + '"'+': Cannot use `null`. '+
  169. 'Even though this input is optional, it still does not allow `null` to '+
  170. 'be explicitly passed in, because `null` is not a valid '+rttc.getDisplayTypeLabel(rttc.getRdt(inputDef.type), {capitalization: 'fragment'})+'. '+
  171. 'Instead, to leave out this value, please omit it altogether or use `undefined`. '+
  172. '(See this input definition for more detailed usage information. If you are the maintainer '+
  173. 'of this function, you specifically want to allow setting `null`, and you are sure your '+
  174. 'implementation already knows how to handle it, then change this input\'s definition to have '+
  175. '`allowNull: true`.)'
  176. );
  177. }
  178. else if (inputDef.isExemplar === true) {
  179. // If `isExemplar` property is set, then we'll interpret the actual runtime value literally
  180. // as an RTTC exemplar schema. In this case, we assume the machine's implementation presumably
  181. // uses this dynamic exemplar schema for some purpose. So we just ensure that it is valid.
  182. // (For example, a "Parse JSON" machine that uses the provided exemplar in order to ensure
  183. // the expected format of the output sent back through its success exit.)
  184. try {
  185. rttc.validateExemplarStrict(argins[inputCodeName], true);
  186. } catch (err) {
  187. switch (err.code) {
  188. case 'E_INVALID_EXEMPLAR':
  189. case 'E_DEPRECATED_SYNTAX':
  190. arginProblems.push(
  191. 'Invalid "' + inputCodeName + '"'+': Expected the value to be provided as a valid "RTTC exemplar schema" '+
  192. '(a special "by-example" format for concisely describing a particular data type), but it could not be parsed '+
  193. 'as such. '+err.message
  194. );
  195. break;
  196. default:
  197. throw err;
  198. }
  199. }
  200. }
  201. else {
  202. // Validate the argin (potentially mildly coercing it as well) and keep track
  203. // of any errors we detect.
  204. var potentiallyCoercedArgin;
  205. try {
  206. if (arginValidationTactic === 'coerceAndCloneOrError') {
  207. potentiallyCoercedArgin = rttc.validate(inputDef.type, argins[inputCodeName]);
  208. }
  209. else if (arginValidationTactic === 'error') {
  210. rttc.validateStrict(inputDef.type, argins[inputCodeName]);
  211. }
  212. } catch (err) {
  213. switch (err.code) {
  214. case 'E_INVALID':
  215. arginProblems.push(rttc.getInvalidityMessage(inputDef.type, argins[inputCodeName], err, '"'+inputCodeName+'"'));
  216. break;
  217. default:
  218. throw err;
  219. }
  220. }
  221. // Re-attach the coerced/cloned version of the argin, if relevant.
  222. //
  223. // Only re-attach if either:
  224. // 1. A clone is necessary for safety since this argin is deep mutable at runtime,
  225. // and the input does not declare itself `readOnly: true`. (i.e. this is to
  226. // protect implementor-land code in the `fn` from accidentally smashing stuff
  227. // in here).
  228. // 2. Or if coercion might have occured
  229. //
  230. // (And NEVER re-attach if argin coercion is not enabled.)
  231. if (arginValidationTactic === 'coerceAndCloneOrError') {
  232. var isArginDeepMutableAtRuntime = argins[inputCodeName] && typeof argins[inputCodeName] === 'object';
  233. var wasArginChanged = (potentiallyCoercedArgin !== argins[inputCodeName]);
  234. if (wasArginChanged || (isArginDeepMutableAtRuntime && !inputDef.readOnly)) {
  235. argins[inputCodeName] = potentiallyCoercedArgin;
  236. }
  237. }//fi
  238. }//fi
  239. }//fi
  240. // ┌─┐┌─┐┌─┐┬ ┬ ┬ ╔╦╗╔═╗╔═╗╔═╗╦ ╦╦ ╔╦╗╔═╗ ╔╦╗╔═╗
  241. // ├─┤├─┘├─┘│ └┬┘ ║║║╣ ╠╣ ╠═╣║ ║║ ║ ╚═╗ ║ ║ ║
  242. // ┴ ┴┴ ┴ ┴─┘┴ ═╩╝╚═╝╚ ╩ ╩╚═╝╩═╝╩ ╚═╝ ╩ ╚═╝
  243. // Where relevant, use the `defaultsTo` value as the runtime value (argin) for the input.
  244. if (inputDef.defaultsTo !== undefined && argins[inputCodeName] === undefined) {
  245. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  246. // Note that, in most cases, this is passed in as a direct reference.
  247. //
  248. // But sometimes, we do deep-clone the default value first to help prevent userland bugs
  249. // from entanglement. Specifically, deep-cloning only occurs if argin coercion is enabled
  250. // anyway, if the default value is something that JavaScript passes by reference, and if
  251. // the input does not claim a "read only" guarantee by flagging itself `readOnly: true`.
  252. //
  253. // For posterity, the original thoughts around this approach (which were changed somewhat):
  254. // https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/private/help-exec-machine-instance.js#L270-L276
  255. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  256. var isDefaultValDeepMutableAtRuntime = inputDef.defaultsTo && typeof inputDef.defaultsTo === 'object';
  257. if (arginValidationTactic === 'coerceAndCloneOrError' && isDefaultValDeepMutableAtRuntime && !inputDef.readOnly) {
  258. argins[inputCodeName] = _.cloneDeep(inputDef.defaultsTo);
  259. }
  260. else {
  261. argins[inputCodeName] = inputDef.defaultsTo;
  262. }
  263. }//fi
  264. // ┌─┐┬ ┬┌─┐┌─┐┬┌─ ┌─┐┌─┐┬─┐ ╦═╗╦ ╦╦ ╔═╗ ╦ ╦╦╔═╗╦ ╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
  265. // │ ├─┤├┤ │ ├┴┐ ├┤ │ │├┬┘ ╠╦╝║ ║║ ║╣ ╚╗╔╝║║ ║║ ╠═╣ ║ ║║ ║║║║╚═╗
  266. // └─┘┴ ┴└─┘└─┘┴ ┴ └ └─┘┴└─ ╩╚═╚═╝╩═╝╚═╝ ╚╝ ╩╚═╝╩═╝╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
  267. // If appropriate, strictly enforce our (potentially-mildly-coerced) argin
  268. // vs. the validation ruleset defined on the corresponding input def.
  269. // Then, if there are any rule violations, track them as a "problem".
  270. //
  271. // > • High-level validation rules are ALWAYS skipped for `null`.
  272. // > • If there is no `validations` input key, then there's nothing for us to do here.
  273. if (arginValidationTactic !== 'doNotCheck' && !_.isNull(argins[inputCodeName]) && argins[inputCodeName] !== undefined) {
  274. var ruleset = _.pick(inputDef, Object.keys(ANCHOR_RULES));
  275. var ruleViolations;
  276. try {
  277. ruleViolations = anchor(argins[inputCodeName], ruleset);
  278. // e.g.
  279. // [ { rule: 'isEmail', message: 'Value was not a valid email address.' }, ... ]
  280. } catch (err) {
  281. throw flaverr({
  282. message: 'Consistency violation: Unexpected error occurred when attempting to apply '+
  283. 'high-level validation rules for the provided "'+inputCodeName+'". '+err.message
  284. }, err);
  285. }
  286. if (ruleViolations.length > 0) {
  287. // Format and push on a rolled-up summary.
  288. // e.g.
  289. // ```
  290. // • Value was not in the configured whitelist (delinquent, new, paid)
  291. // • Value was an empty string.
  292. // ```
  293. arginProblems.push(
  294. 'Invalid "' + inputCodeName + '":\n · ' + _.pluck(ruleViolations, 'message').join('\n · ')
  295. );
  296. }//fi
  297. }//fi
  298. });//∞ </_.each() :: input definition>
  299. return arginProblems;
  300. };