mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-21 23:19:30 +02:00
feat: add JSON comparison tool
This commit is contained in:
@@ -5,6 +5,7 @@ import { tool as validateJson } from './validateJson/meta';
|
|||||||
import { tool as jsonToXml } from './json-to-xml/meta';
|
import { tool as jsonToXml } from './json-to-xml/meta';
|
||||||
import { tool as escapeJson } from './escape-json/meta';
|
import { tool as escapeJson } from './escape-json/meta';
|
||||||
import { tool as tsvToJson } from './tsv-to-json/meta';
|
import { tool as tsvToJson } from './tsv-to-json/meta';
|
||||||
|
import { tool as jsonComparison } from './json-comparison/meta';
|
||||||
|
|
||||||
export const jsonTools = [
|
export const jsonTools = [
|
||||||
validateJson,
|
validateJson,
|
||||||
@@ -13,5 +14,6 @@ export const jsonTools = [
|
|||||||
jsonStringify,
|
jsonStringify,
|
||||||
jsonToXml,
|
jsonToXml,
|
||||||
escapeJson,
|
escapeJson,
|
||||||
tsvToJson
|
tsvToJson,
|
||||||
|
jsonComparison
|
||||||
];
|
];
|
||||||
|
157
src/pages/tools/json/json-comparison/index.tsx
Normal file
157
src/pages/tools/json/json-comparison/index.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import ToolContent from '@components/ToolContent';
|
||||||
|
import ToolTextInput from '@components/input/ToolTextInput';
|
||||||
|
import ToolTextResult from '@components/result/ToolTextResult';
|
||||||
|
import { compareJson } from './service';
|
||||||
|
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||||
|
import { ToolComponentProps } from '@tools/defineTool';
|
||||||
|
import { Box, Grid, styled } from '@mui/material';
|
||||||
|
|
||||||
|
const StyledContainer = styled(Box)({
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '500px',
|
||||||
|
marginBottom: '20px'
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledGrid = styled(Grid)({
|
||||||
|
flex: 1,
|
||||||
|
'& .MuiGrid-item': {
|
||||||
|
height: '100%'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledInputWrapper = styled(Box)({
|
||||||
|
height: '100%',
|
||||||
|
'& > div': {
|
||||||
|
height: '100%',
|
||||||
|
'& textarea': {
|
||||||
|
height: '100% !important',
|
||||||
|
minHeight: '450px',
|
||||||
|
resize: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.5',
|
||||||
|
padding: '12px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type InitialValuesType = {};
|
||||||
|
|
||||||
|
const initialValues: InitialValuesType = {};
|
||||||
|
|
||||||
|
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||||
|
{
|
||||||
|
title: 'Compare Simple JSON Objects',
|
||||||
|
description:
|
||||||
|
'Compare two JSON objects to find differences in their structure and values.',
|
||||||
|
sampleText: `{
|
||||||
|
"name": "John",
|
||||||
|
"age": 30,
|
||||||
|
"address": {
|
||||||
|
"city": "New York",
|
||||||
|
"country": "USA"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
sampleResult: `{
|
||||||
|
"name": "John",
|
||||||
|
"age": 25,
|
||||||
|
"address": {
|
||||||
|
"city": "London",
|
||||||
|
"country": "UK"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
sampleOptions: {
|
||||||
|
...initialValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function JsonComparison({ title }: ToolComponentProps) {
|
||||||
|
const [input1, setInput1] = useState<string>('');
|
||||||
|
const [input2, setInput2] = useState<string>('');
|
||||||
|
const [result, setResult] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const compareInputs = () => {
|
||||||
|
try {
|
||||||
|
// Only compare if at least one input has content
|
||||||
|
if (input1.trim() || input2.trim()) {
|
||||||
|
const differences = compareJson(
|
||||||
|
input1 || '{}',
|
||||||
|
input2 || '{}',
|
||||||
|
'text'
|
||||||
|
);
|
||||||
|
setResult(differences);
|
||||||
|
} else {
|
||||||
|
setResult('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setResult(
|
||||||
|
`Error: ${
|
||||||
|
error instanceof Error ? error.message : 'Invalid JSON format'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
compareInputs();
|
||||||
|
}, [input1, input2]);
|
||||||
|
|
||||||
|
const handleInput1Change = (value: string) => {
|
||||||
|
setInput1(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput2Change = (value: string) => {
|
||||||
|
setInput2(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolContent
|
||||||
|
title={title}
|
||||||
|
input={input1}
|
||||||
|
setInput={setInput1}
|
||||||
|
initialValues={initialValues}
|
||||||
|
compute={() => {}}
|
||||||
|
exampleCards={exampleCards}
|
||||||
|
inputComponent={
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledGrid container spacing={2}>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<StyledInputWrapper>
|
||||||
|
<ToolTextInput
|
||||||
|
title="First JSON"
|
||||||
|
value={input1}
|
||||||
|
onChange={handleInput1Change}
|
||||||
|
placeholder="Paste your first JSON here..."
|
||||||
|
/>
|
||||||
|
</StyledInputWrapper>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<StyledInputWrapper>
|
||||||
|
<ToolTextInput
|
||||||
|
title="Second JSON"
|
||||||
|
value={input2}
|
||||||
|
onChange={handleInput2Change}
|
||||||
|
placeholder="Paste your second JSON here..."
|
||||||
|
/>
|
||||||
|
</StyledInputWrapper>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<StyledInputWrapper>
|
||||||
|
<ToolTextResult
|
||||||
|
title="Differences"
|
||||||
|
value={result}
|
||||||
|
extension={'txt'}
|
||||||
|
/>
|
||||||
|
</StyledInputWrapper>
|
||||||
|
</Grid>
|
||||||
|
</StyledGrid>
|
||||||
|
</StyledContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
13
src/pages/tools/json/json-comparison/meta.ts
Normal file
13
src/pages/tools/json/json-comparison/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineTool } from '@tools/defineTool';
|
||||||
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const tool = defineTool('json', {
|
||||||
|
name: 'Compare JSON',
|
||||||
|
path: 'json-comparison',
|
||||||
|
icon: 'fluent:branch-compare-24-regular',
|
||||||
|
description:
|
||||||
|
'Compare two JSON objects to identify differences in structure and values.',
|
||||||
|
shortDescription: 'Find differences between two JSON objects',
|
||||||
|
keywords: ['json', 'compare', 'diff', 'differences', 'match', 'validation'],
|
||||||
|
component: lazy(() => import('./index'))
|
||||||
|
});
|
64
src/pages/tools/json/json-comparison/service.test.ts
Normal file
64
src/pages/tools/json/json-comparison/service.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { compareJson } from './service';
|
||||||
|
|
||||||
|
describe('compareJson', () => {
|
||||||
|
it('should identify missing properties', () => {
|
||||||
|
const json1 = '{"name": "John", "age": 30}';
|
||||||
|
const json2 = '{"name": "John"}';
|
||||||
|
|
||||||
|
expect(compareJson(json1, json2, 'text')).toContain(
|
||||||
|
'age: Missing in second JSON'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should identify value mismatches', () => {
|
||||||
|
const json1 = '{"name": "John", "age": 30}';
|
||||||
|
const json2 = '{"name": "John", "age": 25}';
|
||||||
|
|
||||||
|
expect(compareJson(json1, json2, 'text')).toContain(
|
||||||
|
'age: Mismatch: 30 != 25'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested objects', () => {
|
||||||
|
const json1 = '{"person": {"name": "John", "age": 30}}';
|
||||||
|
const json2 = '{"person": {"name": "Jane", "age": 30}}';
|
||||||
|
|
||||||
|
expect(compareJson(json1, json2, 'text')).toContain(
|
||||||
|
'person.name: Mismatch: John != Jane'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return JSON format when specified', () => {
|
||||||
|
const json1 = '{"name": "John", "age": 30}';
|
||||||
|
const json2 = '{"name": "Jane", "age": 25}';
|
||||||
|
|
||||||
|
const result = compareJson(json1, json2, 'json');
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
|
||||||
|
expect(parsed).toHaveProperty('name');
|
||||||
|
expect(parsed).toHaveProperty('age');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle arrays', () => {
|
||||||
|
const json1 = '{"numbers": [1, 2, 3]}';
|
||||||
|
const json2 = '{"numbers": [1, 2, 4]}';
|
||||||
|
|
||||||
|
expect(compareJson(json1, json2, 'text')).toContain(
|
||||||
|
'numbers.2: Mismatch: 3 != 4'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "No differences found" for identical JSONs', () => {
|
||||||
|
const json1 = '{"name": "John", "age": 30}';
|
||||||
|
const json2 = '{"name": "John", "age": 30}';
|
||||||
|
|
||||||
|
expect(compareJson(json1, json2, 'text')).toBe('No differences found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for invalid JSON', () => {
|
||||||
|
const json1 = '{"name": "John"';
|
||||||
|
const json2 = '{"name": "John"}';
|
||||||
|
|
||||||
|
expect(() => compareJson(json1, json2, 'text')).toThrow();
|
||||||
|
});
|
||||||
|
});
|
212
src/pages/tools/json/json-comparison/service.ts
Normal file
212
src/pages/tools/json/json-comparison/service.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
const fixTrailingCommas = (json: string): string => {
|
||||||
|
// Replace trailing commas in objects and arrays with proper JSON syntax
|
||||||
|
return json
|
||||||
|
.replace(/,\s*([}\]])/g, '$1') // Remove trailing commas in objects and arrays
|
||||||
|
.replace(/,\s*\n\s*([}\]])/g, '\n$1'); // Also handle when the closing bracket is on a new line
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryParseJSON = (
|
||||||
|
json: string
|
||||||
|
): { valid: boolean; data?: any; error?: string } => {
|
||||||
|
if (!json.trim()) {
|
||||||
|
return { valid: true, data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse after fixing trailing commas
|
||||||
|
const fixedJson = fixTrailingCommas(json);
|
||||||
|
const data = JSON.parse(fixedJson);
|
||||||
|
return { valid: true, data };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error:
|
||||||
|
error instanceof SyntaxError
|
||||||
|
? `Invalid JSON: ${error.message}`
|
||||||
|
: 'Invalid JSON format'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compareJson = (
|
||||||
|
json1: string,
|
||||||
|
json2: string,
|
||||||
|
format: 'text' | 'json'
|
||||||
|
): string => {
|
||||||
|
// Handle empty inputs
|
||||||
|
if (!json1.trim() && !json2.trim()) return '';
|
||||||
|
|
||||||
|
// Parse both JSON inputs
|
||||||
|
const parsed1 = tryParseJSON(json1);
|
||||||
|
const parsed2 = tryParseJSON(json2);
|
||||||
|
|
||||||
|
// Handle parsing errors
|
||||||
|
if (!parsed1.valid || !parsed2.valid) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
parsed1.valid ? null : parsed1.error,
|
||||||
|
parsed2.valid ? null : parsed2.error
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the valid JSON objects
|
||||||
|
if (format === 'json') {
|
||||||
|
const diffs = findDifferencesJSON(parsed1.data, parsed2.data);
|
||||||
|
return JSON.stringify(diffs);
|
||||||
|
} else {
|
||||||
|
const differences = findDifferencesText(parsed1.data, parsed2.data);
|
||||||
|
if (differences.length === 0) {
|
||||||
|
return 'No differences found';
|
||||||
|
}
|
||||||
|
return differences.join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDifferencesText = (
|
||||||
|
obj1: any,
|
||||||
|
obj2: any,
|
||||||
|
path: string[] = []
|
||||||
|
): string[] => {
|
||||||
|
const differences: string[] = [];
|
||||||
|
const processPath = (p: string[]): string =>
|
||||||
|
p.length ? p.join('.') : 'root';
|
||||||
|
|
||||||
|
// Compare all keys in obj1
|
||||||
|
for (const key in obj1) {
|
||||||
|
const currentPath = [...path, key];
|
||||||
|
|
||||||
|
if (!(key in obj2)) {
|
||||||
|
differences.push(`${processPath(currentPath)}: Missing in second JSON`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value1 = obj1[key];
|
||||||
|
const value2 = obj2[key];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof value1 === 'object' &&
|
||||||
|
value1 !== null &&
|
||||||
|
typeof value2 === 'object' &&
|
||||||
|
value2 !== null
|
||||||
|
) {
|
||||||
|
differences.push(...findDifferencesText(value1, value2, currentPath));
|
||||||
|
} else if (value1 !== value2) {
|
||||||
|
differences.push(
|
||||||
|
`${processPath(currentPath)}: Mismatch: ${value1} != ${value2}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for keys in obj2 that don't exist in obj1
|
||||||
|
for (const key in obj2) {
|
||||||
|
if (!(key in obj1)) {
|
||||||
|
const currentPath = [...path, key];
|
||||||
|
differences.push(`${processPath(currentPath)}: Missing in first JSON`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return differences;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDifferencesJSON = (obj1: any, obj2: any): Record<string, string> => {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Compare all properties
|
||||||
|
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
|
||||||
|
|
||||||
|
for (const key of allKeys) {
|
||||||
|
if (!(key in obj1)) {
|
||||||
|
result[key] = 'Missing in first JSON';
|
||||||
|
} else if (!(key in obj2)) {
|
||||||
|
result[key] = 'Missing in second JSON';
|
||||||
|
} else if (obj1[key] !== obj2[key]) {
|
||||||
|
result[key] = `Mismatch: ${obj1[key]} != ${obj2[key]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDifferences = (
|
||||||
|
obj1: any,
|
||||||
|
obj2: any,
|
||||||
|
path: string[] = []
|
||||||
|
): string[] => {
|
||||||
|
const differences: string[] = [];
|
||||||
|
|
||||||
|
// Helper to format values for display
|
||||||
|
const formatValue = (value: any): string => {
|
||||||
|
if (value === undefined) return 'undefined';
|
||||||
|
if (value === null) return 'null';
|
||||||
|
if (typeof value === 'string') return `"${value}"`;
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to get type description
|
||||||
|
const getTypeDescription = (value: any): string => {
|
||||||
|
if (value === null) return 'null';
|
||||||
|
if (Array.isArray(value)) return 'array';
|
||||||
|
return typeof value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processPath = (p: string[]): string =>
|
||||||
|
p.length ? p.join('.') : 'root';
|
||||||
|
|
||||||
|
// Compare all keys in obj1
|
||||||
|
for (const key in obj1) {
|
||||||
|
const currentPath = [...path, key];
|
||||||
|
|
||||||
|
if (!(key in obj2)) {
|
||||||
|
differences.push(
|
||||||
|
`Property ${processPath(
|
||||||
|
currentPath
|
||||||
|
)} exists only in first JSON:\n ${formatValue(obj1[key])}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value1 = obj1[key];
|
||||||
|
const value2 = obj2[key];
|
||||||
|
const type1 = getTypeDescription(value1);
|
||||||
|
const type2 = getTypeDescription(value2);
|
||||||
|
|
||||||
|
if (type1 !== type2) {
|
||||||
|
differences.push(
|
||||||
|
`Type mismatch at ${processPath(
|
||||||
|
currentPath
|
||||||
|
)}:\n First: ${type1} (${formatValue(
|
||||||
|
value1
|
||||||
|
)})\n Second: ${type2} (${formatValue(value2)})`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type1 === 'object' || type1 === 'array') {
|
||||||
|
const childDiffs = findDifferences(value1, value2, currentPath);
|
||||||
|
differences.push(...childDiffs);
|
||||||
|
} else if (value1 !== value2) {
|
||||||
|
differences.push(
|
||||||
|
`Value mismatch at ${processPath(currentPath)}:\n First: ${formatValue(
|
||||||
|
value1
|
||||||
|
)}\n Second: ${formatValue(value2)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for keys in obj2 that don't exist in obj1
|
||||||
|
for (const key in obj2) {
|
||||||
|
if (!(key in obj1)) {
|
||||||
|
const currentPath = [...path, key];
|
||||||
|
differences.push(
|
||||||
|
`Property ${processPath(
|
||||||
|
currentPath
|
||||||
|
)} exists only in second JSON:\n ${formatValue(obj2[key])}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return differences;
|
||||||
|
};
|
Reference in New Issue
Block a user