Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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