index.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. 'use strict';
  2. /* !
  3. * Chai - pathval utility
  4. * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
  5. * @see https://github.com/logicalparadox/filtr
  6. * MIT Licensed
  7. */
  8. /**
  9. * ### .hasProperty(object, name)
  10. *
  11. * This allows checking whether an object has own
  12. * or inherited from prototype chain named property.
  13. *
  14. * Basically does the same thing as the `in`
  15. * operator but works properly with null/undefined values
  16. * and other primitives.
  17. *
  18. * var obj = {
  19. * arr: ['a', 'b', 'c']
  20. * , str: 'Hello'
  21. * }
  22. *
  23. * The following would be the results.
  24. *
  25. * hasProperty(obj, 'str'); // true
  26. * hasProperty(obj, 'constructor'); // true
  27. * hasProperty(obj, 'bar'); // false
  28. *
  29. * hasProperty(obj.str, 'length'); // true
  30. * hasProperty(obj.str, 1); // true
  31. * hasProperty(obj.str, 5); // false
  32. *
  33. * hasProperty(obj.arr, 'length'); // true
  34. * hasProperty(obj.arr, 2); // true
  35. * hasProperty(obj.arr, 3); // false
  36. *
  37. * @param {Object} object
  38. * @param {String|Symbol} name
  39. * @returns {Boolean} whether it exists
  40. * @namespace Utils
  41. * @name hasProperty
  42. * @api public
  43. */
  44. function hasProperty(obj, name) {
  45. if (typeof obj === 'undefined' || obj === null) {
  46. return false;
  47. }
  48. // The `in` operator does not work with primitives.
  49. return name in Object(obj);
  50. }
  51. /* !
  52. * ## parsePath(path)
  53. *
  54. * Helper function used to parse string object
  55. * paths. Use in conjunction with `internalGetPathValue`.
  56. *
  57. * var parsed = parsePath('myobject.property.subprop');
  58. *
  59. * ### Paths:
  60. *
  61. * * Can be infinitely deep and nested.
  62. * * Arrays are also valid using the formal `myobject.document[3].property`.
  63. * * Literal dots and brackets (not delimiter) must be backslash-escaped.
  64. *
  65. * @param {String} path
  66. * @returns {Object} parsed
  67. * @api private
  68. */
  69. function parsePath(path) {
  70. var str = path.replace(/([^\\])\[/g, '$1.[');
  71. var parts = str.match(/(\\\.|[^.]+?)+/g);
  72. return parts.map(function mapMatches(value) {
  73. if (
  74. value === 'constructor' ||
  75. value === '__proto__' ||
  76. value === 'prototype'
  77. ) {
  78. return {};
  79. }
  80. var regexp = /^\[(\d+)\]$/;
  81. var mArr = regexp.exec(value);
  82. var parsed = null;
  83. if (mArr) {
  84. parsed = { i: parseFloat(mArr[1]) };
  85. } else {
  86. parsed = { p: value.replace(/\\([.[\]])/g, '$1') };
  87. }
  88. return parsed;
  89. });
  90. }
  91. /* !
  92. * ## internalGetPathValue(obj, parsed[, pathDepth])
  93. *
  94. * Helper companion function for `.parsePath` that returns
  95. * the value located at the parsed address.
  96. *
  97. * var value = getPathValue(obj, parsed);
  98. *
  99. * @param {Object} object to search against
  100. * @param {Object} parsed definition from `parsePath`.
  101. * @param {Number} depth (nesting level) of the property we want to retrieve
  102. * @returns {Object|Undefined} value
  103. * @api private
  104. */
  105. function internalGetPathValue(obj, parsed, pathDepth) {
  106. var temporaryValue = obj;
  107. var res = null;
  108. pathDepth = typeof pathDepth === 'undefined' ? parsed.length : pathDepth;
  109. for (var i = 0; i < pathDepth; i++) {
  110. var part = parsed[i];
  111. if (temporaryValue) {
  112. if (typeof part.p === 'undefined') {
  113. temporaryValue = temporaryValue[part.i];
  114. } else {
  115. temporaryValue = temporaryValue[part.p];
  116. }
  117. if (i === pathDepth - 1) {
  118. res = temporaryValue;
  119. }
  120. }
  121. }
  122. return res;
  123. }
  124. /* !
  125. * ## internalSetPathValue(obj, value, parsed)
  126. *
  127. * Companion function for `parsePath` that sets
  128. * the value located at a parsed address.
  129. *
  130. * internalSetPathValue(obj, 'value', parsed);
  131. *
  132. * @param {Object} object to search and define on
  133. * @param {*} value to use upon set
  134. * @param {Object} parsed definition from `parsePath`
  135. * @api private
  136. */
  137. function internalSetPathValue(obj, val, parsed) {
  138. var tempObj = obj;
  139. var pathDepth = parsed.length;
  140. var part = null;
  141. // Here we iterate through every part of the path
  142. for (var i = 0; i < pathDepth; i++) {
  143. var propName = null;
  144. var propVal = null;
  145. part = parsed[i];
  146. // If it's the last part of the path, we set the 'propName' value with the property name
  147. if (i === pathDepth - 1) {
  148. propName = typeof part.p === 'undefined' ? part.i : part.p;
  149. // Now we set the property with the name held by 'propName' on object with the desired val
  150. tempObj[propName] = val;
  151. } else if (typeof part.p !== 'undefined' && tempObj[part.p]) {
  152. tempObj = tempObj[part.p];
  153. } else if (typeof part.i !== 'undefined' && tempObj[part.i]) {
  154. tempObj = tempObj[part.i];
  155. } else {
  156. // If the obj doesn't have the property we create one with that name to define it
  157. var next = parsed[i + 1];
  158. // Here we set the name of the property which will be defined
  159. propName = typeof part.p === 'undefined' ? part.i : part.p;
  160. // Here we decide if this property will be an array or a new object
  161. propVal = typeof next.p === 'undefined' ? [] : {};
  162. tempObj[propName] = propVal;
  163. tempObj = tempObj[propName];
  164. }
  165. }
  166. }
  167. /**
  168. * ### .getPathInfo(object, path)
  169. *
  170. * This allows the retrieval of property info in an
  171. * object given a string path.
  172. *
  173. * The path info consists of an object with the
  174. * following properties:
  175. *
  176. * * parent - The parent object of the property referenced by `path`
  177. * * name - The name of the final property, a number if it was an array indexer
  178. * * value - The value of the property, if it exists, otherwise `undefined`
  179. * * exists - Whether the property exists or not
  180. *
  181. * @param {Object} object
  182. * @param {String} path
  183. * @returns {Object} info
  184. * @namespace Utils
  185. * @name getPathInfo
  186. * @api public
  187. */
  188. function getPathInfo(obj, path) {
  189. var parsed = parsePath(path);
  190. var last = parsed[parsed.length - 1];
  191. var info = {
  192. parent:
  193. parsed.length > 1 ?
  194. internalGetPathValue(obj, parsed, parsed.length - 1) :
  195. obj,
  196. name: last.p || last.i,
  197. value: internalGetPathValue(obj, parsed),
  198. };
  199. info.exists = hasProperty(info.parent, info.name);
  200. return info;
  201. }
  202. /**
  203. * ### .getPathValue(object, path)
  204. *
  205. * This allows the retrieval of values in an
  206. * object given a string path.
  207. *
  208. * var obj = {
  209. * prop1: {
  210. * arr: ['a', 'b', 'c']
  211. * , str: 'Hello'
  212. * }
  213. * , prop2: {
  214. * arr: [ { nested: 'Universe' } ]
  215. * , str: 'Hello again!'
  216. * }
  217. * }
  218. *
  219. * The following would be the results.
  220. *
  221. * getPathValue(obj, 'prop1.str'); // Hello
  222. * getPathValue(obj, 'prop1.att[2]'); // b
  223. * getPathValue(obj, 'prop2.arr[0].nested'); // Universe
  224. *
  225. * @param {Object} object
  226. * @param {String} path
  227. * @returns {Object} value or `undefined`
  228. * @namespace Utils
  229. * @name getPathValue
  230. * @api public
  231. */
  232. function getPathValue(obj, path) {
  233. var info = getPathInfo(obj, path);
  234. return info.value;
  235. }
  236. /**
  237. * ### .setPathValue(object, path, value)
  238. *
  239. * Define the value in an object at a given string path.
  240. *
  241. * ```js
  242. * var obj = {
  243. * prop1: {
  244. * arr: ['a', 'b', 'c']
  245. * , str: 'Hello'
  246. * }
  247. * , prop2: {
  248. * arr: [ { nested: 'Universe' } ]
  249. * , str: 'Hello again!'
  250. * }
  251. * };
  252. * ```
  253. *
  254. * The following would be acceptable.
  255. *
  256. * ```js
  257. * var properties = require('tea-properties');
  258. * properties.set(obj, 'prop1.str', 'Hello Universe!');
  259. * properties.set(obj, 'prop1.arr[2]', 'B');
  260. * properties.set(obj, 'prop2.arr[0].nested.value', { hello: 'universe' });
  261. * ```
  262. *
  263. * @param {Object} object
  264. * @param {String} path
  265. * @param {Mixed} value
  266. * @api private
  267. */
  268. function setPathValue(obj, path, val) {
  269. var parsed = parsePath(path);
  270. internalSetPathValue(obj, val, parsed);
  271. return obj;
  272. }
  273. module.exports = {
  274. hasProperty: hasProperty,
  275. getPathInfo: getPathInfo,
  276. getPathValue: getPathValue,
  277. setPathValue: setPathValue,
  278. };