From e4a5f482efb1bd07ce0a9f07d1bdfd9a1ed8063d Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Tue, 15 Oct 2024 10:26:43 +0200 Subject: [PATCH] fix: add helper functions for dealing with AnyXmlValue and export them --- packages/helper/src/__tests__/index.spec.ts | 34 ++++ packages/helper/src/index.ts | 1 + packages/helper/src/mosModel/index.ts | 3 +- packages/helper/src/mosModel/lib.ts | 41 ----- packages/helper/src/mosModel/parseMosTypes.ts | 95 +---------- .../src/mosModel/profile0/xmlConversion.ts | 7 +- .../src/mosModel/profile1/xmlConversion.ts | 5 +- .../src/mosModel/profile2/xmlConversion.ts | 5 +- .../src/mosModel/profile3/xmlConversion.ts | 9 +- .../src/mosModel/profile4/xmlConversion.ts | 3 +- packages/helper/src/utils/ensureMethods.ts | 160 ++++++++++++++++++ packages/model/src/model.ts | 23 +-- 12 files changed, 225 insertions(+), 161 deletions(-) create mode 100644 packages/helper/src/utils/ensureMethods.ts diff --git a/packages/helper/src/__tests__/index.spec.ts b/packages/helper/src/__tests__/index.spec.ts index f9ccb1b6..fc54ac69 100644 --- a/packages/helper/src/__tests__/index.spec.ts +++ b/packages/helper/src/__tests__/index.spec.ts @@ -35,4 +35,38 @@ describe('Index', () => { expect(MOS.MosModel.XMLMosItem.toXML).toBeTruthy() expect(MOS.pad).toBeTruthy() }) + test('manipulate xml data', () => { + // The ensure* methods are useful when reading the XML data + + const testTypes = (anyData: MOS.AnyXMLObject) => { + // ensureString + testType(MOS.ensureString(anyData.strValue, true)) + + testType(MOS.ensureXMLObject(anyData.objA, true)) + // @ts-expect-error wrong return type + testType(MOS.ensureObject(anyData.objA, true)) + + testType(MOS.ensureString(MOS.ensureXMLObject(anyData.objA, true).strValue, true)) + + testType(MOS.ensureString(MOS.ensureXMLObjectArray(anyData.arrayA, true)[0].strValue, true)) + + expect(1).toBeTruthy() + } + + const testType = (_a: T) => { + return + } + + testTypes({ + strValue: 'test', + objA: { + strValue: 'test', + }, + arrayA: [ + { + strValue: 'test', + }, + ], + }) + }) }) diff --git a/packages/helper/src/index.ts b/packages/helper/src/index.ts index 2b83da2a..5501c24f 100644 --- a/packages/helper/src/index.ts +++ b/packages/helper/src/index.ts @@ -9,3 +9,4 @@ export { export * as MosModel from './mosModel' export * from './stringify/stringifyMosObject' export * from './utils/Errors' +export * from './utils/ensureMethods' diff --git a/packages/helper/src/mosModel/index.ts b/packages/helper/src/mosModel/index.ts index 15048bc2..59f94ae5 100644 --- a/packages/helper/src/mosModel/index.ts +++ b/packages/helper/src/mosModel/index.ts @@ -6,7 +6,8 @@ export * from './profile2' export * from './profile3' export * from './profile4' export * from './parseMosTypes' -export { ensureArray, literal, omitUndefined, flattenXMLText } from './lib' +export { literal, omitUndefined, flattenXMLText } from './lib' +export * from '../utils/ensureMethods' export * from './ParseError' import { AnyXMLObject } from '@mos-connection/model' diff --git a/packages/helper/src/mosModel/lib.ts b/packages/helper/src/mosModel/lib.ts index 64cce340..7efd8738 100644 --- a/packages/helper/src/mosModel/lib.ts +++ b/packages/helper/src/mosModel/lib.ts @@ -1,5 +1,4 @@ import { AnyXMLValue } from '@mos-connection/model' -import { ensureSingular } from './parseMosTypes' export function isEmpty(obj: unknown): boolean { if (typeof obj === 'object') { @@ -21,16 +20,6 @@ export function has(obj: unknown, property: string): boolean { return Object.hasOwnProperty.call(obj, property) } -/** - * Ensures that the returned value is an array. - * If the input is not an array, it will be wrapped in an array. - */ -export function ensureArray(v: A | B | B[]): (A | B)[] -export function ensureArray(v: T | T[]): T[] -export function ensureArray(v: T | T[]): T[] { - if (typeof v === 'object' && Array.isArray(v)) return v - else return [v] -} /** * Asserts that a string type is of a certain literal. * Example usage: const str = assertStringLiteral('foo', ['foo', 'bar']) // str is of type 'foo' | 'bar' @@ -39,36 +28,6 @@ export function assertStringLiteral(value: string, options: T[ return options.includes(value as T) } -export function ensureStringLiteral( - xmlValue: AnyXMLValue, - options: T[], - fallback: T, - strict: boolean -): T { - const value = ensureSingular(xmlValue, strict) - - if (!value) { - if (strict) { - throw new Error(`Expected a string, got: "${value}"`) - } else { - return fallback - } - } - - if (assertStringLiteral(value, options)) { - return value - } else if (strict) { - throw new Error(`Invalid literal value: "${value}" (valid values: ${options.join(', ')})`) - } else { - return fallback - } -} -export function ensureString(value: AnyXMLValue, strict: boolean): string { - if (typeof value === 'string') { - return value - } else if (strict) throw new Error(`Expected a string, got: ${JSON.stringify(value)}`) - else return '' -} /** Type assertion */ export function literal(o: T): T { return o diff --git a/packages/helper/src/mosModel/parseMosTypes.ts b/packages/helper/src/mosModel/parseMosTypes.ts index 54534970..585f64c0 100644 --- a/packages/helper/src/mosModel/parseMosTypes.ts +++ b/packages/helper/src/mosModel/parseMosTypes.ts @@ -1,6 +1,7 @@ import { MosTypes, getMosTypes, MosType, AnyXMLObject } from '@mos-connection/model' -import { AnyXMLValue, AnyXMLValueSingular, ensureArray, isEmpty } from './lib' +import { AnyXMLValue } from './lib' import { ParseError } from './ParseError' +import { ensureSingular } from '../utils/ensureMethods' export function getParseMosTypes(strict: boolean): MosParseTypes { const mosTypes = getMosTypes(strict) @@ -127,85 +128,6 @@ export function parseRequired( } } -export function ensureSingular(value: AnyXMLValue, strict: boolean): AnyXMLValueSingular { - // Quick-fix if it is in a xml element: - if (isXMLTextElement(value)) value = value.text - // Quick-fix for empty object - if (isEmpty(value)) value = '' - - if (typeof value === 'object') { - if (Array.isArray(value)) { - if (value.length === 0) return undefined - if (value.length > 1) { - if (strict) - throw new Error(`Expected only one value, got ${value.length} values: ${JSON.stringify(value)}`) - else return undefined - } - return ensureSingular(value[0], strict) - } else if (strict) throw new Error(`Expected only one value, got object: ${JSON.stringify(value)}`) - else return undefined - } else { - return value - } -} -export function ensureSingularArray(value: AnyXMLValue, strict: boolean): AnyXMLValueSingular[] { - if (typeof value === 'object' && Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - value[i] = ensureSingular(value[i], strict) - } - return value as AnyXMLValueSingular[] - } else if (strict) throw new Error(`Expected an Array, got: ${JSON.stringify(value)}`) - else return [] -} -export function ensureXMLObject(value: AnyXMLValue, strict: boolean): AnyXMLObject { - if (typeof value === 'object') { - if (Array.isArray(value)) { - if (value.length === 0) return {} - if (value.length > 1) { - if (strict) - throw new Error(`Expected only one value, got ${value.length} values: ${JSON.stringify(value)}`) - else return {} - } - return ensureXMLObject(value[0], strict) - } else { - return value - } - } else if (strict) throw new Error(`Expected an object, got: ${value}`) - else return {} -} - -export function ensureXMLObjectArray(value: AnyXMLValue, strict: boolean): AnyXMLObject[] { - const objs: AnyXMLObject[] = [] - for (const obj of ensureArray(value)) { - objs.push(ensureXMLObject(obj, strict)) - } - return objs -} -export function isSingular(value: AnyXMLValue): value is AnyXMLValueSingular { - try { - ensureSingular(value, true) - return true - } catch { - return false - } -} -export function isSingularArray(value: AnyXMLValue): value is AnyXMLValueSingular[] { - try { - ensureSingularArray(value, true) - return true - } catch { - return false - } -} -export function isXMLObject(value: AnyXMLValue): value is AnyXMLObject { - try { - ensureXMLObject(value, true) - return true - } catch { - return false - } -} - function getSpecialMosTypes(strict: boolean) { const string: MosType = { create: (anyValue: AnyXMLValue) => { @@ -263,16 +185,3 @@ export function getXMLAttributes(obj: AnyXMLObject): { [key: string]: string } { return obj.attributes as any else return {} } - -interface TextElement { - $type: 'text' - $name: string - text: string -} -function isXMLTextElement(xml: any): xml is TextElement { - return ( - typeof xml === 'object' && - (xml as TextElement).$type === 'text' && - typeof (xml as TextElement).text === 'string' - ) -} diff --git a/packages/helper/src/mosModel/profile0/xmlConversion.ts b/packages/helper/src/mosModel/profile0/xmlConversion.ts index 322a720d..615bf25b 100644 --- a/packages/helper/src/mosModel/profile0/xmlConversion.ts +++ b/packages/helper/src/mosModel/profile0/xmlConversion.ts @@ -6,9 +6,10 @@ import { IMOSListMachInfoDefaultActiveXMode, IMOSString128, } from '@mos-connection/model' -import { ensureArray, ensureStringLiteral, has, omitUndefined } from '../lib' +import { has, omitUndefined } from '../lib' +import { ensureArray, ensureStringLiteral, ensureXMLObject } from '../../utils/ensureMethods' import { addTextElementInternal } from '../../utils/Utils' -import { ensureXMLObject, getParseMosTypes, getXMLAttributes } from '../parseMosTypes' +import { getParseMosTypes, getXMLAttributes } from '../parseMosTypes' import { XMLMosExternalMetaData } from '../profile1' import { ParseError } from '../ParseError' @@ -93,7 +94,7 @@ export namespace XMLSupportedProfiles { ) const parsed: IMOSListMachInfo['supportedProfiles'] = { - deviceType: ensureStringLiteral(xmlSupportedProfiles.deviceType, ['NCS', 'MOS', 'N/A'], 'N/A', strict), + deviceType: ensureStringLiteral(xmlSupportedProfiles.deviceType, ['NCS', 'MOS', 'N/A'], strict, 'N/A'), // Note: .profiles are added below } diff --git a/packages/helper/src/mosModel/profile1/xmlConversion.ts b/packages/helper/src/mosModel/profile1/xmlConversion.ts index dfb75353..d99aa152 100644 --- a/packages/helper/src/mosModel/profile1/xmlConversion.ts +++ b/packages/helper/src/mosModel/profile1/xmlConversion.ts @@ -13,9 +13,10 @@ import { IMOSObjectAirStatus, IMOSScope, } from '@mos-connection/model' -import { AnyXMLObject, ensureArray, flattenXMLText, has, isEmpty, literal, omitUndefined } from '../lib' +import { AnyXMLObject, flattenXMLText, has, isEmpty, literal, omitUndefined } from '../lib' +import { ensureArray, ensureXMLObject, ensureXMLObjectArray, isXMLObject } from '../../utils/ensureMethods' import { addTextElementInternal } from '../../utils/Utils' -import { ensureXMLObject, ensureXMLObjectArray, getParseMosTypes, isXMLObject } from '../parseMosTypes' +import { getParseMosTypes } from '../parseMosTypes' import { ParseError } from '../ParseError' /* eslint-disable @typescript-eslint/no-namespace */ diff --git a/packages/helper/src/mosModel/profile2/xmlConversion.ts b/packages/helper/src/mosModel/profile2/xmlConversion.ts index a7c7de52..0a8304a6 100644 --- a/packages/helper/src/mosModel/profile2/xmlConversion.ts +++ b/packages/helper/src/mosModel/profile2/xmlConversion.ts @@ -10,11 +10,12 @@ import { IMOSROAckObject, AnyXMLValue, } from '@mos-connection/model' -import { ensureArray, has, omitUndefined } from '../lib' +import { has, omitUndefined } from '../lib' +import { ensureArray, ensureXMLObject } from '../../utils/ensureMethods' import { ROAck } from './ROAck' import { XMLMosExternalMetaData, XMLMosObjects, XMLObjectPaths } from '../profile1/xmlConversion' import { addTextElementInternal } from '../../utils/Utils' -import { ensureXMLObject, getParseMosTypes } from '../parseMosTypes' +import { getParseMosTypes } from '../parseMosTypes' import { ParseError } from '../ParseError' /* eslint-disable @typescript-eslint/no-namespace */ diff --git a/packages/helper/src/mosModel/profile3/xmlConversion.ts b/packages/helper/src/mosModel/profile3/xmlConversion.ts index e1908499..62466107 100644 --- a/packages/helper/src/mosModel/profile3/xmlConversion.ts +++ b/packages/helper/src/mosModel/profile3/xmlConversion.ts @@ -7,16 +7,11 @@ import { IMOSSearchField, } from '@mos-connection/model' import { omitUndefined } from '../lib' -import { - getParseMosTypes, - ensureXMLObject, - getXMLAttributes, - isXMLObject, - ensureXMLObjectArray, -} from '../parseMosTypes' +import { getParseMosTypes, getXMLAttributes } from '../parseMosTypes' import { addTextElementInternal } from '../../utils/Utils' import { XMLMosObjects } from '../profile1' import { ParseError } from '../ParseError' +import { ensureXMLObject, ensureXMLObjectArray, isXMLObject } from '../../utils/ensureMethods' /* eslint-disable @typescript-eslint/no-namespace */ export namespace XMLMosRequestObjectList { diff --git a/packages/helper/src/mosModel/profile4/xmlConversion.ts b/packages/helper/src/mosModel/profile4/xmlConversion.ts index 09375b44..99a4f5dd 100644 --- a/packages/helper/src/mosModel/profile4/xmlConversion.ts +++ b/packages/helper/src/mosModel/profile4/xmlConversion.ts @@ -1,8 +1,9 @@ import { IMOSROFullStoryBodyItem, IMOSROFullStory, AnyXMLValue } from '@mos-connection/model' import { XMLROStory, XMLMosItem } from '../profile2/xmlConversion' import { omitUndefined } from '../lib' -import { ensureXMLObject, ensureXMLObjectArray, getParseMosTypes } from '../parseMosTypes' +import { getParseMosTypes } from '../parseMosTypes' import { ParseError } from '../ParseError' +import { ensureXMLObject, ensureXMLObjectArray } from '../../utils/ensureMethods' /* eslint-disable @typescript-eslint/no-namespace */ export namespace XMLROFullStory { diff --git a/packages/helper/src/utils/ensureMethods.ts b/packages/helper/src/utils/ensureMethods.ts new file mode 100644 index 00000000..2f0fd9a5 --- /dev/null +++ b/packages/helper/src/utils/ensureMethods.ts @@ -0,0 +1,160 @@ +import { AnyXMLObject, AnyXMLValue, AnyXMLValueSingular } from '@mos-connection/model' +import { assertStringLiteral, isEmpty } from '../mosModel/lib' + +/** + * Ensures that the returned value is an array. + * If the input is not an array, it will be wrapped in an array. + */ +export function ensureArray(v: A | B | B[]): (A | B)[] +export function ensureArray(v: T | T[]): T[] +export function ensureArray(v: T | T[]): T[] { + if (typeof v === 'object' && Array.isArray(v)) return v + else return [v] +} + +/** + * Ensures that the returned value is a string literal. + * If the input value is not of the correct type, will throw (if strict) or return the fallback value. + */ +export function ensureStringLiteral( + xmlValue: AnyXMLValue, + options: T[], + strict: boolean, + fallback: T +): T { + const value = ensureSingular(xmlValue, strict) + + if (!value) { + if (strict) { + throw new Error(`Expected a string, got: "${value}"`) + } else { + return fallback + } + } + + if (assertStringLiteral(value, options)) { + return value + } else if (strict) { + throw new Error(`Invalid literal value: "${value}" (valid values: ${options.join(', ')})`) + } else { + return fallback + } +} +/** + * Ensures that the returned value is a string. + * If the input value is not of the correct type, will throw (if strict) or return the fallback value. + */ +export function ensureString(value: AnyXMLValue, strict: boolean, fallback = ''): string { + if (typeof value === 'string') { + return value + } else if (strict) throw new Error(`Expected a string, got: ${JSON.stringify(value)}`) + else return fallback +} + +/** + * Ensures that the returned value is an object. + * If the input value is not of the correct type, will throw (if strict) or return the fallback value. + */ +export function ensureXMLObject(value: AnyXMLValue, strict: boolean, fallback: AnyXMLObject = {}): AnyXMLObject { + if (typeof value === 'object') { + if (Array.isArray(value)) { + if (value.length === 0) return {} + if (value.length > 1) { + if (strict) + throw new Error(`Expected only one value, got ${value.length} values: ${JSON.stringify(value)}`) + else return fallback + } + return ensureXMLObject(value[0], strict) + } else { + return value + } + } else if (strict) throw new Error(`Expected an object, got: ${value}`) + else return fallback +} + +/** + * Ensures that the returned value is a singular value (ie a string or undefined). + * If the input value is not of the correct type, will throw (if strict) or return the fallback value. + */ +export function ensureSingular(value: AnyXMLValue, strict: boolean): AnyXMLValueSingular { + // Quick-fix if it is in a xml element: + if (isXMLTextElement(value)) value = value.text + // Quick-fix for empty object + if (isEmpty(value)) value = '' + + if (typeof value === 'object') { + if (Array.isArray(value)) { + if (value.length === 0) return undefined + if (value.length > 1) { + if (strict) + throw new Error(`Expected only one value, got ${value.length} values: ${JSON.stringify(value)}`) + else return undefined + } + return ensureSingular(value[0], strict) + } else if (strict) throw new Error(`Expected only one value, got object: ${JSON.stringify(value)}`) + else return undefined + } else { + return value + } +} +/** + * Ensures that the returned value is an array containing only singular values + * If the input value is not of the correct type, will throw (if strict) or return an empty array + */ +export function ensureSingularArray(value: AnyXMLValue, strict: boolean): AnyXMLValueSingular[] { + if (typeof value === 'object' && Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + value[i] = ensureSingular(value[i], strict) + } + return value as AnyXMLValueSingular[] + } else if (strict) throw new Error(`Expected an Array, got: ${JSON.stringify(value)}`) + else return [] +} +/** + * Ensures that the returned value is an array containing only XMLObjects + * If the input value is not of the correct type, will throw (if strict) or return an empty array + */ +export function ensureXMLObjectArray(value: AnyXMLValue, strict: boolean): AnyXMLObject[] { + const objs: AnyXMLObject[] = [] + for (const obj of ensureArray(value)) { + objs.push(ensureXMLObject(obj, strict)) + } + return objs +} + +export function isSingular(value: AnyXMLValue): value is AnyXMLValueSingular { + try { + ensureSingular(value, true) + return true + } catch { + return false + } +} +export function isSingularArray(value: AnyXMLValue): value is AnyXMLValueSingular[] { + try { + ensureSingularArray(value, true) + return true + } catch { + return false + } +} +export function isXMLObject(value: AnyXMLValue): value is AnyXMLObject { + try { + ensureXMLObject(value, true) + return true + } catch { + return false + } +} +function isXMLTextElement(xml: any): xml is TextElement { + return ( + typeof xml === 'object' && + (xml as TextElement).$type === 'text' && + typeof (xml as TextElement).text === 'string' + ) +} +interface TextElement { + $type: 'text' + $name: string + text: string +} diff --git a/packages/model/src/model.ts b/packages/model/src/model.ts index e790d9db..0c9654a6 100644 --- a/packages/model/src/model.ts +++ b/packages/model/src/model.ts @@ -61,17 +61,18 @@ export interface IMOSROFullStory extends IMOSStory { RunningOrderId: IMOSString128 Body: Array } -export type IMOSROFullStoryBodyItem = - | { - itemType: 'storyItem' - Type: 'storyItem' - Content: IMOSItem // TODO: Make this stricter? IMOSItemObject?? - } - | { - itemType: 'other' - Type: string // e.g. 'p' - Content: AnyXMLValue - } + +export type IMOSROFullStoryBodyItem = IMOSROFullStoryBodyStoryItem | IMOSROFullStoryBodyOtherItem +export type IMOSROFullStoryBodyStoryItem = { + itemType: 'storyItem' + Type: 'storyItem' + Content: IMOSItem // TODO: Make this stricter? IMOSItemObject?? +} +export type IMOSROFullStoryBodyOtherItem = { + itemType: 'other' + Type: string // e.g. 'p' + Content: AnyXMLValue +} export interface IMOSItem { ID: IMOSString128 Slug?: IMOSString128