diff --git a/Cargo.lock b/Cargo.lock index 71a1a70..d8e8d27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,7 +378,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tokio-util", "tower-service", @@ -473,7 +473,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -1823,12 +1823,23 @@ dependencies = [ "temp-dir", "temp-file", "tempfile", + "thiserror 2.0.14", "tokio", "tokio-util", "url", "uuid", ] +[[package]] +name = "harmony-secrets-derive" +version = "0.1.0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "harmony_cli" version = "0.1.0" @@ -1962,7 +1973,7 @@ dependencies = [ "non-blank-string-rs", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -2621,7 +2632,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -2722,7 +2733,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tokio-tungstenite", "tokio-util", @@ -2747,7 +2758,7 @@ dependencies = [ "serde", "serde-value", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -2785,7 +2796,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tokio-util", "tracing", @@ -3366,7 +3377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.14", "ucd-trie", ] @@ -3587,6 +3598,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -3720,7 +3740,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.14", ] [[package]] @@ -4016,7 +4036,7 @@ dependencies = [ "flurry", "log", "serde", - "thiserror 2.0.12", + "thiserror 2.0.14", "tokio", "tokio-util", ] @@ -4579,7 +4599,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.14", "time", ] @@ -4763,9 +4783,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" dependencies = [ "proc-macro2", "quote", @@ -4899,11 +4919,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.14", ] [[package]] @@ -4919,9 +4939,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", @@ -5251,7 +5271,7 @@ dependencies = [ "log", "rand 0.9.1", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.14", "utf-8", ] diff --git a/Cargo.toml b/Cargo.toml index 22645f3..3ead724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "harmony_cli", "k3d", "harmony_composer", + "harmony_secrets_derive", ] [workspace.package] diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 5a42cf7..b46ce33 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -67,6 +67,7 @@ bollard.workspace = true tar.workspace = true base64.workspace = true once_cell = "1.21.3" +thiserror = "2.0.14" [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/harmony.rlib b/harmony/harmony.rlib new file mode 100644 index 0000000..feb8a05 Binary files /dev/null and b/harmony/harmony.rlib differ diff --git a/harmony/src/domain/mod.rs b/harmony/src/domain/mod.rs index 028fa7f..4d46287 100644 --- a/harmony/src/domain/mod.rs +++ b/harmony/src/domain/mod.rs @@ -9,3 +9,4 @@ pub mod inventory; pub mod maestro; pub mod score; pub mod topology; +pub mod secrets; diff --git a/harmony/src/domain/secrets/mod.rs b/harmony/src/domain/secrets/mod.rs new file mode 100644 index 0000000..3951a90 --- /dev/null +++ b/harmony/src/domain/secrets/mod.rs @@ -0,0 +1,265 @@ +//! # Harmony Secrets Module +//! +//! This module provides core abstractions for type-safe secret management within the Harmony framework. +//! +//! ## Design Philosophy +//! +//! The design is centered around three key components: +//! +//! 1. **The `Secret` Trait:** This is the heart of the module. Instead of using strings to identify +//! secrets, we use dedicated, zero-sized types (ZSTs). Each ZST represents a single secret +//! and implements the `Secret` trait to provide metadata (like its namespace and key) and +//! its associated `Value` type. This enables full compile-time verification. +//! +//! 2. **The `Secrets` Struct:** This is the primary user-facing API. It provides the `get` and `set` +//! methods that are generic over any type implementing `Secret`. It's the high-level, +//! easy-to-use entry point for all secret operations. +//! +//! 3. **The `SecretStore` Trait:** This is the low-level backend interface. It defines the contract +//! for how the `Secrets` struct will interact with an actual storage system (like Infisical, +//! a local file, or a database). This decouples the high-level API from the implementation details. +//! +//! ## Example Usage +//! +//! ``` +//! // In an external crate (e.g., harmony-okd): +//! use harmony_secrets::{Secret, StoreError}; +//! +//! // 1. Define a zero-sized struct for each secret. +//! pub struct KubeadminPassword; +//! +//! // 2. Implement the `Secret` trait to provide metadata. +//! impl Secret for KubeadminPassword { +//! // The associated type defines what you get back. +//! type Value = String; +//! +//! const NAMESPACE: &'static str = "okd-installation"; +//! const KEY: &'static str = "kubeadmin-password"; +//! } +//! +//! // 3. Use it with the `Secrets` struct. +//! async fn example(secrets: &harmony_secrets::Secrets) -> Result<(), StoreError> { +//! // The API is type-safe. The compiler knows what `Value` to expect. +//! secrets.set::("password123".to_string()).await?; +//! let password = secrets.get::().await?; +//! assert_eq!(password, "password123"); +//! Ok(()) +//! } +//! ``` + +use async_trait::async_trait; +use serde::{de::DeserializeOwned, Serialize}; +use std::sync::Arc; +use thiserror::Error; + +/// Defines the set of errors that can occur during secret operations. +/// Using `thiserror` provides a great developer experience for error handling. +#[derive(Debug, Error)] +pub enum StoreError { + #[error("Secret not found in store: namespace='{namespace}', key='{key}'")] + NotFound { namespace: String, key: String }, + + #[error("Permission denied for secret: namespace='{namespace}', key='{key}'")] + PermissionDenied { namespace: String, key: String }, + + #[error("Failed to deserialize secret value: {0}")] + Deserialization(String), + + #[error("Failed to serialize secret value: {0}")] + Serialization(String), + + #[error("A backend-specific error occurred: {0}")] + Backend(String), +} + +/// A trait that marks a type as representing a single, retrievable secret. +/// +/// This trait should be implemented on a unique, zero-sized struct for each secret +/// your module needs to manage. This pattern ensures that all secret access is +/// validated at compile time. +pub trait Secret: 'static + Send + Sync { + /// The data type of the secret's value. This ensures that `get` and `set` + /// operations are fully type-safe. The value must be serializable. + type Value: Serialize + DeserializeOwned + Send; + + /// A logical grouping for secrets, similar to a Kubernetes namespace or a + /// directory path. This will be used by the `SecretStore` to organize data. + const NAMESPACE: &'static str; + + /// The unique key for the secret within its namespace. + const KEY: &'static str; +} + +/// The low-level storage trait that concrete secret backends must implement. +/// +/// This trait operates on raw bytes (`Vec`), keeping it decoupled from any +/// specific serialization format. The `Secrets` struct will handle the +// serialization/deserialization boundary. +#[async_trait] +pub trait SecretStore: Send + Sync { + /// Retrieves the raw byte value of a secret from the backend. + async fn get(&self, namespace: &str, key: &str) -> Result, StoreError>; + + /// Saves the raw byte value of a secret to the backend. + async fn set(&self, namespace: &str, key: &str, value: Vec) -> Result<(), StoreError>; +} + +/// The primary, user-facing struct for interacting with secrets. +/// +/// It provides a high-level, type-safe API that is decoupled from the +/// underlying storage mechanism. +#[derive(Clone)] +pub struct Secrets { + /// A shared, thread-safe reference to the underlying secret store. + store: Arc, +} + +impl Secrets { + /// Creates a new `Secrets` instance with the given store implementation. + pub fn new(store: Arc) -> Self { + Self { store } + } + + /// Retrieves a secret from the store in a fully type-safe manner. + /// + /// The type of the secret to retrieve is specified using a generic parameter `S`, + /// which must implement the `Secret` trait. The method returns the `S::Value` + /// associated type, ensuring you always get the data type you expect. + /// + /// # Example + /// `let admin_pass = secrets.get::().await?;` + pub async fn get(&self) -> Result { + let bytes = self + .store + .get(S::NAMESPACE, S::KEY) + .await + .map_err(|e| match e { + // Preserve the NotFound error for better diagnostics. + StoreError::NotFound { .. } => e, + _ => StoreError::Backend(e.to_string()), + })?; + + // The public API uses JSON for serialization. It's robust and human-readable. + serde_json::from_slice(&bytes) + .map_err(|e| StoreError::Deserialization(e.to_string())) + } + + /// Saves a secret to the store in a fully type-safe manner. + /// + /// The method is generic over the secret type `S`, and the `value` parameter + /// must match the `S::Value` associated type, preventing type mismatch errors + /// at compile time. + /// + /// # Example + /// `secrets.set::("new-password".to_string()).await?;` + pub async fn set(&self, value: S::Value) -> Result<(), StoreError> { + let bytes = serde_json::to_vec(&value) + .map_err(|e| StoreError::Serialization(e.to_string()))?; + + self.store + .set(S::NAMESPACE, S::KEY, bytes) + .await + .map_err(|e| StoreError::Backend(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::sync::Mutex; + + // Define a dummy secret for testing purposes. + struct TestApiKey; + impl Secret for TestApiKey { + type Value = String; + const NAMESPACE: &'static str = "global-tests"; + const KEY: &'static str = "api-key"; + } + + struct ComplexSecret; + #[derive(Serialize, serde::Deserialize, PartialEq, Debug, Clone)] + struct ComplexValue { + user: String, + permissions: Vec, + } + impl Secret for ComplexSecret { + type Value = ComplexValue; + const NAMESPACE: &'static str = "complex-tests"; + const KEY: &'static str = "user-data"; + } + + // A mock implementation of the `SecretStore` that uses an in-memory HashMap. + #[derive(Default)] + struct MockStore { + data: Mutex>>, + } + + #[async_trait] + impl SecretStore for MockStore { + async fn get(&self, namespace: &str, key: &str) -> Result, StoreError> { + let path = format!("{}/{}", namespace, key); + let data = self.data.lock().unwrap(); + data.get(&path) + .cloned() + .ok_or_else(|| StoreError::NotFound { + namespace: namespace.to_string(), + key: key.to_string(), + }) + } + + async fn set(&self, namespace: &str, key: &str, value: Vec) -> Result<(), StoreError> { + let path = format!("{}/{}", namespace, key); + let mut data = self.data.lock().unwrap(); + data.insert(path, value); + Ok(()) + } + } + + #[tokio::test] + async fn test_set_and_get_simple_secret() { + let store = Arc::new(MockStore::default()); + let secrets = Secrets::new(store); + + let api_key_value = "secret-key-12345".to_string(); + secrets + .set::(api_key_value.clone()) + .await + .unwrap(); + + let retrieved_key = secrets.get::().await.unwrap(); + assert_eq!(retrieved_key, api_key_value); + } + + #[tokio::test] + async fn test_set_and_get_complex_secret() { + let store = Arc::new(MockStore::default()); + let secrets = Secrets::new(store); + + let complex_value = ComplexValue { + user: "test-user".to_string(), + permissions: vec!["read".to_string(), "write".to_string()], + }; + secrets + .set::(complex_value.clone()) + .await + .unwrap(); + + let retrieved_value = secrets.get::().await.unwrap(); + assert_eq!(retrieved_value, complex_value); + } + + #[tokio::test] + async fn test_get_nonexistent_secret() { + let store = Arc::new(MockStore::default()); + let secrets = Secrets::new(store); + + let result = secrets.get::().await; + assert!(matches!(result, Err(StoreError::NotFound { .. }))); + + if let Err(StoreError::NotFound { namespace, key }) = result { + assert_eq!(namespace, TestApiKey::NAMESPACE); + assert_eq!(key, TestApiKey::KEY); + } + } +} diff --git a/harmony/src/infra/opnsense/mod.rs b/harmony/src/infra/opnsense/mod.rs index 0aa5532..5340bf4 100644 --- a/harmony/src/infra/opnsense/mod.rs +++ b/harmony/src/infra/opnsense/mod.rs @@ -22,12 +22,18 @@ pub struct OPNSenseFirewall { host: LogicalHost, } +// TODO figure out a design to have a unique identifiere for this firewall +// I think a project identifier would be good enough, then the secrets module configuration will +// point to the project's vault and this opnsense modules doesn't need to know anything about it +const OPNSENSE_CREDENTIALS: &str = "OPNSENSE_CREDENTIALS"; + impl OPNSenseFirewall { pub fn get_ip(&self) -> IpAddress { self.host.ip } pub async fn new(host: LogicalHost, port: Option, username: &str, password: &str) -> Self { + // let credentials = Secrets::get_by_name(OPNSENSE_CREDENTIALS) Self { opnsense_config: Arc::new(RwLock::new( opnsense_config::Config::from_credentials(host.ip, port, username, password).await, diff --git a/harmony/src/modules/tenant/credentials.rs.bak b/harmony/src/modules/tenant/credentials.rs.bak new file mode 100644 index 0000000..ec55b94 --- /dev/null +++ b/harmony/src/modules/tenant/credentials.rs.bak @@ -0,0 +1,51 @@ +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::{interpret::InterpretError, score::Score, topology::Topology}; + +/// Create and manage Tenant Credentials. +/// +/// This is meant to be used by cluster administrators who need to provide their tenant users and +/// services with credentials to access their resources. +#[derive(Debug, Clone, Serialize)] +pub struct TenantCredentialScore; + +impl Score for TenantCredentialScore { + fn create_interpret(&self) -> Box> { + todo!() + } + + fn name(&self) -> String { + todo!() + } +} + +#[async_trait] +pub trait TenantCredentialManager { + async fn create_user(&self) -> Result; +} + +#[derive(Debug, Clone)] +pub struct CredentialMetadata { + pub tenant_id: String, + pub credential_id: String, + pub description: String, + pub created_at: DateTime, + pub expires_at: Option>, +} + +#[derive(Debug, Clone)] +pub enum CredentialData { + /// Used to store login instructions destined to a human. Akin to AWS login instructions email + /// upon new console user creation. + PlainText(String), +} + + +pub struct TenantCredentialBundle { + _metadata: CredentialMetadata, + _content: CredentialData, +} + +impl TenantCredentialBundle {} diff --git a/harmony_secrets_derive/Cargo.toml b/harmony_secrets_derive/Cargo.toml new file mode 100644 index 0000000..5a8bcfe --- /dev/null +++ b/harmony_secrets_derive/Cargo.toml @@ -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" diff --git a/harmony_secrets_derive/src/lib.rs b/harmony_secrets_derive/src/lib.rs new file mode 100644 index 0000000..cd6efa1 --- /dev/null +++ b/harmony_secrets_derive/src/lib.rs @@ -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 { + // The attributes are parsed as a comma-separated list of `key = value` pairs. + let parsed_args = Punctuated::::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::()?); + 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`) 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::()); + + 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) +} diff --git a/harmony_secrets_derive/src/lib.rsglm45 b/harmony_secrets_derive/src/lib.rsglm45 new file mode 100644 index 0000000..c4b3439 --- /dev/null +++ b/harmony_secrets_derive/src/lib.rsglm45 @@ -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::(&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(...)]") + })?, + )) +}