test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. /* globals suite test */
  2. const assert = require('assert')
  3. const path = require('path')
  4. const { exec } = require('child_process')
  5. const pkg = require('../package.json')
  6. const flat = require('../index')
  7. const flatten = flat.flatten
  8. const unflatten = flat.unflatten
  9. const primitives = {
  10. String: 'good morning',
  11. Number: 1234.99,
  12. Boolean: true,
  13. Date: new Date(),
  14. null: null,
  15. undefined: undefined
  16. }
  17. suite('Flatten Primitives', function () {
  18. Object.keys(primitives).forEach(function (key) {
  19. const value = primitives[key]
  20. test(key, function () {
  21. assert.deepStrictEqual(flatten({
  22. hello: {
  23. world: value
  24. }
  25. }), {
  26. 'hello.world': value
  27. })
  28. })
  29. })
  30. })
  31. suite('Unflatten Primitives', function () {
  32. Object.keys(primitives).forEach(function (key) {
  33. const value = primitives[key]
  34. test(key, function () {
  35. assert.deepStrictEqual(unflatten({
  36. 'hello.world': value
  37. }), {
  38. hello: {
  39. world: value
  40. }
  41. })
  42. })
  43. })
  44. })
  45. suite('Flatten', function () {
  46. test('Nested once', function () {
  47. assert.deepStrictEqual(flatten({
  48. hello: {
  49. world: 'good morning'
  50. }
  51. }), {
  52. 'hello.world': 'good morning'
  53. })
  54. })
  55. test('Nested twice', function () {
  56. assert.deepStrictEqual(flatten({
  57. hello: {
  58. world: {
  59. again: 'good morning'
  60. }
  61. }
  62. }), {
  63. 'hello.world.again': 'good morning'
  64. })
  65. })
  66. test('Multiple Keys', function () {
  67. assert.deepStrictEqual(flatten({
  68. hello: {
  69. lorem: {
  70. ipsum: 'again',
  71. dolor: 'sit'
  72. }
  73. },
  74. world: {
  75. lorem: {
  76. ipsum: 'again',
  77. dolor: 'sit'
  78. }
  79. }
  80. }), {
  81. 'hello.lorem.ipsum': 'again',
  82. 'hello.lorem.dolor': 'sit',
  83. 'world.lorem.ipsum': 'again',
  84. 'world.lorem.dolor': 'sit'
  85. })
  86. })
  87. test('Custom Delimiter', function () {
  88. assert.deepStrictEqual(flatten({
  89. hello: {
  90. world: {
  91. again: 'good morning'
  92. }
  93. }
  94. }, {
  95. delimiter: ':'
  96. }), {
  97. 'hello:world:again': 'good morning'
  98. })
  99. })
  100. test('Empty Objects', function () {
  101. assert.deepStrictEqual(flatten({
  102. hello: {
  103. empty: {
  104. nested: {}
  105. }
  106. }
  107. }), {
  108. 'hello.empty.nested': {}
  109. })
  110. })
  111. if (typeof Buffer !== 'undefined') {
  112. test('Buffer', function () {
  113. assert.deepStrictEqual(flatten({
  114. hello: {
  115. empty: {
  116. nested: Buffer.from('test')
  117. }
  118. }
  119. }), {
  120. 'hello.empty.nested': Buffer.from('test')
  121. })
  122. })
  123. }
  124. if (typeof Uint8Array !== 'undefined') {
  125. test('typed arrays', function () {
  126. assert.deepStrictEqual(flatten({
  127. hello: {
  128. empty: {
  129. nested: new Uint8Array([1, 2, 3, 4])
  130. }
  131. }
  132. }), {
  133. 'hello.empty.nested': new Uint8Array([1, 2, 3, 4])
  134. })
  135. })
  136. }
  137. test('Custom Depth', function () {
  138. assert.deepStrictEqual(flatten({
  139. hello: {
  140. world: {
  141. again: 'good morning'
  142. }
  143. },
  144. lorem: {
  145. ipsum: {
  146. dolor: 'good evening'
  147. }
  148. }
  149. }, {
  150. maxDepth: 2
  151. }), {
  152. 'hello.world': {
  153. again: 'good morning'
  154. },
  155. 'lorem.ipsum': {
  156. dolor: 'good evening'
  157. }
  158. })
  159. })
  160. test('Transformed Keys', function () {
  161. assert.deepStrictEqual(flatten({
  162. hello: {
  163. world: {
  164. again: 'good morning'
  165. }
  166. },
  167. lorem: {
  168. ipsum: {
  169. dolor: 'good evening'
  170. }
  171. }
  172. }, {
  173. transformKey: function (key) {
  174. return '__' + key + '__'
  175. }
  176. }), {
  177. '__hello__.__world__.__again__': 'good morning',
  178. '__lorem__.__ipsum__.__dolor__': 'good evening'
  179. })
  180. })
  181. test('Should keep number in the left when object', function () {
  182. assert.deepStrictEqual(flatten({
  183. hello: {
  184. '0200': 'world',
  185. '0500': 'darkness my old friend'
  186. }
  187. }), {
  188. 'hello.0200': 'world',
  189. 'hello.0500': 'darkness my old friend'
  190. })
  191. })
  192. })
  193. suite('Unflatten', function () {
  194. test('Nested once', function () {
  195. assert.deepStrictEqual({
  196. hello: {
  197. world: 'good morning'
  198. }
  199. }, unflatten({
  200. 'hello.world': 'good morning'
  201. }))
  202. })
  203. test('Nested twice', function () {
  204. assert.deepStrictEqual({
  205. hello: {
  206. world: {
  207. again: 'good morning'
  208. }
  209. }
  210. }, unflatten({
  211. 'hello.world.again': 'good morning'
  212. }))
  213. })
  214. test('Multiple Keys', function () {
  215. assert.deepStrictEqual({
  216. hello: {
  217. lorem: {
  218. ipsum: 'again',
  219. dolor: 'sit'
  220. }
  221. },
  222. world: {
  223. greet: 'hello',
  224. lorem: {
  225. ipsum: 'again',
  226. dolor: 'sit'
  227. }
  228. }
  229. }, unflatten({
  230. 'hello.lorem.ipsum': 'again',
  231. 'hello.lorem.dolor': 'sit',
  232. 'world.lorem.ipsum': 'again',
  233. 'world.lorem.dolor': 'sit',
  234. world: { greet: 'hello' }
  235. }))
  236. })
  237. test('nested objects do not clobber each other when a.b inserted before a', function () {
  238. const x = {}
  239. x['foo.bar'] = { t: 123 }
  240. x.foo = { p: 333 }
  241. assert.deepStrictEqual(unflatten(x), {
  242. foo: {
  243. bar: {
  244. t: 123
  245. },
  246. p: 333
  247. }
  248. })
  249. })
  250. test('Custom Delimiter', function () {
  251. assert.deepStrictEqual({
  252. hello: {
  253. world: {
  254. again: 'good morning'
  255. }
  256. }
  257. }, unflatten({
  258. 'hello world again': 'good morning'
  259. }, {
  260. delimiter: ' '
  261. }))
  262. })
  263. test('Overwrite', function () {
  264. assert.deepStrictEqual({
  265. travis: {
  266. build: {
  267. dir: '/home/travis/build/kvz/environmental'
  268. }
  269. }
  270. }, unflatten({
  271. travis: 'true',
  272. travis_build_dir: '/home/travis/build/kvz/environmental'
  273. }, {
  274. delimiter: '_',
  275. overwrite: true
  276. }))
  277. })
  278. test('Transformed Keys', function () {
  279. assert.deepStrictEqual(unflatten({
  280. '__hello__.__world__.__again__': 'good morning',
  281. '__lorem__.__ipsum__.__dolor__': 'good evening'
  282. }, {
  283. transformKey: function (key) {
  284. return key.substring(2, key.length - 2)
  285. }
  286. }), {
  287. hello: {
  288. world: {
  289. again: 'good morning'
  290. }
  291. },
  292. lorem: {
  293. ipsum: {
  294. dolor: 'good evening'
  295. }
  296. }
  297. })
  298. })
  299. test('Messy', function () {
  300. assert.deepStrictEqual({
  301. hello: { world: 'again' },
  302. lorem: { ipsum: 'another' },
  303. good: {
  304. morning: {
  305. hash: {
  306. key: {
  307. nested: {
  308. deep: {
  309. and: {
  310. even: {
  311. deeper: { still: 'hello' }
  312. }
  313. }
  314. }
  315. }
  316. }
  317. },
  318. again: { testing: { this: 'out' } }
  319. }
  320. }
  321. }, unflatten({
  322. 'hello.world': 'again',
  323. 'lorem.ipsum': 'another',
  324. 'good.morning': {
  325. 'hash.key': {
  326. 'nested.deep': {
  327. 'and.even.deeper.still': 'hello'
  328. }
  329. }
  330. },
  331. 'good.morning.again': {
  332. 'testing.this': 'out'
  333. }
  334. }))
  335. })
  336. suite('Overwrite + non-object values in key positions', function () {
  337. test('non-object keys + overwrite should be overwritten', function () {
  338. assert.deepStrictEqual(flat.unflatten({ a: null, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
  339. assert.deepStrictEqual(flat.unflatten({ a: 0, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
  340. assert.deepStrictEqual(flat.unflatten({ a: 1, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
  341. assert.deepStrictEqual(flat.unflatten({ a: '', 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
  342. })
  343. test('overwrite value should not affect undefined keys', function () {
  344. assert.deepStrictEqual(flat.unflatten({ a: undefined, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
  345. assert.deepStrictEqual(flat.unflatten({ a: undefined, 'a.b': 'c' }, { overwrite: false }), { a: { b: 'c' } })
  346. })
  347. test('if no overwrite, should ignore nested values under non-object key', function () {
  348. assert.deepStrictEqual(flat.unflatten({ a: null, 'a.b': 'c' }), { a: null })
  349. assert.deepStrictEqual(flat.unflatten({ a: 0, 'a.b': 'c' }), { a: 0 })
  350. assert.deepStrictEqual(flat.unflatten({ a: 1, 'a.b': 'c' }), { a: 1 })
  351. assert.deepStrictEqual(flat.unflatten({ a: '', 'a.b': 'c' }), { a: '' })
  352. })
  353. })
  354. suite('.safe', function () {
  355. test('Should protect arrays when true', function () {
  356. assert.deepStrictEqual(flatten({
  357. hello: [
  358. { world: { again: 'foo' } },
  359. { lorem: 'ipsum' }
  360. ],
  361. another: {
  362. nested: [{ array: { too: 'deep' } }]
  363. },
  364. lorem: {
  365. ipsum: 'whoop'
  366. }
  367. }, {
  368. safe: true
  369. }), {
  370. hello: [
  371. { world: { again: 'foo' } },
  372. { lorem: 'ipsum' }
  373. ],
  374. 'lorem.ipsum': 'whoop',
  375. 'another.nested': [{ array: { too: 'deep' } }]
  376. })
  377. })
  378. test('Should not protect arrays when false', function () {
  379. assert.deepStrictEqual(flatten({
  380. hello: [
  381. { world: { again: 'foo' } },
  382. { lorem: 'ipsum' }
  383. ]
  384. }, {
  385. safe: false
  386. }), {
  387. 'hello.0.world.again': 'foo',
  388. 'hello.1.lorem': 'ipsum'
  389. })
  390. })
  391. test('Empty objects should not be removed', function () {
  392. assert.deepStrictEqual(unflatten({
  393. foo: [],
  394. bar: {}
  395. }), { foo: [], bar: {} })
  396. })
  397. })
  398. suite('.object', function () {
  399. test('Should create object instead of array when true', function () {
  400. const unflattened = unflatten({
  401. 'hello.you.0': 'ipsum',
  402. 'hello.you.1': 'lorem',
  403. 'hello.other.world': 'foo'
  404. }, {
  405. object: true
  406. })
  407. assert.deepStrictEqual({
  408. hello: {
  409. you: {
  410. 0: 'ipsum',
  411. 1: 'lorem'
  412. },
  413. other: { world: 'foo' }
  414. }
  415. }, unflattened)
  416. assert(!Array.isArray(unflattened.hello.you))
  417. })
  418. test('Should create object instead of array when nested', function () {
  419. const unflattened = unflatten({
  420. hello: {
  421. 'you.0': 'ipsum',
  422. 'you.1': 'lorem',
  423. 'other.world': 'foo'
  424. }
  425. }, {
  426. object: true
  427. })
  428. assert.deepStrictEqual({
  429. hello: {
  430. you: {
  431. 0: 'ipsum',
  432. 1: 'lorem'
  433. },
  434. other: { world: 'foo' }
  435. }
  436. }, unflattened)
  437. assert(!Array.isArray(unflattened.hello.you))
  438. })
  439. test('Should keep the zero in the left when object is true', function () {
  440. const unflattened = unflatten({
  441. 'hello.0200': 'world',
  442. 'hello.0500': 'darkness my old friend'
  443. }, {
  444. object: true
  445. })
  446. assert.deepStrictEqual({
  447. hello: {
  448. '0200': 'world',
  449. '0500': 'darkness my old friend'
  450. }
  451. }, unflattened)
  452. })
  453. test('Should not create object when false', function () {
  454. const unflattened = unflatten({
  455. 'hello.you.0': 'ipsum',
  456. 'hello.you.1': 'lorem',
  457. 'hello.other.world': 'foo'
  458. }, {
  459. object: false
  460. })
  461. assert.deepStrictEqual({
  462. hello: {
  463. you: ['ipsum', 'lorem'],
  464. other: { world: 'foo' }
  465. }
  466. }, unflattened)
  467. assert(Array.isArray(unflattened.hello.you))
  468. })
  469. })
  470. if (typeof Buffer !== 'undefined') {
  471. test('Buffer', function () {
  472. assert.deepStrictEqual(unflatten({
  473. 'hello.empty.nested': Buffer.from('test')
  474. }), {
  475. hello: {
  476. empty: {
  477. nested: Buffer.from('test')
  478. }
  479. }
  480. })
  481. })
  482. }
  483. if (typeof Uint8Array !== 'undefined') {
  484. test('typed arrays', function () {
  485. assert.deepStrictEqual(unflatten({
  486. 'hello.empty.nested': new Uint8Array([1, 2, 3, 4])
  487. }), {
  488. hello: {
  489. empty: {
  490. nested: new Uint8Array([1, 2, 3, 4])
  491. }
  492. }
  493. })
  494. })
  495. }
  496. test('should not pollute prototype', function () {
  497. unflatten({
  498. '__proto__.polluted': true
  499. })
  500. unflatten({
  501. 'prefix.__proto__.polluted': true
  502. })
  503. unflatten({
  504. 'prefix.0.__proto__.polluted': true
  505. })
  506. assert.notStrictEqual({}.polluted, true)
  507. })
  508. })
  509. suite('Arrays', function () {
  510. test('Should be able to flatten arrays properly', function () {
  511. assert.deepStrictEqual({
  512. 'a.0': 'foo',
  513. 'a.1': 'bar'
  514. }, flatten({
  515. a: ['foo', 'bar']
  516. }))
  517. })
  518. test('Should be able to revert and reverse array serialization via unflatten', function () {
  519. assert.deepStrictEqual({
  520. a: ['foo', 'bar']
  521. }, unflatten({
  522. 'a.0': 'foo',
  523. 'a.1': 'bar'
  524. }))
  525. })
  526. test('Array typed objects should be restored by unflatten', function () {
  527. assert.strictEqual(
  528. Object.prototype.toString.call(['foo', 'bar'])
  529. , Object.prototype.toString.call(unflatten({
  530. 'a.0': 'foo',
  531. 'a.1': 'bar'
  532. }).a)
  533. )
  534. })
  535. test('Do not include keys with numbers inside them', function () {
  536. assert.deepStrictEqual(unflatten({
  537. '1key.2_key': 'ok'
  538. }), {
  539. '1key': {
  540. '2_key': 'ok'
  541. }
  542. })
  543. })
  544. })
  545. suite('Order of Keys', function () {
  546. test('Order of keys should not be changed after round trip flatten/unflatten', function () {
  547. const obj = {
  548. b: 1,
  549. abc: {
  550. c: [{
  551. d: 1,
  552. bca: 1,
  553. a: 1
  554. }]
  555. },
  556. a: 1
  557. }
  558. const result = unflatten(
  559. flatten(obj)
  560. )
  561. assert.deepStrictEqual(Object.keys(obj), Object.keys(result))
  562. assert.deepStrictEqual(Object.keys(obj.abc), Object.keys(result.abc))
  563. assert.deepStrictEqual(Object.keys(obj.abc.c[0]), Object.keys(result.abc.c[0]))
  564. })
  565. })
  566. suite('CLI', function () {
  567. test('can take filename', function (done) {
  568. const cli = path.resolve(__dirname, '..', pkg.bin)
  569. const pkgJSON = path.resolve(__dirname, '..', 'package.json')
  570. exec(`${cli} ${pkgJSON}`, (err, stdout, stderr) => {
  571. assert.ifError(err)
  572. assert.strictEqual(stdout.trim(), JSON.stringify(flatten(pkg), null, 2))
  573. done()
  574. })
  575. })
  576. test('exits with usage if no file', function (done) {
  577. const cli = path.resolve(__dirname, '..', pkg.bin)
  578. const pkgJSON = path.resolve(__dirname, '..', 'package.json')
  579. exec(`${cli} ${pkgJSON}`, (err, stdout, stderr) => {
  580. assert.ifError(err)
  581. assert.strictEqual(stdout.trim(), JSON.stringify(flatten(pkg), null, 2))
  582. done()
  583. })
  584. })
  585. test('can take piped file', function (done) {
  586. const cli = path.resolve(__dirname, '..', pkg.bin)
  587. const pkgJSON = path.resolve(__dirname, '..', 'package.json')
  588. exec(`cat ${pkgJSON} | ${cli}`, (err, stdout, stderr) => {
  589. assert.ifError(err)
  590. assert.strictEqual(stdout.trim(), JSON.stringify(flatten(pkg), null, 2))
  591. done()
  592. })
  593. })
  594. })