ondc-code-generator 0.5.41 → 0.5.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator/config-compiler.js +6 -2
- package/dist/generator/generators/documentation/markdown-message-generator.d.ts +6 -0
- package/dist/generator/generators/documentation/markdown-message-generator.js +23 -15
- package/dist/generator/generators/javascript/js-generator.d.ts +19 -0
- package/dist/generator/generators/javascript/js-generator.js +215 -0
- package/dist/generator/generators/javascript/js-native-generator.d.ts +0 -0
- package/dist/generator/generators/javascript/js-native-generator.js +1 -0
- package/dist/index.js +21 -11
- package/dist/services/return-complier/ast-functions/compile-to-markdown.d.ts +23 -1
- package/dist/services/return-complier/ast-functions/compile-to-markdown.js +110 -43
- package/dist/types/compiler-types.d.ts +2 -1
- package/dist/types/compiler-types.js +1 -0
- package/dist/utils/logger.js +1 -0
- package/package.json +1 -1
- package/validPaths.json +0 -11229
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from "url";
|
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { duplicateVariablesInChildren } from "../utils/config-utils/duplicateVariables.js";
|
|
12
12
|
import { PythonGenerator } from "./generators/python/py-generator.js";
|
|
13
|
+
import { JavascriptGenerator } from "./generators/javascript/js-generator.js";
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = path.dirname(__filename);
|
|
15
16
|
const defaultConfig = {
|
|
@@ -58,7 +59,6 @@ export class ConfigCompiler {
|
|
|
58
59
|
this.generateCode = async (valConfig, codeName = "L1-Validations", minimal = false, outputPath = "./") => {
|
|
59
60
|
valConfig = JSON.parse(JSON.stringify(valConfig));
|
|
60
61
|
if (this.generatorConfig?.duplicateVariablesInChildren) {
|
|
61
|
-
console.log("Duplicating variables");
|
|
62
62
|
valConfig = duplicateVariablesInChildren(valConfig);
|
|
63
63
|
}
|
|
64
64
|
if (minimal) {
|
|
@@ -80,6 +80,11 @@ export class ConfigCompiler {
|
|
|
80
80
|
codeName: codeName,
|
|
81
81
|
});
|
|
82
82
|
break;
|
|
83
|
+
case SupportedLanguages.Javascript:
|
|
84
|
+
await new JavascriptGenerator(valConfig, this.errorDefinitions ?? [], targetPath).generateCode({
|
|
85
|
+
codeName: codeName,
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
83
88
|
default:
|
|
84
89
|
throw new Error("Language not supported");
|
|
85
90
|
}
|
|
@@ -105,7 +110,6 @@ export class ConfigCompiler {
|
|
|
105
110
|
};
|
|
106
111
|
});
|
|
107
112
|
const template = readFileSync(path.resolve(__dirname, "../generator/generators/typescript/templates/schema-template.mustache"), "utf-8");
|
|
108
|
-
console.log(actions);
|
|
109
113
|
const l0 = Mustache.render(template, { actions });
|
|
110
114
|
await writeAndFormatCode(targetPath, `index.ts`, l0, "typescript");
|
|
111
115
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import { TestObject } from "../../../types/config-types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generates the human-friendly Markdown doc:
|
|
4
|
+
* - Title: "### Validation: <NAME>"
|
|
5
|
+
* - Body: top-level AND/OR group (no 1.1.1 pointers)
|
|
6
|
+
* - Skip: blockquoted "Skip if:" with a unified "Any of these are true:" list
|
|
7
|
+
*/
|
|
2
8
|
export declare function markdownMessageGenerator(returnInput: string, variableValues: TestObject, startingPointer: string, skipInput?: string[]): string;
|
|
@@ -1,26 +1,34 @@
|
|
|
1
|
-
import { CompileToMarkdown } from "../../../services/return-complier/ast-functions/compile-to-markdown.js";
|
|
1
|
+
import { CompileToMarkdown, CompileToMarkdownForSkip, } from "../../../services/return-complier/ast-functions/compile-to-markdown.js";
|
|
2
2
|
import { buildAstFromInput } from "../../../services/return-complier/combined.js";
|
|
3
3
|
import Mustache from "mustache";
|
|
4
|
-
import { addBlockquoteToMarkdown,
|
|
4
|
+
import { addBlockquoteToMarkdown, ConvertArrayToStringsInTestObject, } from "../../../utils/general-utils/string-utils.js";
|
|
5
5
|
import { TestObjectSyntax } from "../../../constants/syntax.js";
|
|
6
|
+
/**
|
|
7
|
+
* Generates the human-friendly Markdown doc:
|
|
8
|
+
* - Title: "### Validation: <NAME>"
|
|
9
|
+
* - Body: top-level AND/OR group (no 1.1.1 pointers)
|
|
10
|
+
* - Skip: blockquoted "Skip if:" with a unified "Any of these are true:" list
|
|
11
|
+
*/
|
|
6
12
|
export function markdownMessageGenerator(returnInput, variableValues, startingPointer, skipInput) {
|
|
7
13
|
const ast = buildAstFromInput(returnInput);
|
|
8
14
|
const returnTemplate = variableValues[TestObjectSyntax.Description]
|
|
9
15
|
? variableValues[TestObjectSyntax.Description]
|
|
10
|
-
: CompileToMarkdown(ast,
|
|
11
|
-
let finalReturn =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
: CompileToMarkdown(ast, /*topLevel*/ true, /*depth*/ 0, /*forNot*/ false);
|
|
17
|
+
let finalReturn = `#### ${startingPointer}\n\n` +
|
|
18
|
+
Mustache.render(returnTemplate, ConvertArrayToStringsInTestObject(variableValues));
|
|
19
|
+
if (skipInput && skipInput.length > 0) {
|
|
20
|
+
let skipMarkdown = `**Skip if:**\n`;
|
|
21
|
+
for (const expr of skipInput) {
|
|
22
|
+
const skAst = buildAstFromInput(expr);
|
|
23
|
+
const skBlock = CompileToMarkdownForSkip(skAst,
|
|
24
|
+
/*topLevel*/ false,
|
|
25
|
+
/*depth*/ 2,
|
|
26
|
+
/*forNot*/ false);
|
|
27
|
+
skipMarkdown += `\n${skBlock}`;
|
|
22
28
|
}
|
|
23
|
-
|
|
29
|
+
// ✅ Render the skip section with Mustache so {{{attr}}} etc. interpolate
|
|
30
|
+
const renderedSkip = Mustache.render(skipMarkdown, ConvertArrayToStringsInTestObject(variableValues));
|
|
31
|
+
const blockSkip = addBlockquoteToMarkdown(renderedSkip);
|
|
24
32
|
finalReturn += `\n\n${blockSkip}`;
|
|
25
33
|
}
|
|
26
34
|
return finalReturn;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CodeGenerator, CodeGeneratorProps } from "../classes/abstract-generator.js";
|
|
2
|
+
export declare class JavascriptGenerator extends CodeGenerator {
|
|
3
|
+
private tempTsPath;
|
|
4
|
+
constructor(validationConfig: any, errorCodes: any[], rootPath?: string);
|
|
5
|
+
generateSessionDataCode: () => Promise<never>;
|
|
6
|
+
generateValidationCode: () => Promise<void>;
|
|
7
|
+
generateCode: (codeConfig: CodeGeneratorProps) => Promise<void>;
|
|
8
|
+
private createTsConfig;
|
|
9
|
+
private compileToJavaScript;
|
|
10
|
+
private processGeneratedFiles;
|
|
11
|
+
private copyAndProcessJsFiles;
|
|
12
|
+
private copyTypeDefinitions;
|
|
13
|
+
private processJavaScriptImports;
|
|
14
|
+
private processTypeDefinitionImports;
|
|
15
|
+
private getAllFiles;
|
|
16
|
+
private generatePackageJson;
|
|
17
|
+
private runCommand;
|
|
18
|
+
private cleanup;
|
|
19
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { CodeGenerator, } from "../classes/abstract-generator.js";
|
|
4
|
+
import { writeAndFormatCode } from "../../../utils/fs-utils.js";
|
|
5
|
+
import { TypescriptGenerator } from "../typescript/ts-generator.js";
|
|
6
|
+
import logger from "../../../utils/logger.js";
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
import { MarkdownDocGenerator } from "../documentation/md-generator.js";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
export class JavascriptGenerator extends CodeGenerator {
|
|
15
|
+
constructor(validationConfig, errorCodes, rootPath = "./") {
|
|
16
|
+
super(validationConfig, errorCodes, rootPath);
|
|
17
|
+
this.generateSessionDataCode = async () => {
|
|
18
|
+
throw new Error("Method not implemented.");
|
|
19
|
+
};
|
|
20
|
+
this.generateValidationCode = async () => {
|
|
21
|
+
// This will be handled by the TypeScript generator in generateCode
|
|
22
|
+
};
|
|
23
|
+
this.generateCode = async (codeConfig) => {
|
|
24
|
+
try {
|
|
25
|
+
logger.info("Generating JavaScript code using TypeScript compilation...");
|
|
26
|
+
// Step 1: Generate TypeScript code in a temporary directory
|
|
27
|
+
logger.debug("Step 1: Generating TypeScript code in temp directory");
|
|
28
|
+
await fs.ensureDir(this.tempTsPath);
|
|
29
|
+
const tsGenerator = new TypescriptGenerator(this.validationConfig, this.errorCodes, this.tempTsPath);
|
|
30
|
+
await tsGenerator.generateCode(codeConfig);
|
|
31
|
+
// Step 2: Create tsconfig.json for compilation
|
|
32
|
+
logger.debug("Step 2: Creating TypeScript configuration");
|
|
33
|
+
await this.createTsConfig();
|
|
34
|
+
// Step 3: Install TypeScript if not available and compile
|
|
35
|
+
logger.debug("Step 3: Compiling TypeScript to JavaScript");
|
|
36
|
+
await this.compileToJavaScript();
|
|
37
|
+
// Step 4: Copy and modify generated files
|
|
38
|
+
logger.debug("Step 4: Processing generated files");
|
|
39
|
+
await this.processGeneratedFiles();
|
|
40
|
+
// Step 5: Generate package.json
|
|
41
|
+
logger.debug("Step 5: Creating package.json");
|
|
42
|
+
await this.generatePackageJson(codeConfig.codeName);
|
|
43
|
+
// Step 6: Generate documentation
|
|
44
|
+
logger.debug("Step 6: Generating documentation");
|
|
45
|
+
await new MarkdownDocGenerator(this.validationConfig, this.errorCodes, this.rootPath).generateCode();
|
|
46
|
+
// Step 7: Clean up temporary files
|
|
47
|
+
logger.debug("Step 7: Cleaning up temporary files");
|
|
48
|
+
await this.cleanup();
|
|
49
|
+
logger.info("JavaScript code generation completed successfully");
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.error("Error during JavaScript code generation:", error);
|
|
53
|
+
await this.cleanup(); // Ensure cleanup even on error
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
this.tempTsPath = path.resolve(rootPath, "../temp-ts-generation");
|
|
58
|
+
}
|
|
59
|
+
async createTsConfig() {
|
|
60
|
+
const tsConfig = {
|
|
61
|
+
compilerOptions: {
|
|
62
|
+
target: "ES2020",
|
|
63
|
+
module: "ESNext",
|
|
64
|
+
moduleResolution: "node",
|
|
65
|
+
declaration: true,
|
|
66
|
+
outDir: "./js-output",
|
|
67
|
+
rootDir: ".",
|
|
68
|
+
strict: false,
|
|
69
|
+
esModuleInterop: true,
|
|
70
|
+
allowSyntheticDefaultImports: true,
|
|
71
|
+
skipLibCheck: true,
|
|
72
|
+
forceConsistentCasingInFileNames: true,
|
|
73
|
+
allowJs: true,
|
|
74
|
+
noEmitOnError: false,
|
|
75
|
+
removeComments: false,
|
|
76
|
+
},
|
|
77
|
+
include: ["./**/*.ts"],
|
|
78
|
+
exclude: ["node_modules", "./js-output"],
|
|
79
|
+
};
|
|
80
|
+
await writeAndFormatCode(this.tempTsPath, "tsconfig.json", JSON.stringify(tsConfig, null, 2), "json");
|
|
81
|
+
}
|
|
82
|
+
async compileToJavaScript() {
|
|
83
|
+
logger.debug("Compiling TypeScript to JavaScript...");
|
|
84
|
+
try {
|
|
85
|
+
// Try using npx tsc first
|
|
86
|
+
await this.runCommand("npx tsc", this.tempTsPath);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// If npx tsc fails, try to install TypeScript locally and retry
|
|
90
|
+
logger.info("TypeScript compilation failed, installing TypeScript...");
|
|
91
|
+
await this.runCommand("npm init -y", this.tempTsPath);
|
|
92
|
+
await this.runCommand("npm install --save-dev typescript", this.tempTsPath);
|
|
93
|
+
await this.runCommand("npx tsc", this.tempTsPath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async processGeneratedFiles() {
|
|
97
|
+
const jsOutputPath = path.resolve(this.tempTsPath, "js-output");
|
|
98
|
+
// Ensure the target directory exists
|
|
99
|
+
await fs.ensureDir(this.rootPath);
|
|
100
|
+
// Copy all .js files and modify imports
|
|
101
|
+
await this.copyAndProcessJsFiles(jsOutputPath, this.rootPath);
|
|
102
|
+
// Copy all .d.ts files
|
|
103
|
+
await this.copyTypeDefinitions(jsOutputPath, this.rootPath);
|
|
104
|
+
}
|
|
105
|
+
async copyAndProcessJsFiles(sourceDir, targetDir) {
|
|
106
|
+
if (!(await fs.pathExists(sourceDir))) {
|
|
107
|
+
throw new Error(`Source directory ${sourceDir} does not exist`);
|
|
108
|
+
}
|
|
109
|
+
const files = await this.getAllFiles(sourceDir, ".js");
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
const relativePath = path.relative(sourceDir, file);
|
|
112
|
+
const targetFile = path.resolve(targetDir, relativePath);
|
|
113
|
+
let content = await fs.readFile(file, "utf-8");
|
|
114
|
+
// Convert TypeScript-style imports to work better with JavaScript
|
|
115
|
+
content = this.processJavaScriptImports(content);
|
|
116
|
+
await fs.ensureDir(path.dirname(targetFile));
|
|
117
|
+
await fs.writeFile(targetFile, content);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async copyTypeDefinitions(sourceDir, targetDir) {
|
|
121
|
+
if (!(await fs.pathExists(sourceDir))) {
|
|
122
|
+
logger.warn(`Type definitions source directory ${sourceDir} does not exist`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const files = await this.getAllFiles(sourceDir, ".d.ts");
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const relativePath = path.relative(sourceDir, file);
|
|
128
|
+
const targetFile = path.resolve(targetDir, relativePath);
|
|
129
|
+
let content = await fs.readFile(file, "utf-8");
|
|
130
|
+
// Process .d.ts imports
|
|
131
|
+
content = this.processTypeDefinitionImports(content);
|
|
132
|
+
await fs.ensureDir(path.dirname(targetFile));
|
|
133
|
+
await fs.writeFile(targetFile, content);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
processJavaScriptImports(content) {
|
|
137
|
+
// For ES modules, we need to keep the .js extensions in imports
|
|
138
|
+
// But fix the .js.js issue if it occurs
|
|
139
|
+
return content
|
|
140
|
+
.replace(/\.js\.js/g, ".js")
|
|
141
|
+
.replace(/from\s+["']([^"']+)\.ts["']/g, "from '$1.js'")
|
|
142
|
+
.replace(/import\s+["']([^"']+)\.ts["']/g, "import '$1.js'");
|
|
143
|
+
}
|
|
144
|
+
processTypeDefinitionImports(content) {
|
|
145
|
+
// Fix TypeScript imports in .d.ts files
|
|
146
|
+
return content
|
|
147
|
+
.replace(/from\s+["']([^"']+)\.ts["']/g, "from '$1'")
|
|
148
|
+
.replace(/import\s+["']([^"']+)\.ts["']/g, "import '$1'")
|
|
149
|
+
.replace(/\.js\.js/g, ".js");
|
|
150
|
+
}
|
|
151
|
+
async getAllFiles(dir, extension) {
|
|
152
|
+
const files = [];
|
|
153
|
+
if (!(await fs.pathExists(dir))) {
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
const fullPath = path.resolve(dir, item.name);
|
|
159
|
+
if (item.isDirectory()) {
|
|
160
|
+
const subFiles = await this.getAllFiles(fullPath, extension);
|
|
161
|
+
files.push(...subFiles);
|
|
162
|
+
}
|
|
163
|
+
else if (item.isFile() && item.name.endsWith(extension)) {
|
|
164
|
+
files.push(fullPath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return files;
|
|
168
|
+
}
|
|
169
|
+
async generatePackageJson(codeName) {
|
|
170
|
+
const packageJson = {
|
|
171
|
+
name: codeName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
172
|
+
version: "1.0.0",
|
|
173
|
+
description: `Generated validation library: ${codeName}`,
|
|
174
|
+
type: "module",
|
|
175
|
+
main: "index.js",
|
|
176
|
+
types: "index.d.ts",
|
|
177
|
+
scripts: {
|
|
178
|
+
test: 'echo "Error: no test specified" && exit 1',
|
|
179
|
+
},
|
|
180
|
+
dependencies: {
|
|
181
|
+
// Add any runtime dependencies your generated code needs
|
|
182
|
+
jsonpath: "^1.1.1",
|
|
183
|
+
},
|
|
184
|
+
keywords: ["validation", "api", "testing", "javascript", "ondc"],
|
|
185
|
+
author: "Generated by ONDC Code Generator",
|
|
186
|
+
license: "MIT",
|
|
187
|
+
};
|
|
188
|
+
await writeAndFormatCode(this.rootPath, "package.json", JSON.stringify(packageJson, null, 2), "json");
|
|
189
|
+
}
|
|
190
|
+
async runCommand(command, cwd) {
|
|
191
|
+
try {
|
|
192
|
+
const { stdout, stderr } = await execAsync(command, { cwd });
|
|
193
|
+
if (stderr && !stderr.includes("npm WARN")) {
|
|
194
|
+
logger.warn(`Command stderr: ${command}`, stderr);
|
|
195
|
+
}
|
|
196
|
+
return stdout;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
logger.error(`Command failed: ${command}`, error.message);
|
|
200
|
+
throw new Error(`Command failed: ${command}
|
|
201
|
+
${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async cleanup() {
|
|
205
|
+
try {
|
|
206
|
+
if (await fs.pathExists(this.tempTsPath)) {
|
|
207
|
+
await fs.remove(this.tempTsPath);
|
|
208
|
+
logger.debug("Cleaned up temporary TypeScript files");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
logger.warn("Failed to clean up temporary files:", error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/dist/index.js
CHANGED
|
@@ -15,22 +15,32 @@ export { ConfigCompiler };
|
|
|
15
15
|
// );
|
|
16
16
|
// const buildYaml = readFileSync(buildPath, "utf-8");
|
|
17
17
|
// const valConfig = JSON.parse(readFileSync(valConfigPath, "utf-8"));
|
|
18
|
-
// await compiler.initialize(buildYaml);
|
|
19
|
-
// await compiler.generateCode(
|
|
18
|
+
// // await compiler.initialize(buildYaml);
|
|
19
|
+
// // await compiler.generateCode(
|
|
20
|
+
// // valConfig,
|
|
21
|
+
// // "L1_validations",
|
|
22
|
+
// // false,
|
|
23
|
+
// // "./alpha/python/"
|
|
24
|
+
// // );
|
|
25
|
+
// // const compilerTy = new ConfigCompiler(SupportedLanguages.Typescript);
|
|
26
|
+
// // await compilerTy.initialize(buildYaml);
|
|
27
|
+
// // await compilerTy.generateCode(
|
|
28
|
+
// // valConfig,
|
|
29
|
+
// // "L1_validations",
|
|
30
|
+
// // false,
|
|
31
|
+
// // "./alpha/typescript/"
|
|
32
|
+
// // );
|
|
33
|
+
// // JavaScript generation example
|
|
34
|
+
// const compilerJs = new ConfigCompiler(SupportedLanguages.Javascript);
|
|
35
|
+
// await compilerJs.initialize(buildYaml);
|
|
36
|
+
// await compilerJs.generateCode(
|
|
20
37
|
// valConfig,
|
|
21
38
|
// "L1_validations",
|
|
22
39
|
// false,
|
|
23
|
-
// "./alpha/
|
|
24
|
-
// );
|
|
25
|
-
// const compilerTy = new ConfigCompiler(SupportedLanguages.Typescript);
|
|
26
|
-
// await compilerTy.initialize(buildYaml);
|
|
27
|
-
// await compilerTy.generateCode(
|
|
28
|
-
// valConfig,
|
|
29
|
-
// "L1_validations",
|
|
30
|
-
// false,
|
|
31
|
-
// "./alpha/typescript/"
|
|
40
|
+
// "./alpha/javascriptNative/"
|
|
32
41
|
// );
|
|
33
42
|
// };
|
|
34
43
|
// (async () => {
|
|
35
44
|
// await main();
|
|
45
|
+
// console.log("========== Code generation completed. ==========");
|
|
36
46
|
// })();
|
|
@@ -1,2 +1,24 @@
|
|
|
1
1
|
import { AstNode } from "../ast.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Public API: Compile an AST to *readable* Markdown with:
|
|
4
|
+
* - No pointer numbering (no 1.1.1)
|
|
5
|
+
* - Natural language groups ("All of the following…" / "Any of these…")
|
|
6
|
+
* - Leaves rendered as short sentences
|
|
7
|
+
*
|
|
8
|
+
* @param ast
|
|
9
|
+
* @param topLevel Whether this is the very first group under the title
|
|
10
|
+
* @param depth Controls bullet/indent
|
|
11
|
+
* @param forNot Carries NOT (!) context downward
|
|
12
|
+
*/
|
|
13
|
+
export declare function CompileToMarkdown(ast: AstNode, topLevel?: boolean, depth?: number, forNot?: boolean): string;
|
|
14
|
+
/**
|
|
15
|
+
* Skip-specific version: Compile an AST to *readable* Markdown for skip conditions with:
|
|
16
|
+
* - More natural language ("{{lhs}} is not in the payload" instead of "{{lhs}} must not be present")
|
|
17
|
+
* - Simpler phrasing suitable for skip conditions
|
|
18
|
+
*
|
|
19
|
+
* @param ast
|
|
20
|
+
* @param topLevel Whether this is the very first group under the title
|
|
21
|
+
* @param depth Controls bullet/indent
|
|
22
|
+
* @param forNot Carries NOT (!) context downward
|
|
23
|
+
*/
|
|
24
|
+
export declare function CompileToMarkdownForSkip(ast: AstNode, topLevel?: boolean, depth?: number, forNot?: boolean): string;
|
|
@@ -1,66 +1,133 @@
|
|
|
1
1
|
import { AllIn, AnyIn, AreUnique, ArePresent, EqualTo, FollowRegex, GreaterThan, LessThan, NoneIn, } from "../tokens.js";
|
|
2
|
+
/**
|
|
3
|
+
* Leaf (unary/binary) message templates, rewritten to be short & human-friendly.
|
|
4
|
+
* NOTE: We keep triple-mustaches so Mustache can expand safely.
|
|
5
|
+
*/
|
|
2
6
|
const uniaryMessages = {
|
|
3
|
-
[AreUnique.LABEL ?? "are unique"]: (variable, forNot) => `
|
|
4
|
-
[ArePresent.LABEL ?? "are present"]: (variable, forNot) => `{{{${variable}}}}
|
|
7
|
+
[AreUnique.LABEL ?? "are unique"]: (variable, forNot) => `All values of {{{${variable}}}} ${forNot ? "must **not** be unique" : "are unique"}`,
|
|
8
|
+
[ArePresent.LABEL ?? "are present"]: (variable, forNot) => `{{{${variable}}}} ${forNot ? "must **not** be present" : "must be present"} in the payload`,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Skip-specific message templates for more natural language in skip conditions
|
|
12
|
+
*/
|
|
13
|
+
const skipUniaryMessages = {
|
|
14
|
+
[AreUnique.LABEL ?? "are unique"]: (variable, forNot) => `{{{${variable}}}} values ${forNot ? "are not unique" : "are unique"}`,
|
|
15
|
+
[ArePresent.LABEL ?? "are present"]: (variable, forNot) => `{{{${variable}}}} ${forNot ? "is not in the payload" : "is in the payload"}`,
|
|
5
16
|
};
|
|
6
17
|
const binaryMessages = {
|
|
7
|
-
[AllIn.LABEL ?? "all in"]: (lhs, rhs, forNot) =>
|
|
8
|
-
[AnyIn.LABEL ?? "any in"]: (lhs, rhs, forNot) =>
|
|
9
|
-
[FollowRegex.LABEL ?? "follow regex"]: (lhs, rhs, forNot) =>
|
|
10
|
-
[NoneIn.LABEL ?? "none in"]: (lhs, rhs, forNot) =>
|
|
11
|
-
[EqualTo.LABEL ?? "equal to"]: (lhs, rhs, forNot) => `{{{${lhs}}}}
|
|
12
|
-
[GreaterThan.LABEL ?? "greater than"]: (lhs, rhs, forNot) => `{{{${lhs}}}}
|
|
13
|
-
[LessThan.LABEL ?? "less than"]: (lhs, rhs, forNot) => `{{{${lhs}}}}
|
|
18
|
+
[AllIn.LABEL ?? "all in"]: (lhs, rhs, forNot) => `${forNot ? "Not all" : "All"} elements of {{{${lhs}}}} ${forNot ? "may be" : "must be"} in {{{${rhs}}}}`,
|
|
19
|
+
[AnyIn.LABEL ?? "any in"]: (lhs, rhs, forNot) => `${forNot ? "None of" : "At least one of"} {{{${lhs}}}} ${forNot ? "may be" : "must be"} in {{{${rhs}}}}`,
|
|
20
|
+
[FollowRegex.LABEL ?? "follow regex"]: (lhs, rhs, forNot) => `${forNot ? "Some elements of" : "All elements of"} {{{${lhs}}}} ${forNot ? "may fail to" : "must"} follow every regex in {{{${rhs}}}}`,
|
|
21
|
+
[NoneIn.LABEL ?? "none in"]: (lhs, rhs, forNot) => `${forNot ? "Some elements of" : "No element of"} {{{${lhs}}}} ${forNot ? "may be in" : "must be in"} {{{${rhs}}}}`,
|
|
22
|
+
[EqualTo.LABEL ?? "equal to"]: (lhs, rhs, forNot) => `{{{${lhs}}}} ${forNot ? "must **not** equal" : "must equal"} {{{${rhs}}}}`,
|
|
23
|
+
[GreaterThan.LABEL ?? "greater than"]: (lhs, rhs, forNot) => `{{{${lhs}}}} ${forNot ? "must **not** be greater than" : "must be greater than"} {{{${rhs}}}}`,
|
|
24
|
+
[LessThan.LABEL ?? "less than"]: (lhs, rhs, forNot) => `{{{${lhs}}}} ${forNot ? "must **not** be less than" : "must be less than"} {{{${rhs}}}}`,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Skip-specific binary message templates for more natural language in skip conditions
|
|
28
|
+
*/
|
|
29
|
+
const skipBinaryMessages = {
|
|
30
|
+
[AllIn.LABEL ?? "all in"]: (lhs, rhs, forNot) => `${forNot ? "not all" : "all"} elements of {{{${lhs}}}} are in {{{${rhs}}}}`,
|
|
31
|
+
[AnyIn.LABEL ?? "any in"]: (lhs, rhs, forNot) => `${forNot ? "none of" : "any of"} {{{${lhs}}}} are in {{{${rhs}}}}`,
|
|
32
|
+
[FollowRegex.LABEL ?? "follow regex"]: (lhs, rhs, forNot) => `{{{${lhs}}}} ${forNot ? "doesn't follow" : "follows"} regex {{{${rhs}}}}`,
|
|
33
|
+
[NoneIn.LABEL ?? "none in"]: (lhs, rhs, forNot) => `${forNot ? "some" : "none"} of {{{${lhs}}}} are in {{{${rhs}}}}`,
|
|
34
|
+
[EqualTo.LABEL ?? "equal to"]: (lhs, rhs, forNot) => `{{{${lhs}}}} ${forNot ? "is not equal to" : "equals"} {{{${rhs}}}}`,
|
|
35
|
+
[GreaterThan.LABEL ?? "greater than"]: (lhs, rhs, forNot) => `{{{${lhs}}}} ${forNot ? "is not greater than" : "is greater than"} {{{${rhs}}}}`,
|
|
36
|
+
[LessThan.LABEL ?? "less than"]: (lhs, rhs, forNot) => `{{{${lhs}}}} ${forNot ? "is not less than" : "is less than"} {{{${rhs}}}}`,
|
|
14
37
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Public API: Compile an AST to *readable* Markdown with:
|
|
40
|
+
* - No pointer numbering (no 1.1.1)
|
|
41
|
+
* - Natural language groups ("All of the following…" / "Any of these…")
|
|
42
|
+
* - Leaves rendered as short sentences
|
|
43
|
+
*
|
|
44
|
+
* @param ast
|
|
45
|
+
* @param topLevel Whether this is the very first group under the title
|
|
46
|
+
* @param depth Controls bullet/indent
|
|
47
|
+
* @param forNot Carries NOT (!) context downward
|
|
48
|
+
*/
|
|
49
|
+
export function CompileToMarkdown(ast, topLevel = true, depth = 0, forNot = false) {
|
|
50
|
+
return compileToMarkdownInternal(ast, topLevel, depth, forNot, false);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Skip-specific version: Compile an AST to *readable* Markdown for skip conditions with:
|
|
54
|
+
* - More natural language ("{{lhs}} is not in the payload" instead of "{{lhs}} must not be present")
|
|
55
|
+
* - Simpler phrasing suitable for skip conditions
|
|
56
|
+
*
|
|
57
|
+
* @param ast
|
|
58
|
+
* @param topLevel Whether this is the very first group under the title
|
|
59
|
+
* @param depth Controls bullet/indent
|
|
60
|
+
* @param forNot Carries NOT (!) context downward
|
|
61
|
+
*/
|
|
62
|
+
export function CompileToMarkdownForSkip(ast, topLevel = true, depth = 0, forNot = false) {
|
|
63
|
+
return compileToMarkdownInternal(ast, topLevel, depth, forNot, true);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Internal implementation shared by both CompileToMarkdown and CompileToMarkdownForSkip
|
|
67
|
+
*/
|
|
68
|
+
function compileToMarkdownInternal(ast, topLevel = true, depth = 0, forNot = false, isSkip = false) {
|
|
69
|
+
const indent = " ".repeat(depth);
|
|
70
|
+
// Render a group label (AND/OR) with correct style.
|
|
71
|
+
function groupLabel(isAnd, asBullet, indentStr) {
|
|
72
|
+
const label = isAnd
|
|
73
|
+
? "**All of the following must be true:**"
|
|
74
|
+
: "**Any of these must be true:**";
|
|
75
|
+
return asBullet ? `${indentStr}- ${label}` : `${indentStr}${label}`;
|
|
76
|
+
}
|
|
77
|
+
// Ensure each line in a block is indented (used for nested groups).
|
|
78
|
+
function indentBlock(block, extraDepth = 1) {
|
|
79
|
+
const pad = " ".repeat(extraDepth);
|
|
80
|
+
return block
|
|
21
81
|
.split("\n")
|
|
22
|
-
.map((
|
|
82
|
+
.map((l) => (l.length ? pad + l : l))
|
|
23
83
|
.join("\n");
|
|
24
84
|
}
|
|
85
|
+
// RETURN: just compile its expression at same depth/topLevel
|
|
25
86
|
if (ast.type === "returnStatement") {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return `${generated}`;
|
|
87
|
+
const ret = ast;
|
|
88
|
+
return compileToMarkdownInternal(ret.expression, topLevel, depth, forNot, isSkip);
|
|
29
89
|
}
|
|
90
|
+
// AND / OR group
|
|
30
91
|
if (ast.type === "binaryOperator") {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
92
|
+
const { operator, lhs, rhs } = ast;
|
|
93
|
+
const isAnd = operator === "&&";
|
|
94
|
+
const labelLine = groupLabel(isAnd, !topLevel, indent);
|
|
95
|
+
// Render children as bullet items (no numbering)
|
|
96
|
+
const leftRendered = compileToMarkdownInternal(lhs, false, depth + 1, forNot, isSkip);
|
|
97
|
+
const rightRendered = compileToMarkdownInternal(rhs, false, depth + 1, forNot, isSkip);
|
|
98
|
+
// If top-level group: label as standalone line, then list items
|
|
99
|
+
// If nested: label as bullet, then nested bullets further indented
|
|
100
|
+
if (topLevel) {
|
|
101
|
+
return [labelLine, leftRendered, rightRendered].join("\n");
|
|
38
102
|
}
|
|
39
|
-
|
|
40
|
-
return
|
|
103
|
+
else {
|
|
104
|
+
return [labelLine, leftRendered, rightRendered].join("\n");
|
|
41
105
|
}
|
|
42
106
|
}
|
|
107
|
+
// NOT
|
|
43
108
|
if (ast.type === "notOperator") {
|
|
44
109
|
const not = ast;
|
|
45
|
-
return
|
|
110
|
+
return compileToMarkdownInternal(not.expression, topLevel, depth, !forNot, isSkip);
|
|
46
111
|
}
|
|
112
|
+
// LEAVES (unary / binary custom functions)
|
|
47
113
|
if (ast.type === "customUniaryFunction") {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
114
|
+
const custom = ast;
|
|
115
|
+
const fn = custom.customFunction;
|
|
116
|
+
const lhs = custom.expression;
|
|
117
|
+
const msgFn = isSkip
|
|
118
|
+
? skipUniaryMessages[fn]
|
|
119
|
+
: uniaryMessages[fn];
|
|
120
|
+
return `${indent}- ${msgFn(lhs.name, forNot)}`;
|
|
53
121
|
}
|
|
54
122
|
if (ast.type === "customBinaryFunction") {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
123
|
+
const custom = ast;
|
|
124
|
+
const fn = custom.customFunction;
|
|
125
|
+
const lhs = custom.lhs;
|
|
126
|
+
const rhs = custom.rhs;
|
|
127
|
+
const msgFn = isSkip
|
|
128
|
+
? skipBinaryMessages[fn]
|
|
129
|
+
: binaryMessages[fn];
|
|
130
|
+
return `${indent}- ${msgFn(lhs.name, rhs.name, forNot)}`;
|
|
61
131
|
}
|
|
62
132
|
throw new Error("Invalid AST node:" + JSON.stringify(ast));
|
|
63
133
|
}
|
|
64
|
-
function getNextPointer(currentPointer, nextIndex) {
|
|
65
|
-
return `${currentPointer}.${nextIndex}`;
|
|
66
|
-
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -31,6 +31,7 @@ const logLevel = process.env.NODE_ENV === "production" ? "info" : "debug";
|
|
|
31
31
|
// Configure Winston logger
|
|
32
32
|
const logger = winston.createLogger({
|
|
33
33
|
level: logLevel,
|
|
34
|
+
silent: true,
|
|
34
35
|
format: combine(timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), errors({ stack: true }), // Include stack trace in error messages
|
|
35
36
|
logFormat),
|
|
36
37
|
transports: [
|