Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions shared/src/model-visitor/logging-visitor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import {Resolvable, ResolvableAndAdaptable} from '../model/resolvable';
import {CalmModelVisitor} from './calm-model-visitor';
import { initLogger } from '../logger.js';
import { Resolvable, ResolvableAndAdaptable } from '../model/resolvable';
import { CalmModelVisitor } from './calm-model-visitor';
import { initLogger, Logger } from '../logger.js';

export class LoggingVisitor implements CalmModelVisitor {
private static logger = initLogger(process.env.DEBUG === 'true', LoggingVisitor.name);
private static _logger: Logger | undefined;

private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', LoggingVisitor.name);
}
return this._logger;
}
// Setter for testing purposes
private static set logger(value: Logger) {
this._logger = value;
}

async visit(obj: unknown, path: string[] = []): Promise<void> {
const logger = LoggingVisitor.logger;
Expand Down
31 changes: 26 additions & 5 deletions shared/src/resolver/calm-reference-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from 'fs';
import { initLogger } from '../logger.js';
import { initLogger, Logger } from '../logger.js';
import axios from 'axios';

export interface CalmReferenceResolver {
Expand All @@ -8,8 +8,14 @@ export interface CalmReferenceResolver {
}

export class FileReferenceResolver implements CalmReferenceResolver {
private static _logger: Logger | undefined;

private static logger = initLogger(process.env.DEBUG === 'true', FileReferenceResolver.name);
private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', FileReferenceResolver.name);
}
return this._logger;
}

canResolve(ref: string): boolean {
return fs.existsSync(ref);
Expand Down Expand Up @@ -48,7 +54,14 @@ export class InMemoryResolver implements CalmReferenceResolver {


export class HttpReferenceResolver implements CalmReferenceResolver {
private static logger = initLogger(process.env.DEBUG === 'true', HttpReferenceResolver.name);
private static _logger: Logger | undefined;

private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', HttpReferenceResolver.name);
}
return this._logger;
}

canResolve(ref: string): boolean {
return ref.startsWith('http://') || ref.startsWith('https://');
Expand All @@ -70,7 +83,15 @@ export class HttpReferenceResolver implements CalmReferenceResolver {
}

export class CompositeReferenceResolver implements CalmReferenceResolver {
private static logger = initLogger(process.env.DEBUG === 'true', CompositeReferenceResolver.name);
private static _logger: Logger | undefined;

private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', CompositeReferenceResolver.name);
}
return this._logger;
}

private httpResolver: HttpReferenceResolver;
private fileResolver: FileReferenceResolver;

Expand Down Expand Up @@ -107,7 +128,7 @@ export class MappedReferenceResolver implements CalmReferenceResolver {
constructor(
private mapping: Map<string, string>,
private delegate: CalmReferenceResolver
) {}
) { }

canResolve(ref: string): boolean {
const effective = this.getEffectiveRef(ref);
Expand Down
13 changes: 10 additions & 3 deletions shared/src/template/template-bundle-file-loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import {IndexFile, TemplateEntry} from './types.js';
import { initLogger } from '../logger.js';
import { IndexFile, TemplateEntry } from './types.js';
import { initLogger, Logger } from '../logger.js';


export interface ITemplateBundleLoader {
Expand Down Expand Up @@ -88,7 +88,14 @@ export class TemplateBundleFileLoader implements ITemplateBundleLoader {
private readonly templateBundlePath: string;
private readonly config: IndexFile;
private readonly templateFiles: Record<string, string>;
private static logger = initLogger(process.env.DEBUG === 'true', TemplateBundleFileLoader.name);
private static _logger: Logger | undefined;

private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', TemplateBundleFileLoader.name);
}
return this._logger;
}

constructor(templateBundlePath: string) {
this.templateBundlePath = templateBundlePath;
Expand Down
8 changes: 5 additions & 3 deletions shared/src/template/template-engine.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ vi.mock('./template-path-extractor.js');
describe('TemplateEngine', () => {
let mockFileLoader: ReturnType<typeof vi.mocked<TemplateBundleFileLoader>>;
let mockTransformer: ReturnType<typeof vi.mocked<CalmTemplateTransformer>>;
let loggerDebugSpy: ReturnType<typeof vi.spyOn>;
let loggerInfoSpy: ReturnType<typeof vi.spyOn>;
let loggerWarnSpy: ReturnType<typeof vi.spyOn>;
const testOutputDir = './test-output';
Expand All @@ -29,6 +30,7 @@ describe('TemplateEngine', () => {
getTransformedModel: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<CalmTemplateTransformer>>;

loggerDebugSpy = vi.spyOn(TemplateEngine['logger'], 'debug').mockImplementation(vi.fn());
loggerInfoSpy = vi.spyOn(TemplateEngine['logger'], 'info').mockImplementation(vi.fn());
loggerWarnSpy = vi.spyOn(TemplateEngine['logger'], 'warn').mockImplementation(vi.fn());

Expand Down Expand Up @@ -119,7 +121,7 @@ describe('TemplateEngine', () => {

vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const mkdirSyncSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => { });

const engine = new TemplateEngine(mockFileLoader, mockTransformer);

Expand Down Expand Up @@ -156,7 +158,7 @@ describe('TemplateEngine', () => {

vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const mkdirSyncSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => { });

const engine = new TemplateEngine(mockFileLoader, mockTransformer);

Expand Down Expand Up @@ -276,7 +278,7 @@ describe('TemplateEngine', () => {
new TemplateEngine(mockFileLoader, mockTransformer);

expect(TemplatePreprocessor.preprocessTemplate).toHaveBeenCalledWith(originalTemplate);
expect(loggerInfoSpy).toHaveBeenCalledWith(preprocessedTemplate);
expect(loggerDebugSpy).toHaveBeenCalledWith(preprocessedTemplate);
});
});

Expand Down
19 changes: 13 additions & 6 deletions shared/src/template/template-engine.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Handlebars from 'handlebars';
import { IndexFile, TemplateEntry, CalmTemplateTransformer } from './types.js';
import {ITemplateBundleLoader} from './template-bundle-file-loader.js';
import { initLogger } from '../logger.js';
import { ITemplateBundleLoader } from './template-bundle-file-loader.js';
import { initLogger, Logger } from '../logger.js';
import fs from 'fs';
import path from 'path';
import {TemplatePathExtractor} from './template-path-extractor.js';
import {TemplatePreprocessor} from './template-preprocessor.js';
import { TemplatePathExtractor } from './template-path-extractor.js';
import { TemplatePreprocessor } from './template-preprocessor.js';

export class TemplateEngine {
private readonly templates: Record<string, Handlebars.TemplateDelegate>;
private readonly config: IndexFile;
private transformer: CalmTemplateTransformer;
private static logger = initLogger(process.env.DEBUG === 'true', TemplateEngine.name);
private static _logger: Logger | undefined;

private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', TemplateEngine.name);
}
return this._logger;
}

constructor(fileLoader: ITemplateBundleLoader, transformer: CalmTemplateTransformer) {
this.config = fileLoader.getConfig();
Expand All @@ -27,7 +34,7 @@ export class TemplateEngine {

for (const [fileName, content] of Object.entries(templateFiles)) {
const preprocessed = TemplatePreprocessor.preprocessTemplate(content);
logger.info(preprocessed);
logger.debug(preprocessed);
compiledTemplates[fileName] = Handlebars.compile(preprocessed);
}

Expand Down
65 changes: 36 additions & 29 deletions shared/src/template/template-path-extractor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JSONPath } from 'jsonpath-plus';
import _ from 'lodash';
import { initLogger } from '../logger.js';
import { initLogger, Logger } from '../logger.js';

export interface PathExtractionOptions {
filter?: Record<string, JsonFragment>;
Expand All @@ -15,17 +15,24 @@ export type JsonFragment = string | number | boolean | null | JsonFragment[] | {
* It translates custom dotted path syntax into JSONPath internally.
*/
export class TemplatePathExtractor {
private static logger = initLogger(process.env.DEBUG === 'true', TemplatePathExtractor.name);
private static _logger: Logger | undefined;

private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', TemplatePathExtractor.name);
}
return this._logger;
}

static convertFromDotNotation(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
document: any,
path: string,
options: PathExtractionOptions = {}
): JsonFragment {
): JsonFragment {
const logger = TemplatePathExtractor.logger;

logger.info(`Extracting path "${path}" from document with options: ${JSON.stringify(options, null, 2)}`);
logger.debug(`Extracting path "${path}" from document with options: ${JSON.stringify(options, null, 2)}`);

try {
// Check if we have options that need processing
Expand All @@ -35,28 +42,28 @@ export class TemplatePathExtractor {

// Optimization: if it's a simple property name (no dots, brackets) AND no options need processing
if (this.isSimpleProperty(path) && !hasOptions) {
logger.info(`PATH: ${path}`);
logger.info('Direct property access (no JSONPath needed, no options to process)');
logger.debug(`PATH: ${path}`);
logger.debug('Direct property access (no JSONPath needed, no options to process)');
const result = document[path];
return result;
}

// For complex paths or when options need processing, use JSONPath
const jsonPath = this.toJsonPath(path);
logger.info(`PATH: ${path}`);
logger.info(`Converted to JSONPath: ${jsonPath}`);
logger.debug(`PATH: ${path}`);
logger.debug(`Converted to JSONPath: ${jsonPath}`);

let result = JSONPath({
path: jsonPath,
json: document,
flatten: false
});

logger.info(`Raw JSONPath result: ${JSON.stringify(result, null, 2)}`);
logger.debug(`Raw JSONPath result: ${JSON.stringify(result, null, 2)}`);

// Ensure result is always an array for consistent processing
result = Array.isArray(result) ? result : [result];
logger.info(`After array normalization: ${JSON.stringify(result, null, 2)}`);
logger.debug(`After array normalization: ${JSON.stringify(result, null, 2)}`);

// Handle empty results early
if (result.length === 0) {
Expand All @@ -68,17 +75,17 @@ export class TemplatePathExtractor {
const shouldReturnArrayFromPath = this.shouldReturnArray(path);
const shouldReturnArrayFromContent = result.length === 1 && Array.isArray(result[0]);

logger.info(`shouldReturnArrayFromPath: ${shouldReturnArrayFromPath}`);
logger.info(`shouldReturnArrayFromContent: ${shouldReturnArrayFromContent}`);
logger.debug(`shouldReturnArrayFromPath: ${shouldReturnArrayFromPath}`);
logger.debug(`shouldReturnArrayFromContent: ${shouldReturnArrayFromContent}`);

// If we have a single result that is an array, apply options to its contents
if (shouldReturnArrayFromContent) {
let arrayContents = result[0];
logger.info(`Processing array contents (length: ${arrayContents.length})`);
logger.debug(`Processing array contents (length: ${arrayContents.length})`);

// Apply filtering to array contents
if (options.filter) {
logger.info(`Applying filter: ${JSON.stringify(options.filter)}`);
logger.debug(`Applying filter: ${JSON.stringify(options.filter)}`);
const beforeFilter = arrayContents.length;

// If filter is a string, parse it; otherwise, use as-is
Expand All @@ -87,63 +94,63 @@ export class TemplatePathExtractor {
: options.filter;

arrayContents = arrayContents.filter(item => this.matchesFilter(item, filterObj!));
logger.info(`After filtering: ${beforeFilter} -> ${arrayContents.length} items`);
logger.debug(`After filtering: ${beforeFilter} -> ${arrayContents.length} items`);
}

// Apply sorting to array contents
if (options.sort) {
const sortKeys = Array.isArray(options.sort) ? options.sort : [options.sort];
logger.info(`Applying sort by: ${JSON.stringify(sortKeys)}`);
logger.debug(`Applying sort by: ${JSON.stringify(sortKeys)}`);
arrayContents = _.orderBy(arrayContents, sortKeys);
}

// Apply limiting to array contents
if (options.limit && options.limit > 0) {
logger.info(`Applying limit: ${options.limit}`);
logger.debug(`Applying limit: ${options.limit}`);
const beforeLimit = arrayContents.length;
arrayContents = arrayContents.slice(0, options.limit);
logger.info(`After limiting: ${beforeLimit} -> ${arrayContents.length} items`);
logger.debug(`After limiting: ${beforeLimit} -> ${arrayContents.length} items`);
}

logger.info(`Final array contents result: ${JSON.stringify(arrayContents, null, 2)}`);
logger.debug(`Final array contents result: ${JSON.stringify(arrayContents, null, 2)}`);
return arrayContents;
}

// For non-array content, apply filtering, sorting, and limiting normally
logger.info(`Processing non-array content (${result.length} items)`);
logger.debug(`Processing non-array content (${result.length} items)`);

if (options.filter) {
logger.info(`Applying filter: ${JSON.stringify(options.filter)}`);
logger.debug(`Applying filter: ${JSON.stringify(options.filter)}`);
const beforeFilter = result.length;
const filterObj = typeof options.filter === 'string'
? this.parseFilter(options.filter)
: options.filter;
result = result.filter(item => this.matchesFilter(item, filterObj!));
logger.info(`After filtering: ${beforeFilter} -> ${result.length} items`);
logger.debug(`After filtering: ${beforeFilter} -> ${result.length} items`);
}

if (options.sort) {
const sortKeys = Array.isArray(options.sort) ? options.sort : [options.sort];
logger.info(`Applying sort by: ${JSON.stringify(sortKeys)}`);
logger.debug(`Applying sort by: ${JSON.stringify(sortKeys)}`);
result = _.orderBy(result, sortKeys);
}

if (options.limit && options.limit > 0) {
logger.info(`Applying limit: ${options.limit}`);
logger.debug(`Applying limit: ${options.limit}`);
const beforeLimit = result.length;
result = result.slice(0, options.limit);
logger.info(`After limiting: ${beforeLimit} -> ${result.length} items`);
logger.debug(`After limiting: ${beforeLimit} -> ${result.length} items`);
}

// Decide whether to return single object or array based on the path type
if (shouldReturnArrayFromPath || result.length !== 1) {
logger.info(`Returning array result (length: ${result.length})`);
logger.info(`Final result: ${JSON.stringify(result, null, 2)}`);
logger.debug(`Returning array result (length: ${result.length})`);
logger.debug(`Final result: ${JSON.stringify(result, null, 2)}`);
return result;
}

logger.info('Returning single result');
logger.info(`Final result: ${JSON.stringify(result[0], null, 2)}`);
logger.debug('Returning single result');
logger.debug(`Final result: ${JSON.stringify(result[0], null, 2)}`);
return result[0];
} catch (err) {
logger.warn(`Failed to extract path "${path}": ${err.message}`);
Expand Down
12 changes: 10 additions & 2 deletions shared/src/template/template-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
SelfProvidedTemplateLoader,
TemplateBundleFileLoader
} from './template-bundle-file-loader.js';
import { initLogger } from '../logger.js';
import { initLogger, Logger } from '../logger.js';
import { CompositeReferenceResolver, MappedReferenceResolver } from '../resolver/calm-reference-resolver.js';
import { pathToFileURL } from 'node:url';
import TemplateDefaultTransformer from './template-default-transformer';
Expand All @@ -26,10 +26,18 @@ export class TemplateProcessor {
private readonly outputPath: string;
private readonly urlToLocalPathMapping: Map<string, string>;
private readonly mode: TemplateProcessingMode;
private static logger = initLogger(process.env.DEBUG === 'true', TemplateProcessor.name);
private readonly supportWidgetEngine: boolean;
private readonly clearOutputDirectory: boolean = false;

private static _logger: Logger | undefined;

private static get logger(): Logger {
if (!this._logger) {
this._logger = initLogger(process.env.DEBUG === 'true', TemplateProcessor.name);
}
return this._logger;
}

constructor(inputPath: string, templateBundlePath: string, outputPath: string, urlToLocalPathMapping: Map<string, string>, mode: TemplateProcessingMode = 'bundle', supportWidgetEngine: boolean = false, clearOutputDirectory: boolean = false) {
this.inputPath = inputPath;
this.templateBundlePath = templateBundlePath;
Expand Down
Loading