add json condenser tool
This commit is contained in:
		
							parent
							
								
									d7578e23a5
								
							
						
					
					
						commit
						fb6257b088
					
				
							
								
								
									
										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'] |     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'] |     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'] |     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'] |     JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default'] | ||||||
|     JsonMinify: typeof import('./src/tools/json-minify/json-minify.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'] |     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 base64FileConverter } from './base64-file-converter'; | ||||||
| import { tool as base64StringConverter } from './base64-string-converter'; | import { tool as base64StringConverter } from './base64-string-converter'; | ||||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | 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 gzipDecompressor } from './gzip-decompressor'; | ||||||
| import { tool as emailNormalizer } from './email-normalizer'; | import { tool as emailNormalizer } from './email-normalizer'; | ||||||
| 
 | 
 | ||||||
| @ -118,6 +119,7 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|       jsonToXml, |       jsonToXml, | ||||||
|       markdownToHtml, |       markdownToHtml, | ||||||
|       gzipDecompressor, |       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…
	
		Reference in New Issue
	
	Block a user