diff --git a/build.py b/build.py index d561d9da..4a0e9081 100644 --- a/build.py +++ b/build.py @@ -2,7 +2,7 @@ import shutil from pipeline.translator import MATLABSchemaBuilder -from pipeline.utils import clone_sources, SchemaLoader, initialise_jinja_template_environment, save_resource_files, save_enumeration_classes +from pipeline.utils import clone_sources, SchemaLoader, initialise_jinja_templates, save_resource_files, save_enumeration_classes print("***************************************") print(f"Triggering the generation of MATLAB-Classes for openMINDS") @@ -14,10 +14,10 @@ if os.path.exists("target"): shutil.rmtree("target") -for schema_version in schema_loader.get_schema_versions(): +# Step 2 - Initialise the jinja templates +jinja_templates = initialise_jinja_templates() - # Step 2 - Initialise the jinja template environment - jinja_template_environment = initialise_jinja_template_environment() +for schema_version in schema_loader.get_schema_versions(): # Step 3 - find all involved schemas for the current version schemas_file_paths = schema_loader.find_schemas(schema_version) @@ -27,11 +27,11 @@ # Step 4 - translate and build each openMINDS schema as MATLAB class schema_root_path = schema_loader.schemas_sources try: - MATLABSchemaBuilder(schema_file_path, schema_root_path, jinja_template_environment).build() + MATLABSchemaBuilder(schema_file_path, schema_root_path, jinja_templates).build() except Exception as e: print(f"Error while building schema {schema_file_path}: {e}") save_resource_files(schema_version, schemas_file_paths) - save_enumeration_classes("Types", schema_version, schema_loader, jinja_template_environment) - save_enumeration_classes("Models", schema_version, schema_loader, jinja_template_environment) + save_enumeration_classes("Types", schema_version, schema_loader, jinja_templates["types_enumeration"]) + save_enumeration_classes("Models", schema_version, schema_loader, jinja_templates["models_enumeration"]) diff --git a/pipeline/templates/controlledterm_class_template.txt b/pipeline/templates/controlledterm_class_template.txt index 88187e42..31cf9589 100644 --- a/pipeline/templates/controlledterm_class_template.txt +++ b/pipeline/templates/controlledterm_class_template.txt @@ -29,11 +29,10 @@ classdef {{ class_name }} < openminds.abstract.ControlledTerm {%- endfor %} ] end - + methods function obj = {{ class_name }}(varargin) obj@openminds.abstract.ControlledTerm(varargin{:}) end end - end diff --git a/pipeline/templates/mixedtype_class_template.txt b/pipeline/templates/mixedtype_class_template.txt new file mode 100644 index 00000000..d8876446 --- /dev/null +++ b/pipeline/templates/mixedtype_class_template.txt @@ -0,0 +1,15 @@ +classdef {{ class_name }} < openminds.internal.abstract.LinkedCategory + properties (Constant, Hidden) + ALLOWED_TYPES = [ ... + {%- for full_type_name in allowed_types_list %} + {%- if not loop.last%} + "{{full_type_name}}", ... + {%- else%} + "{{full_type_name}}" ... + {%- endif -%} + + {%- endfor %} + ] + IS_SCALAR = {{is_scalar}} + end +end diff --git a/pipeline/templates/schema_class_template.txt b/pipeline/templates/schema_class_template.txt index 484bd35f..c13fb0d5 100644 --- a/pipeline/templates/schema_class_template.txt +++ b/pipeline/templates/schema_class_template.txt @@ -52,11 +52,10 @@ classdef {{ class_name }} < {{base_class}} obj@openminds.abstract.Schema(varargin{:}) end end - + methods (Access = protected) function str = getDisplayLabel(obj) {{display_label_method_expression}} end end - end diff --git a/pipeline/templates/types_enumeration_template.txt b/pipeline/templates/types_enumeration_template.txt index 96c66ca3..f06ebc44 100644 --- a/pipeline/templates/types_enumeration_template.txt +++ b/pipeline/templates/types_enumeration_template.txt @@ -1,10 +1,8 @@ classdef Types < openminds.abstract.TypesEnumeration - enumeration None('None') {%- for type in types %} {{type.name}}("{{type.class_name}}") {%- endfor %} end - end diff --git a/pipeline/translator.py b/pipeline/translator.py index 86b3c18f..4ba64169 100644 --- a/pipeline/translator.py +++ b/pipeline/translator.py @@ -7,11 +7,11 @@ import math import os import re -from typing import List +from typing import List, Dict from collections import defaultdict import warnings -from jinja2 import Environment +from jinja2 import Template from pipeline.constants import ( OPENMINDS_BASE_URI, @@ -46,7 +46,7 @@ class MATLABSchemaBuilder(object): """ Class for building MATLAB schema classes """ - def __init__(self, schema_file_path:str, root_path:str, jinja_template_environment:Environment): + def __init__(self, schema_file_path:str, root_path:str, jinja_templates:Dict[str, Template]): self._parse_source_file_path(schema_file_path, root_path) @@ -54,11 +54,11 @@ def __init__(self, schema_file_path:str, root_path:str, jinja_template_environme self._schema_payload = json.load(schema_file) if self._schema_model_name == "controlledTerms": - template_file_name = TEMPLATE_FILE_NAME_CT + self.class_template = jinja_templates["controlledterm_class"] else: - template_file_name = TEMPLATE_FILE_NAME + self.class_template = jinja_templates["schema_class"] - self.schema_template = jinja_template_environment.get_template(template_file_name) + self.mixedtype_class_template = jinja_templates["mixedtype_class"] def build(self): """Build and save the MATLAB schema class file""" @@ -171,6 +171,8 @@ def _extract_template_variables(self): # Resolve property validators in matlab validators = _create_property_validator_functions(property_name, property_info) + mixed_types_list = sorted(possible_types) + if len(possible_types) == 1: possible_types = possible_types[0] possible_types_str = f'"{possible_types}"' @@ -187,6 +189,7 @@ def _extract_template_variables(self): "type": possible_types, "type_doc": possible_types_docstr, "type_list": possible_types_str, + "mixed_type_list": mixed_types_list, "size": size_attribute, "size_doc": size_attribute_doc, "validators": "{{{}}}".format(', '.join(validators)) if validators else "", @@ -241,7 +244,7 @@ def _expand_schema_template(self) -> str: # print(f"Expanding template for {self._schema_file_name}") template_variables = self._template_variables - result = self.schema_template.render(template_variables) + result = self.class_template.render(template_variables) return _strip_trailing_whitespace(result) def _generate_additional_files(self): @@ -271,14 +274,16 @@ def _build_mixed_type_class(self, schema, prop): file_name = property_name + ".m" file_path = os.path.join(*path_parts, file_name) - # Write file content # Todo: use a template - with open(file_path, "w") as fp: - fp.write(f"classdef {property_name} < openminds.internal.abstract.LinkedCategory\n") - fp.write(f" properties (Constant, Hidden)\n") - fp.write(f" ALLOWED_TYPES = {prop['type_list']}\n") - fp.write(f" IS_SCALAR = {str(not(prop['allow_multiple'])).lower()}\n") - fp.write(f" end\n") - fp.write(f"end\n") + template_variables = { + "class_name": property_name, + "allowed_types_list": prop['mixed_type_list'], + "is_scalar": str(not(prop['allow_multiple'])).lower(), + } + + mixedtype_classdef_str = self.mixedtype_class_template.render(template_variables) + + with open(file_path, "w", encoding="utf-8") as target_file: + target_file.write(mixedtype_classdef_str) # # # LOCAL UTILITY FUNCTIONS # # # diff --git a/pipeline/utils.py b/pipeline/utils.py index 2ce174c1..1a6ac608 100644 --- a/pipeline/utils.py +++ b/pipeline/utils.py @@ -7,6 +7,7 @@ from git import Repo, GitCommandError from jinja2 import Environment, select_autoescape, FileSystemLoader +from jinja2 import Template def clone_sources(): @@ -74,12 +75,67 @@ def get_instance_collection(self, version:str, schema_name:str) -> List[str]: return instance_list -def initialise_jinja_template_environment(autoescape:bool=None): - return Environment( - loader=FileSystemLoader(os.path.dirname(os.path.realpath(__file__))), +def initialise_jinja_templates(autoescape:bool=None): + """ + Initializes a Jinja2 environment and preloads templates into a dictionary for reuse. + + This function sets up a Jinja2 `Environment` for loading templates from the `templates` + subdirectory within the current script's directory. It then preloads a set of named templates + into a dictionary, making them accessible by key for efficient repeated rendering. This + approach avoids repeated environment initialization and template loading, optimizing + performance when rendering templates multiple times. + + Parameters: + ----------- + autoescape : bool, optional + Configures the autoescaping behavior for templates: + - `True` enables autoescaping for all templates. + - `False` disables autoescaping. + - `None` (default) uses Jinja2's `select_autoescape()` function to enable autoescaping for + specific file types (e.g., `.html`, `.xml`). + + Returns: + -------- + dict + A dictionary of preloaded Jinja2 template objects keyed by descriptive names, allowing + access to specific templates. The available templates are: + - `"schema_class"`: Template for schema class generation. + - `"controlledterm_class"`: Template for controlled term class generation. + - `"mixedtype_class"`: Template for mixed type class generation. + - `"models_enumeration"`: Template for models enumeration generation. + - `"types_enumeration"`: Template for types enumeration generation. + + Notes: + ------ + - The function uses `os.path.dirname(os.path.realpath(__file__))` to locate the directory of + the current script, ensuring templates are loaded from the `templates` subdirectory. + - Autoescaping is configured to help prevent injection attacks for templates handling HTML or XML. + + Example Usage: + -------------- + templates = initialise_jinja_templates(autoescape=True) + rendered_schema = templates["schema_class"].render(data=schema_data) + """ + + module_directory = os.path.dirname(os.path.realpath(__file__)) + template_directory = os.path.join(module_directory, "templates") + + jinja_environment = Environment( + loader=FileSystemLoader(template_directory), autoescape=select_autoescape(autoescape) if autoescape is not None else select_autoescape() ) + jinja_templates = { + "schema_class": jinja_environment.get_template("schema_class_template.txt"), + "controlledterm_class": jinja_environment.get_template("controlledterm_class_template.txt"), + "mixedtype_class": jinja_environment.get_template("mixedtype_class_template.txt"), + "models_enumeration": jinja_environment.get_template("models_enumeration_template.txt"), + "types_enumeration": jinja_environment.get_template("types_enumeration_template.txt"), + } + + return jinja_templates + + def camel_case(text_string: str): return text_string[0].lower() + text_string[1:] @@ -156,12 +212,8 @@ def save_resource_files(version, schema_path_list): with open(os.path.join(target_directory, "alias.json"), "w") as f: json.dump(alias_json, f, indent=2) -def save_enumeration_classes(enum_type, version, schema_loader, jinja_template_environment): +def save_enumeration_classes(enum_type, version, schema_loader, enumeration_template:Template): - # Load templates - template_file_name = os.path.join("templates", enum_type.lower()+"_enumeration_template.txt") - enumeration_template = jinja_template_environment.get_template(template_file_name) - # Create target file directory target_file_path = _create_enum_target_file_path(version, enum_type) os.makedirs(os.path.dirname(target_file_path), exist_ok=True)