Skip to content

Commit

Permalink
feat(new-tool): diff of two json objects
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh committed Apr 23, 2023
1 parent 61ece23 commit 362f2fa
Show file tree
Hide file tree
Showing 12 changed files with 751 additions and 134 deletions.
264 changes: 133 additions & 131 deletions components.d.ts

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions src/composable/copy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useClipboard } from '@vueuse/core';
import { useClipboard, type MaybeRef } from '@vueuse/core';
import { useMessage } from 'naive-ui';
import type { Ref } from 'vue';

export function useCopy({ source, text = 'Copied to the clipboard' }: { source: Ref; text?: string }) {
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<string>; text?: string }) {
const { copy } = useClipboard({ source });
const message = useMessage();

Expand Down
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as jsonDiff } from './json-diff';
import { tool as ipv4RangeExpander } from './ipv4-range-expander';
import { tool as httpStatusCodes } from './http-status-codes';
import { tool as yamlToJson } from './yaml-to-json-converter';
Expand Down Expand Up @@ -91,6 +92,7 @@ export const toolsByCategory: ToolCategory[] = [
htmlWysiwygEditor,
userAgentParser,
httpStatusCodes,
jsonDiff,
],
},
{
Expand Down
119 changes: 119 additions & 0 deletions src/tools/json-diff/diff-viewer/diff-viewer.models.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import _ from 'lodash';
import { useCopy } from '@/composable/copy';
import type { Difference, ArrayDifference, ObjectDifference } from '../json-diff.types';

export const DiffRootViewer = ({ diff }: { diff: Difference }) => {
return (
<div class={'diffs-viewer'}>
<ul>{DiffViewer({ diff, showKeys: false })}</ul>
</div>
);
};

const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) => {
const { type, status } = diff;

if (status === 'updated') {
return ComparisonViewer({ diff, showKeys });
}

if (type === 'array') {
return ChildrenViewer({ diff, showKeys, showChildrenKeys: false, openTag: '[', closeTag: ']' });
}

if (type === 'object') {
return ChildrenViewer({ diff, showKeys, openTag: '{', closeTag: '}' });
}

return LineDiffViewer({ diff, showKeys });
};

const LineDiffViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => {
const { value, key, status, oldValue } = diff;
const valueToDisplay = status === 'removed' ? oldValue : value;

return (
<li>
<span class={[status, 'result']}>
{showKeys && (
<>
<span class={'key'}>{key}</span>
{': '}
</>
)}
{Value({ value: valueToDisplay, status })}
</span>
,
</li>
);
};

const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => {
const { value, key, oldValue } = diff;

return (
<li class={'updated-line'}>
{showKeys && (
<>
<span class={'key'}>{key}</span>
{': '}
</>
)}
{Value({ value: oldValue, status: 'removed' })}
{Value({ value, status: 'added' })},
</li>
);
};

const ChildrenViewer = ({
diff,
openTag,
closeTag,
showKeys,
showChildrenKeys = true,
}: {
diff: ArrayDifference | ObjectDifference;
showKeys: boolean;
showChildrenKeys?: boolean;
openTag: string;
closeTag: string;
}) => {
const { children, key, status, type } = diff;

return (
<li>
<div class={[type, status]} style={{ display: 'inline-block' }}>
{showKeys && (
<>
<span class={'key'}>{key}</span>
{': '}
</>
)}

{openTag}
{children.length > 0 && <ul>{children.map((diff) => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>}
{closeTag + ','}
</div>
</li>
);
};

function formatValue(value: unknown) {
if (_.isNull(value)) {
return 'null';
}

return JSON.stringify(value);
}

const Value = ({ value, status }: { value: unknown; status: string }) => {
const formatedValue = formatValue(value);

const { copy } = useCopy({ source: formatedValue });

return (
<span class={['value', status]} onClick={copy}>
{formatedValue}
</span>
);
};
110 changes: 110 additions & 0 deletions src/tools/json-diff/diff-viewer/diff-viewer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<template>
<div v-if="showResults">
<n-space justify="center">
<n-form-item label="Only show differences" label-placement="left">
<n-switch v-model:value="onlyShowDifferences" />
</n-form-item>
</n-space>

<c-card data-test-id="diff-result">
<n-text v-if="jsonAreTheSame" depth="3" block text-center italic> The provided JSONs are the same </n-text>
<diff-root-viewer v-else :diff="result" />
</c-card>
</div>
</template>

<script lang="ts" setup>
import { useAppTheme } from '@/ui/theme/themes';
import _ from 'lodash';
import { DiffRootViewer } from './diff-viewer.models';
import { diff } from '../json-diff.models';
const onlyShowDifferences = ref(false);
const props = defineProps<{ leftJson: unknown; rightJson: unknown }>();
const { leftJson, rightJson } = toRefs(props);
const appTheme = useAppTheme();
const result = computed(() =>
diff(leftJson.value, rightJson.value, { onlyShowDifferences: onlyShowDifferences.value }),
);
const jsonAreTheSame = computed(() => _.isEqual(leftJson.value, rightJson.value));
const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value));
</script>

<style lang="less" scoped>
::v-deep(.diffs-viewer) {
color: v-bind('appTheme.text.mutedColor');
& > ul {
padding-left: 0 !important;
}
ul {
list-style: none;
padding-left: 20px;
margin: 0;
li {
.updated-line {
padding: 3px 0;
}
.result,
.array,
.object,
.value {
&:not(:last-child) {
margin-right: 4px;
}
&.added {
padding: 3px 5px;
border-radius: 4px;
background-color: v-bind('appTheme.success.colorFaded');
color: v-bind('appTheme.success.color');
.key {
color: inherit;
}
}
&.removed {
padding: 3px 5px;
border-radius: 4px;
background-color: v-bind('appTheme.error.colorFaded');
color: v-bind('appTheme.error.color');
.key {
color: inherit;
}
}
}
.value {
cursor: pointer;
border: 1px solid transparent;
transition: border-color 0.2s ease-in-out;
&.added:hover {
border-color: v-bind('appTheme.success.color');
}
&.removed:hover {
border-color: v-bind('appTheme.error.color');
}
}
.added .added,
.removed .removed {
background-color: transparent;
color: inherit;
}
.key {
font-weight: 500;
color: v-bind('appTheme.text.baseColor');
}
}
}
}
</style>
12 changes: 12 additions & 0 deletions src/tools/json-diff/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CompareArrowsRound } from '@vicons/material';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'JSON diff',
path: '/json-diff',
description: 'Compare two JSON objects and get the differences between them.',
keywords: ['json', 'diff', 'compare', 'difference', 'object', 'data'],
component: () => import('./json-diff.vue'),
icon: CompareArrowsRound,
createdAt: new Date('2023-04-20'),
});
39 changes: 39 additions & 0 deletions src/tools/json-diff/json-diff.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';

test.describe('Tool - JSON diff', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/json-diff');
});

test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('JSON diff - IT Tools');
});

test('Identical JSONs have a custom result message', async ({ page }) => {
await page.getByTestId('leftJson').fill('{"foo":"bar"}');
await page.getByTestId('rightJson').fill('{ "foo": "bar" } ');

const result = await page.getByTestId('diff-result').innerText();

expect(result).toContain('The provided JSONs are the same');
});

test('Different JSONs have differences listed', async ({ page }) => {
await page.getByTestId('leftJson').fill('{"foo":"bar"}');
await page.getByTestId('rightJson').fill('{"foo":"buz","baz":"qux"}');

const result = await page.getByTestId('diff-result').innerText();

expect(result).toContain(`{\nfoo: "bar""buz",\nbaz: "qux",\n},`);
});

test('Different JSONs have only differences listed when "Only show differences" is checked', async ({ page }) => {
await page.getByTestId('leftJson').fill('{"foo":"bar"}');
await page.getByTestId('rightJson').fill('{"foo":"bar","baz":"qux"}');
await page.getByRole('switch').click();

const result = await page.getByTestId('diff-result').innerText();

expect(result).toContain(`{\nbaz: "qux",\n},`);
});
});
80 changes: 80 additions & 0 deletions src/tools/json-diff/json-diff.models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect, describe, it } from 'vitest';
import { diff } from './json-diff.models';

describe('json-diff models', () => {
describe('diff', () => {
it('list object differences', () => {
const obj = { a: 1, b: 2 };
const newObj = { a: 1, b: 2, c: 3 };
const result = diff(obj, newObj);

expect(result).toEqual({
key: '',
type: 'object',
children: [
{
key: 'a',
type: 'value',
value: 1,
oldValue: 1,
status: 'unchanged',
},
{
key: 'b',
type: 'value',
value: 2,
oldValue: 2,
status: 'unchanged',
},
{
key: 'c',
type: 'value',
value: 3,
oldValue: undefined,
status: 'added',
},
],
oldValue: { a: 1, b: 2 },
value: { a: 1, b: 2, c: 3 },
status: 'children-updated',
});
});

it('list array differences', () => {
const obj = [1, 2];
const newObj = [1, 2, 3];
const result = diff(obj, newObj);

expect(result).toEqual({
key: '',
type: 'array',
children: [
{
key: 0,
type: 'value',
value: 1,
oldValue: 1,
status: 'unchanged',
},
{
key: 1,
type: 'value',
value: 2,
oldValue: 2,
status: 'unchanged',
},
{
key: 2,
type: 'value',
value: 3,
oldValue: undefined,
status: 'added',
},
],
oldValue: [1, 2],
value: [1, 2, 3],
status: 'children-updated',
});
});
});
});
Loading

1 comment on commit 362f2fa

@vercel
Copy link

@vercel vercel bot commented on 362f2fa Apr 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

it-tools – ./

it-tools-git-main-ctmsst.vercel.app
it-tools-ctmsst.vercel.app
it-tools.vercel.app
it-tools.tech

Please sign in to comment.