diff --git a/.gitignore b/.gitignore index e5756b0..f5fdd48 100644 --- a/.gitignore +++ b/.gitignore @@ -105,5 +105,5 @@ dist .tern-port .npmrc -types/ +/types/ .idea/ diff --git a/src/types/application/json.ts b/src/types/application/json.ts new file mode 100644 index 0000000..0efae2c --- /dev/null +++ b/src/types/application/json.ts @@ -0,0 +1,30 @@ +import { TypeOptions } from 'src/common'; + +interface CommonApplicationJsonOptions extends TypeOptions { + type: 'application/json'; +} + +interface SerializeApplicationJsonOptions extends CommonApplicationJsonOptions { + indent?: number; + replacer?: (key: string, value: unknown) => unknown; +} + +export const serialize = async ( + data: T, + options = {} as O, +): Promise => { + const theOptions = options as SerializeApplicationJsonOptions; + return Promise.resolve(JSON.stringify(data, theOptions.replacer, theOptions.indent)); +}; + +interface DeserializeApplicationJsonOptions extends CommonApplicationJsonOptions { + reviver?: (key: string, value: unknown) => unknown; +} + +export const deserialize = async ( + serialized: string, + options = {} as O, +): Promise => { + const theOptions = options as DeserializeApplicationJsonOptions; + return Promise.resolve(JSON.parse(serialized, theOptions.reviver) as T); +}; diff --git a/src/types/application/xml.ts b/src/types/application/xml.ts new file mode 100644 index 0000000..ee2a772 --- /dev/null +++ b/src/types/application/xml.ts @@ -0,0 +1,153 @@ +import type { Element } from 'xml-js'; +import { TypeOptions } from 'src/common'; + +interface CommonApplicationXmlOptions extends TypeOptions { + type: 'application/xml'; +} + +interface SerializeApplicationXmlOptions extends CommonApplicationXmlOptions { + indent?: number | string; + rootElementName?: string; +} + +const constructElement = (data: unknown, name: string): Element[] => { + if (Array.isArray(data)) { + return [ + { + type: 'element', + name, + attributes: { array: 'array' }, + elements: data + .flatMap((item, i) => constructElement(item, `_${i}`)), + }, + ]; + } + + if ( + typeof data === 'string' + || typeof data === 'number' + || typeof data === 'boolean' + ) { + return [ + { + name, + type: 'element', + attributes: { type: typeof data }, + elements: [ + { + type: 'text', + text: data.toString(), + }, + ], + }, + ]; + } + + if (typeof data === 'object' && data !== null) { + return [ + { + name, + type: 'element', + attributes: { type: 'object' }, + elements: Object.entries(data) + .flatMap(([key, value]) => constructElement(value, key)), + }, + ]; + } + + return []; +}; + +let parseElement: (element: Element, allowValueOverride?: boolean) => unknown; + +const parseObjectElement = (element: Element): unknown => Object.fromEntries( + element.elements?.map((e) => [e.name, parseElement(e)]) ?? [], +); + +const parseNumberElement = (element: Element, allowValueOverride = false): unknown => { + if (allowValueOverride) { + return Number(element.elements?.[0].text) || Number(element.attributes?.value); + } + return Number(element.elements?.[0].text); +}; + +const parseBooleanElement = (element: Element, allowValueOverride = false): unknown => { + const stringValue = ( + allowValueOverride + ? (element.elements?.[0].text ?? element.attributes?.value) + : element.elements?.[0].text + ) + ?.toString() + .toLowerCase(); + + if (stringValue === 'true') { + return true; + } + + if (stringValue === 'false') { + return false; + } + + return undefined; +}; + +parseElement = (element: Element, allowValueOverride = false): unknown => { + if (element.attributes && 'array' in element.attributes) { + return element.elements?.map((e) => parseElement(e, allowValueOverride)); + } + + if (element.attributes?.type === 'object') { + return parseObjectElement(element); + } + + if (element.attributes?.type === 'string') { + return element.elements?.[0].text; + } + + if (element.attributes?.type === 'number') { + return parseNumberElement(element, allowValueOverride); + } + + if (element.attributes?.type === 'boolean') { + return parseBooleanElement(element, allowValueOverride); + } + + return ( + allowValueOverride + ? (element.elements?.[0].text ?? element.attributes?.value) + : element.elements?.[0].text + ); +}; + +export const serialize = async ( + data: T, + options = {} as O, +): Promise => { + const { js2xml } = await import('xml-js'); + const { indent, rootElementName = 'root' } = options as SerializeApplicationXmlOptions; + + return js2xml( + { elements: constructElement(data, rootElementName) }, + { + compact: false, + spaces: indent ?? 0, + }, + ); +}; + +interface DeserializeApplicationXmlOptions extends CommonApplicationXmlOptions { + allowValueOverride?: boolean; +} + +export const deserialize = async ( + serialized: string, + options = {} as O, +): Promise => { + const { xml2js } = await import('xml-js'); + const { allowValueOverride } = options as DeserializeApplicationXmlOptions; + const rawElements = xml2js(serialized, { + compact: false, + nativeType: false, + }) as Element; + return parseElement(rawElements.elements?.[0] ?? {}, allowValueOverride) as T; +};