Skip to content

Commit

Permalink
feat(new tool): JSON Size Analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
sharevb committed Sep 28, 2024
1 parent 318fb6e commit 4a3e3ab
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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 emailNormalizer } from './email-normalizer';
import { tool as jsonSizeAnalyzer } from './json-size-analyzer';

import { tool as asciiTextDrawer } from './ascii-text-drawer';

Expand Down Expand Up @@ -147,6 +148,7 @@ export const toolsByCategory: ToolCategory[] = [
crontabGenerator,
jsonViewer,
jsonMinify,
jsonSizeAnalyzer,
jsonToCsv,
sqlPrettify,
chmodCalculator,
Expand Down
12 changes: 12 additions & 0 deletions src/tools/json-size-analyzer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FileAnalytics } from '@vicons/tabler';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'Json Size Analyzer',
path: '/json-size-analyzer',
description: 'Measure JSON nodes relative weights',
keywords: ['json', 'size', 'analyzer'],
component: () => import('./json-size-analyzer.vue'),
icon: FileAnalytics,
createdAt: new Date('2024-07-14'),
});
113 changes: 113 additions & 0 deletions src/tools/json-size-analyzer/json-size-analyzer.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, expect, it } from 'vitest';
import { getJsonUsageTreeNodes } from './json-size-analyzer.service';

describe('json-size-analyzer', () => {
describe('getJsonUsageTreeNodes', () => {
it('return correct tree nodes structures', () => {
expect(getJsonUsageTreeNodes([{ a: [1, 2, 3] }, { b: 'a' }])).to.deep.eq({
children: [
{
children: [
{
children: [
{
children: [],
key: '$.[0].a.[0]',
label: '$.[0].a.[0]: 1 B(26 B gzip)',
},
{
children: [],
key: '$.[0].a.[1]',
label: '$.[0].a.[1]: 1 B(24 B gzip)',
},
{
children: [],
key: '$.[0].a.[2]',
label: '$.[0].a.[2]: 1 B(25 B gzip)',
},
],
key: '$.[0].a',
label: '$.[0].a: 7 B(35 B gzip) ; 28.000% of parent ; biggest child node: \'0\'',
},
],
key: '$.[0]',
label: '$.[0]: 13 B(43 B gzip) ; 52.000% of parent ; biggest child node: \'a\'',
},
{
children: [
{
children: [],
key: '$.[1].b',
label: '$.[1].b: 1 B(25 B gzip)',
},
],
key: '$.[1]',
label: '$.[1]: 9 B(34 B gzip) ; 36.000% of parent ; biggest child node: \'b\'',
},
],
key: '$',
label: '$: 25 B(61 B gzip) ; 100.00% of parent ; biggest child node: \'0\'',
});
expect(getJsonUsageTreeNodes({ a: { b: [1, 2, 3], c: 12 } })).to.deep.eq({
children: [
{
children: [
{
children: [
{
children: [],
key: '$.a.b.[0]',
label: '$.a.b.[0]: 1 B(26 B gzip)',
},
{
children: [],
key: '$.a.b.[1]',
label: '$.a.b.[1]: 1 B(24 B gzip)',
},
{
children: [],
key: '$.a.b.[2]',
label: '$.a.b.[2]: 1 B(25 B gzip)',
},
],
key: '$.a.b',
label: '$.a.b: 7 B(35 B gzip) ; 26.923% of parent ; biggest child node: \'0\'',
},
{
children: [],
key: '$.a.c',
label: '$.a.c: 2 B(24 B gzip)',
},
],
key: '$.a',
label: '$.a: 20 B(50 B gzip) ; 76.923% of parent ; biggest child node: \'b\'',
},
],
key: '$',
label: '$: 26 B(63 B gzip) ; 100.00% of parent ; biggest child node: \'a\'',
});
expect(getJsonUsageTreeNodes({ a: { b: 'azerty', c: 'ueop' } })).to.deep.eq({
children: [
{
children: [
{
children: [],
key: '$.a.b',
label: '$.a.b: 6 B(30 B gzip)',
},
{
children: [],
key: '$.a.c',
label: '$.a.c: 4 B(29 B gzip)',
},
],
key: '$.a',
label: '$.a: 25 B(51 B gzip) ; 80.645% of parent ; biggest child node: \'b\'',
},
],
key: '$',
label: '$: 31 B(61 B gzip) ; 100.00% of parent ; biggest child node: \'a\'',
});
});
});
});
49 changes: 49 additions & 0 deletions src/tools/json-size-analyzer/json-size-analyzer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import jsonAnalyzer from 'json-analyzer';

export interface Meta {
__meta__: {
size?: {
value: number
raw: string
gzip: string
}
number_of_childs?: number
parent_relative_percentage?: string
biggest_node_child: string
}
}
export type AnalysisNode = {
[key: string]: object & Meta
} & Meta;

export type TreeNode = {
key: string
label: string
children: Array<TreeNode>
} & Record<string, unknown>;

function getTreeNodes(obj: AnalysisNode, parentName: string): TreeNode {
const childNodes = Object.entries(obj)
.filter(([key, v]) => key !== '__meta__' && typeof v === 'object')
.map(([k, v]) => ({
key: (Number.isNaN(Number.parseInt(k, 10)) ? `.${k}` : `.[${k}]`),
value: v as AnalysisNode,
}));
return {
key: parentName,
label: obj.__meta__
? `${parentName}: ${obj.__meta__.size?.raw}(${obj.__meta__.size?.gzip} gzip)${obj.__meta__.parent_relative_percentage ? ` ; ${obj.__meta__.parent_relative_percentage} of parent` : ''}${obj.__meta__.biggest_node_child ? ` ; biggest child node: '${obj.__meta__.biggest_node_child}'` : ''}`
: parentName,
children: childNodes.map(childNode => getTreeNodes(childNode.value, parentName + childNode.key)),
};
}

export function getJsonUsageTreeNodes(jsonObj: any, maxDepth: number = 100, targetNode: string = ''): TreeNode {
const analysis = jsonAnalyzer({
json: jsonObj,
verbose: true,
maxDepth,
target: targetNode,
});
return getTreeNodes(analysis, '$');
}
67 changes: 67 additions & 0 deletions src/tools/json-size-analyzer/json-size-analyzer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script setup lang="ts">
import JSON5 from 'json5';
import { getJsonUsageTreeNodes } from './json-size-analyzer.service';
import { useValidation } from '@/composable/validation';
const json = ref('{"a": 1, "b": [1,2,3]}');
const maxDepth = ref(100);
const target = ref('');
const jsonSizes = computed(() => {
const jsonObj = JSON5.parse(json.value);
if (!jsonObj) {
return null;
}
return [getJsonUsageTreeNodes(jsonObj, maxDepth.value - 1, target.value)];
});
const searchInAnalysis = ref('');
const jsonValidation = useValidation({
source: json,
rules: [
{
validator: (v) => {
return JSON5.parse(v);
},
message: 'Provided JSON is not valid.',
},
],
});
</script>

<template>
<div style="max-width: 600px;">
<c-card title="Input" mb-2>
<c-input-text
v-model:value="json"
label="JSON"
multiline
placeholder="Put your JSON data here..."
rows="5"
:validation="jsonValidation"
mb-2
/>

<n-form-item label="Max Depth:" label-placement="left">
<n-input-number v-model:value="maxDepth" :min="0" w-full />
</n-form-item>

<c-input-text
v-model:value="target"
label="Target Node"
placeholder="Where to start the analyze (ie, a[0].b.c)"
mb-2
/>
</c-card>

<c-card v-if="jsonSizes" title="Analysis">
<n-input v-model:value="searchInAnalysis" placeholder="Search in result" />
<n-tree
:show-irrelevant-nodes="false"
:pattern="searchInAnalysis"
:data="jsonSizes"
block-line
/>
</c-card>
</div>
</template>

0 comments on commit 4a3e3ab

Please sign in to comment.