ondc-code-generator 0.4.6 → 0.5.0
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/constants/syntax.d.ts +1 -1
- package/dist/constants/syntax.js +98 -2
- package/dist/generator/config-compiler.js +7 -3
- package/dist/generator/generators/python/py-ast.d.ts +1 -0
- package/dist/generator/generators/python/py-ast.js +72 -0
- package/dist/generator/generators/python/py-generator.d.ts +19 -0
- package/dist/generator/generators/python/py-generator.js +247 -1
- package/dist/generator/generators/python/templates/api-test.mustache +29 -0
- package/dist/generator/generators/python/templates/api-tests-init.mustache +12 -0
- package/dist/generator/generators/python/templates/json-normalizer.mustache +86 -0
- package/dist/generator/generators/python/templates/json-path-utils.mustache +33 -0
- package/dist/generator/generators/python/templates/master-doc.mustache +40 -0
- package/dist/generator/generators/python/templates/requirements.mustache +2 -0
- package/dist/generator/generators/python/templates/test-config.mustache +62 -0
- package/dist/generator/generators/python/templates/test-object.mustache +29 -0
- package/dist/generator/generators/python/templates/validation-code.mustache +35 -0
- package/dist/generator/generators/python/templates/validation-utils.mustache +93 -0
- package/dist/generator/generators/typescript/templates/api-test.mustache +29 -1
- package/dist/generator/generators/typescript/templates/index.mustache +33 -0
- package/dist/generator/generators/typescript/templates/test-config.mustache +45 -5
- package/dist/generator/generators/typescript/templates/test-object.mustache +11 -1
- package/dist/generator/generators/typescript/templates/validation-code.mustache +12 -12
- package/dist/generator/generators/typescript/ts-generator.js +25 -13
- package/dist/generator/validators/tests-config/sub-validations.js +3 -3
- package/dist/index.js +15 -2
- package/dist/types/compiler-types.d.ts +2 -1
- package/dist/types/compiler-types.js +1 -0
- package/dist/utils/fs-utils.js +43 -0
- package/dist/utils/general-utils/string-utils.js +2 -2
- package/package.json +1 -1
package/dist/constants/syntax.js
CHANGED
|
@@ -24,8 +24,9 @@ export const ConfigKeyWords = [
|
|
|
24
24
|
ConfigSyntax.Tests,
|
|
25
25
|
ConfigSyntax.SessionData,
|
|
26
26
|
];
|
|
27
|
-
// JavaScript/
|
|
28
|
-
export const
|
|
27
|
+
// List of reserved keywords in Supported Languages (JavaScript/TypeScript and Python)
|
|
28
|
+
export const ReservedKeywords = new Set([
|
|
29
|
+
// JavaScript/TypeScript keywords
|
|
29
30
|
"break",
|
|
30
31
|
"case",
|
|
31
32
|
"catch",
|
|
@@ -75,4 +76,99 @@ export const nodeReservedKeywords = new Set([
|
|
|
75
76
|
"protected",
|
|
76
77
|
"public",
|
|
77
78
|
"static",
|
|
79
|
+
// Python keywords
|
|
80
|
+
"and",
|
|
81
|
+
"as",
|
|
82
|
+
"assert",
|
|
83
|
+
"def",
|
|
84
|
+
"del",
|
|
85
|
+
"elif",
|
|
86
|
+
"except",
|
|
87
|
+
"exec",
|
|
88
|
+
"from",
|
|
89
|
+
"global",
|
|
90
|
+
"is",
|
|
91
|
+
"lambda",
|
|
92
|
+
"not",
|
|
93
|
+
"or",
|
|
94
|
+
"pass",
|
|
95
|
+
"print",
|
|
96
|
+
"raise",
|
|
97
|
+
"with",
|
|
98
|
+
"yield",
|
|
99
|
+
// Python built-in functions and constants (commonly reserved)
|
|
100
|
+
"True",
|
|
101
|
+
"False",
|
|
102
|
+
"None",
|
|
103
|
+
"abs",
|
|
104
|
+
"all",
|
|
105
|
+
"any",
|
|
106
|
+
"bin",
|
|
107
|
+
"bool",
|
|
108
|
+
"bytearray",
|
|
109
|
+
"bytes",
|
|
110
|
+
"callable",
|
|
111
|
+
"chr",
|
|
112
|
+
"classmethod",
|
|
113
|
+
"compile",
|
|
114
|
+
"complex",
|
|
115
|
+
"delattr",
|
|
116
|
+
"dict",
|
|
117
|
+
"dir",
|
|
118
|
+
"divmod",
|
|
119
|
+
"enumerate",
|
|
120
|
+
"eval",
|
|
121
|
+
"filter",
|
|
122
|
+
"float",
|
|
123
|
+
"format",
|
|
124
|
+
"frozenset",
|
|
125
|
+
"getattr",
|
|
126
|
+
"globals",
|
|
127
|
+
"hasattr",
|
|
128
|
+
"hash",
|
|
129
|
+
"help",
|
|
130
|
+
"hex",
|
|
131
|
+
"id",
|
|
132
|
+
"input",
|
|
133
|
+
"int",
|
|
134
|
+
"isinstance",
|
|
135
|
+
"issubclass",
|
|
136
|
+
"iter",
|
|
137
|
+
"len",
|
|
138
|
+
"list",
|
|
139
|
+
"locals",
|
|
140
|
+
"map",
|
|
141
|
+
"max",
|
|
142
|
+
"memoryview",
|
|
143
|
+
"min",
|
|
144
|
+
"next",
|
|
145
|
+
"object",
|
|
146
|
+
"oct",
|
|
147
|
+
"open",
|
|
148
|
+
"ord",
|
|
149
|
+
"pow",
|
|
150
|
+
"property",
|
|
151
|
+
"range",
|
|
152
|
+
"repr",
|
|
153
|
+
"reversed",
|
|
154
|
+
"round",
|
|
155
|
+
"set",
|
|
156
|
+
"setattr",
|
|
157
|
+
"slice",
|
|
158
|
+
"sorted",
|
|
159
|
+
"staticmethod",
|
|
160
|
+
"str",
|
|
161
|
+
"sum",
|
|
162
|
+
"tuple",
|
|
163
|
+
"type",
|
|
164
|
+
"vars",
|
|
165
|
+
"zip",
|
|
166
|
+
// Python 3.x additional keywords
|
|
167
|
+
"nonlocal",
|
|
168
|
+
"async",
|
|
169
|
+
"await",
|
|
170
|
+
// Soft keywords in Python (context-dependent)
|
|
171
|
+
"match",
|
|
172
|
+
"case",
|
|
173
|
+
"_",
|
|
78
174
|
]);
|
|
@@ -9,6 +9,7 @@ import Mustache from "mustache";
|
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { duplicateVariablesInChildren } from "../utils/config-utils/duplicateVariables.js";
|
|
12
|
+
import { PythonGenerator } from "./generators/python/py-generator.js";
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
14
15
|
const defaultConfig = {
|
|
@@ -70,9 +71,12 @@ export class ConfigCompiler {
|
|
|
70
71
|
const targetPath = `${outputPath}generated/${codeName}`;
|
|
71
72
|
switch (this.language) {
|
|
72
73
|
case SupportedLanguages.Typescript:
|
|
73
|
-
await new TypescriptGenerator(valConfig, this.errorDefinitions ?? [],
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
await new TypescriptGenerator(valConfig, this.errorDefinitions ?? [], targetPath).generateCode({
|
|
75
|
+
codeName: codeName,
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
case SupportedLanguages.Python:
|
|
79
|
+
await new PythonGenerator(valConfig, this.errorDefinitions ?? [], targetPath).generateCode({
|
|
76
80
|
codeName: codeName,
|
|
77
81
|
});
|
|
78
82
|
break;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const compileInputToPy: (input: string) => string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buildAstFromInput } from "../../../services/return-complier/combined.js";
|
|
2
|
+
import { AllIn, AnyIn, ArePresent, AreUnique, EqualTo, FollowRegex, GreaterThan, LessThan, NoneIn, } from "../../../services/return-complier/tokens.js";
|
|
3
|
+
function getPyOperator(op) {
|
|
4
|
+
switch (op) {
|
|
5
|
+
case "&&":
|
|
6
|
+
return "and";
|
|
7
|
+
case "||":
|
|
8
|
+
return "or";
|
|
9
|
+
case "==":
|
|
10
|
+
return "==";
|
|
11
|
+
case "!=":
|
|
12
|
+
return "!=";
|
|
13
|
+
case ">":
|
|
14
|
+
return ">";
|
|
15
|
+
case "<":
|
|
16
|
+
return "<";
|
|
17
|
+
case ">=":
|
|
18
|
+
return ">=";
|
|
19
|
+
case "<=":
|
|
20
|
+
return "<=";
|
|
21
|
+
default:
|
|
22
|
+
throw new Error(`Unsupported operator: ${op}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const uniaryFunction = {
|
|
26
|
+
[AreUnique.LABEL ?? "are unique"]: "are_unique",
|
|
27
|
+
[ArePresent.LABEL ?? "are present"]: "are_present",
|
|
28
|
+
};
|
|
29
|
+
const binaryFunction = {
|
|
30
|
+
[AllIn.LABEL ?? "all in"]: "all_in",
|
|
31
|
+
[AnyIn.LABEL ?? "any in"]: "any_in",
|
|
32
|
+
[FollowRegex.LABEL ?? "follow regex"]: "follow_regex",
|
|
33
|
+
[NoneIn.LABEL ?? "none in"]: "none_in",
|
|
34
|
+
[EqualTo.LABEL ?? "equal to"]: "equal_to",
|
|
35
|
+
[GreaterThan.LABEL ?? "greater than"]: "greater_than",
|
|
36
|
+
[LessThan.LABEL ?? "less than"]: "less_than",
|
|
37
|
+
};
|
|
38
|
+
function compileToPy(node) {
|
|
39
|
+
if (node.type === "returnStatement") {
|
|
40
|
+
const returnNode = node;
|
|
41
|
+
return compileToPy(returnNode.expression);
|
|
42
|
+
}
|
|
43
|
+
if (node.type === "binaryOperator") {
|
|
44
|
+
const binaryNode = node;
|
|
45
|
+
const lhs = compileToPy(binaryNode.lhs);
|
|
46
|
+
const rhs = compileToPy(binaryNode.rhs);
|
|
47
|
+
return `(${lhs}) ${getPyOperator(binaryNode.operator)} (${rhs})`;
|
|
48
|
+
}
|
|
49
|
+
if (node.type === "notOperator") {
|
|
50
|
+
const notNode = node;
|
|
51
|
+
const expression = compileToPy(notNode.expression);
|
|
52
|
+
return `not (${expression})`;
|
|
53
|
+
}
|
|
54
|
+
if (node.type === "customUniaryFunction") {
|
|
55
|
+
const unary = node;
|
|
56
|
+
const func = uniaryFunction[unary.customFunction];
|
|
57
|
+
const varName = unary.expression.name;
|
|
58
|
+
return `validation_utils["${func}"](${varName})`;
|
|
59
|
+
}
|
|
60
|
+
if (node.type === "customBinaryFunction") {
|
|
61
|
+
const binary = node;
|
|
62
|
+
const func = binaryFunction[binary.customFunction];
|
|
63
|
+
const lhs = binary.lhs.name;
|
|
64
|
+
const rhs = binary.rhs.name;
|
|
65
|
+
return `validation_utils["${func}"](${lhs}, ${rhs})`;
|
|
66
|
+
}
|
|
67
|
+
throw new Error("Unknown node type");
|
|
68
|
+
}
|
|
69
|
+
export const compileInputToPy = (input) => {
|
|
70
|
+
const ast = buildAstFromInput(input);
|
|
71
|
+
return compileToPy(ast);
|
|
72
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TestObject } from "../../../types/config-types.js";
|
|
2
|
+
import { CodeGenerator, CodeGeneratorProps } from "../classes/abstract-generator.js";
|
|
3
|
+
export declare class PythonGenerator extends CodeGenerator {
|
|
4
|
+
generateSessionDataCode: () => Promise<never>;
|
|
5
|
+
generateValidationCode: () => Promise<void>;
|
|
6
|
+
generateCode: (codeConfig: CodeGeneratorProps) => Promise<void>;
|
|
7
|
+
generateTestFunction: (testObject: TestObject) => Promise<{
|
|
8
|
+
funcName: string;
|
|
9
|
+
code: string;
|
|
10
|
+
}>;
|
|
11
|
+
private CreateErrorMarkdown;
|
|
12
|
+
private createVariablesCode;
|
|
13
|
+
private convertArrayToStringPython;
|
|
14
|
+
private indentCode;
|
|
15
|
+
private createValidationLogicCode;
|
|
16
|
+
private generateErrorFile;
|
|
17
|
+
private getExternalKeys;
|
|
18
|
+
private generateIndexFile;
|
|
19
|
+
}
|
|
@@ -1 +1,247 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { ConfigSyntax, TestObjectSyntax } from "../../../constants/syntax.js";
|
|
5
|
+
import Mustache from "mustache";
|
|
6
|
+
import { markdownMessageGenerator } from "../documentation/markdown-message-generator.js";
|
|
7
|
+
import { getVariablesFromTest as extractVariablesFromText } from "../../../utils/general-utils/test-object-utils.js";
|
|
8
|
+
import { compileInputToPy } from "./py-ast.js";
|
|
9
|
+
import { CodeGenerator, } from "../classes/abstract-generator.js";
|
|
10
|
+
import { writeAndFormatCode } from "../../../utils/fs-utils.js";
|
|
11
|
+
import { MarkdownDocGenerator } from "../documentation/md-generator.js";
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
export class PythonGenerator extends CodeGenerator {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(...arguments);
|
|
17
|
+
this.generateSessionDataCode = async () => {
|
|
18
|
+
throw new Error("Method not implemented.");
|
|
19
|
+
};
|
|
20
|
+
this.generateValidationCode = async () => {
|
|
21
|
+
const testConfig = this.validationConfig[ConfigSyntax.Tests];
|
|
22
|
+
for (const key in testConfig) {
|
|
23
|
+
const testObjects = testConfig[key];
|
|
24
|
+
const betaConfig = {
|
|
25
|
+
[TestObjectSyntax.Name]: key + "_validations",
|
|
26
|
+
[TestObjectSyntax.Return]: testObjects,
|
|
27
|
+
};
|
|
28
|
+
const testFunction = await this.generateTestFunction(betaConfig);
|
|
29
|
+
const apiTestTemplate = readFileSync(path.resolve(__dirname, "./templates/api-test.mustache"), "utf-8");
|
|
30
|
+
const finalCode = Mustache.render(apiTestTemplate, {
|
|
31
|
+
functionCode: testFunction.code,
|
|
32
|
+
apiName: key,
|
|
33
|
+
});
|
|
34
|
+
await writeAndFormatCode(this.rootPath, `./api_tests/${key}.py`, finalCode, "python");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
this.generateCode = async (codeConfig) => {
|
|
38
|
+
const jsonPathUtilsCode = readFileSync(path.resolve(__dirname, "./templates/json-path-utils.mustache"), "utf-8");
|
|
39
|
+
const validationUtils = readFileSync(path.resolve(__dirname, "./templates/validation-utils.mustache"), "utf-8");
|
|
40
|
+
const typesTemplate = readFileSync(path.resolve(__dirname, "./templates/test-config.mustache"), "utf-8");
|
|
41
|
+
const normalizerTemplate = readFileSync(path.resolve(__dirname, "./templates/json-normalizer.mustache"), "utf-8");
|
|
42
|
+
const apiTestsInitTemplate = readFileSync(path.resolve(__dirname, "./templates/api-tests-init.mustache"), "utf-8");
|
|
43
|
+
const requirementsTemplate = readFileSync(path.resolve(__dirname, "./templates/requirements.mustache"), "utf-8");
|
|
44
|
+
const typesCode = Mustache.render(typesTemplate, {
|
|
45
|
+
externalData: this.getExternalKeys(),
|
|
46
|
+
});
|
|
47
|
+
const requirements = Mustache.render(requirementsTemplate, {});
|
|
48
|
+
const apiNames = Object.keys(this.validationConfig[ConfigSyntax.Tests]);
|
|
49
|
+
const apiTestsInitCode = Mustache.render(apiTestsInitTemplate, {
|
|
50
|
+
apis: apiNames.map((name) => ({ name })),
|
|
51
|
+
});
|
|
52
|
+
await writeAndFormatCode(this.rootPath, "./utils/json_path_utils.py", jsonPathUtilsCode, "python");
|
|
53
|
+
await writeAndFormatCode(this.rootPath, "./utils/json_normalizer.py", normalizerTemplate, "python");
|
|
54
|
+
await writeAndFormatCode(this.rootPath, "./utils/validation_utils.py", validationUtils, "python");
|
|
55
|
+
await writeAndFormatCode(this.rootPath, "./utils/__init__.py", "# Utils package", "python");
|
|
56
|
+
await writeAndFormatCode(this.rootPath, "./types/test_config.py", typesCode, "python");
|
|
57
|
+
await writeAndFormatCode(this.rootPath, "./types/__init__.py", "# Types package", "python");
|
|
58
|
+
await writeAndFormatCode(this.rootPath, "./api_tests/__init__.py", apiTestsInitCode, "python");
|
|
59
|
+
await this.generateValidationCode();
|
|
60
|
+
await writeAndFormatCode(this.rootPath, "requirements.txt", requirements, "text");
|
|
61
|
+
await writeAndFormatCode(this.rootPath, "error.py", this.generateErrorFile(this.errorCodes), "python");
|
|
62
|
+
await writeAndFormatCode(this.rootPath, "__init__.py", this.generateIndexFile(apiNames, codeConfig.codeName), "python");
|
|
63
|
+
await new MarkdownDocGenerator(this.validationConfig, this.errorCodes, this.rootPath).generateCode();
|
|
64
|
+
};
|
|
65
|
+
this.generateTestFunction = async (testObject) => {
|
|
66
|
+
const template = readFileSync(path.resolve(__dirname, "./templates/test-object.mustache"), "utf-8");
|
|
67
|
+
const view = {
|
|
68
|
+
name: testObject[TestObjectSyntax.Name],
|
|
69
|
+
scopePath: testObject[TestObjectSyntax.Scope] ?? "$",
|
|
70
|
+
variables: this.createVariablesCode(testObject),
|
|
71
|
+
hasContinue: testObject[TestObjectSyntax.Continue] ? true : false,
|
|
72
|
+
skipCheckStatement: testObject[TestObjectSyntax.Continue]
|
|
73
|
+
? compileInputToPy(testObject[TestObjectSyntax.Continue])
|
|
74
|
+
: undefined,
|
|
75
|
+
validationCode: await this.createValidationLogicCode(testObject),
|
|
76
|
+
successCode: testObject[TestObjectSyntax.SuccessCode] ?? 200,
|
|
77
|
+
testName: testObject[TestObjectSyntax.Name],
|
|
78
|
+
errorCode: testObject[TestObjectSyntax.ErrorCode] ?? 30000,
|
|
79
|
+
TEST_OBJECT: `${JSON.stringify(testObject)}`,
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
funcName: testObject[TestObjectSyntax.Name],
|
|
83
|
+
code: Mustache.render(template, view),
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
this.createValidationLogicCode = async (testObject) => {
|
|
87
|
+
const template = readFileSync(path.resolve(__dirname, "./templates/validation-code.mustache"), "utf-8");
|
|
88
|
+
const skip = testObject[TestObjectSyntax.Continue];
|
|
89
|
+
const skipList = skip ? [skip] : undefined;
|
|
90
|
+
if (typeof testObject[TestObjectSyntax.Return] === "string") {
|
|
91
|
+
const returnStatement = compileInputToPy(testObject[TestObjectSyntax.Return]);
|
|
92
|
+
return Mustache.render(template, {
|
|
93
|
+
isNested: false,
|
|
94
|
+
returnStatement: returnStatement,
|
|
95
|
+
errorCode: testObject[TestObjectSyntax.ErrorCode] ?? 30000,
|
|
96
|
+
errorDescription: this.CreateErrorMarkdown(testObject, skipList),
|
|
97
|
+
testName: testObject[TestObjectSyntax.Name],
|
|
98
|
+
TEST_OBJECT: `${JSON.stringify(testObject)}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const subObjects = testObject[TestObjectSyntax.Return];
|
|
103
|
+
const functionCodes = [];
|
|
104
|
+
for (const subObject of subObjects) {
|
|
105
|
+
const func = await this.generateTestFunction(subObject);
|
|
106
|
+
functionCodes.push(func);
|
|
107
|
+
}
|
|
108
|
+
const names = functionCodes.map((f) => {
|
|
109
|
+
return { name: f.funcName };
|
|
110
|
+
});
|
|
111
|
+
// Properly indent nested functions
|
|
112
|
+
const indentedNestedFunctions = functionCodes
|
|
113
|
+
.map((f) => this.indentCode(f.code, 2))
|
|
114
|
+
.join("\n\n");
|
|
115
|
+
return Mustache.render(template, {
|
|
116
|
+
isNested: true,
|
|
117
|
+
nestedFunctions: indentedNestedFunctions,
|
|
118
|
+
names: names,
|
|
119
|
+
errorCode: testObject[TestObjectSyntax.ErrorCode] ?? 30000,
|
|
120
|
+
TEST_OBJECT: `${JSON.stringify(testObject)}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
CreateErrorMarkdown(testObject, skipList) {
|
|
126
|
+
return markdownMessageGenerator(testObject[TestObjectSyntax.Return], testObject, testObject[TestObjectSyntax.Name], skipList);
|
|
127
|
+
}
|
|
128
|
+
createVariablesCode(testObject) {
|
|
129
|
+
const variables = [];
|
|
130
|
+
const varNames = extractVariablesFromText(testObject);
|
|
131
|
+
for (const name of varNames) {
|
|
132
|
+
const value = testObject[name];
|
|
133
|
+
const final = typeof value === "string"
|
|
134
|
+
? `payload_utils["get_json_path"](${testObject[TestObjectSyntax.Name]}_obj, "${value}")`
|
|
135
|
+
: this.convertArrayToStringPython(value);
|
|
136
|
+
variables.push({
|
|
137
|
+
name: name,
|
|
138
|
+
value: final,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return variables;
|
|
142
|
+
}
|
|
143
|
+
convertArrayToStringPython(value) {
|
|
144
|
+
// Convert TypeScript array notation to Python list notation
|
|
145
|
+
let stringified = JSON.stringify(value);
|
|
146
|
+
stringified = stringified.replace(/\\\\\\/g, "\\");
|
|
147
|
+
// Replace null with None, true with True, false with False
|
|
148
|
+
return stringified
|
|
149
|
+
.replace(/null/g, "None")
|
|
150
|
+
.replace(/true/g, "True")
|
|
151
|
+
.replace(/false/g, "False");
|
|
152
|
+
}
|
|
153
|
+
indentCode(code, indentLevel = 1) {
|
|
154
|
+
const indent = " ".repeat(indentLevel);
|
|
155
|
+
return code
|
|
156
|
+
.split("\n")
|
|
157
|
+
.map((line) => (line.trim() ? indent + line : line))
|
|
158
|
+
.join("\n");
|
|
159
|
+
}
|
|
160
|
+
generateErrorFile(errors) {
|
|
161
|
+
const allCodes = errors.map((error) => error.code);
|
|
162
|
+
if (allCodes.length !== new Set(allCodes).size) {
|
|
163
|
+
throw new Error("Duplicate error codes found");
|
|
164
|
+
}
|
|
165
|
+
errors.push({
|
|
166
|
+
code: 20006,
|
|
167
|
+
Description: "Invalid response does not meet API contract specifications",
|
|
168
|
+
});
|
|
169
|
+
errors.push({
|
|
170
|
+
code: 30000,
|
|
171
|
+
Description: "Invalid request does not meet API contract specifications",
|
|
172
|
+
});
|
|
173
|
+
const errorsList = errors
|
|
174
|
+
.map((error) => ` {"code": ${error.code}, "message": "${error.Description}"}`)
|
|
175
|
+
.join(",\n");
|
|
176
|
+
const errorConstant = `
|
|
177
|
+
errors = [
|
|
178
|
+
${errorsList}
|
|
179
|
+
]
|
|
180
|
+
`;
|
|
181
|
+
const errorFunction = `def get_error(code):
|
|
182
|
+
for error in errors:
|
|
183
|
+
if error["code"] == code:
|
|
184
|
+
return error
|
|
185
|
+
raise Exception(f"Error code {code} not found")`;
|
|
186
|
+
return `${errorConstant}\n${errorFunction}`;
|
|
187
|
+
}
|
|
188
|
+
getExternalKeys() {
|
|
189
|
+
const apis = Object.keys(this.validationConfig[ConfigSyntax.SessionData]);
|
|
190
|
+
const result = [];
|
|
191
|
+
for (const api of apis) {
|
|
192
|
+
const keys = Object.keys(this.validationConfig[ConfigSyntax.SessionData][api]);
|
|
193
|
+
for (const key of keys) {
|
|
194
|
+
result.push({ name: key });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
generateIndexFile(apis, functionName = "L1Validations") {
|
|
200
|
+
// Clean function name for Python
|
|
201
|
+
functionName = functionName.replace(/[^a-zA-Z0-9_]/g, "");
|
|
202
|
+
let importsCode = apis
|
|
203
|
+
.map((api) => `from .api_tests import ${api}`)
|
|
204
|
+
.join("\n");
|
|
205
|
+
importsCode += `\nfrom .types.test_config import ValidationConfig\n`;
|
|
206
|
+
const masterDoc = readFileSync(path.resolve(__dirname, "./templates/master-doc.mustache"), "utf-8");
|
|
207
|
+
const masterFunction = `
|
|
208
|
+
def perform_${functionName.toLowerCase()}(action, payload,config: ValidationConfig = None, external_data=None):
|
|
209
|
+
${masterDoc}
|
|
210
|
+
|
|
211
|
+
if external_data is None:
|
|
212
|
+
external_data = {}
|
|
213
|
+
|
|
214
|
+
from .utils.json_normalizer import normalize_keys
|
|
215
|
+
normalized_payload = normalize_keys(payload.copy())
|
|
216
|
+
external_data["_SELF"] = normalized_payload
|
|
217
|
+
default_config = {
|
|
218
|
+
"only_invalid": True,
|
|
219
|
+
"standard_logs": False,
|
|
220
|
+
"_debug": False,
|
|
221
|
+
"hide_parent_errors": True,
|
|
222
|
+
}
|
|
223
|
+
# Merge user config with default config
|
|
224
|
+
if config is None:
|
|
225
|
+
config = default_config
|
|
226
|
+
else:
|
|
227
|
+
config = {**default_config, **config}
|
|
228
|
+
|
|
229
|
+
input_data = {
|
|
230
|
+
"payload": normalized_payload,
|
|
231
|
+
"external_data": external_data,
|
|
232
|
+
"config": config,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if action == "${apis[0]}":
|
|
236
|
+
return ${apis[0]}(input_data)
|
|
237
|
+
${apis
|
|
238
|
+
.slice(1)
|
|
239
|
+
.map((api) => ` elif action == "${api}":
|
|
240
|
+
return ${api}(input_data)`)
|
|
241
|
+
.join("\n")}
|
|
242
|
+
else:
|
|
243
|
+
raise Exception("Action not found")
|
|
244
|
+
`;
|
|
245
|
+
return `${importsCode}\n${masterFunction}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from ..utils.json_path_utils import payload_utils
|
|
2
|
+
from ..utils.validation_utils import validation_utils
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
{{{functionCode}}}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def {{apiName}}(input_data):
|
|
9
|
+
total_results = {{apiName}}_validations(input_data)
|
|
10
|
+
|
|
11
|
+
if input_data["config"].get("_debug") is False:
|
|
12
|
+
for r in total_results:
|
|
13
|
+
if "_debug_info" in r:
|
|
14
|
+
del r["_debug_info"]
|
|
15
|
+
|
|
16
|
+
if input_data["config"].get("hide_parent_errors") is True:
|
|
17
|
+
# delete results with valid false and no description
|
|
18
|
+
total_results = [r for r in total_results if not (r["valid"] is False and "description" not in r)]
|
|
19
|
+
|
|
20
|
+
if input_data["config"].get("only_invalid") is True:
|
|
21
|
+
res = [r for r in total_results if r["valid"] is False]
|
|
22
|
+
if len(res) == 0:
|
|
23
|
+
target_success = next((r for r in total_results if r["test_name"] == "{{apiName}}_validations"), None)
|
|
24
|
+
if not target_success:
|
|
25
|
+
raise Exception("Critical: Overall test result not found")
|
|
26
|
+
return [target_success]
|
|
27
|
+
return res
|
|
28
|
+
|
|
29
|
+
return total_results
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Any, Dict, Set
|
|
2
|
+
|
|
3
|
+
JSONObject = Dict[str, Any]
|
|
4
|
+
|
|
5
|
+
def normalize_keys(input_data: Any) -> Any:
|
|
6
|
+
"""
|
|
7
|
+
Normalize keys so that:
|
|
8
|
+
- All objects with the same property name share the union of keys seen anywhere.
|
|
9
|
+
- All objects inside the same array share the union of keys at that array level.
|
|
10
|
+
Missing keys are filled with None.
|
|
11
|
+
"""
|
|
12
|
+
templates_by_prop_name: Dict[str, Set[str]] = {}
|
|
13
|
+
|
|
14
|
+
def collect_templates(node: Any) -> None:
|
|
15
|
+
if isinstance(node, list):
|
|
16
|
+
# Recurse into array items
|
|
17
|
+
for item in node:
|
|
18
|
+
collect_templates(item)
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
if isinstance(node, dict):
|
|
22
|
+
# For each property: if it's a dict (non-list), record its keys under that property name
|
|
23
|
+
for k, v in node.items():
|
|
24
|
+
if isinstance(v, dict):
|
|
25
|
+
s = templates_by_prop_name.setdefault(k, set())
|
|
26
|
+
for child_key in v.keys():
|
|
27
|
+
s.add(child_key)
|
|
28
|
+
# Recurse
|
|
29
|
+
collect_templates(v)
|
|
30
|
+
|
|
31
|
+
def apply_templates(node: Any) -> Any:
|
|
32
|
+
if isinstance(node, list):
|
|
33
|
+
# Compute union of keys across all dict elements for this array level
|
|
34
|
+
array_union: Set[str] = set()
|
|
35
|
+
for item in node:
|
|
36
|
+
if isinstance(item, dict):
|
|
37
|
+
array_union.update(item.keys())
|
|
38
|
+
|
|
39
|
+
normalized_list = []
|
|
40
|
+
for item in node:
|
|
41
|
+
if isinstance(item, dict):
|
|
42
|
+
# Ensure array-level union keys
|
|
43
|
+
next_obj: JSONObject = dict(item)
|
|
44
|
+
for k in array_union:
|
|
45
|
+
if k not in next_obj:
|
|
46
|
+
next_obj[k] = None
|
|
47
|
+
|
|
48
|
+
# Apply templates to nested dict properties
|
|
49
|
+
for k, v in list(next_obj.items()):
|
|
50
|
+
if isinstance(v, dict):
|
|
51
|
+
next_obj[k] = fill_from_template(k, v)
|
|
52
|
+
else:
|
|
53
|
+
next_obj[k] = apply_templates(v)
|
|
54
|
+
normalized_list.append(next_obj)
|
|
55
|
+
else:
|
|
56
|
+
normalized_list.append(apply_templates(item))
|
|
57
|
+
return normalized_list
|
|
58
|
+
|
|
59
|
+
if isinstance(node, dict):
|
|
60
|
+
out: JSONObject = {}
|
|
61
|
+
for k, v in node.items():
|
|
62
|
+
if isinstance(v, dict):
|
|
63
|
+
out[k] = fill_from_template(k, v)
|
|
64
|
+
else:
|
|
65
|
+
out[k] = apply_templates(v)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
# primitives unchanged
|
|
69
|
+
return node
|
|
70
|
+
|
|
71
|
+
def fill_from_template(prop: str, obj: Any) -> Any:
|
|
72
|
+
# Recurse first so nested arrays/objects also normalize
|
|
73
|
+
base = apply_templates(obj)
|
|
74
|
+
templ = templates_by_prop_name.get(prop)
|
|
75
|
+
if not templ or not isinstance(base, dict):
|
|
76
|
+
return base
|
|
77
|
+
|
|
78
|
+
filled: JSONObject = dict(base)
|
|
79
|
+
for key in templ:
|
|
80
|
+
if key not in filled:
|
|
81
|
+
filled[key] = None
|
|
82
|
+
return filled
|
|
83
|
+
|
|
84
|
+
# Run passes
|
|
85
|
+
collect_templates(input_data)
|
|
86
|
+
return apply_templates(input_data)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# pip install jsonpath-ng
|
|
2
|
+
from typing import Any, List
|
|
3
|
+
from jsonpath_ng.ext import parse # ext supports more JSONPath features
|
|
4
|
+
|
|
5
|
+
def is_list_of_strings_or_none(value: Any) -> bool:
|
|
6
|
+
return (
|
|
7
|
+
isinstance(value, list)
|
|
8
|
+
and all((v is None) or isinstance(v, str) for v in value)
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def get_json_path(payload: Any, path: str) -> List[Any]:
|
|
12
|
+
"""
|
|
13
|
+
Evaluate a JSONPath against `payload`.
|
|
14
|
+
|
|
15
|
+
- If the result is a list that contains only strings and/or None,
|
|
16
|
+
convert None -> "null".
|
|
17
|
+
- Return [] when there are no matches.
|
|
18
|
+
"""
|
|
19
|
+
expr = parse(path)
|
|
20
|
+
matches = [m.value for m in expr.find(payload)] # extract raw values
|
|
21
|
+
|
|
22
|
+
if is_list_of_strings_or_none(matches):
|
|
23
|
+
matches = ["null" if v is None else v for v in matches]
|
|
24
|
+
|
|
25
|
+
# Explicitly mirror the TS return of [] for no matches
|
|
26
|
+
return matches if len(matches) > 0 else []
|
|
27
|
+
|
|
28
|
+
# Optional: emulate your default export object
|
|
29
|
+
payload_utils = {
|
|
30
|
+
"get_json_path": get_json_path,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
__all__ = ["get_json_path", "payload_utils"]
|