diff --git a/Cargo.toml b/Cargo.toml index e388b96..8fe6ad7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = ["crates/aerro", "crates/aerro-macros"] # (Reserved for workspace-wide clippy overrides; currently none.) [workspace.package] -version = "0.6.4" +version = "0.7.0" edition = "2024" rust-version = "1.85" license = "MIT OR Apache-2.0" diff --git a/crates/aerro/Cargo.toml b/crates/aerro/Cargo.toml index 60252b4..a06c3e6 100644 --- a/crates/aerro/Cargo.toml +++ b/crates/aerro/Cargo.toml @@ -18,7 +18,7 @@ macro = ["dep:aerro-macros"] tracing = ["dep:tracing", "dep:tracing-opentelemetry", "dep:opentelemetry"] [dependencies] -aerro-macros = { path = "../aerro-macros", version = "0.6.2", optional = true } +aerro-macros = { path = "../aerro-macros", version = "0.7.0", optional = true } tonic = { workspace = true } prost = { workspace = true } bytes = { workspace = true } diff --git a/crates/aerro/examples/trace_chain.rs b/crates/aerro/examples/trace_chain.rs index c240d57..3132c08 100644 --- a/crates/aerro/examples/trace_chain.rs +++ b/crates/aerro/examples/trace_chain.rs @@ -24,7 +24,7 @@ fn middleware() -> Result<(), tonic::Status> { Ok(v) => Ok(v), Err(e) => { let mut sf: ServiceFailure = e.into(); - sf.frames.push(Frame::local( + sf.frames_mut().push(Frame::local( "backend", "ping", Code::Internal, @@ -42,7 +42,7 @@ fn gateway() -> Result<(), tonic::Status> { Ok(v) => Ok(v), Err(st) => { let mut sf = st.into_aerro::().expect("typed"); - sf.frames.push(Frame::local( + sf.frames_mut().push(Frame::local( "middleware", "forward", Code::Internal, @@ -58,7 +58,7 @@ fn main() { let st = gateway().unwrap_err(); let sf = st.into_aerro::().expect("typed at gateway"); let mut sf = sf; - sf.frames.push(Frame::local( + sf.frames_mut().push(Frame::local( "gateway", "handle", Code::Internal, @@ -66,11 +66,11 @@ fn main() { Category::System, )); println!("frames on final hop:"); - for (i, f) in sf.frames.iter().enumerate() { + for (i, f) in sf.frames().iter().enumerate() { println!( " {i}: service={} rpc={} code={:?} msg={}", f.service, f.rpc, f.code, f.message ); } - println!("trace_id={:02x?}", sf.trace.trace_id); + println!("trace_id={:02x?}", sf.trace().trace_id); } diff --git a/crates/aerro/src/ext.rs b/crates/aerro/src/ext.rs index b4b2b54..94cc47a 100644 --- a/crates/aerro/src/ext.rs +++ b/crates/aerro/src/ext.rs @@ -50,6 +50,6 @@ mod tests { fn status_into_aerro_recovers() { let st = Boom { x: 2 }.into_status(&EncodeOptions::default()); let sf = st.into_aerro::().unwrap(); - assert_eq!(sf.inner.x, 2); + assert_eq!(sf.inner().x, 2); } } diff --git a/crates/aerro/src/failure.rs b/crates/aerro/src/failure.rs index 41ce351..e66dd59 100644 --- a/crates/aerro/src/failure.rs +++ b/crates/aerro/src/failure.rs @@ -10,14 +10,11 @@ use smallvec::SmallVec; use crate::{Aerro, Frame, trace::TraceContext}; -/// Boxed state of [`ServiceFailure`]. Exposed only so that field access -/// through `Deref`/`DerefMut` (`sf.inner`, `sf.frames`, `sf.trace`) names a -/// public type; you almost never write this name directly. #[derive(Debug)] -pub struct ServiceFailureInner { - pub inner: E, - pub frames: SmallVec<[Frame; 4]>, - pub trace: TraceContext, +pub(crate) struct ServiceFailureInner { + pub(crate) inner: E, + pub(crate) frames: SmallVec<[Frame; 4]>, + pub(crate) trace: TraceContext, } #[derive(Debug)] @@ -64,18 +61,29 @@ impl ServiceFailure { } = *self.state; (inner, frames, trace) } -} -impl std::ops::Deref for ServiceFailure { - type Target = ServiceFailureInner; - fn deref(&self) -> &ServiceFailureInner { - &self.state + pub fn inner(&self) -> &E { + &self.state.inner + } + + pub fn inner_mut(&mut self) -> &mut E { + &mut self.state.inner + } + + pub fn frames(&self) -> &SmallVec<[Frame; 4]> { + &self.state.frames + } + + pub fn frames_mut(&mut self) -> &mut SmallVec<[Frame; 4]> { + &mut self.state.frames + } + + pub fn trace(&self) -> &TraceContext { + &self.state.trace } -} -impl std::ops::DerefMut for ServiceFailure { - fn deref_mut(&mut self) -> &mut ServiceFailureInner { - &mut self.state + pub fn trace_mut(&mut self) -> &mut TraceContext { + &mut self.state.trace } } @@ -105,8 +113,8 @@ mod tests { #[test] fn from_e_constructs_failure_with_empty_frames() { let sf: ServiceFailure = Boom { x: 1 }.into(); - assert!(sf.frames.is_empty()); - assert!(sf.trace.is_empty()); + assert!(sf.frames().is_empty()); + assert!(sf.trace().is_empty()); } #[test] diff --git a/crates/aerro/src/remote.rs b/crates/aerro/src/remote.rs index 1d59e6b..147bfbd 100644 --- a/crates/aerro/src/remote.rs +++ b/crates/aerro/src/remote.rs @@ -9,28 +9,14 @@ use tonic::Code; use crate::{Aerro, Category, Frame, trace::TraceContext}; -/// Boxed state of [`RemoteError`]. Exposed only so field access through -/// `Deref`/`DerefMut` (`e.type_id`, `e.frames`, ...) names a public type. #[derive(Debug)] pub struct RemoteErrorInner { - pub category: Category, - pub type_id: String, - pub frames: SmallVec<[Frame; 4]>, - pub trace: TraceContext, - pub outer_code: Code, - pub outer_message: String, - pub(crate) payload_bytes: Bytes, -} - -/// Components used to construct a [`RemoteError`] via [`RemoteError::from_parts`]. -#[derive(Debug)] -pub struct RemoteErrorParts { - pub category: Category, - pub type_id: String, - pub frames: SmallVec<[Frame; 4]>, - pub trace: TraceContext, - pub outer_code: Code, - pub outer_message: String, + pub(crate) category: Category, + pub(crate) type_id: String, + pub(crate) frames: SmallVec<[Frame; 4]>, + pub(crate) trace: TraceContext, + pub(crate) outer_code: Code, + pub(crate) outer_message: String, pub(crate) payload_bytes: Bytes, } @@ -40,26 +26,9 @@ pub struct RemoteError { } impl RemoteError { - pub fn from_parts(parts: RemoteErrorParts) -> Self { - let RemoteErrorParts { - category, - type_id, - frames, - trace, - outer_code, - outer_message, - payload_bytes, - } = parts; + pub fn from_parts(inner: RemoteErrorInner) -> Self { Self { - state: Box::new(RemoteErrorInner { - category, - type_id, - frames, - trace, - outer_code, - outer_message, - payload_bytes, - }), + state: Box::new(inner), } } @@ -70,18 +39,29 @@ impl RemoteError { } E::decode_payload(&self.state.type_id, &self.state.payload_bytes).ok() } -} -impl std::ops::Deref for RemoteError { - type Target = RemoteErrorInner; - fn deref(&self) -> &RemoteErrorInner { - &self.state + pub fn category(&self) -> Category { + self.state.category + } + + pub fn type_id(&self) -> &str { + &self.state.type_id + } + + pub fn frames(&self) -> &SmallVec<[Frame; 4]> { + &self.state.frames + } + + pub fn trace(&self) -> &TraceContext { + &self.state.trace + } + + pub fn outer_code(&self) -> Code { + self.state.outer_code } -} -impl std::ops::DerefMut for RemoteError { - fn deref_mut(&mut self) -> &mut RemoteErrorInner { - &mut self.state + pub fn outer_message(&self) -> &str { + &self.state.outer_message } } @@ -98,8 +78,8 @@ mod tests { use super::*; use crate::test_support::Boom; - fn make(parts: RemoteErrorParts) -> RemoteError { - RemoteError::from_parts(parts) + fn make(inner: RemoteErrorInner) -> RemoteError { + RemoteError::from_parts(inner) } #[test] @@ -109,7 +89,7 @@ mod tests { Boom { x: 7 } .encode_payload(Exposure::Internal, &mut buf) .unwrap(); - let r = make(RemoteErrorParts { + let r = make(RemoteErrorInner { category: Category::System, type_id: "toy.boom".into(), frames: SmallVec::new(), @@ -123,7 +103,7 @@ mod tests { #[test] fn downcast_returns_none_on_mismatch() { - let r = make(RemoteErrorParts { + let r = make(RemoteErrorInner { category: Category::Business, type_id: "other".into(), frames: SmallVec::new(), diff --git a/crates/aerro/src/wire/decode.rs b/crates/aerro/src/wire/decode.rs index be53d5d..5beb6c4 100644 --- a/crates/aerro/src/wire/decode.rs +++ b/crates/aerro/src/wire/decode.rs @@ -44,7 +44,7 @@ fn into_remote_error(env: raw::Envelope, status: &Status) -> RemoteError { from_proto(raw::Category::try_from(env.category).unwrap_or(raw::Category::System)); let trace = decode_trace(&env.trace_id, &env.span_id); let frames = decode_frames(&env.frames); - RemoteError::from_parts(crate::remote::RemoteErrorParts { + RemoteError::from_parts(crate::remote::RemoteErrorInner { category, type_id: env.type_id, frames, @@ -56,7 +56,7 @@ fn into_remote_error(env: raw::Envelope, status: &Status) -> RemoteError { } fn transport_remote_error(status: &Status) -> RemoteError { - RemoteError::from_parts(crate::remote::RemoteErrorParts { + RemoteError::from_parts(crate::remote::RemoteErrorInner { category: Category::Transport, type_id: "aerro.transport".into(), frames: SmallVec::new(), @@ -116,7 +116,7 @@ mod tests { let sf: ServiceFailure = Boom { x: 13 }.into(); let st = encode(&sf, &EncodeOptions::default()); let back = decode::(st).expect("known type round-trips"); - assert_eq!(back.inner.x, 13); + assert_eq!(back.inner().x, 13); } #[test] @@ -150,7 +150,7 @@ mod tests { } } let r = decode::(st).expect_err("type_id mismatch"); - assert_eq!(r.type_id, "toy.boom"); + assert_eq!(r.type_id(), "toy.boom"); assert_eq!(r.downcast::().unwrap().x, 5); } @@ -158,7 +158,7 @@ mod tests { fn bare_status_becomes_transport_remote_error() { let st = Status::unavailable("backend down"); let r = decode::(st).err().unwrap(); - assert_eq!(r.category, Category::Transport); - assert_eq!(r.outer_code, Code::Unavailable); + assert_eq!(r.category(), Category::Transport); + assert_eq!(r.outer_code(), Code::Unavailable); } } diff --git a/crates/aerro/src/wire/encode.rs b/crates/aerro/src/wire/encode.rs index 76d84e7..036f622 100644 --- a/crates/aerro/src/wire/encode.rs +++ b/crates/aerro/src/wire/encode.rs @@ -36,25 +36,25 @@ pub fn encode(sf: &ServiceFailure, opts: &EncodeOptions) -> Status // route is what governs what leaves the process. let route = opts.exposure; - let outer_code = sf.inner.code(); - let outer_msg = redact_message(&sf.inner, route); + let outer_code = sf.inner().code(); + let outer_msg = redact_message(sf.inner(), route); let mut payload = Vec::new(); - if let Err(e) = sf.inner.encode_payload(route, &mut payload) { + if let Err(e) = sf.inner().encode_payload(route, &mut payload) { return Status::new(Code::Internal, format!("aerro: encode failed: {e}")); } let wire_frames = if route == Exposure::Public { Vec::new() } else { - elide_to_cap(&sf.frames, opts.max_frames) + elide_to_cap(sf.frames(), opts.max_frames) }; let env = raw::Envelope { - category: to_proto(sf.inner.category()) as i32, - type_id: Aerro::type_id(&sf.inner).to_string(), - trace_id: sf.trace.trace_id.to_vec(), - span_id: sf.trace.span_id.to_vec(), + category: to_proto(sf.inner().category()) as i32, + type_id: Aerro::type_id(sf.inner()).to_string(), + trace_id: sf.trace().trace_id.to_vec(), + span_id: sf.trace().span_id.to_vec(), frames: wire_frames, payload, version: ENVELOPE_VERSION, @@ -142,7 +142,7 @@ mod tests { fn elision_keeps_cap() { let mut sf: ServiceFailure = Boom { x: 0 }.into(); for i in 0..20u32 { - sf.frames.push(Frame::local( + sf.frames_mut().push(Frame::local( "svc", "rpc", Code::Internal, @@ -165,7 +165,7 @@ mod tests { #[test] fn public_drops_frames() { let mut sf: ServiceFailure = Boom { x: 0 }.into(); - sf.frames.push(Frame::local( + sf.frames_mut().push(Frame::local( "svc", "rpc", Code::Internal, diff --git a/crates/aerro/tests/macro_basic.rs b/crates/aerro/tests/macro_basic.rs index 8a8f6e0..0fc4be6 100644 --- a/crates/aerro/tests/macro_basic.rs +++ b/crates/aerro/tests/macro_basic.rs @@ -109,5 +109,5 @@ fn tuple_variant_roundtrips_via_wire() { fn unit_variant_roundtrips_via_wire() { let st = CreateUser::Boom.into_status(&EncodeOptions::default()); let sf: ServiceFailure = st.into_aerro::().unwrap(); - assert!(matches!(sf.inner, CreateUser::Boom)); + assert!(matches!(sf.inner(), CreateUser::Boom)); } diff --git a/crates/aerro/tests/roundtrip.rs b/crates/aerro/tests/roundtrip.rs index efcb963..74894b2 100644 --- a/crates/aerro/tests/roundtrip.rs +++ b/crates/aerro/tests/roundtrip.rs @@ -45,7 +45,7 @@ fn validation_public_roundtrips_payload_and_keeps_message() { assert_eq!(st.code(), Code::InvalidArgument); assert_eq!(st.message(), "val: bad"); // Validation is safe at Public. let sf = st.into_aerro::().unwrap(); - assert!(matches!(sf.inner, Suite::Val(_))); + assert!(matches!(sf.inner(), Suite::Val(_))); } #[test] @@ -55,7 +55,7 @@ fn system_public_redacts_message_but_payload_still_decodes() { assert_eq!(st.message(), "internal error"); // The envelope still carries the type_id and decodes typed: let sf = st.into_aerro::().unwrap(); - assert_eq!(sf.inner.category(), Category::System); + assert_eq!(sf.inner().category(), Category::System); } #[test] @@ -69,7 +69,7 @@ fn transport_trusted_keeps_message() { fn public_drops_frames_internal_keeps_them() { use aerro::{Frame, ServiceFailure}; let mut sf: ServiceFailure = Suite::Sys.into(); - sf.frames.push(Frame::local( + sf.frames_mut().push(Frame::local( "svc", "rpc", Code::Internal, @@ -79,11 +79,11 @@ fn public_drops_frames_internal_keeps_them() { let st_pub = sf.clone_for_test().into_status(&opts(Exposure::Public)); let pub_decoded = st_pub.into_aerro::().unwrap(); - assert!(pub_decoded.frames.is_empty(), "Public must drop frames"); + assert!(pub_decoded.frames().is_empty(), "Public must drop frames"); let st_int = sf.into_status(&opts(Exposure::Internal)); let int_decoded = st_int.into_aerro::().unwrap(); - assert_eq!(int_decoded.frames.len(), 1, "Internal must keep frames"); + assert_eq!(int_decoded.frames().len(), 1, "Internal must keep frames"); } // Helper because ServiceFailure isn't Clone; reconstruct manually. @@ -92,17 +92,17 @@ trait CloneForTest { } impl CloneForTest for aerro::ServiceFailure { fn clone_for_test(&self) -> Self { - let mut clone: aerro::ServiceFailure = match &self.inner { + let mut clone: aerro::ServiceFailure = match self.inner() { Suite::Biz(s) => Suite::Biz(s.clone()), Suite::Val(s) => Suite::Val(s.clone()), Suite::Sys => Suite::Sys, Suite::Trans => Suite::Trans, } .into(); - for f in &self.frames { - clone.frames.push(f.clone()); + for f in self.frames() { + clone.frames_mut().push(f.clone()); } - clone.trace = self.trace; + *clone.trace_mut() = *self.trace(); clone } }