feat: WIP add secrets module and macro crate

This commit is contained in:
2025-08-15 14:40:39 -04:00
parent 67f3a23071
commit 2a6a233fb2
11 changed files with 629 additions and 19 deletions

View File

@@ -0,0 +1,13 @@
[package]
name = "harmony-secrets-derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0"
proc-macro2 = "1.0"
proc-macro-crate = "3.3"
syn = "2.0"

View File

@@ -0,0 +1,152 @@
use syn::DeriveInput;
use syn::parse_macro_input;
use proc_macro::TokenStream;
use proc_macro_crate::{FoundCrate, crate_name};
use quote::quote;
use syn::{
Ident, LitStr, Meta, Token, Type,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
/// A helper struct to parse the contents of the `#[secret(...)]` attribute.
/// This makes parsing robust and allows for better error handling.
struct SecretAttributeArgs {
namespace: LitStr,
key: LitStr,
value_type: Type,
}
impl Parse for SecretAttributeArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
// The attributes are parsed as a comma-separated list of `key = value` pairs.
let parsed_args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
let mut namespace = None;
let mut key = None;
let mut value_type = None;
for arg in parsed_args {
if let Meta::NameValue(nv) = arg {
let ident_str = nv.path.get_ident().map(Ident::to_string);
match ident_str.as_deref() {
Some("namespace") => {
if let syn::Expr::Lit(expr_lit) = nv.value {
if let syn::Lit::Str(lit) = expr_lit.lit {
namespace = Some(lit);
continue;
}
}
return Err(syn::Error::new_spanned(
nv.value,
"Expected a string literal for `namespace`",
));
}
Some("key") => {
if let syn::Expr::Lit(expr_lit) = nv.value {
if let syn::Lit::Str(lit) = expr_lit.lit {
key = Some(lit);
continue;
}
}
return Err(syn::Error::new_spanned(
nv.value,
"Expected a string literal for `key`",
));
}
Some("value_type") => {
if let syn::Expr::Lit(expr_lit) = nv.value {
// This is the key improvement: parse the string literal's content as a Type.
if let syn::Lit::Str(lit) = expr_lit.lit {
value_type = Some(lit.parse::<Type>()?);
continue;
}
}
// This allows for the improved syntax: `value_type = String`
if let syn::Expr::Path(expr_path) = nv.value {
value_type = Some(Type::Path(expr_path.into()));
continue;
}
return Err(syn::Error::new_spanned(
nv.value,
"Expected a type path (e.g., `String` or `Vec<u8>`) for `value_type`",
));
}
_ => {}
}
}
return Err(syn::Error::new_spanned(
arg,
"Unsupported attribute key. Must be `namespace`, `key`, or `value_type`.",
));
}
Ok(SecretAttributeArgs {
namespace: namespace.ok_or_else(|| {
syn::Error::new(input.span(), "Missing required attribute `namespace`")
})?,
key: key
.ok_or_else(|| syn::Error::new(input.span(), "Missing required attribute `key`"))?,
value_type: value_type.ok_or_else(|| {
syn::Error::new(input.span(), "Missing required attribute `value_type`")
})?,
})
}
}
#[proc_macro_derive(Secret, attributes(secret))]
pub fn derive_secret(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Ensure this is a unit struct (e.g., `struct MySecret;`)
if !matches!(&input.data, syn::Data::Struct(s) if s.fields.is_empty()) {
return syn::Error::new_spanned(
&input.ident,
"#[derive(Secret)] can only be used on unit structs.",
)
.to_compile_error()
.into();
}
// Find the `#[secret(...)]` attribute.
let secret_attr = input
.attrs
.iter()
.find(|attr| attr.path().is_ident("secret"))
.ok_or_else(|| syn::Error::new_spanned(&input.ident, "Missing `#[secret(...)]` attribute."))
.and_then(|attr| attr.parse_args::<SecretAttributeArgs>());
let args = match secret_attr {
Ok(args) => args,
Err(e) => return e.to_compile_error().into(),
};
// Find the path to the `harmony_secrets` crate to make the macro work anywhere.
let secret_crate_path = match crate_name("harmony-secrets") {
Ok(FoundCrate::Itself) => quote!(crate),
Ok(FoundCrate::Name(name)) => {
let ident = Ident::new(&name, proc_macro2::Span::call_site());
quote!(::#ident)
}
Err(e) => {
return syn::Error::new(proc_macro2::Span::call_site(), e.to_string())
.to_compile_error()
.into();
}
};
let struct_ident = &input.ident;
let namespace = args.namespace;
let key = args.key;
let value_type = args.value_type;
let expanded = quote! {
impl #secret_crate_path::Secret for #struct_ident {
type Value = #value_type;
const NAMESPACE: &'static str = #namespace;
const KEY: &'static str = #key;
}
};
TokenStream::from(expanded)
}

View File

@@ -0,0 +1,100 @@
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, Attribute, Meta};
use quote::quote;
use proc_macro_crate::crate_name;
#[proc_macro_derive(Secret, attributes(secret))]
pub fn derive_secret(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Verify this is a unit struct
if !matches!(&input.data, syn::Data::Struct(data) if data.fields.is_empty()) {
return syn::Error::new_spanned(
input.ident,
"#[derive(Secret)] only supports unit structs (e.g., `struct MySecret;`)",
)
.to_compile_error()
.into();
}
// Parse the #[secret(...)] attribute
let (namespace, key, value_type) = match parse_secret_attributes(&input.attrs) {
Ok(attrs) => attrs,
Err(e) => return e.into_compile_error().into(),
};
// Get the path to the harmony_secrets crate
let secret_crate_path = match crate_name("harmony-secrets") {
Ok(proc_macro_crate::FoundCrate::Itself) => quote!(crate),
Ok(proc_macro_crate::FoundCrate::Name(name)) => {
let ident = quote::format_ident!("{}", name);
quote!(::#ident)
}
Err(_) => {
return syn::Error::new_spanned(
&input.ident,
"harmony-secrets crate not found in dependencies",
)
.to_compile_error()
.into();
}
};
let struct_ident = input.ident;
TokenStream::from(quote! {
impl #secret_crate_path::Secret for #struct_ident {
type Value = #value_type;
const NAMESPACE: &'static str = #namespace;
const KEY: &'static str = #key;
}
})
}
fn parse_secret_attributes(attrs: &[Attribute]) -> syn::Result<(String, String, syn::Type)> {
let secret_attr = attrs
.iter()
.find(|attr| attr.path().is_ident("secret"))
.ok_or_else(|| {
syn::Error::new_spanned(
attrs.first().unwrap_or_else(|| &attrs[0]),
"missing #[secret(...)] attribute",
)
})?;
let mut namespace = None;
let mut key = None;
let mut value_type = None;
if let Meta::List(meta_list) = &secret_attr.parse_meta()? {
for nested in &meta_list.nested {
if let syn::NestedMeta::Meta(Meta::NameValue(nv)) = nested {
if nv.path.is_ident("namespace") {
if let syn::Lit::Str(lit) = &nv.lit {
namespace = Some(lit.value());
}
} else if nv.path.is_ident("key") {
if let syn::Lit::Str(lit) = &nv.lit {
key = Some(lit.value());
}
} else if nv.path.is_ident("value_type") {
if let syn::Lit::Str(lit) = &nv.lit {
value_type = Some(syn::parse_str::<syn::Type>(&lit.value())?);
}
}
}
}
}
Ok((
namespace.ok_or_else(|| {
syn::Error::new_spanned(secret_attr, "missing `namespace` in #[secret(...)]")
})?,
key.ok_or_else(|| {
syn::Error::new_spanned(secret_attr, "missing `key` in #[secret(...)]")
})?,
value_type.ok_or_else(|| {
syn::Error::new_spanned(secret_attr, "missing `value_type` in #[secret(...)]")
})?,
))
}