Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion objectstore-options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
78 changes: 14 additions & 64 deletions objectstore-typed-options-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
/// "<key>")?)`, where `<key>` 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<Self>`, `init() -> Result<(), Error>`, `refresh()`, and (under
/// `testing` feature) `override_with()`
/// - `OnceLock` statics for the singleton snapshot (`ArcSwap<T>`) and the raw sentry-options
Expand Down Expand Up @@ -78,38 +72,13 @@ fn parse_attrs(input: &DeriveInput) -> syn::Result<Attrs> {
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<String> {
let field_name = field.ident.as_ref().expect("named field");
let mut rename: Option<LitStr> = 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::<LitStr>()?);
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<proc_macro2::TokenStream> {
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(),
Expand All @@ -128,20 +97,6 @@ fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
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::<syn::Result<Vec<_>>>()?;

Ok(quote! {
const _: () = {
static __OPTIONS: ::std::sync::OnceLock<
Expand All @@ -158,14 +113,6 @@ fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
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<Self, ::objectstore_typed_options::Error> {
::std::result::Result::Ok(Self {
#(#field_deserializations),*
})
}
}

impl #name {
Expand Down Expand Up @@ -202,9 +149,10 @@ fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
.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
Expand Down Expand Up @@ -251,9 +199,11 @@ fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
.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
Expand Down
42 changes: 34 additions & 8 deletions objectstore-typed-options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand All @@ -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)]
Expand All @@ -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<u32>,
//! }
Expand Down Expand Up @@ -104,18 +106,42 @@ 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;

/// The raw JSON schema string (embedded at compile time via `include_str!`).
const SCHEMA: &str;

/// Deserializes an instance from the loaded sentry-options values.
fn deserialize(options: &sentry_options::Options) -> Result<Self, Error>;
///
/// 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<Self, Error> {
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)
}
}
Loading