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:
@@ -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
|
||||
@@ -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}"
|
||||
)
|
||||
742
tools/EVerest-main/lib/everest/framework/everestrs/Cargo.lock
generated
Normal file
742
tools/EVerest-main/lib/everest/framework/everestrs/Cargo.lock
generated
Normal 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"
|
||||
@@ -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" }
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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 %}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 %}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 %}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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 }
|
||||
@@ -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)",
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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"],
|
||||
)
|
||||
@@ -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"] }
|
||||
@@ -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
|
||||
```
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
exports_files([
|
||||
"logging.ini",
|
||||
])
|
||||
@@ -0,0 +1,5 @@
|
||||
filegroup(
|
||||
name = "errors",
|
||||
srcs = glob(["*.yaml"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
description: No errors.
|
||||
errors: []
|
||||
@@ -0,0 +1,5 @@
|
||||
filegroup(
|
||||
name = "interfaces",
|
||||
srcs = glob(["*.yaml"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
description: An empty interface.
|
||||
@@ -0,0 +1,5 @@
|
||||
description: >-
|
||||
We make sure that we allow duplicate errors
|
||||
errors:
|
||||
- reference: /errors/example_errors#/ExampleErrorA
|
||||
- reference: /errors/example_errors
|
||||
@@ -0,0 +1,5 @@
|
||||
description: >-
|
||||
We make sure that we respect multiple error definitions
|
||||
errors:
|
||||
- reference: /errors/example_errors
|
||||
- reference: /errors/more_errors
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
exports_files(["smoke_test.sh"])
|
||||
@@ -0,0 +1,96 @@
|
||||
load("@rules_python//python:defs.bzl", "py_test")
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
|
||||
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||
load("//applications/utils:requirements.bzl", "requirement")
|
||||
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env", "everest_test")
|
||||
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "RsAsyncBinary",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
"@everest_framework_crate_index//:tokio",
|
||||
],
|
||||
)
|
||||
|
||||
rs_everest_module(
|
||||
name = "RsAsync",
|
||||
binary = ":RsAsyncBinary",
|
||||
manifest = "manifest.yaml",
|
||||
)
|
||||
|
||||
everest_env(
|
||||
name = "config_env",
|
||||
config_file = "config.yaml",
|
||||
modules = [
|
||||
":RsAsync",
|
||||
],
|
||||
)
|
||||
|
||||
sh_test(
|
||||
name = "integration_test",
|
||||
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
|
||||
data = [":config_env"],
|
||||
tags = ["exclusive"],
|
||||
)
|
||||
|
||||
# Integration tests for RsAsync. Will launch EVerest with a ProbeModule and
|
||||
# validate calls to our trait mocks.
|
||||
rust_test(
|
||||
name = "RsAsyncMockedTestBinary",
|
||||
srcs = ["rs_tests/tests.rs"],
|
||||
compile_data = [
|
||||
"config.yaml",
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
crate_features = [
|
||||
"mockall",
|
||||
"trait",
|
||||
],
|
||||
data = ["config_env"],
|
||||
edition = "2021",
|
||||
proc_macro_deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-derive",
|
||||
"@everest_framework_crate_index//:mockall_double",
|
||||
],
|
||||
rustc_env = {
|
||||
"CARGO_MANIFEST_DIR": "lib/everest/framework/everestrs/tests/modules/RsAsync",
|
||||
"EVEREST_CORE_ROOT": "lib/everest/framework/everestrs/tests",
|
||||
},
|
||||
tags = ["exclusive"],
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"@everest_framework_crate_index//:mockall",
|
||||
"@everest_framework_crate_index//:tokio",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Test config for regression tests for the framework.
|
||||
active_modules:
|
||||
example_0:
|
||||
module: RsAsync
|
||||
connections:
|
||||
receiver:
|
||||
- module_id: example_1
|
||||
implementation_id: sender
|
||||
example_1:
|
||||
module: RsAsync
|
||||
connections:
|
||||
receiver:
|
||||
- module_id: example_0
|
||||
implementation_id: sender
|
||||
@@ -0,0 +1,14 @@
|
||||
description: Simple Example
|
||||
provides:
|
||||
sender:
|
||||
interface: example
|
||||
description: An example interface.
|
||||
requires:
|
||||
receiver:
|
||||
interface: example
|
||||
ignore:
|
||||
errors: true
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
@@ -0,0 +1,60 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[tokio::test]
|
||||
#[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
|
||||
async fn test_tokio_everest(module: &Module) {
|
||||
use super::*;
|
||||
use generated::*;
|
||||
|
||||
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let mut mock_client = MockExampleClientSubscriber::new();
|
||||
mock_client
|
||||
.expect_on_max_current()
|
||||
.withf(|_, value| *value == 123.0)
|
||||
.times(1)
|
||||
.return_once(move |_, _| {
|
||||
tx.send(()).unwrap();
|
||||
});
|
||||
let mock_client = Arc::new(mock_client);
|
||||
|
||||
let mut mock_on_ready = MockOnReadySubscriber::new();
|
||||
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
|
||||
|
||||
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.await.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
|
||||
#[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
|
||||
#[tokio::test]
|
||||
async fn test_everest_tokio(module: &Module) {
|
||||
use super::*;
|
||||
use generated::*;
|
||||
|
||||
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let mut mock_client = MockExampleClientSubscriber::new();
|
||||
mock_client
|
||||
.expect_on_max_current()
|
||||
.withf(|_, value| *value == 123.0)
|
||||
.times(1)
|
||||
.return_once(move |_, _| {
|
||||
tx.send(()).unwrap();
|
||||
});
|
||||
let mock_client = Arc::new(mock_client);
|
||||
|
||||
let mut mock_on_ready = MockOnReadySubscriber::new();
|
||||
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
|
||||
|
||||
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.await.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
use generated::{
|
||||
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
|
||||
OnReadySubscriber,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
// Just to use async.
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub struct OneClass {
|
||||
tx: Mutex<Option<oneshot::Sender<String>>>,
|
||||
}
|
||||
|
||||
impl ExampleServiceSubscriber for OneClass {
|
||||
fn uses_something(&self, _context: &Context, key: String) -> ::everestrs::Result<bool> {
|
||||
log::info!("Received {key}");
|
||||
let tx = self.tx.lock().unwrap().take();
|
||||
if let Some(tx) = tx {
|
||||
tx.send(key).unwrap();
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleClientSubscriber for OneClass {
|
||||
fn on_max_current(&self, context: &Context, value: f64) {
|
||||
log::info!("Received {value}");
|
||||
context
|
||||
.publisher
|
||||
.receiver
|
||||
.uses_something(format!("{value}"))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl OnReadySubscriber for OneClass {
|
||||
fn on_ready(&self, _publishers: &ModulePublisher) {
|
||||
log::info!("Ready");
|
||||
}
|
||||
}
|
||||
|
||||
/// Example how to use async with Everest. Everything in EVerest (all traits)
|
||||
/// remain strictly sync because of the underlying c++ runtime. However, you can
|
||||
/// combine your async code with sync EVerest.
|
||||
///
|
||||
/// You can combine the `everestrs::main` macro with `tokio::main` macro. The
|
||||
/// ordering does not really matter, so for non-main function (functions which
|
||||
/// can receive input args), you can also write
|
||||
/// ```ignore
|
||||
/// #[tokio::main]
|
||||
/// #[everestrs::main]
|
||||
/// async fn my_fun(module: &Module) {}
|
||||
/// ```
|
||||
#[everestrs::main]
|
||||
#[tokio::main]
|
||||
async fn main(module: &Module) {
|
||||
let config = module.get_config();
|
||||
log::info!("Received the config {config:?}");
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let one_class = Arc::new(OneClass {
|
||||
tx: Mutex::new(Some(tx)),
|
||||
});
|
||||
let publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
|
||||
|
||||
publishers.sender.max_current(123.).unwrap();
|
||||
|
||||
// Simulate some async steps...
|
||||
let result = rx.await.unwrap();
|
||||
log::info!("Done {result}");
|
||||
|
||||
loop {
|
||||
sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
|
||||
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env", "everest_test")
|
||||
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "RsCmdErrorsBinary",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "RsCmdErrorsTest",
|
||||
srcs = [],
|
||||
crate = ":RsCmdErrorsBinary",
|
||||
crate_features = [
|
||||
"mockall",
|
||||
"mockall_double",
|
||||
],
|
||||
edition = "2021",
|
||||
proc_macro_deps = ["@everest_framework_crate_index//:mockall_double"],
|
||||
deps = ["@everest_framework_crate_index//:mockall"],
|
||||
)
|
||||
|
||||
rs_everest_module(
|
||||
name = "RsCmdErrors",
|
||||
binary = ":RsCmdErrorsBinary",
|
||||
manifest = "manifest.yaml",
|
||||
)
|
||||
|
||||
everest_env(
|
||||
name = "integration_env",
|
||||
config_file = "config.yaml",
|
||||
modules = [
|
||||
":RsCmdErrors",
|
||||
],
|
||||
)
|
||||
|
||||
sh_test(
|
||||
name = "integration_test",
|
||||
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
|
||||
data = [":integration_env"],
|
||||
tags = ["exclusive"],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
settings:
|
||||
# This is the setting we want to test.
|
||||
forward_exceptions: true
|
||||
active_modules:
|
||||
example_0:
|
||||
module: RsCmdErrors
|
||||
connections:
|
||||
other:
|
||||
- module_id: example_1
|
||||
implementation_id: example
|
||||
example_1:
|
||||
module: RsCmdErrors
|
||||
connections:
|
||||
other:
|
||||
- module_id: example_0
|
||||
implementation_id: example
|
||||
@@ -0,0 +1,18 @@
|
||||
description: Tests if we can raise errors from commands
|
||||
provides:
|
||||
example:
|
||||
interface: example
|
||||
description: An example interface.
|
||||
requires:
|
||||
other:
|
||||
interface: example
|
||||
# We don't care about everything which is non-cmd.
|
||||
ignore:
|
||||
vars:
|
||||
- max_current
|
||||
errors: true
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
enable_external_mqtt: false
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Integration test for the "cmd-errors" handling.
|
||||
//!
|
||||
//! The Rust binding can receive/return errors from command calls. The
|
||||
//! exceptions need the `forward_exceptions` setting. Then errors from the
|
||||
//! server should propagate to the client.
|
||||
//!
|
||||
//! Below we test all possible errors supported by EVerest. In user code it
|
||||
//! only `HandlerException` typically makes sense.
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
use generated::{
|
||||
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
|
||||
OnReadySubscriber,
|
||||
};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::{thread, time};
|
||||
|
||||
pub struct OneClass {
|
||||
publisher: OnceLock<ModulePublisher>,
|
||||
}
|
||||
|
||||
impl ExampleServiceSubscriber for OneClass {
|
||||
fn uses_something(&self, _context: &Context, key: String) -> ::everestrs::Result<bool> {
|
||||
match key.as_str() {
|
||||
"MessageParsingError" => Err(::everestrs::Error::MessageParsingError(
|
||||
"this message?".to_string(),
|
||||
)),
|
||||
"SchemaValidationError" => Err(::everestrs::Error::SchemaValidationError(
|
||||
"not my schema".to_string(),
|
||||
)),
|
||||
"HandlerException" => Err(::everestrs::Error::HandlerException(
|
||||
"my handler".to_string(),
|
||||
)),
|
||||
"CmdTimeout" => Err(::everestrs::Error::CmdTimeout("no time".to_string())),
|
||||
"Shutdown" => Err(::everestrs::Error::Shutdown("dead".to_string())),
|
||||
"NotReady" => Err(::everestrs::Error::NotReady("too soon".to_string())),
|
||||
_ => Ok(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The compilation test is that we don't generate the method interfaces for
|
||||
// the ignored methods.
|
||||
impl ExampleClientSubscriber for OneClass {}
|
||||
|
||||
impl OnReadySubscriber for OneClass {
|
||||
fn on_ready(&self, publishers: &ModulePublisher) {
|
||||
let _ = self.publisher.set(publishers.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::main]
|
||||
fn main(module: &Module) {
|
||||
let one_class = Arc::new(OneClass {
|
||||
publisher: OnceLock::new(),
|
||||
});
|
||||
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
|
||||
log::info!("Module initialized");
|
||||
|
||||
let publisher = one_class.publisher.wait();
|
||||
for (key, _expected) in [
|
||||
(
|
||||
"HandlerException",
|
||||
Err(::everestrs::Error::HandlerException(String::new())),
|
||||
),
|
||||
("foo", Ok(true)),
|
||||
(
|
||||
"SchemaValidationError",
|
||||
Err(::everestrs::Error::SchemaValidationError(String::new())),
|
||||
),
|
||||
(
|
||||
"MessageParsingError",
|
||||
Err(::everestrs::Error::MessageParsingError(String::new())),
|
||||
),
|
||||
(
|
||||
"CmdTimeout",
|
||||
Err(::everestrs::Error::CmdTimeout(String::new())),
|
||||
),
|
||||
("Shutdown", Err(::everestrs::Error::Shutdown(String::new()))),
|
||||
("NotReady", Err(::everestrs::Error::NotReady(String::new()))),
|
||||
] {
|
||||
let res = publisher.other.uses_something(key.to_string());
|
||||
assert!(matches!(res, _expected));
|
||||
}
|
||||
loop {
|
||||
let dt = time::Duration::from_millis(250);
|
||||
thread::sleep(dt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
|
||||
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env")
|
||||
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "RsErrors",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "RsErrorsTest",
|
||||
srcs = [],
|
||||
crate = ":RsErrors",
|
||||
edition = "2021",
|
||||
deps = ["@everest_framework_crate_index//:serde_yaml"],
|
||||
)
|
||||
|
||||
rs_everest_module(
|
||||
name = "RsErrorsModule",
|
||||
binary = ":RsErrors",
|
||||
manifest = "manifest.yaml",
|
||||
)
|
||||
|
||||
everest_env(
|
||||
name = "integration_env",
|
||||
config_file = "config.yaml",
|
||||
modules = [
|
||||
":RsErrorsModule",
|
||||
],
|
||||
)
|
||||
|
||||
sh_test(
|
||||
name = "integration_test",
|
||||
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
|
||||
data = [":integration_env"],
|
||||
tags = ["exclusive"],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Test config for regression tests for the framework.
|
||||
active_modules:
|
||||
errors_0:
|
||||
module: RsErrorsModule
|
||||
connections:
|
||||
errors_multiple:
|
||||
- module_id: errors_1
|
||||
implementation_id: multiple
|
||||
mapping:
|
||||
module:
|
||||
evse: 1
|
||||
errors_1:
|
||||
module: RsErrorsModule
|
||||
connections:
|
||||
errors_multiple:
|
||||
- module_id: errors_0
|
||||
implementation_id: multiple
|
||||
mapping:
|
||||
module:
|
||||
evse: 2
|
||||
@@ -0,0 +1,13 @@
|
||||
description: Tests for the Errors
|
||||
requires:
|
||||
errors_multiple:
|
||||
interface: errors_multiple
|
||||
provides:
|
||||
multiple:
|
||||
interface: errors_multiple
|
||||
description: Multiple errors
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
enable_external_mqtt: false
|
||||
@@ -0,0 +1,119 @@
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
use crate::generated::Context;
|
||||
use crate::generated::ErrorsMultipleClientSubscriber;
|
||||
use crate::generated::Module;
|
||||
use crate::generated::{ModulePublisher, OnReadySubscriber};
|
||||
use everestrs::{ErrorSeverity, ErrorType};
|
||||
use generated::errors::errors_multiple::{Error as ExampleError, ExampleErrorsError};
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
const MESSAGE: &str = "a message";
|
||||
const DESCRIPTION: &str = "a description";
|
||||
const SEVERITY: ErrorSeverity = ErrorSeverity::Low;
|
||||
|
||||
struct ErrorCommunacator {
|
||||
errors_raised: Mutex<HashSet<ExampleErrorsError>>,
|
||||
errors_cleared: Mutex<HashSet<ExampleErrorsError>>,
|
||||
errors_cleared_cv: Condvar,
|
||||
}
|
||||
|
||||
impl Eq for ExampleErrorsError {}
|
||||
|
||||
impl std::hash::Hash for ExampleErrorsError {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::mem::discriminant(self).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl OnReadySubscriber for ErrorCommunacator {
|
||||
fn on_ready(&self, publishers: &ModulePublisher) {
|
||||
let error_a = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
|
||||
let error_b = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorB);
|
||||
let error_c = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorC);
|
||||
publishers.multiple.raise_error(error_a.clone().into());
|
||||
publishers.multiple.raise_error(error_b.into());
|
||||
// Raise an error also with description and severity.
|
||||
let error_c = ErrorType {
|
||||
error_type: error_c,
|
||||
description: DESCRIPTION.to_owned(),
|
||||
message: MESSAGE.to_owned(),
|
||||
severity: SEVERITY,
|
||||
};
|
||||
publishers.multiple.raise_error(error_c);
|
||||
|
||||
publishers.multiple.clear_error(error_a);
|
||||
publishers.multiple.clear_all_errors();
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorsMultipleClientSubscriber for ErrorCommunacator {
|
||||
fn on_error_raised(&self, _context: &Context, error: ErrorType<ExampleError>) {
|
||||
let mut raised_set = self.errors_raised.lock().unwrap();
|
||||
log::info!("Error raised {:?}", error.error_type);
|
||||
if let ExampleError::ExampleErrors(inner) = &error.error_type {
|
||||
raised_set.insert(inner.clone());
|
||||
}
|
||||
|
||||
// Check the handling for custom message, description and severity.
|
||||
if let ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorC) = error.error_type {
|
||||
assert_eq!(&error.description, DESCRIPTION);
|
||||
assert_eq!(&error.message, MESSAGE);
|
||||
assert_eq!(error.severity, SEVERITY);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_error_cleared(&self, _context: &Context, error: ErrorType<ExampleError>) {
|
||||
let mut cleared_set = self.errors_cleared.lock().unwrap();
|
||||
log::info!("Error cleared {:?}", error.error_type);
|
||||
if let ExampleError::ExampleErrors(inner) = error.error_type {
|
||||
cleared_set.insert(inner.clone());
|
||||
}
|
||||
|
||||
// The integration test links this module to another version of itself
|
||||
// so the magic 3 here must match the number of calls to raise_error
|
||||
// (and thus also clear_error through the clear_all_errors call)
|
||||
// in on_ready
|
||||
if cleared_set.len() == 3 {
|
||||
self.errors_cleared_cv.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::generated::ErrorsMultipleServiceSubscriber for ErrorCommunacator {}
|
||||
|
||||
#[everestrs::main]
|
||||
fn main(module: &Module) {
|
||||
let one_class = Arc::new(ErrorCommunacator {
|
||||
errors_raised: Mutex::new(HashSet::new()),
|
||||
errors_cleared: Mutex::new(HashSet::new()),
|
||||
errors_cleared_cv: Condvar::new(),
|
||||
});
|
||||
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
|
||||
|
||||
let mut tests_passed = false;
|
||||
// This mutex goes into the condvar, but it is dummy data as we know that the
|
||||
// notify_once will fire after this
|
||||
let mutex = Mutex::new(true);
|
||||
loop {
|
||||
let mutex_inner = mutex.lock().unwrap();
|
||||
let res = one_class
|
||||
.errors_cleared_cv
|
||||
.wait_timeout(mutex_inner, std::time::Duration::from_secs(2))
|
||||
.unwrap();
|
||||
|
||||
if res.1.timed_out() & !tests_passed {
|
||||
panic!("Timeout hit");
|
||||
} else {
|
||||
let raised_set = one_class.errors_raised.lock().unwrap();
|
||||
let cleared_set = one_class.errors_cleared.lock().unwrap();
|
||||
log::info!("Raised Errors: {:?}", raised_set);
|
||||
log::info!("Cleared Errors: {:?}", cleared_set);
|
||||
assert_eq!(*raised_set, *cleared_set);
|
||||
tests_passed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "RsErrorsCompilation",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "RsErrorsCompilationTest",
|
||||
srcs = [],
|
||||
crate = ":RsErrorsCompilation",
|
||||
edition = "2021",
|
||||
deps = ["@everest_framework_crate_index//:serde_yaml"],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
description: Tests for the Errors
|
||||
requires:
|
||||
errors_multiple:
|
||||
interface: errors_multiple
|
||||
provides:
|
||||
duplicate:
|
||||
interface: errors_duplicate
|
||||
description: Duplicated errors
|
||||
multiple:
|
||||
interface: errors_multiple
|
||||
description: Multiple errors
|
||||
selected:
|
||||
interface: errors_selected
|
||||
description: Selected errors
|
||||
none:
|
||||
interface: errors_none
|
||||
description: No errors defined
|
||||
empty:
|
||||
interface: empty
|
||||
description: An empty interface
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
enable_external_mqtt: false
|
||||
@@ -0,0 +1,86 @@
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
fn main() {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// In this test we check that we can deserialize all four errors.
|
||||
#[test]
|
||||
fn test_duplicate() {
|
||||
use crate::generated::errors::errors_duplicate::Error;
|
||||
use crate::generated::errors::errors_duplicate::ExampleErrorsError;
|
||||
for (error_str, error_enum) in [
|
||||
(
|
||||
"example_errors/ExampleErrorA",
|
||||
ExampleErrorsError::ExampleErrorA,
|
||||
),
|
||||
(
|
||||
"example_errors/ExampleErrorB",
|
||||
ExampleErrorsError::ExampleErrorB,
|
||||
),
|
||||
(
|
||||
"example_errors/ExampleErrorC",
|
||||
ExampleErrorsError::ExampleErrorC,
|
||||
),
|
||||
(
|
||||
"example_errors/ExampleErrorD",
|
||||
ExampleErrorsError::ExampleErrorD,
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
serde_yaml::from_str::<Error>(error_str).unwrap(),
|
||||
Error::ExampleErrors(error_enum)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// In this test we check that we can only deserialize the selected two
|
||||
/// errors.
|
||||
#[test]
|
||||
fn test_selected() {
|
||||
use crate::generated::errors::errors_selected::Error;
|
||||
use crate::generated::errors::errors_selected::ExampleErrorsError;
|
||||
for (error_str, error_enum) in [
|
||||
(
|
||||
"example_errors/ExampleErrorA",
|
||||
ExampleErrorsError::ExampleErrorA,
|
||||
),
|
||||
(
|
||||
"example_errors/ExampleErrorB",
|
||||
ExampleErrorsError::ExampleErrorB,
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
serde_yaml::from_str::<Error>(error_str).unwrap(),
|
||||
Error::ExampleErrors(error_enum)
|
||||
);
|
||||
}
|
||||
|
||||
for error_str in [
|
||||
"example_errors/ExampleErrorC",
|
||||
"example_errors/ExampleErrorD",
|
||||
] {
|
||||
assert!(serde_yaml::from_str::<Error>(error_str).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// This test should just compile. The deserialization is tested above.
|
||||
#[test]
|
||||
fn test_multiple() {
|
||||
{
|
||||
use crate::generated::errors::errors_multiple::ExampleErrorsError;
|
||||
let _ = ExampleErrorsError::ExampleErrorA;
|
||||
let _ = ExampleErrorsError::ExampleErrorB;
|
||||
let _ = ExampleErrorsError::ExampleErrorC;
|
||||
let _ = ExampleErrorsError::ExampleErrorD;
|
||||
}
|
||||
|
||||
{
|
||||
use crate::generated::errors::errors_multiple::MoreErrorsError;
|
||||
let _ = MoreErrorsError::ExampleErrorA;
|
||||
let _ = MoreErrorsError::MoreError;
|
||||
let _ = MoreErrorsError::SnakeCaseError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
load("@rules_python//python:defs.bzl", "py_test")
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
|
||||
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||
load("//applications/utils:requirements.bzl", "requirement")
|
||||
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env", "everest_test")
|
||||
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "RsExampleBinary",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "RsExampleTest",
|
||||
srcs = [],
|
||||
crate = ":RsExampleBinary",
|
||||
crate_features = [
|
||||
"mockall",
|
||||
"mockall_double",
|
||||
],
|
||||
edition = "2021",
|
||||
proc_macro_deps = ["@everest_framework_crate_index//:mockall_double"],
|
||||
deps = ["@everest_framework_crate_index//:mockall"],
|
||||
)
|
||||
|
||||
rs_everest_module(
|
||||
name = "RsExample",
|
||||
binary = ":RsExampleBinary",
|
||||
manifest = "manifest.yaml",
|
||||
)
|
||||
|
||||
everest_env(
|
||||
name = "config_env",
|
||||
config_file = "config.yaml",
|
||||
modules = [
|
||||
":RsExample",
|
||||
],
|
||||
)
|
||||
|
||||
sh_test(
|
||||
name = "integration_test",
|
||||
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
|
||||
data = [":config_env"],
|
||||
tags = ["exclusive"],
|
||||
)
|
||||
|
||||
everest_env(
|
||||
name = "config_probe_env",
|
||||
config_file = "config_probe.yaml",
|
||||
modules = [
|
||||
":RsExample",
|
||||
],
|
||||
)
|
||||
|
||||
# Integration tests for RsExample using Python's everest.testing framework.
|
||||
# Uses ProbeModule to verify RsExample publishes variables and handles commands.
|
||||
py_test(
|
||||
name = "py_mocked_test",
|
||||
srcs = glob(["py_tests/**/*.py"]),
|
||||
data = ["config_probe_env"],
|
||||
legacy_create_init = False,
|
||||
main = "py_tests/mocked_test.py",
|
||||
tags = ["exclusive"],
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
deps = [
|
||||
"//applications/utils/everest-testing",
|
||||
"//lib/everest/framework/everestpy/src:framework",
|
||||
requirement("pytest"),
|
||||
requirement("pytest-asyncio"),
|
||||
],
|
||||
)
|
||||
|
||||
# Integration tests for RsExample. Will launch EVerest with a ProbeModule and
|
||||
# verify that we can launch our module together with the ProbeModule.
|
||||
rust_test(
|
||||
name = "RsExampleHarnessTestBinary",
|
||||
srcs = ["rs_tests/harness_test.rs"],
|
||||
compile_data = [
|
||||
"config_probe.yaml",
|
||||
"config_multiple_connections.yaml",
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
data = ["config_probe_env"],
|
||||
edition = "2021",
|
||||
proc_macro_deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-derive",
|
||||
],
|
||||
rustc_env = {
|
||||
"CARGO_MANIFEST_DIR": "lib/everest/framework/everestrs/tests/modules/RsExample",
|
||||
"EVEREST_CORE_ROOT": "lib/everest/framework/everestrs/tests",
|
||||
},
|
||||
tags = ["exclusive"],
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
],
|
||||
)
|
||||
|
||||
# Integration tests for RsExample. Will launch EVerest with a ProbeModule and
|
||||
# validate calls to our trait mocks.
|
||||
rust_test(
|
||||
name = "RsExampleMockedTestBinary",
|
||||
srcs = ["rs_tests/mocked_test.rs"],
|
||||
compile_data = [
|
||||
"config_probe.yaml",
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
crate_features = [
|
||||
"mockall",
|
||||
"trait",
|
||||
],
|
||||
data = ["config_probe_env"],
|
||||
edition = "2021",
|
||||
proc_macro_deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-derive",
|
||||
"@everest_framework_crate_index//:mockall_double",
|
||||
],
|
||||
rustc_env = {
|
||||
"CARGO_MANIFEST_DIR": "lib/everest/framework/everestrs/tests/modules/RsExample",
|
||||
"EVEREST_CORE_ROOT": "lib/everest/framework/everestrs/tests",
|
||||
},
|
||||
tags = ["exclusive"],
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"@everest_framework_crate_index//:mockall",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Test config for regression tests for the framework.
|
||||
active_modules:
|
||||
example_0:
|
||||
module: RsExample
|
||||
connections:
|
||||
a_friend:
|
||||
- module_id: example_1
|
||||
implementation_id: foobar
|
||||
example_1:
|
||||
module: RsExample
|
||||
connections:
|
||||
a_friend:
|
||||
- module_id: example_0
|
||||
implementation_id: foobar
|
||||
@@ -0,0 +1,21 @@
|
||||
active_modules:
|
||||
example_0:
|
||||
module: RsExample
|
||||
connections:
|
||||
a_friend:
|
||||
- module_id: probe
|
||||
implementation_id: foobar
|
||||
example_1:
|
||||
module: RsExample
|
||||
connections:
|
||||
a_friend:
|
||||
- module_id: probe
|
||||
implementation_id: foobar
|
||||
probe:
|
||||
module: ProbeModule
|
||||
connections:
|
||||
a_friend:
|
||||
- module_id: example_0
|
||||
implementation_id: foobar
|
||||
- module_id: example_1
|
||||
implementation_id: foobar
|
||||
@@ -0,0 +1,14 @@
|
||||
# Test config for regression tests for the framework.
|
||||
active_modules:
|
||||
example_0:
|
||||
module: RsExample
|
||||
connections:
|
||||
a_friend:
|
||||
- module_id: example_1
|
||||
implementation_id: foobar
|
||||
example_1:
|
||||
module: ProbeModule
|
||||
connections:
|
||||
a_friend:
|
||||
- module_id: example_0
|
||||
implementation_id: foobar
|
||||
@@ -0,0 +1,30 @@
|
||||
description: Simple Example
|
||||
config:
|
||||
some_string_config:
|
||||
description: A module level string config.
|
||||
type: string
|
||||
default: Hello world
|
||||
some_number_config:
|
||||
description: A module level number config.
|
||||
type: number
|
||||
default: 42
|
||||
provides:
|
||||
foobar:
|
||||
interface: example
|
||||
description: An example interface.
|
||||
config:
|
||||
some_bool_config:
|
||||
description: An interface level bool config
|
||||
type: boolean
|
||||
default: true
|
||||
some_integer_config:
|
||||
description: An interface level integer config.
|
||||
type: integer
|
||||
default: 1234
|
||||
requires:
|
||||
a_friend:
|
||||
interface: example
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
@@ -0,0 +1,11 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--everest-prefix",
|
||||
action="store",
|
||||
default=".",
|
||||
help="everest prefix path; default = '.' (for bazel runfiles)",
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
"""
|
||||
Smoke test for RsExample using the Python testing framework.
|
||||
|
||||
Launches EVerest with RsExample (example_0) and a ProbeModule (example_1),
|
||||
then verifies that RsExample publishes max_current(123.0) on ready and
|
||||
responds to the uses_something command.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from everest.testing.core_utils.common import Requirement
|
||||
from everest.testing.core_utils.fixtures import *
|
||||
from everest.testing.core_utils.everest_core import EverestCore
|
||||
from everest.testing.core_utils.probe_module import ProbeModule
|
||||
|
||||
|
||||
async def wait_for_mock(mock, timeout=10):
|
||||
"""Poll until mock has been called or timeout."""
|
||||
for _ in range(int(timeout * 10)):
|
||||
if mock.called:
|
||||
return
|
||||
await asyncio.sleep(0.1)
|
||||
raise TimeoutError(f"Mock not called within {timeout}s")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.probe_module(
|
||||
connections={"a_friend": [Requirement("example_0", "foobar")]},
|
||||
module_id="example_1",
|
||||
)
|
||||
@pytest.mark.everest_core_config("config_probe.yaml")
|
||||
async def test_rs_example_publishes_max_current(everest_core: EverestCore):
|
||||
"""Verify that RsExample publishes max_current(123.0) on ready."""
|
||||
everest_core.start()
|
||||
|
||||
probe = ProbeModule(everest_core.get_runtime_session(), module_id="example_1")
|
||||
|
||||
max_current_mock = Mock()
|
||||
probe.subscribe_variable("a_friend", "max_current", max_current_mock)
|
||||
|
||||
probe.implement_command("foobar", "uses_something", lambda args: True)
|
||||
|
||||
probe.start()
|
||||
await probe.wait_to_be_ready(timeout=10)
|
||||
|
||||
await wait_for_mock(max_current_mock, timeout=10)
|
||||
value = max_current_mock.call_args[0][0]
|
||||
assert value == 123.0, f"Expected max_current=123.0, got {value}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.probe_module(
|
||||
connections={"a_friend": [Requirement("example_0", "foobar")]},
|
||||
module_id="example_1",
|
||||
)
|
||||
@pytest.mark.everest_core_config("config_probe.yaml")
|
||||
async def test_rs_example_uses_something(everest_core: EverestCore):
|
||||
"""Verify that we can call uses_something on RsExample."""
|
||||
everest_core.start()
|
||||
|
||||
probe = ProbeModule(everest_core.get_runtime_session(), module_id="example_1")
|
||||
|
||||
probe.implement_command("foobar", "uses_something", lambda args: True)
|
||||
|
||||
probe.start()
|
||||
await probe.wait_to_be_ready(timeout=10)
|
||||
|
||||
result = await probe.call_command("a_friend", "uses_something", {"key": "hello"})
|
||||
# The C++ binding may return None for boolean results; verify at least no exception.
|
||||
assert result is None or result == True, f"Unexpected result: {result!r}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"] + sys.argv[1:]))
|
||||
@@ -0,0 +1,148 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1", harness = true)]
|
||||
fn test_harness_generates_counterpart(module: &Module) {
|
||||
use generated::*;
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::sync::Arc;
|
||||
let (tx, rx) = channel();
|
||||
|
||||
struct Dummy(Sender<()>);
|
||||
|
||||
impl OnReadySubscriber for Dummy {
|
||||
fn on_ready(&self, _pub_impl: &ModulePublisher) {}
|
||||
}
|
||||
|
||||
impl ExampleServiceSubscriber for Dummy {
|
||||
fn uses_something(&self, _context: &Context, _key: String) -> everestrs::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleClientSubscriber for Dummy {
|
||||
fn on_max_current(&self, _context: &Context, value: f64) {
|
||||
assert_eq!(value, 123.);
|
||||
self.0.send(()).unwrap();
|
||||
}
|
||||
fn on_error_raised(
|
||||
&self,
|
||||
_context: &Context,
|
||||
_error: everestrs::ErrorType<errors::example::Error>,
|
||||
) {
|
||||
}
|
||||
fn on_error_cleared(
|
||||
&self,
|
||||
_context: &Context,
|
||||
_error: everestrs::ErrorType<errors::example::Error>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
let dummy = Arc::new(Dummy(tx));
|
||||
let _pub = module.start(dummy.clone(), dummy.clone(), dummy.clone());
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
|
||||
#[everestrs::harness(config = "config_probe.yaml", module = "example_1")]
|
||||
mod some_module {
|
||||
use generated::*;
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Dummy(Sender<()>, f64);
|
||||
|
||||
impl OnReadySubscriber for Dummy {
|
||||
fn on_ready(&self, _pub_impl: &ModulePublisher) {}
|
||||
}
|
||||
|
||||
impl ExampleServiceSubscriber for Dummy {
|
||||
fn uses_something(&self, _context: &Context, _key: String) -> everestrs::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleClientSubscriber for Dummy {
|
||||
fn on_max_current(&self, _context: &Context, value: f64) {
|
||||
assert_eq!(value, self.1);
|
||||
self.0.send(()).unwrap();
|
||||
}
|
||||
fn on_error_raised(
|
||||
&self,
|
||||
_context: &Context,
|
||||
_error: everestrs::ErrorType<errors::example::Error>,
|
||||
) {
|
||||
}
|
||||
fn on_error_cleared(
|
||||
&self,
|
||||
_context: &Context,
|
||||
_error: everestrs::ErrorType<errors::example::Error>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
|
||||
fn test_harness_in_module(module: &Module) {
|
||||
let (tx, rx) = channel();
|
||||
let dummy = Arc::new(Dummy(tx, 123.));
|
||||
let _pub = module.start(dummy.clone(), dummy.clone(), dummy.clone());
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
|
||||
#[should_panic]
|
||||
fn test_harness_with_panic(module: &Module) {
|
||||
let (tx, rx) = channel();
|
||||
let dummy = Arc::new(Dummy(tx, 124.));
|
||||
let _pub = module.start(dummy.clone(), dummy.clone(), dummy.clone());
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::harness(config = "config_multiple_connections.yaml", module = "probe")]
|
||||
mod multiple_connections_compilation {
|
||||
use generated::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Dummy;
|
||||
|
||||
impl OnReadySubscriber for Dummy {
|
||||
fn on_ready(&self, _pub_impl: &ModulePublisher) {}
|
||||
}
|
||||
|
||||
impl ExampleServiceSubscriber for Dummy {
|
||||
fn uses_something(&self, _context: &Context, _key: String) -> everestrs::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleClientSubscriber for Dummy {
|
||||
fn on_max_current(&self, _context: &Context, _value: f64) {}
|
||||
fn on_error_raised(
|
||||
&self,
|
||||
_context: &Context,
|
||||
_error: everestrs::ErrorType<errors::example::Error>,
|
||||
) {
|
||||
}
|
||||
fn on_error_cleared(
|
||||
&self,
|
||||
_context: &Context,
|
||||
_error: everestrs::ErrorType<errors::example::Error>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
|
||||
fn test_harness_in_module(module: &Module) {
|
||||
let dummy = Arc::new(Dummy);
|
||||
let _pub = module.start(dummy.clone(), dummy.clone(), |_a: usize| dummy.clone());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1", harness = true)]
|
||||
fn test_mocked_generates_counterpart(module: &Module) {
|
||||
use generated::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut mock_client = MockExampleClientSubscriber::new();
|
||||
mock_client
|
||||
.expect_on_max_current()
|
||||
.withf(|_, value| *value == 123.0)
|
||||
.times(1)
|
||||
.return_once(move |_, _| {
|
||||
tx.send(()).unwrap();
|
||||
});
|
||||
let mock_client = Arc::new(mock_client);
|
||||
|
||||
let mut mock_on_ready = MockOnReadySubscriber::new();
|
||||
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
|
||||
|
||||
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
|
||||
#[everestrs::harness(config = "config_probe.yaml", module = "example_1")]
|
||||
mod some_module {
|
||||
use generated::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
|
||||
fn test_mocked_in_module(module: &Module) {
|
||||
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut mock_client = MockExampleClientSubscriber::new();
|
||||
mock_client
|
||||
.expect_on_max_current()
|
||||
.withf(|_, value| *value == 123.0)
|
||||
.times(1)
|
||||
.return_once(move |_, _| {
|
||||
tx.send(()).unwrap();
|
||||
});
|
||||
let mock_client = Arc::new(mock_client);
|
||||
|
||||
let mut mock_on_ready = MockOnReadySubscriber::new();
|
||||
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
|
||||
|
||||
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
|
||||
#[should_panic]
|
||||
fn test_mocked_with_panic_handler(module: &Module) {
|
||||
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut mock_client = MockExampleClientSubscriber::new();
|
||||
mock_client
|
||||
.expect_on_max_current()
|
||||
.withf(|_, value| *value != 123.0)
|
||||
.times(1)
|
||||
.return_once(move |_, _| {
|
||||
tx.send(()).unwrap();
|
||||
});
|
||||
let mock_client = Arc::new(mock_client);
|
||||
|
||||
let mut mock_on_ready = MockOnReadySubscriber::new();
|
||||
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
|
||||
|
||||
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
|
||||
|
||||
// Wait for RsExample's on_ready to publish max_current(123.0).
|
||||
rx.recv_timeout(std::time::Duration::from_secs(5))
|
||||
.expect("Timed out waiting for on_max_current");
|
||||
}
|
||||
|
||||
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
|
||||
#[should_panic]
|
||||
fn test_mocked_with_panic_mocks(module: &Module) {
|
||||
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
|
||||
|
||||
let mut mock_client = MockExampleClientSubscriber::new();
|
||||
mock_client
|
||||
.expect_on_max_current()
|
||||
.times(2..) // No one will call us twice.
|
||||
.return_const(());
|
||||
let mock_client = Arc::new(mock_client);
|
||||
|
||||
let mut mock_on_ready = MockOnReadySubscriber::new();
|
||||
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
|
||||
|
||||
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
use everestrs::ErrorType;
|
||||
use generated::errors::example::Error as ExampleError;
|
||||
use generated::{
|
||||
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module,
|
||||
ModulePublisher, OnReadySubscriber,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{thread, time};
|
||||
|
||||
pub struct OneClass {}
|
||||
|
||||
impl ExampleServiceSubscriber for OneClass {
|
||||
fn uses_something(&self, context: &Context, key: String) -> ::everestrs::Result<bool> {
|
||||
use crate::generated::errors::example::ExampleErrorsError;
|
||||
let error = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
|
||||
if key.is_empty() {
|
||||
// Explicit cast
|
||||
let error: ErrorType<_> = error.into();
|
||||
context.publisher.foobar.raise_error(error);
|
||||
} else if &key == "clear_all" {
|
||||
context.publisher.foobar.clear_all_errors();
|
||||
} else {
|
||||
context.publisher.foobar.clear_error(error);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleClientSubscriber for OneClass {
|
||||
fn on_max_current(&self, _context: &Context, value: f64) {
|
||||
log::info!("Received {value}");
|
||||
}
|
||||
|
||||
fn on_error_raised(&self, _context: &Context, error: ErrorType<ExampleError>) {
|
||||
log::warn!("Recieved an error {:?}", error.error_type);
|
||||
}
|
||||
|
||||
fn on_error_cleared(&self, _context: &Context, error: ErrorType<ExampleError>) {
|
||||
log::info!("Cleared an error {:?} - what a relief", error.error_type);
|
||||
}
|
||||
}
|
||||
|
||||
impl OnReadySubscriber for OneClass {
|
||||
fn on_ready(&self, publishers: &ModulePublisher) {
|
||||
log::info!("Ready");
|
||||
match publishers.foobar.max_current(123.0) {
|
||||
Ok(_) => log::info!("Adjusted the max current"),
|
||||
Err(err) => log::error!("Failed to set the max current: {err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::main]
|
||||
fn main(module: &Module) {
|
||||
let config = module.get_config();
|
||||
log::info!("Received the config {config:?}");
|
||||
let one_class = Arc::new(OneClass {});
|
||||
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
|
||||
log::info!("Module initialized");
|
||||
|
||||
loop {
|
||||
let dt = time::Duration::from_millis(250);
|
||||
thread::sleep(dt);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_on_ready() {
|
||||
let mut everest_mock = ModulePublisher::default();
|
||||
everest_mock
|
||||
.foobar
|
||||
.expect_max_current()
|
||||
.times(1)
|
||||
.return_once(|_| Ok(()));
|
||||
|
||||
let module = OneClass {};
|
||||
module.on_ready(&everest_mock);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uses_something() {
|
||||
use mockall::Sequence;
|
||||
|
||||
let mut seq = Sequence::new();
|
||||
let mut everest_mock = ModulePublisher::default();
|
||||
|
||||
everest_mock
|
||||
.foobar
|
||||
.expect_raise_error()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|_| ());
|
||||
|
||||
everest_mock
|
||||
.foobar
|
||||
.expect_clear_error()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|_| ());
|
||||
|
||||
everest_mock
|
||||
.foobar
|
||||
.expect_clear_all_errors()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|| ());
|
||||
|
||||
let context = Context {
|
||||
name: "foo",
|
||||
publisher: &everest_mock,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
let module = OneClass {};
|
||||
for message in [String::new(), "clear".to_owned(), "clear_all".to_owned()] {
|
||||
let _ = module.uses_something(&context, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary")
|
||||
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env")
|
||||
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "RsIgnoreBinary",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
],
|
||||
)
|
||||
|
||||
rs_everest_module(
|
||||
name = "RsIgnore",
|
||||
binary = ":RsIgnoreBinary",
|
||||
manifest = "manifest.yaml",
|
||||
)
|
||||
|
||||
everest_env(
|
||||
name = "integration_env",
|
||||
config_file = "config.yaml",
|
||||
modules = [
|
||||
":RsIgnore",
|
||||
],
|
||||
)
|
||||
|
||||
sh_test(
|
||||
name = "integration_test",
|
||||
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
|
||||
data = [":integration_env"],
|
||||
tags = ["exclusive"],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Test config for regression tests for the framework.
|
||||
active_modules:
|
||||
example_0:
|
||||
module: RsIgnore
|
||||
connections:
|
||||
other:
|
||||
- module_id: example_1
|
||||
implementation_id: example
|
||||
example_1:
|
||||
module: RsIgnore
|
||||
connections:
|
||||
other:
|
||||
- module_id: example_0
|
||||
implementation_id: example
|
||||
@@ -0,0 +1,17 @@
|
||||
description: Simple Example
|
||||
provides:
|
||||
example:
|
||||
interface: example
|
||||
description: An example interface.
|
||||
requires:
|
||||
other:
|
||||
interface: example
|
||||
ignore:
|
||||
vars:
|
||||
- max_current
|
||||
errors: true
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
enable_external_mqtt: false
|
||||
@@ -0,0 +1,50 @@
|
||||
//! Integration test for the "ignore" handling.
|
||||
//!
|
||||
//! The Rust binding allow you to ignore variables and errors. The variables
|
||||
//! are ignored by adding them to the "ignore.vars" list. Errors can only be ignored
|
||||
//! at bulk, by setting "ignore.errors" to true. Ignored elements are removed
|
||||
//! from the trait and thus don't have to be implemented.
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
use generated::errors::example::{Error as ExampleError, ExampleErrorsError};
|
||||
use generated::{
|
||||
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
|
||||
OnReadySubscriber,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{thread, time};
|
||||
|
||||
pub struct OneClass {}
|
||||
|
||||
impl ExampleServiceSubscriber for OneClass {
|
||||
fn uses_something(&self, _context: &Context, _key: String) -> ::everestrs::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
// The compilation test is that we don't generate the method interfaces for
|
||||
// the ignored methods.
|
||||
impl ExampleClientSubscriber for OneClass {}
|
||||
|
||||
impl OnReadySubscriber for OneClass {
|
||||
fn on_ready(&self, publishers: &ModulePublisher) {
|
||||
// Call the other module. This calls should be ignored.
|
||||
publishers.example.max_current(12.3).unwrap();
|
||||
let error = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
|
||||
publishers.example.raise_error(error.clone().into());
|
||||
publishers.example.clear_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::main]
|
||||
fn main(module: &Module) {
|
||||
let one_class = Arc::new(OneClass {});
|
||||
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
|
||||
log::info!("Module initialized");
|
||||
|
||||
loop {
|
||||
let dt = time::Duration::from_millis(250);
|
||||
thread::sleep(dt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary")
|
||||
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env")
|
||||
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "RsOnReadyRaceConditionBinary",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
],
|
||||
)
|
||||
|
||||
rs_everest_module(
|
||||
name = "RsOnReadyRaceCondition",
|
||||
binary = ":RsOnReadyRaceConditionBinary",
|
||||
manifest = "manifest.yaml",
|
||||
)
|
||||
|
||||
everest_env(
|
||||
name = "integration_env",
|
||||
config_file = "config.yaml",
|
||||
modules = [
|
||||
":RsOnReadyRaceCondition",
|
||||
],
|
||||
)
|
||||
|
||||
sh_test(
|
||||
name = "integration_test",
|
||||
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
|
||||
data = [":integration_env"],
|
||||
tags = ["exclusive"],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Test config for regression tests for the framework.
|
||||
active_modules:
|
||||
example_0:
|
||||
module: RsOnReadyRaceCondition
|
||||
connections:
|
||||
other:
|
||||
- module_id: example_1
|
||||
implementation_id: example
|
||||
example_1:
|
||||
module: RsOnReadyRaceCondition
|
||||
connections:
|
||||
other:
|
||||
- module_id: example_0
|
||||
implementation_id: example
|
||||
@@ -0,0 +1,13 @@
|
||||
description: Simple Example
|
||||
provides:
|
||||
example:
|
||||
interface: example
|
||||
description: An example interface.
|
||||
requires:
|
||||
other:
|
||||
interface: example
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
enable_external_mqtt: false
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Integration test for the "ready_received" handling.
|
||||
//!
|
||||
//! We assume that every module shall recieve first `on_ready` before forwarding
|
||||
//! any other call to the user code. The code below recreates the race condition
|
||||
//! by making calls to the `other` module from within `on_ready` (and adding a
|
||||
//! delay in `on_ready`).
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
use everestrs::ErrorType;
|
||||
use generated::errors::example::{Error as ExampleError, ExampleErrorsError};
|
||||
use generated::{
|
||||
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
|
||||
OnReadySubscriber,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::{thread, time};
|
||||
|
||||
pub struct OneClass {
|
||||
/// Flag that the on-ready has been called.
|
||||
on_ready_called: AtomicBool,
|
||||
}
|
||||
|
||||
impl ExampleServiceSubscriber for OneClass {
|
||||
fn uses_something(&self, _context: &Context, _key: String) -> ::everestrs::Result<bool> {
|
||||
assert!(self.on_ready_called.load(Ordering::Relaxed));
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleClientSubscriber for OneClass {
|
||||
fn on_max_current(&self, _context: &Context, _value: f64) {
|
||||
assert!(self.on_ready_called.load(Ordering::Relaxed));
|
||||
log::info!("max current");
|
||||
}
|
||||
|
||||
fn on_error_raised(&self, _context: &Context, _error: ErrorType<ExampleError>) {
|
||||
assert!(self.on_ready_called.load(Ordering::Relaxed));
|
||||
log::info!("Error raised");
|
||||
}
|
||||
|
||||
fn on_error_cleared(&self, _context: &Context, _error: ErrorType<ExampleError>) {
|
||||
assert!(self.on_ready_called.load(Ordering::Relaxed));
|
||||
log::info!("Error cleared");
|
||||
}
|
||||
}
|
||||
|
||||
impl OnReadySubscriber for OneClass {
|
||||
fn on_ready(&self, publishers: &ModulePublisher) {
|
||||
log::info!("Enter Ready");
|
||||
// Call the other module.
|
||||
publishers.example.max_current(12.3).unwrap();
|
||||
let error = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
|
||||
publishers.example.raise_error(error.clone().into());
|
||||
publishers.example.clear_error(error);
|
||||
|
||||
// TODO(ddo) Add here the `uses_something` call once the framework can
|
||||
// reject too early calls.
|
||||
|
||||
// Sleep here to trigger the race condition.
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
// Update the flag.
|
||||
self.on_ready_called.store(true, Ordering::Relaxed);
|
||||
|
||||
log::info!("Exit Ready!");
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::main]
|
||||
fn main(module: &Module) {
|
||||
let one_class = Arc::new(OneClass {
|
||||
on_ready_called: AtomicBool::new(false),
|
||||
});
|
||||
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
|
||||
log::info!("Module initialized");
|
||||
|
||||
loop {
|
||||
let dt = time::Duration::from_millis(250);
|
||||
thread::sleep(dt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
|
||||
|
||||
cargo_build_script(
|
||||
name = "build_script",
|
||||
srcs = ["build.rs"],
|
||||
build_script_env = {
|
||||
"EVEREST_CORE_ROOT": "../..",
|
||||
},
|
||||
data = [
|
||||
"manifest.yaml",
|
||||
"//lib/everest/framework/everestrs/tests/errors",
|
||||
"//lib/everest/framework/everestrs/tests/interfaces",
|
||||
"//lib/everest/framework/everestrs/tests/types",
|
||||
],
|
||||
edition = "2021",
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
],
|
||||
)
|
||||
|
||||
# For now just a compilation test
|
||||
rust_binary(
|
||||
name = "RsOptionalConnection",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":build_script",
|
||||
"//lib/everest/framework/everestrs/everestrs",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
|
||||
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
|
||||
"@everest_framework_crate_index//:log",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
use everestrs_build::Builder;
|
||||
|
||||
pub fn main() {
|
||||
Builder::new(
|
||||
"manifest.yaml",
|
||||
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
|
||||
)
|
||||
.generate()
|
||||
.unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=manifest.yaml");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
description: The tests how to use optinal connections in Rust
|
||||
provides:
|
||||
foobar:
|
||||
interface: example
|
||||
description: An example interface.
|
||||
requires:
|
||||
optional_connection:
|
||||
interface: example
|
||||
min_connections: 0
|
||||
max_connections: 1
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Everest authors
|
||||
enable_external_mqtt: false
|
||||
@@ -0,0 +1,58 @@
|
||||
#![allow(non_snake_case)]
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
use everestrs::ErrorType;
|
||||
use generated::errors::example::Error as ExampleError;
|
||||
use generated::{
|
||||
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
|
||||
OnReadySubscriber,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{thread, time};
|
||||
|
||||
pub struct OptionalConnection {}
|
||||
|
||||
impl ExampleServiceSubscriber for OptionalConnection {
|
||||
fn uses_something(&self, _context: &Context, key: String) -> ::everestrs::Result<bool> {
|
||||
log::info!("Received {key}");
|
||||
Ok(&key == "hello")
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleClientSubscriber for OptionalConnection {
|
||||
fn on_max_current(&self, _context: &Context, value: f64) {
|
||||
log::info!("Received {value}");
|
||||
}
|
||||
|
||||
fn on_error_raised(&self, _context: &Context, error: ErrorType<ExampleError>) {
|
||||
log::info!("Recieved an error {:?}", error.error_type);
|
||||
}
|
||||
|
||||
fn on_error_cleared(&self, _context: &Context, error: ErrorType<ExampleError>) {
|
||||
log::info!("Cleared an error {:?} - what a relief", error.error_type);
|
||||
}
|
||||
}
|
||||
|
||||
impl OnReadySubscriber for OptionalConnection {
|
||||
fn on_ready(&self, publishers: &ModulePublisher) {
|
||||
log::info!("Ready");
|
||||
if let Some(publisher) = publishers.optional_connection_slots.get(0) {
|
||||
let res = publisher.uses_something("hello".to_string()).unwrap();
|
||||
assert!(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[everestrs::main]
|
||||
fn main(module: &Module) {
|
||||
let one_class = Arc::new(OptionalConnection {});
|
||||
let _publishers = module.start(one_class.clone(), one_class.clone(), |_index| {
|
||||
one_class.clone()
|
||||
});
|
||||
log::info!("Module initialized");
|
||||
|
||||
loop {
|
||||
let dt = time::Duration::from_millis(250);
|
||||
thread::sleep(dt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
|
||||
echo "Starting the manager"
|
||||
|
||||
bin/manager --prefix . --config etc/everest/config.yaml &
|
||||
PID_MANAGER=$!
|
||||
|
||||
sleep 5
|
||||
echo "Exit"
|
||||
|
||||
if ps -p $PID_MANAGER > /dev/null
|
||||
then
|
||||
kill $PID_MANAGER
|
||||
else
|
||||
echo "manager died"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,5 @@
|
||||
filegroup(
|
||||
name = "types",
|
||||
srcs = glob(["*.yaml"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
description: Example type
|
||||
types:
|
||||
Something:
|
||||
description: Some simple type.
|
||||
type: string
|
||||
Reference in New Issue
Block a user