diff --git a/__fixtures__/expected.json b/__fixtures__/expected.json index 8abc136..f6105be 100644 --- a/__fixtures__/expected.json +++ b/__fixtures__/expected.json @@ -1 +1 @@ -[{"name":"common","value":[{"name":"follow","stage":"added","value":false},{"name":"setting1","stage":"unchanged","value":"Value 1"},{"name":"setting2","stage":"removed","value":200},{"name":"setting3","stage":"updated","value":{"removed":true,"added":null}},{"name":"setting4","stage":"added","value":"blah blah"},{"name":"setting5","stage":"added","value":{"key5":"value5"}},{"name":"setting6","value":[{"name":"doge","value":[{"name":"wow","stage":"updated","value":{"removed":"","added":"so much"}}],"stage":"nested"},{"name":"key","stage":"unchanged","value":"value"},{"name":"ops","stage":"added","value":"vops"}],"stage":"nested"}],"stage":"nested"},{"name":"group1","value":[{"name":"baz","stage":"updated","value":{"removed":"bas","added":"bars"}},{"name":"foo","stage":"unchanged","value":"bar"},{"name":"nest","stage":"updated","value":{"removed":{"key":"value"},"added":"str"}}],"stage":"nested"},{"name":"group2","stage":"removed","value":{"abc":12345,"deep":{"id":45}}},{"name":"group3","stage":"added","value":{"deep":{"id":{"number":45}},"fee":100500}}] \ No newline at end of file +[{"name":"common","children":[{"name":"follow","stage":"added","value":false},{"name":"setting1","stage":"unchanged","value":"Value 1"},{"name":"setting2","stage":"removed","value":200},{"name":"setting3","stage":"updated","value":{"removed":true,"added":null}},{"name":"setting4","stage":"added","value":"blah blah"},{"name":"setting5","stage":"added","value":{"key5":"value5"}},{"name":"setting6","children":[{"name":"doge","children":[{"name":"wow","stage":"updated","value":{"removed":"","added":"so much"}}],"stage":"nested"},{"name":"key","stage":"unchanged","value":"value"},{"name":"ops","stage":"added","value":"vops"}],"stage":"nested"}],"stage":"nested"},{"name":"group1","children":[{"name":"baz","stage":"updated","value":{"removed":"bas","added":"bars"}},{"name":"foo","stage":"unchanged","value":"bar"},{"name":"nest","stage":"updated","value":{"removed":{"key":"value"},"added":"str"}}],"stage":"nested"},{"name":"group2","stage":"removed","value":{"abc":12345,"deep":{"id":45}}},{"name":"group3","stage":"added","value":{"deep":{"id":{"number":45}},"fee":100500}}] \ No newline at end of file diff --git a/bin/gendiff.js b/bin/gendiff.js index b182441..3d972dd 100755 --- a/bin/gendiff.js +++ b/bin/gendiff.js @@ -10,16 +10,7 @@ program .option('-f, --format ', 'output format', 'stylish') .action((filepath1, filepath2) => { const { format } = program.opts(); - switch (format) { - case ('stylish'): - console.log(compare(filepath1, filepath2, 'stylish')); - break; - case ('plain'): - console.log(compare(filepath1, filepath2, 'plain')); - break; - default: - console.log('Unknown format'); - } + console.log(compare(filepath1, filepath2, format)); }); program.parse(); diff --git a/formatters/plain.js b/formatters/plain.js deleted file mode 100644 index cc32f00..0000000 --- a/formatters/plain.js +++ /dev/null @@ -1,47 +0,0 @@ -import _ from 'lodash'; - -const setQuotes = (item) => { - if (typeof item === 'string') { - return `'${item}'`; - } - return item; -}; - -const plain = (tree) => { - const iter = (item, ancestry) => { - const currentProperty = _.isPlainObject(item) ? Object.keys(item) : item; - - const result = currentProperty.reduce((acc, node) => { - const value = _.isObject(node.value) ? '[complex value]' : setQuotes(node.value); - const { name } = node; - const newAncestry = `${ancestry}.${name}`; - const { stage } = node; - let tail; - let removed; - let added; - if (stage !== 'unchanged') { - switch (stage) { - case 'nested': - acc.push(iter(node.value, newAncestry)); - return acc; - case 'added': - tail = `added with value: ${value}`; - break; - case 'removed': - tail = 'removed'; - break; - default: - removed = _.isObject(node.value.removed) ? '[complex value]' : `${setQuotes(node.value.removed)}`; - added = _.isObject(node.value.added) ? '[complex value]' : `${setQuotes(node.value.added)}`; - tail = `updated. From ${removed} to ${added}`; - } - acc.push([`Property '${newAncestry.slice(1)}' was ${tail}`]); - acc.flat(); - } - return acc; - }, []); - return result.join('\n'); - }; - return iter(tree, ''); -}; -export default plain; diff --git a/formatters/stylish.js b/formatters/stylish.js deleted file mode 100644 index 425555d..0000000 --- a/formatters/stylish.js +++ /dev/null @@ -1,47 +0,0 @@ -import _ from 'lodash'; - -const stylish = (tree, replacer = ' ', spacesCount = 4) => { - const iter = (item, depth) => { - if (!_.isObject(item)) { - return `${item}`; - } - const indentSize = ((depth * spacesCount) - 2); - const currentIndent = replacer.repeat(indentSize); - const bracketIndent = replacer.repeat(indentSize - 2); - const currentValue = _.isPlainObject(item) ? Object.keys(item) : item; - let removed; - let added; - const line = currentValue.map((node) => { - let key = node.name || node; - const { stage } = node; - - let value = item[key] || _.cloneDeep(node.value); - switch (stage) { - case 'added': - key = `+ ${key}`; - break; - case 'removed': - key = `- ${key}`; - break; - case 'updated': - removed = iter(value.removed, depth + 1); - added = iter(value.added, depth + 1); - return `${currentIndent}- ${node.name}: ${removed}\n${currentIndent}+ ${node.name}: ${added}`; - default: - key = ` ${key}`; - } - value = _.isObject(value) ? iter(value, depth + 1) : value; - - return `${currentIndent}${key}: ${value}`; - }); - - return [ - '{', - ...line, - `${bracketIndent}}`, - ].join('\n'); - }; - return iter(tree, 1); -}; - -export default stylish; diff --git a/package.json b/package.json index c97294f..facb1b3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@hexlet/code", "version": "1.0.0", "type": "module", - "description": "[![Actions Status](https://github.com/yonamin/frontend-project-46/workflows/hexlet-check/badge.svg)](https://github.com/yonamin/frontend-project-46/actions)", + "description": "gendiff can find the difference between two data structures", "main": "src/index.js", "bin": { "gendiff": "bin/gendiff.js" diff --git a/formatters/index.js b/src/formatters/index.js similarity index 100% rename from formatters/index.js rename to src/formatters/index.js diff --git a/formatters/json.js b/src/formatters/json.js similarity index 100% rename from formatters/json.js rename to src/formatters/json.js diff --git a/src/formatters/plain.js b/src/formatters/plain.js new file mode 100644 index 0000000..a9c4d39 --- /dev/null +++ b/src/formatters/plain.js @@ -0,0 +1,48 @@ +import _ from 'lodash'; + +const normalizeValue = (item) => { + if (_.isObject(item)) { + return '[complex value]'; + } + if (typeof item === 'string') { + return `'${item}'`; + } + return item; +}; + +const plain = (tree) => { + const iter = (item, ancestry) => { + const lines = item + .filter((node) => node.stage !== 'unchanged') + .flatMap((node) => { + const { stage } = node; + const { name } = node; + const newAncestry = `${ancestry}.${name}`; + if (stage === 'nested') { + return iter(node.children, newAncestry); + } + const getTail = (status) => { + const value = normalizeValue(node.value); + switch (status) { + case 'added': + return `added with value: ${value}`; + case 'removed': + return 'removed'; + case 'updated': { + const removed = `${normalizeValue(node.value.removed)}`; + const added = `${normalizeValue(node.value.added)}`; + return `updated. From ${removed} to ${added}`; + } + default: + return `Unknown stage '${status}'`; + } + }; + + const tail = getTail(stage); + return `Property '${newAncestry.slice(1)}' was ${tail}`; + }); + return lines.join('\n'); + }; + return iter(tree, ''); +}; +export default plain; diff --git a/src/formatters/stylish.js b/src/formatters/stylish.js new file mode 100644 index 0000000..d8dcf89 --- /dev/null +++ b/src/formatters/stylish.js @@ -0,0 +1,56 @@ +import _ from 'lodash'; + +const stylish = (tree, replacer = ' ', spacesCount = 4) => { + const stringify = (item, depth) => { + if (!_.isObject(item) || item === null) { + return String(item); + } + + const indentSize = ((depth * spacesCount) - 2); + const currentIndent = replacer.repeat(indentSize); + const bracketIndent = replacer.repeat(indentSize - 2); + + const currentVal = _.isPlainObject(item) ? Object.entries(item) : item; + const lines = currentVal.flatMap((node) => { + const { stage } = node; + if (!stage) { + const [key, value] = node; + return `${currentIndent} ${key}: ${stringify(value, depth + 1)}`; + } + const getSign = (status) => { + switch (status) { + case 'added': + return '+'; + case 'removed': + return '-'; + default: + return ' '; + } + }; + + const sign = getSign(stage); + const key = `${sign} ${node.name}`; + const value = stringify(node.value, depth + 1); + + if (stage === 'nested') { + return `${currentIndent}${key}: ${stringify(node.children, depth + 1)}`; + } + if (stage === 'updated') { + const removed = stringify(node.value.removed, depth + 1); + const added = stringify(node.value.added, depth + 1); + return `${currentIndent}- ${node.name}: ${removed}\n${currentIndent}+ ${node.name}: ${added}`; + } + + return `${currentIndent}${key}: ${value}`; + }); + + return [ + '{', + ...lines, + `${bracketIndent}}`, + ].join('\n'); + }; + + return stringify(tree, 1); +}; +export default stylish; diff --git a/src/index.js b/src/index.js index ac75c18..1d226df 100644 --- a/src/index.js +++ b/src/index.js @@ -3,52 +3,49 @@ import path from 'path'; import { cwd } from 'process'; import _ from 'lodash'; import parse from './parsers.js'; -import chooseFormat from '../formatters/index.js'; +import chooseFormat from './formatters/index.js'; -const gendiff = (file1, file2) => { - const diff = (data1, data2) => { - const keys = _.sortBy(_.union(Object.keys(data1), Object.keys(data2))); - const result = keys.map((key) => { - const value1 = _.cloneDeep(data1[key]); - const value2 = _.cloneDeep(data2[key]); - const node = {}; - node.name = key; +const buildTree = (file1, file2) => { + const keys = _.sortBy(_.union(Object.keys(file1), Object.keys(file2))); + const result = keys.map((key) => { + const value1 = _.cloneDeep(file1[key]); + const value2 = _.cloneDeep(file2[key]); + const node = {}; + node.name = key; - if (!Object.hasOwn(data1, key)) { - node.stage = 'added'; - node.value = value2; - return node; - } - if (!Object.hasOwn(data2, key)) { - node.stage = 'removed'; - node.value = value1; - return node; - } - if ((_.isPlainObject(value1)) && (_.isPlainObject(value2))) { - node.value = diff(value1, value2); - node.stage = 'nested'; - return node; - } - - if (value1 === value2) { - node.stage = 'unchanged'; - node.value = value1; - return node; - } + if (!Object.hasOwn(file1, key)) { + node.stage = 'added'; + node.value = value2; + return node; + } + if (!Object.hasOwn(file2, key)) { + node.stage = 'removed'; + node.value = value1; + return node; + } + if ((_.isPlainObject(value1)) && (_.isPlainObject(value2))) { + node.children = buildTree(value1, value2); + node.stage = 'nested'; + return node; + } - node.stage = 'updated'; - node.value = { removed: value1, added: value2 }; + if (value1 === value2) { + node.stage = 'unchanged'; + node.value = value1; return node; - }); - return result; - }; - return diff(file1, file2); + } + + node.stage = 'updated'; + node.value = { removed: value1, added: value2 }; + return node; + }); + return result; }; export default (filepath1, filepath2, format = 'stylish') => { - const extname1 = path.extname(filepath1); - const extname2 = path.extname(filepath2); - const parsedFile1 = parse(readFileSync(path.resolve(cwd(), filepath1)), extname1); - const parsedFile2 = parse(readFileSync(path.resolve(cwd(), filepath2)), extname2); - return chooseFormat(gendiff(parsedFile1, parsedFile2), format); + const fileFormat1 = path.extname(filepath1).slice(1); + const fileFormat2 = path.extname(filepath2).slice(1); + const parsedFile1 = parse(readFileSync(path.resolve(cwd(), filepath1)), fileFormat1); + const parsedFile2 = parse(readFileSync(path.resolve(cwd(), filepath2)), fileFormat2); + return chooseFormat(buildTree(parsedFile1, parsedFile2), format); }; diff --git a/src/parsers.js b/src/parsers.js index 0706f72..f620d3c 100644 --- a/src/parsers.js +++ b/src/parsers.js @@ -1,14 +1,15 @@ import yaml from 'js-yaml'; -const parse = (file, extname) => { - let parsed; - if (extname === '.json') { - parsed = JSON.parse(file); +const parse = (data, format) => { + switch (format) { + case 'json': + return JSON.parse(data); + case 'yaml': + case 'yml': + return yaml.load(data); + default: + throw new Error('Unknown format'); } - if (extname === '.yaml' || extname === '.yml') { - parsed = yaml.load(file); - } - return parsed; }; export default parse;