mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-08-06 14:05:18 +02:00
add json condenser tool
This commit is contained in:
parent
d7578e23a5
commit
fb6257b088
7 changed files with 247 additions and 0 deletions
1
components.d.ts
vendored
1
components.d.ts
vendored
|
@ -110,6 +110,7 @@ declare module '@vue/runtime-core' {
|
|||
Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default']
|
||||
Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default']
|
||||
Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default']
|
||||
JsonDataCondenser: typeof import('./src/tools/json-data-condenser/json-data-condenser.vue')['default']
|
||||
JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default']
|
||||
JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
|
||||
JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default']
|
||||
|
|
|
@ -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 jsonDataCondenser } from './json-data-condenser';
|
||||
import { tool as gzipDecompressor } from './gzip-decompressor';
|
||||
import { tool as emailNormalizer } from './email-normalizer';
|
||||
|
||||
|
@ -118,6 +119,7 @@ export const toolsByCategory: ToolCategory[] = [
|
|||
jsonToXml,
|
||||
markdownToHtml,
|
||||
gzipDecompressor,
|
||||
jsonDataCondenser,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
12
src/tools/json-data-condenser/index.ts
Normal file
12
src/tools/json-data-condenser/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ArrowsShuffle } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'Json data condenser',
|
||||
path: '/json-data-condenser',
|
||||
description: 'Removes duplicate-shaped objects in JSON arrays to simplify the data.',
|
||||
keywords: ['json', 'data', 'condenser'],
|
||||
component: () => import('./json-data-condenser.vue'),
|
||||
icon: ArrowsShuffle,
|
||||
createdAt: new Date('2025-06-26'),
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Tool - Json data condenser', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/json-data-condenser');
|
||||
});
|
||||
|
||||
test('Has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Json data condenser - IT Tools');
|
||||
});
|
||||
|
||||
test('Condenses valid JSON input correctly', async ({ page }) => {
|
||||
const validJson = JSON.stringify({
|
||||
users: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
{ id: 4, name: 'David', email: 'david@example.com' },
|
||||
{ id: 5, name: 'Eve' },
|
||||
],
|
||||
status: 'active',
|
||||
}, null, 2);
|
||||
|
||||
await page.getByPlaceholder('Paste a JSON payload here...').fill(validJson);
|
||||
await page.getByRole('button', { name: 'Condense JSON' }).click();
|
||||
|
||||
await expect(page.getByText('"status": "active"')).toBeVisible();
|
||||
await expect(page.getByText('"email": "david@example.com"')).toBeVisible();
|
||||
|
||||
// Only Alice and David (with different structure) should remain
|
||||
await expect(page.locator('textarea')).toContainText('Alice');
|
||||
await expect(page.locator('textarea')).toContainText('David');
|
||||
await expect(page.locator('textarea')).not.toContainText('Bob');
|
||||
await expect(page.locator('textarea')).not.toContainText('Charlie');
|
||||
await expect(page.locator('textarea')).not.toContainText('Eve');
|
||||
});
|
||||
|
||||
test('Displays error on invalid JSON input', async ({ page }) => {
|
||||
await page.getByPlaceholder('Paste a JSON payload here...').fill('{ invalid json ');
|
||||
await page.getByRole('button', { name: 'Condense JSON' }).click();
|
||||
|
||||
await expect(page.getByText('Invalid JSON input. Please fix and try again.')).toBeVisible();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { condenseJsonStructures } from './json-data-condenser.service';
|
||||
|
||||
describe('condenseJsonStructures', () => {
|
||||
it('removes duplicate object structures in an array', () => {
|
||||
const input = {
|
||||
users: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
{ id: 4, name: 'David', email: 'david@example.com' },
|
||||
{ id: 5, name: 'Eve' },
|
||||
],
|
||||
status: 'active',
|
||||
};
|
||||
|
||||
const output = condenseJsonStructures(input);
|
||||
|
||||
expect(output).toEqual({
|
||||
users: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 4, name: 'David', email: 'david@example.com' },
|
||||
],
|
||||
status: 'active',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps all non-array values intact', () => {
|
||||
const input = {
|
||||
status: 'ok',
|
||||
count: 5,
|
||||
success: true,
|
||||
metadata: {
|
||||
source: 'api',
|
||||
},
|
||||
};
|
||||
|
||||
const output = condenseJsonStructures(input);
|
||||
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
it('keeps non-object array values', () => {
|
||||
const input = [1, 2, 3, 'a', true, null];
|
||||
|
||||
const output = condenseJsonStructures(input);
|
||||
|
||||
expect(output).toEqual([1, 2, 3, 'a', true, null]);
|
||||
});
|
||||
|
||||
it('recursively condenses nested object arrays', () => {
|
||||
const input = {
|
||||
groups: [
|
||||
{
|
||||
name: 'Group A',
|
||||
users: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
{ id: 3, name: 'Charlie' },
|
||||
{ id: 4, name: 'David', email: 'david@example.com' },
|
||||
{ id: 5, name: 'Eve' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const output = condenseJsonStructures(input);
|
||||
|
||||
expect(output).toEqual({
|
||||
groups: [
|
||||
{
|
||||
name: 'Group A',
|
||||
users: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 4, name: 'David', email: 'david@example.com' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns primitive values unchanged', () => {
|
||||
expect(condenseJsonStructures(42)).toBe(42);
|
||||
expect(condenseJsonStructures('hello')).toBe('hello');
|
||||
expect(condenseJsonStructures(null)).toBe(null);
|
||||
expect(condenseJsonStructures(true)).toBe(true);
|
||||
});
|
||||
});
|
39
src/tools/json-data-condenser/json-data-condenser.service.ts
Normal file
39
src/tools/json-data-condenser/json-data-condenser.service.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
||||
function getKeySignature(obj: Record<string, any>): string {
|
||||
// Create a normalized signature string of sorted keys
|
||||
return Object.keys(obj).sort().join(',');
|
||||
}
|
||||
|
||||
export function condenseJsonStructures(data: JsonValue): JsonValue {
|
||||
if (Array.isArray(data)) {
|
||||
const seenSignatures = new Set<string>();
|
||||
const result: JsonValue[] = [];
|
||||
|
||||
for (const item of data) {
|
||||
if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
|
||||
const sig = getKeySignature(item);
|
||||
if (!seenSignatures.has(sig)) {
|
||||
seenSignatures.add(sig);
|
||||
result.push(condenseJsonStructures(item));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Keep non-object array items
|
||||
result.push(condenseJsonStructures(item));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
const result: Record<string, JsonValue> = {};
|
||||
for (const key in data) {
|
||||
result[key] = condenseJsonStructures(data[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
61
src/tools/json-data-condenser/json-data-condenser.vue
Normal file
61
src/tools/json-data-condenser/json-data-condenser.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { condenseJsonStructures } from './json-data-condenser.service';
|
||||
|
||||
const rawJson = ref('');
|
||||
const condensedJson = ref('');
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
function condense() {
|
||||
error.value = null;
|
||||
condensedJson.value = '';
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawJson.value);
|
||||
const condensed = condenseJsonStructures(parsed);
|
||||
condensedJson.value = JSON.stringify(condensed, null, 2);
|
||||
}
|
||||
catch (err: any) {
|
||||
error.value = 'Invalid JSON input. Please fix and try again.';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-card title="JSON Condenser" class="mx-auto max-w-4xl px-4">
|
||||
<!-- Input -->
|
||||
<div class="mb-2 font-semibold">
|
||||
Original JSON Input
|
||||
</div>
|
||||
<c-input-text
|
||||
v-model:value="rawJson"
|
||||
placeholder="Paste a JSON payload here..."
|
||||
class="mb-4"
|
||||
rows="12"
|
||||
multiline
|
||||
raw-text
|
||||
monospace
|
||||
/>
|
||||
<c-button @click="condense">
|
||||
Condense JSON
|
||||
</c-button>
|
||||
|
||||
<!-- Output Section -->
|
||||
<div class="mt-10">
|
||||
<div class="mb-2 font-semibold">
|
||||
Condensed Output
|
||||
</div>
|
||||
<c-input-text
|
||||
:value="condensedJson"
|
||||
placeholder="Condensed JSON will appear here..."
|
||||
rows="12"
|
||||
readonly multiline monospace raw-text
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Display -->
|
||||
<c-alert v-if="error" type="error" class="mt-4">
|
||||
{{ error }}
|
||||
</c-alert>
|
||||
</c-card>
|
||||
</template>
|
Loading…
Add table
Add a link
Reference in a new issue