veecle_os_data_support_can_codegen/
lib.rs

1//! Generates Veecle OS code from a CAN-DBC file.
2//!
3//! ```
4//! use veecle_os_data_support_can_codegen::{ArbitraryOptions, Generator, Options};
5//!
6//! let input = include_str!("../tests/cases/CSS-Electronics-SAE-J1939-DEMO.dbc");
7//!
8//! let options = Options {
9//!     veecle_os_runtime: syn::parse_str("veecle_os_runtime")?,
10//!     veecle_os_data_support_can: syn::parse_str("veecle_os_data_support_can")?,
11//!     arbitrary: Some(ArbitraryOptions {
12//!         path: syn::parse_str("arbitrary")?,
13//!         cfg: Some(syn::parse_str(r#"feature = "std""#)?),
14//!     }),
15//!     serde: syn::parse_str("my_serde")?,
16//!     message_frame_validations: Box::new(|_| None),
17//! };
18//!
19//! let code = Generator::new("demo.dbc", options, &input).into_string();
20//!
21//! assert!(code.contains("mod eec1"));
22//!
23//! # anyhow::Ok(())
24//! ```
25
26#![forbid(unsafe_code)]
27
28use std::str::FromStr;
29
30use anyhow::{Context, Result, anyhow};
31use can_dbc::DBC;
32use proc_macro2::{Literal, TokenStream};
33use quote::quote;
34
35mod dbc_ext;
36mod generate;
37
38/// Options to customize the generated code.
39#[derive(Debug)]
40pub struct ArbitraryOptions {
41    /// A path to the `arbitrary` crate, e.g. `::arbitrary` if it is a dependency of the crate the generated code will
42    /// be included in.
43    pub path: syn::Path,
44
45    /// Whether and how to cfg-gate the arbitrary usage, if `Some` the code will be gated with `#[cfg]`/`#[cfg_attr]`
46    /// using the specified clause.
47    pub cfg: Option<syn::Meta>,
48}
49
50impl ArbitraryOptions {
51    fn to_cfg(&self) -> Option<syn::Attribute> {
52        self.cfg
53            .as_ref()
54            .map(|meta| syn::parse_quote!(#[cfg(#meta)]))
55    }
56}
57
58/// Options to customize the generated code.
59pub struct Options {
60    /// A path to the `veecle-os-runtime` crate, e.g. `::veecle_os_runtime` if it is a dependency of the crate the generated code
61    /// will be included in.
62    pub veecle_os_runtime: syn::Path,
63
64    /// A path to the `veecle-os-data-support-can` crate, e.g. `::veecle_os_data_support_can` if it is a dependency of the crate
65    /// the generated code will be included in.
66    pub veecle_os_data_support_can: syn::Path,
67
68    /// Whether and how to generate code integrating with `arbitrary`
69    pub arbitrary: Option<ArbitraryOptions>,
70
71    /// A path to the `serde` crate, e.g. `::serde` if it is a dependency of the crate the generated code will be
72    /// included in.
73    pub serde: syn::Path,
74
75    /// For each message name there can be an associated `fn(&Frame) -> Result<()>` expression that
76    /// will be called to validate the frame during deserialization.
77    #[allow(clippy::type_complexity)]
78    pub message_frame_validations: Box<dyn Fn(&syn::Ident) -> Option<syn::Expr>>,
79}
80
81impl core::fmt::Debug for Options {
82    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83        f.debug_struct("Options")
84            .field("veecle_os_runtime", &self.veecle_os_runtime)
85            .field(
86                "veecle_os_data_support_can",
87                &self.veecle_os_data_support_can,
88            )
89            .field("arbitrary", &self.arbitrary)
90            .field(
91                "message_frame_validation",
92                &format!(
93                    "<value of type {}>",
94                    core::any::type_name_of_val(&*self.message_frame_validations)
95                ),
96            )
97            .finish()
98    }
99}
100
101/// Generates Veecle OS code from a CAN-DBC file.
102#[derive(Debug)]
103pub struct Generator {
104    options: Options,
105    inner: Result<DBC>,
106}
107
108impl Generator {
109    /// Constructs a new `Generator` for the given CAN-DBC `input`.
110    ///
111    /// `context` should be some kind of identifier for error messages, e.g. the source filename.
112    pub fn new(context: &str, options: Options, input: &str) -> Self {
113        fn parse_dbc(input: &str) -> Result<DBC> {
114            DBC::try_from(input).map_err(|error| match error {
115                can_dbc::Error::Incomplete(_, rest) => {
116                    let parsed = &input[..(input.len() - rest.len())];
117                    let lines = parsed.lines().count();
118                    anyhow!("parser error around line {lines}")
119                }
120                can_dbc::Error::Nom(error) => anyhow!(error.to_owned()),
121                can_dbc::Error::MultipleMultiplexors => {
122                    anyhow!(
123                        "can’t Lookup multiplexors because the message uses extended multiplexing"
124                    )
125                }
126            })
127        }
128
129        Self {
130            options,
131            // We don't return the error here so that we can decide later whether to report it via a `Result` or by
132            // generating `compile_error!`.
133            inner: parse_dbc(input).with_context(|| format!("failed to parse `{context}`")),
134        }
135    }
136
137    /// Converts the input into a [`TokenStream`], returning any parsing or semantic errors.
138    pub fn try_into_token_stream(self) -> Result<TokenStream> {
139        generate::generate(&self.options, &self.inner?)
140    }
141
142    /// Converts the input into a [`TokenStream`], converting any error into a generated [`compile_error!`].
143    pub fn into_token_stream(self) -> TokenStream {
144        fn to_compile_error(error: &anyhow::Error) -> TokenStream {
145            use std::fmt::Write;
146
147            let mut msg = error.to_string();
148
149            if error.source().is_some() {
150                write!(msg, "\n\nCaused by:").unwrap();
151                for cause in error.chain().skip(1) {
152                    write!(msg, "\n    {cause}").unwrap();
153                }
154            }
155
156            // Try and use a raw string literal for more readability in the source code, but because there's no good
157            // way to make one this could fail depending on the `error` content, so fallback to a non-raw string
158            // literal in that case.
159            let msg = Literal::from_str(&format!("r#\"\n{msg}\n\"#"))
160                .unwrap_or_else(|_| Literal::string(&msg));
161
162            quote!(compile_error!(#msg);)
163        }
164
165        match self.try_into_token_stream() {
166            Ok(tokens) => tokens,
167            Err(error) => to_compile_error(&error),
168        }
169    }
170
171    /// Converts the input into a formatted code [`String`], returning any parsing or semantic errors.
172    pub fn try_into_string(self) -> Result<String> {
173        Ok(prettyplease::unparse(
174            &syn::parse_file(&self.try_into_token_stream()?.to_string())
175                .context("parsing generated code to prettify")?,
176        ))
177    }
178
179    /// Converts the input into a formatted code [`String`], converting any error into a generated [`compile_error!`].
180    pub fn into_string(self) -> String {
181        fn maybe_pretty(code: String) -> String {
182            match syn::parse_file(&code) {
183                Ok(file) => prettyplease::unparse(&file),
184                Err(_) => code,
185            }
186        }
187
188        maybe_pretty(self.into_token_stream().to_string())
189    }
190}