specpress
Version:
Export PDF and/or DOCX files from a subset of Markdown, ASN.1 and JSON files
297 lines • 11.5 kB
JavaScript
// 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