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,138 @@
---
Language: Cpp
# BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: true
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Right
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: AfterColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
- Regex: '.*'
Priority: 1
SortPriority: 0
IncludeIsMainRegex: '(Test)?$'
IncludeIsMainSourceRegex: ''
IndentCaseLabels: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
Standard: Latest
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
...

View File

@@ -0,0 +1,36 @@
Checks: >
bugprone-*,
cert-*,
concurrency-*,
cppcoreguidelines-*,
misc-*,
performance-*,
-abseil-*,
-altera-*,
-android-*,
-boost-*,
-fuchsia-*,
-google-*,
-llvm-*,
-llvmlibc-*,
-zircon-*,
-bugprone-easily-swappable-parameters,
-cert-err60-cpp,
-cppcoreguidelines-non-private-member-variables-in-classes,
-misc-include-cleaner,
-misc-non-private-member-variables-in-classes,
-modernize-use-trailing-return-type,
-performance-avoid-endl,
-performance-enum-size,
-readability-function-cognitive-complexity,
-readability-identifier-length,
-concurrency-mt-unsafe,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-pro-type-vararg,
# (the last -cppcoreguidelines lines are temporary)
HeaderFilterRegex: ".*"
CheckOptions:
- { key: performance-unnecessary-value-param.AllowedTypes, value: ((std::shared_ptr)) }

View File

@@ -0,0 +1,43 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"extends": [
"airbnb-base"
],
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
"camelcase": "off",
"eqeqeq": [
"error",
"smart"
],
"comma-dangle": [
"warn",
{
"objects": "always-multiline",
"arrays": "always-multiline",
"functions": "never"
}
],
"import/no-unresolved": [
2,
{
"ignore": [
"everestjs"
]
}
],
"max-len": [
"warn",
{
"code": 120,
"tabWidth": 2
}
]
}
}

View File

@@ -0,0 +1,18 @@
*build
*build-cross
/build*
!build.rs
!everestrs-build
target
bazel-bin
bazel-out
bazel-everest-framework
bazel-testlogs
watcher.lua
workspace.yaml
# Clang
.cache/
# Bazel
/bazel-*

View File

@@ -0,0 +1,3 @@
singleQuote: true
tabWidth: 2
quoteProps: consistent

View File

@@ -0,0 +1,24 @@
pep257:
disable:
- D203
- D212
- D213
- D214
- D215
- D404
- D405
- D406
- D407
- D408
- D409
- D410
- D411
- D413
- D415
- D416
- D417
pylint:
options:
extension-pkg-allow-list: everestpy
disable:
- logging-fstring-interpolation

View File

@@ -0,0 +1,170 @@
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_import", "cc_library", "cc_shared_library")
load("@rules_python//python:py_binary.bzl", "py_binary")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
py_binary(
name = "collect_migration_files",
srcs = [
".ci/build-kit/scripts/collect_migration_files.py",
],
imports = ["."],
main = ".ci/build-kit/scripts/collect_migration_files.py",
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
)
genrule(
name = "compile_time_settings",
srcs = ["//lib/everest/framework/schemas/migrations"],
outs = ["include/everest/compile_time_settings.hpp"],
cmd = """
echo "#define EVEREST_INSTALL_PREFIX \\"/usr\\"" > $@
echo "#define EVEREST_INSTALL_LIBDIR \\"/lib\\"" >> $@
echo "#define EVEREST_NAMESPACE (\\"everest\\")" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_LATENCY 1" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_GREEDY 2" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CONSERVATIVE 3" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_FIXED_SIZE 4" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM 5" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_LATENCY" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS 50" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS 5" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD 3" >> $@
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE void" >> $@
$(location :collect_migration_files) --migration-files $(locations //lib/everest/framework/schemas/migrations) --output $@
""",
tools = [":collect_migration_files"],
)
genrule(
name = "version_information",
outs = ["include/generated/version_information.hpp"],
cmd = """
echo "#pragma once" > $@
echo "#define PROJECT_NAME \\"everest-framework\\"" >> $@
echo "#define PROJECT_DESCRIPTION \\"\\"" >> $@
echo "#define PROJECT_VERSION \\"\\"" >> $@
echo "#define GIT_VERSION \\"\\"" >> $@
""",
)
config_setting(
name = "_dynamic_mode_fully",
values = {"dynamic_mode": "fully"},
visibility = ["//visibility:public"],
)
cc_library(
name = "framework_lib",
srcs = glob(["lib/**/*.cpp"]),
hdrs = glob(["include/**/*.hpp"]) + [
":compile_time_settings",
":version_information",
],
cxxopts = ["-std=c++17"],
# See https://github.com/HowardHinnant/date/issues/324
local_defines = [
"BUILD_TZ_LIB=ON",
"USE_SYSTEM_TZ_DB=ON",
"USE_OS_TZDB=1",
"USE_AUTOLOAD=0",
"HAS_REMOTE_API=0",
],
strip_include_prefix = "include",
visibility = ["//visibility:public"],
deps = [
"//lib/everest/io:io",
"//lib/everest/helpers",
"//lib/everest/log:liblog",
"//lib/everest/sqlite:everest-sqlite",
"//lib/everest/util",
"//lib/everest/yaml:everest_yaml",
"@boost.program_options",
"@boost.uuid",
"@com_github_HowardHinnant_date//:date",
"@com_github_fmtlib_fmt//:fmt",
"@com_github_nlohmann_json//:json",
"@com_github_pboettch_json-schema-validator//:json-schema-validator",
],
)
cc_shared_library(
name = "framework_so",
shared_lib_name = "libframework.so",
deps = [":framework_lib"],
exports_filter = [":framework_lib"],
)
cc_import(
name = "framework_dynamic",
shared_library = ":framework_so",
deps = [":framework_lib"],
linkopts = ["-lframework"],
)
alias(
name = "framework",
actual = select({
":_dynamic_mode_fully": ":framework_dynamic",
"//conditions:default": ":framework_lib",
}),
visibility = ["//visibility:public"],
)
cc_library(
name = "controller-ipc",
srcs = ["src/controller/ipc.cpp"],
hdrs = ["src/controller/ipc.hpp"],
cxxopts = ["-std=c++17"],
strip_include_prefix = "src",
deps = [
"@com_github_nlohmann_json//:json",
],
)
cc_binary(
name = "controller",
srcs = glob(
[
"src/controller/*.cpp",
"src/controller/*.hpp",
],
exclude = [
"src/controller/ipc.cpp",
"src/controller/ipc.hpp",
],
),
cxxopts = ["-std=c++17"],
deps = [
":controller-ipc",
":framework",
"//lib/everest/log:liblog",
"@com_github_fmtlib_fmt//:fmt",
"@com_github_warmcatt_libwebsockets//:libwebsockets",
"@libcap",
],
)
cc_binary(
name = "manager",
srcs = glob(
[
"src/*.cpp",
"src/*.hpp",
],
),
cxxopts = ["-std=c++17"],
visibility = ["//visibility:public"],
deps = [
":controller-ipc",
":framework",
"//lib/everest/log:liblog",
"@boost.program_options",
"@com_github_fmtlib_fmt//:fmt",
"@com_github_pboettch_json-schema-validator//:json-schema-validator",
"@libcap",
],
)
exports_files([
"dependencies.yaml",
])

View File

@@ -0,0 +1,289 @@
cmake_minimum_required(VERSION 3.14)
project(everest-framework
VERSION 0.25.0
DESCRIPTION "The open operating system for e-mobility charging stations"
LANGUAGES CXX C
)
if(DEFINED EVEREST_IO_WITH_MQTT AND NOT EVEREST_IO_WITH_MQTT)
message(FATAL_ERROR "everest-framework requires MQTT support in everest::io. "
"Set EVEREST_IO_WITH_MQTT=ON or exclude framework from the build "
"(e.g. via EVEREST_EXCLUDE_LIBS=framework).")
endif()
find_package(everest-cmake 0.5 REQUIRED
PATHS ../everest-cmake
)
# options
option(${PROJECT_NAME}_BUILD_TESTING "Build unit tests, used if included as dependency" OFF)
option(BUILD_TESTING "Build unit tests, used if standalone project" OFF)
option(FRAMEWORK_INSTALL "Install the library (shared data might be installed anyway)" ${EVC_MAIN_PROJECT})
option(CMAKE_RUN_CLANG_TIDY "Run clang-tidy" OFF)
option(EVEREST_ENABLE_JS_SUPPORT "Enable everestjs for JavaScript modules" OFF)
option(EVEREST_ENABLE_PY_SUPPORT "Enable everestpy for Python modules" ON)
option(EVEREST_ENABLE_RS_SUPPORT "Enable everestrs for Rust modules" OFF)
option(EVEREST_ENABLE_ADMIN_PANEL_BACKEND "Enable everest admin panel backend" OFF)
option(EVEREST_INSTALL_ADMIN_PANEL "Download and install everest admin panel" OFF)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY "latency" CACHE STRING
"Thread pool scaling policy for framework message handling: latency, greedy, conservative, fixed_size, custom")
set_property(CACHE EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY PROPERTY STRINGS
latency greedy conservative fixed_size custom)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS "50" CACHE STRING
"Maximum queued task wait time in milliseconds before latency scaling grows the framework message handler pool")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS "5" CACHE STRING
"Supervisor tick in milliseconds for framework message handler latency scaling")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD "3" CACHE STRING
"Queue size threshold for framework message handler fixed-size scaling")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER "" CACHE STRING
"Header to include when EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE "" CACHE STRING
"Fully-qualified scaling policy type when EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_INCLUDE_DIR "" CACHE PATH
"Include directory for the custom thread pool scaling policy header")
foreach(_scaling_option IN ITEMS
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD)
if(NOT ${_scaling_option} MATCHES "^[1-9][0-9]*$")
message(FATAL_ERROR
"${_scaling_option} must be a positive integer, got '${${_scaling_option}}'")
endif()
endforeach()
if(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "latency")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 1)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "greedy")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 2)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "conservative")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 3)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "fixed_size")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 4)
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "custom")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 5)
if(NOT EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER)
message(FATAL_ERROR
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER must be set when "
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
endif()
if(NOT EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE)
message(FATAL_ERROR
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE must be set when "
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
endif()
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG
"${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER}")
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG
"${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE}")
else()
message(FATAL_ERROR
"Invalid EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY: "
"${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY}. "
"Allowed values: latency, greedy, conservative, fixed_size, custom")
endif()
ev_setup_cmake_variables_python_wheel()
option(${PROJECT_NAME}_USE_PYTHON_VENV "Use python venv for pip install targets" OFF)
set(${PROJECT_NAME}_PYTHON_VENV_PATH "${CMAKE_BINARY_DIR}/venv" CACHE PATH "Path to python venv")
ev_setup_python_executable(
USE_PYTHON_VENV ${${PROJECT_NAME}_USE_PYTHON_VENV}
PYTHON_VENV_PATH ${${PROJECT_NAME}_PYTHON_VENV_PATH}
)
if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING)
set(EVEREST_FRAMEWORK_BUILD_TESTING ON)
# this policy allows us to link gcov to targets defined in other directories
if(POLICY CMP0079)
set(CMAKE_POLICY_DEFAULT_CMP0079 NEW)
endif()
if (NOT CMAKE_BUILD_TYPE)
if(EVEREST_ENABLE_DEBUG_BUILD)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
else()
set(CMAKE_BUILD_TYPE MinSizeRel CACHE STRING "Build type" FORCE)
endif()
endif()
endif()
# make own cmake modules available
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# this policy allows us to continue using the removed FindBoost module for now
if(POLICY CMP0167)
cmake_policy(SET CMP0167 OLD)
endif()
# dependencies
find_package(Boost
COMPONENTS
program_options
thread
REQUIRED
)
if(Boost_VERSION_STRING VERSION_LESS "1.69.0")
find_package(Boost
COMPONENTS
system
REQUIRED
)
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(libcap
REQUIRED
IMPORTED_TARGET
libcap
)
# Make sure stdc++fs is linked for GCC < 9
if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9)
set(STD_FILESYSTEM_COMPAT_LIB "stdc++fs")
else()
set(STD_FILESYSTEM_COMPAT_LIB "")
endif()
if(NOT DISABLE_EDM)
evc_setup_edm()
# In EDM mode, we can't install exports (because the dependencies usually do not install their exports)
set(FRAMEWORK_INSTALL OFF)
if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
# FIXME (aw): libwebsockets/ninja clean doesn't delete recursivly ..
set_property(
TARGET websockets_shared
APPEND
PROPERTY ADDITIONAL_CLEAN_FILES "${libwebsockets_BINARY_DIR}/include/libwebsockets"
)
# FIXME (aw): libwebsockets enum-int-mismatch FIX
# see https://github.com/warmcat/libwebsockets/pull/2824
if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_C_COMPILER_VERSION VERSION_GREATER 13.0)
target_compile_options(websockets_shared PRIVATE -Wno-error=enum-int-mismatch)
endif()
endif()
set_property(TARGET nlohmann_json_schema_validator PROPERTY POSITION_INDEPENDENT_CODE ON)
# FIXME (aw): add catch2's cmake folder
if (BUILD_TESTING)
list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/contrib")
endif()
else()
find_package(date REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(nlohmann_json_schema_validator REQUIRED)
find_package(fmt REQUIRED)
if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
find_package(libwebsockets REQUIRED)
endif()
if (BUILD_TESTING)
find_package(Catch2 REQUIRED)
endif()
include(find-mqttc)
endif()
include(${everest-sqlite_SOURCE_DIR}/cmake/CollectMigrationFiles.cmake)
set(EVEREST_FRAMEWORK_GENERATED_INC_DIR ${PROJECT_BINARY_DIR}/generated)
configure_file(
include/compile_time_settings.hpp.in
${EVEREST_FRAMEWORK_GENERATED_INC_DIR}/everest/compile_time_settings.hpp
)
# library code
add_subdirectory(lib)
# executable code
add_subdirectory(src)
# auxillary files
add_subdirectory(schemas)
# everest javascript wrapper
if (EVEREST_ENABLE_JS_SUPPORT)
add_subdirectory(everestjs)
endif()
# everest python wrapper
if (EVEREST_ENABLE_PY_SUPPORT)
set(PYTHON_MODULE_EXTENSION ".so")
add_subdirectory(everestpy)
endif()
if (EVEREST_ENABLE_RS_SUPPORT)
add_subdirectory(everestrs)
endif()
# FIXME (aw): should this be installed or not? Right now it is needed for the
# current packaging approach
install(TARGETS framework
EXPORT framework-targets
LIBRARY
)
# packaging
if (FRAMEWORK_INSTALL)
install(
DIRECTORY include/
DESTINATION include/everest
)
install(
FILES ${EVEREST_FRAMEWORK_GENERATED_INC_DIR}/everest/compile_time_settings.hpp
DESTINATION include/everest
)
evc_setup_package(
NAME everest-framework
EXPORT framework-targets
NAMESPACE everest
ADDITIONAL_CONTENT
"find_dependency(everest-helpers)"
"find_dependency(everest-util)"
"find_dependency(nlohmann_json)"
"find_dependency(nlohmann_json_schema_validator)"
"find_dependency(fmt)"
"find_dependency(date)"
"set(EVEREST_SCHEMA_DIR \"@PACKAGE_EVEREST_SCHEMA_DIR@\")"
PATH_VARS
EVEREST_SCHEMA_DIR "${CMAKE_INSTALL_DATADIR}/everest/schemas"
)
endif ()
# testing
if(EVEREST_FRAMEWORK_BUILD_TESTING)
include(CTest)
add_subdirectory(tests)
else()
message(STATUS "Not running unit tests")
endif()
# configure clang-tidy if requested
if(CMAKE_RUN_CLANG_TIDY)
message("Enabling clang-tidy")
set(CMAKE_CXX_CLANG_TIDY clang-tidy)
endif()
# build doxygen documentation if doxygen is available
find_package(Doxygen)
if(DOXYGEN_FOUND)
set( DOXYGEN_OUTPUT_DIRECTORY dist/docs )
doxygen_add_docs(doxygen-${PROJECT_NAME} everest.js include lib src)
else()
message("Doxygen is needed to generate documentation")
endif()

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,13 @@
# EVerest Framework
This subproject of EVerest is providing a mechanism to manage dependencies between different modules communicating with an wrapped MQTT protocol. On startup it parses a set of configuration file, checks them agains the manifests of different modules and launches each module needed.
Additional documentation can be found in [docs](docs).
The framework message handler thread pool scaling policy is selected at CMake
configure time with `EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY`. Supported
values are `latency` (default), `greedy`, `conservative`, `fixed_size` and
`custom`. The latency and fixed-size policies have additional CMake options for
their thresholds. See the main EVerest documentation under
`docs/source/explanation/dev-tools/edm.rst` for full build examples and the
custom policy interface.

View File

@@ -0,0 +1,4 @@
# Third-party dependencies used by this project
- [CodeCoverage.cmake](https://github.com/bilke/cmake-modules/blob/master/CodeCoverage.cmake) licensed under [The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause)
- [Macros for metaprogramming](https://github.com/jspahrsummers/libextobjc/blob/master/extobjc/metamacros.h) licensed under [The MIT License](https://opensource.org/licenses/MIT)

View File

@@ -0,0 +1,307 @@
load("@rules_python//python:defs.bzl", "PyInfo")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
def _everest_env(ctx):
"""Everest Root rule
Rule creates a everest root from provided modules and config file.
"""
# Validate the input - we make sure that the set of provided modules matches
# the set of modules declared in config.yaml
validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
ctx.actions.run(
inputs = ctx.attr.config_file[DefaultInfo].files,
outputs = [validation_output],
executable = ctx.executable._validation_tool,
arguments = [
"--output",
validation_output.path,
"--config",
ctx.attr.config_file[DefaultInfo].files.to_list()[0].path,
"--",
] +
[mod.label.name for mod in ctx.attr.modules],
)
symlinks = {}
files = []
py_toolchain = ctx.toolchains["@bazel_tools//tools/python:toolchain_type"]
py_interpreter = py_toolchain.py3_runtime.interpreter.dirname.removeprefix("external/")
py_imports = []
py_transitive_sources = []
# Python modules get a special handling.
for mod in ctx.attr.modules + ctx.attr.test_modules:
if PyInfo in mod:
py_imports.extend(mod[PyInfo].imports.to_list())
py_transitive_sources.extend(mod[PyInfo].transitive_sources.to_list())
for mod in ctx.attr.modules + ctx.attr.test_modules:
# Find the manifest in the data_runfiles and use its path as prefix.
manifest = [
file
for file in mod[DefaultInfo].data_runfiles.files.to_list()
if file.basename in ["manifest.yaml", "manifest.yml"]
][0]
prefix = manifest.dirname
symlinks.update(
{
"libexec/everest/modules/{0}{1}".format(
mod.label.name,
file.path.removeprefix(prefix),
): file
for file in mod[DefaultInfo].data_runfiles.files.to_list()
if file.path.startswith(prefix)
},
)
[
files.append(file)
for file in mod[DefaultInfo].default_runfiles.files.to_list()
if not file.path.startswith(prefix)
]
config_file = ctx.attr.config_file[DefaultInfo].files.to_list()[0]
config_path = "etc/everest/{0}".format(config_file.basename)
symlinks.update({"bin/manager_impl": ctx.attr.manager[DefaultInfo].files.to_list()[0]})
symlinks.update(
{
config_path: config_file,
},
)
symlinks.update(
{
"etc/everest/default_logging.cfg": ctx.attr.default_logging_file[DefaultInfo].files.to_list()[0],
},
)
# EVerest expects that there is a `share/everest/www` directory but does
# not care about the content... We just symlink the config.yaml into it.
symlinks.update(
{
"share/everest/www/config.yaml": ctx.attr.config_file[DefaultInfo].files.to_list()[0],
},
)
symlinks.update(
{
"share/everest/schemas/{0}".format(file.basename): file
for file in ctx.attr.schemas[DefaultInfo].files.to_list()
},
)
symlinks.update(
{
"share/everest/interfaces/{0}".format(file.basename): file
for interfaces in ctx.attr.interfaces
for file in interfaces[DefaultInfo].files.to_list()
},
)
symlinks.update(
{
"share/everest/types/{0}".format(file.basename): file
for types in ctx.attr.types
for file in types[DefaultInfo].files.to_list()
},
)
symlinks.update(
{
"share/everest/errors/{0}".format(file.basename): file
for errors in ctx.attr.errors
for file in errors[DefaultInfo].files.to_list()
},
)
# For the executable we need to export the python specific variables by
# hand.
script = ctx.actions.declare_file("manager_wrapper.{}".format(ctx.label.name))
script_content = """\
#!/bin/sh
set -eu
SCRIPT_DIR=$(cd "$(dirname "$0")/.." && pwd)
export PATH="$SCRIPT_DIR/{py_interpreter}:$PATH"
{pythonpath_lines}
""".format(
py_interpreter = py_interpreter.removeprefix("external/"),
pythonpath_lines = "\n".join([
'export PYTHONPATH="$SCRIPT_DIR/{0}:$PYTHONPATH"'.format(imp)
for imp in py_imports
]),
)
if ctx.attr._is_test:
script_content += """
exec bin/manager_impl --prefix . --config {0} --check
""".format(config_path)
else:
script_content += """
if [ $# -gt 0 ]; then
exec bin/manager_impl "$@"
else
exec bin/manager_impl --prefix . --config {0}
fi
""".format(config_path)
ctx.actions.write(script, script_content, is_executable = True)
symlinks.update({"bin/manager": script})
runfiles = ctx.runfiles(
symlinks = symlinks,
files = files + [script],
)
return [
DefaultInfo(
executable = script,
runfiles = runfiles,
),
OutputGroupInfo(_validation = depset([validation_output])),
PyInfo(
imports = depset(py_imports),
transitive_sources = depset(py_transitive_sources),
),
]
ATTRS = {
"config_file": attr.label(
doc = """
The EVerest configuration file. It will be linked to
`/etc/everest/<basename>`""",
allow_single_file = True,
),
"manager": attr.label(
doc = "The EVerest manager.",
default = Label("//lib/everest/framework:manager"),
allow_single_file = True,
executable = True,
cfg = "target",
),
"schemas": attr.label(
doc = "The target with the EVerest schemas.",
default = Label("//lib/everest/framework/schemas"),
),
"interfaces": attr.label_list(
doc = "A list of targets with EVerest interfaces.",
default = [
Label("//lib/everest/framework/everestrs/tests/interfaces"),
],
),
"types": attr.label_list(
doc = "A list of targets with EVerest types.",
default = [
Label("//lib/everest/framework/everestrs/tests/types"),
],
),
"errors": attr.label_list(
doc = "A list of targets with EVerest errors.",
default = [
Label("//lib/everest/framework/everestrs/tests/errors"),
],
),
"default_logging_file": attr.label(
doc = "The target with the EVerest logging.ini file.",
default = Label("//lib/everest/framework/everestrs/tests:logging.ini"),
allow_single_file = True,
),
"modules": attr.label_list(
doc = """
The list of targets with the EVerest modules under test.
The rule validates that the set of provided modules matches the set of modules
defined in the given `config_file`.""",
allow_files = False,
),
"test_modules": attr.label_list(
doc = """
The list of targets with EVerest modules which are only enabled by the
`everest.testing` framework.
The rule will not enforce that these modules are defined in the given
`config_file`.
""",
allow_files = False,
),
"_validation_tool": attr.label(
default = Label("//lib/everest/framework/bazel/validate"),
executable = True,
cfg = "exec",
),
"_is_test": attr.bool(
default = False,
doc = "Indicates if target is test target to validate config",
),
}
everest_impl_env = rule(
implementation = _everest_env,
attrs = ATTRS,
executable = True,
toolchains = ["@bazel_tools//tools/python:toolchain_type"],
)
_everest_impl_test = rule(
implementation = _everest_env,
attrs = dict(ATTRS, _is_test = attr.bool(default = True)),
toolchains = ["@bazel_tools//tools/python:toolchain_type"],
doc = """
Creates an EVerest Test.
Example:
Suppose you have the EVerest modules `ModuleFoo` and `ModuleBar` and the
EVerest config `my_config.yaml` which uses both modules. The test will launch
the modules and return when the manager process returns.
Then you can create an environment by writing:
```
everest_test(
name = "my_everest_env",
modules = [":ModuleFoo", ":ModuleBar"],
config_file = ":my_config.yaml",
test_script=":my_test_script",
)
```
You can run it with `bazel test`.
""",
test = True,
)
def everest_test(name, target_compatible_with = [], **kwargs):
"""Wrapper around the everest_test rule that marks the target as
incompatible with cross-compilation platforms (no Python toolchain)."""
_everest_impl_test(
name = name,
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE + target_compatible_with,
**kwargs
)
def everest_env(name, target_compatible_with = [], **kwargs):
"""
Creates an EVerest environment.
Example:
Suppose you have the EVerest modules `ModuleFoo` and `ModuleBar` and the
EVerest config `my_config.yaml` which uses both modules.
Then you can create an environment by writing:
```
everest_env(
name = "my_everest_env",
modules = [":ModuleFoo", ":ModuleBar"],
config_file = ":my_config.yaml",
)
```
You can either run this target with `bazel run` or pass it for example to a (py)
test which will run your tests against the environment.
"""
everest_impl_env(
name = name,
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE + target_compatible_with,
**kwargs
)
everest_test(name = name + "__manager_test", tags = ["exclusive"], target_compatible_with = target_compatible_with, **kwargs)

View File

@@ -0,0 +1,22 @@
def rs_everest_module(
name,
manifest,
binary):
native.genrule(
name = "copy_to_subdir",
srcs = [binary, manifest],
outs = [
"{}/manifest.yaml".format(name),
"{}/{}".format(name, name),
],
cmd = "mkdir -p $(RULEDIR)/{} && ".format(name) +
"cp $(location {}) $(RULEDIR)/{}/{} && ".format(binary, name, name) +
"cp $(location {}) $(RULEDIR)/{}/".format(manifest, name),
)
native.filegroup(
name = name,
srcs = [
":copy_to_subdir",
],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,11 @@
load("@everest_framework_validate_crate_index//:defs.bzl", "all_crate_deps")
load("@rules_rust//rust:defs.bzl", "rust_binary")
rust_binary(
name = "validate",
srcs = glob(["src/**/*.rs"]),
deps = all_crate_deps(),
visibility = ["//visibility:public"],
edition = "2021",
)

View File

@@ -0,0 +1,305 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "everest_validate"
version = "0.1.0"
dependencies = [
"clap",
"serde",
"serde_yaml",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"

View File

@@ -0,0 +1,11 @@
[package]
name = "everest_validate"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
serde = { version = "1.0.200", features = ["derive"] }
serde_yaml = "0.9.34"

View File

@@ -0,0 +1,64 @@
use clap::Parser;
/// Validates the EVerest config.
#[derive(Parser, Debug)]
struct Args {
/// The output file to touch. Bazel will look for this one.
#[arg(long)]
output: String,
/// The input file containing the config. We will parse this file.
#[arg(long)]
config: String,
/// The list of expected modules.
#[arg(required = true)]
modules: Vec<String>,
}
/// The relevant sub-portion of EVerest's config.
mod config {
#[derive(Debug, serde::Deserialize)]
pub struct Module {
pub module: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct EverestConfig {
pub active_modules: std::collections::HashMap<String, Module>,
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let config = std::fs::read_to_string(args.config)?;
let config: config::EverestConfig = serde_yaml::from_str(&config)?;
let config_modules: std::collections::HashSet<_> = config
.active_modules
.into_values()
.filter_map(|m| {
if &m.module == "ProbeModule" {
None
} else {
Some(m.module)
}
})
.collect();
let given_modules: std::collections::HashSet<_> = args.modules.into_iter().collect();
assert!(
given_modules == config_modules,
"given_modules != config_modules.\ngiven_modules: {:?}\nconfig_modules: {:?}",
given_modules,
config_modules
);
std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(args.output)?;
Ok(())
}

View File

@@ -0,0 +1,42 @@
# check node API version based on https://nodejs.org/api/n-api.html#node-api-version-matrix
function(get_node_api_version NODE_VERSION_IN OUTPUT_NODE_API_VERSION)
string(REPLACE "v" "" NODE_VERSION "${NODE_VERSION_IN}")
set(${OUTPUT_NODE_API_VERSION} "" PARENT_SCOPE)
if( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "16")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "15.12")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.22"))
set(${OUTPUT_NODE_API_VERSION} 8 PARENT_SCOPE)
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "15")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "14.12")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.19")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.23"))
set(${OUTPUT_NODE_API_VERSION} 7 PARENT_SCOPE)
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "14")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.17")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.20"))
set(${OUTPUT_NODE_API_VERSION} 6 PARENT_SCOPE)
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "13")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.11")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.17"))
set(${OUTPUT_NODE_API_VERSION} 5 PARENT_SCOPE)
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "11.8")
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.16"))
set(${OUTPUT_NODE_API_VERSION} 4 PARENT_SCOPE)
elseif(("${NODE_VERSION}" VERSION_GREATER_EQUAL "10"))
set(${OUTPUT_NODE_API_VERSION} 3 PARENT_SCOPE)
endif()
endfunction()
function(require_node_api_version NODE_VERSION NODE_API_VERSION_REQUIRED)
get_node_api_version("${NODE_VERSION}" NODE_API_VERSION)
if ("${NODE_API_VERSION}" STREQUAL "")
message(FATAL_ERROR "Could not determine a Node-API version from the provided nodejs version '${NODE_VERSION}'")
endif()
if("${NODE_API_VERSION}" LESS "${NODE_API_VERSION_REQUIRED}")
message(FATAL_ERROR "Node-API version ${NODE_API_VERSION_REQUIRED} or higher is required. However your nodejs version '${NODE_VERSION}' can only provide Node-API version '${NODE_API_VERSION}'")
else()
message(STATUS "Found nodejs version '${NODE_VERSION}' that can provide Node-API version '${NODE_API_VERSION}' which satifies the requirement of Node-API version '${NODE_API_VERSION_REQUIRED}'")
endif()
endfunction()

View File

@@ -0,0 +1,14 @@
# FIXME (aw): quite hacky, should check at least if target already exists
add_library(mqttc STATIC IMPORTED)
find_library(MQTTC_LIB_FILE mqttc)
find_path(MQTTC_INCLUDE_DIR mqtt.h)
if(NOT MQTTC_LIB_FILE OR NOT MQTTC_INCLUDE_DIR)
message(FATAL_ERROR "Could not find mqttc library")
endif()
set_target_properties(mqttc PROPERTIES
IMPORTED_LOCATION ${MQTTC_LIB_FILE}
INTERFACE_INCLUDE_DIRECTORIES ${MQTTC_INCLUDE_DIR}
)

View File

@@ -0,0 +1,17 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(everest-tls)
find_dependency(everest-helpers)
find_dependency(everest-util)
find_dependency(nlohmann_json)
find_dependency(nlohmann_json_schema_validator)
find_dependency(fmt)
find_dependency(date)
set(EVEREST_SCHEMA_DIR "@PACKAGE_EVEREST_SCHEMA_DIR@")
include(${CMAKE_CURRENT_LIST_DIR}/everest-framework-targets.cmake)
check_required_components(everest-framework)

View File

@@ -0,0 +1,81 @@
---
libwebsockets:
git: https://github.com/warmcat/libwebsockets.git
git_tag: v4.5.2
cmake_condition: "EVEREST_ENABLE_ADMIN_PANEL_BACKEND"
options:
- CMAKE_POLICY_VERSION_MINIMUM 3.5
- CMAKE_POLICY_DEFAULT_CMP0077 NEW
- LWS_ROLE_RAW_FILE OFF
- LWS_UNIX_SOCK OFF
- LWS_IPV6 ON
- LWS_WITH_SYS_STATE OFF
- LWS_WITH_SYS_SMD OFF
- LWS_WITH_UPNG OFF
- LWS_WITH_JPEG OFF
- LWS_WITH_DLO OFF
- LWS_WITH_SECURE_STREAMS OFF
- LWS_WITH_STATIC OFF
- LWS_WITH_LHP OFF
- LWS_WITH_LEJP_CONF OFF
- LWS_WITH_MINIMAL_EXAMPLES OFF
- LWS_WITH_CACHE_NSCOOKIEJAR OFF
- LWS_WITHOUT_TESTAPPS ON
- LWS_WITHOUT_TEST_SERVER ON
- LWS_WITHOUT_TEST_SERVER_EXTPOLL ON
- LWS_WITHOUT_TEST_PING ON
- LWS_WITHOUT_TEST_CLIENT ON
- LWS_INSTALL_LIB_DIR ${CMAKE_INSTALL_LIBDIR}
- DISABLE_WERROR ON
nlohmann_json:
git: https://github.com/nlohmann/json
git_tag: v3.12.0
options: ["JSON_BuildTests OFF", "JSON_MultipleHeaders ON"]
nlohmann_json_schema_validator:
git: https://github.com/pboettch/json-schema-validator
git_tag: 2.4.0
options:
[
"JSON_VALIDATOR_INSTALL OFF",
"JSON_VALIDATOR_BUILD_TESTS OFF",
"JSON_VALIDATOR_BUILD_EXAMPLES OFF",
"JSON_VALIDATOR_BUILD_SHARED_LIBS ON",
]
mosquitto:
git: https://github.com/eclipse-mosquitto/mosquitto
git_tag: v2.0.22
options:
- DOCUMENTATION OFF
- WITH_BROKER OFF
- WITH_APPS OFF
- WITH_PLUGINS OFF
- WITH_TESTS OFF
libfmt:
git: https://github.com/fmtlib/fmt.git
git_tag: 12.1.0
options:
["FMT_TEST OFF", "FMT_DOC OFF", "BUILD_SHARED_LIBS ON", "FMT_INSTALL ON", "FMT_SYSTEM_HEADERS ON"]
date:
git: https://github.com/HowardHinnant/date.git
git_tag: v3.0.4
options:
[
"BUILD_TZ_LIB ON",
"HAS_REMOTE_API 0",
"USE_AUTOLOAD 0",
"USE_SYSTEM_TZ_DB ON",
"BUILD_SHARED_LIBS ON",
]
catch2:
git: https://github.com/catchorg/Catch2.git
git_tag: v3.9.0
cmake_condition: "EVEREST_FRAMEWORK_BUILD_TESTING OR EVEREST_CORE_BUILD_TESTING"
pybind11:
git: https://github.com/pybind/pybind11.git
git_tag: v3.0.2
cmake_condition: "EVEREST_ENABLE_PY_SUPPORT"
options: ["PYBIND11_TEST OFF", "PYBIND11_USE_CROSSCOMPILING ON"]
pybind11_json:
git: https://github.com/pybind/pybind11_json.git
git_tag: 0.2.15
cmake_condition: "EVEREST_ENABLE_PY_SUPPORT"

View File

@@ -0,0 +1,33 @@
# EVerest Boot Mode Application Logic
The `manager` process retrieves the EVerest configuration, manages module dependencies and communication, and starts EVerest modules as individual processes.
The EVerest configuration can be provided via a YAML file, an SQLite database, or both. This section explains how EVerest starts up depending on the CLI options used:
- `--config`: Full path to the EVerest YAML configuration file.
- `--db`: Full path to the EVerest SQLite configuration database.
- `--db-init`: Indicates that the specified config should be used to initialize the database if it does not exist or does not contain a valid configuration.
Based on these options, there are three possible boot modes:
- **YAML Boot Mode**: The configuration is loaded from a YAML file. This mode is used when only the `--config` argument is provided.
- **Database Boot Mode**: The configuration is loaded from an SQLite database. This mode is used when only the `--db` argument is provided.
- **DatabaseInit Boot Mode**: The configuration is preferably loaded from an SQLite database. If the database does not exist or does not contain a valid EVerest configuration, the YAML file specified by `--config` is used instead, and the configuration is then written to the database. This mode requires all three options: `--config`, `--db`, and `--db-init`. In a subsequent start of the application, the database can be used to retrieve
the configuration.
## Boot Mode Application Logic Flow Chart
```mermaid
flowchart TD
A[Start] --> B[Parse CLI arguments]
B --> C{--config provided}
C -->|yes| D{--db provided}
D -->|yes| E{--db-init provided}
E -->|yes| F[Boot Mode: DatabaseInit]
E -->|no| H[Invalid combination]
D -->|no| J[Boot Mode: YAML]
C -->|no| L{--db provided}
L -->|yes| M[Boot Mode: Database]
L -->|no| O[Invalid combination]
```

View File

@@ -0,0 +1,134 @@
# EVerest Framework MQTT Topic Structure
This document describes the MQTT topic structure used by the EVerest framework for communication between modules.
## Topic Prefix Structure
The EVerest framework uses a configurable MQTT prefixes for topics. This allows multiple instances of EVerest
to run at the same time using the same broker. The default prefix is `everest`.
## 1. Variables (Vars)
The following structure applies for variable topics:
### Topic Structure
```bash
{everest_prefix}modules/{module_id}/impl/{impl_id}/var/{var_name}
```
### Message Payload Structure
The payload contains the actual variable data in the `data` field.
```json
{
"data": <variable_value>
}
```
## 2. Commands (Cmds)
The following structure applies for command topics. Modules that provide (implement) the command subscribe to the command topic, while modules that call the command publish to it. Each command call generates a unique UUID as the call ID. The origin field identifies the calling module. Command handlers process the request and publish responses to a separate response topic.
### Topic Structure
```bash
{everest_prefix}modules/{module_id}/impl/{impl_id}/cmd/{cmd_name}
```
### Message Payload Structure
```json
{
"id": "<unique_call_id>",
"args": {
"arg1": "value1",
"arg2": "value2"
},
"origin": "<calling_module_id>"
}
```
## 3. Command Responses
The following structure applies for command response topics. These are used to send back results or errors from command handlers to the calling module. Response topics include the calling module ID for proper routing. The call ID matches the original command request.
### Topic Structure
```bash
{everest_prefix}modules/{module_id}/impl/{impl_id}/cmd/{cmd_name}/response/{calling_module_id}
```
### Message Payload Structure
#### Successful Response
```json
{
"name": "<cmd_name>",
"type": "result",
"data": {
"id": "<matching_call_id>",
"retval": <return_value>,
"origin": "<responding_module_id>"
}
}
```
#### Error Response
```json
{
"name": "<cmd_name>",
"type": "result",
"data": {
"id": "<matching_call_id>",
"error": {
"event": "<error_type>",
"msg": "<error_message>"
},
"origin": "<responding_module_id>"
}
}
```
### Error Types
Command responses can contain the following error events:
- `MessageParsingFailed`: JSON parsing error
- `SchemaValidationFailed`: Schema validation error
- `HandlerException`: Exception in command handler
- `Timeout`: Command execution timeout
- `Shutdown`: System shutdown
- `NotReady`: Module not ready
## 4. Errors
The following structure applies for error topics. Errors are raised by modules and can be subscribed to by other modules. Each error has a unique UUID and includes information about the originating module, implementation, EVSE, and connector if applicable.
### Topic Structure
```bash
{everest_prefix}modules/{module_id}/impl/{impl_id}/error/{error_type}
```
### Message Payload Structure
```json
{
"type": "<error_namespace>/<error_name>",
"message": "<error_description>",
"severity": "<error_severity>",
"origin": {
"module_id": "<originating_module>",
"implementation_id": "<originating_impl>",
"evse": <evse_number>,
"connector": <connector_number>
},
"state": "<error_state>",
"timestamp": "<iso_timestamp>",
"uuid": "<unique_error_id>"
}
```

View File

@@ -0,0 +1,120 @@
# Module configuration distributed via MQTT
Since everest-framework 0.19.0 the module configuration is parsed once
by the manager and then distributed to the modules via MQTT.
This is achieved by parsing the MQTT settings from the config,
spawning the modules and passing these MQTT settings to them.
The modules themselves then ask for their module config via MQTT,
which is in turn provided to them from the manager.
After the modules have received their config, their init() function is called.
Afterwards they signal ready to the manager.
The manager sends out the global ready signal
once it has received all Module ready signals.
The following sequence diagram illustrates this startup process
```mermaid
sequenceDiagram
create participant manager
create participant ManagerSettings
manager-)ManagerSettings: ManagerSettings(prefix, config_path)
ManagerSettings-->>manager: return ms
create participant ManagerConfig
manager-)ManagerConfig: ManagerConfig(ms)
create participant MQTTAbstraction
manager-)MQTTAbstraction: MQTTAbstraction(ms.mqtt_settings)
MQTTAbstraction-->>manager: return mqtt_abstraction
activate manager
manager->>manager: start_modules()
manager->>ManagerConfig: serialize()
ManagerConfig-->>manager: serialized_config
manager->>MQTTAbstraction: publish(interfaces, types, schemas, manifests, settings, retain=true)
loop For every module
manager->>manager: spawn_modules(Module)
create participant Module
manager->>Module: spawn Module
Module->>MQTTAbstraction: get(Config)
MQTTAbstraction->>manager: get(Config of Module)
manager-->>MQTTAbstraction: publish(module configs, mappings)
MQTTAbstraction-->>Module: publish(module configs, mappings)
Module->>Module: init
Module->>MQTTAbstraction: publish(ready)
MQTTAbstraction->>manager: publish(ready of Module)
end
manager->>MQTTAbstraction: publish global ready
```
Class diagram
```mermaid
classDiagram
ConfigBase <|-- ManagerConfig
ConfigBase <|-- Config
MQTTSettings *-- ConfigBase
ManagerSettings *-- ManagerConfig
note for ConfigBase "
Baseclass containing json config, manifests, interfaces,
types and functions to access this information which
needs to be available in all derived classes
"
class ManagerSettings{
+fs::path configs_dir
+fs::path schemas_dir
+fs::path interfaces_dir
+fs::path types_dir
+fs::path errors_dir
+fs::path config_file
+fs::path www_dir
+int controller_port
+int controller_rpc_timeout_ms
+std::string run_as_user
+std::string version_information
+nlohmann::json config
+MQTTSettings mqtt_settings
+std::unique_ptr<RuntimeSettings> runtime_settings
+ManagerSettings(const std::string& prefix, const std::string& config)
+const RuntimeSettings& get_runtime_settings()
}
class MQTTSettings{
+std::string broker_socket_path
+std::string broker_host
+int broker_port
+std::string everest_prefix
+std::string external_prefix
+bool uses_socket()
}
class ConfigBase{
#const MQTTSettings mqtt_settings
+ConfigBase(const MQTTSettings& mqtt_settings)
}
class ManagerConfig{
-const ManagerSettings& ms
+ManagerConfig(const ManagerSettings& ms)
+nlohmann::json serialize()
-load_and_validate_manifest(const std::string& module_id, const nlohmann::json& module_config)
-std::tuple~nlohmann::json, int64_t~ load_and_validate_with_schema(const fs::path& file_path, const nlohmann::json& schema)
-nlohmann::json resolve_interface(const std::string& intf_name)
-nlohmann::json load_interface_file(const std::string& intf_name)
-resolve_all_requirements()
-parse(nlohmann::json config)
}
class Config{
+Config(const MQTTSettings& mqtt_settings, nlohmann::json config)
+bool module_provides(const std::string& module_name, const std::string& impl_id);
+nlohmann::json get_module_cmds(const std::string& module_name, const std::string& impl_id)
+nlohmann::json resolve_requirement(const std::string& module_id, const std::string& requirement_id)
+std::list~Requirement~ get_requirements(const std::string& module_id)
+RequirementInitialization get_requirement_initialization(const std::string& module_id)
+ModuleConfigs get_module_configs(const std::string& module_id)
+nlohmann::json get_module_json_config(const std::string& module_id)
+ModuleInfo get_module_info(const std::string& module_id)
+std::optional~<~TelemetryConfig~ get_telemetry_config()
+nlohmann::json get_interface_definition(const std::string& interface_name) const;
}
```

View File

@@ -0,0 +1,3 @@
# Additional documentation about the inner workings of EVerest Framework
[MQTT Config distribution](MQTTConfigDistribution.md)

View File

@@ -0,0 +1,2 @@
*node_modules
package-lock.json

View File

@@ -0,0 +1,120 @@
find_program(
NODE
node
REQUIRED
)
execute_process(
COMMAND ${NODE} --version
OUTPUT_VARIABLE NODE_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# We need Node-API version 6 or higher
set(NODE_API_VERSION_REQUIRED 6)
include(NodeApiVersion)
require_node_api_version("${NODE_VERSION}" "${NODE_API_VERSION_REQUIRED}")
find_program(
NPM
npm
REQUIRED
)
# Include Node-API wrappers
# FIXME (aw): we want this as an requirement, not implicitely install it by ourself
execute_process(
COMMAND
${NPM} list -p node-addon-api --loglevel=error | grep node-addon-api
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
OUTPUT_VARIABLE
NODE_ADDON_API_PACKAGE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(NODE_ADDON_API_INSTALL_VERSION "8.1")
message(STATUS "Adding node-addon-api@${NODE_ADDON_API_INSTALL_VERSION}")
if (NOT NODE_ADDON_API_PACKAGE_DIR)
execute_process(
COMMAND
${NPM} install node-addon-api@${NODE_ADDON_API_INSTALL_VERSION}
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
OUTPUT_QUIET
RESULT_VARIABLE
NPM_INSTALL_NODE_ADDON_API_FAILED
)
if (NPM_INSTALL_NODE_ADDON_API_FAILED)
message(FATAL_ERROR "Installation of node-addon-api failed")
endif ()
endif ()
execute_process(
COMMAND ${NODE} -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
OUTPUT_VARIABLE NODE_ADDON_API_DIR
)
string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}")
find_path(NODEJS_INCLUDE_DIR "node_api.h"
PATH_SUFFIXES
"node"
"node16"
"node17"
"node18"
"node19"
"node20"
"node21"
"node22"
"nodejs/src"
HINTS
"$ENV{HOME}/.nvm/versions/node/${NODE_VERSION}/include"
REQUIRED
)
add_library(everestjs SHARED)
target_sources(everestjs
PRIVATE
everestjs.cpp
conversions.cpp
js_exec_ctx.cpp
)
target_compile_options(everestjs PRIVATE ${COMPILER_WARNING_OPTIONS})
# define NAPI_VERSION
target_compile_definitions(everestjs
PRIVATE
-DNAPI_VERSION=${NODE_API_VERSION_REQUIRED}
-DNAPI_CPP_EXCEPTIONS
)
set_target_properties(everestjs PROPERTIES PREFIX "" SUFFIX ".node")
target_include_directories(everestjs
PRIVATE
$<BUILD_INTERFACE:${NODE_ADDON_API_DIR}>
$<BUILD_INTERFACE:${NODEJS_INCLUDE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_link_libraries(everestjs
PRIVATE
everest::framework
everest::log
)
install(
TARGETS everestjs
LIBRARY
DESTINATION ${CMAKE_INSTALL_LIBDIR}/everest/node_modules/everestjs
)
install(
FILES
index.js
package.json
DESTINATION ${CMAKE_INSTALL_LIBDIR}/everest/node_modules/everestjs
)

View File

@@ -0,0 +1,213 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#include "conversions.hpp"
#include <everest/exceptions.hpp>
#include <everest/logging.hpp>
#include <utils/error/error_json.hpp>
namespace EverestJs {
Everest::json convertToJson(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsNull() || value.IsUndefined()) {
return Everest::json(nullptr);
} else if (value.IsString()) {
return Everest::json(std::string(value.As<Napi::String>()));
} else if (value.IsNumber()) {
int64_t intNumber = value.As<Napi::Number>();
double floatNumber = value.As<Napi::Number>();
if (floatNumber == intNumber)
return Everest::json(intNumber);
return Everest::json(floatNumber);
} else if (value.IsBoolean()) {
return Everest::json(bool(value.As<Napi::Boolean>()));
} else if (value.IsArray()) {
auto j = Everest::json::array();
Napi::Array array = value.As<Napi::Array>();
for (uint64_t i = 0; i < array.Length(); i++) {
Napi::Value entry = Napi::Value(array[i]);
j[i] = convertToJson(entry);
}
return j;
} else if (value.IsObject() && value.Type() == napi_object) {
auto j = Everest::json({});
Napi::Object obj = value.As<Napi::Object>();
Napi::Array keys = obj.GetPropertyNames();
for (uint64_t i = 0; i < keys.Length(); i++) {
Napi::Value key = keys[i];
if (key.IsString()) {
std::string k = key.As<Napi::String>();
Napi::Value v = Napi::Value(obj[k]);
j[k] = convertToJson(v);
} else {
EVTHROW(EVEXCEPTION(Everest::EverestApiError,
"Javascript type of object key can not be converted to Everest::json: ",
napi_valuetype_strings[key.Type()]));
}
}
return j;
}
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::json: ",
napi_valuetype_strings[value.Type()]));
}
Everest::TelemetryMap convertToTelemetryMap(const Napi::Object& obj) {
BOOST_LOG_FUNCTION();
Everest::TelemetryMap telemetry;
Napi::Array keys = obj.GetPropertyNames();
for (uint64_t i = 0; i < keys.Length(); i++) {
Napi::Value key = keys[i];
if (key.IsString()) {
std::string k = key.As<Napi::String>();
Napi::Value value = Napi::Value(obj[k]);
if (value.IsString()) {
telemetry[k] = std::string(value.As<Napi::String>());
} else if (value.IsNumber()) {
int intNumber = value.As<Napi::Number>();
double floatNumber = value.As<Napi::Number>();
if (floatNumber == intNumber) {
telemetry[k] = intNumber;
} else {
telemetry[k] = floatNumber;
}
} else if (value.IsBoolean()) {
telemetry[k] = bool(value.As<Napi::Boolean>());
}
}
}
return telemetry;
}
Napi::Value convertToNapiValue(const Napi::Env& env, const json& value) {
BOOST_LOG_FUNCTION();
if (value.is_null()) {
return env.Null();
} else if (value.is_string()) {
return Napi::String::New(env, std::string(value));
} else if (value.is_number_integer()) {
return Napi::Number::New(env, int64_t(value));
} else if (value.is_number_float()) {
return Napi::Number::New(env, double(value));
} else if (value.is_boolean()) {
return Napi::Boolean::New(env, bool(value));
} else if (value.is_array()) {
Napi::Array v = Napi::Array::New(env);
for (uint64_t i = 0; i < value.size(); i++) {
v.Set(i, convertToNapiValue(env, value[i]));
}
return v;
} else if (value.is_object()) {
Napi::Object v = Napi::Object::New(env);
for (auto& el : value.items()) {
v.Set(el.key(), convertToNapiValue(env, el.value()));
}
return v;
}
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Napi::Value: ", value));
}
Everest::error::Error convertToError(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
Everest::json j = convertToJson(value);
return j.get<Everest::error::Error>();
}
Everest::error::ErrorType convertToErrorType(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsString()) {
return Everest::error::ErrorType(std::string(value.As<Napi::String>()));
}
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::error::ErrorType: ",
napi_valuetype_strings[value.Type()]));
}
Everest::error::ErrorSubType convertToErrorSubType(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsString()) {
return Everest::error::ErrorSubType(std::string(value.As<Napi::String>()));
}
EVTHROW(EVEXCEPTION(Everest::EverestApiError,
"Javascript type can not be converted to Everest::error::ErrorSubType: ",
napi_valuetype_strings[value.Type()]));
}
Everest::error::Severity convertToErrorSeverity(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsString()) {
return Everest::error::string_to_severity(std::string(value.As<Napi::String>()));
}
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::error::Severity: ",
napi_valuetype_strings[value.Type()]));
}
Everest::error::State convertToErrorState(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsString()) {
return Everest::error::string_to_state(std::string(value.As<Napi::String>()));
}
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::error::State: ",
napi_valuetype_strings[value.Type()]));
}
Napi::Value convertToNapiValue(const Napi::Env& env, const Everest::error::Error& error) {
BOOST_LOG_FUNCTION();
json j(error);
Napi::Value res = convertToNapiValue(env, j);
return res;
}
bool isSingleErrorStateCondition(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsArray()) {
return false;
}
return true;
}
Everest::error::ErrorStateMonitor::StateCondition convertToErrorStateCondition(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsObject()) {
Napi::Object obj = value.As<Napi::Object>();
Napi::Value type = obj.Get("type");
Napi::Value sub_type = obj.Get("sub_type");
Napi::Value active = obj.Get("active");
return Everest::error::ErrorStateMonitor::StateCondition(
convertToErrorType(type), convertToErrorSubType(sub_type), bool(active.As<Napi::Boolean>()));
}
EVTHROW(EVEXCEPTION(Everest::EverestApiError,
"Javascript type can not be converted to Everest::error::ErrorStateMonitor::StateCondition: ",
napi_valuetype_strings[value.Type()]));
}
std::list<Everest::error::ErrorStateMonitor::StateCondition>
convertToErrorStateConditionList(const Napi::Value& value) {
BOOST_LOG_FUNCTION();
if (value.IsArray()) {
std::list<Everest::error::ErrorStateMonitor::StateCondition> conditions;
Napi::Array array = value.As<Napi::Array>();
for (uint64_t i = 0; i < array.Length(); i++) {
Napi::Value entry = Napi::Value(array[i]);
conditions.push_back(convertToErrorStateCondition(entry));
}
return conditions;
}
EVTHROW(EVEXCEPTION(
Everest::EverestApiError,
"Javascript type can not be converted to std::list<Everest::error::ErrorStateMonitor::StateCondition>: ",
napi_valuetype_strings[value.Type()]));
}
} // namespace EverestJs

View File

@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#ifndef CONVERSIONS_HPP
#define CONVERSIONS_HPP
#include <framework/everest.hpp>
#include <napi.h>
#include <utils/conversions.hpp>
#include <utils/types.hpp>
#include <utils/error.hpp>
#include <utils/error/error_state_monitor.hpp>
namespace EverestJs {
static const char* const napi_valuetype_strings[] = {
"undefined", //
"null", //
"boolean", //
"number", //
"string", //
"symbol", //
"object", //
"function", //
"external", //
"bigint", //
};
Everest::json convertToJson(const Napi::Value& value);
Everest::json convertToConfigMap(const Everest::json& json_config);
Everest::TelemetryMap convertToTelemetryMap(const Napi::Object& obj);
Napi::Value convertToNapiValue(const Napi::Env& env, const Everest::json& value);
// Error related
Everest::error::Error convertToError(const Napi::Value& value);
Everest::error::ErrorType convertToErrorType(const Napi::Value& value);
Everest::error::ErrorSubType convertToErrorSubType(const Napi::Value& value);
Everest::error::Severity convertToErrorSeverity(const Napi::Value& value);
Everest::error::State convertToErrorState(const Napi::Value& value);
Napi::Value convertToNapiValue(const Napi::Env& env, const Everest::error::Error& error);
// ErrorStateCondition related
bool isSingleErrorStateCondition(const Napi::Value& value);
Everest::error::ErrorStateMonitor::StateCondition convertToErrorStateCondition(const Napi::Value& value);
std::list<Everest::error::ErrorStateMonitor::StateCondition> convertToErrorStateConditionList(const Napi::Value& value);
} // namespace EverestJs
#endif // CONVERSIONS_HPP

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
const util = require('util');
const addon = require('./everestjs.node');
const helpers = {
get_default: (obj, key, defaultValue) => ((obj[key] === undefined) ? defaultValue : obj[key]),
to_string: (...values) => {
let log_string = '';
values.forEach((value) => {
if (typeof value == 'string') {
log_string += value;
} else if (typeof value == 'number') {
log_string += value;
} else if (typeof value == 'boolean') {
log_string += value ? 'true' : 'false';
} else if (typeof value == 'object') {
log_string += util.inspect(value, false, 6, true);
} else {
throw new Error(`The logging function cannot handle input of type ${typeof value}`);
}
});
return log_string;
},
};
const EverestModule = function EverestModule(handler_setup, user_settings) {
const env_settings = {
module: process.env.EV_MODULE,
prefix: process.env.EV_PREFIX,
logging_config_file: process.env.EV_LOG_CONF_FILE,
mqtt_everest_prefix: process.env.EV_MQTT_EVEREST_PREFIX,
mqtt_external_prefix: process.env.EV_MQTT_EXTERNAL_PREFIX,
mqtt_broker_socket_path: process.env.EV_MQTT_BROKER_SOCKET_PATH,
mqtt_server_address: process.env.EV_MQTT_BROKER_HOST,
mqtt_server_port: process.env.EV_MQTT_BROKER_PORT,
validate_schema: process.env.EV_VALIDATE_SCHEMA,
};
const settings = { ...env_settings, ...user_settings };
if (!settings.module) {
throw new Error('parameter "module" is missing');
}
const config = {
module: settings.module,
prefix: settings.prefix,
logging_config_file: settings.logging_config_file,
mqtt_everest_prefix: settings.mqtt_everest_prefix,
mqtt_external_prefix: helpers.get_default(settings, 'mqtt_external_prefix', ''),
mqtt_broker_socket_path: helpers.get_default(settings, 'mqtt_broker_socket_path', ''),
mqtt_server_address: helpers.get_default(settings, 'mqtt_server_address', ''),
mqtt_server_port: helpers.get_default(settings, 'mqtt_server_port', 0),
validate_schema: helpers.get_default(settings, 'validate_schema', false),
};
function callbackWrapper(on, request, ...args) {
const result = request(...args);
Promise.resolve(result).then(on.fulfill, on.reject);
}
const available_handlers = addon.boot_module.call(this, config, callbackWrapper);
const module_setup = {
setup: available_handlers,
info: this.info,
config: this.config,
mqtt: this.mqtt,
telemetry: this.telemetry,
};
if (this.mqtt === undefined) {
const missing_mqtt_getter = {
get() { throw new Error('External mqtt not available - missing enable_external_mqtt in manifest?'); },
};
Object.defineProperty(module_setup, 'mqtt', missing_mqtt_getter);
Object.defineProperty(this, 'mqtt', missing_mqtt_getter);
}
if (this.telemetry === undefined) {
const missing_telemetry_getter = {
get() { throw new Error('Telemetry not available - missing enable_telemetry in manifest?'); },
};
Object.defineProperty(module_setup, 'telemetry', missing_telemetry_getter);
Object.defineProperty(this, 'telemetry', missing_telemetry_getter);
}
// check, if we need to register cmds
if (typeof handler_setup === 'undefined') {
if (Object.keys(available_handlers.provides).length !== 0) {
throw new Error('handler setup callback is missing - you need to register at least one command');
}
return addon.signal_ready.call(this);
}
if (typeof handler_setup === 'function') {
return Promise.resolve(handler_setup.call({}, module_setup)).then(
() => addon.signal_ready.call(this)
);
}
throw new Error('handler setup callback needs to be of type function');
};
// setup log handlers
exports.evlog = {};
Object.keys(addon.log).forEach((key) => {
exports.evlog[key] = (...log_args) => addon.log[key](helpers.to_string(...log_args));
});
let boot_module_called = false;
exports.boot_module = (handler_setup, settings) => {
if (boot_module_called) throw Error('Calling initModule more than once is not supported right now');
boot_module_called = true;
return new EverestModule(handler_setup, settings);
};

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#include "js_exec_ctx.hpp"
#include "utils.hpp"
void JsExecCtx::tramp(Napi::Env env, Napi::Function callback, std::nullptr_t* context, JsExecCtx* this_) {
(void)context; // this is unused in this callback
try {
std::vector<napi_value> args{this_->result_handler_ref.Value()};
if (this_->arg_func != nullptr) {
// append args from our arg function via move magic ...
auto append_args = this_->arg_func(env);
args.reserve(args.size() + append_args.size());
std::move(std::begin(append_args), std::end(append_args), std::back_inserter(args));
append_args.clear();
}
callback.Call(args);
} catch (std::exception& e) {
EVLOG_AND_RETHROW(env);
}
}
Napi::Value JsExecCtx::on_fulfill(const Napi::CallbackInfo& info) {
JsExecCtx* this_ = reinterpret_cast<JsExecCtx*>(info.Data());
if (this_->res_func != nullptr) {
this_->res_func(info, false);
}
this_->promise.set_value();
return info.Env().Undefined();
}
Napi::Value JsExecCtx::on_reject(const Napi::CallbackInfo& info) {
JsExecCtx* this_ = reinterpret_cast<JsExecCtx*>(info.Data());
if (this_->res_func != nullptr) {
this_->res_func(info, true);
} else {
// there is no catch handler registered, so we throw
throw Napi::Error::New(
info.Env(),
"JsExecCtx call into javascript code got rejected and could not be handled (rejection handler not defined");
}
this_->promise.set_value();
return info.Env().Undefined();
}
void JsExecCtx::exec(const ArgFuncType& arg_func, const ResFuncType& res_func) {
// FIXME (aw): we're blocking all other threads trying to call this function
// a proper solution should be found
std::unique_lock<std::mutex> lock(exec_mutex);
this->arg_func = arg_func;
this->res_func = res_func;
promise = std::promise<void>();
tsfn.BlockingCall(this);
promise.get_future().get();
}

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
//
// author: aw@pionix.de
//
#ifndef JS_EXEC_CTX_HPP
#define JS_EXEC_CTX_HPP
#include <future>
#include <napi.h>
class JsExecCtx {
private:
static void tramp(Napi::Env env, Napi::Function callback, std::nullptr_t*, JsExecCtx* this_);
static Napi::Value on_fulfill(const Napi::CallbackInfo& info);
static Napi::Value on_reject(const Napi::CallbackInfo& info);
public:
using TsfnType = Napi::TypedThreadSafeFunction<std::nullptr_t, JsExecCtx, JsExecCtx::tramp>;
using ArgFuncType = std::function<std::vector<napi_value>(Napi::Env&)>;
using ResFuncType = std::function<void(const Napi::CallbackInfo&, bool)>;
// FIXME (aw): proper module_instance handling if nullptr
JsExecCtx(const Napi::Env& env, const Napi::Function& func, const std::string& res_name = "RequestDispatcher") :
tsfn(TsfnType::New(env, func, res_name, 0, 1)),
result_handler_ref(Napi::Persistent(Napi::Object::New(env))),
func_ref(Napi::Persistent(func)) {
result_handler_ref.Value().DefineProperty(Napi::PropertyDescriptor::Value(
"fulfill", Napi::Function::New(env, on_fulfill, nullptr, this), napi_enumerable));
result_handler_ref.Value().DefineProperty(Napi::PropertyDescriptor::Value(
"reject", Napi::Function::New(env, on_reject, nullptr, this), napi_enumerable));
}
~JsExecCtx() {
tsfn.Release();
}
void exec(const ArgFuncType& arg_func, const ResFuncType& res_func);
private:
ArgFuncType arg_func;
ResFuncType res_func;
// FIXME (aw): will the referenced object be GC'd when the references get destroyed?
// and is okay to be destroyed in our async thread?
TsfnType tsfn;
Napi::ObjectReference result_handler_ref;
Napi::FunctionReference func_ref{};
std::mutex exec_mutex;
std::promise<void> promise;
};
#endif // JS_EXEC_CTX_HPP

View File

@@ -0,0 +1,9 @@
{
"name": "everestjs",
"main": "index.js",
"version": "0.25.0",
"description": "EVerest API for node.js",
"dependencies": {
"node-addon-api": "^3.2.1"
}
}

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#ifndef UTILS_HPP
#define UTILS_HPP
#include <everest/logging.hpp>
#include <everest/metamacros.hpp>
namespace EverestJs {
// this is needed to get javascript stacktraces whenever possible while still logging boost error information
#define EVLOG_AND_RETHROW(...) \
do { \
try { \
throw; \
} catch (Napi::Error & e) { \
try { \
BOOST_THROW_EXCEPTION(boost::enable_error_info(e) \
<< boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); \
} catch (boost::exception & ex) { \
char const* const* f = boost::get_error_info<boost::throw_file>(ex); \
int const* l = boost::get_error_info<boost::throw_line>(ex); \
char const* const* fn = boost::get_error_info<boost::throw_function>(ex); \
EVLOG_critical << "Catched top level Napi::Error, forwarding to javascript..." << std::endl \
<< (f ? *f : "") << ":" << (l ? std::to_string(*l) : "") << " in Function " \
<< (fn ? *fn : "") << std::endl \
<< boost::diagnostic_information(e, true) << boost::diagnostic_information(ex, false) \
<< std::endl \
<< "==============================" << std::endl \
<< std::endl; \
} \
throw; /* this will forward the exception back to javascript and enable js backtraces */ \
} catch (std::exception & e) { \
try { \
BOOST_THROW_EXCEPTION(boost::enable_error_info(e) \
<< boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); \
} catch (boost::exception & ex) { \
char const* const* f = boost::get_error_info<boost::throw_file>(ex); \
int const* l = boost::get_error_info<boost::throw_line>(ex); \
char const* const* fn = boost::get_error_info<boost::throw_function>(ex); \
EVLOG_critical << "Catched top level exception, forwarding to javascript..." << std::endl \
<< (f ? *f : "") << ":" << (l ? std::to_string(*l) : "") << " in Function " \
<< (fn ? *fn : "") << std::endl \
<< boost::diagnostic_information(e, true) << boost::diagnostic_information(ex, false) \
<< std::endl \
<< "==============================" << std::endl \
<< std::endl; \
/* this will forward the exception to javascript and enable js backtraces */ \
metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))(throw;)(EVTHROW(Napi::Error::New( \
metamacro_at(0, __VA_ARGS__), Napi::String::New(metamacro_at(0, __VA_ARGS__), e.what())));) \
} \
} \
} while (0)
} // namespace EverestJs
#endif // UTILS_HPP

View File

@@ -0,0 +1,8 @@
__pycache__/
*.so
*.egg-info/
htmlcov/
.coverage

View File

@@ -0,0 +1,17 @@
ev_create_pip_install_targets(
PACKAGE_NAME
everestpy
DIST_DEPENDS
everestpy_ln_dist
LOCAL_DEPENDS
everestpy_ln_local
)
ev_create_python_wheel_targets(
PACKAGE_NAME
everestpy
DEPENDS
everestpy_ln_dist
)
add_subdirectory(src/everest)

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,31 @@
[metadata]
name = everestpy
version = attr: everest.framework.__version__
author = 'Kai-Uwe Hermann'
author_email = kh@pionix.de
description = everest framework python binding
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://github.com/EVerest/everest-framework
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
[options]
package_dir =
= src
# packages = find:
python_requires = >=3.7
[options.packages.find]
exclude =
tests*
[options.package_data]
* = *.pyi, *.so
[options.extras_require]
test =
coverage

View File

@@ -0,0 +1,25 @@
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension")
load("@rules_python//python:py_library.bzl", "py_library")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
pybind_extension(
name = "everestpy",
srcs = glob(["everest/**/*.cpp", "everest/**/*.hpp"]),
deps = ["//lib/everest/framework:framework", "@pybind11_json//:pybind11_json"],
includes = ["everest"],
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
visibility = [
"//visibility:public",
],
)
py_library(
name = "framework",
data = [":everestpy.so"],
srcs = glob(["everest/framework/**/*.py",]),
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
visibility = [
"//visibility:public",
],
imports = ["."],
)

View File

@@ -0,0 +1,71 @@
find_package(Python3
REQUIRED
COMPONENTS
Development
)
if (DISABLE_EDM)
find_package(pybind11 REQUIRED)
find_package(pybind11_json REQUIRED)
endif()
# support overriding where the include files are to be found.
# Used for Yocto where the pybind11 libraries may have come from a cache and the
# full path is no longer the same
if (PYBIND11_INTERFACE_INCLUDE_DIRECTORIES)
set_target_properties(pybind11::pybind11 PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${PYBIND11_INTERFACE_INCLUDE_DIRECTORIES}
)
set_target_properties(pybind11_json PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${PYBIND11_INTERFACE_INCLUDE_DIRECTORIES}
)
endif()
pybind11_add_module(everestpy misc.cpp module.cpp everestpy.cpp)
target_compile_options(everestpy PRIVATE ${COMPILER_WARNING_OPTIONS})
target_link_libraries(everestpy
PRIVATE
everest::framework
everest::log
pybind11_json
fmt::fmt
)
set(EVERESTPY_LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/everest/everestpy/everest/framework)
set(EVERESTPY_DIST_INSTALL_PATH ${CMAKE_INSTALL_PREFIX}/${EVERESTPY_LIB_INSTALL_DIR}/$<TARGET_FILE_NAME:everestpy>)
add_custom_target(everestpy_ln_dist
COMMAND
test -f $$DESTDIR${EVERESTPY_DIST_INSTALL_PATH} || (echo "everestpy library not found, did you run the install target?" && false)
COMMAND
${CMAKE_COMMAND} -E create_symlink $$DESTDIR${EVERESTPY_DIST_INSTALL_PATH} $<TARGET_FILE_NAME:everestpy>
WORKING_DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR}/framework
COMMENT
"Creating symlink to distributed everestpy"
)
add_custom_target(everestpy_ln_local
COMMAND
${CMAKE_COMMAND} -E create_symlink $<TARGET_FILE:everestpy> $<TARGET_FILE_NAME:everestpy>
WORKING_DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR}/framework
DEPENDS
everestpy
COMMENT
"Creating symlink to build everestpy"
)
install(
TARGETS everestpy
LIBRARY
DESTINATION ${EVERESTPY_LIB_INSTALL_DIR}
)
install(
FILES
framework/__init__.py
DESTINATION ${EVERESTPY_LIB_INSTALL_DIR}
)

View File

@@ -0,0 +1,173 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#include <filesystem>
#include <fstream>
#include <cstdlib>
#include <pybind11/functional.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/stl/filesystem.h>
#include <pybind11_json/pybind11_json.hpp>
#include "misc.hpp"
#include "module.hpp"
#include <utils/error.hpp>
#include <utils/error/error_factory.hpp>
#include <utils/error/error_state_monitor.hpp>
namespace py = pybind11;
PYBIND11_MODULE(everestpy, m) {
// FIXME (aw): add m.doc?
py::class_<RuntimeSession>(m, "RuntimeSession")
.def(py::init<>())
.def(py::init<const std::string&, const std::string&>());
py::class_<ModuleInfo::Paths>(m, "ModuleInfoPaths")
.def_readonly("etc", &ModuleInfo::Paths::etc)
.def_readonly("libexec", &ModuleInfo::Paths::libexec)
.def_readonly("share", &ModuleInfo::Paths::share);
py::class_<ModuleInfo>(m, "ModuleInfo")
.def_readonly("name", &ModuleInfo::name)
.def_readonly("authors", &ModuleInfo::authors)
.def_readonly("license", &ModuleInfo::license)
.def_readonly("id", &ModuleInfo::id)
.def_readonly("paths", &ModuleInfo::paths)
.def_readonly("telemetry_enabled", &ModuleInfo::telemetry_enabled);
auto error_submodule = m.def_submodule("error");
py::enum_<Everest::error::Severity>(error_submodule, "Severity")
.value("Low", Everest::error::Severity::Low)
.value("Medium", Everest::error::Severity::Medium)
.value("High", Everest::error::Severity::High)
.export_values();
py::class_<ImplementationIdentifier>(error_submodule, "ImplementationIdentifier")
.def(py::init<const std::string&, const std::string&, std::optional<Mapping>>())
.def_readwrite("module_id", &ImplementationIdentifier::module_id)
.def_readwrite("implementation_id", &ImplementationIdentifier::implementation_id)
.def_readwrite("mapping", &ImplementationIdentifier::mapping);
py::class_<Everest::error::UUID>(error_submodule, "UUID")
.def(py::init<>())
.def(py::init<const std::string&>())
.def_readwrite("uuid", &Everest::error::UUID::uuid);
py::enum_<Everest::error::State>(error_submodule, "State")
.value("Active", Everest::error::State::Active)
.value("ClearedByModule", Everest::error::State::ClearedByModule)
.value("ClearedByReboot", Everest::error::State::ClearedByReboot)
.export_values();
py::class_<Everest::error::Error>(error_submodule, "Error")
.def(py::init<>())
.def_readwrite("type", &Everest::error::Error::type)
.def_readwrite("sub_type", &Everest::error::Error::sub_type)
.def_readwrite("description", &Everest::error::Error::description)
.def_readwrite("message", &Everest::error::Error::message)
.def_readwrite("severity", &Everest::error::Error::severity)
.def_readwrite("origin", &Everest::error::Error::origin)
.def_readwrite("timestamp", &Everest::error::Error::timestamp)
.def_readwrite("uuid", &Everest::error::Error::uuid)
.def_readwrite("state", &Everest::error::Error::state);
py::class_<Everest::error::ErrorStateMonitor::StateCondition>(error_submodule, "ErrorStateCondition")
.def(py::init<std::string, std::string, bool>())
.def_readwrite("type", &Everest::error::ErrorStateMonitor::StateCondition::type)
.def_readwrite("sub_type", &Everest::error::ErrorStateMonitor::StateCondition::sub_type)
.def_readwrite("active", &Everest::error::ErrorStateMonitor::StateCondition::active);
py::class_<Everest::error::ErrorStateMonitor, std::shared_ptr<Everest::error::ErrorStateMonitor>>(
error_submodule, "ErrorStateMonitor")
.def("is_error_active", &Everest::error::ErrorStateMonitor::is_error_active)
.def("is_condition_satisfied", py::overload_cast<const Everest::error::ErrorStateMonitor::StateCondition&>(
&Everest::error::ErrorStateMonitor::is_condition_satisfied, py::const_))
.def("is_condition_satisfied",
py::overload_cast<const std::list<Everest::error::ErrorStateMonitor::StateCondition>&>(
&Everest::error::ErrorStateMonitor::is_condition_satisfied, py::const_));
py::class_<Everest::error::ErrorFactory, std::shared_ptr<Everest::error::ErrorFactory>>(error_submodule,
"ErrorFactory")
.def("create_error", py::overload_cast<>(&Everest::error::ErrorFactory::create_error, py::const_))
.def("create_error",
py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
const std::string&>(&Everest::error::ErrorFactory::create_error, py::const_))
.def("create_error", py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
const std::string&, Everest::error::Severity>(
&Everest::error::ErrorFactory::create_error, py::const_))
.def("create_error", py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
const std::string&, Everest::error::State>(
&Everest::error::ErrorFactory::create_error, py::const_))
.def("create_error", py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
const std::string&, Everest::error::Severity, Everest::error::State>(
&Everest::error::ErrorFactory::create_error, py::const_));
py::class_<Interface>(m, "Interface")
.def_readonly("variables", &Interface::variables)
.def_readonly("commands", &Interface::commands)
.def_readonly("errors", &Interface::errors);
py::class_<Fulfillment>(m, "Fulfillment")
.def_readonly("module_id", &Fulfillment::module_id)
.def_readonly("implementation_id", &Fulfillment::implementation_id);
py::class_<ModuleSetup::Configurations>(m, "ModuleSetupConfigurations")
.def_readonly("implementations", &ModuleSetup::Configurations::implementations)
.def_readonly("module", &ModuleSetup::Configurations::module);
py::class_<ModuleSetup>(m, "ModuleSetup")
.def_readonly("configs", &ModuleSetup::configs)
.def_readonly("connections", &ModuleSetup::connections);
py::class_<Module>(m, "Module")
.def(py::init<const RuntimeSession&>())
.def(py::init<const std::string&, const RuntimeSession&>())
.def("say_hello", &Module::say_hello)
.def("init_done", py::overload_cast<>(&Module::init_done))
.def("init_done", py::overload_cast<const std::function<void()>&>(&Module::init_done))
.def("call_command", &Module::call_command)
.def("publish_variable", &Module::publish_variable)
.def("implement_command", &Module::implement_command)
.def("subscribe_variable", &Module::subscribe_variable)
.def("raise_error", &Module::raise_error)
.def("clear_error",
py::overload_cast<const std::string&, const Everest::error::ErrorType&>(&Module::clear_error),
py::arg("impl_id"), py::arg("type"))
.def("clear_error",
py::overload_cast<const std::string&, const Everest::error::ErrorType&,
const Everest::error::ErrorSubType&>(&Module::clear_error),
py::arg("impl_id"), py::arg("type"), py::arg("sub_type"))
.def("clear_all_errors_of_impl", py::overload_cast<const std::string&>(&Module::clear_all_errors_of_impl),
py::arg("impl_id"))
.def("clear_all_errors_of_impl",
py::overload_cast<const std::string&, const Everest::error::ErrorType&>(&Module::clear_all_errors_of_impl),
py::arg("impl_id"), py::arg("type"))
.def("get_error_state_monitor_impl", &Module::get_error_state_monitor_impl)
.def("get_error_factory", &Module::get_error_factory)
.def("subscribe_error", &Module::subscribe_error)
.def("subscribe_all_errors", &Module::subscribe_all_errors)
.def("get_error_state_monitor_req", &Module::get_error_state_monitor_req)
.def_property_readonly("fulfillments", &Module::get_fulfillments)
.def_property_readonly("implementations", &Module::get_implementations)
.def_property_readonly("requirements", &Module::get_requirements)
.def_property_readonly("info", &Module::get_info);
auto log_submodule = m.def_submodule("log");
log_submodule.def("update_process_name",
[](const std::string& process_name) { Everest::Logging::update_process_name(process_name); });
log_submodule.def("verbose", [](const std::string& message) { EVLOG_verbose << message; });
log_submodule.def("debug", [](const std::string& message) { EVLOG_debug << message; });
log_submodule.def("info", [](const std::string& message) { EVLOG_info << message; });
log_submodule.def("warning", [](const std::string& message) { EVLOG_warning << message; });
log_submodule.def("error", [](const std::string& message) { EVLOG_error << message; });
log_submodule.def("critical", [](const std::string& message) { EVLOG_critical << message; });
m.attr("__version__") = "0.25.0";
}

View File

@@ -0,0 +1,6 @@
__version__ = '0.25.0'
try:
from .everestpy import *
except ImportError:
from everestpy import *

View File

@@ -0,0 +1,127 @@
from pathlib import Path
from typing import Callable, overload
from . import error
class ModuleInfoPaths:
@property
def etc(self) -> Path: ...
@property
def libexec(self) -> Path: ...
@property
def share(self) -> Path: ...
class Interface:
@property
def commands(self) -> list[str]: ...
@property
def variables(self) -> list[str]: ...
class ModuleInfo:
@property
def authors(self) -> list[str]: ...
@property
def license(self) -> str: ...
@property
def id(self) -> str: ...
@property
def name(self) -> str: ...
@property
def paths(self) -> ModuleInfoPaths: ...
class ModuleSetupConfigurations:
@property
def implementations(self) -> dict[str, dict]: ...
@property
def module(self) -> dict: ...
class ModuleSetup:
@property
def configs(self) -> ModuleSetupConfigurations: ...
@property
def connections(self) -> dict[str, list[Fulfillment]]: ...
class Fulfillment:
@property
def module_id(self) -> str: ...
@property
def implementation_id(self) -> str: ...
class RuntimeSession:
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, prefix: str, config_file_path: str) -> None: ...
class Module:
@overload
def __init__(self, session: RuntimeSession) -> None: ...
@overload
def __init__(self, module_id: str, session: RuntimeSession) -> None: ...
def say_hello(self) -> ModuleSetup: ...
@overload
def init_done(self) -> None: ...
@overload
def init_done(self, on_ready_handler: Callable[[], None]) -> None: ...
def call_command(self, fulfillment: Fulfillment,
command_name: str, args: dict) -> None: ...
def publish_variable(self, implementation_id: str,
variable_name: str, value: dict) -> None: ...
def implement_command(self, implementation_id: str, command_name: str,
handler: Callable[[dict], dict]) -> None: ...
def subscribe_variable(self, fulfillment: Fulfillment,
variable_name: str, callback: Callable[[dict], None]) -> None: ...
def raise_error(self, implementation_id: str, error: error.Error) -> None: ...
def clear_error(self, implementation_id: str, type: str) -> None: ...
def clear_error(self, implementation_id: str, type: str, sub_type: str) -> None: ...
def clear_all_errors_of_impl(self, implementation_id: str) -> None: ...
def clear_all_errors_of_impl(self, implementation_id: str, type: str) -> None: ...
def get_error_state_monitor_impl(self, implementation_id: str) -> error.ErrorStateMonitor: ...
def get_error_factory(self, implementation_id: str) -> error.ErrorFactory: ...
def subscribe_error(
self,
fulfillment: Fulfillment,
error_type: str,
callback: error.ErrorCallback,
clear_callback: error.ErrorCallback
) -> None: ...
def subscribe_all_errors(
self,
fulfillment: Fulfillment,
callback: error.ErrorCallback,
clear_callback: error.ErrorCallback
) -> None: ...
def get_error_state_monitor_req(self, fulfillment: Fulfillment) -> error.ErrorStateMonitor: ...
@property
def requirements(self) -> dict[str, Interface]: ...
@property
def implementations(self) -> dict[str, Interface]: ...
@property
def info(self) -> ModuleInfo: ...

View File

@@ -0,0 +1,113 @@
from enum import Enum
from typing import Callable, overload, Optional
class Severity(Enum):
Low = "Low",
Medium = "Medium",
High = "High"
class Mapping:
@overload
def __init__(self, evse: int) -> None ...
@overload
def __init__(self, evse: int, connector: int) -> None ...
@property
def evse(self) -> int ...
@property
def connector(self) -> Optional[int] ...
class ImplementationIdentifier:
def __init__(self, module_id: str, implementation_id: str, mapping: Optional[Mapping]) -> None: ...
@property
def module_id(self) -> str: ...
@property
def implementation_id(self) -> str: ...
@property
def mapping(self) -> Optional[Mapping]: ...
class UUID:
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, uuid: str) -> None: ...
class State(Enum):
Active = "Active",
ClearedByModule = "ClearedByModule",
ClearedByReboot = "ClearedByReboot"
class Error:
def __init__(self) -> None: ...
@property
def type(self) -> str: ...
@property
def sub_type(self) -> str: ...
@property
def description(self) -> str: ...
@property
def message(self) -> str: ...
@property
def severity(self) -> Severity: ...
@property
def origin(self) -> ImplementationIdentifier: ...
@property
def timestamp(self) -> int: ...
@property
def uuid(self) -> UUID: ...
@property
def state(self) -> State: ...
ErrorCallback = Callable[[Error], None]
class ErrorStateCondition:
def __init__(self, type: str, sub_type: str, active: bool) -> None: ...
@property
def type(self) -> str: ...
@property
def sub_type(self) -> str: ...
@property
def active(self) -> bool: ...
class ErrorStateMonitor:
def is_error_active(self, type: str, sub_type: str) -> bool: ...
@overload
def is_condition_satisfied(self, condition: ErrorStateCondition) -> bool: ...
@overload
def is_condition_satisfied(self, condition: list[ErrorStateCondition]) -> bool: ...
class ErrorFactory:
@overload
def create_error(self) -> Error: ...
@overload
def create_error(self, type: str, sub_type: str, message: str) -> Error: ...
@overload
def create_error(self, type: str, sub_type: str, message: str, severity: Severity) -> Error: ...
@overload
def create_error(self, type: str, sub_type: str, message: str, state: State) -> Error: ...
@overload
def create_error(self, type: str, sub_type: str, message: str, severity: Severity, state: State) -> Error: ...

View File

@@ -0,0 +1,9 @@
def verbose(message: str) -> None: ...
def debug(message: str) -> None: ...
def info(message: str) -> None: ...
def warning(message: str) -> None: ...
def error(message: str) -> None: ...
def critical(message: str) -> None: ...
def update_process_name(name: str) -> None: ...

View File

@@ -0,0 +1,164 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "misc.hpp"
#include <cstddef>
#include <cstdlib>
#include <stdexcept>
#include <utils/filesystem.hpp>
const std::string get_variable_from_env(const std::string& variable) {
const auto value = std::getenv(variable.c_str());
if (value == nullptr) {
throw std::runtime_error(variable + " needed for everestpy");
}
return value;
}
const std::string get_variable_from_env(const std::string& variable, const std::string& default_value) {
const auto value = std::getenv(variable.c_str());
if (value == nullptr) {
return default_value;
}
return value;
}
namespace {
Everest::MQTTSettings get_mqtt_settings_from_env() {
const auto mqtt_everest_prefix =
get_variable_from_env(Everest::EV_MQTT_EVEREST_PREFIX, Everest::defaults::MQTT_EVEREST_PREFIX);
const auto mqtt_external_prefix =
get_variable_from_env(Everest::EV_MQTT_EXTERNAL_PREFIX, Everest::defaults::MQTT_EXTERNAL_PREFIX);
const auto mqtt_broker_socket_path = std::getenv(Everest::EV_MQTT_BROKER_SOCKET_PATH);
const auto mqtt_broker_host = std::getenv(Everest::EV_MQTT_BROKER_HOST);
const auto mqtt_broker_port = std::getenv(Everest::EV_MQTT_BROKER_PORT);
if (mqtt_broker_socket_path == nullptr) {
if (mqtt_broker_host == nullptr or mqtt_broker_port == nullptr) {
throw std::runtime_error("If EV_MQTT_BROKER_SOCKET_PATH is not set EV_MQTT_BROKER_HOST and "
"EV_MQTT_BROKER_PORT are needed for everestpy");
}
auto mqtt_broker_port_ = Everest::defaults::MQTT_BROKER_PORT;
try {
mqtt_broker_port_ = std::stoul(mqtt_broker_port);
} catch (...) {
EVLOG_warning << "Could not parse MQTT broker port, using default: " << mqtt_broker_port_;
}
return Everest::create_mqtt_settings(mqtt_broker_host, mqtt_broker_port_, mqtt_everest_prefix,
mqtt_external_prefix);
} else {
return Everest::create_mqtt_settings(mqtt_broker_socket_path, mqtt_everest_prefix, mqtt_external_prefix);
}
}
} // namespace
/// This is just kept for compatibility
RuntimeSession::RuntimeSession(const std::string& prefix, const std::string& config_file) {
EVLOG_warning
<< "everestpy: Usage of the old RuntimeSession ctor detected, config should be loaded via MQTT not via "
"the provided config_file. For this please set the appropriate environment variables and call "
"RuntimeSession()";
// We extract the settings from the config file so everest-testing doesn't break
const auto ms = Everest::ManagerSettings(prefix, config_file);
Everest::Logging::init(ms.runtime_settings.logging_config_file.string());
this->mqtt_settings = ms.mqtt_settings;
}
RuntimeSession::RuntimeSession() {
const auto module_id = get_variable_from_env("EV_MODULE");
namespace fs = std::filesystem;
const fs::path logging_config_file =
Everest::assert_file(get_variable_from_env("EV_LOG_CONF_FILE"), "Default logging config");
Everest::Logging::init(logging_config_file.string(), module_id);
this->mqtt_settings = get_mqtt_settings_from_env();
}
ModuleSetup create_setup_from_config(const std::string& module_id, Everest::Config& config) {
ModuleSetup setup;
const std::string& module_name = config.get_module_name(module_id);
const auto& module_manifest = config.get_manifests().at(module_name);
// setup connections
for (const auto& requirement : module_manifest.at("requires").items()) {
const auto& requirement_id = requirement.key();
const auto fulfillments = config.resolve_requirement(module_id, requirement_id);
// Make sure we store the index information in our Fulfillment structures
std::vector<Fulfillment> indexed_fulfillments;
indexed_fulfillments.reserve(fulfillments.size());
for (size_t ii = 0; ii != fulfillments.size(); ++ii) {
Fulfillment indexed_fulfillment = fulfillments.at(ii);
indexed_fulfillment.requirement.index = ii;
EVLOG_verbose << "Setting up " << ii << " implementation_id=" << indexed_fulfillment.implementation_id
<< ", module_id=" << indexed_fulfillment.module_id;
indexed_fulfillments.emplace_back(indexed_fulfillment);
}
setup.connections[requirement_id] = indexed_fulfillments;
}
const auto& module_config = config.get_module_config();
for (const auto& [impl_id, configuration_parameters] : module_config.configuration_parameters) {
json json_config_map;
for (const auto& config_param : configuration_parameters) {
json_config_map[config_param.name] = config_param.value; // implicit conversion to json
}
if (impl_id == "!module") {
setup.configs.module = json_config_map;
continue;
}
setup.configs.implementations.emplace(impl_id, json_config_map);
}
return setup;
}
Interface create_everest_interface_from_definition(const json& def) {
Interface intf;
if (def.contains("cmds")) {
const auto& cmds = def.at("cmds");
intf.commands.reserve(cmds.size());
for (const auto& cmd : cmds.items()) {
intf.commands.push_back(cmd.key());
}
}
if (def.contains("vars")) {
const auto& vars = def.at("vars");
intf.variables.reserve(vars.size());
for (const auto& var : vars.items()) {
intf.variables.push_back(var.key());
}
}
if (def.contains("errors")) {
const auto& errors = def.at("errors");
std::size_t errors_size = 0;
for (const auto& error_namespace_it : errors.items()) {
errors_size += error_namespace_it.value().size();
}
intf.errors.reserve(errors_size);
for (const auto& error_namespace_it : errors.items()) {
for (const auto& error_name_it : error_namespace_it.value().items()) {
intf.errors.push_back(error_namespace_it.key() + "/" + error_name_it.key());
}
}
}
return intf;
}

View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVERESTPY_MISC_HPP
#define EVERESTPY_MISC_HPP
#include <string>
#include <framework/runtime.hpp>
#include <utils/config/mqtt_settings.hpp>
#include <utils/config/types.hpp>
#include <utils/types.hpp>
const std::string get_variable_from_env(const std::string& variable);
const std::string get_variable_from_env(const std::string& variable, const std::string& default_value);
class RuntimeSession {
public:
RuntimeSession(const std::string& prefix, const std::string& config_file);
RuntimeSession();
const Everest::MQTTSettings& get_mqtt_settings() const {
return mqtt_settings;
}
private:
Everest::MQTTSettings mqtt_settings;
};
struct Interface {
std::vector<std::string> variables;
std::vector<std::string> commands;
std::vector<std::string> errors;
};
Interface create_everest_interface_from_definition(const json& def);
struct ModuleSetup {
struct Configurations {
std::map<std::string, json> implementations;
json module;
};
Configurations configs;
std::map<std::string, std::vector<Fulfillment>> connections;
};
ModuleSetup create_setup_from_config(const std::string& module_id, Everest::Config& config);
#endif // EVERESTPY_MISC_HPP

View File

@@ -0,0 +1,145 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#include "module.hpp"
#include <pybind11/pybind11.h>
#include <utils/error/error_factory.hpp>
#include <utils/error/error_manager_impl.hpp>
#include <utils/error/error_manager_req.hpp>
#include <utils/error/error_state_monitor.hpp>
std::unique_ptr<Everest::Everest>
Module::create_everest_instance(const std::string& module_id, const Everest::Config& config,
const Everest::RuntimeSettings& rs,
std::shared_ptr<Everest::MQTTAbstraction> mqtt_abstraction) {
return std::make_unique<Everest::Everest>(module_id, config, rs.validate_schema, mqtt_abstraction,
rs.telemetry_prefix, rs.telemetry_enabled, rs.forward_exceptions);
}
Module::Module(const RuntimeSession& session) : Module(get_variable_from_env("EV_MODULE"), session) {
}
Module::Module(const std::string& module_id_, const RuntimeSession& session_) :
module_id(module_id_), session(session_), start_time(std::chrono::steady_clock::now()) {
this->mqtt_abstraction =
std::shared_ptr<Everest::MQTTAbstraction>(Everest::make_mqtt_abstraction(session.get_mqtt_settings()));
this->mqtt_abstraction->connect();
this->mqtt_abstraction->spawn_main_loop_thread();
const auto result = Everest::get_module_config(this->mqtt_abstraction, module_id);
Everest::RuntimeSettings result_settings = result.at("settings");
this->rs = std::make_unique<Everest::RuntimeSettings>(std::move(result_settings));
this->config_ = std::make_unique<Everest::Config>(session.get_mqtt_settings(), result);
const auto& config = get_config();
this->handle = create_everest_instance(module_id, config, *this->rs, this->mqtt_abstraction);
// determine the fulfillments for our requirements
const auto& module_name = config.get_module_name(this->module_id);
const auto module_manifest = config.get_manifests().at(module_name);
// setup module info
module_info = config.get_module_info(module_id);
populate_module_info_path_from_runtime_settings(module_info, *this->rs);
// setup implementations
for (const auto& implementation : module_manifest.at("provides").items()) {
const auto& implementation_id = implementation.key();
const std::string interface_name = implementation.value().at("interface");
const auto& interface_def = config.get_interface_definition(interface_name);
implementations.emplace(implementation_id, create_everest_interface_from_definition(interface_def));
}
// setup requirements
for (const auto& requirement : module_manifest.at("requires").items()) {
const auto& requirement_id = requirement.key();
const std::string interface_name = requirement.value().at("interface");
const auto& interface_def = config.get_interface_definition(interface_name);
requirements.emplace(requirement_id, create_everest_interface_from_definition(interface_def));
}
}
ModuleSetup Module::say_hello() {
handle->connect();
handle->spawn_main_loop_thread();
return create_setup_from_config(module_id, get_config());
}
json Module::call_command(const Fulfillment& fulfillment, const std::string& cmd_name, json args) {
// FIXME (aw): we're releasing the GIL here, because the mqtt thread will want to aquire it when calling the
// callbacks
const pybind11::gil_scoped_release release;
const auto& result = handle->call_cmd(fulfillment.requirement, cmd_name, std::move(args));
return result;
}
void Module::publish_variable(const std::string& impl_id, const std::string& var_name, json value) {
// NOTE (aw): publish_var just sends output directly via mqtt, so we don't need to release here as opposed to
// call_command
handle->publish_var(impl_id, var_name, std::move(value));
}
void Module::implement_command(const std::string& impl_id, const std::string& cmd_name,
std::function<json(json)> command_handler) {
auto& handler = command_handlers.emplace_back(std::move(command_handler));
handle->provide_cmd(impl_id, cmd_name, [&handler](json args) { return handler(std::move(args)); });
}
void Module::subscribe_variable(const Fulfillment& fulfillment, const std::string& var_name,
std::function<void(json)> subscription_callback) {
auto& callback = subscription_callbacks.emplace_back(std::move(subscription_callback));
handle->subscribe_var(fulfillment.requirement, var_name, [&callback](json args) { callback(std::move(args)); });
}
void Module::raise_error(const std::string& impl_id, const Everest::error::Error& error) {
handle->get_error_manager_impl(impl_id)->raise_error(error);
}
void Module::clear_error(const std::string& impl_id, const Everest::error::ErrorType& type) {
handle->get_error_manager_impl(impl_id)->clear_error(type);
}
void Module::clear_error(const std::string& impl_id, const Everest::error::ErrorType& type,
const Everest::error::ErrorSubType& sub_type) {
handle->get_error_manager_impl(impl_id)->clear_error(type, sub_type);
}
void Module::clear_all_errors_of_impl(const std::string& impl_id) {
handle->get_error_manager_impl(impl_id)->clear_all_errors();
}
void Module::clear_all_errors_of_impl(const std::string& impl_id, const Everest::error::ErrorType& type) {
handle->get_error_manager_impl(impl_id)->clear_all_errors(type);
}
std::shared_ptr<Everest::error::ErrorStateMonitor>
Module::get_error_state_monitor_impl(const std::string& impl_id) const {
return handle->get_error_state_monitor_impl(impl_id);
}
std::shared_ptr<Everest::error::ErrorFactory> Module::get_error_factory(const std::string& impl_id) const {
return handle->get_error_factory(impl_id);
}
void Module::subscribe_error(const Fulfillment& fulfillment, const Everest::error::ErrorType& type,
const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback) {
handle->get_error_manager_req(fulfillment.requirement)->subscribe_error(type, callback, clear_callback);
}
void Module::subscribe_all_errors(const Fulfillment& fulfillment, const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback) {
handle->get_error_manager_req(fulfillment.requirement)->subscribe_all_errors(callback, clear_callback);
}
std::shared_ptr<Everest::error::ErrorStateMonitor>
Module::get_error_state_monitor_req(const Fulfillment& fulfillment) const {
return handle->get_error_state_monitor_req(fulfillment.requirement);
}

View File

@@ -0,0 +1,113 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVERESTPY_MODULE_HPP
#define EVERESTPY_MODULE_HPP
#include <chrono>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <framework/everest.hpp>
#include "misc.hpp"
class Module {
public:
Module(const RuntimeSession&);
Module(const std::string&, const RuntimeSession&);
ModuleSetup say_hello();
void init_done(const std::function<void()>& on_ready_handler) {
this->handle->check_code();
if (on_ready_handler) {
handle->register_on_ready_handler(on_ready_handler);
}
const auto end_time = std::chrono::steady_clock::now();
EVLOG_info << "Module " << fmt::format(Everest::TERMINAL_STYLE_BLUE, "{}", this->module_id) << " initialized ["
<< std::chrono::duration_cast<std::chrono::milliseconds>(end_time - this->start_time).count()
<< "ms]";
handle->signal_ready();
}
void init_done() {
init_done(nullptr);
}
Everest::Config& get_config() {
return *config_;
}
json call_command(const Fulfillment& fulfillment, const std::string& cmd_name, json args);
void publish_variable(const std::string& impl_id, const std::string& var_name, json value);
void implement_command(const std::string& impl_id, const std::string& cmd_name, std::function<json(json)> handler);
void subscribe_variable(const Fulfillment& fulfillment, const std::string& var_name,
std::function<void(json)> callback);
void raise_error(const std::string& impl_id, const Everest::error::Error& error);
void clear_error(const std::string& impl_id, const Everest::error::ErrorType& type);
void clear_error(const std::string& impl_id, const Everest::error::ErrorType& type,
const Everest::error::ErrorSubType& sub_type);
void clear_all_errors_of_impl(const std::string& impl_id);
void clear_all_errors_of_impl(const std::string& impl_id, const Everest::error::ErrorType& type);
std::shared_ptr<Everest::error::ErrorStateMonitor> get_error_state_monitor_impl(const std::string& impl_id) const;
std::shared_ptr<Everest::error::ErrorFactory> get_error_factory(const std::string& impl_id) const;
void subscribe_error(const Fulfillment& fulfillment, const Everest::error::ErrorType& type,
const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback);
void subscribe_all_errors(const Fulfillment& fulfillment, const Everest::error::ErrorCallback& callback,
const Everest::error::ErrorCallback& clear_callback);
std::shared_ptr<Everest::error::ErrorStateMonitor>
get_error_state_monitor_req(const Fulfillment& fulfillment) const;
const auto& get_fulfillments() const {
return fulfillments;
}
const auto& get_info() const {
return module_info;
}
const auto& get_requirements() const {
return requirements;
}
const auto& get_implementations() const {
return implementations;
}
private:
const std::string module_id;
const RuntimeSession& session;
const std::chrono::time_point<std::chrono::steady_clock> start_time;
std::unique_ptr<Everest::RuntimeSettings> rs;
std::shared_ptr<Everest::MQTTAbstraction> mqtt_abstraction;
std::unique_ptr<Everest::Config> config_;
std::unique_ptr<Everest::Everest> handle;
// NOTE (aw): we're keeping the handlers local to the module instance and don't pass them by copy-construction
// to "external" c/c++ code, so no GIL related problems should appear
std::deque<std::function<json(json)>> command_handlers{};
std::deque<std::function<void(json)>> subscription_callbacks{};
std::deque<std::function<void(json)>> err_susbcription_callbacks{};
std::deque<std::function<void(json)>> err_cleared_susbcription_callbacks{};
static std::unique_ptr<Everest::Everest>
create_everest_instance(const std::string& module_id, const Everest::Config& config,
const Everest::RuntimeSettings& rs,
std::shared_ptr<Everest::MQTTAbstraction> mqtt_abstraction);
ModuleInfo module_info{};
std::map<std::string, Interface> requirements;
std::map<std::string, Interface> implementations;
std::map<std::string, std::vector<Fulfillment>> fulfillments;
};
#endif // EVERESTPY_MODULE_HPP

View File

@@ -0,0 +1,4 @@
# The default path to the link dependencies file generated by CMake. Overwritten by CMake, this is a fallback for IDEs.
[env.EVEREST_RS_LINK_DEPENDENCIES]
value = "../../../build/everestrs-link-dependencies.txt"
relative = true

View File

@@ -0,0 +1,134 @@
set(CXXBRIDGE_VERSION 1.0.194) # Must be kept in sync with `everestrs/Cargo.toml`
set(CXXBRIDGE_INSTALL_PATH ${CMAKE_CURRENT_BINARY_DIR}/cargo/bin/cxxbridge)
# Checks if the cxxbridge binary at CXXBRIDGE_PATH exists and matches the required version.
# If not, sets VERSION_MATCHES to FALSE.
function(everestrs_check_cxxbridge_version CXXBRIDGE_PATH VERSION_MATCHES)
if(NOT CXXBRIDGE_BINARY OR NOT EXISTS ${CXXBRIDGE_BINARY})
set(${VERSION_MATCHES} FALSE PARENT_SCOPE)
endif()
execute_process(
COMMAND
${CXXBRIDGE_BINARY} --version
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
OUTPUT_VARIABLE
CXXBRIDGE_VERSION_OUTPUT
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(CXXBRIDGE_VERSION_OUTPUT MATCHES "cxxbridge ${CXXBRIDGE_VERSION}")
set(${VERSION_MATCHES} TRUE PARENT_SCOPE)
else()
set(${VERSION_MATCHES} FALSE PARENT_SCOPE)
endif()
endfunction()
# If cxxbridge is already installed system-wide, check if it's the correct version.
find_program(CXXBRIDGE_BINARY cxxbridge)
everestrs_check_cxxbridge_version(${CXXBRIDGE_BINARY} CXXBRIDGE_INSTALLED)
# Otherwise, check if we previously installed it in our build dir.
if(NOT CXXBRIDGE_INSTALLED)
set(CXXBRIDGE_BINARY ${CXXBRIDGE_INSTALL_PATH})
everestrs_check_cxxbridge_version(${CXXBRIDGE_BINARY} CXXBRIDGE_INSTALLED)
endif()
# Either we never had cxxbridge installed, or the version is incorrect. Install the correct version to our build dir.
if(NOT CXXBRIDGE_INSTALLED)
message(STATUS "Fetching rust cxxbridge cli tool")
execute_process(
COMMAND
${CMAKE_COMMAND} -E env --unset=CARGO_BUILD_TARGET # Ensure we always build for the host
cargo install cxxbridge-cmd --force --version ${CXXBRIDGE_VERSION} --root ${CMAKE_CURRENT_BINARY_DIR}/cargo
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
RESULT_VARIABLE
CARGO_INSTALL_RESULT
OUTPUT_QUIET
ERROR_VARIABLE
CARGO_INSTALL_ERROR
)
if(NOT CARGO_INSTALL_RESULT)
set(CXXBRIDGE_BINARY ${CXXBRIDGE_INSTALL_PATH})
else()
message(FATAL_ERROR "cargo install cxxbridge-cmd failed with:\n${CARGO_INSTALL_ERROR}")
endif()
endif()
set(CXXBRIDGE_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR})
set(CXXBRIDGE_HEADER ${CXXBRIDGE_OUTPUT_DIR}/rust/cxx.h)
set(EVERESTRS_FFI_HEADER ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src/lib.rs.h)
set(EVERESTRS_FFI_SOURCE ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src/lib.rs.cc)
add_custom_command(
OUTPUT
${EVERESTRS_FFI_HEADER}
${EVERESTRS_FFI_SOURCE}
${CXXBRIDGE_HEADER}
COMMAND
${CMAKE_COMMAND} -E make_directory ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src ${CXXBRIDGE_OUTPUT_DIR}/rust
COMMAND
${CXXBRIDGE_BINARY} --header -o ${CXXBRIDGE_HEADER}
COMMAND
${CXXBRIDGE_BINARY} ${CMAKE_CURRENT_SOURCE_DIR}/everestrs/src/lib.rs --header -o ${EVERESTRS_FFI_HEADER}
COMMAND
${CXXBRIDGE_BINARY} ${CMAKE_CURRENT_SOURCE_DIR}/everestrs/src/lib.rs -o ${EVERESTRS_FFI_SOURCE}
DEPENDS
everestrs/src/lib.rs
COMMENT "Generating cxxbridge glue for everestrs"
VERBATIM
DEPENDS
${CXXBRIDGE_BINARY}
)
add_library(everestrs_sys STATIC
${EVERESTRS_FFI_SOURCE}
everestrs/src/everestrs_sys.cpp
)
add_library(everest::everestrs_sys ALIAS everestrs_sys)
set_property(
TARGET
everestrs_sys
PROPERTY
EVERESTRS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/everestrs
)
set_property(
TARGET
everestrs_sys
PROPERTY
EVERESTRS_BUILD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/everestrs-build
)
target_include_directories(everestrs_sys
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
)
# This is a requirement that linking works on systems enforcing PIE
# FIXME (aw): investicate why this is really necessary
set_property(TARGET everestrs_sys PROPERTY POSITION_INDEPENDENT_CODE ON)
target_link_libraries(everestrs_sys
PRIVATE
everest::framework
everest::log
)
set(EVERESTRS_LINK_DEPENDENCIES $<TARGET_FILE:everest::everestrs_sys>)
get_target_property(EVERESTRS_LINK_TARGETS everest::everestrs_sys LINK_LIBRARIES)
foreach(link_target IN LISTS EVERESTRS_LINK_TARGETS)
list(APPEND EVERESTRS_LINK_DEPENDENCIES $<TARGET_FILE:${link_target}>)
endforeach()
set_property(
TARGET
everestrs_sys
PROPERTY
EVERESTRS_LINK_DEPENDENCIES "${EVERESTRS_LINK_DEPENDENCIES}"
)

View File

@@ -0,0 +1,742 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "argh"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240"
dependencies = [
"argh_derive",
"argh_shared",
"rust-fuzzy-search",
]
[[package]]
name = "argh_derive"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "argh_shared"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cc"
version = "1.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "codespan-reporting"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
"unicode-width",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cxx"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e"
dependencies = [
"cc",
"cxx-build",
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e"
dependencies = [
"cc",
"codespan-reporting",
"indexmap",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-cmd"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328"
dependencies = [
"clap",
"codespan-reporting",
"indexmap",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a"
[[package]]
name = "cxxbridge-macro"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "downcast"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "everestrs"
version = "0.25.0"
dependencies = [
"clap",
"cxx",
"everestrs-build",
"everestrs-derive",
"log",
"mockall",
"mockall_double",
"nix",
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"tokio",
]
[[package]]
name = "everestrs-build"
version = "0.25.0"
dependencies = [
"anyhow",
"argh",
"convert_case",
"minijinja",
"serde",
"serde_json",
"serde_yaml",
]
[[package]]
name = "everestrs-derive"
version = "0.25.0"
dependencies = [
"everestrs-build",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "fragile"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "link-cplusplus"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
dependencies = [
"cc",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minijinja"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3287d827e6da221ea11aa173c66b82ab69db27a1b177e8439f730b478bf33a7b"
dependencies = [
"serde",
]
[[package]]
name = "mockall"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
dependencies = [
"cfg-if",
"downcast",
"fragile",
"mockall_derive",
"predicates",
"predicates-tree",
]
[[package]]
name = "mockall_derive"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "mockall_double"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "nix"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rust-fuzzy-search"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scratch"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"pin-project-lite",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@@ -0,0 +1,11 @@
[workspace]
resolver = "2"
members = [
"everestrs",
"everestrs-build",
"everestrs-derive",
]
[workspace.dependencies]
everestrs-build = { path="everestrs-build" }
everestrs-derive = { path="everestrs-derive" }

View File

@@ -0,0 +1,46 @@
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
filegroup(
name = "templates",
srcs = glob(["jinja/**/*"]),
)
rust_library(
name = "everestrs-build",
srcs = glob(["src/**/*.rs"], exclude = ["src/bin/**"]),
deps = [
"@everest_framework_crate_index//:anyhow",
"@everest_framework_crate_index//:argh",
"@everest_framework_crate_index//:convert_case",
"@everest_framework_crate_index//:minijinja",
"@everest_framework_crate_index//:serde",
"@everest_framework_crate_index//:serde_json",
"@everest_framework_crate_index//:serde_yaml",
],
compile_data = [":templates"],
visibility = ["//visibility:public"],
edition = "2021",
)
rust_binary(
name = "codegen",
srcs = glob(["src/bin/**/*.rs"]),
deps = [
"@everest_framework_crate_index//:anyhow",
"@everest_framework_crate_index//:argh",
"@everest_framework_crate_index//:convert_case",
"@everest_framework_crate_index//:minijinja",
"@everest_framework_crate_index//:serde",
"@everest_framework_crate_index//:serde_json",
"@everest_framework_crate_index//:serde_yaml",
":everestrs-build",
],
visibility = ["//visibility:public"],
edition = "2021",
)
rust_test(
name = "test",
crate = ":everestrs-build",
edition = "2021",
)

View File

@@ -0,0 +1,15 @@
[package]
name = "everestrs-build"
version = "0.25.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
argh = "0.1.12"
convert_case = "0.6.0"
minijinja = "1.0.8"
serde = "1.0.188"
serde_json = "1.0.107"
serde_yaml = "0.9.25"

View File

@@ -0,0 +1,152 @@
/// {{trait.description | replace("\n", " ")}}
pub(crate) trait {{trait.name | title}}ClientSubscriber: Sync + Send {
{% for var in trait.vars %}
fn on_{{ var.name | snake }}(&self, context: &Context, value: {{ var.data_type.name }});
{% endfor %}
{%- if trait.errors %}
fn on_error_raised(&self, context: &Context, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
fn on_error_cleared(&self, context: &Context, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
{%- endif %}
}
#[cfg(all(feature = "mockall", feature = "trait"))]
mockall::mock! {
pub(crate) {{trait.name | title}}ClientSubscriber {}
impl {{trait.name | title}}ClientSubscriber for {{trait.name | title}}ClientSubscriber {
{% for var in trait.vars %}
fn on_{{ var.name | snake }}<'a>(&self, context: &Context<'a>, value: {{ var.data_type.name }});
{% endfor %}
{%- if trait.errors %}
fn on_error_raised<'a>(&self, context: &Context<'a>, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
fn on_error_cleared<'a>(&self, context: &Context<'a>, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
{%- endif %}
}
}
fn dispatch_variable_to_{{ trait.name | snake }}(
context: &Context,
client_subscriber: &dyn {{trait.name | title}}ClientSubscriber,
name: &str,
value: __serde_json::Value,
) -> ::everestrs::Result<()> {
match name {
{%- for var in trait.vars %}
"{{ var.name }}" => {
let v: {{ var.data_type.name }} = __serde_json::from_value(value)
.map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to deserialize variable `{{ var.name }}`: {e:?})")))?;
client_subscriber.on_{{ var.name | snake }}(context, v);
Ok(())
},
{%- endfor %}
other => Err(::everestrs::Error::MessageParsingError(format!("Unknown variable {other} received.").to_string())),
}
}
fn dispatch_error_to_{{ trait.name | snake }} (
context: &Context,
client_subscriber: &dyn {{ trait.name | title }}ClientSubscriber,
error: ::everestrs::FfiErrorType,
raised: bool
) {
{%- if trait.errors %}
// The type is errors::{{ trait.name | snake }}::Error
let Ok(v) = __serde_yaml::from_str(&error.error_type) else {
everestrs::log::error!("Failed to deserialize error `{}`", error.error_type);
return;
};
let error_type = ::everestrs::ErrorType {
error_type: v,
description: error.description,
message: error.message,
severity: error.severity,
};
if raised {
client_subscriber.on_error_raised(context, error_type);
} else {
client_subscriber.on_error_cleared(context, error_type);
}
{%- endif %}
}
pub(crate) mod __mockall_{{trait.name | snake }}_client {
use super::__serde_json;
use super::types;
use super::errors;
#[derive(Clone)]
pub(crate) struct {{trait.name | title }}ClientPublisher {
pub(super) implementation_id: &'static str,
pub(super) runtime: ::std::sync::Weak<::everestrs::Runtime>,
pub(super) index: usize,
}
impl {{trait.name | title }}ClientPublisher {
{%- for cmd in trait.cmds %}
/// {{cmd.description | replace("\n", " ")}}
///
{%- for arg in cmd.arguments %}
/// `{{arg.name}}`: {{arg.description | replace("\n", " ")}}
{%- endfor %}
{% if cmd.result -%}
///
/// Returns: {{cmd.result.description | replace("\n", " ")}}
{% endif -%}
pub(crate) fn {{cmd.name | identifier}}(&self,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}
> {
let args = __serde_json::json!({
{%- for arg in cmd.arguments %}
"{{arg.name}}": {{arg.name | identifier}},
{%- endfor %}
});
let rt = self.runtime.upgrade().ok_or_else(|| {
::everestrs::Error::HandlerException(
"publisher used after Module was dropped".into(),
)
})?;
rt.call_command(self.implementation_id, self.index, "{{ cmd.name }}", &args)
}
{% endfor %}
}
#[cfg(all(feature = "mockall", not(feature = "trait")))]
mockall::mock!{
pub(crate) {{trait.name | title }}ClientPublisher {
{%- for cmd in trait.cmds %}
pub(crate) fn {{cmd.name | identifier}}(&self,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}
>;
{% endfor %}
}
impl Clone for {{trait.name | title }}ClientPublisher {
fn clone(&self) -> Self;
}
}
}
#[cfg_attr(all(feature = "mockall", not(feature = "trait")), mockall_double::double)]
pub(crate) use __mockall_{{trait.name | snake }}_client::{{trait.name | title }}ClientPublisher;

View File

@@ -0,0 +1,50 @@
{% for p_config in provided_config %}
/// The configuration for the {{ p_config.name }}.
#[derive(Debug)]
pub(crate) struct {{ p_config.name | title }}Config {
{% for config in p_config.config %}
/// {{ config.description }}
pub(crate) {{ config.name | identifier }}: {{ config.data_type.name }},
{% endfor %}
}
{% endfor %}
/// The configuration for the module. It also contains the config for all other
/// interfaces.
#[derive(Debug)]
pub(crate) struct ModuleConfig {
{% for config in module_config %}
/// {{ config.description }}
pub(crate) {{ config.name | identifier }}: {{ config.data_type.name }},
{% endfor %}
{% for p_config in provided_config %}
/// The config for the `{{ p_config.name }}` interface.
pub(crate) {{ p_config.name }}_config: {{ p_config.name | title }}Config,
{% endfor %}
}
/// Returns the config for the whole module.
impl Module {
pub(crate) fn get_config(&self) -> ModuleConfig {
let raw_config = self.runtime.get_module_configs();
{% for p_config in provided_config %}
let {{ p_config.name }}_config = {{ p_config.name | title }}Config {
{% for config in p_config.config %}
{{ config.name | identifier }}: raw_config.get("{{ p_config.name }}").unwrap().get("{{ config.name }}").unwrap().try_into().unwrap(),
{% endfor %}
};
{% endfor %}
ModuleConfig {
{% for config in module_config %}
{{ config.name | identifier }}: raw_config.get("!module").unwrap().get("{{ config.name }}").unwrap().try_into().unwrap(),
{% endfor %}
{% for p_config in provided_config %}
{{ p_config.name }}_config,
{% endfor %}
}
}
}

View File

@@ -0,0 +1,28 @@
{%- for name, errors in involved_errors | items %}
#[allow(clippy::enum_variant_names)]
pub mod {{ name | snake }} {
use everestrs::serde as __serde;
{%- for error_group in errors %}
/// The error definition of the {{ name }} interface.
/// {{ error_group.error_list.description | replace("\n", " ") }}
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
pub enum {{ error_group.name | title }}Error {
{%- for error_entry in error_group.error_list.errors %}
/// {{ error_entry.description | replace("\n", " ")}}
#[serde(rename = "{{ error_group.name | snake }}/{{ error_entry.name }}")]
{{ error_entry.name | title}},
{%- endfor %}
}
{%- endfor %}
/// All possible errors of the {{ name }} interface.
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
#[serde(untagged)]
pub enum Error {
{%- for error_group in errors %}
{{ error_group.name | title }}({{ error_group.name | title }}Error),
{%- endfor %}
}
}
{%- endfor %}

View File

@@ -0,0 +1,302 @@
mod generated {
#![allow(
clippy::let_unit_value,
clippy::match_single_binding,
clippy::upper_case_acronyms,
clippy::useless_conversion,
clippy::too_many_arguments,
dead_code,
non_camel_case_types,
unused_mut,
unused_variables,
unused_imports,
)]
use everestrs::serde_json as __serde_json;
use everestrs::serde_yaml as __serde_yaml;
pub mod types {
{% include "types" %}
}
pub mod errors {
{% include "errors" %}
}
{% include "config" %}
/// Called when the module receives on_ready from EVerest.
pub(crate) trait OnReadySubscriber: Sync + Send {
fn on_ready(&self, pub_impl: &ModulePublisher);
}
#[cfg(all(feature = "mockall", feature = "trait"))]
mockall::mock! {
pub(crate) OnReadySubscriber {}
impl OnReadySubscriber for OnReadySubscriber {
fn on_ready(&self, pub_impl: &ModulePublisher);
}
}
{% for trait in provided_interfaces %}
{% include "service" %}
{% endfor %}
{% for trait in required_interfaces %}
{% include "client" %}
{% endfor %}
#[derive(Clone)]
#[cfg_attr(all(test, feature="mockall", not(feature="trait")), derive(Default))]
pub(crate) struct ModulePublisher {
{% for provide in provides %}
pub(crate) {{ provide.implementation_id | identifier }}: {{provide.interface | title}}ServicePublisher,
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
pub(crate) {{ require.implementation_id | identifier }}: {{require.interface | title}}ClientPublisher,
{% elif require.min_connections == require.max_connections %}
pub(crate) {{ require.implementation_id | identifier }}_slots: [{{ require.interface | title}}ClientPublisher; {{require.min_connections}}],
{% else %}
pub(crate) {{ require.implementation_id | identifier }}_slots: Vec<{{require.interface | title}}ClientPublisher>,
{% endif %}
{% endfor %}
}
struct ModuleInner {
on_ready: ::std::sync::Arc<dyn OnReadySubscriber>,
{% for provide in provides %}
{{ provide.implementation_id | identifier }}: ::std::sync::Arc<dyn {{provide.interface | title}}ServiceSubscriber>,
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }}: ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
{% elif require.min_connections == require.max_connections %}
{{ require.implementation_id | identifier }}_slots: [::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>; {{require.max_connections}}],
{% else %}
{{ require.implementation_id | identifier }}_slots: Vec<::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>>,
{% endif %}
{% endfor %}
publisher: ModulePublisher,
ready: ::std::sync::Condvar,
ready_flag: ::std::sync::Mutex<bool>,
}
pub(crate) struct Module {
runtime: ::std::pin::Pin<::std::sync::Arc<::everestrs::Runtime>>,
inner: ::std::sync::OnceLock<::std::sync::Arc<ModuleInner>>,
}
/// The context structure.
pub(crate) struct Context<'a> {
pub(crate) publisher: &'a ModulePublisher,
{# TODO(ddo) Clarify the naming. #}
/// The name as in `implementation_id`.
pub name: &'a str,
/// The index of the slot.
pub index: usize,
}
impl Module {
#[must_use]
pub(crate) fn new() -> Self {
let runtime = ::everestrs::Runtime::new();
Self {
runtime,
inner: ::std::sync::OnceLock::new(),
}
}
#[must_use]
pub(crate) fn new_with_args(args: ::everestrs::Args) -> Self {
let runtime = ::everestrs::Runtime::new_with_args(args);
Self {
runtime,
inner: ::std::sync::OnceLock::new(),
}
}
pub(crate) fn start
{% if requires_with_generics %}
<
{% for require in requires %}
{% if require.min_connections != 1 or require.max_connections != 1 %}
{{ require.implementation_id | title }}Callback: FnMut(usize) -> ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
{% endif %}
{% endfor %}
>
{% endif %}
(
&self,
on_ready: ::std::sync::Arc<dyn OnReadySubscriber>,
{% for provide in provides %}
{{ provide.implementation_id | identifier }}: ::std::sync::Arc<dyn {{provide.interface | title}}ServiceSubscriber>,
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }}: ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
{% else %}
{{ require.implementation_id | identifier }}_cb: {{ require.implementation_id | title }}Callback,
{% endif %}
{% endfor %}
) -> &ModulePublisher {
let runtime = &self.runtime;
let connections = runtime.get_module_connections();
// Publishers hold a Weak<Runtime> so ModuleInner -> publishers ->
// Runtime -> sub_impl -> ModuleInner is not a cycle. Drop of the
// Module deterministically tears down Runtime, ModuleInner, and the
// mock subscribers inside it.
let runtime_weak = ::std::sync::Arc::downgrade(&::std::pin::Pin::into_inner(runtime.clone()));
let inner = self.inner.get_or_init(|| {
::std::sync::Arc::new(ModuleInner {
on_ready,
{% for provide in provides %}
{{ provide.implementation_id | identifier }},
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }},
{% elif require.min_connections == require.max_connections %}
{{ require.implementation_id | identifier }}_slots: ::core::array::from_fn({{ require.implementation_id | identifier }}_cb),
{% else %}
{{ require.implementation_id | identifier }}_slots: (0..connections.get("{{require.implementation_id}}").cloned().unwrap_or(0)).map({{ require.implementation_id | identifier }}_cb).collect(),
{% endif %}
{% endfor %}
#[cfg(any(not(test), not(feature = "mockall"), feature = "trait"))]
publisher: ModulePublisher {
{% for provide in provides %}
{{ provide.implementation_id | identifier }}: {{provide.interface | title}}ServicePublisher {
implementation_id: "{{ provide.implementation_id }}",
runtime: runtime_weak.clone(),
},
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }}: {{require.interface | title}}ClientPublisher {
implementation_id: "{{ require.implementation_id }}",
runtime: runtime_weak.clone(),
index: 0,
},
{% elif require.min_connections == require.max_connections %}
{{ require.implementation_id | identifier }}_slots: ::core::array::from_fn(|i| {{require.interface | title}}ClientPublisher{
implementation_id: "{{ require.implementation_id }}",
runtime: runtime_weak.clone(),
index: i,
}),
{% else %}
{{ require.implementation_id | identifier }}_slots: (0..connections.get("{{require.implementation_id}}").cloned().unwrap_or(0)).map(|i| {{require.interface | title}}ClientPublisher{
implementation_id: "{{ require.implementation_id }}",
runtime: runtime_weak.clone(),
index: i,
}).collect(),
{% endif %}
{% endfor %}
},
#[cfg(all(test, feature = "mockall", not(feature = "trait")))]
publisher: ModulePublisher::default(),
ready: ::std::sync::Condvar::new(),
ready_flag: ::std::sync::Mutex::new(false),
})
});
runtime.as_ref().set_subscriber(inner.clone());
// Block until on_ready has fired.
let mut ready = inner.ready_flag.lock().unwrap();
while !*ready {
ready = inner.ready.wait(ready).unwrap();
}
&inner.publisher
}
}
impl ::everestrs::Subscriber for ModuleInner {
fn handle_command(
&self,
implementation_id: &str,
name: &str,
parameters: ::std::collections::HashMap<String, __serde_json::Value>,
) -> ::everestrs::Result<__serde_json::Value> {
let context = Context {
publisher: &self.publisher,
name: implementation_id,
index: 0,
};
match implementation_id {
{% for provide in provides %}
"{{ provide.implementation_id }}" => {
dispatch_command_to_{{ provide.interface | snake }}(&context, self.{{ provide.implementation_id | identifier }}.as_ref(), name, parameters)
},
{% endfor %}
other => Err(::everestrs::Error::MessageParsingError(
format!("Unknown implementation_id {other} called."),
)),
}
}
fn handle_variable(
&self,
implementation_id: &str,
index: usize,
name: &str,
value: __serde_json::Value,
) -> ::everestrs::Result<()> {
let context = Context {
publisher: &self.publisher,
name: implementation_id,
index,
};
match implementation_id {
{% for req in requires %}
"{{ req.implementation_id }}" => {
{% if req.min_connections == 1 and req.max_connections == 1 %}
dispatch_variable_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}.as_ref(), name, value)
{% else %}
dispatch_variable_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}_slots[index].as_ref(), name, value)
{% endif %}
},
{% endfor %}
other => Err(::everestrs::Error::MessageParsingError(
format!("Unknown variable {other} received."),
))
}
}
fn handle_on_error(
&self,
implementation_id: &str,
index: usize,
error: ::everestrs::FfiErrorType,
raised: bool
) {
let context = Context {
publisher: &self.publisher,
name: implementation_id,
index,
};
match implementation_id {
{% for req in requires %}
"{{ req.implementation_id }}" => {
{% if req.min_connections == 1 and req.max_connections == 1 %}
dispatch_error_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}.as_ref(), error, raised)
{% else %}
dispatch_error_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}_slots[index].as_ref(), error, raised)
{% endif %}
},
{% endfor %}
_ => everestrs::log::error!("Received an unknown error from {implementation_id}"),
}
}
fn on_ready(&self) {
self.on_ready.on_ready(&self.publisher);
let mut ready = self.ready_flag.lock().unwrap();
*ready = true;
self.ready.notify_all();
}
}
}

View File

@@ -0,0 +1,138 @@
/// {{trait.description | replace("\n", " ")}}
pub(crate) trait {{trait.name | title}}ServiceSubscriber: Sync + Send {
{%- for cmd in trait.cmds %}
/// {{cmd.description | replace("\n", " ")}}
///
{%- for arg in cmd.arguments %}
/// `{{arg.name}}`: {{arg.description | replace("\n", " ")}}
{%- endfor %}
{% if cmd.result -%}
///
/// Returns: {{cmd.result.description | replace("\n", " ")}}
{% endif -%}
fn {{cmd.name}}(&self,
context: &Context,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}>;
{% endfor %}
}
#[cfg(all(feature = "mockall", feature = "trait"))]
mockall::mock! {
pub(crate) {{trait.name | title}}ServiceSubscriber {}
impl {{trait.name | title}}ServiceSubscriber for {{trait.name | title}}ServiceSubscriber {
{%- for cmd in trait.cmds %}
fn {{cmd.name}}<'a>(&self,
context: &Context<'a>,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}>;
{% endfor %}
}
}
fn dispatch_command_to_{{ trait.name | snake }}(
context: &Context,
service: &dyn {{trait.name | title}}ServiceSubscriber,
name: &str,
mut parameters: ::std::collections::HashMap<String, __serde_json::Value>,
) -> ::everestrs::Result<__serde_json::Value> {
match name {
{%- for cmd in trait.cmds %}
"{{ cmd.name }}" => {
{%- for arg in cmd.arguments %}
let {{ arg.name | identifier }}: {{ arg.data_type.name }} = __serde_json::from_value(
parameters.remove("{{ arg.name }}")
.ok_or(::everestrs::Error::MessageParsingError("Argument `{{ arg.name }}` not provided".to_string()))?,
)
.map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to deserialize argument `{{ arg.name }}`: {e:?}")))?;
{%- endfor %}
let retval = service.{{ cmd.name }}(context,
{%- for arg in cmd.arguments %}
{{ arg.name | identifier }},
{%- endfor %}
)?;
__serde_json::to_value(retval).map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to serialize result: {e:?}")))
},
{%- endfor %}
other => Err(::everestrs::Error::MessageParsingError(format!("Unknown command `{other}` called."))),
}
}
pub(crate) mod __mockall_{{trait.name | snake }}_service {
use super::types;
use super::errors;
#[derive(Clone)]
pub(crate) struct {{trait.name | title }}ServicePublisher {
pub(super) implementation_id: &'static str,
pub(super) runtime: ::std::sync::Weak<::everestrs::Runtime>,
}
impl {{trait.name | title }}ServicePublisher {
{% for var in trait.vars %}
pub(crate) fn {{ var.name | identifier }}(&self, value: {{ var.data_type.name }}) -> ::everestrs::Result<()> {
if let Some(runtime) = self.runtime.upgrade() {
runtime.publish_variable(self.implementation_id, "{{ var.name }}", &value)
}
Ok(())
}
{% endfor %}
{%- if trait.errors %}
pub(crate) fn raise_error(&self, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>) {
if let Some(runtime) = self.runtime.upgrade() {
runtime.raise_error(self.implementation_id, error);
}
}
pub(crate) fn clear_error(&self, error: errors::{{ trait.name | snake }}::Error) {
if let Some(runtime) = self.runtime.upgrade() {
runtime.clear_error(self.implementation_id, error, true);
}
}
pub(crate) fn clear_all_errors(&self) {
if let Some(runtime) = self.runtime.upgrade() {
runtime.clear_error(self.implementation_id, "", true);
}
}
{%- endif %}
}
#[cfg(all(feature = "mockall", not(feature = "trait")))]
mockall::mock!{
pub(crate) {{trait.name | title }}ServicePublisher {
{% for var in trait.vars %}
pub(crate) fn {{ var.name | identifier }}(&self, value: {{ var.data_type.name }}) -> ::everestrs::Result<()>;
{% endfor %}
{%- if trait.errors %}
pub(crate) fn raise_error(&self, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
pub(crate) fn clear_error(&self, error: errors::{{ trait.name | snake }}::Error);
pub(crate) fn clear_all_errors(&self);
{%- endif %}
}
impl Clone for {{trait.name | title }}ServicePublisher {
fn clone(&self) -> Self;
}
}
}
#[cfg_attr(all(feature = "mockall", not(feature = "trait")), mockall_double::double)]
pub(crate) use __mockall_{{trait.name | snake }}_service::{{trait.name | title }}ServicePublisher;

View File

@@ -0,0 +1,31 @@
{% for name, types in types.children | items %}
pub mod {{ name }} {
mod types { pub use super::super::*; }
{% include "types" %}
}
{% endfor %}
use everestrs::serde as __serde;
{% for object in types.objects %}
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
pub struct {{ object.name }} {
{% for p in object.properties %}
/// {{ p.description | replace("\n", " ") }}
#[serde(rename="{{ p.name }}"{% if p.data_type.extra_serde_annotations %},{{ p.data_type.extra_serde_annotations | join(",") }}{% endif %})]
pub {{ p.name | identifier }}: {{ p.data_type.name }},
{% endfor %}
}
{% endfor %}
{% for enum in types.enums %}
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
pub enum {{ enum.name }} {
{% for item in enum.items %}
{{ item }},
{% endfor %}
}
{% endfor %}

View File

@@ -0,0 +1,30 @@
use anyhow::Result;
use argh::FromArgs;
use everestrs_build::Builder;
use std::path::PathBuf;
#[derive(FromArgs)]
/// Codegen for EVerest-rs
struct Args {
/// path to EVerest
#[argh(option)]
pub everest_core: Vec<PathBuf>,
/// manifest to generate code for
#[argh(option)]
pub manifest: PathBuf,
/// output directory to put the generated code to.
#[argh(option)]
pub out_dir: PathBuf,
}
pub fn main() -> Result<()> {
let args: Args = argh::from_env();
Builder::new(args.manifest, args.everest_core)
.out_dir(args.out_dir)
.generate()?;
Ok(())
}

View File

@@ -0,0 +1,904 @@
use crate::schema::{
self,
interface::ErrorReference,
manifest::{ConfigEntry, ConfigEnum, Ignore},
types::{DataTypes, ObjectOptions, StringOptions, Type, TypeBase, TypeEnum},
ErrorList, Interface, Manifest,
};
use anyhow::{anyhow, bail, Context, Result};
use convert_case::{Case, Casing};
use minijinja::{Environment, UndefinedBehavior};
use serde::{de::DeserializeOwned, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
// We include the JINJA templates into the binary. This has the disadvantage
// that every change to the templates requires a recompilation, but the
// advantage that the codegen library/binary is truly standalone and needs
// nothing shipped with it to work.
const CLIENT_JINJA: &str = include_str!("../jinja/client.jinja2");
const CONFIG_JINJA: &str = include_str!("../jinja/config.jinja2");
const ERRORS_JINJA: &str = include_str!("../jinja/errors.jinja2");
const MODULE_JINJA: &str = include_str!("../jinja/module.jinja2");
const SERVICE_JINJA: &str = include_str!("../jinja/service.jinja2");
const TYPES_JINJA: &str = include_str!("../jinja/types.jinja2");
fn is_reserved_keyword(s: &str) -> bool {
// From https://doc.rust-lang.org/reference/keywords.html.
matches!(
s,
"abstract"
| "as"
| "async"
| "await"
| "become"
| "box"
| "break"
| "const"
| "continue"
| "crate"
| "do"
| "dyn"
| "else"
| "enum"
| "extern"
| "false"
| "final"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "macro"
| "macro_rules"
| "match"
| "mod"
| "move"
| "mut"
| "override"
| "priv"
| "pub"
| "ref"
| "return"
| "self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "try"
| "type"
| "typeof"
| "union"
| "unsafe"
| "unsized"
| "use"
| "virtual"
| "where"
| "while"
| "yield"
)
}
fn lazy_load<'a, T: DeserializeOwned>(
storage: &'a mut HashMap<String, T>,
everest_root: &Vec<PathBuf>,
prefix: &str,
postfix: &str,
) -> Result<&'a mut T> {
if storage.contains_key(postfix) {
return Ok(storage.get_mut(postfix).unwrap());
}
let mut matches = everest_root
.iter()
.filter_map(|core| {
let p = core.join(format!("{prefix}/{postfix}.yaml"));
// If the file is missing we ignore the error since it may be
// present in an different root.
let Ok(blob) = fs::read_to_string(&p) else {
return None;
};
let out = serde_yaml::from_str(&blob).with_context(|| format!("Failed to parse {p:?}"));
match out {
Err(err) => {
println!("{err:?}");
None
}
Ok(res) => Some(res),
}
})
.collect::<Vec<_>>();
assert!(
matches.len() == 1,
"The name `{prefix}/{postfix}` must be defined exactly once: Found {}",
{ matches.len() }
);
storage.insert(postfix.to_string(), matches.pop().unwrap());
Ok(storage.get_mut(postfix).unwrap())
}
/// A lazy loader for YAML files. If the same file is requested twice, it will
/// not be re-parsed again.
#[derive(Default, Debug)]
struct YamlRepo {
// This might be also a HashMap of "namespaces" and paths.
everest_root: Vec<PathBuf>,
interfaces: HashMap<String, Interface>,
data_types: HashMap<String, DataTypes>,
error_types: HashMap<String, ErrorList>,
}
impl YamlRepo {
pub fn new(everest_root: Vec<PathBuf>) -> Self {
Self {
everest_root,
..Default::default()
}
}
pub fn get_interface<'a>(&'a mut self, name: &str) -> Result<&'a mut Interface> {
lazy_load(&mut self.interfaces, &self.everest_root, "interfaces", name)
}
pub fn get_data_types<'a>(&'a mut self, name: &str) -> Result<&'a mut DataTypes> {
lazy_load(&mut self.data_types, &self.everest_root, "types", name)
}
pub fn get_errors<'a>(&'a mut self, prefix: &str, name: &str) -> Result<&'a mut ErrorList> {
lazy_load(&mut self.error_types, &self.everest_root, prefix, name)
}
}
// We just pull out of ObjectOptions what we really need for codegen.
#[derive(Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
struct TypeRef {
/// The same as the file name under EVerest/types.
module_path: Vec<String>,
type_name: String,
}
impl TypeRef {
fn from_object(args: &ObjectOptions) -> Result<Self> {
assert!(args.object_reference.is_some());
assert!(
args.properties.is_empty(),
"Found an object with $ref, but also with properties. Cannot handle that case."
);
Self::from_reference(args.object_reference.as_ref().unwrap())
}
fn from_string(args: &StringOptions) -> Result<Self> {
assert!(args.object_reference.is_some());
Self::from_reference(args.object_reference.as_ref().unwrap())
}
fn from_reference(r: &str) -> Result<Self> {
let parts: Vec<_> = r.trim_start_matches('/').split("#/").collect();
if parts.len() != 2 {
bail!("Unexpected type reference: {}", r);
}
let module_name = parts[0].to_string();
let module_path = module_name.split('/').map(|s| s.to_string()).collect();
let type_name = parts[1].to_string();
Ok(Self {
module_path,
type_name,
})
}
pub fn module_name(&self) -> String {
format!("types::{}", self.module_path.join("::"),)
}
pub fn absolute_type_path(&self) -> String {
format!("{}::{}", self.module_name(), self.type_name)
}
}
impl std::fmt::Debug for TypeRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"TypeRef /{}#/{}",
self.module_path.join("/"),
self.type_name
)
}
}
fn as_typename(arg: &TypeBase, type_refs: &mut BTreeSet<TypeRef>) -> Result<String> {
use TypeBase::*;
use TypeEnum::*;
Ok(match arg {
Single(Null) => "()".to_string(),
Single(Boolean(_)) => "bool".to_string(),
Single(String(args)) => {
if args.object_reference.is_none() {
"String".to_string()
} else {
let t = TypeRef::from_string(args)?;
let name = t.absolute_type_path();
type_refs.insert(t);
name
}
}
Single(Number(_)) => "f64".to_string(),
Single(Integer(_)) => "i64".to_string(),
Single(Object(args)) => {
if args.object_reference.is_none() {
"__serde_json::Value".to_string()
} else {
let t = TypeRef::from_object(args)?;
let name = t.absolute_type_path();
type_refs.insert(t);
name
}
}
Single(Array(args)) => match args.items {
None => "Vec<__serde_json::Value>".to_string(),
Some(ref v) => {
let item_type = as_typename(&v.arg, type_refs)?;
format!("Vec<{item_type}>")
}
},
Multiple(_) => "__serde_json::Value".to_string(),
})
}
#[derive(Debug, Clone, Serialize)]
struct DataTypeContext {
name: String,
extra_serde_annotations: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
struct ArgumentContext {
name: String,
description: Option<String>,
data_type: DataTypeContext,
}
impl ArgumentContext {
pub fn from_schema(
name: String,
var: &Type,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<Self> {
Ok(ArgumentContext {
name,
description: var.description.clone(),
data_type: DataTypeContext {
name: as_typename(&var.arg, type_refs)?,
extra_serde_annotations: Vec::new(),
},
})
}
}
#[derive(Debug, Clone, Serialize)]
struct CommandContext {
name: String,
description: String,
result: Option<ArgumentContext>,
arguments: Vec<ArgumentContext>,
}
impl CommandContext {
pub fn from_schema(
name: String,
cmd: &crate::schema::interface::Command,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<Self> {
let mut arguments = Vec::new();
for (name, arg) in &cmd.arguments {
arguments.push(ArgumentContext::from_schema(name.clone(), arg, type_refs)?);
}
Ok(CommandContext {
name,
description: cmd.description.clone(),
result: match &cmd.result {
None => None,
Some(arg) => Some(ArgumentContext::from_schema(
"return_value".to_string(),
arg,
type_refs,
)?),
},
arguments,
})
}
}
/// The error group maps to one error yaml file.
#[derive(Debug, Clone, Serialize)]
struct ErrorGroupContext {
/// The name is basically the yaml file in which the errors are defined.
name: String,
/// The list of errors
error_list: schema::error::ErrorList,
}
mod impl_error {
#[derive(Hash, Eq, PartialEq)]
pub struct ErrorPath<'a> {
/// The prefix where the error files are.
pub prefix: &'a str,
/// The error file itself.
pub file: &'a str,
}
pub struct ErrorDefinition<'a> {
/// The path of the error.
pub path: ErrorPath<'a>,
/// The type which is optional. If the type is not defined we accept
/// all errors in the path.
pub error_type: Option<&'a str>,
}
impl<'a> ErrorDefinition<'a> {
/// Try to construct an error definition from the string.
pub fn try_new(value: &'a str) -> anyhow::Result<Self> {
let mut splits = value.split("#/");
let path = splits.next().ok_or(anyhow::anyhow!("No path defined"))?;
// Split the path and remove the empty parts.
// (The first element might be empty if we have a leading `/`).
let paths = path
.split("/")
.filter(|path| !path.is_empty())
.collect::<Vec<_>>();
anyhow::ensure!(paths.len() == 2, "Expecting exactly two paths");
anyhow::ensure!(
paths.iter().all(|path| !path.is_empty()),
"Empty paths not allowed"
);
let path = ErrorPath {
prefix: paths[0],
file: paths[1],
};
let error_type = splits.next();
if let Some(inner) = error_type {
anyhow::ensure!(!inner.is_empty(), "Type must not be empty");
}
Ok(Self { path, error_type })
}
}
}
impl ErrorGroupContext {
/// Generates the [ErrorGroupContext] from the `error_reference`.
///
/// The error_reference can have two forms:
/// - /errors/example
/// - /errors/example#/ExampleErrorA
///
/// The first type is straight forward. For the second type however, we want
/// to group them by their file name.
fn from_yaml(yaml_repo: &mut YamlRepo, errors: &[ErrorReference]) -> Vec<Self> {
// The errors may be defined multiple times. If we find a definition
// which would use all, we use all. Otherwise we use the specific
// defintions.
enum ErrorOption {
/// Use all errors in a file.
All,
/// Use only specific errors in a file.
Some(HashSet<String>),
}
// Find all the error options defined.
let mut error_definitions = HashMap::new();
for error_ref in errors {
let new_error = impl_error::ErrorDefinition::try_new(&error_ref.reference)
.expect("Failed to parse {error_ref}");
let mut error_definition = error_definitions
.entry(new_error.path)
.or_insert(ErrorOption::Some(HashSet::new()));
// We don't "downgrade" `All` to `Some`.
if let ErrorOption::Some(options) = &mut error_definition {
if let Some(new_option) = new_error.error_type {
options.insert(new_option.to_string());
} else {
*error_definition = ErrorOption::All;
}
}
}
let mut output = Vec::new();
// Load the error yaml form the disk.
for (error_path, error_option) in error_definitions {
let error_list = yaml_repo
.get_errors(error_path.prefix, error_path.file)
.unwrap();
let mut error_group_context = ErrorGroupContext {
name: error_path.file.to_string(),
error_list: error_list.clone(),
};
// Remove unused options.
if let ErrorOption::Some(options) = error_option {
error_group_context
.error_list
.errors
.retain(|e| options.contains(&e.name));
}
// The yaml file might have no errors defined at all. This would
// still comply with the EVerest schema but the user can't do
// anything with it.
if !error_group_context.error_list.errors.is_empty() {
output.push(error_group_context);
}
}
output
}
}
#[derive(Debug, Clone, Serialize)]
struct InterfaceContext {
name: String,
description: String,
cmds: Vec<CommandContext>,
vars: Vec<ArgumentContext>,
/// The errors of an interface.
errors: Vec<ErrorGroupContext>,
}
impl InterfaceContext {
pub fn from_yaml(
yaml_repo: &mut YamlRepo,
name: &str,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<Self> {
let interface_yaml = yaml_repo.get_interface(name)?;
let mut vars = Vec::new();
for (name, var) in &interface_yaml.vars {
vars.push(ArgumentContext::from_schema(name.clone(), var, type_refs)?);
}
let mut cmds = Vec::new();
for (name, cmd) in &interface_yaml.cmds {
cmds.push(CommandContext::from_schema(name.clone(), cmd, type_refs)?);
}
// We can only borrow the yaml_repo once. It's actually not necessary so
// we should refactor this.
let description = interface_yaml.description.clone();
let errors = interface_yaml.errors.clone();
let errors = ErrorGroupContext::from_yaml(yaml_repo, &errors);
Ok(InterfaceContext {
name: name.to_string(),
description,
vars,
cmds,
errors,
})
}
}
#[derive(Debug, Clone, Serialize, Default)]
struct TypeModuleContext {
children: BTreeMap<String, TypeModuleContext>,
objects: Vec<ObjectTypeContext>,
enums: Vec<EnumTypeContext>,
}
#[derive(Debug, Clone, Serialize)]
struct ObjectTypeContext {
name: String,
properties: Vec<ArgumentContext>,
}
#[derive(Debug, Clone, Serialize)]
struct EnumTypeContext {
name: String,
items: Vec<String>,
}
#[derive(Debug, Clone)]
enum TypeContext {
Object(ObjectTypeContext),
Enum(EnumTypeContext),
}
fn type_context_from_ref(
r: &TypeRef,
yaml_repo: &mut YamlRepo,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<TypeContext> {
use TypeBase::*;
use TypeEnum::*;
let module_path = r.module_path.join("/");
let data_types_yaml = yaml_repo.get_data_types(&module_path)?;
let type_descr = data_types_yaml
.types
.get_mut(&r.type_name)
.ok_or_else(|| anyhow!("Unable to find data type {:?}. Is it defined?", r))?;
let mut new_types: BTreeMap<std::string::String, Type> = BTreeMap::new();
let res = match &mut type_descr.arg {
Single(Object(args)) => {
let mut properties = Vec::new();
for (name, var) in &mut args.properties {
let mut extra_serde_annotations = Vec::new();
let data_type = {
// This is some "trick" - if we have enums which are defined
// inplace, we create a new entry.
if let Single(String(enum_args)) = &mut var.arg {
match &enum_args.enum_items {
Some(items) => {
let new_type = Type {
description: Some("An inlined type".to_string()),
arg: Single(String(StringOptions {
pattern: None,
format: None,
max_length: None,
min_length: None,
enum_items: Some(items.clone()),
default: None,
object_reference: None,
})),
qos: None,
};
let new_name = format!(
"{}AutoGen{}",
r.type_name.to_case(Case::Pascal),
name.to_case(Case::Pascal)
);
enum_args.object_reference =
Some(format!("/{}#/{}", module_path, new_name));
new_types.insert(new_name, new_type);
}
_ => {}
}
}
let d = as_typename(&var.arg, type_refs)?;
if !args.required.contains(name) {
extra_serde_annotations
.push("skip_serializing_if = \"Option::is_none\"".to_string());
format!("Option<{}>", d)
} else {
d
}
};
properties.push(ArgumentContext {
name: name.clone(),
description: var.description.clone(),
data_type: DataTypeContext {
name: data_type,
extra_serde_annotations,
},
});
}
Ok(TypeContext::Object(ObjectTypeContext {
name: r.type_name.clone(),
properties,
}))
}
Single(String(args)) => {
assert!(
args.enum_items.is_some(),
"Expected a named string type to be an enum, but {} was not.",
r.type_name
);
Ok(TypeContext::Enum(EnumTypeContext {
name: r.type_name.clone(),
items: args.enum_items.clone().unwrap(),
}))
}
other => unreachable!("Does not support $ref for {other:?}"),
};
data_types_yaml.types.extend(new_types);
return res;
}
#[derive(Debug, Clone, Serialize)]
struct SlotContext {
implementation_id: String,
interface: String,
min_connections: i64,
max_connections: i64,
}
#[derive(Debug, Clone, Serialize)]
struct ConfigContext {
name: String,
config: Vec<ArgumentContext>,
}
#[derive(Debug, Clone, Serialize)]
struct RenderContext {
/// The interfaces the user will need to fill in.
provided_interfaces: Vec<InterfaceContext>,
/// The interfaces we are requiring.
required_interfaces: Vec<InterfaceContext>,
/// All errors involved - those we can raise and those we can receive.
involved_errors: HashMap<String, Vec<ErrorGroupContext>>,
provides: Vec<SlotContext>,
requires: Vec<SlotContext>,
requires_with_generics: bool,
types: TypeModuleContext,
module_config: Vec<ArgumentContext>,
provided_config: Vec<ConfigContext>,
}
fn title_case(arg: String) -> String {
arg.to_case(Case::Pascal)
}
fn snake_case(arg: String) -> String {
arg.to_case(Case::Snake)
}
/// Like `snake_case`, but can deal with reserved names (and will then use raw identifiers).
fn identifier_case(arg: String) -> String {
let arg = snake_case(arg);
if is_reserved_keyword(&arg) {
format!("r#{arg}")
} else {
arg
}
}
/// Converts the config data read from yaml and generates the context for Jinja.
///
/// The config data contains the config name (key) and the config data (value).
/// We use the value to derive the type and the (optional) description.
fn emit_config(config: BTreeMap<String, ConfigEntry>) -> Vec<ArgumentContext> {
config
.into_iter()
.map(|(k, v)| match v.value {
ConfigEnum::Boolean(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "bool".to_string(),
extra_serde_annotations: Vec::new(),
},
},
ConfigEnum::Integer(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "i64".to_string(),
extra_serde_annotations: Vec::new(),
},
},
ConfigEnum::Number(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "f64".to_string(),
extra_serde_annotations: Vec::new(),
},
},
ConfigEnum::String(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "String".to_string(),
extra_serde_annotations: Vec::new(),
},
},
})
.collect::<Vec<_>>()
}
pub fn emit(manifest_path: PathBuf, everest_core: Vec<PathBuf>) -> Result<String> {
let blob = fs::read_to_string(&manifest_path).context("While reading manifest file")?;
let manifest: Manifest = serde_yaml::from_str(&blob).context("While parsing manifest")?;
emit_manifest(manifest, everest_core)
}
pub fn emit_manifest(manifest: Manifest, everest_core: Vec<PathBuf>) -> Result<String> {
let mut yaml_repo = YamlRepo::new(everest_core);
let mut env = Environment::new();
env.set_undefined_behavior(UndefinedBehavior::Strict);
env.add_filter("title", title_case);
env.add_filter("snake", snake_case);
env.add_filter("identifier", identifier_case);
env.add_template("client", CLIENT_JINJA)?;
env.add_template("config", CONFIG_JINJA)?;
env.add_template("errors", ERRORS_JINJA)?;
env.add_template("module", MODULE_JINJA)?;
env.add_template("service", SERVICE_JINJA)?;
env.add_template("types", TYPES_JINJA)?;
let provided_config = manifest
.provides
.iter()
.filter(|(_, data)| !data.config.is_empty())
.map(|(name, data)| ConfigContext {
name: name.clone(),
config: emit_config(data.config.clone()),
})
.collect::<Vec<_>>();
let mut type_refs = BTreeSet::new();
let mut provided_interfaces = HashMap::with_capacity(manifest.provides.len());
let mut provides = Vec::with_capacity(manifest.provides.len());
for (implementation_id, imp) in manifest.provides {
if !provided_interfaces.contains_key(&imp.interface) {
let interface_context =
InterfaceContext::from_yaml(&mut yaml_repo, &imp.interface, &mut type_refs)?;
provided_interfaces.insert(imp.interface.clone(), interface_context);
}
provides.push(SlotContext {
implementation_id,
interface: imp.interface.clone(),
min_connections: 1,
max_connections: 1,
})
}
let mut required_interfaces = HashMap::with_capacity(manifest.requires.len());
let mut requires = Vec::with_capacity(manifest.requires.len());
// We remove the intersection off all ignored interfaces from the trait
// signature.
let mut ignored = HashMap::with_capacity(manifest.requires.len());
for (implementation_id, imp) in manifest.requires {
ignored
.entry(imp.interface.clone())
.and_modify(|merged_ignore: &mut Ignore| {
merged_ignore.vars = merged_ignore
.vars
.intersection(&imp.ignore.vars)
.cloned()
.collect();
merged_ignore.errors = merged_ignore.errors & imp.ignore.errors;
})
.or_insert(imp.ignore);
if !required_interfaces.contains_key(&imp.interface) {
let interface_context =
InterfaceContext::from_yaml(&mut yaml_repo, &imp.interface, &mut type_refs)?;
required_interfaces.insert(imp.interface.clone(), interface_context);
}
requires.push(SlotContext {
implementation_id,
interface: imp.interface.clone(),
min_connections: imp.min_connections.unwrap_or(1),
max_connections: imp.max_connections.unwrap_or(1),
})
}
for (interface, merged_ignore) in ignored.into_iter() {
// Check if all ignored interfaces are known.
if let Some(required_interface) = required_interfaces.get(&interface) {
if let Some(unknown_var) = merged_ignore.vars.iter().find(|&ignored_var| {
required_interface
.vars
.iter()
.find(|&required_var| &required_var.name == ignored_var)
.is_none()
}) {
panic!("The interface `{interface}` cannot ignore unkown variable `{unknown_var}`");
}
}
// Remove those interfaces which were never used.
required_interfaces
.entry(interface)
.and_modify(|interface| {
interface
.vars
.retain(|cmd| !merged_ignore.vars.contains(&cmd.name));
if merged_ignore.errors {
interface.errors.clear();
}
});
}
let mut type_module_root = TypeModuleContext::default();
let mut done: BTreeSet<TypeRef> = BTreeSet::new();
while done.len() != type_refs.len() {
let mut new = BTreeSet::new();
for t in &type_refs {
if done.contains(t) {
continue;
}
let mut module = &mut type_module_root;
for p in &t.module_path {
module = module.children.entry(p.clone()).or_default();
}
match type_context_from_ref(t, &mut yaml_repo, &mut new)? {
TypeContext::Object(item) => module.objects.push(item),
TypeContext::Enum(item) => module.enums.push(item),
}
done.insert(t.clone());
}
type_refs.extend(new.into_iter());
}
let module_config = emit_config(manifest.config);
let requires_with_generics = requires
.iter()
.any(|elem| elem.min_connections != 1 || elem.max_connections != 1);
let involved_errors = provided_interfaces
.iter()
.chain(required_interfaces.iter())
.filter(|(_key, value)| !value.errors.is_empty())
.map(|(key, value)| (key.clone(), value.errors.clone()))
.collect::<HashMap<_, _>>();
let context = RenderContext {
provided_interfaces: provided_interfaces.values().cloned().collect(),
required_interfaces: required_interfaces.values().cloned().collect(),
involved_errors,
provides,
requires,
requires_with_generics,
types: type_module_root,
module_config,
provided_config,
};
let tmpl = env.get_template("module").unwrap();
Ok(tmpl.render(context).unwrap())
}
#[cfg(test)]
mod tests {
#[test]
fn test_split_paths_invalid() {
use super::impl_error::*;
let invalid_input = [
"/foo/bar/baz", // too many
"/foo", // too few,
"/foo/", // no type
"//foo", // no path,
"", // just empty
];
for input in invalid_input {
assert!(ErrorDefinition::try_new(input).is_err());
}
}
#[test]
fn test_split_paths() {
use super::impl_error::*;
let res = ErrorDefinition::try_new("/foo/bar#/baz").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(matches!(res.error_type, Some("baz")));
let res = ErrorDefinition::try_new("/foo/bar").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(res.error_type.is_none());
let res = ErrorDefinition::try_new("foo/bar#/baz").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(matches!(res.error_type, Some("baz")));
let res = ErrorDefinition::try_new("foo/bar").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(res.error_type.is_none());
}
}

View File

@@ -0,0 +1,53 @@
pub mod codegen;
pub mod manifest_resolver;
pub mod schema;
use anyhow::{Context, Result};
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
pub use manifest_resolver::build_test_manifest;
#[derive(Debug, Default)]
pub struct Builder {
everest_root: Vec<PathBuf>,
// TODO(hrapp): This is almost always the same anyways.
manifest_path: PathBuf,
out_dir: Option<PathBuf>,
}
impl Builder {
pub fn new(manifest_path: impl Into<PathBuf>, everest_root: Vec<impl Into<PathBuf>>) -> Self {
Self {
everest_root: everest_root
.into_iter()
.map(|element| element.into())
.collect::<Vec<_>>(),
manifest_path: manifest_path.into(),
..Builder::default()
}
}
pub fn out_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.out_dir = Some(path.into());
self
}
pub fn generate(self) -> Result<()> {
let path = self
.out_dir
.unwrap_or_else(|| PathBuf::from(std::env::var("OUT_DIR").unwrap()))
.join("generated.rs");
let out = codegen::emit(self.manifest_path, self.everest_root)?;
let mut f = std::fs::File::create(&path).context("Could not generate the output file.")?;
f.write_all(out.as_bytes())?;
if let Err(_) = Command::new("rustfmt").args(path.to_str()).output() {
println!("Failed to format code");
}
Ok(())
}
}

View File

@@ -0,0 +1,205 @@
use crate::schema;
use crate::schema::manifest::{Manifest, ProvidesEntry, RequiresEntry};
use anyhow::{anyhow, bail, Context, Result};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
mod inner {
use super::*;
/// A cache that lazily loads and stores module manifests by module type name.
pub(super) struct ManifestCache<'a> {
everest_core: &'a [PathBuf],
entries: BTreeMap<String, (PathBuf, Manifest)>,
}
impl<'a> ManifestCache<'a> {
pub(super) fn new(everest_core: &'a [PathBuf]) -> Self {
Self {
everest_core,
entries: BTreeMap::new(),
}
}
/// Returns the manifest for `module_type`, loading it on first access.
pub(super) fn get(&mut self, module_type: &str) -> Result<&Manifest> {
if !self.entries.contains_key(module_type) {
let (path, manifest) = find_manifest(module_type, self.everest_core)?;
self.entries
.insert(module_type.to_string(), (path, manifest));
}
Ok(&self.entries[module_type].1)
}
/// Returns the paths of all manifests that were loaded.
pub(super) fn into_paths(self) -> impl Iterator<Item = PathBuf> {
self.entries.into_values().map(|(path, _)| path)
}
}
/// Recursively searches `dir` for a directory named `module_type` containing
/// a `manifest.yaml`. Returns the path to the manifest on first match.
/// Symlinks are skipped to avoid circular traversal.
fn find_manifest_in(dir: &Path, module_type: &str) -> Option<PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let ft = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if ft.is_symlink() {
continue;
}
let path = entry.path();
if path.file_name().map_or(false, |n| n == module_type) {
let manifest = path.join("manifest.yaml");
if manifest.is_file() {
return Some(manifest);
}
}
if ft.is_dir() {
if let Some(found) = find_manifest_in(&path, module_type) {
return Some(found);
}
}
}
None
}
/// Finds a module manifest by searching for `{ModuleName}/manifest.yaml`
/// anywhere under each everest_core root. Modules can be nested arbitrarily
/// deep (e.g. `modules/Examples/RustExamples/RsExample/manifest.yaml`).
/// Symlinks are skipped to avoid circular traversal.
fn find_manifest(module_type: &str, everest_core: &[PathBuf]) -> Result<(PathBuf, Manifest)> {
for root in everest_core {
if let Some(path) = find_manifest_in(root, module_type) {
let blob = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read {path:?}"))?;
let manifest: Manifest = serde_yaml::from_str(&blob)
.with_context(|| format!("Failed to parse {path:?}"))?;
return Ok((path, manifest));
}
}
bail!("Could not find manifest for module type '{module_type}' in any everest_core root");
}
}
/// Reads config.yaml, finds the target module instance, deduces its
/// interfaces from connected modules' manifests, returns a synthetic
/// Manifest and the list of files read (for dependency tracking).
pub fn build_test_manifest(
config_path: &Path,
module_instance: &str,
everest_core: &[PathBuf],
) -> Result<(Manifest, Vec<PathBuf>)> {
let blob = std::fs::read_to_string(config_path)
.with_context(|| format!("While reading config {config_path:?}"))?;
let config: schema::Config =
serde_yaml::from_str(&blob).with_context(|| format!("While parsing {config_path:?}"))?;
let target = config.active_modules.get(module_instance).ok_or_else(|| {
anyhow!("Module instance '{module_instance}' not found in {config_path:?}")
})?;
let mut cache = inner::ManifestCache::new(everest_core);
// Step 1: Resolve outgoing connections (what the target module requires).
// For each connection slot in the target module, find what interface the
// connected module provides at that implementation_id.
let mut requires = BTreeMap::new();
for (slot_name, connections) in &target.connections {
let mut interfaces = std::collections::HashSet::new();
for conn in connections {
let connected_module = config.active_modules.get(&conn.module_id).ok_or_else(|| {
anyhow!(
"Connected module '{}' not found in {config_path:?}",
conn.module_id
)
})?;
let connected_manifest = cache.get(&connected_module.module)?;
let provides_entry = connected_manifest
.provides
.get(&conn.implementation_id)
.ok_or_else(|| {
anyhow!(
"Module type '{}' does not provide '{}'",
connected_module.module,
conn.implementation_id
)
})?;
interfaces.insert(provides_entry.interface.clone());
}
if interfaces.len() != 1 {
bail!("Slot '{slot_name}' has connections with mismatched interfaces: {interfaces:?}");
}
requires.insert(
slot_name.clone(),
RequiresEntry {
interface: interfaces.into_iter().next().unwrap(),
min_connections: Some(1),
max_connections: Some(connections.len() as i64),
ignore: Default::default(),
},
);
}
// Step 2: Resolve incoming connections (what the target module must provide).
// Scan all other modules' connections to find ones pointing at our target.
let mut provides = BTreeMap::new();
for (other_id, other_module) in &config.active_modules {
if other_id == module_instance {
continue;
}
for (other_slot, connections) in &other_module.connections {
for conn in connections {
if conn.module_id != module_instance {
continue;
}
// other_module requires interface via other_slot,
// connected to our target's conn.implementation_id.
let other_manifest = cache.get(&other_module.module)?;
let requires_entry = other_manifest.requires.get(other_slot).ok_or_else(|| {
anyhow!(
"Module type '{}' does not require '{}'",
other_module.module,
other_slot
)
})?;
provides.insert(
conn.implementation_id.clone(),
ProvidesEntry {
interface: requires_entry.interface.clone(),
description: format!(
"Auto-generated from {}.{} connection",
other_id, other_slot
),
config: BTreeMap::new(),
},
);
}
}
}
let manifest = Manifest {
description: format!("Synthetic test manifest for {module_instance}"),
metadata: None,
provides,
requires,
enable_telemetry: false,
enable_external_mqtt: false,
config: BTreeMap::new(),
capabilities: Vec::new(),
enable_global_errors: false,
};
let mut tracked_files = vec![config_path.to_path_buf()];
tracked_files.extend(cache.into_paths());
Ok((manifest, tracked_files))
}

View File

@@ -0,0 +1,20 @@
use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Debug, Deserialize)]
pub struct Config {
pub active_modules: BTreeMap<String, ActiveModule>,
}
#[derive(Debug, Deserialize)]
pub struct ActiveModule {
pub module: String,
#[serde(default)]
pub connections: BTreeMap<String, Vec<Connection>>,
}
#[derive(Debug, Deserialize)]
pub struct Connection {
pub module_id: String,
pub implementation_id: String,
}

View File

@@ -0,0 +1,57 @@
use serde::{Deserialize, Serialize};
/// Implements the schema defined under `error-declaration.yaml`. Every type has
/// mandatory `name` and `description` fields.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Error {
/// The description of the error.
pub description: String,
/// The name of the error.
pub name: String,
/// The namespace of the error.
pub namespace: Option<String>,
}
/// Implements the list of errors.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ErrorList {
/// The description of all errors in the file.
pub description: String,
/// The list of errors.
/// We add default to allow make the `errors` field optional.
#[serde(default)]
pub errors: Vec<Error>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_yaml;
#[test]
fn test_deserialization() {
// Test with the list.
let _ = serde_yaml::from_str::<ErrorList>(
r#"
description: this is a description
errors:
- name: foo
description: bar
- name: this
description: that
"#,
)
.unwrap();
// Test without the list
let _ = serde_yaml::from_str::<ErrorList>(
r#"
description: just a description without errors
"#,
)
.unwrap();
}
}

View File

@@ -0,0 +1,78 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::types::Type;
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Interface {
pub description: String,
#[serde(default)]
pub cmds: BTreeMap<String, Command>,
#[serde(default)]
pub vars: BTreeMap<String, Type>,
/// The error reference represents the entry in the manifest were
/// we reference an error file.
#[serde(default)]
pub errors: Vec<ErrorReference>,
}
/// The same as the one above but the cpp runtime returns the errors as a map
/// contrary to the definition inside the yaml file...
#[derive(Debug, Deserialize, Serialize)]
pub struct InterfaceFromEverest {
// Note: EVerest config over mqtt does not return descriptions even so
// they should be necessary.
#[serde(default)]
pub description: String,
#[serde(default)]
pub cmds: BTreeMap<String, Command>,
#[serde(default)]
pub vars: BTreeMap<String, Type>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Command {
// Note: EVerest config over mqtt does not return descriptions even so
// they should be necessary.
#[serde(default)]
pub description: String,
#[serde(default)]
pub arguments: BTreeMap<String, Type>,
pub result: Option<Type>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ErrorReference {
pub reference: String,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_deserialization() {
serde_yaml::from_str::<Interface>(
r#"
description: >-
This is an example interface used for the error framework example modules.
errors:
- reference: /errors/example#/ExampleErrorA
- reference: /errors/example#/ExampleErrorB
- reference: /errors/example#/ExampleErrorC
- reference: /errors/example#/ExampleErrorD
"#,
)
.unwrap();
serde_yaml::from_str::<Interface>(
r#"
description: Nothing here.
"#,
)
.unwrap();
}
}

View File

@@ -0,0 +1,98 @@
use super::types::{BooleanOptions, IntegerOptions, NumberOptions, StringOptions};
use serde::Deserialize;
use std::collections::{BTreeMap, HashSet};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
#[serde(default)]
pub description: String,
#[serde(default)]
pub metadata: Option<Metadata>,
pub provides: BTreeMap<String, ProvidesEntry>,
#[serde(default)]
pub requires: BTreeMap<String, RequiresEntry>,
#[serde(default)]
pub enable_telemetry: bool,
// This is just here, so that we do not crash for deny_unknown_fields,
// this is never used in Rust code.
#[allow(dead_code)]
#[serde(default)]
pub enable_external_mqtt: bool,
#[serde(default)]
pub config: BTreeMap<String, ConfigEntry>,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub enable_global_errors: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProvidesEntry {
pub interface: String,
pub description: String,
#[serde(default)]
pub config: BTreeMap<String, ConfigEntry>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RequiresEntry {
pub interface: String,
pub min_connections: Option<i64>,
pub max_connections: Option<i64>,
#[serde(default)]
pub ignore: Ignore,
}
#[derive(Debug, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Ignore {
#[serde(default)]
pub vars: HashSet<String>,
#[serde(default)]
pub errors: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Metadata {
pub license: String,
pub authors: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ConfigEntry {
pub description: Option<String>,
#[serde(flatten)]
pub value: ConfigEnum,
#[serde(default = "MutabilityEnum::default")]
pub mutability: MutabilityEnum,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type", deny_unknown_fields)]
pub enum ConfigEnum {
Boolean(BooleanOptions),
String(StringOptions),
Integer(IntegerOptions),
Number(NumberOptions),
}
#[derive(Debug, Clone, Deserialize)]
pub enum MutabilityEnum {
ReadOnly,
ReadWrite,
WriteOnly,
}
impl MutabilityEnum {
fn default() -> Self {
MutabilityEnum::ReadOnly
}
}

View File

@@ -0,0 +1,11 @@
pub mod config;
pub mod error;
pub mod interface;
pub mod manifest;
pub mod types;
pub use config::Config;
pub use error::ErrorList;
pub use interface::{Interface, InterfaceFromEverest};
pub use manifest::Manifest;
pub use types::Type;

View File

@@ -0,0 +1,155 @@
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, HashSet};
/// Implements the schema defined under `type.yaml`. Every type has a `type`
/// and a `description` field.
#[derive(Debug, Deserialize, Serialize)]
pub struct Type {
// TODO(ddo) The schema says that this field is required, but multiple
// type definitions do not obey this rule.
pub description: Option<String>,
#[serde(flatten)]
pub arg: TypeBase,
/// This is part of the Variable definition.
pub qos: Option<i64>,
}
/// The type may be either represented by a string or by an array of strings.
/// In the case of an array of strings.
#[derive(Debug, Serialize)]
pub enum TypeBase {
Single(TypeEnum),
Multiple(Vec<TypeEnum>),
}
impl<'de> Deserialize<'de> for TypeBase {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let serde_yaml::Value::Mapping(map) = Deserialize::deserialize(deserializer)? else {
return Err(serde::de::Error::custom("Variable must be a mapping"));
};
let arg_type = map
.get("type")
.ok_or("The `type` tag is missing")
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
let arg = match arg_type {
serde_yaml::Value::String(_) => {
let t: TypeEnum = serde_yaml::from_value(serde_yaml::Value::Mapping(map))
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
TypeBase::Single(t)
}
serde_yaml::Value::Sequence(s) => {
let mut types = Vec::with_capacity(s.len());
for t in s.into_iter() {
let mut mapping = serde_yaml::Mapping::new();
mapping.insert(serde_yaml::Value::String("type".to_string()), t.clone());
let t: TypeEnum = serde_yaml::from_value(serde_yaml::Value::Mapping(mapping))
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
types.push(t);
}
TypeBase::Multiple(types)
}
_ => {
return Err(serde::de::Error::custom(
"'type' must be a sequence or a string.",
))
}
};
Ok(arg)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BooleanOptions {
pub default: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NumberOptions {
pub minimum: Option<f64>,
pub maximum: Option<f64>,
pub default: Option<f64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IntegerOptions {
pub minimum: Option<i64>,
pub maximum: Option<i64>,
pub default: Option<i64>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ArrayOptions {
pub min_items: Option<usize>,
pub max_items: Option<usize>,
pub items: Option<Box<Type>>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ObjectOptions {
#[serde(default)]
pub properties: BTreeMap<String, Type>,
#[serde(default)]
pub required: HashSet<String>,
#[serde(default)]
pub additional_properties: bool,
#[serde(rename = "$ref")]
pub object_reference: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum StringFormat {
#[serde(rename = "date-time")]
DateTime,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StringOptions {
pub pattern: Option<String>,
pub format: Option<StringFormat>,
pub max_length: Option<usize>,
pub min_length: Option<usize>,
#[serde(rename = "enum")]
pub enum_items: Option<Vec<String>>,
pub default: Option<String>,
#[serde(rename = "$ref")]
pub object_reference: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", tag = "type", deny_unknown_fields)]
pub enum TypeEnum {
Null,
Boolean(BooleanOptions),
String(StringOptions),
Number(NumberOptions),
Integer(IntegerOptions),
Array(ArrayOptions),
Object(ObjectOptions),
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DataTypes {
pub description: String,
pub types: BTreeMap<String, Type>,
}

View File

@@ -0,0 +1,14 @@
load("@rules_rust//rust:defs.bzl", "rust_proc_macro")
rust_proc_macro(
name = "everestrs-derive",
srcs = ["src/lib.rs"],
edition = "2021",
visibility = ["//visibility:public"],
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
"@everest_framework_crate_index//:proc-macro2",
"@everest_framework_crate_index//:quote",
"@everest_framework_crate_index//:syn",
],
)

View File

@@ -0,0 +1,13 @@
[package]
name = "everestrs-derive"
version = "0.25.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
everestrs-build = { workspace = true }

View File

@@ -0,0 +1,528 @@
use proc_macro::TokenStream;
use quote::quote;
use std::path::PathBuf;
use syn::{parse_macro_input, spanned::Spanned, ItemFn, ItemMod, Type};
/// Attribute macro that wraps an EVerest module's `main` function to control
/// module lifecycle. The user function receives a borrowed `&Module`, ensuring
/// it cannot be dropped prematurely. After the user function returns, the
/// `Module` is dropped deterministically before process exit.
///
/// # Basics
///
/// The basic usage is demonstated below
///
/// ```ignore
/// #[everestrs::main]
/// fn main(module: &Module) {
/// let class = Arc::new(MyModule {});
/// let _publishers = module.start(class.clone(), class.clone());
/// loop { std::thread::sleep(std::time::Duration::from_secs(1)); }
/// }
/// ```
///
/// # Async
///
/// You can also use async for your code. The pattern is quite similar:
///
/// ```ignore
/// #[everestrs::main]
/// #[tokio::main]
/// async fn main(module: &Module) {
/// let class = Arc::new(MyModule {});
/// ...
/// }
#[proc_macro_attribute]
pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
if !attr.is_empty() {
let attr2: proc_macro2::TokenStream = attr.into();
return syn::Error::new_spanned(attr2, "#[everestrs::main] takes no arguments")
.to_compile_error()
.into();
}
let input = parse_macro_input!(item as ItemFn);
if let Err(e) = main_validate(&input) {
return e.to_compile_error().into();
}
let sig = &input.sig;
let param = &sig.inputs[0];
let body = &input.block;
let ret = &sig.output;
let maybe_async = &sig.asyncness;
let maybe_await = maybe_async.as_ref().map(|_| quote! {.await});
let ident = &sig.ident;
let attrs = &input.attrs;
// Extract the parameter name and inner type from `name: &Type`.
let (param_name, inner_ty) = match param {
syn::FnArg::Typed(pat_type) => {
let name = &pat_type.pat;
match pat_type.ty.as_ref() {
Type::Reference(type_ref) => (name, &type_ref.elem),
_ => unreachable!("validated above"),
}
}
_ => unreachable!("validated above"),
};
let expanded = quote! {
#(#attrs)*
#maybe_async fn #ident() #ret {
let #param_name = #inner_ty::new();
let __everest_result = {
#maybe_async fn __everest_main(#param_name: &#inner_ty) #ret
#body
__everest_main(&#param_name) #maybe_await
};
__everest_result
}
};
expanded.into()
}
fn main_validate(input: &ItemFn) -> Result<(), syn::Error> {
if !input.sig.generics.params.is_empty() {
return Err(syn::Error::new(
input.sig.generics.span(),
"#[everestrs::main] does not support generic functions",
));
}
if input.sig.inputs.len() != 1 {
return Err(syn::Error::new(
input.sig.inputs.span(),
"#[everestrs::main] function must have exactly one parameter: `module: &Module`",
));
}
let param = &input.sig.inputs[0];
match param {
syn::FnArg::Receiver(_) => {
return Err(syn::Error::new(
param.span(),
"#[everestrs::main] function must not take `self`",
));
}
syn::FnArg::Typed(pat_type) => match pat_type.ty.as_ref() {
Type::Reference(type_ref) => {
if type_ref.mutability.is_some() {
return Err(syn::Error::new(
type_ref.mutability.span(),
"#[everestrs::main] parameter must be a shared reference (`&Module`), not `&mut`",
));
}
}
_ => {
return Err(syn::Error::new(
pat_type.ty.span(),
"#[everestrs::main] parameter must be a reference (e.g. `module: &Module`)",
));
}
},
}
Ok(())
}
struct TestAttr {
config_path: syn::LitStr,
module_name: syn::LitStr,
harness: bool,
}
impl syn::parse::Parse for TestAttr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut config_path = None;
let mut module_name = None;
let mut harness = false;
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let _: syn::Token![=] = input.parse()?;
match ident.to_string().as_str() {
"config" => {
config_path = Some(input.parse::<syn::LitStr>()?);
}
"module" => {
module_name = Some(input.parse::<syn::LitStr>()?);
}
"harness" => {
harness = input.parse::<syn::LitBool>()?.value;
}
other => {
return Err(syn::Error::new(
ident.span(),
format!(
"unknown attribute `{other}`, expected `config`, `module`, or `harness`"
),
));
}
}
if !input.is_empty() {
let _: syn::Token![,] = input.parse()?;
}
}
let config_path = config_path
.ok_or_else(|| syn::Error::new(input.span(), "missing `config` attribute"))?;
let module_name = module_name
.ok_or_else(|| syn::Error::new(input.span(), "missing `module` attribute"))?;
Ok(Self {
config_path,
module_name,
harness,
})
}
}
/// Attribute macro that creates an EVerest test. It launches the manager with
/// the given config, spawns the module standalone, and then executes the test
/// body. Each test gets a unique MQTT prefix so tests can run in parallel
/// without topic collisions.
///
/// # Basics
///
/// Both `config` and `module` are required.
///
/// It can either be applied inside a module tagged with `everestrs::harness`.
/// In this case all EVerest bindings generation is done by the harness and tests
/// can share generated code.
///
/// ```ignore
/// #[everestrs::harness(config = "config.yaml", module = "example_1")]
/// mod my_tests {
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_a(module: &Module) { ... }
///
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_b(module: &Module) { ... }
/// }
/// ```
///
/// Alternatively you can use the test macro to generate the EVerest bindings.
/// In this case the bindings are only accessible in the test itself:
/// ```ignore
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// fn test_a(module: &Module) { ... }
/// ```
///
/// # Other macros
///
/// The macro can be combined with other commonly used macros, for example
///
/// ```ignore
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// #[should_panic]
/// fn test_a(module: &Module) {
/// assert!(false);
/// }
/// ```
///
/// You can also combine it with #[tokio::test]. The ordering does not matter
///
/// ```ignore
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// #[tokio::test]
/// async fn my_test(module: &Module) {
/// }
/// ```
///
/// works same as
///
/// ```ignore
/// #[tokio::test]
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// async fn my_test(module: &Module) {
/// }
/// ```
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
let test_attr = parse_macro_input!(attr as TestAttr);
let input = parse_macro_input!(item as ItemFn);
// If config is provided, generate harness + test in a wrapping module.
// If not, just emit the test function (assumes harness is on an enclosing mod).
match test_impl(&test_attr, input) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn test_validate(input: &ItemFn) -> Result<(), syn::Error> {
if !input.sig.generics.params.is_empty() {
return Err(syn::Error::new(
input.sig.generics.span(),
"#[everestrs::test] does not support generic functions",
));
}
if input.sig.inputs.len() != 1 {
return Err(syn::Error::new(
input.sig.inputs.span(),
"#[everestrs::test] function must have exactly one parameter",
));
}
let param = &input.sig.inputs[0];
match param {
syn::FnArg::Receiver(_) => {
return Err(syn::Error::new(
param.span(),
"#[everestrs::test] function must not take `self`",
));
}
syn::FnArg::Typed(pat_type) => match pat_type.ty.as_ref() {
Type::Reference(type_ref) => {
if type_ref.mutability.is_some() {
return Err(syn::Error::new(
type_ref.mutability.span(),
"#[everestrs::test] parameter must be a shared reference, not `&mut`",
));
}
}
_ => {
return Err(syn::Error::new(
pat_type.ty.span(),
"#[everestrs::test] parameter must be a reference (e.g. `module: &Module`)",
));
}
},
}
Ok(())
}
fn test_impl(attr: &TestAttr, item_fn: ItemFn) -> Result<proc_macro2::TokenStream, syn::Error> {
// Sanity checks.
test_validate(&item_fn)?;
// Forward all attributes from the user function to the generated #[test] fn
// (e.g. #[should_panic], #[ignore], #[allow(...)]).
let attrs = &item_fn.attrs;
let sig = &item_fn.sig;
let ident = &sig.ident;
let param = &sig.inputs[0];
let ret = &sig.output;
let body = &item_fn.block;
// Check if someone else after us might emit #[test]. `rstest` does
// something similar to prevent reemit this. We match any attribute whose
// last path segment is `test` — covers `#[test]`, `#[tokio::test]`,
// `#[async_std::test]`, etc.
// See https://github.com/la10736/rstest/blob/master/rstest_macros/src/utils.rs#L38
// and https://github.com/la10736/rstest/blob/master/rstest_macros/src/parse/rstest/test_attr.rs#L25
let maybe_test = if attrs.iter().any(|a| {
a.path()
.segments
.last()
.map_or(false, |s| s.ident == "test")
}) {
quote! {}
} else {
quote! { #[test] }
};
let maybe_async = &sig.asyncness;
let maybe_await = maybe_async.as_ref().map(|_| quote! {.await});
let module = &attr.module_name;
// Extract the parameter name and inner type from `name: &Type`.
let (param_name, inner_ty) = match param {
syn::FnArg::Typed(pat_type) => {
let name = &pat_type.pat;
match pat_type.ty.as_ref() {
Type::Reference(type_ref) => (name, &type_ref.elem),
_ => unreachable!("validated above"),
}
}
_ => unreachable!("validated above"),
};
// Derive the config filename from the attribute. The bazel rule symlinks
// the config file to etc/everest/<basename>.
let config_basename = std::path::Path::new(&attr.config_path.value())
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
// The function we would like to emit
let test_fn = quote! {
#maybe_test
#(#attrs)*
#maybe_async fn #ident() #ret {
let prefix = std::env::current_dir().expect("Failed to get current directory");
let config = prefix.join(format!("etc/everest/{}", #config_basename));
// Each test gets a unique MQTT prefix so tests can run in parallel
// without topic collisions.
let __mqtt_prefix = format!(
"everest_test_{:?}_{:?}/",
std::process::id(),
std::thread::current().id(),
);
// Start the manager, telling it not to spawn the module under test.
// Blocks until the manager signals readiness via --status-fifo.
let _manager = ::everestrs::manager::Manager::start(
&prefix, &config, &[#module], Some(&__mqtt_prefix),
).expect("Failed to start manager");
let args = everestrs::Args {
prefix: prefix.clone(),
module: #module.to_string(),
log_config: prefix.join("etc/everest/default_logging.cfg"),
mqtt_broker_socket_path: None,
mqtt_broker_host: "localhost".to_string(),
mqtt_broker_port: 1883,
mqtt_everest_prefix: __mqtt_prefix,
mqtt_external_prefix: "".to_string(),
};
let #param_name = #inner_ty::new_with_args(args);
let __everest_result = {
#maybe_async fn __everest_test(#param_name: &#inner_ty) #ret
#body
__everest_test(&#param_name) #maybe_await
};
drop(#param_name);
__everest_result
}
};
if attr.harness {
let generated_tokens = generate_harness_tokens(attr)?;
let mod_ident = &item_fn.sig.ident;
Ok(quote! {
mod #mod_ident {
#generated_tokens
#[allow(unused_imports)]
use generated::Module;
#test_fn
}
})
} else {
Ok(test_fn)
}
}
fn resolve_everest_core_roots() -> Vec<PathBuf> {
if let Ok(val) = std::env::var("EVEREST_CORE_ROOT") {
val.split(':').map(PathBuf::from).collect()
} else {
// Fallback: try CARGO_MANIFEST_DIR and walk up to find the repo root.
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
let mut dir = PathBuf::from(&manifest_dir);
// Walk up looking for a directory that contains an "interfaces" subdirectory.
loop {
if dir.join("interfaces").is_dir() {
return vec![dir];
}
if !dir.pop() {
break;
}
}
}
Vec::new()
}
}
/// Core logic: resolves config, builds synthetic manifest, runs codegen.
/// Returns the generated tokens.
fn generate_harness_tokens(attr: &TestAttr) -> Result<proc_macro2::TokenStream, syn::Error> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map_err(|_| syn::Error::new(attr.config_path.span(), "CARGO_MANIFEST_DIR not set"))?;
let config_path = PathBuf::from(&manifest_dir).join(attr.config_path.value());
let module_instance = attr.module_name.value();
let everest_core_roots = resolve_everest_core_roots();
if everest_core_roots.is_empty() {
return Err(syn::Error::new(
attr.config_path.span(),
"Could not determine everest-core root. Set EVEREST_CORE_ROOT environment variable.",
));
}
let (manifest, _tracked_files) =
everestrs_build::build_test_manifest(&config_path, &module_instance, &everest_core_roots)
.map_err(|e| {
syn::Error::new(
attr.module_name.span(),
format!("Failed to build test manifest: {e}"),
)
})?;
let generated_code = everestrs_build::codegen::emit_manifest(manifest, everest_core_roots)
.map_err(|e| {
syn::Error::new(
attr.module_name.span(),
format!("Code generation failed: {e}"),
)
})?;
syn::parse_str(&generated_code).map_err(|e| {
syn::Error::new(
attr.module_name.span(),
format!("Failed to parse generated code: {e}"),
)
})
}
/// Attribute macro that auto-generates a test counterpart module by reading
/// a config.yaml and deducing interfaces from connected modules' manifests.
///
/// Applied to a `mod` to share generated types across multiple tests and
/// fixtures. For single test functions, use
/// `#[everestrs::test(config = "...", module = "...")]` instead.
///
/// ```ignore
/// #[everestrs::harness(config = "config.yaml", module = "example_1")]
/// mod my_tests {
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_a(module: &Module) { ... }
///
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_b(module: &Module) { ... }
/// }
/// ```
#[proc_macro_attribute]
pub fn harness(attr: TokenStream, item: TokenStream) -> TokenStream {
let test_attr = parse_macro_input!(attr as TestAttr);
let item_mod = parse_macro_input!(item as ItemMod);
match harness_impl(&test_attr, item_mod) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
/// Generates harness code and injects it into a module body.
fn harness_impl(
attr: &TestAttr,
item_mod: syn::ItemMod,
) -> Result<proc_macro2::TokenStream, syn::Error> {
let generated_tokens = generate_harness_tokens(attr)?;
if let Some((_brace, items)) = item_mod.content {
let vis = &item_mod.vis;
let ident = &item_mod.ident;
let attrs = &item_mod.attrs;
Ok(quote! {
#(#attrs)*
#vis mod #ident {
#generated_tokens
#[allow(unused_imports)]
use generated::Module;
#(#items)*
}
})
} else {
Err(syn::Error::new_spanned(
&item_mod,
"#[everestrs::harness] requires a module with a body (not just a declaration)",
))
}
}

View File

@@ -0,0 +1,81 @@
load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
load("@rules_cc//cc:defs.bzl", "cc_library")
load("@rules_rust//rust:defs.bzl", "rust_library")
rust_library(
name = "everestrs",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
proc_macro_deps = [
"//lib/everest/framework/everestrs/everestrs-derive",
],
visibility = ["//visibility:public"],
deps = [
":everestrs_bridge",
":everestrs_sys",
"//lib/everest/framework/everestrs/everestrs-build",
"@cxx.rs//:cxx",
"@everest_framework_crate_index//:clap",
"@everest_framework_crate_index//:log",
"@everest_framework_crate_index//:nix",
"@everest_framework_crate_index//:serde",
"@everest_framework_crate_index//:serde_json",
"@everest_framework_crate_index//:serde_yaml",
"@everest_framework_crate_index//:thiserror",
],
)
run_binary(
name = "everestrs_bridge/generated",
srcs = ["src/lib.rs"],
outs = [
"src/lib.rs.cc",
"src/lib.rs.h",
],
args = [
"$(location src/lib.rs)",
"-o",
"$(location src/lib.rs.h)",
"-o",
"$(location src/lib.rs.cc)",
],
tool = "@cxx.rs//:codegen",
)
cc_library(
name = "everestrs_bridge",
srcs = ["src/lib.rs" + ".cc"],
cxxopts = ["-std=c++17"],
visibility = ["//visibility:public"],
deps = [
":everestrs_bridge/include",
":everestrs_sys_include",
"//lib/everest/framework",
],
)
cc_library(
name = "everestrs_sys",
srcs = ["src/everestrs_sys.cpp"],
cxxopts = ["-std=c++17"],
visibility = ["//visibility:public"],
deps = [
":everestrs_bridge/include",
":everestrs_sys_include",
"//lib/everest/framework",
],
alwayslink = True,
)
cc_library(
name = "everestrs_bridge/include",
hdrs = ["src/lib.rs" + ".h"],
include_prefix = "everestrs",
)
cc_library(
name = "everestrs_sys_include",
hdrs = ["src/everestrs_sys.hpp"],
include_prefix = "everestrs",
deps = ["@cxx.rs//:core"],
)

View File

@@ -0,0 +1,28 @@
[package]
name = "everestrs"
version = "0.25.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.27", features = ["derive"] }
everestrs-build = { workspace = true }
everestrs-derive = { workspace = true }
log = { version = "0.4.20", features = ["std"] }
serde = { version = "1.0.175", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9.34"
thiserror = "1.0.48"
nix = { version = "0.31.2", features = ["fs"] }
# Note: the version must be kept in sync with the `cxxbridge-cmd` version installed in `everestrs/CMakeLists.txt`.
# The `=` before the version number ensures that Cargo uses exactly this version when generating new lockfiles.
cxx = { version = "=1.0.194", features = ["c++17"] }
[features]
build_bazel = []
# We use this for the integration tests in this project.
[dev-dependencies]
mockall = "0.13.0"
mockall_double = "0.3.1"
# For tests
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "sync", "time"] }

View File

@@ -0,0 +1,63 @@
# Rust support for EVerest
This is Rust support using cxx.rs to wrap the framework C++ library.
## Trying it out
- Install Rust as outlined on <https://rustup.rs/>, which should just be this
one line: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
- Built your workspace as outlined in `EVerest` README, make sure to tell
cMake to enable `EVEREST_ENABLE_RS_SUPPORT`.
- You can now try building the code, but it will not do anything: `cd everestrs
&& cargo build --all`
- You should now be able to configure the `RsExample` or `RsExampleUser` modules in your config
YAML.
## Differences to other EVerest language wrappers
- The `enable_external_mqtt` is ignored for Rust modules. If you want to interact
with MQTT externally, just pull an external mqtt module (for example the
really excellent [rumqttc](https://docs.rs/rumqttc/latest/rumqttc/)) crate
into your module and use it directly. This is a conscious decision to future
proof, should EVerest at some point move to something different than MQTT as
transport layer and for cleaner abstraction.
## Status
Full support for requiring and providing interfaces is implemented, missing
currently is:
- Support for EVerest logging
- Support for implementations with `max_connections != 1` or `min_connections != 1`
## Mocking in Unit-Tests
The Rust wrapper supports mocking, which allows you to unit tests your modules.
To enable mocking in your code you need to do some steps however
* Add [mockall](https://github.com/asomers/mockall) and
[mockall_double](https://github.com/asomers/mockall) to your module as dependencies
* Add a `mockall` feature to your module and enable it for your tests.
Then all publishers are mocked with `mockall`.
## Building from external repositories without CMake
Note that the `EVerest` (and `everest-framework` prior to migration to a monorepo layout) repositories are
automatically configured for this use case, this section is only relevant for
Rust modules residing in their own repositories.
While external Rust modules can be compiled using CMake without any setup,
compiling with `cargo` directly requires some additional setup. This is
required for rust-analyzer's IDE integration to work properly.
- First, build your EVerest workspace with Rust support enabled by
passing `-DEVEREST_ENABLE_RS_SUPPORT=ON` to CMake.
- Create a file at `modules/.cargo/config.toml` in your repository.
It file should contain the following, with `<build-dir>` replaced by the
name of your EVerest build directory.
```toml
[env.EVEREST_RS_LINK_DEPENDENCIES]
value = "../<build-dir>/everestrs-link-dependencies.txt"
relative = true
```

View File

@@ -0,0 +1,81 @@
use std::{
env,
fs::File,
io::{self, BufRead, BufReader},
path::Path,
};
/// Takes a path to a library like `libframework.so` and returns the name for the linker, aka
/// `framework`
fn libname_from_path(p: &Path) -> String {
let base = p
.file_stem()
.and_then(|os_str| os_str.to_str())
.expect("'p' must be valid UTF-8 and have an extension.")
.strip_prefix("lib")
.expect("'p' should start with `lib`");
// foo.so.1.2.3 -> foo
base.split('.').next().unwrap_or(base).to_string()
}
fn print_link_options(p: &Path) {
println!(
"cargo:rustc-link-search=native={}",
p.parent().unwrap().to_string_lossy()
);
println!("cargo:rustc-link-lib={}", libname_from_path(p));
}
/// Registers the libraries specified in the `EVEREST_RS_LINK_DEPENDENCIES` environment variable.
/// Expected to be a path to a text file that contains one object file path per line.
fn register_everest_link_deps(link_deps_path: &str) -> io::Result<()> {
let link_deps = File::open(link_deps_path).map_err(|e| {
io::Error::new(
e.kind(),
format!("Could not open EVEREST_RS_LINK_DEPENDENCIES file '{link_deps_path}': {e}"),
)
})?;
let mut found_any = false;
for line in BufReader::new(link_deps).lines() {
let line = line?;
let path = Path::new(&line);
if !path.is_file() {
return Err(io::Error::new(io::ErrorKind::NotFound, format!(
"Cannot find library path '{line}' specified in EVEREST_RS_LINK_DEPENDENCIES ({link_deps_path}).",
)));
}
print_link_options(path);
found_any = true;
}
if found_any {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("No library paths found in EVEREST_RS_LINK_DEPENDENCIES ({link_deps_path})."),
))
}
}
fn main() {
// See https://doc.rust-lang.org/cargo/reference/features.html#build-scripts
// for details.
if env::var("CARGO_FEATURE_BUILD_BAZEL").is_ok() {
println!("Skipping due to bazel");
return;
}
let Ok(link_deps_path) = env::var("EVEREST_RS_LINK_DEPENDENCIES") else {
panic!("EVEREST_RS_LINK_DEPENDENCIES environment variable is not set. This variable should point to the build/everestrs-link-dependencies.txt file generated by CMake.");
};
register_everest_link_deps(&link_deps_path)
.expect("Failed to register libraries specified in EVEREST_RS_LINK_DEPENDENCIES");
println!("cargo:rustc-link-lib=boost_log");
println!("cargo:rustc-link-lib=boost_log_setup");
}

View File

@@ -0,0 +1,310 @@
#include "everestrs/src/everestrs_sys.hpp"
#include "everestrs/src/lib.rs.h"
#include <everest/logging.hpp>
#include <utils/conversions.hpp>
#include <utils/error/error_manager_impl.hpp>
#include <utils/error/error_manager_req.hpp>
#include <utils/exceptions.hpp>
#include <utils/types.hpp>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <stdexcept>
#include <type_traits>
#include <variant>
namespace {
JsonBlob json2blob(const json& j) {
// I did not find a way to not copy the data at least once here.
const std::string dumped = j.dump();
rust::Vec<uint8_t> vec;
vec.reserve(dumped.size());
std::copy(dumped.begin(), dumped.end(), std::back_inserter(vec));
return JsonBlob{vec};
}
// Below are overloads to be used with std::visit and our std::variant. We force
// a compilation error if someone changes the underlying std::variant without
// extending/adjusting the functions below.
template <typename T, typename... VARIANT_T> struct VariantMemberImpl : public std::false_type {};
template <typename T, typename... VARIANT_T>
struct VariantMemberImpl<T, std::variant<VARIANT_T...>> : public std::disjunction<std::is_same<T, VARIANT_T>...> {};
/// @brief Static checker if the type T can be converted to `everest::config::ConfigEntry`.
///
/// We use this to detect `get_config_field` overloads which receive arguments
/// which aren't part of our `everest::config::ConfigEntry` variant.
template <typename T> struct ConfigEntryMember : public VariantMemberImpl<T, everest::config::ConfigEntry> {};
inline ConfigField get_config_field(const std::string& _name, bool _value) {
static_assert(ConfigEntryMember<decltype(_value)>::value);
return {_name, ConfigType::Boolean, _value, {}, 0, 0};
}
inline ConfigField get_config_field(const std::string& _name, const std::string& _value) {
static_assert(ConfigEntryMember<std::remove_cv_t<std::remove_reference_t<decltype(_value)>>>::value);
return {_name, ConfigType::String, false, _value, 0, 0};
}
inline ConfigField get_config_field(const std::string& _name, double _value) {
static_assert(ConfigEntryMember<decltype(_value)>::value);
return {_name, ConfigType::Number, false, {}, _value, 0};
}
inline ConfigField get_config_field(const std::string& _name, int _value) {
static_assert(ConfigEntryMember<decltype(_value)>::value);
return {_name, ConfigType::Integer, false, {}, 0, _value};
}
} // namespace
std::unique_ptr<Module> create_module(rust::Str module_id, rust::Str prefix, rust::Str mqtt_broker_socket_path,
rust::Str mqtt_broker_host, const std::uint16_t& mqtt_broker_port,
rust::Str mqtt_everest_prefix, rust::Str mqtt_external_prefix) {
auto socket_path = std::string(mqtt_broker_socket_path);
Everest::MQTTSettings mqtt_settings;
if (not socket_path.empty()) {
Everest::populate_mqtt_settings(mqtt_settings, socket_path, std::string(mqtt_everest_prefix),
std::string(mqtt_external_prefix));
} else {
Everest::populate_mqtt_settings(mqtt_settings, std::string(mqtt_broker_host), mqtt_broker_port,
std::string(mqtt_everest_prefix), std::string(mqtt_external_prefix));
}
return std::make_unique<Module>(std::string(module_id), std::string(prefix), mqtt_settings);
}
Module::Module(const std::string& module_id, const std::string& prefix, const Everest::MQTTSettings& mqtt_settings) :
module_id_(module_id) {
const auto mqtt_abstraction =
std::shared_ptr<Everest::MQTTAbstraction>(Everest::make_mqtt_abstraction(mqtt_settings));
// TODO(ddo) what happens when this returns false?
mqtt_abstraction->connect();
mqtt_abstraction->spawn_main_loop_thread();
const auto result = Everest::get_module_config(mqtt_abstraction, this->module_id_);
this->rs_ = std::make_unique<Everest::RuntimeSettings>(result.at("settings"));
config_ = std::make_shared<Everest::Config>(mqtt_settings, result);
handle_ = std::make_unique<Everest::Everest>(this->module_id_, *this->config_, this->rs_->validate_schema,
mqtt_abstraction, this->rs_->telemetry_prefix,
this->rs_->telemetry_enabled, this->rs_->forward_exceptions);
// Not needed but done to be congruent with the other bindings.
handle_->spawn_main_loop_thread();
}
Module::~Module() {
// MQTTAbstractionImpl spawns a main loop thread whose only exit condition
// is `disconnect_event` being notified. Without this call the `Thread`
// member destructor joins a thread that will never exit.
if (handle_) {
handle_->disconnect();
}
}
JsonBlob Module::get_interface(rust::Str interface_name) const {
const auto& interface_def = config_->get_interface_definition(std::string(interface_name));
return json2blob(interface_def);
}
JsonBlob Module::get_manifest() const {
const std::string& module_name = config_->get_module_name(std::string(module_id_));
return json2blob(config_->get_manifests().at(module_name));
}
void Module::signal_ready(const Runtime& rt) const {
handle_->register_on_ready_handler([&rt]() { rt.on_ready(); });
handle_->signal_ready();
}
void Module::provide_command(const Runtime& rt, rust::String implementation_id, rust::String name) const {
using namespace Everest;
handle_->provide_cmd(std::string(implementation_id), std::string(name), [&rt, implementation_id, name](json args) {
JsonBlob blob = rt.handle_command(implementation_id, name, json2blob(args));
const auto retval = json::parse(blob.data.begin(), blob.data.end());
// Check if our command handler failed.
if (retval.contains(conversions::ERROR_TYPE)) {
const auto error_str = retval.at(conversions::ERROR_TYPE).get<std::string>();
const auto error_msg = retval.at(conversions::ERROR_MSG).get<std::string>();
const auto error_enm = conversions::string_to_cmd_error_type(error_str);
switch (error_enm) {
case CmdErrorType::MessageParsingError:
throw MessageParsingError(error_msg);
case CmdErrorType::SchemaValidationError:
throw SchemaValidationError(error_msg);
case CmdErrorType::HandlerException:
throw HandlerException(error_msg);
case CmdErrorType::CmdTimeout:
throw CmdTimeout(error_msg);
case CmdErrorType::Shutdown:
throw Shutdown(error_msg);
case CmdErrorType::NotReady:
throw NotReady(error_msg);
}
}
return retval;
});
}
void Module::subscribe_variable(const Runtime& rt, rust::String implementation_id, std::size_t index,
rust::String name) const {
const auto req = Requirement{std::string(implementation_id), index};
// The handle_ptr is guaranteed to be alive in the callback.
const auto handle_ptr = handle_.get();
handle_->subscribe_var(req, std::string(name), [&rt, implementation_id, index, name, handle_ptr](json args) {
handle_ptr->ensure_ready();
rt.handle_variable(implementation_id, index, name, json2blob(args));
});
}
void Module::subscribe_all_errors(const Runtime& rt) const {
for (const Requirement& req : config_->get_requirements(module_id_)) {
std::shared_ptr<Everest::error::ErrorManagerReq> error_manager_ptr = nullptr;
try {
error_manager_ptr = handle_->get_error_manager_req(req);
} catch (const std::runtime_error& ex) {
// This is expected if the manifest has `ignore.errors=true`
// configured.
continue;
}
const auto handle_ptr = handle_.get();
error_manager_ptr->subscribe_all_errors(
[&rt, req, handle_ptr](Everest::error::Error error) {
handle_ptr->ensure_ready();
const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
rt.handle_on_error(rust::Str(req.id), req.index, rust_error, true);
},
[&rt, req, handle_ptr](Everest::error::Error error) {
handle_ptr->ensure_ready();
const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
rt.handle_on_error(rust::Str(req.id), req.index, rust_error, false);
});
}
}
JsonBlob Module::call_command(rust::Str implementation_id, std::size_t index, rust::Str name, JsonBlob blob) const {
using namespace Everest;
const auto req = Requirement{std::string(implementation_id), index};
json retval;
try {
retval = handle_->call_cmd(req, std::string(name), json::parse(blob.data.begin(), blob.data.end()));
} catch (const MessageParsingError& ex) {
to_json(retval, CmdResultError{CmdErrorType::MessageParsingError, ex.what(), nullptr});
} catch (const SchemaValidationError& ex) {
to_json(retval, CmdResultError{CmdErrorType::SchemaValidationError, ex.what(), nullptr});
} catch (const HandlerException& ex) {
to_json(retval, CmdResultError{CmdErrorType::HandlerException, ex.what(), nullptr});
} catch (const CmdTimeout& ex) {
to_json(retval, CmdResultError{CmdErrorType::CmdTimeout, ex.what(), nullptr});
} catch (const Shutdown& ex) {
to_json(retval, CmdResultError{CmdErrorType::Shutdown, ex.what(), nullptr});
} catch (const NotReady& ex) {
to_json(retval, CmdResultError{CmdErrorType::NotReady, ex.what(), nullptr});
}
return json2blob(retval);
}
void Module::publish_variable(rust::Str implementation_id, rust::Str name, JsonBlob blob) const {
handle_->publish_var(std::string(implementation_id), std::string(name),
json::parse(blob.data.begin(), blob.data.end()));
}
void Module::raise_error(rust::Str implementation_id, ErrorType error_type) const {
// Here everything is called implementation_id: We have the Rust string type
// then we have its c++ counterpart as std::string. And then we have the
// ImplementationIdentifier type :S
const std::string impl_id = std::string(implementation_id);
const auto full_mapping = handle_->get_3_tier_model_mapping();
std::optional<Mapping> mapping;
if (full_mapping.has_value()) {
const auto& inner = *full_mapping;
mapping = inner.module;
// We might have multiple mappings. In this case we pick the
// implementation mapping is since this is more specific.
const auto impl_mapping_iter = inner.implementations.find(impl_id);
if (impl_mapping_iter != inner.implementations.end()) {
mapping = impl_mapping_iter->second;
}
}
const ImplementationIdentifier id{module_id_, impl_id, mapping};
const Everest::error::Error error{std::string(error_type.error_type),
std::string{},
std::string(error_type.message),
std::string(error_type.description),
id,
static_cast<Everest::error::Severity>(error_type.severity)};
handle_->get_error_manager_impl(std::string(implementation_id))->raise_error(error);
}
void Module::clear_error(rust::Str implementation_id, rust::Str error_type, bool clear_all) const {
const auto manager = handle_->get_error_manager_impl(std::string(implementation_id));
if (error_type.empty()) {
manager->clear_all_errors();
} else {
if (clear_all) {
manager->clear_all_errors(std::string(error_type));
} else {
manager->clear_error(std::string(error_type));
}
}
}
rust::Vec<RsModuleConnections> Module::get_module_connections() const {
const auto connections = config_->get_module_config().connections;
// Iterate over the connections block.
rust::Vec<RsModuleConnections> out;
out.reserve(connections.size());
for (const auto& [req_id, fulfillment] : connections) {
out.emplace_back(RsModuleConnections{rust::String{req_id}, fulfillment.size()});
};
return out;
}
rust::Vec<RsModuleConfig> Module::get_module_configs(rust::Str module_id) const {
// TODO(ddo) We call this before initializing the logger.
const auto module_configs = config_->get_module_configs(std::string(module_id));
rust::Vec<RsModuleConfig> out;
out.reserve(module_configs.size());
// Iterate over all modules stored in the module_config.
for (const auto& mm : module_configs) {
RsModuleConfig mm_out{mm.first, {}};
mm_out.data.reserve(mm.second.size());
// Iterate over all configs stored in the mm (our current module).
for (const auto& cc : mm.second) {
mm_out.data.emplace_back(
std::visit([&](auto&& _value) { return ::get_config_field(cc.first, _value); }, cc.second));
}
out.emplace_back(std::move(mm_out));
}
return out;
}
int init_logging(rust::Str module_id, rust::Str prefix, rust::Str logging_config_file) {
const std::string module_id_cpp{module_id};
const std::string logging_config_file_cpp{logging_config_file};
// // Init the CPP logger.
return Everest::Logging::init(logging_config_file_cpp, module_id_cpp);
}
void log2cxx(int level, int line, rust::Str file, rust::Str message) {
Everest::Logging::ffi_log(level, line, std::string(file), std::string(message));
}

View File

@@ -0,0 +1,66 @@
#pragma once
#include <cstddef>
#include <memory>
#include <string>
#include <framework/everest.hpp>
#include <framework/runtime.hpp>
#include <utils/types.hpp>
#include "rust/cxx.h"
struct JsonBlob;
struct Runtime;
struct RsModuleConfig;
struct RsModuleConnections;
struct ConfigField;
struct ErrorType;
enum class ConfigTypes : uint8_t;
enum class ErrorState : uint8_t;
enum class ErrorSeverity : uint8_t;
class Module {
public:
/// @brief The c'tor should not be called by the user code.
///
/// In order to create the Module use the `create_module` function.
Module(const std::string& module_id, const std::string& prefix, const Everest::MQTTSettings& mqtt_settings);
/// Stops the MQTT main loop before member destruction so the
/// `mqtt_mainloop_thread` join inside `~MQTTAbstractionImpl` doesn't
/// deadlock.
~Module();
JsonBlob get_manifest() const;
JsonBlob get_interface(rust::Str interface_name) const;
rust::Vec<RsModuleConfig> get_module_configs(rust::Str module_name) const;
rust::Vec<RsModuleConnections> get_module_connections() const;
void signal_ready(const Runtime& rt) const;
void provide_command(const Runtime& rt, rust::String implementation_id, rust::String name) const;
JsonBlob call_command(rust::Str implementation_id, std::size_t index, rust::Str name, JsonBlob args) const;
void subscribe_variable(const Runtime& rt, rust::String implementation_id, std::size_t index,
rust::String name) const;
void subscribe_all_errors(const Runtime& rt) const;
void publish_variable(rust::Str implementation_id, rust::Str name, JsonBlob blob) const;
void raise_error(rust::Str implementation_id, ErrorType error_type) const;
void clear_error(rust::Str implementation_id, rust::Str error_type, bool clear_all) const;
private:
const std::string module_id_;
std::unique_ptr<Everest::RuntimeSettings> rs_;
std::shared_ptr<Everest::Config> config_;
std::unique_ptr<Everest::Everest> handle_;
};
std::unique_ptr<Module> create_module(rust::Str module_id, rust::Str prefix, rust::Str mqtt_broker_socket_path,
rust::Str mqtt_broker_host, const std::uint16_t& mqtt_broker_port,
rust::Str mqtt_everest_prefix, rust::Str mqtt_external_prefix);
int init_logging(rust::Str module_id, rust::Str prefix, rust::Str logging_config_file);
void log2cxx(int level, int line, rust::Str file, rust::Str message);

View File

@@ -0,0 +1,854 @@
pub mod manager;
use everestrs_build::schema;
use clap::Parser;
use log::debug;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Once;
use std::sync::OnceLock;
use thiserror::Error;
/// Prevent calling the init of loggers more than once.
static INIT_LOGGER_ONCE: Once = Once::new();
// Reexport everything so the clients can use it.
pub use everestrs_derive::{harness, main, test};
pub use log;
pub use serde;
pub use serde_json;
// TODO(ddo) Drop this again - its only there as a MVP for the enum support
// of errors.
pub use serde_yaml;
/// Errors matching the exceptions defined under `exceptions.hpp`.
///
/// The tags must match the tags defined under `conversions.hpp`. For client
/// side code - always use `HandlerException`.
#[derive(Error, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "__everest__error_type", content = "__everest__error_msg")]
pub enum Error {
#[error("Message Parsing Error: {0}")]
MessageParsingError(String),
#[error("Schema Validation Error: {0}")]
SchemaValidationError(String),
#[error("Handler Exception: {0}")]
HandlerException(String),
#[error("Command Timeout: {0}")]
CmdTimeout(String),
#[error("Shutdown: {0}")]
Shutdown(String),
#[error("Not Ready: {0}")]
NotReady(String),
}
pub type Result<T> = ::std::result::Result<T, Error>;
#[cxx::bridge]
mod ffi {
extern "Rust" {
type Runtime;
fn handle_command(
self: &Runtime,
implementation_id: &str,
name: &str,
json: JsonBlob,
) -> JsonBlob;
fn handle_variable(
self: &Runtime,
implementation_id: &str,
index: usize,
name: &str,
json: JsonBlob,
);
fn handle_on_error(
self: &Runtime,
implementation_id: &str,
index: usize,
error: ErrorType,
raised: bool,
);
fn on_ready(&self);
}
struct JsonBlob {
data: Vec<u8>,
}
/// The possible types a config can have. Note: Naturally this would be am
/// enum **with** values - however, cxx can't (for now) map Rusts enums to
/// std::variant or union.
#[derive(Debug)]
enum ConfigType {
Boolean = 0,
String = 1,
Number = 2,
Integer = 3,
}
/// One config entry: As said above, we can't use an enum and have to
/// declare all values. Note: also Option is not an option...
struct ConfigField {
/// The name of the option, e.x. `max_voltage`.
name: String,
/// Our poor-mans enum.
config_type: ConfigType,
/// The value of the config field. The field has only a meaning if
/// `conifg_type is set to [ConfigType::Boolean].
bool_value: bool,
/// The value of the config field. The field has only a meaning if
/// `config_type` is set to [ConfigType::String].
string_value: String,
/// The value of the config field. The field has only a meaning if
/// `config_type` is set to [ConfigType::Number].
number_value: f64,
/// The value of the config field. The field has only a meaning if
/// `config_type` is set to [ConfigType::Integer].
integer_value: i64,
}
/// The configs of one module. Roughly maps to the cpp's counterpart
/// `ModuleConfig`.
struct RsModuleConfig {
/// The name of the group, e.x. "PowerMeter".
module_name: String,
/// All `ConfigFields` in this group.
data: Vec<ConfigField>,
}
/// The information form the `connections` field of our current module.
struct RsModuleConnections {
/// The `implementation_id` of the connection block.
implementation_id: String,
/// Number of slots under the `implementation_id`.
slots: usize,
}
#[derive(Debug)]
pub enum ErrorSeverity {
Low,
Medium,
High,
}
/// Rust's version of the `<utils/error.hpp>`'s Error.
#[derive(Debug)]
pub struct ErrorType {
/// The type of the error. We generate that in the codegen. The
/// full error type looks like "evse_manager/PowermeterTransactionStartFailed"
/// and may have a namespace sprinkled into it (?).
pub error_type: String,
/// The description.
pub description: String,
/// The message - no idea what the difference to the description
/// actually is.
pub message: String,
/// The severity of the error.
pub severity: ErrorSeverity,
}
unsafe extern "C++" {
include!("everestrs/src/everestrs_sys.hpp");
type Module;
/// Creates the module only once. The module lives then until the end
/// of the process.
fn create_module(
module_id: &str,
prefix: &str,
mqtt_broker_socket_path: &str,
mqtt_broker_host: &str,
mqtt_broker_port: &u16,
mqtt_everest_prefix: &str,
mqtt_external_prefix: &str,
) -> UniquePtr<Module>;
/// Returns the manifest.
fn get_manifest(self: &Module) -> JsonBlob;
/// Returns the interface definition.
fn get_interface(self: &Module, interface_name: &str) -> JsonBlob;
/// Registers the callback of the `Subscriber` to be called and calls
/// `Everest::Module::signal_ready`.
fn signal_ready(self: &Module, rt: Pin<&Runtime>);
/// Informs the runtime that we implement the command described by `implementation_id` and
/// `name`, and registers the `handle_command` method from the `Subscriber` as the handler.
fn provide_command(
self: &Module,
rt: Pin<&Runtime>,
implementation_id: String,
name: String,
);
/// Call the command described by 'implementation_id' and `name` with the given 'args'.
/// Returns the return value.
fn call_command(
self: &Module,
implementation_id: &str,
index: usize,
name: &str,
args: JsonBlob,
) -> JsonBlob;
/// Informs the runtime that we want to receive the variable described by
/// `implementation_id` and `name` and registers the `handle_variable` method from the
/// `Subscriber` as the handler.
fn subscribe_variable(
self: &Module,
rt: Pin<&Runtime>,
implementation_id: String,
index: usize,
name: String,
);
/// Subscribes to all errors of the required modules.
fn subscribe_all_errors(self: &Module, rt: Pin<&Runtime>);
/// Returns the `connections` block defined in the `config.yaml` for
/// the current module.
fn get_module_connections(self: &Module) -> Vec<RsModuleConnections>;
/// Publishes the given `blob` under the `implementation_id` and `name`.
fn publish_variable(self: &Module, implementation_id: &str, name: &str, blob: JsonBlob);
/// Raises an error
fn raise_error(self: &Module, implementation_id: &str, error: ErrorType);
/// Clears an error
/// If the error_type is empty, we will clear all errors from the module.
fn clear_error(self: &Module, implementation_id: &str, error_type: &str, clear_all: bool);
/// Returns the module config from cpp.
fn get_module_configs(self: &Module, module_id: &str) -> Vec<RsModuleConfig>;
/// Call this once.
fn init_logging(module_id: &str, prefix: &str, conf: &str) -> i32;
/// Logging sink for the EVerest module.
fn log2cxx(level: i32, line: i32, file: &str, message: &str);
}
}
impl ffi::JsonBlob {
fn as_bytes(&self) -> &[u8] {
&self.data
}
fn deserialize<T: DeserializeOwned>(self) -> T {
// TODO(hrapp): Error handling
serde_json::from_slice(self.as_bytes()).expect(&format!(
"Failed to deserialize {:?}",
String::from_utf8_lossy(self.as_bytes())
))
}
fn from_vec(data: Vec<u8>) -> Self {
Self { data }
}
}
/// Very simple logger to use by the Rust modules.
mod logger {
use super::ffi;
use crate::INIT_LOGGER_ONCE;
pub(crate) struct Logger {
level: log::Level,
}
impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
// Rust gives the Error level 1 and all other severities a higher
// value.
metadata.level() <= self.level
}
fn log(&self, record: &log::Record) {
// The doc says `log` has to perform the filtering itself.
if !self.enabled(record.metadata()) {
return;
}
// This mapping should be kept in sync with liblog's
// Everest::Logging::severity_level.
let level = match record.level() {
log::Level::Trace => 0,
log::Level::Debug => 1,
log::Level::Info => 2,
log::Level::Warn => 3,
log::Level::Error => 4,
};
ffi::log2cxx(
level,
record.line().unwrap_or_default() as i32,
record.file().unwrap_or_default(),
&format!("{}", record.args()),
)
}
fn flush(&self) {}
}
impl Logger {
/// Init the logger for everest.
///
/// Don't do this on your own as we must also control some cxx code.
pub(crate) fn init_logger(module_name: &str, prefix: &str, conf: &str) {
INIT_LOGGER_ONCE.call_once(|| {
let level = match ffi::init_logging(module_name, prefix, conf) {
-1 => {
return;
}
0 => log::Level::Trace,
1 => log::Level::Debug,
2 => log::Level::Info,
3 => log::Level::Warn,
4 => log::Level::Error,
_ => log::Level::Info,
};
let logger = Self { level };
log::set_boxed_logger(Box::new(logger)).unwrap();
log::set_max_level(level.to_level_filter());
});
}
}
}
/// The cpp_module is for Rust an opaque type - so Rust can't tell if it is safe
/// to be accessed from multiple threads. We know that the c++ runtime is meant
/// to be used concurrently.
unsafe impl Sync for ffi::Module {}
unsafe impl Send for ffi::Module {}
pub use ffi::{ErrorSeverity, ErrorType as FfiErrorType};
#[derive(Debug)]
pub struct ErrorType<T> {
/// Serialised type from the FfiErrorType
pub error_type: T,
/// Carried over directly from the FfiErrorType
pub description: String,
/// Carried over directly from the FfiErrorType
pub message: String,
/// The severity of the error.
/// Carried over directly from the FfiErrorType
pub severity: ErrorSeverity,
}
impl<T> From<T> for ErrorType<T> {
fn from(t: T) -> ErrorType<T> {
ErrorType {
error_type: t,
description: String::new(),
message: String::new(),
severity: ErrorSeverity::High,
}
}
}
/// Arguments for an EVerest node.
#[derive(Parser, Debug)]
pub struct Args {
/// prefix of installation.
#[arg(long)]
pub prefix: PathBuf,
/// logging configuration that we are using.
#[arg(long, long = "log_config")]
pub log_config: PathBuf,
/// module name for us.
#[arg(long)]
pub module: String,
/// MQTT broker socket path
#[arg(long = "mqtt_broker_socket_path")]
pub mqtt_broker_socket_path: Option<PathBuf>,
/// MQTT broker hostname
#[arg(long = "mqtt_broker_host")]
pub mqtt_broker_host: String,
/// MQTT broker port
#[arg(long = "mqtt_broker_port")]
pub mqtt_broker_port: u16,
/// MQTT EVerest prefix
#[arg(long = "mqtt_everest_prefix")]
pub mqtt_everest_prefix: String,
/// MQTT external prefix
#[arg(long = "mqtt_external_prefix")]
pub mqtt_external_prefix: String,
}
/// Implements the handling of commands & variables, but has no specific information about the
/// details of the current module, i.e. it deals with JSON blobs and strings as command names. Code
/// generation is used to build the concrete, strongly typed abstractions that are then used by
/// final implementors.
pub trait Subscriber: Sync + Send {
/// Handler for the command `name` on `implementation_id` with the given `parameters`. The return value
/// will be returned as the result of the call.
fn handle_command(
&self,
implementation_id: &str,
name: &str,
parameters: HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value>;
/// Handler for the variable `name` on `implementation_id` with the given `value`.
fn handle_variable(
&self,
implementation_id: &str,
index: usize,
name: &str,
value: serde_json::Value,
) -> Result<()>;
/// Handler for the error raised/cleared callback
/// The `raised` flag indicates if the error is raised or cleared.
fn handle_on_error(
&self,
implementation_id: &str,
index: usize,
error: ffi::ErrorType,
raised: bool,
);
fn on_ready(&self) {}
}
enum PanicStrategy {
/// Log the panic and abort the process. Appropriate for production modules
/// where a panic in a callback means the module is in a broken state.
Abort,
/// Capture the panic and re-raise it on the main/test thread when the
/// module is dropped. Appropriate for tests where panics (e.g. from
/// mockall assertions) should be forwarded to the test harness.
Capture(std::sync::Mutex<Option<Box<dyn std::any::Any + Send>>>),
}
/// The [Runtime] is the central piece of the bridge between c++ and Rust. Rust
/// owns the [ffi::Module], and the ffi::Module owns the entire mqtt stack. So
/// when dropping, we first drop the ffi::Module and then the
/// Subscribers/Runtime which own the callbacks.
pub struct Runtime {
cpp_module: cxx::UniquePtr<ffi::Module>,
sub_impl: OnceLock<Arc<dyn Subscriber>>,
/// The config for the client module.
config: HashMap<String, HashMap<String, Config>>,
panic_strategy: PanicStrategy,
}
impl Runtime {
/// Handle a panic payload according to the configured strategy.
fn handle_panic(&self, payload: Box<dyn std::any::Any + Send>) {
match &self.panic_strategy {
PanicStrategy::Abort => {
// Extract a message for the log, then abort.
let msg = if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
log::error!("Panic on MQTT callback thread: {msg}");
std::process::abort();
}
PanicStrategy::Capture(captured) => {
let mut slot = captured.lock().unwrap();
if slot.is_none() {
*slot = Some(payload);
}
}
}
}
/// If a panic was captured on an MQTT callback thread, re-raise it on the
/// current thread. This is intended to be called from `Drop`.
pub fn check_panic(&self) {
if let PanicStrategy::Capture(captured) = &self.panic_strategy {
if let Some(payload) = captured.lock().unwrap().take() {
std::panic::resume_unwind(payload);
}
}
}
fn on_ready(&self) {
if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.sub_impl.get().unwrap().on_ready();
})) {
self.handle_panic(payload);
}
}
fn handle_command(&self, impl_id: &str, name: &str, json: ffi::JsonBlob) -> ffi::JsonBlob {
debug!("handle_command: {impl_id}, {name}, '{:?}'", json.as_bytes());
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let parameters: Option<HashMap<String, serde_json::Value>> = json.deserialize();
let retval = self.sub_impl.get().unwrap().handle_command(
impl_id,
name,
parameters.unwrap_or_default(),
);
match retval {
Ok(blob) => ffi::JsonBlob::from_vec(serde_json::to_vec(&blob).unwrap()),
Err(err) => ffi::JsonBlob::from_vec(serde_json::to_vec(&err).unwrap()),
}
}));
match result {
Ok(blob) => blob,
Err(payload) => {
self.handle_panic(payload);
// Return an error response so the C++ side doesn't hang.
// Only reached with PanicStrategy::Capture (Abort never returns).
let err = Error::HandlerException("panic in command handler".into());
ffi::JsonBlob::from_vec(serde_json::to_vec(&err).unwrap())
}
}
}
fn handle_variable(&self, impl_id: &str, index: usize, name: &str, json: ffi::JsonBlob) {
debug!(
"handle_variable: {impl_id}, {name}, '{:?}'",
json.as_bytes()
);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if let Err(err) = self.sub_impl.get().unwrap().handle_variable(
impl_id,
index,
name,
json.deserialize(),
) {
log::error!("`handle_variable` failed: {err:?}");
}
}));
if let Err(payload) = result {
self.handle_panic(payload);
}
}
fn handle_on_error(&self, impl_id: &str, index: usize, error: ffi::ErrorType, raised: bool) {
debug!("handle_on_error: {impl_id}, index {index}, raised {raised}");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.sub_impl
.get()
.unwrap()
.handle_on_error(impl_id, index, error, raised);
}));
if let Err(payload) = result {
self.handle_panic(payload);
}
}
pub fn publish_variable<T: serde::Serialize>(
&self,
impl_id: &str,
var_name: &str,
message: &T,
) {
let blob = ffi::JsonBlob::from_vec(
serde_json::to_vec(&message).expect("Serialization of data cannot fail."),
);
(self.cpp_module).publish_variable(impl_id, var_name, blob);
}
pub fn call_command<T: serde::Serialize, R: serde::de::DeserializeOwned>(
&self,
impl_id: &str,
index: usize,
name: &str,
args: &T,
) -> Result<R> {
let blob = ffi::JsonBlob::from_vec(
serde_json::to_vec(args).expect("Serialization of data cannot fail."),
);
let return_value = (self.cpp_module).call_command(impl_id, index, name, blob);
match serde_json::from_slice(&return_value.data) {
Ok(ok) => Ok(ok),
Err(_) => match serde_json::from_slice::<Error>(&return_value.data) {
Ok(err) => Err(err),
Err(err) => Err(Error::MessageParsingError(format!("{err:?}"))),
},
}
}
/// Called from the generated code.
/// The type T should be an error.
pub fn raise_error<T: serde::Serialize + core::fmt::Debug>(
&self,
impl_id: &str,
error: ErrorType<T>,
) {
let error_string = serde_yaml::to_string(&error.error_type).unwrap_or_default();
// Remove the new line -> this should be gone once we stop using yaml
// since we don't really want yaml.
let error_string = error_string.strip_suffix("\n").unwrap_or(&error_string);
debug!("Raising error {error_string:?} from {error:?}");
let error_type = ffi::ErrorType {
error_type: error_string.to_string(),
description: error.description,
message: error.message,
severity: error.severity,
};
self.cpp_module.raise_error(impl_id, error_type);
}
/// Called from the generated code.
/// The type T should be an error.
pub fn clear_error<T: serde::Serialize + core::fmt::Debug>(
&self,
impl_id: &str,
error: T,
clear_all: bool,
) {
let error_string = serde_yaml::to_string(&error).unwrap_or_default();
let mut error_string = error_string.strip_suffix("\n").unwrap_or(&error_string);
// The yaml conversion changes empty strings into a string containing two
// single quotes which we catch and convert to an actual empty string
if error_string == "''" {
error_string = "";
}
debug!("Clearing the {error_string} from {error:?}");
self.cpp_module
.clear_error(impl_id, &error_string, clear_all);
}
/// Create a runtime by parsing CLI arguments. Uses [`PanicStrategy::Abort`]
/// since this is the production entry point.
pub fn new() -> Pin<Arc<Self>> {
let args: Args = Args::parse();
Self::create(args, PanicStrategy::Abort)
}
/// Create a runtime with explicit args instead of parsing CLI arguments.
/// Uses [`PanicStrategy::Capture`] since this is used by test harnesses.
pub fn new_with_args(args: Args) -> Pin<Arc<Self>> {
Self::create(args, PanicStrategy::Capture(std::sync::Mutex::new(None)))
}
fn create(args: Args, panic_strategy: PanicStrategy) -> Pin<Arc<Self>> {
logger::Logger::init_logger(
&args.module,
&args.prefix.to_string_lossy(),
&args.log_config.to_string_lossy(),
);
let cpp_module = ffi::create_module(
&args.module,
&args.prefix.to_string_lossy(),
&args
.mqtt_broker_socket_path
.unwrap_or_default()
.to_string_lossy(),
&args.mqtt_broker_host,
&args.mqtt_broker_port,
&args.mqtt_everest_prefix,
&args.mqtt_external_prefix,
);
let raw_config = cpp_module.get_module_configs(&args.module);
// Convert the nested Vec's into nested HashMaps.
let mut config: HashMap<String, HashMap<String, Config>> = HashMap::new();
for mm_config in raw_config {
let cc_config = mm_config
.data
.into_iter()
.map(|field| {
let value = match field.config_type {
ffi::ConfigType::Boolean => Config::Boolean(field.bool_value),
ffi::ConfigType::String => Config::String(field.string_value),
ffi::ConfigType::Number => Config::Number(field.number_value),
ffi::ConfigType::Integer => Config::Integer(field.integer_value),
_ => panic!("Unexpected value {:?}", field.config_type),
};
(field.name, value)
})
.collect::<HashMap<_, _>>();
// If we have already an entry with the `module_name`, we try to extend
// it.
config
.entry(mm_config.module_name)
.or_default()
.extend(cc_config);
}
Arc::pin(Self {
cpp_module,
sub_impl: OnceLock::new(),
config,
panic_strategy,
})
}
pub fn set_subscriber(self: Pin<&Self>, sub_impl: Arc<dyn Subscriber>) {
self.sub_impl
.set(sub_impl)
.unwrap_or_else(|_| panic!("set_subscriber called twice"));
let manifest_json = self.cpp_module.get_manifest();
let manifest: schema::Manifest = manifest_json.deserialize();
log::debug!("Deserialiazed the manifest {manifest:?}");
// Implement all commands for all of our implementations, dispatch everything to the
// Subscriber.
for (implementation_id, provides) in manifest.provides {
let interface_s = self.cpp_module.get_interface(&provides.interface);
let interface: schema::InterfaceFromEverest = interface_s.deserialize();
log::debug!("Deserialiazed the interface {interface:?}");
for (name, _) in interface.cmds {
self.cpp_module
.provide_command(self, implementation_id.clone(), name);
}
}
let connections = self.get_module_connections();
// Subscribe to all variables that might be of interest.
for (implementation_id, requires) in manifest.requires {
let connection = connections.get(&implementation_id).cloned().unwrap_or(0);
let interface_s = self.cpp_module.get_interface(&requires.interface);
// EVerest framework may return null if an interface is not used in
// the config (the connection is then 0).
if interface_s.as_bytes() == b"null" && connection == 0 {
debug!("Skipping the interface {implementation_id}");
continue;
}
let interface: schema::InterfaceFromEverest = interface_s.deserialize();
log::debug!("Deserialiazed the interface {interface:?}");
for i in 0usize..connection {
for (name, _) in interface.vars.iter() {
if requires.ignore.vars.contains(name) {
continue;
}
self.cpp_module.subscribe_variable(
self,
implementation_id.clone(),
i,
name.clone(),
);
}
}
}
self.cpp_module.subscribe_all_errors(self);
// Since users can choose to overwrite `on_ready`, we can call signal_ready right away.
// TODO(hrapp): There were some doubts if this strategy is too inflexible, discuss design
// again.
(self.cpp_module).signal_ready(self);
}
/// The interface for fetching the module connections though the C++ runtime.
pub fn get_module_connections(&self) -> HashMap<String, usize> {
let raw_connections = self.cpp_module.get_module_connections();
raw_connections
.into_iter()
.map(|connection| (connection.implementation_id, connection.slots))
.collect()
}
/// Interface for fetching the configurations through the C++ runtime.
pub fn get_module_configs(&self) -> &HashMap<String, HashMap<String, Config>> {
&self.config
}
}
impl Drop for Runtime {
fn drop(&mut self) {
// Re-raise any panic that was captured on an MQTT callback thread,
// but only if we're not already unwinding from another panic.
if !std::thread::panicking() {
self.check_panic();
}
}
}
/// A store for our config values. The type is closely related to
/// [ffi::ConfigField] and [ffi::ConfigType].
#[derive(Debug)]
pub enum Config {
Boolean(bool),
String(String),
Number(f64),
Integer(i64),
}
impl TryFrom<&Config> for bool {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::Boolean(value) => Ok(*value),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}
impl TryFrom<&Config> for String {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::String(value) => Ok(value.clone()),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}
impl TryFrom<&Config> for f64 {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::Number(value) => Ok(*value),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}
impl TryFrom<&Config> for i64 {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::Integer(value) => Ok(*value),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}

View File

@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
use nix::fcntl::{open, OFlag};
use nix::sys::stat;
use nix::unistd;
use std::io::{self};
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
/// Handle to a running EVerest manager process.
///
/// The manager is spawned as a subprocess using `{prefix}/bin/manager`. When
/// dropped, the manager process is killed if it is still running.
pub struct Manager {
child: Arc<Mutex<Child>>,
stopping: Arc<AtomicBool>,
watcher: Option<thread::JoinHandle<()>>,
}
impl Manager {
/// Start the EVerest manager as a subprocess and wait until it is ready.
///
/// * `prefix` — the EVerest sysroot (passed as `--prefix`)
/// * `config` — path to the config file (passed as `--config`)
/// * `standalone` — module IDs that the manager should not spawn
/// (passed as `--standalone`). If non-empty, this function blocks until
/// the manager reports that all managed modules are started and it is
/// waiting for the standalone modules.
/// * `mqtt_everest_prefix` — optional MQTT topic prefix override. When set,
/// passed as `--mqtt_everest_prefix` to the manager, allowing multiple
/// test instances to run in parallel without topic collisions.
pub fn start(
prefix: &Path,
config: &Path,
standalone: &[&str],
mqtt_everest_prefix: Option<&str>,
) -> io::Result<Self> {
let binary = Self::manager_binary(prefix);
let mut cmd = Command::new(&binary);
cmd.arg("--prefix").arg(prefix);
cmd.arg("--config").arg(config);
if let Some(mqtt_prefix) = mqtt_everest_prefix {
cmd.arg("--mqtt_everest_prefix").arg(mqtt_prefix);
}
let mut fifo = None;
if !standalone.is_empty() {
let fifo_name = match mqtt_everest_prefix {
Some(p) => format!("status-{}.fifo", p.replace('/', "_")),
None => format!("status-{}.fifo", std::process::id()),
};
let fifo_path = std::env::temp_dir().join(fifo_name);
unistd::mkfifo(&fifo_path, stat::Mode::S_IRWXU)?;
fifo = Some(open(
&fifo_path,
OFlag::O_RDONLY | OFlag::O_NONBLOCK,
stat::Mode::empty(),
)?);
cmd.arg("--status-fifo").arg(&fifo_path);
cmd.arg("--standalone");
for module_id in standalone {
cmd.arg(module_id);
}
}
let child = Arc::new(Mutex::new(cmd.spawn().map_err(|e| {
io::Error::new(
e.kind(),
format!("Failed to start manager at {}: {e}", binary.display()),
)
})?));
let stopping = Arc::new(AtomicBool::new(false));
let watcher = {
let child = child.clone();
let stopping = stopping.clone();
Some(std::thread::spawn(move || loop {
let status = child.lock().unwrap().try_wait().unwrap();
match status {
None => thread::sleep(Duration::from_millis(100)),
Some(status) => {
if !stopping.load(Ordering::Relaxed) {
panic!("Manager process exited unexpectedly with status: {status}");
}
return;
}
}
}))
};
// Wait for the manager to signal readiness via the status fifo.
if let Some(fd) = fifo {
let mut buf = [0u8; 256];
let mut accumulated = String::new();
loop {
match unistd::read(&fd, &mut buf) {
Ok(n) => {
accumulated.push_str(&String::from_utf8_lossy(&buf[..n]));
if accumulated.contains("WAITING_FOR_STANDALONE_MODULES")
|| accumulated.contains("ALL_MODULES_STARTED")
{
break;
}
}
Err(nix::errno::Errno::EAGAIN) => {
thread::sleep(Duration::from_millis(50));
}
Err(e) => return Err(io::Error::from_raw_os_error(e as i32)),
}
// The watcher thread crashed - we won't see anything in our
// fifo.
if watcher.as_ref().unwrap().is_finished() {
break;
}
}
let _ = unistd::close(fd);
}
Ok(Self {
child,
stopping,
watcher,
})
}
/// Returns the path to the manager binary for a given prefix.
fn manager_binary(prefix: &Path) -> PathBuf {
prefix.join("bin").join("manager")
}
}
impl Drop for Manager {
fn drop(&mut self) {
self.stopping.store(true, Ordering::Relaxed);
let _ = self.child.lock().unwrap().kill();
if let Err(panic) = self.watcher.take().expect("always there").join() {
std::panic::resume_unwind(panic);
}
}
}

View File

@@ -0,0 +1,3 @@
exports_files([
"logging.ini",
])

View File

@@ -0,0 +1,5 @@
filegroup(
name = "errors",
srcs = glob(["*.yaml"]),
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,10 @@
description: Example errors
errors:
- name: ExampleErrorA
description: Example error A
- name: ExampleErrorB
description: Example error B
- name: ExampleErrorC
description: Example error C
- name: ExampleErrorD
description: Example error D

View File

@@ -0,0 +1,10 @@
description: More errors.
errors:
- name: ExampleErrorA
description: >-
The same error as in example_errors.yaml. The error names are placed in a
namespace
- name: MoreError
description: additinal error
- name: snake_case_error
description: We will correct snake cases

View File

@@ -0,0 +1,2 @@
description: No errors.
errors: []

View File

@@ -0,0 +1,5 @@
filegroup(
name = "interfaces",
srcs = glob(["*.yaml"]),
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1 @@
description: An empty interface.

View File

@@ -0,0 +1,5 @@
description: >-
We make sure that we allow duplicate errors
errors:
- reference: /errors/example_errors#/ExampleErrorA
- reference: /errors/example_errors

View File

@@ -0,0 +1,5 @@
description: >-
We make sure that we respect multiple error definitions
errors:
- reference: /errors/example_errors
- reference: /errors/more_errors

View File

@@ -0,0 +1,4 @@
description: >-
We make sure that we can parse the error files which don't define any errors
errors:
- reference: /errors/no_errors

View File

@@ -0,0 +1,5 @@
description: >-
We make sure that we respect a subset of errors
errors:
- reference: /errors/example_errors#/ExampleErrorA
- reference: /errors/example_errors#/ExampleErrorB

View File

@@ -0,0 +1,20 @@
description: >-
This interface defines an example interface that uses multiple framework
features
cmds:
uses_something:
description: This command checks if something is stored under a given key
arguments:
key:
description: Key to check the existence for
type: string
pattern: ^[A-Za-z0-9_.]+$
result:
description: Returns 'True' if something was stored for this key
type: boolean
vars:
max_current:
description: Provides maximum current of this supply in ampere
type: number
errors:
- reference: /errors/example_errors

View File

@@ -0,0 +1,24 @@
# for documentation on this file format see:
# https://www.boost.org/doc/libs/1_54_0/libs/log/doc/html/log/detailed/utilities.html#log.detailed.utilities.setup.filter_formatter
[Core]
DisableLogging=false
# To get debug logs of only one module, add the "%Process% contains" filter, e.g.:
#
# "(%Process% contains OCPP201 and %Severity% >= DEBG)"
#
# whereas "OCPP201" is the value of the field `active_modules.NAME.module` in the respective /config/config-*.yaml.
Filter="%Severity% >= INFO"
[Sinks.Console]
Destination=Console
# Filter="%Target% contains \"MySink1\""
Format="%TimeStamp% [%Severity%] \033[1;32m%Process%\033[0m \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%"
Asynchronous=false
AutoFlush=true
SeverityStringColorDebug="\033[1;30m"
SeverityStringColorInfo="\033[1;37m"
SeverityStringColorWarning="\033[1;33m"
SeverityStringColorError="\033[1;31m"
SeverityStringColorCritical="\033[1;35m"

View File

@@ -0,0 +1 @@
exports_files(["smoke_test.sh"])

Some files were not shown because too many files have changed in this diff Show More