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.
Files changed (30) hide show
  1. package/dist/constants/syntax.d.ts +1 -1
  2. package/dist/constants/syntax.js +98 -2
  3. package/dist/generator/config-compiler.js +7 -3
  4. package/dist/generator/generators/python/py-ast.d.ts +1 -0
  5. package/dist/generator/generators/python/py-ast.js +72 -0
  6. package/dist/generator/generators/python/py-generator.d.ts +19 -0
  7. package/dist/generator/generators/python/py-generator.js +247 -1
  8. package/dist/generator/generators/python/templates/api-test.mustache +29 -0
  9. package/dist/generator/generators/python/templates/api-tests-init.mustache +12 -0
  10. package/dist/generator/generators/python/templates/json-normalizer.mustache +86 -0
  11. package/dist/generator/generators/python/templates/json-path-utils.mustache +33 -0
  12. package/dist/generator/generators/python/templates/master-doc.mustache +40 -0
  13. package/dist/generator/generators/python/templates/requirements.mustache +2 -0
  14. package/dist/generator/generators/python/templates/test-config.mustache +62 -0
  15. package/dist/generator/generators/python/templates/test-object.mustache +29 -0
  16. package/dist/generator/generators/python/templates/validation-code.mustache +35 -0
  17. package/dist/generator/generators/python/templates/validation-utils.mustache +93 -0
  18. package/dist/generator/generators/typescript/templates/api-test.mustache +29 -1
  19. package/dist/generator/generators/typescript/templates/index.mustache +33 -0
  20. package/dist/generator/generators/typescript/templates/test-config.mustache +45 -5
  21. package/dist/generator/generators/typescript/templates/test-object.mustache +11 -1
  22. package/dist/generator/generators/typescript/templates/validation-code.mustache +12 -12
  23. package/dist/generator/generators/typescript/ts-generator.js +25 -13
  24. package/dist/generator/validators/tests-config/sub-validations.js +3 -3
  25. package/dist/index.js +15 -2
  26. package/dist/types/compiler-types.d.ts +2 -1
  27. package/dist/types/compiler-types.js +1 -0
  28. package/dist/utils/fs-utils.js +43 -0
  29. package/dist/utils/general-utils/string-utils.js +2 -2
  30. package/package.json +1 -1
@@ -13,4 +13,4 @@ export declare enum ConfigSyntax {
13
13
  SessionData = "_SESSION_DATA_"
14
14
  }
15
15
  export declare const ConfigKeyWords: (TestObjectSyntax | ConfigSyntax)[];
16
- export declare const nodeReservedKeywords: Set<string>;
16
+ export declare const ReservedKeywords: Set<string>;
@@ -24,8 +24,9 @@ export const ConfigKeyWords = [
24
24
  ConfigSyntax.Tests,
25
25
  ConfigSyntax.SessionData,
26
26
  ];
27
- // JavaScript/Node.js keywords
28
- export const nodeReservedKeywords = new Set([
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
- // `./generated/${codeName}`
75
- targetPath).generateCode({
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
- "use strict";
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,12 @@
1
+ # API Tests module
2
+ # This file makes the api_tests directory a Python package
3
+
4
+ {{#apis}}
5
+ from .{{name}} import {{name}}
6
+ {{/apis}}
7
+
8
+ __all__ = [
9
+ {{#apis}}
10
+ "{{name}}",
11
+ {{/apis}}
12
+ ]
@@ -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"]