Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,3 @@
build
__pycache__
*.egg-info

View File

@@ -0,0 +1,31 @@
load("//applications/utils:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
exports_files(
["BUILD.bazel"],
)
py_library(
name = "lib",
srcs = glob(["src/ev_cli/*.py"]),
imports = ["src"],
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
deps = [
requirement("jinja2"),
requirement("jsonschema"),
requirement("stringcase"),
requirement("pyyaml"),
],
)
py_binary(
name = "ev-cli",
srcs = ["src/ev_cli/ev.py"],
data = glob(["src/ev_cli/templates/**"]),
legacy_create_init = False,
main = "src/ev_cli/ev.py",
visibility = ["//visibility:public"],
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
deps = [":lib"],
)

View File

@@ -0,0 +1,4 @@
ev_create_python_wheel_targets(
PACKAGE_NAME
"ev-dev-tools"
)

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,195 @@
=========================
EVerest development tools
=========================
This python project currently consists of the following packages
- `ev_cli`: EVerest module auto generation
Install
-------
To install `ev_cli`:
python3 -m pip install .
ev_cli
------
The `ev_cli` package comes with a command line tool, named ``ev-cli``.
It has the following subcommands
- module:
auto generation and update of EVerest modules from its interface and
manifest definitions
- interface:
auto generation of c++ header files for defined interfaces
- helpers:
utility commands
There exist short forms, for all subcommands and options. Simply call:
ev-cli --help
for getting the list of short forms.
Both the `module` and `interface` command have the following options in
common:
- `--work-dir`:
work directory which also contains the manifest definitions (default: ``.``)
- `--everest-dir`:
root directory of EVerest core or any directory containing interface
and module definitions (default: ``.``)
- `--schemas-dir`:
schemas directory of the EVerest framework, containing the schema
definitions (default: ``EVerest/lib/everest/framework/schemas``)
- `--clang-format-file`:
if c++output should be formatted, set this to the path of the
.clang-format file
Generating c++ header files for defined interfaces
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Assuming that the interface definitions in json format are located at
``./interfaces/*.json``, simply:
ev-cli interfaces generate-headers
This will generate the c++ header files for all interfaces and output them
to ``./generated/include/generated``. To generate only a single interface, call:
ev-cli interfaces generate-headers InterfaceName.json
For each interface an ``Implementation.hpp`` and ``Interface.hpp``
header file will be generated. The former represents the `implementers`
view, and the latter the `users` view of the interface, when used in a
module.
Creating and updating auto generated files for modules (c++ only)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Assuming the modules are located at ``./modules`` and the initial
skeleton for a module named `Example` with its manifest in
``./modules/Example/manifest.json`` should be created, call:
ev-cli module create Example
This will create the following files inside the ``./modules/Example``
subdirectory
- ``CMakeLists.txt``:
build instruction file for CMake
- ``ld-ev.hpp``/``ld-ev.cpp``:
glue code files for this module to get hooked up by the EVerest
framework
- ``Example.hpp``/``Example.cpp``:
header and source file for the module
Furthermore, for each interface provided by the module a subdirectory
with the name of the `interface id` will be created. If, for example,
the manifest looks like::
{
"description": "Example module",
"provides": {
"main": {
"description": "SampleInterface implementation",
"interface": "SampleInterface",
...
}
}
...
}
a subdirectory named ``main`` will be created, including two files
``SampleInterfaceImpl.hpp`` and ``SampleInterfaceImpl.cpp``. The header
file declares the implementation of `SampleInterface`, which derives
from the auto generated interface header files from the previous
subsection.
Now it is up to the user to implement logic in the module and interface
implementation `cpp` source files.
If the modules' ``manifest.json`` or inferface definitions, used by the
module, change, you can update the generated files by using:
ev-cli module update Example
**Note**:
1.
``cpp`` source files will never be changed or overwritten by the
`update` subcommand. The `create` subcommand only resets / overrides
the files when using the ``--force`` option
2.
``hpp`` header files and the ``CMakeLists.txt`` file will get
updated, if its interface dependencies definitions change and the
`update` subcommand is used. You can force an update by using the
``--force`` option. During an update, the sections marked like::
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
.....
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
will be kept. If you want to completely reset / override these
files, you need to recreate the using `create` subcommand with the
``--force`` option.
3.
Generated files will never be deleted. So make sure, you do this if
you, for example, change the interface ids or remove interfaces from
the module
These additional options might be useful for the `create` and `update`
subcommands:
1. ``--force``:
force creation or update
2. ``--diff``:
don't touch anything, only show a `diff` of what would be changed
3. ``--only``:
this option takes a comma separated list of files, that should be
touched only. This is especially helpful, if you want to recreate
only a single interface implementation ``cpp`` file, because you
changed the corresponding interface a lot. To get a list of possible files, you can simply call:
ev-cli module create Example --only which
this would output for the above mentioned example::
Available files for category "core"
cmakelists
ld-ev.hpp
ld-ev.cpp
module.hpp
module.cpp
Available files for category "interfaces"
main.hpp
main.cpp
So calling:
ev-cli module create Example --only main.cpp,cmakelists --force
would recreate the ``CMakeLists.txt`` and the
``main/SampleInterfaceImpl.cpp`` files, whereas:
ev-clie module update Example --only module.hpp
would update only the module header file ``Example.hpp``
Auto generating NodeJS modules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**tbd**

View File

@@ -0,0 +1,9 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
[tool.autopep8]
max_line_length = 120

View File

@@ -0,0 +1,34 @@
[metadata]
name = ev-dev-tools
version = attr: ev_cli.__version__
author = aw
author_email = aw@pionix.de
description = Utilities for developing with the everest framework
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://github.com/EVerest/everest-utils
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
[options]
install_requires =
jinja2 ~=3.1
jsonschema ~=4.26
stringcase ~=1.2
pyyaml ~=6.0
package_dir =
= src
packages = ev_cli
python_requires = >=3.7
[options.entry_points]
console_scripts =
ev-cli = ev_cli.ev:main
[options.package_data]
ev_cli =
templates/*.j2
licenses/**

View File

@@ -0,0 +1,3 @@
from setuptools import setup
setup()

View File

@@ -0,0 +1,2 @@
"""EVerest command line utility."""
__version__ = '0.7.3'

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
"""
Provide error parsing functionality.
author: andreas.heinrich@pionix.de
"""
from typing import List, NamedTuple
from pathlib import Path
from . import helpers
import yaml, jsonschema
class ErrorDefinition(NamedTuple):
"""Error definition class."""
namespace: str
name: str
description: str
class ErrorParser:
"""Error parser class."""
validators = None
error_definitions = {}
@classmethod
def load_error_definition_file(cls, path: Path):
try:
error_def = yaml.safe_load(path.read_text())
ErrorParser.validators['error_declaration_list'].validate(error_def)
except OSError as err:
raise Exception(f'Could not open error definition file {err.filename}: {err.strerror}') from err
except yaml.YAMLError as err:
raise Exception(f'Could not parse error definition file {path}: {err}') from err
except jsonschema.ValidationError as err:
raise Exception(f'Error definition file {path} is not valid: {err}') from err
namespace = path.stem
if namespace in cls.error_definitions:
raise Exception(f'Error definition namespace { namespace } already exists.')
else:
cls.error_definitions[namespace] = {}
for entry in error_def['errors']:
error = ErrorDefinition(namespace, entry['name'], entry['description'])
if error.name in cls.error_definitions[error.namespace]:
raise Exception(f'Error definition { error.namespace }/{ error.name } already exists.')
cls.error_definitions[error.namespace][error.name] = error
@classmethod
def get_error_definition(cls, namespace: str, name: str) -> ErrorDefinition:
"""Get error definition for the provided namespace and name."""
if namespace not in cls.error_definitions:
path = helpers.resolve_everest_dir_path(f'errors/{ namespace }.yaml')
cls.load_error_definition_file(path)
if name not in cls.error_definitions[namespace]:
raise Exception(f'Error definition { namespace }/{ name } does not exist.')
return cls.error_definitions[namespace][name]
@classmethod
def get_error_definitions(cls, namespace: str) -> List[ErrorDefinition]:
"""Get error definitions for the provided namespace."""
if namespace not in cls.error_definitions:
path = helpers.resolve_everest_dir_path(f'errors/{ namespace }.yaml')
cls.load_error_definition_file(path)
result = []
for error in cls.error_definitions[namespace].values():
result.append(error)
return result
@classmethod
def resolve_error_reference(cls, error_ref: str) -> List[ErrorDefinition]:
"""Resolve error reference."""
ref_prefix = '/errors/'
if not error_ref.startswith(ref_prefix):
raise Exception(f'Error reference { error_ref } does not start with { ref_prefix }.')
error_ref = error_ref[len(ref_prefix):]
if '#/' in error_ref:
namespace, name = error_ref.split('#/')
result = []
result.append(cls.get_error_definition(namespace, name))
return result
else:
return cls.get_error_definitions(error_ref)

View File

@@ -0,0 +1,902 @@
#!/usr/bin/env -S python3 -tt
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
"""
author: aw@pionix.de
FIXME (aw): Module documentation.
"""
from ev_cli import __version__
from ev_cli import helpers
from ev_cli.type_parsing import TypeParser
from ev_cli.error_parsing import ErrorParser
from datetime import datetime
from pathlib import Path
import jinja2 as j2
import argparse
import stringcase
from typing import List
# FIXME (aw): remove these global variables
# global variables
everest_dirs: List[Path] = []
work_dir: Path = None
# jinja template environment and global variable
env = j2.Environment(loader=j2.FileSystemLoader(Path(__file__).parent / 'templates'),
lstrip_blocks=True, trim_blocks=True, undefined=j2.StrictUndefined,
keep_trailing_newline=True)
templates = {}
validators = {}
# Function declarations
def setup_jinja_env():
env.globals['timestamp'] = datetime.utcnow()
# FIXME (aw): which repo to use? everest or everest-framework?
env.filters['snake_case'] = helpers.snake_case
env.filters['create_dummy_result'] = helpers.create_dummy_result
templates.update({
'interface_base': env.get_template('interface-Base.hpp.j2'),
'interface_exports': env.get_template('interface-Exports.hpp.j2'),
'interface_impl.hpp': env.get_template('interface-Impl.hpp.j2'),
'interface_impl.cpp': env.get_template('interface-Impl.cpp.j2'),
'types.hpp': env.get_template('types.hpp.j2'),
'module.hpp': env.get_template('module.hpp.j2'),
'module.cpp': env.get_template('module.cpp.j2'),
'ld-ev.hpp': env.get_template('ld-ev.hpp.j2'),
'ld-ev.cpp': env.get_template('ld-ev.cpp.j2'),
'cmakelists': env.get_template('CMakeLists.txt.j2'),
'index.rst': env.get_template('index.rst.j2'),
})
def generate_tmpl_data_for_if(interface, if_def, type_file):
helpers.parsed_enums.clear()
helpers.parsed_types.clear()
helpers.type_headers.clear()
types = []
enums = []
vars = []
for var, var_info in if_def.get('vars', {}).items():
(type_info, enum_info) = helpers.extended_build_type_info(var, var_info, type_file)
if enum_info and type_file:
enums.append(enum_info)
vars.append(type_info)
cmds = []
for cmd, cmd_info in if_def.get('cmds', {}).items():
args = []
for arg, arg_info in cmd_info.get('arguments', {}).items():
(type_info, enum_info) = helpers.extended_build_type_info(arg, arg_info, type_file)
if enum_info and type_file:
enums.append(enum_info)
args.append(type_info)
result_type_info = None
if 'result' in cmd_info:
result_info = cmd_info['result']
(result_type_info, enum_info) = helpers.extended_build_type_info('result', result_info, type_file)
if enum_info and type_file:
enums.append(enum_info)
cmds.append({'name': cmd, 'args': args, 'result': result_type_info})
if type_file:
for parsed_enum in helpers.parsed_enums:
enum_info = {
'name': parsed_enum['name'],
'description': parsed_enum['description'],
'enum_type': stringcase.capitalcase(parsed_enum['name']),
'enum': parsed_enum['enums']
}
enums.append(enum_info)
if type_file:
for parsed_type in helpers.parsed_types:
parsed_type['name'] = stringcase.capitalcase(parsed_type['name'])
if 'properties' in parsed_type:
for prop in parsed_type['properties']:
if 'type_dict' in prop['info']:
path = Path('generated/types') / \
prop['info']['type_dict']['type_relative_path'].with_suffix('.hpp')
helpers.type_headers.add(path.as_posix())
types.append(parsed_type)
error_lists = if_def.get('errors', [])
# Use a dict to avoid duplicate error definitions
errors_dict = {}
for entry in error_lists:
if not 'reference' in entry:
raise Exception(f'Error definition {entry} does not have a reference.')
for error in ErrorParser.resolve_error_reference(entry['reference']):
if error.namespace not in errors_dict:
errors_dict[error.namespace] = {}
if error.name in errors_dict[error.namespace]:
raise Exception(f'Error definition {error.namespace}/{error.name} already referenced.')
errors_dict[error.namespace][error.name] = error
errors = []
for value in errors_dict.values():
errors.extend(value.values())
tmpl_data = {
'info': {
'base_class_header': f'generated/interfaces/{interface}/Implementation.hpp',
'interface': interface,
'desc': if_def['description'],
'type_headers': sorted(helpers.type_headers)
},
'enums': enums,
'types': types,
'vars': vars,
'cmds': cmds,
'errors': errors,
}
return tmpl_data
def generate_tmpl_data_for_module(module, module_def):
provides = []
for impl, impl_info in module_def.get('provides', {}).items():
config = []
for conf_id, conf_info in impl_info.get('config', {}).items():
type_info = helpers.build_type_info(conf_id, conf_info['type'])
config.append(type_info)
provides.append({
'id': impl,
'type': impl_info['interface'],
'desc': impl_info['description'],
'config': config,
'class_name': f'{impl_info["interface"]}Impl',
'base_class': f'{impl_info["interface"]}ImplBase',
'base_class_header': f'generated/interfaces/{impl_info["interface"]}/Implementation.hpp'
})
requires = []
for requirement_id, req_info in module_def.get('requires', {}).items():
# min_connections=1 and max_connections=1 is the default if not provided otherwise (see manifest meta schema)
is_vector = not (
('min_connections' not in req_info or req_info['min_connections'] == 1) and
('max_connections' not in req_info or req_info['max_connections'] == 1))
requires.append({
'id': requirement_id,
'is_vector': is_vector,
'type': req_info['interface'],
'class_name': f'{req_info["interface"]}Intf',
'exports_header': f'generated/interfaces/{req_info["interface"]}/Interface.hpp'
})
module_config = []
for conf_id, conf_info in module_def.get('config', {}).items():
type_info = helpers.build_type_info(conf_id, conf_info['type'])
module_config.append(type_info)
tmpl_data = {
'info': {
'name': module,
'class_name': module, # FIXME (aw): enforce capital case?
'desc': module_def['description'],
'module_header': f'{module}.hpp',
'module_config': module_config,
'ld_ev_header': 'ld-ev.hpp',
'enable_external_mqtt': module_def.get('enable_external_mqtt', False),
'enable_telemetry': module_def.get('enable_telemetry', False),
'enable_global_errors': module_def.get('enable_global_errors', False)
},
'provides': provides,
'requires': requires,
}
return tmpl_data
def construct_impl_file_paths(impl):
interface = impl['type']
common_part = f'{impl["id"]}/{interface}'
return (f'{common_part}Impl.hpp', f'{common_part}Impl.cpp')
def set_impl_specific_path_vars(tmpl_data, output_path):
"""Set cpp_file_rel_path and class_header vars to implementation template data."""
for impl in tmpl_data['provides']:
(impl['class_header'], impl['cpp_file_rel_path']) = construct_impl_file_paths(impl)
def generate_module_loader_files(rel_mod_dir, output_dir):
loader_files = []
(_, _, mod) = rel_mod_dir.rpartition('/')
mod_path = work_dir / f'modules/{rel_mod_dir}/manifest.yaml'
if not mod_path.exists():
raise Exception(f'Could not find module manifest ({mod_path}')
mod_def = helpers.load_validated_module_def(mod_path, validators['module'])
tmpl_data = generate_tmpl_data_for_module(mod, mod_def)
set_impl_specific_path_vars(tmpl_data, mod_path.parent)
# ld-ev.hpp
tmpl_data['info']['hpp_guard'] = 'LD_EV_HPP'
loader_files.append({
'filename': 'ld-ev.hpp',
'path': output_dir / mod / 'ld-ev.hpp',
'printable_name': f'{mod}/ld-ev.hpp',
'content': templates['ld-ev.hpp'].render(tmpl_data),
'template_path': Path(templates['ld-ev.hpp'].filename),
'last_mtime': mod_path.stat().st_mtime
})
# ld-ev.cpp
loader_files.append({
'filename': 'ld-ev.cpp',
'path': output_dir / mod / 'ld-ev.cpp',
'printable_name': f'{mod}/ld-ev.cpp',
'content': templates['ld-ev.cpp'].render(tmpl_data),
'template_path': Path(templates['ld-ev.cpp'].filename),
'last_mtime': mod_path.stat().st_mtime
})
return loader_files
def generate_module_files(rel_mod_dir, update_flag, licenses):
(_, _, mod) = rel_mod_dir.rpartition('/')
mod_files = {'core': [], 'interfaces': [], 'docs': []}
mod_path = work_dir / f'modules/{rel_mod_dir}/manifest.yaml'
mod_def = helpers.load_validated_module_def(mod_path, validators['module'])
default_license_dir = Path(__file__).parent / 'licenses'
current_license_dir = work_dir / 'licenses'
additional_license_dir = Path(licenses)
license_dirs = [default_license_dir, current_license_dir, additional_license_dir]
license_url = mod_def['metadata']['license']
license_header = helpers.get_license_header(license_dirs, license_url)
if not license_header:
print(f'Could not find license "{license_url}" in {license_dirs}.')
print('Consider providing a additonal custom license directory with --licenses')
exit(1)
tmpl_data = generate_tmpl_data_for_module(mod, mod_def)
output_path = mod_path.parent
# FIXME (aw): we might move the following function into generate_tmp_data_for_module
set_impl_specific_path_vars(tmpl_data, output_path)
cmakelists_blocks = {
'version': 'v1',
'format_str': '# ev@{uuid}:{version}',
'regex_str': '^(?P<indent>\s*)# ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
'definitions': {
'add_general': {
'id': 'bcc62523-e22b-41d7-ba2f-825b493a3c97',
'content': '# insert your custom targets and additional config variables here'
},
'add_other': {
'id': 'c55432ab-152c-45a9-9d2e-7281d50c69c3',
'content': '# insert other things like install cmds etc here'
}
}
}
impl_hpp_blocks = {
'version': 'v1',
'format_str': '// ev@{uuid}:{version}',
'regex_str': '^(?P<indent>\s*)// ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
'definitions': {
'add_headers': {
'id': '75ac1216-19eb-4182-a85c-820f1fc2c091',
'content': '// insert your custom include headers here'
},
'public_defs': {
'id': '8ea32d28-373f-4c90-ae5e-b4fcc74e2a61',
'content': '// insert your public definitions here'
},
'protected_defs': {
'id': 'd2d1847a-7b88-41dd-ad07-92785f06f5c4',
'content': '// insert your protected definitions here'
},
'private_defs': {
'id': '3370e4dd-95f4-47a9-aaec-ea76f34a66c9',
'content': '// insert your private definitions here'
},
'after_class': {
'id': '3d7da0ad-02c2-493d-9920-0bbbd56b9876',
'content': '// insert other definitions here'
}
}
}
mod_hpp_blocks = {
'version': 'v1',
'format_str': '// ev@{uuid}:{version}',
'regex_str': '^(?P<indent>\s*)// ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
'definitions': {
'add_headers': {
'id': '4bf81b14-a215-475c-a1d3-0a484ae48918',
'content': '// insert your custom include headers here'
},
'public_defs': {
'id': '1fce4c5e-0ab8-41bb-90f7-14277703d2ac',
'content': '// insert your public definitions here'
},
'protected_defs': {
'id': '4714b2ab-a24f-4b95-ab81-36439e1478de',
'content': '// insert your protected definitions here'
},
'private_defs': {
'id': '211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8',
'content': '// insert your private definitions here'
},
'after_class': {
'id': '087e516b-124c-48df-94fb-109508c7cda9',
'content': '// insert other definitions here'
}
}
}
# provided interface implementations (impl cpp & hpp)
for impl in tmpl_data['provides']:
interface = impl['type']
(impl_hpp_file, impl_cpp_file) = construct_impl_file_paths(impl)
# load template data for interface
if_def, last_mtime = load_interface_definition(interface)
if_tmpl_data = generate_tmpl_data_for_if(interface, if_def, False)
if_tmpl_data['info'].update({
'hpp_guard': helpers.snake_case(f'{impl["id"]}_{interface}').upper() + '_IMPL_HPP',
'config': impl['config'],
'class_name': interface + 'Impl',
'class_parent': interface + 'ImplBase',
'module_header': f'../{mod}.hpp',
'module_class': mod,
'interface_implementation_id': impl['id']
})
if_tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(
impl_hpp_blocks, output_path / impl_hpp_file, update_flag)
if_tmpl_data['info']['license_header'] = license_header
# FIXME (aw): time stamp should include parent interfaces modification dates
mod_files['interfaces'].append({
'abbr': f'{impl["id"]}.hpp',
'path': output_path / impl_hpp_file,
'printable_name': impl_hpp_file,
'content': templates['interface_impl.hpp'].render(if_tmpl_data),
'template_path': Path(templates['interface_impl.hpp'].filename),
'last_mtime': last_mtime,
'license_header': license_header
})
mod_files['interfaces'].append({
'abbr': f'{impl["id"]}.cpp',
'path': output_path / impl_cpp_file,
'printable_name': impl_cpp_file,
'content': templates['interface_impl.cpp'].render(if_tmpl_data),
'template_path': Path(templates['interface_impl.cpp'].filename),
'last_mtime': last_mtime,
'license_header': license_header
})
cmakelists_file = output_path / 'CMakeLists.txt'
tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(cmakelists_blocks, cmakelists_file, update_flag)
tmpl_data['info']['license_header'] = license_header
mod_files['core'].append({
'abbr': 'cmakelists',
'path': cmakelists_file,
'content': templates['cmakelists'].render(tmpl_data),
'template_path': Path(templates['cmakelists'].filename),
'last_mtime': mod_path.stat().st_mtime
})
# module.hpp
tmpl_data['info']['hpp_guard'] = helpers.snake_case(mod).upper() + '_HPP'
mod_hpp_file = output_path / f'{mod}.hpp'
tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(mod_hpp_blocks, mod_hpp_file, update_flag)
mod_files['core'].append({
'abbr': 'module.hpp',
'path': mod_hpp_file,
'content': templates['module.hpp'].render(tmpl_data),
'template_path': Path(templates['module.hpp'].filename),
'last_mtime': mod_path.stat().st_mtime,
'license_header': license_header
})
# module.cpp
mod_cpp_file = output_path / f'{mod}.cpp'
mod_files['core'].append({
'abbr': 'module.cpp',
'path': mod_cpp_file,
'content': templates['module.cpp'].render(tmpl_data),
'template_path': Path(templates['module.cpp'].filename),
'last_mtime': mod_path.stat().st_mtime,
'license_header': license_header
})
# docs/index.rst
mod_files['docs'].append({
'abbr': 'index.rst',
'path': output_path / 'docs' / 'index.rst',
'content': templates['index.rst'].render(tmpl_data),
'template_path': Path(templates['index.rst'].filename),
'last_mtime': mod_path.stat().st_mtime
})
for file_info in [*mod_files['core'], *mod_files['interfaces'], *mod_files['docs']]:
file_info['printable_name'] = file_info['path'].relative_to(output_path)
return mod_files
def load_interface_definition(interface):
if_path = helpers.resolve_everest_dir_path(f'interfaces/{interface}.yaml')
if_def = helpers.load_validated_interface_def(if_path, validators['interface'])
if 'vars' not in if_def:
if_def['vars'] = {}
if 'cmds' not in if_def:
if_def['cmds'] = {}
last_mtime = if_path.stat().st_mtime
return if_def, last_mtime
def generate_interface_headers(interface, all_interfaces_flag, output_dir):
if_parts = {'base': None, 'exports': None, 'types': None}
try:
if_def, last_mtime = load_interface_definition(interface)
except Exception as e:
if not all_interfaces_flag:
raise
else:
# FIXME (aw): should we really silently ignore that?
print(f'Ignoring interface {interface} with reason: {e}')
return
tmpl_data = generate_tmpl_data_for_if(interface, if_def, False)
output_path = output_dir / interface
output_path.mkdir(parents=True, exist_ok=True)
tmpl_data['info']['interface_name'] = f'{interface}'
tmpl_data['info']['namespace'] = [f'{interface}']
# generate Base file (providers view)
tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_IMPLEMENTATION_HPP'
tmpl_data['info']['class_name'] = f'{interface}ImplBase'
base_file = output_path / 'Implementation.hpp'
if_parts['base'] = {
'path': base_file,
'content': templates['interface_base'].render(tmpl_data),
'template_path': Path(templates['interface_base'].filename),
'last_mtime': last_mtime,
'printable_name': base_file.relative_to(output_path.parent)
}
# generate Exports file (users view)
tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_INTERFACE_HPP'
tmpl_data['info']['class_name'] = f'{interface}Intf'
exports_file = output_path / 'Interface.hpp'
if_parts['exports'] = {
'path': exports_file,
'content': templates['interface_exports'].render(tmpl_data),
'template_path': Path(templates['interface_exports'].filename),
'last_mtime': last_mtime,
'printable_name': exports_file.relative_to(output_path.parent)
}
# generate Types file
tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_TYPES_HPP'
types_file = output_path / 'Types.hpp'
if_parts['types'] = {
'path': types_file,
'content': templates['types.hpp'].render(tmpl_data),
'template_path': Path(templates['types.hpp'].filename),
'last_mtime': last_mtime,
'printable_name': types_file.relative_to(output_path.parent)
}
return if_parts
def module_create(args):
create_strategy = 'force-create' if args.force else 'create'
detected_projects = helpers.detect_everest_projects(args.everest_projects, args.build_dir)
if detected_projects:
helpers.everest_dirs.extend(detected_projects)
mod_files = generate_module_files(args.module, False, args.licenses)
if args.only == 'which':
helpers.print_available_mod_files(mod_files)
return
else:
try:
helpers.filter_mod_files(args.only, mod_files)
except Exception as err:
print(err)
return
for file_info in mod_files['core'] + mod_files['interfaces'] + mod_files['docs']:
if not args.disable_clang_format:
helpers.clang_format(args.clang_format_file, file_info)
helpers.write_content_to_file(file_info, create_strategy, args.diff)
def module_update(args):
detected_projects = helpers.detect_everest_projects(args.everest_projects, args.build_dir)
if detected_projects:
helpers.everest_dirs.extend(detected_projects)
# Always generate type info before updating module
for type_with_namespace in list_types_with_namespace():
_tmpl_data, _last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types=True)
primary_update_strategy = 'force-update' if args.force else 'update'
update_strategy = {'module.cpp': 'update-if-non-existent'}
for file_name in ['cmakelists', 'module.hpp']:
update_strategy[file_name] = primary_update_strategy
# FIXME (aw): refactor out this only handling and rename it properly
mod_files = generate_module_files(args.module, True, args.licenses)
if args.only == 'which':
helpers.print_available_mod_files(mod_files)
return
else:
try:
helpers.filter_mod_files(args.only, mod_files)
except Exception as err:
print(err)
return
if not args.disable_clang_format:
for file_info in mod_files['core'] + mod_files['interfaces']:
helpers.clang_format(args.clang_format_file, file_info)
for file_info in mod_files['core']:
helpers.write_content_to_file(file_info, update_strategy[file_info['abbr']], args.diff, '', True)
for file_info in mod_files['interfaces']:
if file_info['abbr'].endswith('.hpp'):
helpers.write_content_to_file(file_info, primary_update_strategy, args.diff, '', True)
else:
helpers.write_content_to_file(file_info, 'update-if-non-existent', args.diff, '', True)
def module_genld(args):
output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
'build/generated/generated/modules'
primary_update_strategy = 'force-update' if args.force else 'update'
loader_files = generate_module_loader_files(args.module, output_dir)
if not args.disable_clang_format:
for file_info in loader_files:
helpers.clang_format(args.clang_format_file, file_info)
for file_info in loader_files:
helpers.write_content_to_file_and_check_template(file_info, primary_update_strategy)
def module_get_templates(args):
interface_files = args.separator.join(
[templates['ld-ev.hpp'].filename,
templates['ld-ev.cpp'].filename])
print(f'{interface_files}')
def interface_genhdr(args):
# Always generate type info before generating interfaces
for type_with_namespace in list_types_with_namespace():
_tmpl_data, _last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types=True)
output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
'build/generated/include/generated/interfaces'
primary_update_strategy = 'force-update' if args.force else 'update'
interfaces = args.interfaces
all_interfaces = False
if not interfaces:
all_interfaces = True
interfaces = []
for everest_dir in everest_dirs:
if_dir = everest_dir / 'interfaces'
interfaces += [if_path.stem for if_path in if_dir.iterdir() if (if_path.is_file()
and if_path.suffix == '.yaml')]
for interface in interfaces:
if_parts = generate_interface_headers(interface, all_interfaces, output_dir)
if not args.disable_clang_format:
# FIXME (aw): this broken, because in case all_interfaces is true, if_parts might be none for invalid interface files
helpers.clang_format(args.clang_format_file, if_parts['base'])
helpers.clang_format(args.clang_format_file, if_parts['exports'])
helpers.clang_format(args.clang_format_file, if_parts['types'])
helpers.write_content_to_file_and_check_template(if_parts['base'], primary_update_strategy, args.diff)
helpers.write_content_to_file_and_check_template(if_parts['exports'], primary_update_strategy, args.diff)
helpers.write_content_to_file_and_check_template(if_parts['types'], primary_update_strategy, args.diff)
def interface_get_templates(args):
interface_files = args.separator.join(
[templates['interface_base'].filename,
templates['interface_exports'].filename])
print(f'{interface_files}')
def helpers_genuuids(args):
if (args.count <= 0):
raise Exception(f'Invalid number ("{args.count}") of uuids to generate')
helpers.generate_some_uuids(args.count)
def helpers_yaml2json(args):
helpers.yaml2json(Path(args.input).resolve(), Path(args.output).resolve())
def helpers_json2yaml(args):
helpers.json2yaml(Path(args.input).resolve(), Path(args.output).resolve())
def list_types_with_namespace(types=None) -> List:
if not types:
types = []
for everest_dir in everest_dirs:
types_dir = everest_dir / 'types'
types += list(types_dir.glob('**/*.yaml'))
types_with_namespace = []
for type_path in types:
relative_path = None
for everest_dir in everest_dirs:
types_dir = everest_dir / 'types'
try:
relative_path = type_path.relative_to(types_dir).with_suffix('')
break
except ValueError:
pass
uppercase_path = []
for part in relative_path.parts:
uppercase_path.append(stringcase.capitalcase(part))
namespace = '::'.join(relative_path.parts)
type_with_namespace = {
'path': type_path,
'relative_path': relative_path,
'namespace': namespace,
'uppercase_path': uppercase_path,
}
types_with_namespace.append(type_with_namespace)
return types_with_namespace
def types_genhdr(args):
print('Generating global type headers.')
output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
'build/generated/generated/types'
primary_update_strategy = 'force-update' if args.force else 'update'
types = None
all_types = False
if 'types' not in args:
all_types = True
else:
types = args.types
types_with_namespace = list_types_with_namespace(types=types)
for type_with_namespace in types_with_namespace:
type_parts = TypeParser.generate_type_headers(type_with_namespace, all_types, output_dir)
if not args.disable_clang_format:
helpers.clang_format(args.clang_format_file, type_parts['types'])
helpers.write_content_to_file_and_check_template(type_parts['types'], primary_update_strategy, args.diff)
def types_get_templates(args):
interface_files = templates['types.hpp'].filename
print(f'{interface_files}')
def main():
global validators, everest_dirs, work_dir
everest_dir = Path(__file__).parent.parent.parent.parent.parent.parent
everest_framework_dir = everest_dir / 'lib' / 'everest' / 'framework'
schemas_dir = everest_framework_dir / 'schemas'
everest_dir_default = [str(everest_dir), str(Path.cwd().parent / 'EVerest')]
parser = argparse.ArgumentParser(description='Everest command line tool')
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
common_parser = argparse.ArgumentParser(add_help=False)
common_parser.add_argument('--work-dir', '-wd', type=str,
help='work directory containing the manifest definitions (default: .)', default=str(Path.cwd()))
common_parser.add_argument('--everest-dir', '-ed', nargs='*',
help=f'everest directory containing the interface definitions (default: {everest_dir_default})',
default=everest_dir_default)
common_parser.add_argument('--everest-projects', '-ep', nargs='*',
help='everest project names. used in auto detection of their directories to get eg. interface defintions (default: EVerest)',
default=['EVerest'])
common_parser.add_argument('--schemas-dir', '-sd', type=str,
help=f'everest framework directory containing the schema definitions (default: {schemas_dir})',
default=str(schemas_dir))
common_parser.add_argument('--licenses', '-lc', type=str,
help='license directory from which ev-cli will attempt to parse custom license texts (default ../licenses)',
default=str(Path.cwd() / '../licenses'))
common_parser.add_argument('--build-dir', '-bd', type=str,
help='everest build directory from which ev-cli will attempt to parse the everest framework schema definitions (default ./build)',
default=str(Path.cwd() / 'build'))
common_parser.add_argument('--clang-format-file', type=str, default=str(Path.cwd()),
help='Path to the directory, containing the .clang-format file (default: .)')
common_parser.add_argument('--disable-clang-format', action='store_true', default=False,
help='Set this flag to disable clang-format')
subparsers = parser.add_subparsers(metavar='<command>', help='available commands', required=True)
parser_mod = subparsers.add_parser('module', aliases=['mod'], help='module related actions')
parser_if = subparsers.add_parser('interface', aliases=['if'], help='interface related actions')
parser_hlp = subparsers.add_parser('helpers', aliases=['hlp'], help='helper actions')
parser_types = subparsers.add_parser('types', aliases=['ty'], help='type related actions')
mod_actions = parser_mod.add_subparsers(metavar='<action>', help='available actions', required=True)
mod_create_parser = mod_actions.add_parser('create', aliases=['c'], parents=[
common_parser], help='create module(s)')
mod_create_parser.add_argument('module', type=str, help='name of the module, that should be created')
mod_create_parser.add_argument('-f', '--force', action='store_true', help='force overwriting - use with care!')
mod_create_parser.add_argument('-d', '--diff', '--dry-run', action='store_true',
help='show resulting diff on create or overwrite')
mod_create_parser.add_argument('--only', type=str,
help='Comma separated filter list of module files, that should be created. '
'For a list of available files use "--only which".')
mod_create_parser.set_defaults(action_handler=module_create)
mod_update_parser = mod_actions.add_parser('update', aliases=['u'], parents=[
common_parser], help='update module(s)')
mod_update_parser.add_argument('module', type=str, help='name of the module, that should be updated')
mod_update_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
mod_update_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
mod_update_parser.add_argument('--only', type=str,
help='Comma separated filter list of module files, that should be updated. '
'For a list of available files use "--only which".')
mod_update_parser.set_defaults(action_handler=module_update)
mod_genld_parser = mod_actions.add_parser(
'generate-loader', aliases=['gl'], parents=[common_parser], help='generate everest loader')
mod_genld_parser.add_argument(
'module', type=str, help='name of the module, for which the loader should be generated')
mod_genld_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
mod_genld_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated loader '
'files (default: {everest-dir}/build/generated/generated/modules)')
mod_genld_parser.set_defaults(action_handler=module_genld)
if_actions = parser_if.add_subparsers(metavar='<action>', help='available actions', required=True)
if_genhdr_parser = if_actions.add_parser(
'generate-headers', aliases=['gh'], parents=[common_parser], help='generate headers')
if_genhdr_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
if_genhdr_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated interface '
'headers (default: {everest-dir}/build/generated/generated/interfaces)')
if_genhdr_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
if_genhdr_parser.add_argument('interfaces', nargs='*', help='a list of interfaces, for which header files should '
'be generated - if no interface is given, all will be processed and non-processable '
'will be skipped')
if_genhdr_parser.set_defaults(action_handler=interface_genhdr)
hlp_actions = parser_hlp.add_subparsers(metavar='<action>', help='available actions', required=True)
hlp_genuuid_parser = hlp_actions.add_parser('generate-uuids', help='generate uuids')
hlp_genuuid_parser.add_argument('count', type=int, default=3)
hlp_genuuid_parser.set_defaults(action_handler=helpers_genuuids)
hlp_yaml2json_parser = hlp_actions.add_parser('yaml2json', help='convert yaml into json')
hlp_yaml2json_parser.add_argument('input', type=str, help='path to yaml input file')
hlp_yaml2json_parser.add_argument('output', type=str, help='path to json output file')
hlp_yaml2json_parser.set_defaults(action_handler=helpers_yaml2json)
hlp_json2yaml_parser = hlp_actions.add_parser('json2yaml', help='convert json into yaml')
hlp_json2yaml_parser.add_argument('input', type=str, help='path to json input file')
hlp_json2yaml_parser.add_argument('output', type=str, help='path to yaml output file')
hlp_json2yaml_parser.set_defaults(action_handler=helpers_json2yaml)
types_actions = parser_types.add_subparsers(metavar='<action>', help='available actions', required=True)
types_genhdr_parser = types_actions.add_parser(
'generate-headers', aliases=['gh'], parents=[common_parser], help='generate type headers')
types_genhdr_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
types_genhdr_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated type '
'headers (default: {everest-dir}/build/generated/generated/types)')
types_genhdr_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
types_genhdr_parser.add_argument('types', nargs='*', help='a list of types, for which header files should '
'be generated - if no type is given, all will be processed and non-processable '
'will be skipped')
types_genhdr_parser.set_defaults(action_handler=types_genhdr)
for sub_parser, get_template_function in [
(mod_actions, module_get_templates),
(if_actions, interface_get_templates),
(types_actions, types_get_templates)
]:
get_templates_parser = sub_parser.add_parser(
'get-templates', aliases=['gt'], parents=[common_parser], help='get paths to template files')
get_templates_parser.add_argument(
'-s', '--separator', type=str, default='\n', help='separator between template files')
get_templates_parser.set_defaults(action_handler=get_template_function)
args = parser.parse_args()
if 'everest_dir' in args:
# FIXME (aw): the helper commands do not set everest_dir, work_dir and schema_dirs, but the following common
# code has to run for all other commands - we need some better check here than just checking for
# 'everest_dir' in args!
for entry in args.everest_dir:
everest_dir = Path(entry).resolve()
everest_dirs.append(everest_dir)
helpers.everest_dirs = everest_dirs
work_dir = Path(args.work_dir).resolve()
setup_jinja_env()
schemas_dir = Path(args.schemas_dir).resolve()
if not schemas_dir.exists():
print(f'The default ("{schemas_dir}") xor supplied (via --schemas-dir) schemas directory'
' doesn\'t exist.\n'
f'dir: {schemas_dir}')
cmake_cache_path = Path(args.build_dir) / 'CMakeCache.txt'
found_dir = helpers.get_path_from_cmake_cache('everest-framework', cmake_cache_path, '--schemas-dir')
if not found_dir:
exit(1)
schemas_dir = found_dir / 'schemas'
if not schemas_dir.exists():
exit(1)
validators = helpers.load_validators(schemas_dir)
TypeParser.validators = validators
TypeParser.templates = templates
ErrorParser.validators = validators
args.action_handler(args)
if __name__ == '__main__':
try:
main()
except helpers.EVerestParsingException as e:
raise SystemExit(e)

View File

@@ -0,0 +1,817 @@
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
"""
author: aw@pionix.de
FIXME (aw): Module documentation.
"""
from .type_parsing import TypeParser
from pathlib import Path
import shutil
import subprocess
import re
from typing import Dict, List, Tuple
import keyword
import json
import jsonschema
import yaml
from uuid import uuid4
import stringcase
everest_dirs: List[Path] = []
class EVerestParsingException(SystemExit):
pass
def snake_case(word: str) -> str:
"""Convert capital case to snake case
Only alphanumerical characters are allowed. Only inserts camelcase
between a consecutive lower and upper alphabetical character and
lowers first letter
"""
out = ''
if len(word) == 0:
return out
cur_char: str = ''
for i in range(len(word)):
if i == 0:
cur_char = word[i]
if not cur_char.isalnum():
raise Exception('Non legal character in: ' + word)
out += cur_char.lower()
continue
last_char: str = cur_char
cur_char = word[i]
if (last_char.islower() and last_char.isalpha() and cur_char.isupper() and cur_char.isalpha):
out += '_'
if not cur_char.isalnum():
out += '_'
else:
out += cur_char.lower()
return out
def create_dummy_result(json_type) -> str:
def primitive_to_sample_value(type):
if type == 'boolean':
return 'true'
elif type == 'integer':
return '42'
elif type == 'number':
return '3.14'
elif type == 'string':
return '"everest"'
elif type == 'object':
return '{}'
elif type == 'array':
return '{}'
else:
raise Exception(f'This json type "{type}" is not known or not implemented')
if isinstance(json_type, list):
return '{}' # default initialization for variant
else:
return primitive_to_sample_value(json_type)
cpp_type_map = {
'null': 'std::nullptr_t', # FIXME (aw): who gets the null, json? or the variant
'integer': 'int',
'number': 'double',
'string': 'std::string',
'boolean': 'bool',
'array': 'Array',
'object': 'Object',
}
def clang_format(config_file_path, file_info):
# check if we handle cpp and hpp files
if not file_info['path'].suffix in ('.hpp', '.cpp'):
return
clang_format_path = shutil.which('clang-format')
if clang_format_path is None:
raise RuntimeError('Could not find clang-format executable - needed when passing clang-format config file')
config_file_path = Path(config_file_path)
if not config_file_path.is_dir():
raise RuntimeError(f'Supplied directory for the clang-format file ({config_file_path}) does not exist')
if not (config_file_path / '.clang-format').exists():
raise RuntimeError(f'Supplied directory for the clang-format file '
f'({config_file_path}) does not contain a .clang-format file')
content = file_info['content']
run_parms = {'capture_output': True, 'cwd': config_file_path, 'encoding': 'utf-8', 'input': content}
format_cmd = subprocess.run([clang_format_path, '--style=file'], **run_parms)
if format_cmd.returncode != 0:
raise RuntimeError(f'clang-format failed with:\n{format_cmd.stderr}')
file_info['content'] = format_cmd.stdout
def resolve_everest_dir_path(postfix):
resolved_path = None
for everest_dir in everest_dirs:
path = everest_dir / postfix
if path.exists():
resolved_path = path
break
if not resolved_path:
raise EVerestParsingException(
f'Could not resolve "{postfix}" in any of the provided everest-dir ({everest_dirs}).')
return resolved_path
def build_type_info(name, json_type):
ti = {
'name': name,
'is_variant': False,
'cpp_type': None,
'json_type': json_type
}
if isinstance(json_type, list):
ti['is_variant'] = True
ti['cpp_type'] = [cpp_type_map[e] for e in json_type if e != 'null']
ti['cpp_type'].sort() # sort, so template generation might get reduced
# prepend boost::blank if type 'null' exists, so the variant
# gets default initialized with blank
if 'null' in json_type:
ti['cpp_type'].insert(0, cpp_type_map['null'])
else:
ti['cpp_type'] = cpp_type_map[json_type]
return ti
type_headers = set()
parsed_types: List = []
parsed_enums: List = []
current_defs: Dict = {}
format_types = dict()
# format_types['date-time'] = 'DateTime'
def object_exists(name: str) -> bool:
"""Check if an object already exists."""
for el in parsed_types:
if el['name'] == name:
return True
return False
def add_enum_type(name: str, enums: Tuple[str], description: str):
"""Add enum type to parsed_types."""
for el in parsed_enums:
if el['name'] == name:
raise Exception('Warning: enum ' + name + ' already exists')
parsed_enums.append({
'name': name,
'enums': enums,
'description': description
})
def parse_ref(ref: str, prop_type, prop_info: Dict) -> Tuple[str, dict]:
if ref not in TypeParser.all_types:
TypeParser.all_types[ref] = TypeParser.parse_type_url(type_url=ref)
type_dict = TypeParser.all_types[ref]
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
if not type_path or not type_path.exists():
raise EVerestParsingException('$ref: ' + ref + f' referenced type file "{type_path}" does not exist.')
(td, _mod) = TypeParser.load_type_definition(type_path)
if 'types' in td and type_dict['type_name'] in td['types']:
local_type_info = td['types'][type_dict['type_name']]
if local_type_info['type'] == 'string' and 'enum' in local_type_info:
prop_info['enum'] = True
prop_type = type_dict['namespaced_type']
prop_info['prop']['type'] = prop_type
prop_info['type_dict'] = type_dict
path = Path('generated/types') / \
type_dict['type_relative_path'].with_suffix('.hpp')
type_headers.add(path.as_posix())
return (prop_type, prop_info)
def parse_property(prop_name: str, prop: Dict, depends_on: List[str], type_file: bool) -> Tuple[str, dict]:
"""Determine type of property and proceed with it.
In case it is a $ref, look it up in the TypeParser
Currently, the following property types are supported:
- string (and enum as a special case)
- integer
- number
- boolean
- array
- object (will be parsed recursivly)
"""
prop_type = None
prop_info = {
'description': prop.get('description', 'TODO: description'),
'prop': prop,
'enum': False
}
if '$ref' in prop:
return parse_ref(prop['$ref'], prop_type, prop_info)
if 'type' not in prop:
raise EVerestParsingException(f'{prop_name} does not contain a type property')
if prop['type'] == 'string':
if 'enum' in prop and type_file:
prop_type = stringcase.capitalcase(prop_name)
add_enum_type(prop_type, prop['enum'], prop_info['description'])
elif 'format' in prop:
if prop['format'] in format_types:
prop_type = format_types[prop['format']]
else:
# unsupported format type
prop_type = 'std::string'
prop_info['unsupported_format'] = True
else:
prop_type = 'std::string'
elif prop['type'] == 'integer':
prop_type = 'int32_t'
elif prop['type'] == 'number':
prop_type = 'float'
elif prop['type'] == 'boolean':
prop_type = 'bool'
elif prop['type'] == 'array':
if 'items' in prop:
prop_type = 'std::vector<' + parse_property(prop_name, prop['items'], depends_on, type_file)[0] + '>'
else:
raise EVerestParsingException(f'Property items of array {prop_name} does not contain a type property')
elif prop['type'] == 'object':
prop_type = stringcase.capitalcase(prop_name)
depends_on.append(prop_type)
if not object_exists(prop_type):
parse_object(prop_type, prop, type_file)
else:
raise Exception('Unknown type: ' + prop['type'])
return (prop_type, prop_info)
def parse_object(ob_name: str, json_schema: Dict, type_file: bool):
"""Parse a JSON object.
Iterates over the properties of this object, parses their type
and puts these information into the global dict parsed_types.
"""
ob_dict = {'name': ob_name, 'properties': [], 'depends_on': []}
parsed_types.insert(0, ob_dict)
if 'properties' not in json_schema:
# object has no properties, probably not a complex object
if '$ref' in json_schema:
if json_schema['$ref'] not in TypeParser.all_types:
TypeParser.all_types[json_schema['$ref']] = TypeParser.parse_type_url(type_url=json_schema['$ref'])
type_dict = TypeParser.all_types[json_schema['$ref']]
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'].with_suffix('.yaml'))
if not type_path or not type_path.exists():
raise EVerestParsingException(
'$ref: ' + json_schema['$ref'] + f' referenced type file "{type_path}" does not exist.')
TypeParser.does_type_exist(type_url=json_schema['$ref'], json_type=json_schema['type'])
prop_type = type_dict['namespaced_type']
ob_dict['name'] = prop_type
path = Path('generated/types') / \
type_dict['type_relative_path'].with_suffix('.hpp')
type_headers.add(path.as_posix())
return ob_dict
return
if not type_file:
return
for prop_name, prop in json_schema['properties'].items():
if not prop_name.isidentifier() or keyword.iskeyword(prop_name):
raise Exception(prop_name + ' can\'t be used as an identifier!')
(prop_type, prop_info) = parse_property(prop_name, prop, ob_dict['depends_on'], type_file)
ob_dict['properties'].append({
'name': prop_name,
'json_name': prop_name,
'type': prop_type,
'info': prop_info,
'enum': 'enum' in prop or prop_info['enum'],
'required': prop_name in json_schema.get('required', {}),
})
ob_dict['properties'].sort(key=lambda x: x.get('required'), reverse=True)
return ob_dict
def generate_header_for_type(type_name: str) -> Path:
return (Path('generated/types') / type_name).with_suffix('.hpp')
def extended_build_type_info(name: str, info: dict, type_file=False) -> Tuple[dict, dict]:
"""Extend build_type_info with enum and object type handling."""
type_info = build_type_info(name, info['type'])
enum_info = None
if type_info['json_type'] == 'string':
if 'enum' in info and type_file:
enum_info = {
'name': name,
'description': info.get('description', 'TODO: description'),
'enum_type': stringcase.capitalcase(name),
'enum': info['enum']
}
type_info['enum_type'] = enum_info['enum_type']
elif '$ref' in info:
if info['$ref'] not in TypeParser.all_types:
TypeParser.all_types[info['$ref']] = TypeParser.parse_type_url(type_url=info['$ref'])
type_dict = TypeParser.all_types[info['$ref']]
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
if not type_path or not type_path.exists():
raise EVerestParsingException('$ref: ' + info['$ref'] +
f' referenced type file "{type_path}" does not exist.')
(td, _mod) = TypeParser.load_type_definition(type_path)
if 'types' in td and type_dict['type_name'] in td['types']:
local_type_info = td['types'][type_dict['type_name']]
if local_type_info['type'] == 'string' and 'enum' in local_type_info:
enum_info = {
'name': name,
'description': local_type_info.get('description', 'TODO: description'),
'enum_type': type_dict['namespaced_type'],
'enum': local_type_info['enum']
}
type_info['enum_type'] = enum_info['enum_type']
path = generate_header_for_type(type_dict['type_relative_path'])
type_headers.add(path.as_posix())
elif type_info['json_type'] == 'object':
try:
ob = parse_object(name, info, type_file)
if ob and 'name' in ob:
type_info['object_type'] = ob['name']
except EVerestParsingException as e:
raise EVerestParsingException(f'Error parsing object {name}: {e}')
elif type_info['json_type'] == 'array':
if '$ref' in info['items']:
if info['items']['$ref'] not in TypeParser.all_types:
TypeParser.all_types[info['items']['$ref']] = TypeParser.parse_type_url(type_url=info['items']['$ref'])
type_dict = TypeParser.all_types[info['items']['$ref']]
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
if not type_path or not type_path.exists():
raise EVerestParsingException(
'$ref: ' + info['items']['$ref'] + f' referenced type file "{type_path}" does not exist.')
(td, _mod) = TypeParser.load_type_definition(type_path)
if 'types' in td and type_dict['type_name'] in td['types']:
local_type_info = td['types'][type_dict['type_name']]
if 'enum' in local_type_info:
type_info['array_type_contains_enum'] = True
type_info['array_type'] = type_dict['namespaced_type']
path = generate_header_for_type(type_dict['type_relative_path'])
type_headers.add(path.as_posix())
return (type_info, enum_info)
def load_validators(schema_path: Path):
# FIXME (aw): we should also patch the schemas like in everest-framework
validators = {}
for validator, filename in zip(
['interface', 'module', 'config', 'type', 'error_declaration_list'],
['interface', 'manifest', 'config', 'type', 'error-declaration-list']):
try:
schema = yaml.safe_load((schema_path / f'{filename}.yaml').read_text())
jsonschema.Draft7Validator.check_schema(schema)
validators[validator] = jsonschema.Draft7Validator(schema)
except OSError as err:
print(f'Could not open schema file {err.filename}: {err.strerror}')
exit(1)
except jsonschema.SchemaError as err:
print(f'Schema error in schema file {filename}.yaml')
raise
except yaml.YAMLError as err:
raise Exception(f'Could not parse interface definition file {schema_path}') from err
return validators
def load_validated_interface_def(if_def_path: Path, validator):
if_def = {}
try:
if_def = yaml.safe_load(if_def_path.read_text())
# validating interface
validator.validate(if_def)
# validate var/cmd subparts
if 'vars' in if_def:
for _var_name, var_def in if_def['vars'].items():
jsonschema.Draft7Validator.check_schema(var_def)
if 'cmds' in if_def:
for _cmd_name, cmd_def in if_def['cmds'].items():
if 'arguments' in cmd_def:
for _arg_name, arg_def in cmd_def['arguments'].items():
jsonschema.Draft7Validator.check_schema(arg_def)
if 'result' in cmd_def:
jsonschema.Draft7Validator.check_schema(cmd_def['result'])
except OSError as err:
raise Exception(f'Could not open interface definition file {err.filename}: {err.strerror}') from err
except jsonschema.ValidationError as err:
raise Exception(f'Validation error in interface definition file {if_def_path}: {err}') from err
except yaml.YAMLError as err:
raise Exception(f'Could not parse interface definition file {if_def_path}') from err
return if_def
def load_validated_type_def(type_def_path: Path, validator):
"""Load a type definition from the provided path and validate it with the provided validator."""
try:
type_def = yaml.safe_load(type_def_path.read_text())
# validating type definition
validator.validate(type_def)
return type_def
except OSError as err:
raise Exception(f'Could not open type definition file {err.filename}: {err.strerror}') from err
except jsonschema.ValidationError as err:
raise Exception(f'Validation error in type definition file {type_def_path}') from err
except yaml.YAMLError as err:
raise Exception(f'Could not parse interface definition file {type_def_path}') from err
return type_def
def load_validated_module_def(module_path: Path, validator):
try:
module_def = yaml.safe_load(module_path.read_text())
validator.validate(module_def)
except OSError as err:
raise Exception(f'Could not open type definition file {err.filename}: {err.strerror}') from err
except jsonschema.ValidationError as err:
raise Exception(f'Validation error in module definition file {module_path}') from err
except yaml.YAMLError as err:
raise Exception(f'Could not parse interface definition file {module_path}') from err
return module_def
def generate_some_uuids(count):
for i in range(count):
print(uuid4())
def yaml2json(yaml_file: Path, json_file: Path):
if not yaml_file.exists():
print(f'The input file ({yaml_file}) does not exist')
exit(1)
with open(yaml_file, 'r') as yaml_content:
content_as_dict = yaml.safe_load(yaml_content)
with open(json_file, 'w') as json_content:
json.dump(content_as_dict, json_content, indent=2)
def json2yaml(json_file: Path, yaml_file: Path):
if not json_file.exists():
print(f'The input file ({json_file}) does not exist')
exit(1)
with open(json_file, 'r') as json_content:
content_as_dict = json.load(json_content)
with open(yaml_file, 'w') as yaml_content:
yaml.safe_dump(content_as_dict, yaml_content, indent=2, sort_keys=False, width=120)
def __check_for_match(blocks_def, line, line_no, file_path):
match = re.search(blocks_def['regex_str'], line)
if not match:
return None
# mb = match_block
mb = {
'id': match.group('uuid'),
'version': match.group('version'),
'tag': blocks_def['format_str'].format(
uuid=match.group('uuid'),
version=match.group('version')
)
}
# check if uuid and version exists
if blocks_def['version'] != mb['version']:
raise ValueError(
f'Error while parsing {file_path}:\n'
f' matched line {line_no}: {line}\n'
f' contains version "{mb["version"]}", which is different from the blocks definition version "{blocks_def["version"]}"'
)
for block, block_info in blocks_def['definitions'].items():
if block_info['id'] != mb['id']:
continue
mb['name'] = block
mb['block'] = block_info
if not 'block' in mb:
raise ValueError(
f'Error while parsing {file_path}:\n'
f' matched line {line_no}: {line}\n'
f' contains uuid "{mb["id"]}", which doesn\'t exist in the block definition'
)
return mb
def generate_tmpl_blocks(blocks_def, file_path=None):
tmpl_block = {}
for block_name, block_def in blocks_def['definitions'].items():
tmpl_block[block_name] = {
'tag': blocks_def['format_str'].format(
uuid=block_def['id'],
version=blocks_def['version']
),
'content': block_def['content'],
'first_use': True
}
if not file_path:
return tmpl_block
try:
file_data = file_path.read_text()
except OSError as err:
print(f'Could not open file {err.filename} for parsing blocks: {err.strerror}')
exit(1)
line_no = 0
matched_block = None
content = None
for line in file_data.splitlines(True):
line_no += 1
if not matched_block:
matched_block = __check_for_match(blocks_def, line.rstrip(), line_no, file_path)
content = None
continue
if (line.strip() == matched_block['tag']):
if (content):
tmpl_block[matched_block['name']]['content'] = content.rstrip()
tmpl_block[matched_block['name']]['first_use'] = False
matched_block = None
else:
content = (content + line) if content else line
if matched_block:
raise ValueError(
f'Error while parsing {file_path}:\n'
f' matched tag line {matched_block["tag"]}\n'
f' could not find closing tag'
)
return tmpl_block
def load_tmpl_blocks(blocks_def, file_path, update):
if update and file_path.exists():
return generate_tmpl_blocks(blocks_def, file_path)
else:
return generate_tmpl_blocks(blocks_def)
def __show_diff_for(file_info):
diff_path = shutil.which('diff')
if diff_path == None:
raise Exception('Can\'t generate diff, because "diff" executable not found')
file_path = file_info['path']
diff_ignore = ''
if file_path.suffix in ('.hpp', '.cpp'):
diff_ignore = '^//.*'
elif file_path.name == 'CMakeLists.txt':
diff_ignore = '^#.*'
diff_ignore_args = ['-I', diff_ignore] if diff_ignore else []
run_parms = {'input': file_info['content'], 'capture_output': True, 'encoding': 'utf-8'}
diff = subprocess.run([
diff_path,
'-ruN',
*diff_ignore_args,
'--label', file_info['printable_name'],
'--color=always',
file_path,
'-'
], **run_parms).stdout
if diff:
print(diff)
def filter_mod_files(only, mod_files):
if not only:
return
filter_files = set([c.strip() for c in only.split(',')])
not_filtered_files = filter_files.copy()
# first check if all selected file filters are valid
for category_files in mod_files.values():
for file_info in category_files:
if file_info['abbr'] in not_filtered_files:
not_filtered_files.remove(file_info['abbr'])
if not_filtered_files:
raise Exception(f'Unknown file filters for --only option: {not_filtered_files}\n'
'Use "--only which" to show available file filters')
# now do the filtering
for category, category_files in mod_files.items():
mod_files[category] = list(filter(lambda x: x['abbr'] in filter_files, category_files))
def print_available_mod_files(mod_files):
for category, category_files in mod_files.items():
print(f'Available files for category "{category}"')
for file_info in category_files:
print(f' {file_info["abbr"]}')
def get_mtime(filename: str | Path) -> float:
if isinstance(filename, str):
filename = Path(filename)
return filename.stat().st_mtime
def is_template_newer(file_info) -> Tuple[bool, str]:
template_path = file_info['template_path']
generated_path = file_info['path']
if not generated_path.exists():
return (True, ' (Generated file did not exist)')
if get_mtime(template_path) > get_mtime(generated_path):
return (True, ' (Template file has changed since last generation)')
return (False, '')
def write_content_to_file(file_info, strategy, only_diff=False, reason = '', check_license_header=False):
# strategy:
# update: update only if dest older or not existent
# force-update: update, even if dest newer
# update-if-non-existent: update only if file does not exists
# create: create file only if it does not exist
# force-create: create file, even if it exists
# FIXME (aw): we should have this as an enum
strategies = ['update', 'force-update', 'update-if-non-existent', 'create', 'force-create']
file_path = file_info['path']
file_dir = file_path.parent
printable_name = file_info['printable_name']
method = ''
if only_diff:
return __show_diff_for(file_info)
if strategy == 'update':
if file_path.exists() and file_path.stat().st_mtime > file_info['last_mtime']:
print(f'Skipping {printable_name} (up-to-date)')
return
method = 'Updating'
elif strategy == 'force-update':
method = 'Force-updating' if file_path.exists() else 'Creating'
elif strategy == 'force-create':
method = 'Overwriting' if file_path.exists() else 'Creating'
elif strategy == 'update-if-non-existent' or strategy == 'create':
if file_path.exists():
print(f'Skipping {printable_name} (use create --force to recreate)')
return
method = 'Creating'
else:
raise Exception(f'Invalid strategy "{strategy}"\nSupported strategies: {strategies}')
print(f'{method} file {printable_name}{reason}')
if not file_dir.exists():
file_dir.mkdir(parents=True, exist_ok=True)
# check if file header is different from license header
if check_license_header:
if 'license_header' in file_info and file_path.exists():
original_content = file_path.read_text()
if not original_content.startswith(file_info['license_header']):
# determine likely end of license header
search_terms = ['#ifndef', '#pragma once', '#include']
original_license_header = ''
for search in search_terms:
index = original_content.find(search)
if index >= 0:
original_license_header = original_content[0:index]
break
print(f'Keeping the existing licence header:\n{original_license_header}')
file_info['content'] = file_info['content'].replace(
file_info['license_header'], original_license_header.strip())
file_path.write_text(file_info['content'])
def write_content_to_file_and_check_template(file_info, strategy, only_diff=False):
# check if template is newer and force-update file if it is
update_strategy = strategy
(newer, reason) = is_template_newer(file_info)
if newer:
update_strategy = 'force-update'
write_content_to_file(file_info, update_strategy, only_diff, reason)
def get_license_header(license_dirs, license_url):
url_schemas = ['http://', 'https://']
for url_schema in url_schemas:
if license_url.startswith(url_schema):
license_url = license_url.replace(url_schema, '', 1)
license_path = None
for license_dir in license_dirs:
check_license_path = license_dir / license_url
print(f'Checking if license "{check_license_path}" exists...')
if check_license_path.exists():
license_path = check_license_path
if not license_path:
return None
with open(license_path, 'r') as custom_license_file:
return custom_license_file.read().strip()
def get_path_from_cmake_cache(variable_prefix, cmake_cache_path, option_name):
print(f'Searching for {variable_prefix} in: {cmake_cache_path}')
print(f'You can either provide the {variable_prefix} directory with {option_name} or influence the'
' automatic search path by setting --build-dir (default: ./build)')
if not cmake_cache_path.exists():
print(f'CMakeCache.txt does not exist: {cmake_cache_path}')
return None
with open(cmake_cache_path, 'r') as cmake_cache_file:
search = f'{variable_prefix}_SOURCE_DIR:STATIC='
for line in cmake_cache_file:
if line.startswith(search):
found_dir = Path(line.replace(search, '', 1).strip(' \t\n\r'))
if found_dir.exists():
print(f'Found {variable_prefix} directory: {found_dir}')
user_choice = input('Do you want to use this? [Y/n] ').lower()
if user_choice == 'y' or not user_choice:
return found_dir
break
return None
def detect_everest_projects(everest_projects, build_dir):
detected_everest_project = False
for everest_dir in everest_dirs:
if everest_dir.exists() and everest_dir.name in everest_projects:
detected_everest_project = True
found_dirs = []
if not detected_everest_project:
print('Could not detect ' + ", ".join(everest_projects) + ' path in --everest-dir')
cmake_cache_path = Path(build_dir) / 'CMakeCache.txt'
for everest_project in everest_projects:
found_dir = get_path_from_cmake_cache(everest_project, cmake_cache_path, '--everest-dir')
if found_dir:
found_dirs.append(found_dir)
return found_dirs

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest

View File

@@ -0,0 +1,19 @@
{% from "helper_macros.j2" import insert_block, print_template_info %}
{{ print_template_info('3', 'marked regions will be kept', '#') }}
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
{{ insert_block(info.blocks.add_general) }}
{% if provides %}
target_sources(${MODULE_NAME}
PRIVATE
{% for impl in provides %}
"{{ impl.cpp_file_rel_path }}"
{% endfor %}
)
{% endif %}
{{ insert_block(info.blocks.add_other) }}

View File

@@ -0,0 +1,127 @@
{% macro cpp_type(type, interface=none, call=false) -%}
{% if type.is_variant -%}
std::variant<{{ type.cpp_type|join(', ') }}>
{%- else -%}
{% if interface is not none and 'object_type' in type %}
{{ interface + '::' + type.object_type }}
{% else %}
{% if call -%}{{'const '}}{%- endif %}
{% if 'object_type' in type -%}
{{ type.object_type }}
{%- elif 'enum_type' in type -%}
{{ type.enum_type }}
{%- elif 'array_type' in type -%}
std::vector<{{ type.array_type }}>
{%- else -%}
{{ type.cpp_type }}
{%- endif %}
{% if call -%}&{%- endif %}
{%- endif %}
{%- endif %}
{%- endmacro %}
{% macro result_type(type, interface=none) -%}
{% if type -%}
{{ cpp_type(type) }}
{%- else -%}
void
{%- endif %}
{%- endmacro %}
{% macro var_to_any(var, name) -%}
{% if var.is_variant -%}
Everest::variant_to_json({{ name }})
{%- else -%}
{{ name }}
{%- endif %}
{%- endmacro %}
{% macro var_to_cpp(arg) -%}
{% if arg.is_variant -%}
Everest::json_to_variant<{{ arg.cpp_type|join(', ')}}>
{%- else -%}
static_cast<{{ arg.cpp_type }}>
{%- endif %}
{%- endmacro %}
{% macro call_cmd_signature(cmd, interface=none) -%}
{{ result_type(cmd.result, interface) }} call_{{ cmd.name }}(
{%- for arg in cmd.args -%}
{{ cpp_type(arg, none, true) }} {{ arg.name }}{{ ', ' if not loop.last }}
{%- endfor -%}
)
{%- endmacro %}
{% macro handle_cmd_signature(cmd, class_name=None, interface=none) -%}
{% if not class_name %}virtual {% endif -%}
{{ result_type(cmd.result, interface) }} {% if class_name %}{{ class_name }}::{% endif -%}
handle_{{ cmd.name }}(
{%- for arg in cmd.args -%}
{{ cpp_type(arg) }}& {{ arg.name }}{{ ', ' if not loop.last }}
{%- endfor -%}
)
{%- endmacro %}
{% macro publish_var_signature(var) %}
void publish_{{ var.name }}({% if var.json_type != 'null' %}{{ cpp_type(var) }} value{% endif %}) {
_ev->publish(_name, "{{ var.name }}", {% if var.json_type != 'null' %}{{ var_to_any(var, 'value')}}{% else %}nullptr{% endif %});
}
{% endmacro %}
{% macro list_json_types(json_type) -%}
{% if json_type is iterable and json_type is not string -%}
{
{%- for type in json_type -%}
"{{ type }}"{{ ', ' if not loop.last}}
{%- endfor -%}
}
{%- else -%}
{"{{ json_type }}"}
{%- endif %}
{%- endmacro %}
{% macro insert_block(block, indent=0) %}
{{ block.tag }}
{{ " "*indent if block.first_use }}{{ block.content }}
{{ " "*indent }}{{ block.tag }}
{%- endmacro %}
{% macro print_spdx_line(license, year_tag=None) %}
// SPDX-License-Identifier: {{ license }}
// Copyright{% if year_tag %} {{ year_tag }}{% endif %} Pionix GmbH and Contributors to EVerest
{%- endmacro %}
{% macro print_license_header(license) %}
{{ license }}
{%- endmacro %}
{% macro print_template_info(version, title='DO NOT EDIT!', comment_sep='//') %}
{{ comment_sep }}
{{ comment_sep }} AUTO GENERATED - {{ title|upper }}
{{ comment_sep }} template version {{ version }}
{{ comment_sep }}
{%- endmacro %}
{% macro string_to_enum(enum_type) %}
{% if '::' not in enum_type %}
string_to_{{ enum_type | snake_case -}}
{% else %}
{{ enum_type[:enum_type.rfind('::')] }}::string_to_{{ enum_type[(enum_type.rfind('::')+2):] | snake_case -}}
{% endif %}
{%- endmacro %}
{% macro enum_to_string(enum_type) %}
{% if '::' not in enum_type %}
{{ enum_type | snake_case }}_to_string
{%- else %}
{{ enum_type[:enum_type.rfind('::')] }}::{{ enum_type[(enum_type.rfind('::')+2):] | snake_case }}_to_string
{%- endif %}
{%- endmacro %}
{% macro enum_to_string_view(enum_type) %}
{% if '::' not in enum_type %}
{{ enum_type | snake_case }}_to_string_view
{%- else %}
{{ enum_type[:enum_type.rfind('::')] }}::{{ enum_type[(enum_type.rfind('::')+2):] | snake_case }}_to_string_view
{%- endif %}
{%- endmacro %}

View File

@@ -0,0 +1,24 @@
.. _everest_modules_handwritten_{{ info.name }}:
.. This file is a placeholder for optional multiple files
handwritten documentation for the {{ info.name }} module.
.. This handwritten documentation is optional. In case
you do not want to write it, you can delete the doc/ directory.
.. The documentation can be written in reStructuredText,
and will be converted to HTML and PDF by Sphinx.
This index.rst file is the entry point for the module documentation.
.. Use underlined-only headlines inside this document (highest-level
sub-section headline should use "=" characters)
.. The content of this file will be included in the auto-generated HTML
page for the module. You can link to it using the following
reference: everest_modules_{{ info.name }}.
.. *******************************************
.. {{ info.name }}
.. *******************************************
{{ info.desc }}

View File

@@ -0,0 +1,194 @@
{% from "helper_macros.j2" import handle_cmd_signature, publish_var_signature, list_json_types, var_to_cpp, var_to_any, print_template_info, print_spdx_line, string_to_enum, enum_to_string, enum_to_string_view %}
{{ print_spdx_line('Apache-2.0') }}
#ifndef {{ info.hpp_guard }}
#define {{ info.hpp_guard }}
{{ print_template_info('6') }}
#include <framework/ModuleAdapter.hpp>
#include <utils/types.hpp>
#include <utils/error.hpp>
#include <utils/error/error_state_monitor.hpp>
#include <utils/error/error_manager_impl.hpp>
#include <utils/error/error_factory.hpp>
#include "Types.hpp"
class {{ info.class_name }} : public Everest::ImplementationBase {
public:
{{ info.class_name }}(Everest::ModuleAdapter* ev, const std::string& name)
: Everest::ImplementationBase(),
_ev(ev),
_name(name) {
if (ev == nullptr) {
EVLOG_error << "ev is nullptr, please check the initialization of the module";
error_manager = nullptr;
error_state_monitor = nullptr;
error_factory = nullptr;
EVLOG_error << "error_manager, error_state_monitor and error_factory are nullptr";
} else {
error_manager = ev->get_error_manager_impl(name);
if (error_manager == nullptr) {
EVLOG_error << "error_manager is nullptr";
}
error_state_monitor = ev->get_error_state_monitor_impl(name);
if (error_state_monitor == nullptr) {
EVLOG_error << "error_state_monitor is nullptr";
}
error_factory = ev->get_error_factory(name);
if (error_factory == nullptr) {
EVLOG_error << "error_factory is nullptr";
}
impl_mapping = Everest::get_impl_mapping(ev->get_mapping(), name);
}
}
{% if not vars %}
// no variables defined for this interface
{% else %}
// publish functions for variables
{% for var in vars %}
void publish_{{ var.name }}(
{%- if 'array_type' in var %}
const std::vector<{{ var.array_type }}>&
{%- elif 'object_type' in var %}
{{- 'const '}} {{- var.object_type }}&
{%- elif 'enum_type' in var %}
{{- 'const '}} {{- var.enum_type }}&
{%- else %}
{{- 'const '}} {{- var.cpp_type }}&
{%- endif -%}
{{ ' value) {' }}
{% if 'object_type' in var %}
_ev->publish(_name, "{{ var.name }}", value);
{% elif 'enum_type' in var %}
_ev->publish(_name, "{{ var.name }}", {{ enum_to_string_view(var.enum_type) }}(value));
{% elif 'array_type' in var %}
{% if 'array_type_contains_enum' in var %}
std::vector<std::string> string_array;
string_array.reserve(value.size());
for (const auto& entry : value) {
string_array.push_back({{ enum_to_string(var.array_type) }}(entry));
}
_ev->publish(_name, "{{ var.name }}", string_array);
{% else %}
_ev->publish(_name, "{{ var.name }}", value);
{% endif %}
{% else %}
_ev->publish(_name, "{{ var.name }}", value);
{% endif %}
}
{% endfor %}
{% endif %}
void raise_error(const Everest::error::Error& error) {
error_manager->raise_error(error);
}
void clear_error(const Everest::error::ErrorType& type) {
error_manager->clear_error(type);
}
void clear_error(const Everest::error::ErrorType& type, const Everest::error::ErrorSubType& sub_type) {
error_manager->clear_error(type, sub_type);
}
void clear_all_errors_of_impl() {
error_manager->clear_all_errors();
}
void clear_all_errors_of_impl(const Everest::error::ErrorType& type) {
error_manager->clear_all_errors(type);
}
std::shared_ptr<Everest::error::ErrorStateMonitor> error_state_monitor;
std::shared_ptr<Everest::error::ErrorFactory> error_factory;
std::shared_ptr<Everest::error::ErrorManagerImpl> error_manager;
std::optional<Mapping> get_mapping() {
return impl_mapping;
}
protected:
{% if not cmds %}
// no commands defined for this interface
{% else %}
// command handler functions (virtual)
{% for cmd in cmds %}
{{ handle_cmd_signature(cmd, none, info.interface_name) }} = 0;
{% endfor %}
{% endif %}
private:
Everest::ModuleAdapter* const _ev;
const std::string _name;
std::optional<Mapping> impl_mapping;
// helper function for getting all commands
void _gather_cmds([[maybe_unused]] std::vector<Everest::cmd>& cmds) override {
{% if not cmds %}
// this interface does not offer any commands
{% else %}
{% for cmd in cmds %}
// {{ cmd.name }} command
Everest::cmd {{ cmd.name }}_cmd;
{{ cmd.name }}_cmd.impl_id = _name;
{{ cmd.name }}_cmd.cmd_name = "{{ cmd.name }}";
{% if not cmd.args %}
// cmd {{ cmd.name }} has no arguments
{% else %}
{{ cmd.name }}_cmd.arg_types = {
{% for arg in cmd.args %}
{"{{ arg.name }}", {{ list_json_types(arg.json_type) + '}' }}{{ ',' if not loop.last }}
{% endfor %}
};
{% endif %}
{{ cmd.name }}_cmd.cmd = [this](const Parameters& args) -> Result {
{% for arg in cmd.args %}
{% if 'object_type' in arg %}
auto {{ arg.name }} = args.at("{{ arg.name }}").get<{{ arg.object_type }}>();
{% elif 'enum_type' in arg %}
auto {{ arg.name }} = {{ string_to_enum(arg.enum_type) }}(args.at("{{ arg.name }}").get<std::string>());
{% elif 'array_type' in arg %}
json {{ arg.name }}_json_array = args["{{ arg.name }}"];
{% if 'array_type_contains_enum' in arg %}
std::vector<{{ arg.array_type }}> {{ arg.name }};
for (auto entry : {{ arg.name }}_json_array) {
{{ arg.name }}.push_back({{ string_to_enum(arg.array_type) }}(entry));
}
{% else %}
auto {{ arg.name }} = args.at("{{ arg.name }}").get<std::vector<{{ arg.array_type }}>>();
{% endif %}
{% else %}
auto {{ arg.name }} = {{ var_to_cpp(arg) }}(args.at("{{ arg.name }}"));
{% endif %}
{% else %}
(void) args; // no arguments used for this callback
{% endfor %}
{{ 'auto result = ' if cmd.result }}this->handle_{{ cmd.name }}(
{%- for arg in cmd.args -%}
{{ arg.name }}{{ ', ' if not loop.last }}
{%- endfor -%}
);
{% if cmd.result and 'object_type' in cmd.result %}
return result;
{% elif cmd.result and 'enum_type' in cmd.result %}
return {{ enum_to_string(cmd.result.enum_type) }}(result);
{% else %}
return {{ var_to_any(cmd.result, 'result') if cmd.result else 'nullptr'}};
{% endif %}
};
{% if cmd.result %}
{{ cmd.name }}_cmd.return_type = {{ list_json_types(cmd.result.json_type) }};
{% endif %}
cmds.emplace_back(std::move({{ cmd.name }}_cmd));
{% if not loop.last %}
{% endif %}
{% endfor %}
{% endif %}
};
};
#endif // {{ info.hpp_guard }}

View File

@@ -0,0 +1,163 @@
{% from "helper_macros.j2" import call_cmd_signature, var_to_any, var_to_cpp, print_template_info, cpp_type, result_type, print_spdx_line, string_to_enum, enum_to_string %}
{{ print_spdx_line('Apache-2.0') }}
#ifndef {{ info.hpp_guard }}
#define {{ info.hpp_guard }}
{{ print_template_info('5') }}
#include <framework/ModuleAdapter.hpp>
#include <utils/types.hpp>
#include <utils/error.hpp>
#include <utils/error/error_state_monitor.hpp>
#include <utils/error/error_manager_req.hpp>
#include "Types.hpp"
class {{ info.class_name }} {
public:
{{ info.class_name }}(Everest::ModuleAdapter* adapter, Requirement req, const std::string& module_id, std::optional<Mapping> mapping)
: module_id(module_id),
_adapter(adapter),
_req(req),
_mapping(mapping) {
if (adapter == nullptr) {
EVLOG_error << "adapter is nullptr, please check the initialization of the module";
error_manager = nullptr;
error_state_monitor = nullptr;
EVLOG_error << "error_manager and error_state_monitor are nullptr";
} else {
error_manager = adapter->get_error_manager_req(req);
if (error_manager == nullptr) {
EVLOG_error << "error_manager is nullptr";
}
error_state_monitor = adapter->get_error_state_monitor_req(req);
if (error_state_monitor == nullptr) {
EVLOG_error << "error_state_monitor is nullptr";
}
}
}
const std::string module_id;
{% if not vars %}
// this interface does not export any variables to subscribe to
{% else %}
// variables available for subscription
{% for var in vars %}
void subscribe_{{ var.name }}(const std::function<void({% if var.json_type != 'null' %}const {{ cpp_type(var) }}&{% endif %})>& listener) {
_adapter->subscribe(_req, "{{ var.name }}", [func = std::move(listener)](const Value& value) {
{% if 'object_type' in var %}
func(value);
{% elif 'enum_type' in var %}
func({{ string_to_enum(var.enum_type) }}({{ var_to_cpp(var) }}(value)));
{% elif 'array_type' in var %}
{% if 'array_type_contains_enum' in var %}
std::vector<{{ var.array_type }}> typed_value;
for (auto& entry : value) {
typed_value.push_back({{ string_to_enum(var.array_type) }}(entry));
}
func(typed_value);
{% else %}
func(value);
{% endif %}
{% elif var.json_type != 'null' %}
func({{ var_to_cpp(var) }}(value));
{% else %}
if (not Everest::detail::is_type_compatible<{{ cpp_type(var) }}>(value.type())) {
EVLOG_error << "Callback for variable '{{ var.name }}' in interface '{{ info.interface }}' has wrong type!";
}
func();
{% endif %}
});
}
{% if not loop.last %}
{% endif %}
{% endfor %}
{% endif %}
void subscribe_error(
const Everest::error::ErrorType& type,
const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback
) {
error_manager->subscribe_error(type, callback, clear_callback);
}
void subscribe_all_errors(
const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback
) {
error_manager->subscribe_all_errors(callback, clear_callback);
}
{% if not cmds %}
// this interface does not export any commands to call
{% else %}
// commands available to call
{% for cmd in cmds %}
{{ call_cmd_signature(cmd, info.interface_name) }} {
{% for arg in cmd.args %}
{% if 'array_type' in arg %}
{% if 'array_type_contains_enum' in arg %}
Array {{ arg.name }}_array;
for (const auto& {{ arg.name }}_entry : {{ arg.name }}) {
{{ arg.name }}_array.push_back({{ enum_to_string(arg.array_type) }}({{ arg.name }}_entry));
}
{% endif %}
{% endif %}
{% endfor %}
{{ '' }}{% if cmd.result %}Result result = {% endif %}_adapter->call(_req, "{{ cmd.name }}",
{{ 'Parameters{' }}
{% for arg in cmd.args %}
{% if 'enum_type' in arg %}
{"{{ arg.name }}", {{ enum_to_string(arg.enum_type) }}({{ arg.name }})}
{% elif 'object_type' in arg %}
{"{{ arg.name }}", {{ arg.name }}}
{% elif 'array_type' in arg %}
{% if 'array_type_contains_enum' in arg %}
{"{{ arg.name }}", {{ var_to_any(arg, arg.name + '_array') }}}
{% else %}
{"{{ arg.name }}", {{ var_to_any(arg, arg.name) }}}
{% endif %}
{% else %}
{"{{ arg.name }}", {{ var_to_any(arg, arg.name) }}}
{% endif%}
{% if not loop.last %},{% endif %}
{% endfor %}
{{ '}' }}
);
{% if cmd.result %}
{% if 'enum_type' in cmd.result %}
auto retval = {{ string_to_enum(cmd.result.enum_type) }}({{ var_to_cpp(cmd.result) }}(result.value()));
{% elif 'object_type' in cmd.result %}
json retval_json = result.value();
{{ result_type(cmd.result) }} retval = retval_json;
{% elif 'array_type' in cmd.result %}
{{ result_type(cmd.result) }} retval (result.value().begin(), result.value().end());
{% else %}
auto retval = {{ var_to_cpp(cmd.result) }}(result.value());
{% endif %}
return retval;
{% endif %}
}
{% if not loop.last %}
{% endif %}
{% endfor %}
{% endif %}
std::shared_ptr<Everest::error::ErrorStateMonitor> error_state_monitor;
std::optional<Mapping> get_mapping() {
return _mapping;
}
private:
std::shared_ptr<Everest::error::ErrorManagerReq> error_manager;
Everest::ModuleAdapter* const _adapter;
Requirement _req;
std::optional<Mapping> _mapping;
};
#endif // {{ info.hpp_guard }}

View File

@@ -0,0 +1,27 @@
{% from "helper_macros.j2" import handle_cmd_signature, print_license_header %}
{{ print_license_header(info.license_header) }}
#include "{{ info.class_name}}.hpp"
namespace module {
namespace {{ info.interface_implementation_id }} {
void {{ info.class_name }}::init() {
}
void {{ info.class_name }}::ready() {
}
{% for cmd in cmds %}
{{ handle_cmd_signature(cmd, info.class_name, '::'+info.interface) }}{
// your code for cmd {{ cmd.name }} goes here
{% if cmd.result %}
return {{ cmd.result.json_type|create_dummy_result }};
{% endif %}
}
{% endfor %}
} // namespace {{ info.interface_implementation_id }}
} // namespace module

View File

@@ -0,0 +1,61 @@
{% from "helper_macros.j2" import handle_cmd_signature, print_template_info, insert_block, cpp_type, print_license_header %}
{{ print_license_header(info.license_header) }}
#ifndef {{ info.hpp_guard }}
#define {{ info.hpp_guard }}
{{ print_template_info('3', 'marked regions will be kept') }}
#include <{{ info.base_class_header }}>
#include "{{ info.module_header }}"
{{ insert_block(info.blocks.add_headers) }}
namespace module {
namespace {{ info.interface_implementation_id }} {
struct Conf {
{% for item in info.config %}
{{ cpp_type(item) }} {{ item.name }};
{% endfor %}
};
class {{ info.class_name }} : public {{ info.class_parent}} {
public:
{{ info.class_name }}() = delete;
{{ info.class_name }}(Everest::ModuleAdapter* ev, const Everest::PtrContainer<{{ info.module_class }}> &mod, Conf& config) :
{{ info.class_parent }}(ev, "{{ info.interface_implementation_id }}"),
mod(mod),
config(config)
{};
{{ insert_block(info.blocks.public_defs, indent=4) }}
protected:
{% if not cmds %}
// no commands defined for this interface
{% else %}
// command handler functions (virtual)
{% for cmd in cmds %}
{{ handle_cmd_signature(cmd, none, '::'+info.interface) }} override;
{% endfor %}
{% endif %}
{{ insert_block(info.blocks.protected_defs, indent=4) }}
private:
const Everest::PtrContainer<{{ info.module_class }}>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
{{ insert_block(info.blocks.private_defs, indent=4) }}
};
{{ insert_block(info.blocks.after_class) }}
} // namespace {{ info.interface_implementation_id }}
} // namespace module
#endif // {{ info.hpp_guard }}

View File

@@ -0,0 +1,172 @@
{% from "helper_macros.j2" import print_template_info, var_to_cpp, print_spdx_line %}
{{ print_spdx_line('Apache-2.0') }}
{{ print_template_info('5') }}
#include "{{ info.ld_ev_header }}"
#ifdef EVEREST_COVERAGE_ENABLED
#include <everest/helpers/coverage.hpp>
#endif
#include "{{ info.module_header }}"
{% for impl in provides %}
#include "{{ impl.class_header }}"
{% endfor %}
#include <framework/runtime.hpp>
#include <utils/types.hpp>
#include <generated/version_information.hpp>
#ifndef PROJECT_NAME
#define PROJECT_NAME "undefined project"
#endif
#ifndef PROJECT_VERSION
#define PROJECT_VERSION "undefined version"
#endif
#ifndef GIT_VERSION
#define GIT_VERSION "undefined git version"
#endif
{% if info.enable_global_errors %}
#include <utils/error/error_manager_req_global.hpp>
{% endif %}
namespace module {
// FIXME (aw): could this way of keeping static variables be changed somehow?
static Everest::ModuleAdapter adapter {};
static Everest::PtrContainer<{{info.class_name }}> mod_ptr {};
// per module configs
{# FIXME (aw): instead of being static, this could also be inside a map #}
{% for impl in provides %}
static {{ impl.id }}::Conf {{ impl.id }}_config;
{% endfor %}
static Conf module_conf;
static ModuleInfo module_info;
{% if info.enable_global_errors %}
void subscribe_global_all_errors(
const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback
) {
adapter.get_global_error_manager()->subscribe_global_all_errors(callback, clear_callback);
}
std::shared_ptr<Everest::error::ErrorStateMonitor> get_global_error_state_monitor() {
return adapter.get_global_error_state_monitor();
}
{% endif %}
std::shared_ptr<Everest::config::ConfigServiceClient> get_config_service_client() {
return adapter.get_config_service_client();
}
void LdEverest::init(ModuleConfigs module_configs, const ModuleInfo& mod_info) {
EVLOG_debug << "init() called on module {{ info.name }}";
// populate config for provided implementations
{% for impl in provides %}
auto {{ impl.id }}_config_input = std::move(module_configs["{{ impl.id }}"]);
{% for item in impl.config %}
{{ impl.id }}_config.{{ item.name }} = std::get<{{ item.cpp_type }}>({{ impl.id }}_config_input["{{ item.name }}"]);
{% endfor %}
{% endfor %}
{% if info.module_config|length %}
{% for item in info.module_config %}
module_conf.{{ item.name }} = std::get<{{ item.cpp_type }}>(module_configs["!module"]["{{ item.name }}"]);
{% endfor %}
{% endif %}
module_info = mod_info;
mod_ptr->init();
}
void LdEverest::ready() {
EVLOG_debug << "ready() called on module {{ info.name }}";
mod_ptr->ready();
}
void register_module_adapter(Everest::ModuleAdapter module_adapter) {
adapter = std::move(module_adapter);
}
std::vector<Everest::cmd> everest_register(const RequirementInitialization& requirement_init) {
EVLOG_debug << "everest_register() called on module {{ info.name }}";
adapter.check_complete();
{% for impl in provides %}
auto p_{{ impl.id }} = std::make_unique<{{ impl.id }}::{{ impl.class_name }}>(&adapter, mod_ptr, {{ impl.id }}_config);
adapter.gather_cmds(*p_{{ impl.id }});
{% endfor %}
{% for requirement in requires %}
{# FIXME: needs refactoring #}
{% if requirement.is_vector %}
auto r_{{ requirement.id }} = std::vector<std::unique_ptr<{{ requirement.class_name }}>>();
if (auto it = requirement_init.find("{{ requirement.id }}"); it != requirement_init.end()) {
for (const auto& requirement_initializer : (*it).second) {
auto requirement_module_id = requirement_initializer.fulfillment.module_id;
auto requirement = requirement_initializer.requirement;
auto mapping = requirement_initializer.mapping;
r_{{ requirement.id }}.emplace_back(std::make_unique<{{ requirement.class_name }}>(&adapter, requirement, requirement_module_id, mapping));
}
}
{% else %}
std::string r_{{ requirement.id }}_requirement_module_id;
Requirement r_{{ requirement.id }}_requirement;
std::optional<Mapping> r_{{ requirement.id }}_mapping;
if (auto it = requirement_init.find("{{ requirement.id }}"); it != requirement_init.end()) {
auto requirement_initializer = (*it).second;
if (requirement_initializer.size() > 0) {
r_{{ requirement.id }}_requirement_module_id = requirement_initializer.at(0).fulfillment.module_id;
r_{{ requirement.id }}_requirement = requirement_initializer.at(0).requirement;
r_{{ requirement.id }}_mapping = requirement_initializer.at(0).mapping;
}
}
auto r_{{ requirement.id }} = std::make_unique<{{ requirement.class_name }}>(&adapter, r_{{ requirement.id }}_requirement, r_{{ requirement.id }}_requirement_module_id, r_{{ requirement.id }}_mapping);
{% endif %}
{% else %}
(void) requirement_init; // no requirements -> unused requirement initialization
{% endfor %}
{% if info.enable_external_mqtt %}
static Everest::MqttProvider mqtt_provider(adapter);
{% endif %}
static Everest::TelemetryProvider telemetry_provider(adapter);
static {{ info.class_name }} module(
module_info,
{%- if info.enable_external_mqtt %}mqtt_provider, {% endif -%}
{%- if info.enable_telemetry %}telemetry_provider, {% endif -%}
{%- for impl in provides -%}
std::move(p_{{ impl.id }}){{ ', ' }}
{%- endfor -%}
{%- for requirement in requires -%}
std::move(r_{{ requirement.id }}){{ ', ' }}
{%- endfor -%}
module_conf);
mod_ptr.set(&module);
return adapter.registered_commands;
}
} // namespace module
int main(int argc, char* argv[]) {
#ifdef EVEREST_COVERAGE_ENABLED
everest::helpers::install_signal_handlers_for_gcov();
#endif
auto module_loader = Everest::ModuleLoader(argc, argv, Everest::ModuleCallbacks(
module::register_module_adapter, module::everest_register,
module::LdEverest::init, module::LdEverest::ready),
{PROJECT_NAME, PROJECT_VERSION, GIT_VERSION});
return module_loader.initialize();
}

View File

@@ -0,0 +1,37 @@
{% from "helper_macros.j2" import print_template_info, print_spdx_line %}
{{ print_spdx_line('Apache-2.0') }}
#ifndef {{ info.hpp_guard }}
#define {{ info.hpp_guard }}
{{ print_template_info('3') }}
#include <framework/ModuleAdapter.hpp>
#include <framework/everest.hpp>
#include <everest/logging.hpp>
namespace module {
// helper class for invoking private functions on module
struct LdEverest {
static void init(ModuleConfigs module_configs, const ModuleInfo& info);
static void ready();
};
void register_module_adapter(Everest::ModuleAdapter module_adapter);
std::vector<Everest::cmd> everest_register(const RequirementInitialization& requirement_init);
{% if info.enable_global_errors %}
void subscribe_global_all_errors(
const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback
);
std::shared_ptr<Everest::error::ErrorStateMonitor> get_global_error_state_monitor();
{% endif %}
std::shared_ptr<Everest::config::ConfigServiceClient> get_config_service_client();
} // namespace module
#endif // {{ info.hpp_guard }}

View File

@@ -0,0 +1,19 @@
{% from "helper_macros.j2" import print_license_header %}
{{ print_license_header(info.license_header) }}
#include "{{ info.module_header }}"
namespace module {
void {{ info.class_name }}::init() {
{% for impl in provides %}
invoke_init(*p_{{ impl.id }});
{% endfor %}
}
void {{ info.class_name }}::ready() {
{% for impl in provides %}
invoke_ready(*p_{{ impl.id }});
{% endfor %}
}
} // namespace module

View File

@@ -0,0 +1,109 @@
{% from "helper_macros.j2" import print_template_info, insert_block, cpp_type, print_license_header %}
{{ print_license_header(info.license_header) }}
#ifndef {{ info.hpp_guard }}
#define {{ info.hpp_guard }}
{{ print_template_info('2', 'marked regions will be kept') }}
#include "{{ info.ld_ev_header }}"
{% for impl in provides %}
{% if loop.first %}
// headers for provided interface implementations
{% endif %}
#include <{{ impl.base_class_header }}>
{% endfor %}
{% for interface in requires %}
{% if loop.first %}
// headers for required interface implementations
{% endif %}
#include <{{ interface.exports_header }}>
{% endfor %}
{{ insert_block(info.blocks.add_headers) }}
namespace module {
struct Conf {
{% for item in info.module_config %}
{{ cpp_type(item) }} {{ item.name }};
{% endfor %}
};
class {{ info.class_name }} : public Everest::ModuleBase {
public:
{{ info.class_name }}() = delete;
{{ info.class_name }}(
const ModuleInfo& info,
{% if info.enable_external_mqtt %}
Everest::MqttProvider& mqtt_provider,
{% endif %}
{% if info.enable_telemetry %}
Everest::TelemetryProvider& telemetry,
{% endif %}
{% for impl in provides %}
std::unique_ptr<{{ impl.base_class }}> p_{{ impl.id }},
{% endfor %}
{% for requirement in requires %}
{% if requirement.is_vector %}
std::vector<std::unique_ptr<{{ requirement.class_name }}>> r_{{ requirement.id }},
{% else %}
std::unique_ptr<{{ requirement.class_name }}> r_{{ requirement.id }},
{% endif %}
{% endfor %}
Conf& config
) :
ModuleBase(info),
{% if info.enable_external_mqtt %}
mqtt(mqtt_provider),
{% endif %}
{% if info.enable_telemetry %}
telemetry(telemetry),
{% endif %}
{% for impl in provides %}
p_{{ impl.id }}(std::move(p_{{ impl.id }})),
{% endfor %}
{% for requirement in requires %}
r_{{ requirement.id }}(std::move(r_{{ requirement.id }})),
{% endfor %}
config(config)
{};
{% if info.enable_external_mqtt %}
Everest::MqttProvider& mqtt;
{% endif %}
{% if info.enable_telemetry %}
Everest::TelemetryProvider& telemetry;
{% endif %}
{% for impl in provides %}
const std::unique_ptr<{{ impl.base_class }}> p_{{ impl.id }};
{% endfor %}
{% for requirement in requires %}
{% if requirement.is_vector %}
const std::vector<std::unique_ptr<{{ requirement.class_name }}>> r_{{ requirement.id }};
{% else %}
const std::unique_ptr<{{ requirement.class_name }}> r_{{ requirement.id }};
{% endif %}
{% endfor %}
const Conf& config;
{{ insert_block(info.blocks.public_defs, indent=4) }}
protected:
{{ insert_block(info.blocks.protected_defs, indent=4) }}
private:
friend class LdEverest;
void init();
void ready();
{{ insert_block(info.blocks.private_defs, indent=4) }}
};
{{ insert_block(info.blocks.after_class) }}
} // namespace module
#endif // {{ info.hpp_guard }}

View File

@@ -0,0 +1,243 @@
{% from "helper_macros.j2" import print_template_info, print_spdx_line, string_to_enum, enum_to_string, enum_to_string_view %}
{{ print_spdx_line('Apache-2.0') }}
#ifndef {{ info.hpp_guard }}
#define {{ info.hpp_guard }}
{{ print_template_info('5') }}
#include <optional>
#include <ostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>
#include <nlohmann/json.hpp>
{% if 'type_headers' in info %}
{% for type_header in info.type_headers %}
#include <{{type_header}}>
{% endfor %}
{% endif %}
using nlohmann::json;
{% if not enums %}
// no enums defined for this interface
{% else %}
// enums of {{ info.interface_name }}
{% for enum in enums %}
{% for namespace in info.namespace %}
namespace {{ namespace }} {
{% endfor %}
enum class {{ enum.enum_type}}
{
{% for e in enum.enum %}
{{e}},
{% endfor %}
};
/// \brief Converts the given {{ enum.enum_type }} \p e to human readable string
/// \returns a string representation of the {{ enum.enum_type }}
inline std::string {{ enum.enum_type | snake_case }}_to_string(const {{ enum.enum_type }}& e) {
switch (e) {
{% for e in enum.enum %}
case {{ enum.enum_type }}::{{ e }}: return "{{e}}";
{% endfor %}
}
throw std::out_of_range("No known string conversion for provided enum of type {{ enum.enum_type }}");
}
/// \brief Converts the given {{ enum.enum_type }} \p e to human readable string
/// \returns a string_view representation of the {{ enum.enum_type }}
inline constexpr std::string_view {{ enum.enum_type | snake_case }}_to_string_view(const {{ enum.enum_type }}& e) {
switch (e) {
{% for e in enum.enum %}
case {{ enum.enum_type }}::{{ e }}: return "{{e}}";
{% endfor %}
}
throw std::out_of_range("No known string conversion for provided enum of type {{ enum.enum_type }}");
}
/// \brief Converts the given std::string \p s to {{ enum.enum_type }}
/// \returns a {{ enum.enum_type }} from a string representation
inline {{ enum.enum_type }} string_to_{{ enum.enum_type | snake_case }}(const std::string& s) {
{% for e in enum.enum %}
if (s == "{{e}}") {
return {{ enum.enum_type }}::{{ e }};
}
{% endfor %}
throw std::out_of_range("Provided string " + s + " could not be converted to enum of type {{ enum.enum_type }}");
}
/// \brief Writes the string representation of the given {{ enum.enum_type }} \p {{ enum.enum_type | snake_case }} to the given output stream \p os
/// \returns an output stream with the {{ enum.enum_type }} written to
inline std::ostream& operator<<(std::ostream& os, const types::{{ info.interface_name }}::{{ enum.enum_type }}& {{ enum.enum_type | snake_case }}) {
os << types::{{info.interface_name}}::{{ enum.enum_type | snake_case }}_to_string({{ enum.enum_type | snake_case }});
return os;
}
{% for namespace in info.namespace|reverse %}
} // namespace {{namespace}}
{% endfor %}
{% endfor %}
{% endif%}
{% if not types %}
// no types defined for this interface
{% else %}
// types of {{ info.interface_name }}
{% for namespace in info.namespace %}
namespace {{ namespace }} {
{% endfor %}
{% for parsed_type in types %}
{% if parsed_type.properties|length > 0 %}
struct {{ parsed_type.name }} {
{% for property in parsed_type.properties %}
{# Constraints will be checked by the framework #}
{{ 'std::optional<' if not property.required -}}
{{ property.type -}}
{{ '>' if not property.required -}}
{{ ' ' + property.name + ';' }} ///< {{ property.info.description }}
{% endfor %}
/// \brief Conversion from a given {{ parsed_type.name }} \p k to a given json object \p j
friend void to_json(json& j, const {{ parsed_type.name }}& k) {
// the required parts of the type
{% if parsed_type.properties|selectattr('required')|list|length %}
j = json{
{%- endif %}
{%- for property in parsed_type.properties %}
{%- if property.required +%}
{"{{property.name}}",
{%- if property.enum %} {{ enum_to_string_view(property.type) }}(k.{{ property.name }})
{%- else %}
{%- if property.type == 'DateTime' %} k.{{property.name}}.to_rfc3339()
{%- else %} k.{{property.name}}
{%- endif %}
{%- endif %}},
{%- endif %}
{%- endfor %}
{% if not parsed_type.properties|selectattr('required')|list|length %}
j = json ({});
{%- else +%}
};
{%- endif %}
// the optional parts of the type
{% for property in parsed_type.properties %}
{% if not property.required %}
if (k.{{property.name}}) {
{% if property.type.startswith('std::vector<') %}
{%- if parsed_type.properties|selectattr('required')|list|length %}
j.emplace("{{property.name}}", *k.{{property.name}});
{%- else %}
{#only optional keys in json#}
{#TODO: add key to json when there are no required keys but multiple optional keys#}
{# FIXME: this is never generated?#}
if (j.size() == 0) {
j = json{{'{{"'+property.name+'", json::array()}};'}}
} else {
j.emplace("{{property.name}}", *k.{{property.name}});
}
{% endif %}
{% else %}
{%- if property.enum %}
j.emplace("{{property.name}}", {{ enum_to_string_view(property.type) }}(*k.{{ property.name }}));
{%- else %}
{%- if property.type == 'DateTime' %}
j.emplace("{{property.name}}", k.{{property.name}}->to_rfc3339());
{%- else %}
j.emplace("{{property.name}}", *k.{{property.name}});
{%- endif %}
{%- endif %}
{% endif %}
}
{% endif %}
{% endfor %}
}
/// \brief Conversion from a given json object \p j to a given {{ parsed_type.name }} \p k
friend void from_json(const json& j, {{ parsed_type.name }}& k) {
// the required parts of the type
{% for property in parsed_type.properties %}
{% if property.required %}
{% if property.type.startswith('std::vector<') %}
k.{{property.name}} = std::move(j.at("{{property.name}}").get<{{property.type}}>());
{% else %}
k.{{property.name}} =
{%- if property.enum %} {{ string_to_enum(property.type) }}(j.at("{{property.name}}"))
{%- else %}
{%- if property.type == 'DateTime' %} DateTime(std::string(j.at("{{property.name}}")));
{%- else %} j.at("{{property.name}}")
{%- endif %}
{%- endif %};
{% endif %}
{% endif %}
{%- endfor %}
// the optional parts of the type
auto it = j.end();
{% for property in parsed_type.properties %}
{% if not property.required %}
it = it = j.find("{{property.name}}");
if (it != j.end()) {
{% if property.type.startswith('std::vector<') %}
k.{{property.name}} = j.at("{{property.name}}").get<{{property.type}}>();
{% else %}
{%- if property.enum %}
k.{{property.name}} = {{ string_to_enum(property.type) }}(it->get<std::string>());
{%- else %}
k.{{property.name}} = it->get<{{property.type}}>();
{% endif %}
{% endif %}
}
{% endif %}
{% endfor %}
}
/// \brief Compares objects of type {{ parsed_type.name }} for equality
friend constexpr bool operator==(const {{ parsed_type.name }}& k, const {{ parsed_type.name }}& l) {
{%- for var, tuple in [('k.', 'lhs'), ('l.', 'rhs')] +%}
const auto& {{ tuple }}_tuple = std::tie(
{%- for property in parsed_type.properties +%}
{{ var }}{{ property.name }}
{%- if not loop.last %},
{%- endif %}
{%- endfor +%}
);
{%- endfor +%}
return lhs_tuple == rhs_tuple;
}
/// \brief Compares objects of type {{ parsed_type.name }} for inequality
friend constexpr bool operator!=(const {{ parsed_type.name }}& k, const {{ parsed_type.name }}& l) {
return not operator==(k, l);
}
/// \brief Writes the string representation of the given {{ parsed_type.name }} \p k to the given output stream \p os
/// \returns an output stream with the {{ parsed_type.name }} written to
friend std::ostream& operator<<(std::ostream& os, const {{ parsed_type.name }}& k) {
os << json(k).dump(4);
return os;
}
};
{% endif %}
{% endfor %}
{% for namespace in info.namespace|reverse %}
} // namespace {{namespace}}
{% endfor %}
{% endif %}
#endif // {{ info.hpp_guard }}

View File

@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
"""
Provide type parsing functionality.
author: kai-uwe.hermann@pionix.de
"""
from . import helpers
from pathlib import Path
from typing import Dict, List, Tuple
import stringcase
class TypeParser:
"""Provide generation of type headers from type definitions."""
validators = None
templates = None
all_types = {}
validated_type_defs = {}
@classmethod
def parse_type_url(cls, type_url: str) -> Dict:
"""Parse a global type URL in the following format /filename#/typename."""
type_dict = {
'type_relative_path': None,
'namespaced_type': None,
'header_file': None,
'type_name': None
}
if not type_url.startswith('/'):
raise Exception('type_url: ' + type_url + ' needs to start with a "/".')
if '#/' not in type_url:
raise Exception('type_url: ' + type_url + ' needs to refer to a specific type with "#/TypeName".')
type_relative_path, prop_type = type_url.split('#/')
type_relative_path = Path(type_relative_path[1:])
namespaced_type = 'types::' + '::'.join(type_relative_path.parts) + f'::{prop_type}'
type_dict['type_relative_path'] = type_relative_path
type_dict['namespaced_type'] = namespaced_type
type_dict['type_name'] = prop_type
return type_dict
@classmethod
def does_type_exist(cls, type_url: str, json_type: str):
"""Checks if the referenced type exists"""
if type_url not in TypeParser.all_types:
TypeParser.all_types[type_url] = TypeParser.parse_type_url(type_url=type_url)
type_dict = TypeParser.all_types[type_url]
type_path = helpers.resolve_everest_dir_path('types' / type_dict['type_relative_path'].with_suffix('.yaml'))
if not type_path or not type_path.exists():
raise helpers.EVerestParsingException(
'$ref: ' + type_url + f' referenced type file "{type_path}" does not exist.')
if type_path not in TypeParser.validated_type_defs:
TypeParser.validated_type_defs[type_path] = helpers.load_validated_type_def(
type_path, TypeParser.validators['type'])
if type_dict['type_name'] not in TypeParser.validated_type_defs[type_path]['types']:
raise helpers.EVerestParsingException('$ref: ' + type_url + ' referenced type "' +
type_dict['type_name'] + f'" does not exist in type file "{type_path}".')
type_schema = TypeParser.validated_type_defs[type_path]['types'][type_dict['type_name']]
if json_type != type_schema['type']:
raise helpers.EVerestParsingException('$ref: ' + type_url + ' referenced type "' +
type_dict['type_name'] + f'" in type file "{type_path}"' +
f' should be of type "{json_type}" but is of type: "' +
type_schema['type'] + '".')
@classmethod
def generate_tmpl_data_for_type(cls, type_with_namespace, type_def):
"""Generate template data based on the provided type and type definition."""
helpers.parsed_enums.clear()
helpers.parsed_types.clear()
helpers.type_headers.clear()
types = []
enums = []
for type_name, type_properties in type_def.get('types', {}).items():
type_url = f'/{type_with_namespace["relative_path"]}#/{type_name}'
TypeParser.all_types[type_url] = TypeParser.parse_type_url(type_url=type_url)
try:
(_type_info, enum_info) = helpers.extended_build_type_info(type_name, type_properties, type_file=True)
if enum_info:
enums.append(enum_info)
except helpers.EVerestParsingException as e:
raise helpers.EVerestParsingException(f'Error parsing type {type_name}: {e}')
for parsed_enum in helpers.parsed_enums:
enum_info = {
'name': parsed_enum['name'],
'description': parsed_enum['description'],
'enum_type': stringcase.capitalcase(parsed_enum['name']),
'enum': parsed_enum['enums']
}
enums.append(enum_info)
for parsed_type in helpers.parsed_types:
parsed_type['name'] = stringcase.capitalcase(parsed_type['name'])
types.append(parsed_type)
type_headers = sorted(helpers.type_headers)
# Remove the header itself from the includes.
own_header = helpers.generate_header_for_type(type_with_namespace["relative_path"])
type_headers = [header for header in type_headers if str(header) != str(own_header)]
# sort types, so no forward declaration is necessary
sorted_types: List = []
for struct_type in types:
insert_at: int = 0
for dep_struct_type in struct_type['depends_on']:
for i, _entry in enumerate(sorted_types):
# the new one depends on the current
if sorted_types[i]['name'] == dep_struct_type:
insert_at = max(insert_at, i + 1)
break
sorted_types.insert(insert_at, struct_type)
tmpl_data = {
'info': {
'type': type_with_namespace['namespace'],
'desc': type_def['description'],
'type_headers': type_headers,
},
'enums': enums,
'types': sorted_types,
}
return tmpl_data
@classmethod
def load_type_definition(cls, type_path: Path):
"""Load a type definition from the provided path and check its last modification time."""
if type_path not in TypeParser.validated_type_defs:
TypeParser.validated_type_defs[type_path] = helpers.load_validated_type_def(
type_path, TypeParser.validators['type'])
last_mtime = type_path.stat().st_mtime
return TypeParser.validated_type_defs[type_path], last_mtime
@classmethod
def generate_type_info(cls, type_with_namespace, all_types) -> Tuple:
"""Generate type template data."""
try:
type_def, last_mtime = TypeParser.load_type_definition(type_with_namespace['path'])
except Exception as e:
if not all_types:
raise
else:
print(f'Ignoring type {type_with_namespace["namespace"]} with reason: {e}')
return
tmpl_data = TypeParser.generate_tmpl_data_for_type(type_with_namespace, type_def)
return (tmpl_data, last_mtime)
@classmethod
def generate_type_headers(cls, type_with_namespace, all_types, output_dir):
"""Render template data to generate type headers."""
tmpl_data, last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types)
types_parts = {'types': None}
output_path = output_dir / type_with_namespace['relative_path']
types_file = output_path.with_suffix('.hpp')
output_path = output_path.parent
output_path.mkdir(parents=True, exist_ok=True)
namespaces = ['types']
namespaces.extend(type_with_namespace['relative_path'].parts)
tmpl_data['info']['interface_name'] = f'{type_with_namespace["namespace"]}'
tmpl_data['info']['namespace'] = namespaces
tmpl_data['info']['hpp_guard'] = 'TYPES_' + helpers.snake_case(
''.join(type_with_namespace['uppercase_path'])).upper() + '_TYPES_HPP'
types_parts['types'] = {
'path': types_file,
'content': TypeParser.templates['types.hpp'].render(tmpl_data),
'last_mtime': last_mtime,
'template_path': Path(TypeParser.templates['types.hpp'].filename),
'printable_name': types_file.relative_to(output_path.parent)
}
return types_parts