use crate::utils::{
bump_stmt, consume_meta_value, fnv1a_hash, try_parse_lit_str, variant_ident_for_field,
FORMAT_VERSION_KEY, MAGIC_KEY,
};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::DeriveInput;
struct StoreField {
ident: syn::Ident,
name: String,
ty: syn::Type,
default: Option<String>,
env_default: Option<String>,
bump: Option<String>,
}
struct KeyInfo {
_name: String,
hash: u64,
variant: syn::Ident,
}
fn parse_store_fields(data: &syn::DataStruct) -> (Vec<StoreField>, Vec<KeyInfo>) {
let mut fields = Vec::new();
let mut keys = Vec::new();
for field in &data.fields {
let ident = field.ident.as_ref().expect("unnamed fields not supported");
let name = ident.to_string();
let mut skip = false;
let mut default: Option<String> = None;
let mut env_default: Option<String> = None;
let mut bump: Option<String> = None;
for attr in &field.attrs {
if attr.path().is_ident("config_store") {
if let Err(e) = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
skip = true;
} else if meta.path.is_ident("default") {
default = try_parse_lit_str(&meta);
} else if meta.path.is_ident("env_default") {
env_default = try_parse_lit_str(&meta);
} else if meta.path.is_ident("bump") {
bump = try_parse_lit_str(&meta);
} else {
consume_meta_value(&meta);
}
Ok(())
}) {
let _ = e; }
}
}
if skip {
continue;
}
let hash = fnv1a_hash(&name);
let variant = variant_ident_for_field(ident);
keys.push(KeyInfo {
_name: name.clone(),
hash,
variant,
});
fields.push(StoreField {
ident: ident.clone(),
name,
ty: field.ty.clone(),
default,
env_default,
bump,
});
}
(fields, keys)
}
fn gen_collision_check(all_hashes: &[u64]) -> TokenStream {
let mut stmts = Vec::new();
for i in 0..all_hashes.len() {
for j in (i + 1)..all_hashes.len() {
let hi = all_hashes[i];
let hj = all_hashes[j];
stmts.push(quote! {
::core::assert!(#hi != #hj, "Config key hash collision detected");
});
}
}
if stmts.is_empty() {
quote! {}
} else {
quote! {
const _: () = {
#(#stmts)*
};
}
}
}
fn gen_key_enum(keys: &[KeyInfo]) -> TokenStream {
let magic_hash = fnv1a_hash(MAGIC_KEY);
let format_version_hash = fnv1a_hash(FORMAT_VERSION_KEY);
let mut variants = vec![
quote! { Magic = #magic_hash },
quote! { FormatVersion = #format_version_hash },
];
variants.extend(keys.iter().map(|k| {
let v = &k.variant;
let h = k.hash;
quote! { #v = #h }
}));
quote! {
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u64)]
pub enum ConfigKey {
#(#variants),*
}
impl ConfigKey {
pub fn as_key(&self) -> u64 {
*self as u64
}
}
}
}
fn gen_accessors(fields: &[StoreField]) -> (Vec<TokenStream>, Vec<TokenStream>) {
let getters = fields
.iter()
.map(|f| {
let ident = &f.ident;
let ty = &f.ty;
quote! {
pub fn #ident(&self) -> <#ty as wifi_caddy::config_storage::ConfigValue>::Getter<'_> {
wifi_caddy::config_storage::ConfigValue::to_getter(&self.#ident)
}
}
})
.collect();
let setters = fields
.iter()
.map(|f| {
let ident = &f.ident;
let ty = &f.ty;
let set_ident = format_ident!("set_{}", ident);
let bump_ts = bump_stmt(f.bump.as_ref(), ident);
quote! {
pub fn #set_ident(&mut self, value: impl Into<#ty>) {
self.#ident = value.into();
#bump_ts
}
}
})
.collect();
(getters, setters)
}
fn gen_str_arms(fields: &[StoreField]) -> (Vec<TokenStream>, Vec<TokenStream>) {
let get_arms = fields
.iter()
.map(|f| {
let ident = &f.ident;
let key_lit = syn::LitStr::new(&f.name, proc_macro2::Span::call_site());
quote! {
#key_lit => Some(alloc::format!("{}", self.#ident))
}
})
.collect();
let set_arms = fields
.iter()
.map(|f| {
let ident = &f.ident;
let key_lit = syn::LitStr::new(&f.name, proc_macro2::Span::call_site());
let bump_ts = bump_stmt(f.bump.as_ref(), ident);
quote! {
#key_lit => {
if let Ok(v) = value.parse() {
self.#ident = v;
#bump_ts
true
} else {
false
}
}
}
})
.collect();
(get_arms, set_arms)
}
fn gen_key_arms(fields: &[StoreField]) -> (Vec<TokenStream>, Vec<TokenStream>) {
let get_arms = fields
.iter()
.map(|f| {
let ident = &f.ident;
let variant = variant_ident_for_field(ident);
quote! {
ConfigKey::#variant => Some(alloc::format!("{}", self.#ident))
}
})
.collect();
let set_arms = fields
.iter()
.map(|f| {
let ident = &f.ident;
let variant = variant_ident_for_field(ident);
let bump_ts = bump_stmt(f.bump.as_ref(), ident);
quote! {
ConfigKey::#variant => {
if let Ok(v) = value.parse() {
self.#ident = v;
#bump_ts
true
} else {
false
}
}
}
})
.collect();
(get_arms, set_arms)
}
fn gen_load_calls(fields: &[StoreField]) -> Vec<TokenStream> {
fields
.iter()
.map(|f| {
let ident = &f.ident;
let ty = &f.ty;
let variant = variant_ident_for_field(ident);
let set_ident = format_ident!("set_{}", ident);
if let Some(ref env_var_name) = f.env_default {
let env_lit = syn::LitStr::new(env_var_name, proc_macro2::Span::call_site());
quote! {
let val = storage.get_value::<#ty>(ConfigKey::#variant.as_key()).await?;
if let Some(v) = val {
config.#set_ident(v);
} else if let Some(env_val) = option_env!(#env_lit) {
if let Ok(parsed) = env_val.parse::<#ty>() {
config.#set_ident(parsed);
}
}
}
} else if let Some(ref default_val) = f.default {
let default_lit = syn::LitStr::new(default_val, proc_macro2::Span::call_site());
quote! {
let val = storage.get_value::<#ty>(ConfigKey::#variant.as_key()).await?;
if let Some(v) = val {
config.#set_ident(v);
} else if let Ok(parsed) = #default_lit.parse::<#ty>() {
config.#set_ident(parsed);
}
}
} else {
quote! {
if let Some(v) = storage.get_value::<#ty>(ConfigKey::#variant.as_key()).await? {
config.#set_ident(v);
}
}
}
})
.collect()
}
fn gen_store_calls(fields: &[StoreField]) -> Vec<TokenStream> {
fields
.iter()
.map(|f| {
let ident = &f.ident;
let variant = variant_ident_for_field(ident);
quote! {
storage.set_value(ConfigKey::#variant.as_key(), &self.#ident).await?;
}
})
.collect()
}
pub fn derive_config_store_impl(input: &DeriveInput) -> TokenStream {
let name = &input.ident;
let syn::Data::Struct(data) = &input.data else {
return syn::Error::new_spanned(input, "ConfigStore only supports structs")
.to_compile_error();
};
let (fields, keys) = parse_store_fields(data);
let mut all_hashes = vec![fnv1a_hash(MAGIC_KEY), fnv1a_hash(FORMAT_VERSION_KEY)];
all_hashes.extend(keys.iter().map(|k| k.hash));
let collision_check = gen_collision_check(&all_hashes);
let key_enum = gen_key_enum(&keys);
let (getters, setters) = gen_accessors(&fields);
let (get_str_arms, set_str_arms) = gen_str_arms(&fields);
let (get_key_arms, set_key_arms) = gen_key_arms(&fields);
let load_calls = gen_load_calls(&fields);
let store_calls = gen_store_calls(&fields);
let metadata_get_arms = quote! { ConfigKey::Magic | ConfigKey::FormatVersion => None, };
let metadata_set_arms = quote! { ConfigKey::Magic | ConfigKey::FormatVersion => false, };
quote! {
#collision_check
#key_enum
impl #name {
#(#getters)*
#(#setters)*
pub fn get(&self, key: &str) -> Option<alloc::string::String> {
match key {
#(#get_str_arms),*,
_ => None,
}
}
pub fn set(&mut self, key: &str, value: &str) -> bool {
match key {
#(#set_str_arms),*,
_ => false,
}
}
pub fn get_by_key(&self, key: ConfigKey) -> Option<alloc::string::String> {
match key {
#metadata_get_arms
#(#get_key_arms),*
}
}
pub fn set_by_key(&mut self, key: ConfigKey, value: &str) -> bool {
match key {
#metadata_set_arms
#(#set_key_arms),*
}
}
}
impl wifi_caddy::config_storage::ConfigGet for #name {
fn get(&self, key: &str) -> Option<alloc::string::String> {
match key {
#(#get_str_arms),*,
_ => None,
}
}
}
impl wifi_caddy::config_storage::ConfigLoadStore for #name {
async fn load_from<S: wifi_caddy::config_storage::ConfigStorage>(
storage: &mut S,
) -> Result<Self, wifi_caddy::config_storage::ConfigError> {
let mut config = #name::default();
#(#load_calls)*
Ok(config)
}
async fn store_to<S: wifi_caddy::config_storage::ConfigStorage>(
&self,
storage: &mut S,
) -> Result<(), wifi_caddy::config_storage::ConfigError> {
#(#store_calls)*
Ok(())
}
}
}
}