99
1010@dataclass
1111class Enum :
12- mapping : dict [str , str ] = None
12+ mapping : OrderedDict [str , str ] = None
1313 description : str = None
1414
1515@dataclass
@@ -23,7 +23,7 @@ class Parameter:
2323
2424@dataclass
2525class ProcedureDetails :
26- fields : list [Parameter ] = None
26+ fields : List [Parameter ] = None
2727 filename : str = None
2828 line : int = None # Line number of the procedure declaration
2929
@@ -96,7 +96,7 @@ def process_params_block(file, lineno: int):
9696 return fields , lineno
9797
9898def process_enum_block (file , lineno : int ) -> tuple [Enum , int ]:
99- result = Enum (mapping = dict [str , str ]())
99+ result = Enum (mapping = OrderedDict [str , str ]())
100100 inside = False
101101 label_value = None
102102 for line in file :
@@ -398,24 +398,8 @@ def generate_procedure_map_impl(model: Model, filename: str):
398398 f .write ("};\n " )
399399 f .write ("} // namespace ComplianceEngine\n " )
400400
401- def main ():
402- """Main function to process a directory and print results."""
403- basedir = os .path .dirname (os .path .realpath (__file__ ))
404- print (f"Processing procedures in { basedir } " )
405- model = Model (OrderedDict [str , Procedure ](), OrderedDict [str , Enum ](), set [str ]())
406- model .supported_types .add ("int" )
407- model .supported_types .add ("std::string" )
408- model .supported_types .add ("regex" )
409-
410- # Iterate over header files and update the model
411- process_directory (model , f"{ basedir } /procedures/" )
412-
413- validate_model (model )
414-
415- generate_procedure_map_header (model , f"{ basedir } /ProcedureMap2.h" )
416- generate_procedure_map_impl (model , f"{ basedir } /ProcedureMap2.cpp" )
417-
418- # Split the model into per-file schemas
401+ def split_model_by_file (model : Model ) -> OrderedDict [str , Model ]:
402+ """Split the model into per-file schemas."""
419403 schema_model = OrderedDict [str , Model ]()
420404 for name , details in model .procedures .items ():
421405 if details .audit :
@@ -428,29 +412,105 @@ def main():
428412 if filename not in schema_model :
429413 schema_model [filename ] = Model (OrderedDict [str , Procedure ](), OrderedDict [str , Enum ](), set [str ]())
430414 schema_model [filename ].procedures [name ] = details
431- for filename , model in schema_model .items ():
415+ return schema_model
416+
417+ def generate_global_json_schema (split_model : OrderedDict [str , Model ], basedir : str ):
418+ """Generate a global JSON schema file."""
419+ print ("Reading global payload schema." )
420+ with open (f"{ basedir } /payload.schema.json" , "r" ) as f :
421+ global_schema = json .load (f )
422+
423+ # Used for model validation
424+ definitions = global_schema .get ("definitions" , {})
425+ audits = definitions .get ("auditProcedure" , {}).get ("anyOf" , [])
426+ audits_dict = dict ()
427+ for audit in audits :
428+ pat = r"procedures/([^/]+)\.schema\.json#/definitions/audit"
429+ match = re .match (pat , audit .get ("$ref" ,"" ))
430+ if not match :
431+ raise ValueError (f"invalid audit reference { audit } " )
432+ fname = match .group (1 )
433+ audits_dict [fname ] = audit
434+
435+ remediations = definitions .get ("remediationProcedure" , {}).get ("anyOf" , [])
436+ remediations_dict = dict ()
437+ for remediation in remediations :
438+ pat = r"procedures/([^/]+)\.schema\.json#/definitions/remediation"
439+ match = re .match (pat , remediation .get ("$ref" ,"" ))
440+ if not match :
441+ # This may happend when the missing remediation fallback is present
442+ continue
443+ fname = match .group (1 )
444+ remediations_dict [fname ] = remediation
445+
446+ for filename in split_model .keys ():
447+ if not filename .endswith (".h" ):
448+ raise ValueError ("filename must end with .h" )
449+ filename = filename [:- 2 ]
450+ audits_dict [filename ] = {"$ref" : f"procedures/{ filename } .schema.json#/definitions/audit" }
451+ remediations_dict [filename ] = {"$ref" : f"procedures/{ filename } .schema.json#/definitions/remediation" }
452+
453+ audits = []
454+ for k , v in sorted (audits_dict .items ()):
455+ audits .append (v )
456+ remediations = []
457+ for k , v in sorted (audits_dict .items ()):
458+ remediations .append (v )
459+
460+ definitions ["auditProcedure" ]["anyOf" ] = audits
461+ definitions ["remediationProcedure" ]["anyOf" ] = remediations
462+ # If there's no remediation we fall back to audit
463+ fallback = { "$ref" : "#definitions/auditProcedure" }
464+ if fallback not in definitions ["remediationProcedure" ]["anyOf" ]:
465+ definitions ["remediationProcedure" ]["anyOf" ].append ( { "$ref" : "#definitions/auditProcedure" })
466+ print ("Dumping global schema." )
467+ with open (f"{ basedir } /payload.schema.json" , "w" ) as f :
468+ json .dump (global_schema , f , indent = 2 )
469+ # Keep EOL check happy
470+ f .write ("\n " )
471+
472+ def generate_json_schema (model : Model , basedir : str ):
473+ """Generate JSON schema files for each procedure."""
474+ split_model = split_model_by_file (model )
475+ print ("Reading global payload schema." )
476+ with open (f"{ basedir } /payload.schema.json" , "r" ) as f :
477+ global_schema = json .load (f )
478+
479+ for filename , model in split_model .items ():
432480 print (f"Writing schema for { filename } ." )
433- if filename .endswith (".h" ):
434- filename = filename [:- 2 ]
481+ if not filename .endswith (".h" ):
482+ raise ValueError ("filename must end with .h" )
483+ filename = filename [:- 2 ]
435484 json_schema_model = create_file_schema (model )
436485 with open (f"{ basedir } /procedures/{ filename } .schema.json" ,"w" ) as f :
437486 json .dump (json_schema_model , f , indent = 2 )
438487 # Keep EOL check happy
439488 f .write ("\n " )
440- # Used for model validation
441- # print("Reading global payload schema.")
442- # schema = None
443- # with open(f"{basedir}/payload.schema.json", "r") as f:
444- # schema = json.load(f)
445- # schema["definitions"]["auditProcedure"]["anyOf"] = [ {"$ref" : f"procedures/{fname}.schema.json#/definitions/audit" } for fname in result.keys() ]
446- # schema["definitions"]["remediationProcedure"]["anyOf"] = [ {"$ref" : f"procedures/{fname}.schema.json#/definitions/remediation" } for fname in result.keys() ]
447- # # If there's no remediation we fall back to audit
448- # schema["definitions"]["remediationProcedure"]["anyOf"].append( { "$ref": "#definitions/auditProcedure" })
449- # print("Dumping global schema.")
450- # with open(f"{basedir}/payload.schema.json", "w") as f:
451- # json.dump(schema, f, indent=2)
452- # # Keep EOL check happy
453- # f.write("\n")
489+
490+ generate_global_json_schema (split_model , basedir )
491+
492+ def main ():
493+ """Main function to process a directory and print results."""
494+ basedir = os .path .dirname (os .path .realpath (__file__ ))
495+ model = Model (OrderedDict [str , Procedure ](), OrderedDict [str , Enum ](), set [str ]())
496+
497+ # Supported types are used in model validation
498+ model .supported_types .add ("int" )
499+ model .supported_types .add ("std::string" )
500+ model .supported_types .add ("regex" )
501+
502+ # Iterate over header files and update the model
503+ process_directory (model , f"{ basedir } /procedures/" )
504+
505+ # Test model integrity
506+ validate_model (model )
507+
508+ # Generate C++ procedure map files
509+ generate_procedure_map_header (model , f"{ basedir } /ProcedureMap2.h" )
510+ generate_procedure_map_impl (model , f"{ basedir } /ProcedureMap2.cpp" )
511+
512+ # Generate JSON schema files
513+ generate_json_schema (model , basedir )
454514
455515if __name__ == "__main__" :
456516 import sys
0 commit comments