diff options
Diffstat (limited to 'src/tools/json-to-csv')
-rw-r--r-- | src/tools/json-to-csv/index.ts | 12 | ||||
-rw-r--r-- | src/tools/json-to-csv/json-to-csv.e2e.spec.ts | 29 | ||||
-rw-r--r-- | src/tools/json-to-csv/json-to-csv.service.test.ts | 89 | ||||
-rw-r--r-- | src/tools/json-to-csv/json-to-csv.service.ts | 35 | ||||
-rw-r--r-- | src/tools/json-to-csv/json-to-csv.vue | 32 |
5 files changed, 197 insertions, 0 deletions
diff --git a/src/tools/json-to-csv/index.ts b/src/tools/json-to-csv/index.ts new file mode 100644 index 0000000..acfef02 --- /dev/null +++ b/src/tools/json-to-csv/index.ts @@ -0,0 +1,12 @@ +import { List } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'JSON to CSV', + path: '/json-to-csv', + description: 'Convert JSON to CSV with automatic header detection.', + keywords: ['json', 'to', 'csv', 'convert'], + component: () => import('./json-to-csv.vue'), + icon: List, + createdAt: new Date('2023-06-18'), +}); diff --git a/src/tools/json-to-csv/json-to-csv.e2e.spec.ts b/src/tools/json-to-csv/json-to-csv.e2e.spec.ts new file mode 100644 index 0000000..840469c --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.e2e.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Tool - JSON to CSV', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/json-to-csv'); + }); + + test('Has correct title', async ({ page }) => { + await expect(page).toHaveTitle('JSON to CSV - IT Tools'); + }); + + test('Provided json is converted to csv', async ({ page }) => { + await page.getByTestId('input').fill(` +[ + {'Age': 18.0, 'Salary': 20000.0, 'Gender': 'Male', 'Country': 'Germany', 'Purchased': 'N'}, + {'Age': 19.0, 'Salary': 22000.0, 'Gender': 'Female', 'Country': 'France', 'Purchased': 'N'}, +] + `); + + const generatedJson = await page.getByTestId('area-content').innerText(); + + expect(generatedJson.trim()).toEqual(` +Age,Salary,Gender,Country,Purchased +18,20000,Male,Germany,N +19,22000,Female,France,N + `.trim(), + ); + }); +}); diff --git a/src/tools/json-to-csv/json-to-csv.service.test.ts b/src/tools/json-to-csv/json-to-csv.service.test.ts new file mode 100644 index 0000000..c27bf9b --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.service.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest'; +import { convertArrayToCsv, getHeaders } from './json-to-csv.service'; + +describe('json-to-csv service', () => { + describe('getHeaders', () => { + it('extracts all the keys from the array of objects', () => { + expect(getHeaders({ array: [{ a: 1, b: 2 }, { a: 3, c: 4 }] })).toEqual(['a', 'b', 'c']); + }); + + it('returns an empty array if the array is empty', () => { + expect(getHeaders({ array: [] })).toEqual([]); + }); + }); + + describe('convertArrayToCsv', () => { + it('converts an array of objects to a CSV string', () => { + const array = [ + { a: 1, b: 2 }, + { a: 3, b: 4 }, + + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + 1,2 + 3,4" + `); + }); + + it('converts an array of objects with different keys to a CSV string', () => { + const array = [ + { a: 1, b: 2 }, + { a: 3, c: 4 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b,c + 1,2, + 3,,4" + `); + }); + + it('when a value is null, it is converted to the string "null"', () => { + const array = [ + { a: null, b: 2 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + null,2" + `); + }); + + it('when a value is undefined, it is converted to an empty string', () => { + const array = [ + { a: undefined, b: 2 }, + { b: 3 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + ,2 + ,3" + `); + }); + + it('when a value contains a comma, it is wrapped in double quotes', () => { + const array = [ + { a: 'hello, world', b: 2 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + \\"hello, world\\",2" + `); + }); + + it('when a value contains a double quote, it is escaped with another double quote', () => { + const array = [ + { a: 'hello "world"', b: 2 }, + ]; + + expect(convertArrayToCsv({ array })).toMatchInlineSnapshot(` + "a,b + hello \\\\\\"world\\\\\\",2" + `); + }); + }); +}); diff --git a/src/tools/json-to-csv/json-to-csv.service.ts b/src/tools/json-to-csv/json-to-csv.service.ts new file mode 100644 index 0000000..ab3c04e --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.service.ts @@ -0,0 +1,35 @@ +export { getHeaders, convertArrayToCsv }; + +function getHeaders({ array }: { array: Record<string, unknown>[] }): string[] { + const headers = new Set<string>(); + + array.forEach(item => Object.keys(item).forEach(key => headers.add(key))); + + return Array.from(headers); +} + +function serializeValue(value: unknown): string { + if (value === null) { + return 'null'; + } + + if (value === undefined) { + return ''; + } + + const valueAsString = String(value).replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"'); + + if (valueAsString.includes(',')) { + return `"${valueAsString}"`; + } + + return valueAsString; +} + +function convertArrayToCsv({ array }: { array: Record<string, unknown>[] }): string { + const headers = getHeaders({ array }); + + const rows = array.map(item => headers.map(header => serializeValue(item[header]))); + + return [headers.join(','), ...rows].join('\n'); +} diff --git a/src/tools/json-to-csv/json-to-csv.vue b/src/tools/json-to-csv/json-to-csv.vue new file mode 100644 index 0000000..cf15400 --- /dev/null +++ b/src/tools/json-to-csv/json-to-csv.vue @@ -0,0 +1,32 @@ +<script setup lang="ts"> +import JSON5 from 'json5'; +import { convertArrayToCsv } from './json-to-csv.service'; +import type { UseValidationRule } from '@/composable/validation'; +import { withDefaultOnError } from '@/utils/defaults'; + +function transformer(value: string) { + return withDefaultOnError(() => { + if (value === '') { + return ''; + } + return convertArrayToCsv({ array: JSON5.parse(value) }); + }, ''); +} + +const rules: UseValidationRule<string>[] = [ + { + validator: (v: string) => v === '' || JSON5.parse(v), + message: 'Provided JSON is not valid.', + }, +]; +</script> + +<template> + <format-transformer + input-label="Your raw json" + input-placeholder="Paste your raw json here..." + output-label="CSV version of your JSON" + :input-validation-rules="rules" + :transformer="transformer" + /> +</template> |