1
0
Fork 0
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:
Bryan Liao 2025-07-03 14:52:42 -07:00
parent d7578e23a5
commit fb6257b088
7 changed files with 247 additions and 0 deletions

1
components.d.ts vendored
View file

@ -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']

View file

@ -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,
],
},
{

View 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'),
});

View file

@ -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();
});
});

View file

@ -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);
});
});

View 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;
}

View 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>