From a5c06155de955a0f47ec4ec65294ee7bea021801 Mon Sep 17 00:00:00 2001 From: Andrej Dyck <43913051+andrej-dyck@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:22:14 +0100 Subject: [PATCH] improve json parse functions --- json/index.ts | 25 +++++++++++++++----- json/jsonParse.test.ts | 29 ----------------------- json/maybeJson.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- pnpm-lock.yaml | 7 ++++++ 5 files changed, 81 insertions(+), 36 deletions(-) delete mode 100644 json/jsonParse.test.ts create mode 100644 json/maybeJson.test.ts diff --git a/json/index.ts b/json/index.ts index b610502..9be7876 100644 --- a/json/index.ts +++ b/json/index.ts @@ -1,12 +1,16 @@ /** * Safely parse a json string. Result is either a non-nullable unknown or an error. */ -export const safeJsonParse = (value: string | undefined | null): - { success: true, output: NonNullable } +export const safeJsonParse = >( + value: string | undefined | null, + parseUnknown?: (json: unknown) => T +): { success: true, output: T } | { success: false, error: unknown } => { try { - const output = JSON.parse(value ?? '')// as unknown - return output != null ? { success: true, output } : { success: false, error: 'null' } + const output = JSON.parse(value ?? '') // as unknown + return output != null + ? { success: true, output: parseUnknown?.(output) ?? output as T } + : { success: false, error: 'null' } } catch (error: unknown) { return { success: false, error } } @@ -15,8 +19,11 @@ export const safeJsonParse = (value: string | undefined | null): /** * Safely parse a json string. Value is either a non-nullable unknown or undefined. */ -export const maybeJsonParse = (value: string | undefined | null): NonNullable | undefined => { - const r = safeJsonParse(value) +export const maybeJson = >( + value: string | undefined | null, + parseUnknown?: (json: unknown) => T +): T | undefined => { + const r = safeJsonParse(value, parseUnknown) return r.success ? r.output : undefined } @@ -28,4 +35,10 @@ export const maybeJsonParse = (value: string | undefined | null): NonNullable z.object({ data: z.object({ id: z.string() }) }).parse(u)) + * // { data: { id: '01' } } of type { data: { id: string } } */ diff --git a/json/jsonParse.test.ts b/json/jsonParse.test.ts deleted file mode 100644 index 3051a6a..0000000 --- a/json/jsonParse.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from 'vitest' -import { maybeJsonParse } from './index.js' - -test.each([ - { input: '{}', expectedOutput: {} }, - { input: '{ "number": 1.07 }', expectedOutput: { number: 1.07 } }, - { input: '{ "string": "abc" }', expectedOutput: { string: 'abc' } }, - { input: '{ "boolean": true }', expectedOutput: { boolean: true } }, - { input: '{ "nested": { "string": "abc" } }', expectedOutput: { nested: { string: 'abc' } } }, - { input: '[]', expectedOutput: [] }, - { input: '[1, 2, 3]', expectedOutput: [1, 2, 3] }, - { input: '""', expectedOutput: '' }, - { input: '1.07', expectedOutput: 1.07 }, - { input: 'true', expectedOutput: true }, -])('can parse what JSON.parse can; %j', ({ input, expectedOutput }) => { - expect(maybeJsonParse(input)).toEqual(expectedOutput) -}) - -test.each([ - { input: '' }, - { input: 'null' }, - { input: 'undefined' }, - { input: '{' }, - { input: ']' }, - { input: '{ bar: foo }' }, - { input: '{ "bar": 1a }' }, -])('is undefined when JSON.parse fails; %j', ({ input }) => { - expect(maybeJsonParse(input)).toBeUndefined() -}) diff --git a/json/maybeJson.test.ts b/json/maybeJson.test.ts new file mode 100644 index 0000000..a66183e --- /dev/null +++ b/json/maybeJson.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from 'vitest' +import { maybeJson } from './index.js' +import { z } from 'zod' + +test.each([ + { input: '{}', expectedOutput: {} }, + { input: '{ "number": 1.07 }', expectedOutput: { number: 1.07 } }, + { input: '{ "string": "abc" }', expectedOutput: { string: 'abc' } }, + { input: '{ "boolean": true }', expectedOutput: { boolean: true } }, + { input: '{ "nested": { "string": "abc" } }', expectedOutput: { nested: { string: 'abc' } } }, + { input: '[]', expectedOutput: [] }, + { input: '[1, 2, 3]', expectedOutput: [1, 2, 3] }, + { input: '""', expectedOutput: '' }, + { input: '1.07', expectedOutput: 1.07 }, + { input: 'true', expectedOutput: true }, +])('maybeJson can parse what JSON.parse can; %j', ({ input, expectedOutput }) => { + expect(maybeJson(input)).toEqual(expectedOutput) +}) + +test.each([ + { input: '' }, + { input: 'null' }, + { input: 'undefined' }, + { input: '{' }, + { input: ']' }, + { input: '{ bar: foo }' }, + { input: '{ "bar": 1a }' }, +])('maybeJson is undefined when JSON.parse fails; %j', ({ input }) => { + expect(maybeJson(input)).toBeUndefined() +}) + +test('maybeJson accepts a parse function that further parses the unknown to a type', () => { + const parsedObject = maybeJson( + '{ "number": 1.07, "letters": ["a", "b", "c"] }', + (o) => z.object({ letters: z.array(z.string()) }).parse(o) // with zod + ) + expect(parsedObject?.letters).toEqual(['a', 'b', 'c']) +}) + +test('maybeJson is undefined if the parse function fails to parse the unknown', () => { + const parsedObject = maybeJson( + '{ "number": 1.07, "letters": [1, 2, 3] }', + (o) => z.object({ letters: z.array(z.string()) }).parse(o) + ) + expect(parsedObject).toBeUndefined() +}) + +test('maybeJson can assume that unknown is of type T without a parse function (not recommended)', () => { + const parsedObject = maybeJson<{ letters: string[] }>( + '{ "number": 1.07, "letters": ["a", "b", "c"] }', + ) + expect(parsedObject?.letters).toEqual(['a', 'b', 'c']) +}) diff --git a/package.json b/package.json index d056fd4..1e44662 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@typescript-eslint/parser": "6.14.0", "eslint": "8.56.0", "typescript": "5.3.3", - "vitest": "1.0.4" + "vitest": "1.0.4", + "zod": "3.22.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b5ae4f..a5788d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ devDependencies: vitest: specifier: 1.0.4 version: 1.0.4(@types/node@20.10.4) + zod: + specifier: 3.22.4 + version: 3.22.4 packages: @@ -1751,3 +1754,7 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: true