Make de/serializer functions sync, and add URL-encoded form type.master
@@ -1,5 +1,5 @@ | |||||
{ | { | ||||
"name": "@modal-sh/oatmeal", | |||||
"name": "@modal-sh/oatmeal-core", | |||||
"version": "0.0.0", | "version": "0.0.0", | ||||
"files": [ | "files": [ | ||||
"dist", | "dist", | ||||
@@ -48,5 +48,22 @@ | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"xml-js": "^1.6.11" | "xml-js": "^1.6.11" | ||||
}, | |||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js", | |||||
"exports": { | |||||
".": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/index.js", | |||||
"import": "./dist/esm/development/index.js" | |||||
}, | |||||
"require": "./dist/cjs/production/index.js", | |||||
"import": "./dist/esm/production/index.js", | |||||
"types": "./dist/types/index.d.ts" | |||||
} | |||||
}, | |||||
"typesVersions": { | |||||
"*": {} | |||||
} | } | ||||
} | } |
@@ -3,6 +3,6 @@ export interface TypeOptions { | |||||
} | } | ||||
export interface SerializeType<Options extends TypeOptions = TypeOptions> { | export interface SerializeType<Options extends TypeOptions = TypeOptions> { | ||||
serialize: <T>(data: T, options: Options) => Promise<string>; | |||||
deserialize: <T>(serialized: string, options: Options) => Promise<T>; | |||||
serialize: <T>(data: T, options: Options) => string; | |||||
deserialize: <T>(serialized: string, options: Options) => T; | |||||
} | } |
@@ -1,40 +1,38 @@ | |||||
import { TypeOptions, SerializeType } from 'src/common'; | import { TypeOptions, SerializeType } from 'src/common'; | ||||
import * as applicationJson from 'src/types/application/json'; | |||||
import * as applicationXml from 'src/types/application/xml'; | |||||
import * as applicationXWwwFormUrlencoded from 'src/types/application/x-www-form-urlencoded'; | |||||
interface LoadSerializeModule<T extends SerializeType> { | |||||
(): Promise<T>; | |||||
} | |||||
const TYPES: Record<string, LoadSerializeModule<SerializeType>> = { | |||||
'application/json': () => import('src/types/application/json'), | |||||
'text/json': () => import('src/types/application/json'), | |||||
'application/xml': () => import('src/types/application/xml'), | |||||
'text/xml': () => import('src/types/application/xml'), | |||||
const TYPES: Record<string, SerializeType> = { | |||||
'application/json': applicationJson, | |||||
'text/json': applicationJson, | |||||
'application/xml': applicationXml, | |||||
'text/xml': applicationXml, | |||||
'application/x-www-form-urlencoded': applicationXWwwFormUrlencoded, | |||||
}; | }; | ||||
export const AVAILABLE_TYPES: (keyof typeof TYPES)[] = Object.keys(TYPES); | export const AVAILABLE_TYPES: (keyof typeof TYPES)[] = Object.keys(TYPES); | ||||
export const serialize = async <T, O extends TypeOptions = TypeOptions>( | |||||
export const serialize = <T, O extends TypeOptions = TypeOptions>( | |||||
data: T, | data: T, | ||||
options: O, | options: O, | ||||
): Promise<string> => { | |||||
): string => { | |||||
const { type: optionsType } = options; | const { type: optionsType } = options; | ||||
const { [optionsType]: loadType } = TYPES; | |||||
if (!loadType) { | |||||
const { [optionsType]: serializeType } = TYPES; | |||||
if (!serializeType) { | |||||
throw new Error(`Unsupported type: ${optionsType}`); | throw new Error(`Unsupported type: ${optionsType}`); | ||||
} | } | ||||
const serializeType = await loadType(); | |||||
return serializeType.serialize(data, options); | return serializeType.serialize(data, options); | ||||
}; | }; | ||||
export const deserialize = async <T, O extends TypeOptions = TypeOptions>( | |||||
export const deserialize = <T, O extends TypeOptions = TypeOptions>( | |||||
serialized: string, | serialized: string, | ||||
options: O, | options: O, | ||||
): Promise<T> => { | |||||
): T => { | |||||
const { type: optionsType } = options; | const { type: optionsType } = options; | ||||
const { [optionsType]: loadType } = TYPES; | |||||
if (!loadType) { | |||||
const { [optionsType]: serializeType } = TYPES; | |||||
if (!serializeType) { | |||||
throw new Error(`Unsupported type: ${optionsType}`); | throw new Error(`Unsupported type: ${optionsType}`); | ||||
} | } | ||||
const serializeType = await loadType(); | |||||
return serializeType.deserialize<T>(serialized, options); | return serializeType.deserialize<T>(serialized, options); | ||||
}; | }; |
@@ -9,22 +9,22 @@ interface SerializeApplicationJsonOptions extends CommonApplicationJsonOptions { | |||||
replacer?: (key: string, value: unknown) => unknown; | replacer?: (key: string, value: unknown) => unknown; | ||||
} | } | ||||
export const serialize = async <T, O extends TypeOptions = SerializeApplicationJsonOptions>( | |||||
export const serialize = <T, O extends TypeOptions = SerializeApplicationJsonOptions>( | |||||
data: T, | data: T, | ||||
options = {} as O, | options = {} as O, | ||||
): Promise<string> => { | |||||
): string => { | |||||
const theOptions = options as SerializeApplicationJsonOptions; | const theOptions = options as SerializeApplicationJsonOptions; | ||||
return Promise.resolve(JSON.stringify(data, theOptions.replacer, theOptions.indent)); | |||||
return JSON.stringify(data, theOptions.replacer, theOptions.indent); | |||||
}; | }; | ||||
interface DeserializeApplicationJsonOptions extends CommonApplicationJsonOptions { | interface DeserializeApplicationJsonOptions extends CommonApplicationJsonOptions { | ||||
reviver?: (key: string, value: unknown) => unknown; | reviver?: (key: string, value: unknown) => unknown; | ||||
} | } | ||||
export const deserialize = async <T, O extends TypeOptions = DeserializeApplicationJsonOptions>( | |||||
export const deserialize = <T, O extends TypeOptions = DeserializeApplicationJsonOptions>( | |||||
serialized: string, | serialized: string, | ||||
options = {} as O, | options = {} as O, | ||||
): Promise<T> => { | |||||
): T => { | |||||
const theOptions = options as DeserializeApplicationJsonOptions; | const theOptions = options as DeserializeApplicationJsonOptions; | ||||
return Promise.resolve(JSON.parse(serialized, theOptions.reviver) as T); | |||||
return JSON.parse(serialized, theOptions.reviver) as T; | |||||
}; | }; |
@@ -0,0 +1,16 @@ | |||||
export const serialize = <T>( | |||||
data: T, | |||||
): string => { | |||||
if (typeof data !== 'object') { | |||||
throw new TypeError('Data must be an object'); | |||||
} | |||||
return new URLSearchParams(data as Record<string, string>).toString(); | |||||
} | |||||
export const deserialize = <T>( | |||||
serialized: string, | |||||
): T => { | |||||
return Object.fromEntries( | |||||
new URLSearchParams(serialized).entries() | |||||
) as unknown as T; | |||||
} |
@@ -1,4 +1,4 @@ | |||||
import type { Element } from 'xml-js'; | |||||
import { js2xml, xml2js, type Element } from 'xml-js'; | |||||
import { TypeOptions } from 'src/common'; | import { TypeOptions } from 'src/common'; | ||||
interface CommonApplicationXmlOptions extends TypeOptions { | interface CommonApplicationXmlOptions extends TypeOptions { | ||||
@@ -119,11 +119,10 @@ parseElement = (element: Element, allowValueOverride = false): unknown => { | |||||
); | ); | ||||
}; | }; | ||||
export const serialize = async <T, O extends TypeOptions = SerializeApplicationXmlOptions>( | |||||
export const serialize = <T, O extends TypeOptions = SerializeApplicationXmlOptions>( | |||||
data: T, | data: T, | ||||
options = {} as O, | options = {} as O, | ||||
): Promise<string> => { | |||||
const { js2xml } = await import('xml-js'); | |||||
): string => { | |||||
const { indent, rootElementName = 'root' } = options as SerializeApplicationXmlOptions; | const { indent, rootElementName = 'root' } = options as SerializeApplicationXmlOptions; | ||||
return js2xml( | return js2xml( | ||||
@@ -139,11 +138,10 @@ interface DeserializeApplicationXmlOptions extends CommonApplicationXmlOptions { | |||||
allowValueOverride?: boolean; | allowValueOverride?: boolean; | ||||
} | } | ||||
export const deserialize = async <T, O extends TypeOptions = DeserializeApplicationXmlOptions>( | |||||
export const deserialize = <T, O extends TypeOptions = DeserializeApplicationXmlOptions>( | |||||
serialized: string, | serialized: string, | ||||
options = {} as O, | options = {} as O, | ||||
): Promise<T> => { | |||||
const { xml2js } = await import('xml-js'); | |||||
): T => { | |||||
const { allowValueOverride } = options as DeserializeApplicationXmlOptions; | const { allowValueOverride } = options as DeserializeApplicationXmlOptions; | ||||
const rawElements = xml2js(serialized, { | const rawElements = xml2js(serialized, { | ||||
compact: false, | compact: false, | ||||
@@ -3,65 +3,65 @@ import { deserialize, serialize } from '../../src/types/application/json'; | |||||
describe('application/json', () => { | describe('application/json', () => { | ||||
describe('serialize', () => { | describe('serialize', () => { | ||||
it('should serialize a string', async () => { | |||||
const result = await serialize('Hello, World!', { type: 'application/json' }); | |||||
it('should serialize a string', () => { | |||||
const result = serialize('Hello, World!', { type: 'application/json' }); | |||||
expect(result).toBe('"Hello, World!"'); | expect(result).toBe('"Hello, World!"'); | ||||
}); | }); | ||||
it('should serialize a number', async () => { | |||||
const result = await serialize(123, { type: 'application/json' }); | |||||
it('should serialize a number', () => { | |||||
const result = serialize(123, { type: 'application/json' }); | |||||
expect(result).toBe('123'); | expect(result).toBe('123'); | ||||
}); | }); | ||||
it('should serialize a boolean', async () => { | |||||
const result = await serialize(true, { type: 'application/json' }); | |||||
it('should serialize a boolean', () => { | |||||
const result = serialize(true, { type: 'application/json' }); | |||||
expect(result).toBe('true'); | expect(result).toBe('true'); | ||||
}); | }); | ||||
it('should serialize an array', async () => { | |||||
const result = await serialize([1, 2, 3], { type: 'application/json' }); | |||||
it('should serialize an array', () => { | |||||
const result = serialize([1, 2, 3], { type: 'application/json' }); | |||||
expect(result).toBe('[1,2,3]'); | expect(result).toBe('[1,2,3]'); | ||||
}); | }); | ||||
it('should serialize an object', async () => { | |||||
const result = await serialize({ a: 1, b: 2, c: 3 }, { type: 'application/json' }); | |||||
it('should serialize an object', () => { | |||||
const result = serialize({ a: 1, b: 2, c: 3 }, { type: 'application/json' }); | |||||
expect(result).toBe('{"a":1,"b":2,"c":3}'); | expect(result).toBe('{"a":1,"b":2,"c":3}'); | ||||
}); | }); | ||||
it('should serialize an object with indent', async () => { | |||||
const result = await serialize({ a: 1, b: 2, c: 3 }, { type: 'application/json', indent: 2 }); | |||||
it('should serialize an object with indent', () => { | |||||
const result = serialize({ a: 1, b: 2, c: 3 }, { type: 'application/json', indent: 2 }); | |||||
expect(result).toBe('{\n "a": 1,\n "b": 2,\n "c": 3\n}'); | expect(result).toBe('{\n "a": 1,\n "b": 2,\n "c": 3\n}'); | ||||
}); | }); | ||||
}); | }); | ||||
describe('deserialize', () => { | describe('deserialize', () => { | ||||
it('should deserialize a string', async () => { | |||||
const result = await deserialize<string>('"Hello, World!"', { type: 'application/json' }); | |||||
it('should deserialize a string', () => { | |||||
const result = deserialize<string>('"Hello, World!"', { type: 'application/json' }); | |||||
expect(result).toBe('Hello, World!'); | expect(result).toBe('Hello, World!'); | ||||
}); | }); | ||||
it('should deserialize a number', async () => { | |||||
const result = await deserialize<number>('123', { type: 'application/json' }); | |||||
it('should deserialize a number', () => { | |||||
const result = deserialize<number>('123', { type: 'application/json' }); | |||||
expect(result).toBe(123); | expect(result).toBe(123); | ||||
}); | }); | ||||
it('should deserialize a boolean', async () => { | |||||
const result = await deserialize<boolean>('true', { type: 'application/json' }); | |||||
it('should deserialize a boolean', () => { | |||||
const result = deserialize<boolean>('true', { type: 'application/json' }); | |||||
expect(result).toBe(true); | expect(result).toBe(true); | ||||
}); | }); | ||||
it('should deserialize an array', async () => { | |||||
const result = await deserialize<number[]>('[1,2,3]', { type: 'application/json' }); | |||||
it('should deserialize an array', () => { | |||||
const result = deserialize<number[]>('[1,2,3]', { type: 'application/json' }); | |||||
expect(result).toEqual([1, 2, 3]); | expect(result).toEqual([1, 2, 3]); | ||||
}); | }); | ||||
it('should deserialize an object', async () => { | |||||
const result = await deserialize<{ a: number, b: number, c: number }>('{"a":1,"b":2,"c":3}', { type: 'application/json' }); | |||||
it('should deserialize an object', () => { | |||||
const result = deserialize<{ a: number, b: number, c: number }>('{"a":1,"b":2,"c":3}', { type: 'application/json' }); | |||||
expect(result).toEqual({ a: 1, b: 2, c: 3 }); | expect(result).toEqual({ a: 1, b: 2, c: 3 }); | ||||
}); | }); | ||||
it('should deserialize an object with indent', async () => { | |||||
const result = await deserialize<{ a: number, b: number, c: number }>( | |||||
it('should deserialize an object with indent', () => { | |||||
const result = deserialize<{ a: number, b: number, c: number }>( | |||||
'{\n "a": 1,\n "b": 2,\n "c": 3\n}', | '{\n "a": 1,\n "b": 2,\n "c": 3\n}', | ||||
{ type: 'application/json' }, | { type: 'application/json' }, | ||||
); | ); | ||||
@@ -0,0 +1,34 @@ | |||||
import { describe, expect, it } from 'vitest'; | |||||
import { deserialize, serialize } from '../../src/types/application/x-www-form-urlencoded'; | |||||
describe('application/x-www-form-urlencoded', () => { | |||||
describe('serialize', () => { | |||||
it('should throw when serializing a string', () => { | |||||
expect(() => serialize('Hello, World!')).toThrow(); | |||||
}); | |||||
it('should throw when serializing a number', () => { | |||||
expect(() => serialize(123)).toThrow(); | |||||
}); | |||||
it('should throw when serializing a boolean', () => { | |||||
expect(() => serialize(true)).toThrow(); | |||||
}); | |||||
it('should throw when serializing an array', () => { | |||||
expect(() => serialize([1, 2, 3])).toThrow(); | |||||
}); | |||||
it('should serialize an object', () => { | |||||
const result = serialize({ a: 1, b: 2, c: 3 }); | |||||
expect(result).toBe('a=1&b=2&c=3'); | |||||
}); | |||||
}); | |||||
describe('deserialize', () => { | |||||
it('should deserialize an object', () => { | |||||
const result = deserialize<{ a: string, b: string, c: string }>('a=1&b=2&c=3'); | |||||
expect(result).toEqual({ a: '1', b: '2', c: '3' }); | |||||
}); | |||||
}); | |||||
}); |
@@ -3,38 +3,38 @@ import { serialize, deserialize } from '../../src/types/application/xml'; | |||||
describe('application/xml', () => { | describe('application/xml', () => { | ||||
describe('serialize', () => { | describe('serialize', () => { | ||||
it('should serialize a string', async () => { | |||||
const result = await serialize('Hello, World!', { type: 'application/xml' }); | |||||
it('should serialize a string', () => { | |||||
const result = serialize('Hello, World!', { type: 'application/xml' }); | |||||
expect(result).toBe('<root type="string">Hello, World!</root>'); | expect(result).toBe('<root type="string">Hello, World!</root>'); | ||||
}); | }); | ||||
it('should serialize a number', async () => { | |||||
const result = await serialize(123, { type: 'application/xml' }); | |||||
it('should serialize a number', () => { | |||||
const result = serialize(123, { type: 'application/xml' }); | |||||
expect(result).toBe('<root type="number">123</root>'); | expect(result).toBe('<root type="number">123</root>'); | ||||
}); | }); | ||||
it('should serialize a boolean', async () => { | |||||
const result = await serialize(true, { type: 'application/xml' }); | |||||
it('should serialize a boolean', () => { | |||||
const result = serialize(true, { type: 'application/xml' }); | |||||
expect(result).toBe('<root type="boolean">true</root>'); | expect(result).toBe('<root type="boolean">true</root>'); | ||||
}); | }); | ||||
it('should serialize an array', async () => { | |||||
const result = await serialize([1, 2, 3], { type: 'application/xml' }); | |||||
it('should serialize an array', () => { | |||||
const result = serialize([1, 2, 3], { type: 'application/xml' }); | |||||
expect(result).toBe('<root array="array"><_0 type="number">1</_0><_1 type="number">2</_1><_2 type="number">3</_2></root>'); | expect(result).toBe('<root array="array"><_0 type="number">1</_0><_1 type="number">2</_1><_2 type="number">3</_2></root>'); | ||||
}); | }); | ||||
it('should serialize an object', async () => { | |||||
const result = await serialize({ a: 1, b: 2, c: 3 }, { type: 'application/xml' }); | |||||
it('should serialize an object', () => { | |||||
const result = serialize({ a: 1, b: 2, c: 3 }, { type: 'application/xml' }); | |||||
expect(result).toBe('<root type="object"><a type="number">1</a><b type="number">2</b><c type="number">3</c></root>'); | expect(result).toBe('<root type="object"><a type="number">1</a><b type="number">2</b><c type="number">3</c></root>'); | ||||
}); | }); | ||||
it('should serialize an object with a custom root element name', async () => { | |||||
const result = await serialize({ a: 1, b: 2, c: 3 }, { type: 'application/xml', rootElementName: 'fsh', }); | |||||
it('should serialize an object with a custom root element name', () => { | |||||
const result = serialize({ a: 1, b: 2, c: 3 }, { type: 'application/xml', rootElementName: 'fsh', }); | |||||
expect(result).toBe('<fsh type="object"><a type="number">1</a><b type="number">2</b><c type="number">3</c></fsh>'); | expect(result).toBe('<fsh type="object"><a type="number">1</a><b type="number">2</b><c type="number">3</c></fsh>'); | ||||
}); | }); | ||||
it('should serialize an object with indent', async () => { | |||||
const result = await serialize({ a: 1, b: 2, c: 3 }, { type: 'application/xml', indent: 2 }); | |||||
it('should serialize an object with indent', () => { | |||||
const result = serialize({ a: 1, b: 2, c: 3 }, { type: 'application/xml', indent: 2 }); | |||||
expect(result).toBe(`<root type="object"> | expect(result).toBe(`<root type="object"> | ||||
<a type="number">1</a> | <a type="number">1</a> | ||||
<b type="number">2</b> | <b type="number">2</b> | ||||
@@ -44,28 +44,28 @@ describe('application/xml', () => { | |||||
}); | }); | ||||
describe('deserialize', () => { | describe('deserialize', () => { | ||||
it('should deserialize a string', async () => { | |||||
const result = await deserialize<string>('<root type="string">Hello, World!</root>', { type: 'application/xml' }); | |||||
it('should deserialize a string', () => { | |||||
const result = deserialize<string>('<root type="string">Hello, World!</root>', { type: 'application/xml' }); | |||||
expect(result).toBe('Hello, World!'); | expect(result).toBe('Hello, World!'); | ||||
}); | }); | ||||
it('should deserialize a number', async () => { | |||||
const result = await deserialize<number>('<root type="number">123</root>', { type: 'application/xml' }); | |||||
it('should deserialize a number', () => { | |||||
const result = deserialize<number>('<root type="number">123</root>', { type: 'application/xml' }); | |||||
expect(result).toBe(123); | expect(result).toBe(123); | ||||
}); | }); | ||||
it('should deserialize a boolean', async () => { | |||||
const result = await deserialize<boolean>('<root type="boolean">true</root>', { type: 'application/xml' }); | |||||
it('should deserialize a boolean', () => { | |||||
const result = deserialize<boolean>('<root type="boolean">true</root>', { type: 'application/xml' }); | |||||
expect(result).toBe(true); | expect(result).toBe(true); | ||||
}); | }); | ||||
it('should deserialize an array', async () => { | |||||
const result = await deserialize<number[]>('<root array="array"><_0 type="number">1</_0><_1 type="number">2</_1><_2 type="number">3</_2></root>', { type: 'application/xml' }); | |||||
it('should deserialize an array', () => { | |||||
const result = deserialize<number[]>('<root array="array"><_0 type="number">1</_0><_1 type="number">2</_1><_2 type="number">3</_2></root>', { type: 'application/xml' }); | |||||
expect(result).toEqual([1, 2, 3]); | expect(result).toEqual([1, 2, 3]); | ||||
}); | }); | ||||
it('should deserialize an object', async () => { | |||||
const result = await deserialize<{ a: number, b: number, c: number }>( | |||||
it('should deserialize an object', () => { | |||||
const result = deserialize<{ a: number, b: number, c: number }>( | |||||
'<root type="object"><a type="number">1</a><b type="number">2</b><c type="number">3</c></root>', | '<root type="object"><a type="number">1</a><b type="number">2</b><c type="number">3</c></root>', | ||||
{ type: 'application/xml' }, | { type: 'application/xml' }, | ||||
); | ); | ||||