feat: WIP add secrets module and macro crate
This commit is contained in:
13
harmony_secrets_derive/Cargo.toml
Normal file
13
harmony_secrets_derive/Cargo.toml
Normal 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"
|
||||
152
harmony_secrets_derive/src/lib.rs
Normal file
152
harmony_secrets_derive/src/lib.rs
Normal 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)
|
||||
}
|
||||
100
harmony_secrets_derive/src/lib.rsglm45
Normal file
100
harmony_secrets_derive/src/lib.rsglm45
Normal 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(...)]")
|
||||
})?,
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user