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,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>,
|
||||
}
|
||||
Reference in New Issue
Block a user