const CONFIG = require('./config');
const fs = require('fs');
const path = require('path');
const logger = require('@marketto/js-logger').global();
const memoryCache = require('memory-cache');
const fastXmlParser = require('fast-xml-parser');
const yaml = require('js-yaml');
const { PathRetriever } = require('./path-retriever.class');
/**
* @description Resource loader utility
* @class ResourceLoader
*/
class ResourceLoader {
/**
* @description Hanlde Route Http Config yaml
* @static
* @method statusCodeRoute
* @param {Express.Request} req Request
* @param {Express.Response} res Response
* @param {Function} next Next route
* @memberof ResourceLoader
*/
static resourceConfigRoute(req, res, next) {
const cacheKey = JSON.stringify([req.servicePath, req.method, 'config']);
const cachedConfig = memoryCache.get(cacheKey);
const cacheLifetime = req.cacheLifetime || 300;
if (cachedConfig) {
logger.info(`Code Served from cache for ${req.method} on ${req.url}`);
req.resConfig = cachedConfig;
res.append('cached-config', 'ResourceLoader');
return next();
}
const configPath = PathRetriever.find({
target : req.servicePath,
ext : 'config.yml',
prefix: req.method,
cwd: req.workingDir
});
logger.debug(`[ConfigPath] ${configPath}`);
if (!configPath) {
logger.debug(`Config File not found`);
return next();
}
logger.debug(`Config File found`);
try{
const parsedConfig = yaml.safeLoad(fs.readFileSync(configPath)) || {};
if (typeof parsedConfig !== 'object'){
throw new Error(`Yaml syntax error`);
}
//Checking status code
if (parsedConfig.status) {
const parsedStatus = parseInt(parsedConfig.status);
if (!isNaN(parsedStatus) && parsedStatus >= 100 && parsedStatus < 600) {
parsedConfig.status = parsedStatus;
logger.info(`Status Code loaded: ${parsedStatus}`);
} else {
return next(new Error(`Provided config status is not a valid number between 100 and 599`));
}
}
//Checking delay value
if (parsedConfig.delay) {
const parsedDelay = parseInt(parsedConfig.delay);
if (!isNaN(parsedDelay) && parsedDelay >= 0 && parsedDelay <= CONFIG.MAX_DELAY) {
parsedConfig.delay = parsedDelay;
logger.info(`Delay Code loaded: ${parsedDelay}`);
} else {
return next(new Error(`Provided config delay is not a valid number between 0 and ${CONFIG.MAX_DELAY}`));
}
}
req.resConfig = parsedConfig;
memoryCache.put(cacheKey, parsedConfig, cacheLifetime);
logger.info(`Config loaded ${configPath}: ${parsedConfig}`);
} catch (e){
return next(new Error(`Unable to read config.yml file @${configPath} ${e.message}`));
}
return next();
}
/**
* @property {Array<string>} TYPES
* @readonly
* @static
* @memberof ResourceLoader
*/
static get TYPES() {
return ['json', 'xml', 'txt'];
}
/**
* @description Determinates mock types to seek based on accept request header
* @static
* @method acceptedTypes
* @param {Express.Request} req Request
* @returns {Array<string>}
* @memberof ResourceLoader
*/
static acceptedTypes(req) {
logger.debug(`Req accept JSON: ${req.accepts('json')}`);
logger.debug(`Req accept XML: ${req.accepts('xml')}`);
return this.TYPES.sort((typeA, typeB) => !req.accepts(typeA) - !req.accepts(typeB));
}
/**
* @description Recognize content type and returns it parsed
* @static
* @param {string} content Raw content to parse
* @param {string} fileExtension fileExtension
* @returns {Object}
* @memberof ResourceLoader
*/
static autoParse(content, fileExtension) {
const fileType = ((fileExtension.match(/[^.]+$/i) || [])[0] || '').toLowerCase();
const PARSE_CFG = {
xml: input => {
const valid = fastXmlParser.validate(input);
if (valid !== true) {
throw new Error(valid.err.msg);
}
return fastXmlParser.parse(input);
},
json: input => {
return JSON.parse(input);
},
txt: input => input
};
logger.debug(`Content type ${fileType}`);
const parser = PARSE_CFG[fileType];
if (!parser) {
throw new Error(`Can't parse ${fileType} files`);
}
return parser(content);
}
/**
* @description Hanlde Route Http response raw body (json/xml etc) and request json-schema validation
* @static
* @method statusCodeRoute
* @param {Express.Request} req Request
* @param {Express.Response} res Response
* @param {Function} next Next route
* @memberof ResourceLoader
*/
static resourceRoute(req, res, next){
const acceptedTypes = ResourceLoader.acceptedTypes(req);
const cacheKey = JSON.stringify([req.servicePath, req.method, ...acceptedTypes]);
const cachedData = memoryCache.get(cacheKey);
const cacheLifetime = req.cacheLifetime || 300;
if (cachedData) {
logger.info(`Body Served from cache for ${req.method} on ${req.url}`);
res.append('cached-response', 'ResourceLoader');
req.resBody = cachedData;
return next();
}
logger.debug(`Accepted types: ${JSON.stringify(acceptedTypes)}`);
const mockPath = PathRetriever.find({
target : req.servicePath,
ext : acceptedTypes,
prefix: req.method,
cwd: req.workingDir
});
if (!mockPath){
logger.info(`File not found for ${req.servicePath}`);
return next();
}
logger.info(`File found: ${mockPath}`);
try{
const jsonData = ResourceLoader.autoParse(fs.readFileSync(mockPath, {
encoding: 'utf-8'
}), path.extname(mockPath));
memoryCache.put(cacheKey, jsonData, cacheLifetime);
logger.debug(`Served ${mockPath} for ${req.method} on ${req.url}`);
req.resBody = jsonData;
} catch (err) {
return next(new Error(`Unable to read or parse file @${mockPath} ${err}`));
}
return next();
}
}
exports.ResourceLoader = ResourceLoader;