feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+178
@@ -0,0 +1,178 @@
|
||||
use heck::{
|
||||
ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToTrainCase,
|
||||
ToUpperCamelCase,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Ident, LitStr,
|
||||
};
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CaseStyle {
|
||||
CamelCase,
|
||||
KebabCase,
|
||||
MixedCase,
|
||||
ShoutySnakeCase,
|
||||
SnakeCase,
|
||||
TitleCase,
|
||||
UpperCase,
|
||||
LowerCase,
|
||||
ScreamingKebabCase,
|
||||
PascalCase,
|
||||
TrainCase,
|
||||
}
|
||||
|
||||
const VALID_CASE_STYLES: &[&str] = &[
|
||||
"camelCase",
|
||||
"PascalCase",
|
||||
"kebab-case",
|
||||
"snake_case",
|
||||
"SCREAMING_SNAKE_CASE",
|
||||
"SCREAMING-KEBAB-CASE",
|
||||
"lowercase",
|
||||
"UPPERCASE",
|
||||
"title_case",
|
||||
"mixed_case",
|
||||
"Train-Case",
|
||||
];
|
||||
|
||||
impl Parse for CaseStyle {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let text = input.parse::<LitStr>()?;
|
||||
let val = text.value();
|
||||
|
||||
val.as_str().parse().map_err(|_| {
|
||||
syn::Error::new_spanned(
|
||||
&text,
|
||||
format!(
|
||||
"Unexpected case style for serialize_all: `{}`. Valid values are: `{:?}`",
|
||||
val, VALID_CASE_STYLES
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CaseStyle {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(text: &str) -> Result<Self, ()> {
|
||||
Ok(match text {
|
||||
// "camel_case" is a soft-deprecated case-style left for backward compatibility.
|
||||
// <https://github.com/Peternator7/strum/pull/250#issuecomment-1374682221>
|
||||
"PascalCase" | "camel_case" => CaseStyle::PascalCase,
|
||||
"camelCase" => CaseStyle::CamelCase,
|
||||
"snake_case" | "snek_case" => CaseStyle::SnakeCase,
|
||||
"kebab-case" | "kebab_case" => CaseStyle::KebabCase,
|
||||
"SCREAMING-KEBAB-CASE" => CaseStyle::ScreamingKebabCase,
|
||||
"SCREAMING_SNAKE_CASE" | "shouty_snake_case" | "shouty_snek_case" => {
|
||||
CaseStyle::ShoutySnakeCase
|
||||
}
|
||||
"title_case" => CaseStyle::TitleCase,
|
||||
"mixed_case" => CaseStyle::MixedCase,
|
||||
"lowercase" => CaseStyle::LowerCase,
|
||||
"UPPERCASE" => CaseStyle::UpperCase,
|
||||
"Train-Case" => CaseStyle::TrainCase,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CaseStyleHelpers {
|
||||
fn convert_case(&self, case_style: Option<CaseStyle>) -> String;
|
||||
}
|
||||
|
||||
impl CaseStyleHelpers for Ident {
|
||||
fn convert_case(&self, case_style: Option<CaseStyle>) -> String {
|
||||
let ident_string = self.to_string();
|
||||
if let Some(case_style) = case_style {
|
||||
match case_style {
|
||||
CaseStyle::PascalCase => ident_string.to_upper_camel_case(),
|
||||
CaseStyle::KebabCase => ident_string.to_kebab_case(),
|
||||
CaseStyle::MixedCase => ident_string.to_lower_camel_case(),
|
||||
CaseStyle::ShoutySnakeCase => ident_string.to_shouty_snake_case(),
|
||||
CaseStyle::SnakeCase => ident_string.to_snake_case(),
|
||||
CaseStyle::TitleCase => ident_string.to_title_case(),
|
||||
CaseStyle::UpperCase => ident_string.to_uppercase(),
|
||||
CaseStyle::LowerCase => ident_string.to_lowercase(),
|
||||
CaseStyle::ScreamingKebabCase => ident_string.to_kebab_case().to_uppercase(),
|
||||
CaseStyle::TrainCase => ident_string.to_train_case(),
|
||||
CaseStyle::CamelCase => {
|
||||
let camel_case = ident_string.to_upper_camel_case();
|
||||
let mut pascal = String::with_capacity(camel_case.len());
|
||||
let mut it = camel_case.chars();
|
||||
if let Some(ch) = it.next() {
|
||||
pascal.extend(ch.to_lowercase());
|
||||
}
|
||||
pascal.extend(it);
|
||||
pascal
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ident_string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_convert_case() {
|
||||
let id = Ident::new("test_me", proc_macro2::Span::call_site());
|
||||
assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase)));
|
||||
assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase)));
|
||||
assert_eq!("Test-Me", id.convert_case(Some(CaseStyle::TrainCase)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_impl_from_str_for_case_style_pascal_case() {
|
||||
use CaseStyle::*;
|
||||
let f = CaseStyle::from_str;
|
||||
|
||||
assert_eq!(PascalCase, f("PascalCase").unwrap());
|
||||
assert_eq!(PascalCase, f("camel_case").unwrap());
|
||||
|
||||
assert_eq!(CamelCase, f("camelCase").unwrap());
|
||||
|
||||
assert_eq!(SnakeCase, f("snake_case").unwrap());
|
||||
assert_eq!(SnakeCase, f("snek_case").unwrap());
|
||||
|
||||
assert_eq!(KebabCase, f("kebab-case").unwrap());
|
||||
assert_eq!(KebabCase, f("kebab_case").unwrap());
|
||||
|
||||
assert_eq!(ScreamingKebabCase, f("SCREAMING-KEBAB-CASE").unwrap());
|
||||
|
||||
assert_eq!(ShoutySnakeCase, f("SCREAMING_SNAKE_CASE").unwrap());
|
||||
assert_eq!(ShoutySnakeCase, f("shouty_snake_case").unwrap());
|
||||
assert_eq!(ShoutySnakeCase, f("shouty_snek_case").unwrap());
|
||||
|
||||
assert_eq!(LowerCase, f("lowercase").unwrap());
|
||||
|
||||
assert_eq!(UpperCase, f("UPPERCASE").unwrap());
|
||||
|
||||
assert_eq!(TitleCase, f("title_case").unwrap());
|
||||
|
||||
assert_eq!(MixedCase, f("mixed_case").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
/// heck doesn't treat numbers as new words, but this function does.
|
||||
/// E.g. for input `Hello2You`, heck would output `hello2_you`, and snakify would output `hello_2_you`.
|
||||
pub fn snakify(s: &str) -> String {
|
||||
let mut output: Vec<char> = s.to_string().to_snake_case().chars().collect();
|
||||
let mut num_starts = vec![];
|
||||
for (pos, c) in output.iter().enumerate() {
|
||||
if c.is_digit(10) && pos != 0 && !output[pos - 1].is_digit(10) {
|
||||
num_starts.push(pos);
|
||||
}
|
||||
}
|
||||
// need to do in reverse, because after inserting, all chars after the point of insertion are off
|
||||
for i in num_starts.into_iter().rev() {
|
||||
output.insert(i, '_')
|
||||
}
|
||||
output.into_iter().collect()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use super::metadata::{InnerVariantExt, InnerVariantMeta};
|
||||
use super::occurrence_error;
|
||||
use syn::{Field, LitStr};
|
||||
|
||||
pub trait HasInnerVariantProperties {
|
||||
fn get_variant_inner_properties(&self) -> syn::Result<StrumInnerVariantProperties>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Default)]
|
||||
pub struct StrumInnerVariantProperties {
|
||||
pub default_with: Option<LitStr>,
|
||||
}
|
||||
|
||||
impl HasInnerVariantProperties for Field {
|
||||
fn get_variant_inner_properties(&self) -> syn::Result<StrumInnerVariantProperties> {
|
||||
let mut output = StrumInnerVariantProperties { default_with: None };
|
||||
|
||||
let mut default_with_kw = None;
|
||||
for meta in self.get_named_metadata()? {
|
||||
match meta {
|
||||
InnerVariantMeta::DefaultWith { kw, value } => {
|
||||
if let Some(fst_kw) = default_with_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "default_with"));
|
||||
}
|
||||
default_with_kw = Some(kw);
|
||||
output.default_with = Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
parse2, parse_str,
|
||||
punctuated::Punctuated,
|
||||
Attribute, DeriveInput, Expr, ExprLit, Field, Ident, Lit, LitBool, LitStr, Meta, MetaNameValue,
|
||||
Path, Token, Variant, Visibility,
|
||||
};
|
||||
|
||||
use super::case_style::CaseStyle;
|
||||
|
||||
pub mod kw {
|
||||
use syn::custom_keyword;
|
||||
pub use syn::token::Crate;
|
||||
|
||||
// enum metadata
|
||||
custom_keyword!(serialize_all);
|
||||
custom_keyword!(use_phf);
|
||||
custom_keyword!(prefix);
|
||||
|
||||
// enum discriminant metadata
|
||||
custom_keyword!(derive);
|
||||
custom_keyword!(name);
|
||||
custom_keyword!(vis);
|
||||
|
||||
// variant metadata
|
||||
custom_keyword!(message);
|
||||
custom_keyword!(detailed_message);
|
||||
custom_keyword!(serialize);
|
||||
custom_keyword!(to_string);
|
||||
custom_keyword!(disabled);
|
||||
custom_keyword!(default);
|
||||
custom_keyword!(default_with);
|
||||
custom_keyword!(props);
|
||||
custom_keyword!(ascii_case_insensitive);
|
||||
}
|
||||
|
||||
pub enum EnumMeta {
|
||||
SerializeAll {
|
||||
kw: kw::serialize_all,
|
||||
case_style: CaseStyle,
|
||||
},
|
||||
AsciiCaseInsensitive(kw::ascii_case_insensitive),
|
||||
Crate {
|
||||
kw: kw::Crate,
|
||||
crate_module_path: Path,
|
||||
},
|
||||
UsePhf(kw::use_phf),
|
||||
Prefix {
|
||||
kw: kw::prefix,
|
||||
prefix: LitStr,
|
||||
},
|
||||
}
|
||||
|
||||
impl Parse for EnumMeta {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = input.lookahead1();
|
||||
if lookahead.peek(kw::serialize_all) {
|
||||
let kw = input.parse::<kw::serialize_all>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
let case_style = input.parse()?;
|
||||
Ok(EnumMeta::SerializeAll { kw, case_style })
|
||||
} else if lookahead.peek(kw::Crate) {
|
||||
let kw = input.parse::<kw::Crate>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
let path_str: LitStr = input.parse()?;
|
||||
let path_tokens = parse_str(&path_str.value())?;
|
||||
let crate_module_path = parse2(path_tokens)?;
|
||||
Ok(EnumMeta::Crate {
|
||||
kw,
|
||||
crate_module_path,
|
||||
})
|
||||
} else if lookahead.peek(kw::ascii_case_insensitive) {
|
||||
Ok(EnumMeta::AsciiCaseInsensitive(input.parse()?))
|
||||
} else if lookahead.peek(kw::use_phf) {
|
||||
Ok(EnumMeta::UsePhf(input.parse()?))
|
||||
} else if lookahead.peek(kw::prefix) {
|
||||
let kw = input.parse::<kw::prefix>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
let prefix = input.parse()?;
|
||||
Ok(EnumMeta::Prefix { kw, prefix })
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EnumDiscriminantsMeta {
|
||||
Derive { kw: kw::derive, paths: Vec<Path> },
|
||||
Name { kw: kw::name, name: Ident },
|
||||
Vis { kw: kw::vis, vis: Visibility },
|
||||
Other { path: Path, nested: TokenStream },
|
||||
}
|
||||
|
||||
impl Parse for EnumDiscriminantsMeta {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
if input.peek(kw::derive) {
|
||||
let kw = input.parse()?;
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
let paths = content.parse_terminated(Path::parse, Token![,])?;
|
||||
Ok(EnumDiscriminantsMeta::Derive {
|
||||
kw,
|
||||
paths: paths.into_iter().collect(),
|
||||
})
|
||||
} else if input.peek(kw::name) {
|
||||
let kw = input.parse()?;
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
let name = content.parse()?;
|
||||
Ok(EnumDiscriminantsMeta::Name { kw, name })
|
||||
} else if input.peek(kw::vis) {
|
||||
let kw = input.parse()?;
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
let vis = content.parse()?;
|
||||
Ok(EnumDiscriminantsMeta::Vis { kw, vis })
|
||||
} else {
|
||||
let path = input.parse()?;
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
let nested = content.parse()?;
|
||||
Ok(EnumDiscriminantsMeta::Other { path, nested })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DeriveInputExt {
|
||||
/// Get all the strum metadata associated with an enum.
|
||||
fn get_metadata(&self) -> syn::Result<Vec<EnumMeta>>;
|
||||
|
||||
/// Get all the `strum_discriminants` metadata associated with an enum.
|
||||
fn get_discriminants_metadata(&self) -> syn::Result<Vec<EnumDiscriminantsMeta>>;
|
||||
}
|
||||
|
||||
impl DeriveInputExt for DeriveInput {
|
||||
fn get_metadata(&self) -> syn::Result<Vec<EnumMeta>> {
|
||||
get_metadata_inner("strum", &self.attrs)
|
||||
}
|
||||
|
||||
fn get_discriminants_metadata(&self) -> syn::Result<Vec<EnumDiscriminantsMeta>> {
|
||||
get_metadata_inner("strum_discriminants", &self.attrs)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum VariantMeta {
|
||||
Message {
|
||||
kw: kw::message,
|
||||
value: LitStr,
|
||||
},
|
||||
DetailedMessage {
|
||||
kw: kw::detailed_message,
|
||||
value: LitStr,
|
||||
},
|
||||
Serialize {
|
||||
kw: kw::serialize,
|
||||
value: LitStr,
|
||||
},
|
||||
Documentation {
|
||||
value: LitStr,
|
||||
},
|
||||
ToString {
|
||||
kw: kw::to_string,
|
||||
value: LitStr,
|
||||
},
|
||||
Disabled(kw::disabled),
|
||||
Default(kw::default),
|
||||
DefaultWith {
|
||||
kw: kw::default_with,
|
||||
value: LitStr,
|
||||
},
|
||||
AsciiCaseInsensitive {
|
||||
kw: kw::ascii_case_insensitive,
|
||||
value: bool,
|
||||
},
|
||||
Props {
|
||||
kw: kw::props,
|
||||
props: Vec<(LitStr, LitStr)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Parse for VariantMeta {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = input.lookahead1();
|
||||
if lookahead.peek(kw::message) {
|
||||
let kw = input.parse()?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
let value = input.parse()?;
|
||||
Ok(VariantMeta::Message { kw, value })
|
||||
} else if lookahead.peek(kw::detailed_message) {
|
||||
let kw = input.parse()?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
let value = input.parse()?;
|
||||
Ok(VariantMeta::DetailedMessage { kw, value })
|
||||
} else if lookahead.peek(kw::serialize) {
|
||||
let kw = input.parse()?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
let value = input.parse()?;
|
||||
Ok(VariantMeta::Serialize { kw, value })
|
||||
} else if lookahead.peek(kw::to_string) {
|
||||
let kw = input.parse()?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
let value = input.parse()?;
|
||||
Ok(VariantMeta::ToString { kw, value })
|
||||
} else if lookahead.peek(kw::disabled) {
|
||||
Ok(VariantMeta::Disabled(input.parse()?))
|
||||
} else if lookahead.peek(kw::default) {
|
||||
Ok(VariantMeta::Default(input.parse()?))
|
||||
} else if lookahead.peek(kw::default_with) {
|
||||
let kw = input.parse()?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
let value = input.parse()?;
|
||||
Ok(VariantMeta::DefaultWith { kw, value })
|
||||
} else if lookahead.peek(kw::ascii_case_insensitive) {
|
||||
let kw = input.parse()?;
|
||||
let value = if input.peek(Token![=]) {
|
||||
let _: Token![=] = input.parse()?;
|
||||
input.parse::<LitBool>()?.value
|
||||
} else {
|
||||
true
|
||||
};
|
||||
Ok(VariantMeta::AsciiCaseInsensitive { kw, value })
|
||||
} else if lookahead.peek(kw::props) {
|
||||
let kw = input.parse()?;
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
let props = content.parse_terminated(Prop::parse, Token![,])?;
|
||||
Ok(VariantMeta::Props {
|
||||
kw,
|
||||
props: props
|
||||
.into_iter()
|
||||
.map(|Prop(k, v)| (LitStr::new(&k.to_string(), k.span()), v))
|
||||
.collect(),
|
||||
})
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Prop(Ident, LitStr);
|
||||
|
||||
impl Parse for Prop {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
use syn::ext::IdentExt;
|
||||
|
||||
let k = Ident::parse_any(input)?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
let v = input.parse()?;
|
||||
|
||||
Ok(Prop(k, v))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VariantExt {
|
||||
/// Get all the metadata associated with an enum variant.
|
||||
fn get_metadata(&self) -> syn::Result<Vec<VariantMeta>>;
|
||||
}
|
||||
|
||||
impl VariantExt for Variant {
|
||||
fn get_metadata(&self) -> syn::Result<Vec<VariantMeta>> {
|
||||
let result = get_metadata_inner("strum", &self.attrs)?;
|
||||
self.attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.meta.path().is_ident("doc"))
|
||||
.try_fold(result, |mut vec, attr| {
|
||||
if let Meta::NameValue(MetaNameValue {
|
||||
value:
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(value),
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) = &attr.meta
|
||||
{
|
||||
vec.push(VariantMeta::Documentation {
|
||||
value: value.clone(),
|
||||
})
|
||||
}
|
||||
Ok(vec)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_metadata_inner<'a, T: Parse>(
|
||||
ident: &str,
|
||||
it: impl IntoIterator<Item = &'a Attribute>,
|
||||
) -> syn::Result<Vec<T>> {
|
||||
it.into_iter()
|
||||
.filter(|attr| attr.path().is_ident(ident))
|
||||
.try_fold(Vec::new(), |mut vec, attr| {
|
||||
vec.extend(attr.parse_args_with(Punctuated::<T, Token![,]>::parse_terminated)?);
|
||||
Ok(vec)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InnerVariantMeta {
|
||||
DefaultWith { kw: kw::default_with, value: LitStr },
|
||||
}
|
||||
|
||||
impl Parse for InnerVariantMeta {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = input.lookahead1();
|
||||
if lookahead.peek(kw::default_with) {
|
||||
let kw = input.parse()?;
|
||||
let _: Token![=] = input.parse()?;
|
||||
let value = input.parse()?;
|
||||
Ok(InnerVariantMeta::DefaultWith { kw, value })
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait InnerVariantExt {
|
||||
/// Get all the metadata associated with an enum variant inner.
|
||||
fn get_named_metadata(&self) -> syn::Result<Vec<InnerVariantMeta>>;
|
||||
}
|
||||
|
||||
impl InnerVariantExt for Field {
|
||||
fn get_named_metadata(&self) -> syn::Result<Vec<InnerVariantMeta>> {
|
||||
let result = get_metadata_inner("strum", &self.attrs)?;
|
||||
self.attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.meta.path().is_ident("default_with"))
|
||||
.try_fold(result, |vec, _attr| Ok(vec))
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
pub use self::case_style::snakify;
|
||||
pub use self::inner_variant_props::HasInnerVariantProperties;
|
||||
pub use self::type_props::HasTypeProperties;
|
||||
pub use self::variant_props::HasStrumVariantProperties;
|
||||
|
||||
pub mod case_style;
|
||||
pub mod inner_variant_props;
|
||||
mod metadata;
|
||||
pub mod type_props;
|
||||
pub mod variant_props;
|
||||
|
||||
use proc_macro2::Span;
|
||||
use quote::ToTokens;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub fn non_enum_error() -> syn::Error {
|
||||
syn::Error::new(Span::call_site(), "This macro only supports enums.")
|
||||
}
|
||||
|
||||
pub fn non_unit_variant_error() -> syn::Error {
|
||||
syn::Error::new(
|
||||
Span::call_site(),
|
||||
"This macro only supports enums of strictly unit variants. Consider \
|
||||
using it in conjunction with [`EnumDiscriminants`]",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn strum_discriminants_passthrough_error(span: &impl Spanned) -> syn::Error {
|
||||
syn::Error::new(
|
||||
span.span(),
|
||||
"expected a pass-through attribute, e.g. #[strum_discriminants(serde(rename = \"var0\"))]",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn occurrence_error<T: ToTokens>(fst: T, snd: T, attr: &str) -> syn::Error {
|
||||
let mut e = syn::Error::new_spanned(
|
||||
snd,
|
||||
format!("Found multiple occurrences of strum({})", attr),
|
||||
);
|
||||
e.combine(syn::Error::new_spanned(fst, "first one here"));
|
||||
e
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use std::default::Default;
|
||||
use syn::{parse_quote, DeriveInput, Ident, LitStr, Path, Visibility};
|
||||
|
||||
use super::case_style::CaseStyle;
|
||||
use super::metadata::{DeriveInputExt, EnumDiscriminantsMeta, EnumMeta};
|
||||
use super::occurrence_error;
|
||||
|
||||
pub trait HasTypeProperties {
|
||||
fn get_type_properties(&self) -> syn::Result<StrumTypeProperties>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StrumTypeProperties {
|
||||
pub case_style: Option<CaseStyle>,
|
||||
pub ascii_case_insensitive: bool,
|
||||
pub crate_module_path: Option<Path>,
|
||||
pub discriminant_derives: Vec<Path>,
|
||||
pub discriminant_name: Option<Ident>,
|
||||
pub discriminant_others: Vec<TokenStream>,
|
||||
pub discriminant_vis: Option<Visibility>,
|
||||
pub use_phf: bool,
|
||||
pub prefix: Option<LitStr>,
|
||||
pub enum_repr: Option<TokenStream>,
|
||||
}
|
||||
|
||||
impl HasTypeProperties for DeriveInput {
|
||||
fn get_type_properties(&self) -> syn::Result<StrumTypeProperties> {
|
||||
let mut output = StrumTypeProperties::default();
|
||||
|
||||
let strum_meta = self.get_metadata()?;
|
||||
let discriminants_meta = self.get_discriminants_metadata()?;
|
||||
|
||||
let mut serialize_all_kw = None;
|
||||
let mut ascii_case_insensitive_kw = None;
|
||||
let mut use_phf_kw = None;
|
||||
let mut crate_module_path_kw = None;
|
||||
let mut prefix_kw = None;
|
||||
for meta in strum_meta {
|
||||
match meta {
|
||||
EnumMeta::SerializeAll { case_style, kw } => {
|
||||
if let Some(fst_kw) = serialize_all_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "serialize_all"));
|
||||
}
|
||||
|
||||
serialize_all_kw = Some(kw);
|
||||
output.case_style = Some(case_style);
|
||||
}
|
||||
EnumMeta::AsciiCaseInsensitive(kw) => {
|
||||
if let Some(fst_kw) = ascii_case_insensitive_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "ascii_case_insensitive"));
|
||||
}
|
||||
|
||||
ascii_case_insensitive_kw = Some(kw);
|
||||
output.ascii_case_insensitive = true;
|
||||
}
|
||||
EnumMeta::UsePhf(kw) => {
|
||||
if let Some(fst_kw) = use_phf_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "use_phf"));
|
||||
}
|
||||
|
||||
use_phf_kw = Some(kw);
|
||||
output.use_phf = true;
|
||||
}
|
||||
EnumMeta::Crate {
|
||||
crate_module_path,
|
||||
kw,
|
||||
} => {
|
||||
if let Some(fst_kw) = crate_module_path_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "Crate"));
|
||||
}
|
||||
|
||||
crate_module_path_kw = Some(kw);
|
||||
output.crate_module_path = Some(crate_module_path);
|
||||
}
|
||||
EnumMeta::Prefix { prefix, kw } => {
|
||||
if let Some(fst_kw) = prefix_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "prefix"));
|
||||
}
|
||||
|
||||
prefix_kw = Some(kw);
|
||||
output.prefix = Some(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut name_kw = None;
|
||||
let mut vis_kw = None;
|
||||
for meta in discriminants_meta {
|
||||
match meta {
|
||||
EnumDiscriminantsMeta::Derive { paths, .. } => {
|
||||
output.discriminant_derives.extend(paths);
|
||||
}
|
||||
EnumDiscriminantsMeta::Name { name, kw } => {
|
||||
if let Some(fst_kw) = name_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "name"));
|
||||
}
|
||||
|
||||
name_kw = Some(kw);
|
||||
output.discriminant_name = Some(name);
|
||||
}
|
||||
EnumDiscriminantsMeta::Vis { vis, kw } => {
|
||||
if let Some(fst_kw) = vis_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "vis"));
|
||||
}
|
||||
|
||||
vis_kw = Some(kw);
|
||||
output.discriminant_vis = Some(vis);
|
||||
}
|
||||
EnumDiscriminantsMeta::Other { path, nested } => {
|
||||
output.discriminant_others.push(quote! { #path(#nested) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attrs = &self.attrs;
|
||||
for attr in attrs {
|
||||
if let Ok(list) = attr.meta.require_list() {
|
||||
if let Some(ident) = list.path.get_ident() {
|
||||
if ident == "repr" {
|
||||
output.enum_repr = Some(list.tokens.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
impl StrumTypeProperties {
|
||||
pub fn crate_module_path(&self) -> Path {
|
||||
self.crate_module_path
|
||||
.as_ref()
|
||||
.map_or_else(|| parse_quote!(::strum), |path| parse_quote!(#path))
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
use std::default::Default;
|
||||
use syn::{Ident, LitStr, Variant};
|
||||
|
||||
use super::case_style::{CaseStyle, CaseStyleHelpers};
|
||||
use super::metadata::{kw, VariantExt, VariantMeta};
|
||||
use super::occurrence_error;
|
||||
|
||||
pub trait HasStrumVariantProperties {
|
||||
fn get_variant_properties(&self) -> syn::Result<StrumVariantProperties>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Default)]
|
||||
pub struct StrumVariantProperties {
|
||||
pub disabled: Option<kw::disabled>,
|
||||
pub default: Option<kw::default>,
|
||||
pub default_with: Option<LitStr>,
|
||||
pub ascii_case_insensitive: Option<bool>,
|
||||
pub message: Option<LitStr>,
|
||||
pub detailed_message: Option<LitStr>,
|
||||
pub documentation: Vec<LitStr>,
|
||||
pub string_props: Vec<(LitStr, LitStr)>,
|
||||
serialize: Vec<LitStr>,
|
||||
pub to_string: Option<LitStr>,
|
||||
ident: Option<Ident>,
|
||||
}
|
||||
|
||||
impl StrumVariantProperties {
|
||||
fn ident_as_str(&self, case_style: Option<CaseStyle>) -> LitStr {
|
||||
let ident = self.ident.as_ref().expect("identifier");
|
||||
LitStr::new(&ident.convert_case(case_style), ident.span())
|
||||
}
|
||||
|
||||
pub fn get_preferred_name(
|
||||
&self,
|
||||
case_style: Option<CaseStyle>,
|
||||
prefix: Option<&LitStr>,
|
||||
) -> LitStr {
|
||||
let mut output = self.to_string.as_ref().cloned().unwrap_or_else(|| {
|
||||
self.serialize
|
||||
.iter()
|
||||
.max_by_key(|s| s.value().len())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.ident_as_str(case_style))
|
||||
});
|
||||
|
||||
if let Some(prefix) = prefix {
|
||||
output = LitStr::new(&(prefix.value() + &output.value()), output.span());
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_serializations(&self, case_style: Option<CaseStyle>) -> Vec<LitStr> {
|
||||
let mut attrs = self.serialize.clone();
|
||||
if let Some(to_string) = &self.to_string {
|
||||
attrs.push(to_string.clone());
|
||||
}
|
||||
|
||||
if attrs.is_empty() {
|
||||
attrs.push(self.ident_as_str(case_style));
|
||||
}
|
||||
|
||||
attrs
|
||||
}
|
||||
}
|
||||
|
||||
impl HasStrumVariantProperties for Variant {
|
||||
fn get_variant_properties(&self) -> syn::Result<StrumVariantProperties> {
|
||||
let mut output = StrumVariantProperties {
|
||||
ident: Some(self.ident.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut message_kw = None;
|
||||
let mut detailed_message_kw = None;
|
||||
let mut disabled_kw = None;
|
||||
let mut default_kw = None;
|
||||
let mut default_with_kw = None;
|
||||
let mut to_string_kw = None;
|
||||
let mut ascii_case_insensitive_kw = None;
|
||||
for meta in self.get_metadata()? {
|
||||
match meta {
|
||||
VariantMeta::Message { value, kw } => {
|
||||
if let Some(fst_kw) = message_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "message"));
|
||||
}
|
||||
|
||||
message_kw = Some(kw);
|
||||
output.message = Some(value);
|
||||
}
|
||||
VariantMeta::DetailedMessage { value, kw } => {
|
||||
if let Some(fst_kw) = detailed_message_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "detailed_message"));
|
||||
}
|
||||
|
||||
detailed_message_kw = Some(kw);
|
||||
output.detailed_message = Some(value);
|
||||
}
|
||||
VariantMeta::Documentation { value } => {
|
||||
output.documentation.push(value);
|
||||
}
|
||||
VariantMeta::Serialize { value, .. } => {
|
||||
output.serialize.push(value);
|
||||
}
|
||||
VariantMeta::ToString { value, kw } => {
|
||||
if let Some(fst_kw) = to_string_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "to_string"));
|
||||
}
|
||||
|
||||
to_string_kw = Some(kw);
|
||||
output.to_string = Some(value);
|
||||
}
|
||||
VariantMeta::Disabled(kw) => {
|
||||
if let Some(fst_kw) = disabled_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "disabled"));
|
||||
}
|
||||
|
||||
disabled_kw = Some(kw);
|
||||
output.disabled = Some(kw);
|
||||
}
|
||||
VariantMeta::Default(kw) => {
|
||||
if let Some(fst_kw) = default_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "default"));
|
||||
}
|
||||
|
||||
default_kw = Some(kw);
|
||||
output.default = Some(kw);
|
||||
}
|
||||
VariantMeta::DefaultWith { kw, value } => {
|
||||
if let Some(fst_kw) = default_with_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "default_with"));
|
||||
}
|
||||
|
||||
default_with_kw = Some(kw);
|
||||
output.default_with = Some(value);
|
||||
}
|
||||
VariantMeta::AsciiCaseInsensitive { kw, value } => {
|
||||
if let Some(fst_kw) = ascii_case_insensitive_kw {
|
||||
return Err(occurrence_error(fst_kw, kw, "ascii_case_insensitive"));
|
||||
}
|
||||
|
||||
ascii_case_insensitive_kw = Some(kw);
|
||||
output.ascii_case_insensitive = Some(value);
|
||||
}
|
||||
VariantMeta::Props { props, .. } => {
|
||||
output.string_props.extend(props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user