Skip to content
149 changes: 149 additions & 0 deletions src/utils/__test__/hashable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
const { hashable } = require('../hashable')

const sample1 = {
"id": "0002",
"type": "donut",
"name": "Raised",
"ppu": 0.55,
"batters":
{
"batter":
[
{ "id": "1001", "type": "Regular" },
{ "type": "Large", "id": "0001" }
]
},
"topping":
[
{ "id": "5001", "type": "None" },
{ "type": "Glazed", "id": "5002" },
{ "id": "5005", "type": "Sugar" },
{ "id": "5003", "type": "Chocolate", "aaa": "boss" },
{ "id": "5004", "type": "Maple" }
]
}

const sample1String = `{"id":"0002","type":"donut","name":"Raised","ppu":0.55,"batters":{"batter":[{"id":"1001","type":"Regular"},{"type":"Large","id":"0001"}]},"topping":[{"id":"5001","type":"None"},{"type":"Glazed","id":"5002"},{"id":"5005","type":"Sugar"},{"id":"5003","type":"Chocolate","aaa":"boss"},{"id":"5004","type":"Maple"}]}`

const arraySorted1 = {
"id": "0002",
"type": "donut",
"name": "Raised",
"ppu": 0.55,
"batters":
{
"batter":
[
{ "type": "Large", "id": "0001" },
{ "id": "1001", "type": "Regular" }
]
},
"topping":
[
{ "id": "5001", "type": "None" },
{ "type": "Glazed", "id": "5002" },
{ "id": "5003", "type": "Chocolate", "aaa": "boss" },
{ "id": "5004", "type": "Maple" },
{ "id": "5005", "type": "Sugar" }
]
}

const allSorted1 = {
"batters":
{
"batter":
[
{ "id": "0001", "type": "Large" },
{ "id": "1001", "type": "Regular" }
]
},
"id": "0002",
"name": "Raised",
"ppu": 0.55,
"topping":
[
{ "id": "5001", "type": "None" },
{ "id": "5002", "type": "Glazed" },
{ "aaa": "boss", "id": "5003", "type": "Chocolate" },
{ "id": "5004", "type": "Maple" },
{ "id": "5005", "type": "Sugar" }
],
"type": "donut",
}

const sample2 = {
"_id": "5973782bdb9a930533b05cb2",
"isActive": true,
"balance": "$1,446.35",
"age": 32,
"eyeColor": "green",
"name": "Logan Keller",
"gender": "male",
"company": "ARTIQ",
"email": "[email protected]",
"phone": "+1 (952) 533-2258",
"friends": [
{
"id": 2,
"name": "Carol Martin"
},
{
"name": "Colon Salazar",
"id": 0,
},
{
"id": 1,
"name": "French Mcneil"
}
],
"favoriteFruit": "banana"
}

const allSorted2 = {
"_id": "5973782bdb9a930533b05cb2",
"age": 32,
"balance": "$1,446.35",
"company": "ARTIQ",
"email": "[email protected]",
"eyeColor": "green",
"favoriteFruit": "banana",
"friends": [
{
"id": 0,
"name": "Colon Salazar"
},
{
"id": 1,
"name": "French Mcneil"
},
{
"id": 2,
"name": "Carol Martin"
}
],
"gender": "male",
"isActive": true,
"name": "Logan Keller",
"phone": "+1 (952) 533-2258"
}

describe('hashable returns the desired sorted JSON', () => {
it('hashable does not change the input objects', () => {
expect(JSON.stringify(hashable(sample1, { sortObject: true }))).toBe(JSON.stringify(allSorted1))
expect(JSON.stringify(sample1)).toBe(sample1String)
})

it('hashable sorts array only for sample JSON 1', () => {
expect(JSON.stringify(hashable(sample1))).toBe(JSON.stringify(arraySorted1))
expect(JSON.stringify(sample1)).toBe(sample1String)
})

it('hashable sorts array and object for sample JSON 1', () => {
expect(JSON.stringify(hashable(sample1, { sortObject: true }))).toBe(JSON.stringify(allSorted1))
expect(JSON.stringify(sample1)).toBe(sample1String)
})

it('hashable sorts array and object for sample JSON 2', () => {
expect(JSON.stringify(hashable(sample2, { sortObject: true }))).toBe(JSON.stringify(allSorted2))
})
})
77 changes: 54 additions & 23 deletions src/utils/hashable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

/**
* Generate consistent hashable JSON payload
* great for piping through hash functions
* (does not sort object keys)
* great for piping through hash functions.
* Works in CLI and importable
*
* Use --in-place to overwrite the original JSON file. Default: false
* Use --sort-object to sort object keys. Default: false
* Use --priority to specify array sorting
* priority (default priority is used
* if not specified)
*
* Usage:
* $0 first.json second.json
Expand All @@ -15,7 +21,7 @@
const fs = require('fs')
const path = require('path')
const { readInput, parseArgs } = require('./readInput')
const defaultSort = ['language', 'id', 'name', 'store_id', 'category_id', 'value', 'label']
const defaultSort = ['id', '_id', 'name', 'category', 'value', 'label', 'page']

if (module && module.parent) {
module.exports = {
Expand All @@ -27,19 +33,26 @@ if (module && module.parent) {

async function useCommandLine(argv) {
const { args, options } = parseArgs(argv)
const sortObject = options['sort-object'] || false
const inPlace = options['in-place'] || false
const priority = (options['sort'] || '').split(',').concat(defaultSort)
if (options['sort']) console.error('@deprecated use --priority')
const priority = (options['priority'] || options['sort'] || '').split(',').concat(defaultSort)
const sortOptions = {
priority,
sortObject
}
const fileInputHandler = files => files
.map(file => path.resolve(process.cwd(), file))
.map(file => ({ file, content: require(file) }))
.map(data => ({ ...data, content: hashable(data.content, priority) }))
.map(data => ({ ...data, content: hashable(data.content, sortOptions) }))
.map(({ file, content }) => inPlace
? fs.writeFile(file, JSON.stringify(content, null, 2), () => {})
? fs.writeFile(file, JSON.stringify(content, null, 2), () => { })
: content
)

const dataInputHandler = data => {
try {
return hashable(JSON.parse(data), priority)
return hashable(JSON.parse(data), sortOptions)
} catch (e) {
console.error('JSON.parse', e)
process.exit(1)
Expand All @@ -52,30 +65,48 @@ async function useCommandLine(argv) {
}
}

function hashable(data, priority = defaultSort) {
function hashable(data, { priority = defaultSort, sortObject = false } = {}) {
if (!_isObject(data)) return data

const sorted = {}
Object.keys(data).forEach(key => {
// array
if (Array.isArray(data[key])) {
data[key] = data[key].filter(e => e !== null).sort((a, b) => {
if (['number', 'string'].includes(typeof a)) return sortBy(a, b)

for (var i in priority) {
const field = priority[i]
const first = _get(a, field)
if (typeof first !== 'undefined') return sortBy(first, _get(b, field))
}
console.error('hashable sorting', a, b)
})
return data[key].map(obj => hashable(obj, priority))
sorted[key] = data[key].filter(e => e !== null)
.sort((a, b) => {
if (['number', 'string'].includes(typeof a)) return sortBy(a, b)

for (var i in priority) {
const field = priority[i]
const first = _get(a, field)
if (typeof first !== 'undefined') return sortBy(first, _get(b, field))
}
console.error('hashable sorting', a, b)
})
.map(obj => hashable(obj, { priority, sortObject }))
}

// object
else if (_isObject(data[key])) {
sorted[key] = hashable(data[key], { priority, sortObject })
}
if (_isObject(data[key])) {
return hashable(data[key], priority)

// primitive
else {
sorted[key] = data[key]
}
return data[key]
})

return data
if (sortObject) {
return Object.keys(sorted)
.sort()
.reduce((accumulator, key) => {
accumulator[key] = sorted[key]
return accumulator
}, {})
}

return sorted
}

function sortBy(a, b) {
Expand Down