UNPKG

specpress

Version:

Export PDF and/or DOCX files from a subset of Markdown, ASN.1 and JSON files

297 lines 11.5 kB
// Utils.ts — KEYS, utility functions, BaseClass, BaseList import { JsonObject, RAN4JsonEncoder, LoadJsonFileToDict, GetAllJsonFilesInFolder, GetJsonFilesByPattern, ValidateSchema } from "./JsonTools.js"; import { logger } from "./Logger.js"; import { existsSync, mkdirSync } from "node:fs"; import { resolve, join, basename } from "node:path"; ////////////////////////////// // Constants export class KEYS { static scs = "scs"; static scsList = "scsList"; static bandwidthList = "bandwidthList"; static bw = "bw"; static uplink = "uplink"; static downlink = "downlink"; static notes = "notes"; static bcId = "bcId"; static bcsList = "bcsList"; static bcsId = "bcsId"; static bandList = "bandList"; static ulConfigList = "ulConfigList"; static bandEntry = "bandEntry"; static bandNumber = "bandNumber"; static contCarrierGroups = "contCarrierGroups"; static nonContiguousCarriers = "nonContiguousCarriers"; static contiguousCarriers = "contiguousCarriers"; static referencedComponents = "referencedComponents"; static maxAggBwPerBand = "maxAggBwPerBand"; static bandCombinationList = "bandCombinationList"; static singleUlAllowed = "singleUlAllowed"; static dlInterruptionsAllowed = "dlInterruptionsAllowed"; static specification = "specification"; static schemaVersion = "schemaVersion"; } ////////////////////////////// // Exceptions export class UnsupportedKeyException extends Error { constructor(message) { super(message); this.name = "UnsupportedKeyException"; } } export class MissingParentObjectException extends Error { constructor(message) { super(message); this.name = "MissingParentObjectException"; } } export class InvalidInputTypeException extends Error { constructor(message) { super(message); this.name = "InvalidInputTypeException"; } } export class NoEntriesException extends Error { constructor(message) { super(message); this.name = "NoEntriesException"; } } export class DuplicateEntryException extends Error { constructor(message) { super(message); this.name = "DuplicateEntryException"; } } export class FileNameMismatchException extends Error { constructor(message) { super(message); this.name = "FileNameMismatchException"; } } //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// // Generic utility functions // //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// export function IsInt(aStr) { if (typeof aStr === "number" && Number.isInteger(aStr)) return true; if (typeof aStr === "number") return false; const s = String(aStr); if (s === "") return false; // Match Python: str.isnumeric() only matches digit characters (no sign, no dot) if (/^\d+$/.test(s)) { const f = parseFloat(s); if (f === Math.trunc(f)) return true; } return false; } export function listToString(aList) { return aList.join(", "); } export function FindAll(aString, aSubString, aStartIndex = 0) { const occurrences = []; if (aString === null || aString === undefined || aString === "" || aSubString === null || aSubString === undefined || aSubString === "") { return occurrences; } if (aStartIndex < 0) { throw new Error(`aStartIndex must be larger than or equal to 0 but was ${aStartIndex}`); } for (let i = aStartIndex; i <= aString.length - aSubString.length; i++) { if (aString.startsWith(aSubString, i)) { occurrences.push(i); } } return occurrences; } export class BaseClass extends JsonObject { parent; constructor(aParent = null) { super(); this.parent = aParent; } getParent(aType) { if (this.parent !== null) { if (aType === undefined || this.parent instanceof aType) { return this.parent; } else { return this.parent.getParent(aType); } } return null; } getDescriptor(aMaxNrofLevels = 10) { let s = this.constructor.name; const tag = this.getTag(); if (tag !== "") { s = `${s}(${tag})`; } if (aMaxNrofLevels > 1) { const p = this.getParent(); if (p !== null && p instanceof BaseClass) { s = `${p.getDescriptor(aMaxNrofLevels - 1)} -> ${s}`; } } return s; } } export class BaseList extends BaseClass { data = new Map(); constructor(aParent = null) { super(aParent); } getTag() { return ""; } has(aKey) { return this.data.has(String(aKey)); } get(aKey) { return this.data.get(String(aKey)); } add(aValue) { const newEntry = this._createEntry(aValue, this); const entryId = this._getEntryId(newEntry); if (this.data.has(entryId)) { throw new DuplicateEntryException(`${this.getDescriptor()}: Entry for '${entryId}' exists already.`); } this.data.set(entryId, newEntry); } addAll(aList) { for (const oneElement of aList) { this.add(oneElement); } } validate() { if (this.data.size === 0) { throw new NoEntriesException(`${this.getDescriptor()}: List contains no entries.`); } for (const oneEntry of this.data.values()) { oneEntry.validate(); } } validateCollectErrors() { const errors = []; if (this.data.size === 0) { errors.push(`${this.getDescriptor()}: List contains no entries.`); return errors; } for (const oneEntry of this.data.values()) { try { oneEntry.validate(); } catch (e) { errors.push(String(e.message ?? e)); } } return errors; } _getTargetSubfolder(_anEntry) { throw new Error(`Subclass ${this.constructor.name} shall implement _getTargetSubfolder()`); } _getFileName(anEntry) { return `${this._getEntryId(anEntry)}.json`; } _shouldStoreEntry(_anEntry) { return true; } storeAsJsonFiles(aFolder) { const absPath = resolve(aFolder); if (!existsSync(absPath)) { throw new Error(`${this.getDescriptor()}: The given target path '${absPath}' does not exist.`); } logger.log(`${this.getDescriptor()}: Starting to write ${this.data.size} JSON files to folder '${absPath}'`); for (const oneEntry of this.data.values()) { if (!this._shouldStoreEntry(oneEntry)) continue; const targetPath = join(absPath, this._getTargetSubfolder(oneEntry)); if (!existsSync(targetPath)) { mkdirSync(targetPath, { recursive: true }); } const enc = new RAN4JsonEncoder(join(targetPath, this._getFileName(oneEntry))); oneEntry.toJSON(enc, 0); enc.flush(); } logger.log(`${this.getDescriptor()}: Wrote ${this.data.size} JSON files to folder '${absPath}'`); } loadFromFolder(aFolder, skipValidation = false, aJsonSchema = null, abortOnError = true) { return this._loadFiles(GetAllJsonFilesInFolder(aFolder), aJsonSchema, `folder '${aFolder}'`, skipValidation, abortOnError); } loadByPattern(aRootFolder, aPattern, skipValidation = false, aJsonSchema = null, abortOnError = true) { return this._loadFiles(GetJsonFilesByPattern(aRootFolder, aPattern), aJsonSchema, `pattern '${aPattern}' in '${aRootFolder}'`, skipValidation, abortOnError); } _loadFiles(aFileList, aJsonSchema, aSource, skipValidation, abortOnError) { let n = 0; let schemaErrors = 0; let contentErrors = 0; for (const aJsonFile of aFileList) { const tempDict = LoadJsonFileToDict(aJsonFile); if (!ValidateSchema(tempDict, aJsonSchema, `${this.getDescriptor()}: ${aJsonFile}`, abortOnError)) { schemaErrors++; } let newEntry; try { newEntry = this._createEntry(tempDict, this); } catch (e) { const msg = `${this.getDescriptor()}: ${aJsonFile}: ${e.message}`; if (abortOnError) throw e; logger.log(msg); contentErrors++; continue; } const fileName = basename(aJsonFile).replace(".json", ""); if (this._getEntryId(newEntry) !== fileName) { throw new FileNameMismatchException(`${this.getDescriptor()}: The ID '${this._getEntryId(newEntry)}' does not match the file name '${fileName}'.`); } this.add(newEntry); n++; } logger.log(`${this.getDescriptor()}: Imported ${n} entries from ${aSource}.`); if (!skipValidation) { logger.log(`${this.getDescriptor()}: Starting validation of all ${this.data.size} entries.`); if (abortOnError) { this.validate(); } else { const errors = this.validateCollectErrors(); contentErrors = errors.length; if (contentErrors > 0) { for (const oneError of errors) { logger.log(`${this.getDescriptor()}: Content validation error: ${oneError}`); } logger.log(`${this.getDescriptor()}: Found ${contentErrors} content validation error(s).`); } } logger.log(`${this.getDescriptor()}: Validated all ${this.data.size} entries.`); } return { schemaErrors, contentErrors }; } toJSON(anEncoder, aLevel) { throw new Error(`Subclass ${this.constructor.name} shall implement toJSON()`); } } ////////////////////////////// // Normalize any RAN4 JSON file /** * Normalizes a single RAN4 JSON file (Band, CA, or DC configuration). * * Detects the file type from the JSON content and applies appropriate normalization: * - Key ordering enforced by toJSON() methods * - UL configs sorted (for CA/DC) * - notes object keys sorted alphabetically * - Consistent indentation via RAN4JsonEncoder * * @param filePath - Path to the JSON file to normalize * @returns The absolute path of the normalized file * @throws Error if the file is not a valid RAN4 JSON file */ export async function normalizeJsonFile(filePath) { const absPath = resolve(filePath); const dict = LoadJsonFileToDict(absPath); let obj; if ("scsList" in dict) { const { ChBwOneBand } = await import("./ChannelBandwidthPerBand.js"); obj = new ChBwOneBand(dict); } else if (typeof dict["bcId"] === "string" && dict["bcId"].startsWith("DC_")) { const { DualConnectivityConfig } = await import("./DualConnectivity.js"); obj = new DualConnectivityConfig(dict, null, false, false); } else { const { BC } = await import("./BandCombinations.js"); obj = new BC(dict); } const enc = new RAN4JsonEncoder(absPath); obj.toJSON(enc, 0); enc.flush(); return absPath; } //# sourceMappingURL=Utils.js.map