use std::collections::HashSet;
use syn::{Expr, spanned::Spanned};
use quote::{quote, quote_spanned, format_ident};
use darling::FromMeta;
#[derive(FromMeta, Debug)]
struct SecopParam {
name: String,
doc: String,
datatype: String,
readonly: bool,
#[darling(default)]
swonly: bool,
#[darling(default)]
mandatory: bool,
#[darling(default)]
default: Option<String>,
#[darling(default)]
polling: Option<i64>,
#[darling(default)]
unit: String,
#[darling(default)]
group: String,
#[darling(default = "default_visibility")]
visibility: String,
}
const VISIBILITIES: &[&str] = &["none", "user", "advanced", "expert"];
fn default_visibility() -> String { "user".into() }
#[derive(FromMeta, Debug)]
struct SecopCommand {
name: String,
doc: String,
argtype: String,
restype: String,
#[darling(default)]
group: String,
#[darling(default = "default_visibility")]
visibility: String,
}
fn parse_attr<T: FromMeta>(attr: &syn::Attribute) -> Result<T, proc_macro2::TokenStream> {
attr.parse_meta()
.map_err(|err| format!("invalid param attribute: {}", err))
.and_then(|meta| T::from_meta(&meta).map_err(|_| "could not parse this attribute".into()))
.map_err(|e| quote_spanned! { attr.span() => compile_error!(#e); })
}
pub fn derive_module(input: synstructure::Structure) -> proc_macro2::TokenStream {
let mut params = Vec::new();
let mut commands = Vec::new();
let name = &input.ast().ident;
let vis = &input.ast().vis;
let param_cache_name = format_ident!("{}ParamCache", name);
for attr in &input.ast().attrs {
if attr.path.segments[0].ident == "param" {
match parse_attr::<SecopParam>(attr) {
Ok(param) => params.push(param),
Err(err) => return err
}
} else if attr.path.segments[0].ident == "command" {
match parse_attr::<SecopCommand>(attr) {
Ok(cmd) => commands.push(cmd),
Err(err) => return err
}
}
}
let mut has_internals = false;
let mut has_cache = false;
match &input.ast().data {
syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(fields), .. }) => {
for field in &fields.named {
if field.ident.as_ref().unwrap() == "internals" { has_internals = true; }
if field.ident.as_ref().unwrap() == "cache" { has_cache = true; }
}
}
_ => panic!("derive(ModuleBase) is only possible for a struct with named fields")
}
if !has_internals || !has_cache {
panic!("struct {} must have \"internals: ModInternals\" and \
\"cache: {}ParamCache\" members", name, name);
}
let mut lc_names = HashSet::new();
let mut statics = vec![];
let mut par_read_arms = vec![];
let mut par_write_arms = vec![];
let mut cmd_arms = vec![];
let mut descriptive = vec![];
let mut param_cache = vec![];
let mut poll_busy_params = vec![];
let mut poll_other_params = vec![];
let mut activate_updates = vec![];
let mut init_params_swonly = vec![];
let mut init_params_write = vec![];
let mut init_params_read = vec![];
for SecopParam { name, doc, datatype, readonly, swonly, mandatory, polling,
default, unit, group, visibility } in params {
let polling = polling.unwrap_or(if swonly { 0 } else { 1 });
if !lc_names.insert(name.to_lowercase()) {
panic!("param/cmd name {} is not unique", name)
}
if !VISIBILITIES.iter().any(|&v| v == visibility) {
panic!("visibility {:?} is not an allowed value for param {}", visibility, name);
}
if swonly {
if polling != 0 {
panic!("software-only parameters cannot be polled");
}
if default.is_none() && !mandatory {
panic!("software-only parameters must have a default if not mandatory");
}
} else {
if default.is_some() && readonly {
panic!("readonly hardware parameters cannot have a default");
}
if mandatory && readonly {
panic!("readonly hardware parameters cannot be mandatory");
}
}
let name_id = format_ident!("{}", name);
let type_static = format_ident!("PAR_TYPE_{}", name);
let type_expr = syn::parse_str::<Expr>(&datatype).expect("unparseable datatype");
statics.push(quote! {
static ref #type_static: typedesc_type!(#type_expr) = #type_expr;
});
let type_repr = quote! { <typedesc_type!(#type_expr) as TypeDesc>::Repr };
param_cache.push(quote! {
#name_id: secop_core::module::CachedParam<#type_repr>,
});
let read_method = format_ident!("read_{}", name);
let write_method = format_ident!("write_{}", name);
let update_method = format_ident!("update_{}", name);
par_read_arms.push(match swonly {
false => quote! {
#name => (|| {
let read_value = self.#read_method()?;
let (value, time, send) = self.cache.#name_id.update(read_value, &*#type_static)?;
if send {
self.send_update(#name, value.clone(), time);
}
Ok((value, time))
})()
},
true => quote! {
#name => (|| {
let value = #type_static.to_json(self.cache.#name_id.clone())?;
Ok((value, self.cache.#name_id.time()))
})()
},
});
par_write_arms.push(match (swonly, readonly) {
(false, false) => quote! {
#name => (|| {
self.#write_method(#type_static.from_json(&value)?)?;
self.read(#name)
})()
},
(true, false) => quote! {
#name => (|| {
let (value, time, send) =
self.cache.#name_id.update(#type_static.from_json(&value)?, &*#type_static)?;
if send {
self.send_update(#name, value.clone(), time);
self.#update_method(self.cache.#name_id.clone())?;
}
Ok(json!([value, {"t": time}]))
})()
},
(_, true) => quote! {
#name => Err(Error::new(ErrorKind::ReadOnly, ""))
},
});
if polling != 0 {
let polling_period = polling.abs() as usize;
let poll_it = quote! {
if n % #polling_period == 0 {
let _ = self.read(#name);
}
};
if polling > 0 {
poll_busy_params.push(poll_it);
} else {
poll_other_params.push(poll_it);
}
}
activate_updates.push(quote! {
if let Ok(value) = #type_static.to_json(self.cache.#name_id.clone()) {
res.push(Msg::Update { module: self.name().to_string(),
param: #name.to_string(),
data: json!([value, {"t": self.cache.#name_id.time()}]) });
}
});
let def_expr = default.map(|def| syn::parse_str::<Expr>(&def).unwrap_or_else(
|e| panic!("unparseable default value for param {}: {}", name, e)));
let def_option = def_expr.map_or(quote!(None::<fn() -> #type_repr>),
|expr| quote!(Some(|| #expr)));
let upd_closure = if swonly && !readonly {
quote! { |slf, v| slf.#update_method(v) }
} else {
quote! { |_, _| Ok(()) }
};
let init_stanza = quote! {
if let Err(e) = self.init_parameter(#name, |slf| &mut slf.cache.#name_id, &*#type_static,
#upd_closure, #swonly, #readonly, #def_option) {
return Err(e.amend(concat!("while initializing parameter ", #name)));
}
};
match (swonly, readonly) {
(true, _) => init_params_swonly.push(init_stanza),
(false, false) => init_params_write.push(init_stanza),
(false, true) => init_params_read.push(init_stanza),
}
if visibility != "none" {
let unit_entry = if !unit.is_empty() {
quote! { "unit": #unit, }
} else { quote! {} };
descriptive.push(quote! {
#name: {
"description": #doc,
"datainfo": #type_static.type_json(),
"readonly": #readonly,
"group": #group,
"visibility": #visibility,
#unit_entry
},
});
}
}
for SecopCommand { name, doc, argtype, restype, group, visibility } in commands {
if !lc_names.insert(name.to_lowercase()) {
panic!("param/cmd name {} is not unique", name)
}
if !VISIBILITIES.iter().any(|&v| v == visibility) {
panic!("visibility {:?} is not an allowed value for param {}", visibility, name);
}
let argtype_static = format_ident!("CMD_ARG_{}", name);
let argtype_expr = syn::parse_str::<Expr>(&argtype).expect("unparseable datatype");
let restype_static = format_ident!("CMD_RES_{}", name);
let restype_expr = syn::parse_str::<Expr>(&restype).expect("unparseable datatype");
let do_method = format_ident!("do_{}", name);
statics.push(quote! {
static ref #argtype_static: typedesc_type!(#argtype_expr) = #argtype_expr;
static ref #restype_static: typedesc_type!(#restype_expr) = #restype_expr;
});
cmd_arms.push(quote! {
#name => (|| {
let result_r = self.#do_method(#argtype_static.from_json(&arg)?)?;
let result = #restype_static.to_json(result_r)?;
Ok(json!([result, {"t": localtime()}]))
})()
});
if visibility != "none" {
descriptive.push(quote! {
#name: {
"description": #doc,
"datainfo": {"type": "command",
"argument": #argtype_static.type_json(),
"result": #restype_static.type_json()},
"group": #group,
"visibility": #visibility,
},
});
}
}
let poll_busy_params = &poll_busy_params;
let generated_impl = input.gen_impl(quote! {
use serde_json::{Value, json};
use lazy_static::lazy_static;
use mlzutil::time::localtime;
use secop_core::errors::{Error, ErrorKind, Result};
use secop_core::proto::Msg;
use secop_core::module::ModuleBase;
lazy_static! {
#( #statics )*
}
gen impl ModuleBase for @Self {
fn internals(&self) -> &ModInternals { &self.internals }
fn internals_mut(&mut self) -> &mut ModInternals { &mut self.internals }
fn describe(&self) -> Value {
json!({
"description": self.config().description,
"interface_classes": ["Drivable"], "features": [],
"visibility": self.config().visibility,
"group": self.config().group,
"accessibles": {
#( #descriptive )*
}
})
}
fn read(&mut self, param: &str) -> Result<Value> {
debug!("reading parameter {}", param);
let result = match param {
#( #par_read_arms, )*
_ => Err(Error::no_param())
};
match result {
Ok((value, time)) => Ok(json!([value, {"t": time}])),
Err(e) => {
error!("while reading parameter {}: {}", param, e);
Err(e)
}
}
}
fn change(&mut self, param: &str, value: Value) -> Result<Value> {
debug!("changing parameter {} to {}", param, value);
let result = match param {
#( #par_write_arms, )*
_ => Err(Error::no_param())
};
if let Err(ref e) = result {
error!("while changing parameter {} to {}: {}", param, value, e);
}
result
}
fn command(&mut self, cmd: &str, arg: Value) -> Result<Value> {
debug!("executing command {} with arg {}", cmd, arg);
let result = match cmd {
#( #cmd_arms, )*
_ => Err(Error::no_command())
};
if let Err(ref e) = result {
error!("while executing command {} with arg {}: {}", cmd, arg, e);
}
result
}
fn activate_updates(&mut self) -> Vec<Msg> {
let mut res = Vec::new();
#( #activate_updates )*
res
}
fn init_params(&mut self) -> Result<()> {
#( #init_params_swonly )*
#( #init_params_write )*
#( #init_params_read )*
Ok(())
}
fn poll_normal(&mut self, n: usize) {
if self.cache.status.0 != StatusConst::Busy {
#( #poll_busy_params )*
}
#( #poll_other_params )*
}
fn poll_busy(&mut self, n: usize) {
if self.cache.status.0 == StatusConst::Busy {
#( #poll_busy_params )*
}
}
}
});
let drop_impl = input.gen_impl(quote! {
gen impl Drop for @Self {
fn drop(&mut self) {
self.teardown();
}
}
});
let generated = quote! {
#[derive(Default)]
#vis struct #param_cache_name {
#( #param_cache )*
}
#generated_impl
#drop_impl
};
generated
}