Server/project/src/utils/JsonUtil.ts
TheSparta 418d9f2a8f Import path alias on the whole project (!157)
- Ability to use @spt-aki path alias on the whole project.
- Swapped all imports from relative paths, for imports using the path alias.

Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/157
Co-authored-by: TheSparta <thesparta@noreply.dev.sp-tarkov.com>
Co-committed-by: TheSparta <thesparta@noreply.dev.sp-tarkov.com>
2023-10-19 17:21:17 +00:00

250 lines
7.3 KiB
TypeScript

import fixJson from "json-fixer";
import JSON5 from "json5";
import { jsonc } from "jsonc";
import { IParseOptions, IStringifyOptions, Reviver } from "jsonc/lib/interfaces";
import { inject, injectable } from "tsyringe";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { VFS } from "@spt-aki/utils/VFS";
@injectable()
export class JsonUtil
{
protected fileHashes = null;
protected jsonCacheExists = false;
protected jsonCachePath = "./user/cache/jsonCache.json";
constructor(
@inject("VFS") protected vfs: VFS,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("WinstonLogger") protected logger: ILogger
)
{ }
/**
* From object to string
* @param data object to turn into JSON
* @param prettify Should output be prettified
* @returns string
*/
public serialize(data: any, prettify = false): string
{
if (prettify)
{
return JSON.stringify(data, null, "\t");
}
else
{
return JSON.stringify(data);
}
}
/**
* From object to string
* @param data object to turn into JSON
* @param replacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.
* @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
* @returns string
*/
public serializeAdvanced(data: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string
{
return JSON.stringify(data, replacer, space);
}
/**
* From object to string
* @param data object to turn into JSON
* @param filename Name of file being serialized
* @param options Stringify options or a replacer.
* @returns The string converted from the JavaScript value
*/
public serializeJsonC(
data: any,
filename?: string | null,
options?: IStringifyOptions | Reviver): string
{
try
{
return jsonc.stringify(data, options);
}
catch (error)
{
this.logger.error(`unable to stringify jsonC file: ${filename} message: ${error.message}, stack: ${error.stack}`);
}
}
public serializeJson5(
data: any,
filename?: string | null,
prettify = false): string
{
try
{
if (prettify)
{
return JSON5.stringify(data, null, "\t");
}
else
{
return JSON5.stringify(data);
}
}
catch (error)
{
this.logger.error(`unable to stringify json5 file: ${filename} message: ${error.message}, stack: ${error.stack}`);
}
}
/**
* From string to object
* @param jsonString json string to turn into object
* @param filename Name of file being deserialized
* @returns object
*/
public deserialize<T>(jsonString: string, filename = ""): T
{
try
{
return JSON.parse(jsonString);
}
catch (error)
{
this.logger.error(`unable to parse json file: ${filename} message: ${error.message}, stack: ${error.stack}`);
}
}
/**
* From string to object
* @param jsonString json string to turn into object
* @param filename Name of file being deserialized
* @param options Parsing options
* @returns object
*/
public deserializeJsonC<T>(jsonString: string, filename = "", options?: IParseOptions): T
{
try
{
return jsonc.parse(jsonString, options);
}
catch (error)
{
this.logger.error(`unable to parse jsonC file: ${filename} message: ${error.message}, stack: ${error.stack}`);
}
}
public deserializeJson5<T>(jsonString: string, filename = ""): T
{
try
{
return JSON5.parse(jsonString);
}
catch (error)
{
this.logger.error(`unable to parse json file: ${filename} message: ${error.message}, stack: ${error.stack}`);
}
}
public async deserializeWithCacheCheckAsync<T>(jsonString: string, filePath: string): Promise<T>
{
return new Promise((resolve) =>
{
resolve(this.deserializeWithCacheCheck<T>(jsonString, filePath));
});
}
/**
* From json string to object
* @param jsonString String to turn into object
* @param filePath Path to json file being processed
* @returns Object
*/
public deserializeWithCacheCheck<T>(jsonString: string, filePath: string): T
{
this.ensureJsonCacheExists(this.jsonCachePath);
this.hydrateJsonCache(this.jsonCachePath);
// Generate hash of string
const generatedHash = this.hashUtil.generateSha1ForData(jsonString);
// Get hash of file and check if missing or hash mismatch
let savedHash = this.fileHashes[filePath];
if (!savedHash || savedHash !== generatedHash)
{
try
{
const { data, changed } = fixJson(jsonString);
if (changed) // data invalid, return it
{
this.logger.error(`${filePath} - Detected faulty json, please fix your json file using VSCodium`);
}
else
{
// data valid, save hash and call function again
this.fileHashes[filePath] = generatedHash;
this.vfs.writeFile(this.jsonCachePath, this.serialize(this.fileHashes, true));
savedHash = generatedHash;
}
return data as T;
}
catch (error)
{
const errorMessage = `Attempted to parse file: ${filePath}. Error: ${error.message}`;
this.logger.error(errorMessage);
throw new Error(errorMessage);
}
}
// Doesn't match
if (savedHash !== generatedHash)
{
throw new Error(`Catastrophic failure processing file ${filePath}`);
}
// Match!
return this.deserialize<T>(jsonString);
}
/**
* Create file if nothing found
* @param jsonCachePath path to cache
*/
protected ensureJsonCacheExists(jsonCachePath: string): void
{
if (!this.jsonCacheExists)
{
if (!this.vfs.exists(jsonCachePath))
{
// Create empty object at path
this.vfs.writeFile(jsonCachePath, "{}");
}
this.jsonCacheExists = true;
}
}
/**
* Read contents of json cache and add to class field
* @param jsonCachePath Path to cache
*/
protected hydrateJsonCache(jsonCachePath: string) : void
{
// Get all file hashes
if (!this.fileHashes)
{
this.fileHashes = this.deserialize(this.vfs.readFile(`${jsonCachePath}`));
}
}
/**
* Convert into string and back into object to clone object
* @param objectToClone Item to clone
* @returns Cloned parameter
*/
public clone<T>(objectToClone: T): T
{
return structuredClone(objectToClone);
}
}