nimcodec
Version:
Encoder/decoder for satellite IoT using Non-IP Messages
196 lines (187 loc) • 5.9 kB
JavaScript
/**
* Helpers for importing and validating NIMO codec keys/files.
*/
const Types = require('./types');
const fs = require('fs');
const { xml2json } = require('xml-js');
/** @type {Types.NimoCodec} */
/**
* Substitutions for conversion from legacy XML format
* @private
*/
const substitutions = {
MessageDefinition: 'nimoCodecKey',
SIN: 'serviceKey',
MIN: 'messageKey',
'xsi:type': 'type',
ForwardMessages: 'downlinkMessages',
ReturnMessages: 'uplinkMessages',
ArrayField: 'array',
BooleanField: 'bool',
DataField: 'data',
EnumField: 'enum',
UnsignedIntField: 'uint',
SignedIntField: 'int',
BitKeyListField: 'bitkeylist',
};
// function getKeyByValue(obj, value) {
// return Object.keys(obj).find(key => obj[key] === value);
// }
/**
* Convert a key/string to camelCase representation
* @private
* @param {String} original The string to convert
* @param {boolean} skip_caps Set to avoid converting CAPITAL_CASE
* @returns A camelCase representation
*/
function camelCase(original, skip_caps = false) {
if (typeof original != 'string') throw new Error('Invalid string input');
if (original.toUpperCase() === original && skip_caps) return original;
const words = original.match(/[A-Z][a-z]+/g);
if (words === null) return original;
words[0] = words[0].toLowerCase();
return words.join('');
}
/**
* Convert an imported legacy Message Definition File to `NimoCodec` key
* @private
* @param {Object} obj A legacy Message Definition File converted to JSON object
* @returns {NimoCodec} Simplified codec Object
*/
function legacyMdfToCodec(obj) {
try {
if (typeof obj != 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
if (!obj.every((el) => (typeof el === 'object' && el != null))) {
return obj;
}
for (let i = 0; i < obj.length; i++) {
obj[i] = legacyMdfToCodec(obj[i]);
}
}
for (let [key, value] of Object.entries(obj)) {
if (key === '_text') {
return value;
}
if (typeof value === 'object') {
if (Object.hasOwn(value, '_text')) {
value = value._text;
} else if (Object.hasOwn(value, 'string')) {
value = value.string;
}
}
if (key === '_attributes') {
if (Object.hasOwn(value, 'xsi:type')) {
obj.type = substitutions[value['xsi:type']];
}
delete obj[key];
continue;
}
const listKeys = ['Services', 'ForwardMessages', 'ReturnMessages', 'Fields'];
if (listKeys.includes(key)) {
let subKey = key.includes('Message') ? 'Message' : key.slice(0, -1);
const newKey = key in substitutions ? substitutions[key] : camelCase(key);
obj[newKey] = [];
if (Array.isArray(value[subKey])) {
for (let x of value[subKey]) {
obj[newKey].push(legacyMdfToCodec(x));
}
} else {
if (typeof value[subKey] != 'undefined') {
obj[newKey].push(legacyMdfToCodec(value[subKey]));
}
}
delete obj[key];
} else {
if (typeof value === 'object' && value !== null) {
value = legacyMdfToCodec(value);
} else if (value in substitutions) {
value = substitutions[value];
}
let newKey;
if (Object.hasOwn(substitutions, key)) {
newKey = substitutions[key];
} else {
newKey = camelCase(key);
}
if (newKey != key) {
const intKeys = ['codecServiceId', 'codecMessageId', 'size'];
const boolKeys = ['optional', 'fixed'];
if (intKeys.includes(newKey)) {
value = parseInt(value);
} else if (boolKeys.includes(newKey)) {
value = (value.toLowerCase() === 'true');
}
obj[newKey] = value;
delete obj[key];
}
}
}
} catch (err) {
console.error(err);
}
return obj;
}
/**
* Imports and basic sanity on the specified `NimCodec` key.
* Accepts valid `NimoCodec`, string representation in XML or JSON, or the path
* to a valid XML(.idpmsg) or JSON file.
* @memberof nimo
* @param {NimoCodec|string} codec Codec object, string or file path
* @returns {NimoCodec} valid Codec object
* @throws If unable to parse or validate the codec
*/
function importCodec(codec) {
if (typeof codec === 'string') {
if (fs.existsSync(codec)) {
try {
codec = fs.readFileSync(codec).toString();
} catch (err) {
throw new Error(`Unable to read file ${codec} (${err})`);
}
}
if (codec.startsWith('<')) {
try {
codec = legacyMdfToCodec(JSON.parse(
xml2json(codec, {compact: true, spaces: 0})));
} catch (err) {
throw new Error(`Unable to parse XML codec (${err})`);
}
} else if (codec.startsWith('{')) {
try {
codec = JSON.parse(codec);
} catch (err) {
throw new Error(`Unable to parse JSON codec (${err})`);
}
} else {
throw new Error('Invalid codec must be JSON or XML');
}
}
if (Object.hasOwn(codec, 'nimoCodecKey')) {
codec = codec.nimoCodecKey;
}
if (!Object.hasOwn(codec, 'services') || !Array.isArray(codec.services)) {
throw new Error('Invalid codec');
}
return codec;
}
/**
* Export to NIM codec key file format
* @memberof nimo
* @param {NimoCodec} codec
* @param {String} filepath
*/
function exportJson(codec, filepath) {
const wrapped = { messageDefinition: codec };
fs.writeFileSync(filepath, JSON.stringify(wrapped));
}
/**
* Expert to IDPMSG/XML format (PLACEHOLDER NOT IMPLEMENTED)
* @memberof nimo
* @param {NimoCodec} codec The codec key object
* @param {String} filepath The location/name of the file to output
*/
function exportXml(codec, filepath) {
throw new Error('Not implemented');
}
module.exports = { importCodec, exportJson, exportXml };