diff --git a/objectstore-options/src/lib.rs b/objectstore-options/src/lib.rs index 1684d0e9..5bc4a76d 100644 --- a/objectstore-options/src/lib.rs +++ b/objectstore-options/src/lib.rs @@ -39,7 +39,7 @@ pub fn init() -> Result<(), Error> { /// /// Obtain a snapshot of the current options via [`Options::get`]. Before calling `get`, /// the global instance must be initialized with [`Options::init`]. -#[derive(Debug, SentryOptions)] +#[derive(Debug, Deserialize, SentryOptions)] #[sentry_options(namespace = "objectstore", path = "../../sentry-options")] pub struct Options { /// Active killswitches that may disable access to specific object contexts. diff --git a/objectstore-typed-options-derive/src/lib.rs b/objectstore-typed-options-derive/src/lib.rs index 2ed4c23e..1faad298 100644 --- a/objectstore-typed-options-derive/src/lib.rs +++ b/objectstore-typed-options-derive/src/lib.rs @@ -8,25 +8,19 @@ use syn::{DeriveInput, Fields, LitStr, parse_macro_input}; /// Derives the `SentryOptions` trait and generates runtime option machinery. /// +/// The struct must also derive `serde::Deserialize` — deserialization is handled by +/// the default trait method, which builds a map from schema properties and delegates to +/// serde. Use `#[serde(rename = "...")]` when a field name differs from its schema key. +/// /// # Container attributes /// /// - `namespace` — the sentry-options namespace (e.g. `"objectstore"`) /// - `path` — relative path to the `sentry-options/` directory; the schema is resolved as /// `{path}/schemas/{namespace}/schema.json` /// -/// # Field attributes -/// -/// - `rename` — the option key to look up for this field, overriding the default (the field's -/// Rust identifier). Use it when the schema key differs from the field name, e.g. -/// `#[sentry_options(rename = "max-retries")] max_retries: u32`. -/// /// # Generated code /// -/// For each struct field, `deserialize` calls `Deserialize::deserialize(options.get(NAMESPACE, -/// "")?)`, where `` is the `rename` value if present and otherwise the field name. -/// -/// Additionally generates: -/// - `SentryOptions` trait impl with `NAMESPACE`, `SCHEMA`, and `deserialize` +/// - `SentryOptions` trait impl with `NAMESPACE` and `SCHEMA` constants /// - Inherent `get() -> Arc`, `init() -> Result<(), Error>`, `refresh()`, and (under /// `testing` feature) `override_with()` /// - `OnceLock` statics for the singleton snapshot (`ArcSwap`) and the raw sentry-options @@ -78,38 +72,13 @@ fn parse_attrs(input: &DeriveInput) -> syn::Result { Ok(Attrs { namespace, path }) } -/// Returns the option key for a field: the `#[sentry_options(rename = "...")]` value if present, -/// otherwise the field's Rust identifier. -fn parse_field_key(field: &syn::Field) -> syn::Result { - let field_name = field.ident.as_ref().expect("named field"); - let mut rename: Option = None; - - for attr in &field.attrs { - if !attr.path().is_ident("sentry_options") { - continue; - } - - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("rename") { - let value = meta.value()?; - rename = Some(value.parse::()?); - Ok(()) - } else { - Err(meta.error("unknown sentry_options field attribute")) - } - })?; - } - - Ok(rename.map_or_else(|| field_name.to_string(), |lit| lit.value())) -} - fn expand(input: DeriveInput) -> syn::Result { let attrs = parse_attrs(&input)?; let name = &input.ident; - let fields = match &input.data { + match &input.data { syn::Data::Struct(data) => match &data.fields { - Fields::Named(named) => &named.named, + Fields::Named(_) => {} _ => { return Err(syn::Error::new( name.span(), @@ -128,20 +97,6 @@ fn expand(input: DeriveInput) -> syn::Result { let namespace_str = &attrs.namespace; let path_str = &attrs.path; - // Build deserialize body: one line per field. - let field_deserializations = fields - .iter() - .map(|f| { - let field_name = f.ident.as_ref().expect("named field"); - let field_key = parse_field_key(f)?; - Ok(quote! { - #field_name: ::objectstore_typed_options::serde::Deserialize::deserialize( - options.get(Self::NAMESPACE, #field_key)? - )? - }) - }) - .collect::>>()?; - Ok(quote! { const _: () = { static __OPTIONS: ::std::sync::OnceLock< @@ -158,14 +113,6 @@ fn expand(input: DeriveInput) -> syn::Result { const SCHEMA: &str = include_str!( concat!(#path_str, "/schemas/", #namespace_str, "/schema.json") ); - - fn deserialize( - options: &::objectstore_typed_options::sentry_options::Options, - ) -> ::std::result::Result { - ::std::result::Result::Ok(Self { - #(#field_deserializations),* - }) - } } impl #name { @@ -202,9 +149,10 @@ fn expand(input: DeriveInput) -> syn::Result { .build() .expect("options schema should be valid"); - ::std::sync::Arc::new( - Self::deserialize(&inner).expect("failed to deserialize options"), - ) + let deserialized = + <#name as ::objectstore_typed_options::SentryOptions>::deserialize(&inner) + .expect("failed to deserialize options"); + ::std::sync::Arc::new(deserialized) } /// Reloads options from sentry-options and atomically swaps in the new @@ -251,9 +199,11 @@ fn expand(input: DeriveInput) -> syn::Result { .with_schemas(&[(Self::NAMESPACE, Self::SCHEMA)]) .build()?; + let deserialized = + <#name as ::objectstore_typed_options::SentryOptions>::deserialize(&inner)?; __OPTIONS .set(::objectstore_typed_options::arc_swap::ArcSwap::from_pointee( - Self::deserialize(&inner)?, + deserialized, )) .map_err(|_| { ::objectstore_typed_options::sentry_options::OptionsError::AlreadyInitialized diff --git a/objectstore-typed-options/src/lib.rs b/objectstore-typed-options/src/lib.rs index 4b3eee39..988979c8 100644 --- a/objectstore-typed-options/src/lib.rs +++ b/objectstore-typed-options/src/lib.rs @@ -13,9 +13,9 @@ //! - `path` — relative path (from the source file) to the `sentry-options/` directory; //! the schema is resolved as `{path}/schemas/{namespace}/schema.json` //! -//! Each struct field must implement [`serde::Deserialize`] and corresponds to a key of the -//! same name within the namespace. To look up a key that differs from the field name, annotate -//! the field with `#[sentry_options(rename = "...")]`. +//! The struct must implement [`serde::Deserialize`] (typically via `#[derive(Deserialize)]`). +//! Each field corresponds to a schema property of the same name within the namespace. To +//! look up a key that differs from the field name, use `#[serde(rename = "...")]`. //! //! # Generated API //! @@ -37,7 +37,8 @@ //! //! ```rust,no_run //! # use objectstore_typed_options::SentryOptions; -//! # #[derive(Debug, SentryOptions)] +//! # use serde::Deserialize; +//! # #[derive(Debug, Deserialize, SentryOptions)] //! # #[sentry_options(namespace = "objectstore", path = "../../sentry-options")] //! # struct Options {} //! #[cfg(test)] @@ -55,16 +56,17 @@ //! //! ```rust,no_run //! use objectstore_typed_options::SentryOptions; +//! use serde::Deserialize; //! //! // `path` points to the sentry-options directory; the schema is resolved as //! // `{path}/schemas/{namespace}/schema.json` and embedded at compile time. -//! #[derive(Debug, SentryOptions)] +//! #[derive(Debug, Deserialize, SentryOptions)] //! #[sentry_options( //! namespace = "objectstore", //! path = "../../sentry-options" //! )] //! pub struct Options { -//! #[sentry_options(rename = "retries")] +//! #[serde(rename = "retries")] //! max_retries: u32, //! allowed_orgs: Vec, //! } @@ -104,12 +106,14 @@ pub enum Error { Options(#[from] sentry_options::OptionsError), #[error("failed to deserialize option value")] Deserialize(#[from] serde_json::Error), + #[error("invalid options schema: {0}")] + InvalidSchema(String), } /// Trait implemented by option structs that are backed by sentry-options. /// /// Typically derived via `#[derive(SentryOptions)]` rather than implemented manually. -pub trait SentryOptions: Sized + Send + Sync + 'static { +pub trait SentryOptions: serde::de::DeserializeOwned + Send + Sync + 'static { /// The sentry-options namespace for this type. const NAMESPACE: &str; @@ -117,5 +121,27 @@ pub trait SentryOptions: Sized + Send + Sync + 'static { const SCHEMA: &str; /// Deserializes an instance from the loaded sentry-options values. - fn deserialize(options: &sentry_options::Options) -> Result; + /// + /// The default implementation parses [`SCHEMA`](Self::SCHEMA) to discover property + /// keys, fetches each value via [`Options::get`](sentry_options::Options::get), and + /// deserializes the resulting map into `Self` using serde. Field-level renames should + /// use `#[serde(rename = "...")]`. + fn deserialize(options: &sentry_options::Options) -> Result { + let schema: serde_json::Value = + serde_json::from_str(Self::SCHEMA).map_err(|e| Error::InvalidSchema(e.to_string()))?; + + let properties = schema + .get("properties") + .and_then(|p| p.as_object()) + .ok_or_else(|| Error::InvalidSchema("missing \"properties\" object".into()))?; + + let mut map = serde_json::Map::with_capacity(properties.len()); + for key in properties.keys() { + let value = options.get(Self::NAMESPACE, key)?; + map.insert(key.clone(), value); + } + + let result = serde_json::from_value(serde_json::Value::Object(map))?; + Ok(result) + } }