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}