diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2033e8337d9..142f03f13f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: - uses: ./.github/actions/warm-up-repo - name: Create Release Pull Request or Publish to npm - uses: changesets/action@3de3850952bec538fde60aac71731376e57b9b57 # v1.4.8 + uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9 with: publish: yarn changeset publish env: diff --git a/Cargo.lock b/Cargo.lock index 3db9cb1203d..e8a37007f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2630,6 +2630,7 @@ dependencies = [ "pin-project-lite", "serde", "serde_json", + "thiserror", "tokio", ] @@ -2678,6 +2679,7 @@ dependencies = [ name = "harpc-server" version = "0.0.0" dependencies = [ + "bytes", "derive-where", "derive_more 1.0.0", "error-stack", @@ -6094,9 +6096,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553f8299af7450cda9a52d3a370199904e7a46b5ffd1bef187c4a6af3bb6db69" +checksum = "f2c1f7fc6deb21665a9060dfc7d271be784669295a31babdcd4dd2c79ae8cbfb" dependencies = [ "sdd", ] @@ -7232,6 +7234,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aaec3aedb7e..d9969fdf216 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,7 +185,7 @@ proptest = { version = "=1.5.0", default-features = false, features = ["alloc"] rand = { version = "=0.8.5", default-features = false } refinery = { version = "=0.8.14", default-features = false } rustc_version = { version = "=0.4.1", default-features = false } -scc = { version = "=2.2.1", default-features = false } +scc = { version = "=2.2.2", default-features = false } sentry = { version = "=0.34.0", default-features = false, features = ["backtrace", "contexts", "debug-images", "panic", "reqwest", "rustls", "tracing", "tower-http"] } seq-macro = { version = "=0.3.5", default-features = false } serde_plain = { version = "=1.0.2", default-features = false } diff --git a/libs/@local/harpc/codec/Cargo.toml b/libs/@local/harpc/codec/Cargo.toml index 6934750daa2..31041ec4e94 100644 --- a/libs/@local/harpc/codec/Cargo.toml +++ b/libs/@local/harpc/codec/Cargo.toml @@ -25,6 +25,7 @@ futures-util = { workspace = true, optional = true } serde_json = { workspace = true, optional = true, public = true } pin-project-lite = { workspace = true, optional = true } memchr = { workspace = true, optional = true } +thiserror = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/libs/@local/harpc/codec/src/json.rs b/libs/@local/harpc/codec/src/json.rs index d386346db99..8c41bf87230 100644 --- a/libs/@local/harpc/codec/src/json.rs +++ b/libs/@local/harpc/codec/src/json.rs @@ -5,11 +5,11 @@ use core::{ }; use bytes::{Buf, BufMut, Bytes, BytesMut}; -use error_stack::Report; +use error_stack::{Report, ResultExt}; use futures_core::Stream; use futures_util::stream::StreamExt; use harpc_types::error_code::ErrorCode; -use serde::{de::DeserializeOwned, ser::Error as _}; +use serde::de::DeserializeOwned; use crate::{ decode::{Decoder, ErrorDecoder}, @@ -17,6 +17,14 @@ use crate::{ error::{EncodedError, ErrorBuffer, NetworkError}, }; +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, thiserror::Error)] +pub enum JsonError { + #[error("unable to encode JSON value")] + Encode, + #[error("unable to decode JSON value")] + Decode, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct JsonCodec; @@ -27,7 +35,7 @@ impl JsonCodec { impl Encoder for JsonCodec { type Buf = Bytes; - type Error = serde_json::Error; + type Error = Report; fn encode( self, @@ -40,17 +48,19 @@ impl Encoder for JsonCodec { let buf = BytesMut::new(); let mut writer = buf.writer(); - serde_json::to_writer(&mut writer, &item).map(|()| { - let mut buf = writer.into_inner(); - buf.put_u8(Self::SEPARATOR); - buf.freeze() - }) + serde_json::to_writer(&mut writer, &item) + .map(|()| { + let mut buf = writer.into_inner(); + buf.put_u8(Self::SEPARATOR); + buf.freeze() + }) + .change_context(JsonError::Encode) }) } } impl Decoder for JsonCodec { - type Error = serde_json::Error; + type Error = Report; fn decode( self, @@ -104,13 +114,13 @@ where B: Buf, T: DeserializeOwned, { - type Item = Result; + type Item = Result>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // check if we have a full message in the buffer, in theory multiple messages could be in // the buffer at the same time, this guards against that. if let Some(value) = Self::poll_item(self.as_mut(), 0) { - return Poll::Ready(Some(value)); + return Poll::Ready(Some(value.change_context(JsonError::Decode))); } loop { @@ -123,7 +133,7 @@ where // the underlying stream has already returned `None`, we now only flush the // remaining buffer. if let Some(value) = Self::poll_item(self.as_mut(), 0) { - return Poll::Ready(Some(value)); + return Poll::Ready(Some(value.change_context(JsonError::Decode))); } return Poll::Ready(None); @@ -135,7 +145,7 @@ where // potentially we still have items in the buffer, try to decode them. if let Some(value) = Self::poll_item(self.as_mut(), 0) { - return Poll::Ready(Some(value)); + return Poll::Ready(Some(value.change_context(JsonError::Decode))); } return Poll::Ready(None); @@ -148,16 +158,21 @@ where offset } // TODO: we lose quite a bit of information here, any way to retrieve it? + // The problem is that we don't know if the underlying error is a report, **or** if + // it is a plain error. + // in **theory** we could do: `impl Into> + Debug + Display`, but then we + // don't know what `C` should be. Err(_error) => { - return Poll::Ready(Some(Err(serde_json::Error::custom( - "underlying stream returned an error", - )))); + // return Poll::Ready(Some(Err(serde_json::Error::custom( + // "underlying stream returned an error", + // )))); + return Poll::Ready(Some(Err(Report::new(JsonError::Decode)))); } }; // look if we found a separator between the offset and the end of the buffer if let Some(value) = Self::poll_item(self.as_mut(), offset) { - return Poll::Ready(Some(value)); + return Poll::Ready(Some(value.change_context(JsonError::Decode))); } // if not we continue to the next iteration @@ -166,7 +181,7 @@ where } #[derive(Debug, serde::Serialize, serde::Deserialize)] -struct JsonError { +struct JsonErrorRepr { message: String, details: T, } @@ -181,7 +196,7 @@ impl ErrorEncoder for JsonCodec { let buffer = ErrorBuffer::error(); let mut writer = buffer.writer(); - if let Err(error) = serde_json::to_writer(&mut writer, &JsonError { + if let Err(error) = serde_json::to_writer(&mut writer, &JsonErrorRepr { message: error.to_string(), details: error, }) { @@ -235,7 +250,7 @@ impl ErrorDecoder for JsonCodec { let bytes: BytesMut = bytes.collect().await; let bytes = bytes.freeze(); - serde_json::from_slice::>(&bytes).map(|error| error.details) + serde_json::from_slice::>(&bytes).map(|error| error.details) } async fn decode_report( @@ -446,7 +461,7 @@ mod tests { )))); let mut decoder = JsonCodec.decode::(input); - decoder + let _report = decoder .next() .await .expect("should have a value") diff --git a/libs/@local/harpc/net/Cargo.toml b/libs/@local/harpc/net/Cargo.toml index da6301ab517..20356ee51cc 100644 --- a/libs/@local/harpc/net/Cargo.toml +++ b/libs/@local/harpc/net/Cargo.toml @@ -46,7 +46,7 @@ scc = { workspace = true } serde = { workspace = true, features = ["derive"] } tachyonix = { workspace = true } thiserror = { workspace = true } -tokio-stream = { workspace = true, features = ["time"] } +tokio-stream = { workspace = true, features = ["time", "sync"] } tokio-util = { workspace = true, features = ["codec", "compat", "rt", "tracing"] } tracing = { workspace = true } diff --git a/libs/@local/harpc/net/src/session/server/mod.rs b/libs/@local/harpc/net/src/session/server/mod.rs index 36032f5145b..626eff2afde 100644 --- a/libs/@local/harpc/net/src/session/server/mod.rs +++ b/libs/@local/harpc/net/src/session/server/mod.rs @@ -12,7 +12,7 @@ use core::{ task::{Context, Poll, ready}, }; -use error_stack::{Result, ResultExt}; +use error_stack::{Report, ResultExt}; use futures::{Stream, stream::FusedStream}; use harpc_codec::encode::ErrorEncoder; use libp2p::Multiaddr; @@ -33,6 +33,22 @@ pub enum SessionEvent { SessionDropped { id: SessionId }, } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)] +pub enum SessionEventError { + /// The receiving stream lagged too far behind. Attempting to receive again will + /// return the oldest message still retained by the underlying broadcast channel. + /// + /// Includes the number of skipped messages. + #[error("The receiving stream lagged to far behind, and {amount} messages were dropped.")] + Lagged { amount: u64 }, +} + +impl From for SessionEventError { + fn from(never: !) -> Self { + never + } +} + pub struct ListenStream { inner: mpsc::Receiver, @@ -77,6 +93,53 @@ impl FusedStream for ListenStream { } } +pin_project_lite::pin_project! { + // Wrapper around a broadcast, allowing for a more controlled API, and our own error, making the underlying broadcast + // channel an implementation detail. + pub struct EventStream { + #[pin] + inner: tokio_stream::wrappers::BroadcastStream, + + is_finished: bool, + } +} + +impl Stream for EventStream { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + + if *this.is_finished { + return Poll::Ready(None); + } + + // we're purposefully not implementing `From` as that would + // require us to mark `tokio_stream` as a public dependency, something we want to avoid with + // this specifically. + match ready!(this.inner.poll_next(cx)) { + Some(Ok(event)) => Poll::Ready(Some(Ok(event))), + Some(Err(tokio_stream::wrappers::errors::BroadcastStreamRecvError::Lagged(amount))) => { + Poll::Ready(Some(Err(SessionEventError::Lagged { amount }))) + } + None => { + *this.is_finished = true; + + Poll::Ready(None) + } + } + } +} + +// there is no chance this stream will ever be picked-up again, because receivers are only created +// from this one sender, and only expose a stream API, and will be alive as long as the task is +// alive, once all senders are dropped, it indicates that the task has completely shutdown. +impl FusedStream for EventStream { + fn is_terminated(&self) -> bool { + self.is_finished + } +} + /// Session Layer /// /// The session layer is responsible for accepting incoming connections, and splitting them up into @@ -119,8 +182,13 @@ where } #[must_use] - pub fn events(&self) -> broadcast::Receiver { - self.events.subscribe() + pub fn events(&self) -> EventStream { + let receiver = self.events.subscribe(); + + EventStream { + inner: receiver.into(), + is_finished: false, + } } #[must_use] @@ -133,7 +201,7 @@ where /// # Errors /// /// Returns an error if the transport layer fails to listen on the given address. - pub async fn listen(self, address: Multiaddr) -> Result { + pub async fn listen(self, address: Multiaddr) -> Result> { self.transport .listen_on(address) .await diff --git a/libs/@local/harpc/server/Cargo.toml b/libs/@local/harpc/server/Cargo.toml index 4cfef4127ac..40f62e468fa 100644 --- a/libs/@local/harpc/server/Cargo.toml +++ b/libs/@local/harpc/server/Cargo.toml @@ -14,7 +14,7 @@ harpc-service = { workspace = true, public = true } # Public third-party dependencies frunk_core = { version = "0.4.3", public = true } -tower = { workspace = true, public = true } +tower = { workspace = true, public = true, features = ["make"] } # Private workspace dependencies harpc-net = { workspace = true } @@ -31,8 +31,9 @@ tokio = { workspace = true, features = ["macros"] } tokio-util = { workspace = true, features = ["rt"] } tracing = { workspace = true } harpc-codec = { workspace = true } -derive_more = { version = "1.0.0", features = ["display"] } +derive_more = { version = "1.0.0", features = ["display", "error"] } serde = { workspace = true, features = ["derive"] } +bytes.workspace = true [lints] workspace = true diff --git a/libs/@local/harpc/server/examples/account.rs b/libs/@local/harpc/server/examples/account.rs index e2cb8aaa841..65fd4d0851b 100644 --- a/libs/@local/harpc/server/examples/account.rs +++ b/libs/@local/harpc/server/examples/account.rs @@ -11,7 +11,7 @@ use core::fmt::Debug; use error_stack::Report; use frunk::HList; -use futures::{StreamExt, TryStreamExt, pin_mut, stream}; +use futures::{StreamExt, pin_mut, stream}; use graph_types::account::AccountId; use harpc_codec::{decode::Decoder, encode::Encoder, json::JsonCodec}; use harpc_server::{router::RouterBuilder, serve::serve}; @@ -24,7 +24,9 @@ use harpc_service::{ }; use harpc_tower::{ body::{Body, BodyExt}, - layer::{boxed::BoxedResponseLayer, report::HandleReportLayer}, + layer::{ + body_report::HandleBodyReportLayer, boxed::BoxedResponseLayer, report::HandleReportLayer, + }, request::Request, response::{Parts, Response}, }; @@ -160,7 +162,7 @@ where type Service = Account; type Body - = impl Body, Error = !> + = impl Body, Error = ::Error> where Source: Body + Send + Sync; @@ -188,9 +190,7 @@ where let payload = stream.next().await.unwrap().unwrap(); let account_id = self.service.create_account(&session, payload).await?; - let data = codec - .encode(stream::iter([account_id])) - .map_err(|error| panic!("ignore any error, {error:?}")); + let data = codec.encode(stream::iter([account_id])); Ok(Response::from_ok(Parts::new(session_id), data)) } @@ -201,15 +201,20 @@ where #[tokio::main] async fn main() { let router = RouterBuilder::new::<()>(JsonCodec) - .with_builder(|builder| { + .with_builder(|builder, codec| { builder .layer(BoxedResponseLayer::new()) - .layer(HandleReportLayer::new(JsonCodec)) + .layer(HandleReportLayer::new(*codec)) + .layer(HandleBodyReportLayer::new(*codec)) }) .register(AccountServerDelegate { service: AccountServiceImpl, - }) - .build(); + }); + + let task = router.background_task::<_, !>(stream::empty()); + tokio::spawn(task.into_future()); + + let router = router.build(); - serve(stream::empty(), JsonCodec, router).await; + serve(stream::empty(), router).await; } diff --git a/libs/@local/harpc/server/src/lib.rs b/libs/@local/harpc/server/src/lib.rs index 427866941f2..9689f0b2a8c 100644 --- a/libs/@local/harpc/server/src/lib.rs +++ b/libs/@local/harpc/server/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; pub mod delegate; pub mod error; +pub mod route; pub mod router; pub mod serve; pub mod session; diff --git a/libs/@local/harpc/server/src/route.rs b/libs/@local/harpc/server/src/route.rs new file mode 100644 index 00000000000..8e714c1812d --- /dev/null +++ b/libs/@local/harpc/server/src/route.rs @@ -0,0 +1,149 @@ +use core::future::ready; + +use bytes::Bytes; +use frunk::{HCons, HNil}; +use futures::FutureExt; +use harpc_codec::encode::ErrorEncoder; +use harpc_tower::{ + body::{Body, controlled::Controlled, full::Full}, + request::Request, + response::{Parts, Response}, +}; +use harpc_types::{response_kind::ResponseKind, service::ServiceId, version::Version}; +use tower::{Service, ServiceExt, util::Oneshot}; + +use crate::error::NotFound; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct Handler { + service: ServiceId, + version: Version, + + inner: S, +} + +impl Handler { + pub(crate) const fn new(inner: S) -> Self + where + Svc: harpc_service::Service, + { + Self { + service: Svc::ID, + version: Svc::VERSION, + + inner, + } + } +} + +/// Route requests to the appropriate handler based on the request's service and version. +/// +/// This is essentially a type-level linked list of handlers, it's boxed equivalent is [`Steer`], +/// but unlike [`Steer`] it doesn't require the same type for all handlers and a separation of both +/// the meta information (service id and version) and the actual handler. Unlike [`Steer`], it also +/// doesn't require `&mut self` access, which allows for more granular cloning. +/// +/// # Design motivations +/// +/// The reason why we essentially require `Clone`/`Copy` for the handlers is that once the route is +/// constructed it needs to be available for each request that happens, now, there are multiple ways +/// to achieve this. +/// +/// One way would be to simply clone the entire [`Router`] during each request, but that has the +/// downside of potentially cloning a lot of data that isn't actually required for each request, +/// making the addition of new handlers slower and slower. +/// The other solution instead would be to `Mutex` the entire [`Router`], but that would make the +/// entire [`Router`] essentially single-threaded, which is not ideal. +/// +/// This takes a middle ground, which is similar in implementation to other tower-based frameworks, +/// such as axum. The inner routes are stored in an `Arc`, which is cheap to clone, but means we +/// need to require `&self` during routing. Once a route was chosen, we simply clone the service +/// (and oneshot) the service. This keeps the cloned data minimal and allows for multi-threading. +/// +/// The downside is that we're unable to keep any state in a service delegate that's persisted +/// across invocations. To signal this, `ServiceDelegate` takes `self` and consumes it (even though +/// it isn't strictly needed), as well as the use of sessions. To store any information across +/// calls, one must make use of smart pointers, such as `Arc`. +/// +/// [`Router`]: crate::router::Router +/// [`Steer`]: https://docs.rs/tower/latest/tower/steer/struct.Steer.html +pub trait Route { + type ResponseBody: Body, Error = !>; + type Future: Future>; + + fn call(&self, request: Request, codec: C) -> Self::Future + where + ReqBody: Body + Send + Sync, + C: ErrorEncoder + Send + Sync; +} + +// The clone requirement might seem odd here, but is the same as in axum's router implementation. +// see: https://docs.rs/axum/latest/src/axum/routing/route.rs.html#45 +impl Route for HCons, Tail> +where + Svc: Service, Response = Response, Error = !> + Clone, + Tail: Route, + ResBody: Body, Error = !>, +{ + // cannot use `impl Future` here, as it would require additional constraints on the associated + // type, that are already present on the `call` method. + type Future = futures::future::Either< + futures::future::Map< + Oneshot>, + fn(Result, !>) -> Response, + >, + futures::future::Map< + Tail::Future, + fn(Response) -> Response, + >, + >; + type ResponseBody = harpc_tower::either::Either; + + fn call(&self, request: Request, codec: C) -> Self::Future + where + ReqBody: Body + Send + Sync, + C: ErrorEncoder + Send + Sync, + { + let requirement = self.head.version.into_requirement(); + + if self.head.service == request.service().id + && requirement.compatible(request.service().version) + { + let service = self.head.inner.clone(); + + futures::future::Either::Left( + service + .oneshot(request) + .map(|Ok(response)| response.map_body(harpc_tower::either::Either::Left)), + ) + } else { + futures::future::Either::Right( + self.tail + .call(request, codec) + .map(|response| response.map_body(harpc_tower::either::Either::Right)), + ) + } + } +} + +impl Route for HNil { + type Future = core::future::Ready>; + type ResponseBody = Controlled>; + + fn call(&self, request: Request, codec: C) -> Self::Future + where + ReqBody: Body + Send + Sync, + C: ErrorEncoder + Send + Sync, + { + let error = NotFound { + service: request.service().id, + version: request.service().version, + }; + + let session = request.session(); + + let error = codec.encode_error(error); + + ready(Response::from_error(Parts::new(session), error)) + } +} diff --git a/libs/@local/harpc/server/src/router.rs b/libs/@local/harpc/server/src/router.rs index 3bfe1d6a46d..0dd60ded0ab 100644 --- a/libs/@local/harpc/server/src/router.rs +++ b/libs/@local/harpc/server/src/router.rs @@ -1,144 +1,35 @@ use alloc::sync::Arc; use core::{ - future::{self, Ready, ready}, + future::{self, Ready}, task::{Context, Poll}, }; use frunk::{HCons, HNil}; -use futures::{ - FutureExt, - future::{Either, Map}, -}; +use futures::{FutureExt, Stream}; use harpc_codec::encode::ErrorEncoder; +use harpc_net::session::server::SessionEvent; use harpc_service::delegate::ServiceDelegate; use harpc_tower::{ - body::{Body, BodyExt}, + body::Body, + net::pack::{PackLayer, PackService}, request::Request, - response::{BoxedResponse, Parts, Response}, + response::Response, }; -use harpc_types::{service::ServiceId, version::Version}; -use tower::{Layer, Service, ServiceBuilder, ServiceExt, layer::util::Identity, util::Oneshot}; - -use crate::{delegate::ServiceDelegateHandler, error::NotFound, session::SessionStorage}; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct Handler { - service: ServiceId, - version: Version, - - inner: S, -} - -/// Route requests to the appropriate handler based on the request's service and version. -/// -/// This is essentially a type-level linked list of handlers, it's boxed equivalent is [`Steer`], -/// but unlike [`Steer`] it doesn't require the same type for all handlers and a separation of both -/// the meta information (service id and version) and the actual handler. Unlike [`Steer`], it also -/// doesn't require `&mut self` access, which allows for more granular cloning. -/// -/// # Design motivations -/// -/// The reason why we essentially require `Clone`/`Copy` for the handlers is that once the route is -/// constructed it needs to be available for each request that happens, now, there are multiple ways -/// to achieve this. -/// -/// One way would be to simply clone the entire [`Router`] during each request, but that has the -/// downside of potentially cloning a lot of data that isn't actually required for each request, -/// making the addition of new handlers slower and slower. -/// The other solution instead would be to `Mutex` the entire [`Router`], but that would make the -/// entire [`Router`] essentially single-threaded, which is not ideal. -/// -/// This takes a middle ground, which is similar in implementation to other tower-based frameworks, -/// such as axum. The inner routes are stored in an `Arc`, which is cheap to clone, but means we -/// need to require `&self` during routing. Once a route was chosen, we simply clone the service -/// (and oneshot) the service. This keeps the cloned data minimal and allows for multi-threading. -/// -/// The downside is that we're unable to keep any state in a service delegate that's persisted -/// across invocations. To signal this, `ServiceDelegate` takes `self` and consumes it (even though -/// it isn't strictly needed), as well as the use of sessions. To store any information across -/// calls, one must make use of smart pointers, such as `Arc`. -/// -/// [`Steer`]: https://docs.rs/tower/latest/tower/steer/struct.Steer.html -pub trait Route { - type ResponseBodyError; - type Future: Future> + Send; +use tokio_util::sync::CancellationToken; +use tower::{Layer, Service, ServiceBuilder, layer::util::Identity}; - fn call(&self, request: Request, codec: C) -> Self::Future - where - B: Body + Send + Sync, - C: ErrorEncoder + Send + Sync; -} - -// The clone requirement might seem odd here, but is the same as in axum's router implementation. -// see: https://docs.rs/axum/latest/src/axum/routing/route.rs.html#45 -impl Route for HCons, Tail> -where - Svc: Service, Response = BoxedResponse, Error = !, Future: Send> - + Clone - + Send - + Sync, - Tail: Route + Send, - B: Send, -{ - // cannot use `impl Future` here, as it would require additional constraints on the associated - // type, that are already present on the `call` method. - type Future = Either< - Map>, fn(Result, !>) -> BoxedResponse>, - Tail::Future, - >; - type ResponseBodyError = E; - - fn call(&self, request: Request, codec: C) -> Self::Future - where - B: Body + Send + Sync, - C: ErrorEncoder + Send + Sync, - { - let requirement = self.head.version.into_requirement(); - - if self.head.service == request.service().id - && requirement.compatible(request.service().version) - { - let service = self.head.inner.clone(); - - Either::Left(service.oneshot(request).map(|Ok(response)| response)) - } else { - Either::Right(self.tail.call(request, codec)) - } - } -} - -impl Route for HNil -where - B: Body + Send + Sync, - C: ErrorEncoder + Send + Sync, -{ - type ResponseBodyError = !; - - type Future = impl Future> + Send; - - fn call(&self, request: Request, codec: C) -> Self::Future - where - B: Body + Send + Sync, - C: ErrorEncoder + Send + Sync, - { - let error = NotFound { - service: request.service().id, - version: request.service().version, - }; - - let session = request.session(); - - let error = codec.encode_error(error); - - ready(Response::from_error(Parts::new(session), error).map_body(BodyExt::boxed)) - } -} +use crate::{ + delegate::ServiceDelegateHandler, + route::{Handler, Route}, + session::{self, SessionStorage}, +}; pub struct RouterBuilder { routes: R, builder: ServiceBuilder, session: Arc>, codec: C, + cancel: CancellationToken, } impl RouterBuilder { @@ -153,22 +44,32 @@ impl RouterBuilder { builder: ServiceBuilder::new(), session: Arc::new(SessionStorage::new()), codec, + cancel: CancellationToken::new(), } } } +// only allow to set the cancellation token **before** any routes or layers are added. +impl RouterBuilder { + #[must_use] + pub fn with_cancellation_token(self, cancel: CancellationToken) -> Self { + Self { cancel, ..self } + } +} + type ServiceHandler = Handler<>>::Service>; impl RouterBuilder { pub fn with_builder( self, - builder: impl FnOnce(ServiceBuilder) -> ServiceBuilder, + builder: impl FnOnce(ServiceBuilder, &C) -> ServiceBuilder, ) -> RouterBuilder { RouterBuilder { routes: self.routes, - builder: builder(self.builder), + builder: builder(self.builder, &self.codec), session: self.session, codec: self.codec, + cancel: self.cancel, } } @@ -189,30 +90,44 @@ impl RouterBuilder { S: Default + Send + Sync + 'static, C: Clone + Send + 'static, { - let service_id = ::ID; - let version = ::VERSION; - let service = ServiceDelegateHandler::new(delegate, Arc::clone(&self.session), self.codec.clone()); let service = self.builder.service(service); RouterBuilder { routes: HCons { - head: Handler { - service: service_id, - version, - inner: service, - }, + head: Handler::new::(service), tail: self.routes, }, builder: self.builder, session: self.session, codec: self.codec, + cancel: self.cancel, } } } impl RouterBuilder { + /// Creates a background task for session storage management. + /// + /// The returned [`session::Task`] implements `IntoFuture`, allowing it to be + /// easily spawned onto an executor. Configuration options such as `sweep_interval` + /// can be adjusted via methods on the task before spawning. + /// + /// # Note + /// + /// It is not necessary to spawn the task if the router is spawned, but it is **highly** + /// recommended, as otherwise sessions will not be cleaned up, which will lead to memory leaks. + pub fn background_task(&self, stream: St) -> session::Task + where + S: Send + Sync + 'static, + St: Stream> + Send + 'static, + { + Arc::clone(&self.session) + .task(stream) + .with_cancellation_token(self.cancel.child_token()) + } + pub fn build(self) -> Router where R: Send + Sync + 'static, @@ -230,22 +145,22 @@ pub struct RouterService { codec: C, } -impl Service> for RouterService +impl Service> for RouterService where - R: Route, - B: Body + Send + Sync, + R: Route, + ReqBody: Body + Send + Sync, C: ErrorEncoder + Clone + Send + Sync + 'static, { type Error = !; - type Response = BoxedResponse; + type Response = Response; - type Future = impl Future> + Send; + type Future = impl Future>; fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: Request) -> Self::Future { let codec = self.codec.clone(); self.routes.call(req, codec).map(Ok) @@ -262,8 +177,8 @@ where C: Clone, { type Error = !; - type Future = Ready, !>>; - type Response = RouterService; + type Future = Ready>; + type Response = PackService, C>; fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -273,7 +188,9 @@ where let routes = Arc::clone(&self.routes); let codec = self.codec.clone(); - future::ready(Ok(RouterService { routes, codec })) + let layer = PackLayer::new(codec.clone()); + + future::ready(Ok(layer.layer(RouterService { routes, codec }))) } } diff --git a/libs/@local/harpc/server/src/serve.rs b/libs/@local/harpc/server/src/serve.rs index 06335bee1cd..c02c8ce04fb 100644 --- a/libs/@local/harpc/server/src/serve.rs +++ b/libs/@local/harpc/server/src/serve.rs @@ -1,29 +1,31 @@ +use core::future::poll_fn; + +use bytes::Bytes; use futures::{Stream, StreamExt}; -use harpc_codec::encode::ErrorEncoder; +use harpc_codec::error::EncodedError; use harpc_net::session::server::Transaction; use harpc_tower::{ body::server::request::RequestBody, - net::pack::Pack, request::{self, Request}, - response::BoxedResponse, }; use tokio::pin; use tokio_util::task::TaskTracker; -use tower::{Service, ServiceExt}; +use tower::{MakeService, ServiceExt}; -// TODO: do we want `BoxedResponse` to be `!`? or is there a better way of doing this? We could have -// a body that takes any E and converts it to a `!` error (by adding a `TransactionError` and -// terminating) -pub async fn serve( +pub async fn serve( stream: impl Stream + Send, - encoder: impl ErrorEncoder + Clone + Send + 'static, mut make_service: M, ) -> TaskTracker where - M: Service<(), Response = S, Error = !, Future: Send> + Send, - S: Service, Response = BoxedResponse, Error = !, Future: Send> - + Send - + 'static, + M: MakeService< + (), + Request, + Response: futures::Stream> + Send, + Error = !, + Service: tower::Service, Future: Send> + Send + 'static, + MakeError = !, + Future: Send, + > + Send, { // we're not using a JoinSet here on purpose, a TaskTracker immediately frees the memory from a // task, unlike a JoinSet @@ -40,16 +42,15 @@ where let parts = request::Parts::from_transaction(&context); let request = Request::new(parts, RequestBody::new(stream)); - let Ok(service): Result = make_service.call(()).await; - let encoder = encoder.clone(); + let Ok(()) = poll_fn(|cx| make_service.poll_ready(cx)).await; + let Ok(service) = make_service.make_service(()).await; tasks.spawn(async move { - let Ok(response): Result, !> = service.oneshot(request).await; - let response = response.into_body(); + let Ok(stream) = service.oneshot(request).await; + let stream = stream.map(Ok); + pin!(stream); - // TODO: move the pack creation into the `make_service` service. - let pack = Pack::new(response, encoder).map(Ok); - if let Err(error) = pack.forward(sink).await { + if let Err(error) = stream.forward(sink).await { tracing::error!(?error, "failed to send response"); } }); diff --git a/libs/@local/harpc/server/src/session.rs b/libs/@local/harpc/server/src/session.rs index b93bee17dac..12622d2616f 100644 --- a/libs/@local/harpc/server/src/session.rs +++ b/libs/@local/harpc/server/src/session.rs @@ -2,9 +2,10 @@ use alloc::sync::Arc; use core::{fmt::Debug, sync::atomic::AtomicUsize, time::Duration}; use std::{collections::HashSet, sync::Mutex}; -use harpc_net::session::server::{SessionEvent, SessionId}; +use futures::{Stream, StreamExt}; +use harpc_net::session::server::{SessionEvent, SessionEventError, SessionId}; use scc::{ebr::Guard, hash_index::Entry}; -use tokio::sync::broadcast; +use tokio::pin; use tokio_util::sync::CancellationToken; pub struct Session { @@ -172,6 +173,12 @@ where } } +impl SessionStorage { + pub(crate) fn task(self: Arc, stream: S) -> Task { + Task::new(self, stream) + } +} + impl SessionStorage where T: Default + Send + Sync + 'static, @@ -241,58 +248,91 @@ where /// "potentially stale" (a flag that is removed once the session is accessed again). Once a certain /// about of inactivity has passed (the `sweep_interval`) any session that hasn't been accessed is /// removed. -pub struct SessionStorageTask { +pub struct Task { storage: Arc>, - receiver: broadcast::Receiver, + stream: S, + cancel: CancellationToken, sweep_interval: Duration, } -impl SessionStorageTask { - pub const fn new( - storage: Arc>, - receiver: broadcast::Receiver, - sweep_interval: Duration, - ) -> Self { +impl Task { + fn new(storage: Arc>, stream: S) -> Self { Self { storage, - receiver, - - sweep_interval, + stream, + // Default values + cancel: CancellationToken::new(), + sweep_interval: Duration::from_secs(60), } } + + /// Sets the sweep interval for the task. + #[must_use] + pub const fn with_sweep_interval(mut self, sweep_interval: Duration) -> Self { + self.sweep_interval = sweep_interval; + self + } + + /// Sets the cancellation token for the task. + #[must_use] + pub fn with_cancellation_token(mut self, cancel: CancellationToken) -> Self { + self.cancel = cancel; + self + } +} + +impl IntoFuture for Task +where + T: Send + Sync + 'static, + S: Stream> + Send, + E: Into + Send, +{ + type Output = (); + + type IntoFuture = impl Future + Send; + + fn into_future(self) -> Self::IntoFuture { + self.run() + } } -impl SessionStorageTask +impl Task where T: Send + Sync + 'static, + S: Stream> + Send, + E: Into + Send, { - pub async fn run(mut self, cancel: CancellationToken) { + async fn run(self) { + pin!(let stream = self.stream;); + loop { #[expect( clippy::integer_division_remainder_used, reason = "tokio macro uses remainder internally" )] let event = tokio::select! { - () = cancel.cancelled() => break, - event = self.receiver.recv() => event, + () = self.cancel.cancelled() => break, + event = stream.next() => event, () = tokio::time::sleep(self.sweep_interval), if self.storage.has_marked() => { self.storage.remove_marked().await; continue; } }; + let event = event.map(|event| event.map_err(Into::into)); + match event { - Ok(SessionEvent::SessionDropped { id }) => { - self.storage.remove(id).await; - } - Err(broadcast::error::RecvError::Closed) => { - tracing::warn!("Session event channel closed, stopping session storage task"); + None => { + tracing::warn!("Session event stream ended, stopping session storage task"); break; } - Err(broadcast::error::RecvError::Lagged(_)) => { + Some(Ok(SessionEvent::SessionDropped { id })) => { + self.storage.remove(id).await; + } + Some(Err(SessionEventError::Lagged { .. })) => { tracing::warn!( - "Session event channel lagged, marking existing sessions as potentially \ + "Session event stream lagged, marking existing sessions as potentially \ removed" ); diff --git a/libs/@local/harpc/tower/Cargo.toml b/libs/@local/harpc/tower/Cargo.toml index 35669a15aed..246e3370b0c 100644 --- a/libs/@local/harpc/tower/Cargo.toml +++ b/libs/@local/harpc/tower/Cargo.toml @@ -40,6 +40,7 @@ tower-test = { workspace = true } tokio-test = { workspace = true } harpc-codec = { workspace = true, features = ["json"] } insta.workspace = true +serde = { workspace = true, features = ["unstable"] } [lints] workspace = true diff --git a/libs/@local/harpc/tower/src/body/encode_error.rs b/libs/@local/harpc/tower/src/body/encode_error.rs new file mode 100644 index 00000000000..8c37b0cfe28 --- /dev/null +++ b/libs/@local/harpc/tower/src/body/encode_error.rs @@ -0,0 +1,368 @@ +use core::{ + error::Error, + pin::Pin, + task::{Context, Poll, ready}, +}; + +use bytes::Bytes; +use harpc_codec::encode::ErrorEncoder; +use harpc_types::response_kind::ResponseKind; + +use super::{Body, full::Full}; +use crate::{ + body::{BodyExt, controlled::Controlled}, + either::Either, +}; + +pin_project_lite::pin_project! { + #[project = StateProj] + enum State { + Inner { + #[pin] + inner: B + }, + Error { + inner: Controlled> + } + } +} + +pin_project_lite::pin_project! { + /// A body that encodes errors using a specified error encoder. + /// + /// # Behavior + /// + /// When an error occurs, this body replaces the underlying stream with a controlled stream + /// that represents the encoded error. + /// + /// # Error Handling + /// + /// Once an error is encountered, this body stops processing the inner body. This is because: + /// + /// 1. It's impossible to determine if the error occurred mid-way through a structured value. + /// 2. There's no reliable way to find a safe point to resume encoding after the error. + /// + /// # Safety + /// + /// This approach prevents potential cascading failures that could occur if encoding were to + /// continue after an error without proper delimiters or context. + /// + /// # Note + /// + /// While this method ensures safe error handling, it means that any data in the inner body + /// after an error will not be processed. + pub struct EncodeError { + #[pin] + state: State, + encoder: E, + } +} + +impl EncodeError { + pub const fn new(inner: B, encoder: E) -> Self { + Self { + state: State::Inner { inner }, + encoder, + } + } +} + +impl Body for EncodeError +where + B: Body, + E: ErrorEncoder + Clone, +{ + type Control = Either; + type Data = Either; + type Error = !; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll>> { + loop { + let this = self.as_mut().project(); + let state = this.state.project(); + + let next = match state { + StateProj::Inner { inner } => { + let frame = ready!(inner.poll_frame(cx)); + + match frame { + None => None, + Some(Ok(frame)) => { + Some(Ok(frame.map_data(Either::Left).map_control(Either::Left))) + } + Some(Err(error)) => { + let error = this.encoder.clone().encode_error(error); + let (code, data) = error.into_parts(); + + let inner = Controlled::new(ResponseKind::Err(code), Full::new(data)); + self.as_mut().project().state.set(State::Error { inner }); + + // next iteration will poll the error stream + continue; + } + } + } + StateProj::Error { inner } => { + let frame = ready!(inner.poll_frame_unpin(cx)); + + frame.map(|frame| { + frame.map(|data| data.map_data(Either::Right).map_control(Either::Right)) + }) + } + }; + + return Poll::Ready(next); + } + } + + fn state(&self) -> Option { + match &self.state { + State::Inner { inner } => inner.state(), + State::Error { inner } => inner.state(), + } + } + + fn size_hint(&self) -> super::SizeHint { + // we're still in the middle of encoding an error, add the size hint of the error + match &self.state { + State::Inner { inner } => inner.size_hint(), + State::Error { inner } => inner.size_hint(), + } + } +} + +#[cfg(test)] +mod test { + use core::assert_matches::assert_matches; + + use bytes::Bytes; + use harpc_codec::json::JsonCodec; + use harpc_types::{error_code::ErrorCode, response_kind::ResponseKind}; + use insta::assert_debug_snapshot; + + use crate::{ + body::{ + Body, BodyState, Frame, SizeHint, encode_error::EncodeError, test::poll_frame_unpin, + }, + either::Either, + test::{PollExt, StaticBody}, + }; + + #[derive(Debug, thiserror::Error, serde::Serialize)] + #[error("test error")] + struct TestError; + + #[test] + fn encode_error() { + let inner = StaticBody::::new([Err(TestError)]); + let mut body = EncodeError::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + assert_matches!( + frame, + Frame::Control(Either::Right(ResponseKind::Err( + ErrorCode::INTERNAL_SERVER_ERROR + ))) + ); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + insta::assert_debug_snapshot!(frame, @r###" + Data( + Right( + b"\x01{\"message\":\"test error\",\"details\":null}", + ), + ) + "###); + + let frame = poll_frame_unpin(&mut body).expect("should be ready"); + assert!(frame.is_none()); + } + + #[test] + fn passthrough_data() { + let inner = + StaticBody::::new([Ok(Frame::new_data(Bytes::from_static(b"test data")))]); + let mut body = EncodeError::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + assert_matches!(frame, Frame::Data(Either::Left(data)) if data == Bytes::from_static(b"test data")); + + let frame = poll_frame_unpin(&mut body).expect("should be ready"); + assert!(frame.is_none()); + } + + #[test] + fn passthrough_control() { + let inner = StaticBody::::new([Ok(Frame::new_control(2_i32))]); + let mut body = EncodeError::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + assert_matches!(frame, Frame::Control(Either::Left(2))); + + let frame = poll_frame_unpin(&mut body).expect("should be ready"); + assert!(frame.is_none()); + } + + #[test] + fn passthrough_sizehint() { + const DATA: &[u8] = b"test data"; + + let inner = StaticBody::::new([Ok(Frame::new_data(Bytes::from_static(DATA)))]); + let mut body = EncodeError::new(inner, JsonCodec); + + assert_eq!(body.size_hint(), SizeHint::with_exact(DATA.len() as u64)); + + let _frame = poll_frame_unpin(&mut body); + + assert_eq!(body.size_hint(), SizeHint::with_exact(0)); + } + + #[test] + fn size_hint_on_error() { + const DATA: &[u8] = b"test data"; + + let inner = StaticBody::::new([ + Err(TestError), + Ok(Frame::new_data(Bytes::from_static(DATA))), + ]); + let mut body = EncodeError::new(inner, JsonCodec); + + // no error yet, so the size hint should be the size of the data + assert_eq!(body.size_hint(), SizeHint::with_exact(DATA.len() as u64)); + + let _frame = poll_frame_unpin(&mut body); + + // we now have an error, therefore the size hint should include the error + // `40` is taken from the serialization in the unit test above + assert_eq!(body.size_hint(), SizeHint::with_exact(40)); + + let _frame = poll_frame_unpin(&mut body); + + // once finished (2nd poll), we've exhausted and should have a size hint of 0 + assert_eq!(body.size_hint(), SizeHint::with_exact(0)); + } + + #[test] + fn passthrough_state() { + let inner = + StaticBody::::new([Ok(Frame::new_data(Bytes::from_static(b"test data")))]); + let mut body = EncodeError::new(inner, JsonCodec); + + assert_eq!(body.state(), None); + + let _frame = poll_frame_unpin(&mut body); + + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn state_on_error() { + let inner = StaticBody::::new([Err(TestError)]); + let mut body = EncodeError::new(inner, JsonCodec); + + assert_eq!(body.state(), None); + + // polling the control frame + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), None); + + // polling the data frame (after that we're finished) + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn state_trailing_data_after_error() { + let inner = StaticBody::::new([ + Err(TestError), + Ok(Frame::new_data(Bytes::from_static(b"test data"))), + ]); + let mut body = EncodeError::new(inner, JsonCodec); + + assert_eq!(body.state(), None); + + // polling the control frame + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), None); + + // polling the error data frame, we're finished, because we **cannot** continue after an + // error + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn size_hint_and_state_with_empty_body() { + let inner = StaticBody::::new([]); + let body = EncodeError::new(inner, JsonCodec); + + assert_eq!(body.size_hint(), SizeHint::with_exact(0)); + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn does_not_continue_after_error() { + let inner = StaticBody::::new([ + Ok(Frame::new_data(Bytes::from_static(b"data1"))), + Err(TestError), + Ok(Frame::new_data(Bytes::from_static(b"data2"))), + Ok(Frame::new_control(())), + Ok(Frame::new_data(Bytes::from_static(b"data3"))), + ]); + let mut body = EncodeError::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + assert_matches!(frame, Frame::Data(data) if *data.inner() == Bytes::from_static(b"data1")); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + assert_matches!( + frame, + Frame::Control(Either::Right(ResponseKind::Err( + ErrorCode::INTERNAL_SERVER_ERROR + ))) + ); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + assert_debug_snapshot!(frame, @r###" + Data( + Right( + b"\x01{\"message\":\"test error\",\"details\":null}", + ), + ) + "###); + + assert!( + poll_frame_unpin(&mut body) + .expect("should be ready") + .is_none() + ); + } +} diff --git a/libs/@local/harpc/tower/src/body/encode_report.rs b/libs/@local/harpc/tower/src/body/encode_report.rs new file mode 100644 index 00000000000..5d18de6af91 --- /dev/null +++ b/libs/@local/harpc/tower/src/body/encode_report.rs @@ -0,0 +1,376 @@ +use core::{ + pin::Pin, + task::{Context, Poll, ready}, +}; + +use bytes::Bytes; +use error_stack::Report; +use harpc_codec::encode::ErrorEncoder; +use harpc_types::response_kind::ResponseKind; + +use super::{Body, full::Full}; +use crate::{ + body::{BodyExt, controlled::Controlled}, + either::Either, +}; + +pin_project_lite::pin_project! { + #[project = StateProj] + enum State { + Inner { + #[pin] + inner: B + }, + Error { + inner: Controlled> + } + } +} + +pin_project_lite::pin_project! { + /// A body that encodes errors using a specified error encoder. + /// + /// # Behavior + /// + /// When an error occurs, this body replaces the underlying stream with a controlled stream + /// that represents the encoded error. + /// + /// # Error Handling + /// + /// Once an error is encountered, this body stops processing the inner body. This is because: + /// + /// 1. It's impossible to determine if the error occurred mid-way through a structured value. + /// 2. There's no reliable way to find a safe point to resume encoding after the error. + /// + /// # Safety + /// + /// This approach prevents potential cascading failures that could occur if encoding were to + /// continue after an error without proper delimiters or context. + /// + /// # Note + /// + /// While this method ensures safe error handling, it means that any data in the inner body + /// after an error will not be processed. + // We need a separate type for this because of the `Error` bound, `Report` could implement `Error`, in that case we would have a conflicting implementation. + pub struct EncodeReport { + #[pin] + state: State, + encoder: E, + } +} + +impl EncodeReport { + pub const fn new(inner: B, encoder: E) -> Self { + Self { + state: State::Inner { inner }, + encoder, + } + } +} + +impl Body for EncodeReport +where + B: Body>, + E: ErrorEncoder + Clone, + C: error_stack::Context, +{ + type Control = Either; + type Data = Either; + type Error = !; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll>> { + loop { + let this = self.as_mut().project(); + let state = this.state.project(); + + let next = match state { + StateProj::Inner { inner } => { + let frame = ready!(inner.poll_frame(cx)); + + match frame { + None => None, + Some(Ok(frame)) => { + Some(Ok(frame.map_data(Either::Left).map_control(Either::Left))) + } + Some(Err(error)) => { + let error = this.encoder.clone().encode_report(error); + let (code, data) = error.into_parts(); + + let inner = Controlled::new(ResponseKind::Err(code), Full::new(data)); + self.as_mut().project().state.set(State::Error { inner }); + + // next iteration will poll the error stream + continue; + } + } + } + StateProj::Error { inner } => { + let frame = ready!(inner.poll_frame_unpin(cx)); + + frame.map(|frame| { + frame.map(|data| data.map_data(Either::Right).map_control(Either::Right)) + }) + } + }; + + return Poll::Ready(next); + } + } + + fn state(&self) -> Option { + match &self.state { + State::Inner { inner } => inner.state(), + State::Error { inner } => inner.state(), + } + } + + fn size_hint(&self) -> super::SizeHint { + // we're still in the middle of encoding an error, add the size hint of the error + match &self.state { + State::Inner { inner } => inner.size_hint(), + State::Error { inner } => inner.size_hint(), + } + } +} + +#[cfg(test)] +mod test { + use core::assert_matches::assert_matches; + + use bytes::Bytes; + use error_stack::Report; + use harpc_codec::json::JsonCodec; + use harpc_types::{error_code::ErrorCode, response_kind::ResponseKind}; + use insta::assert_debug_snapshot; + + use crate::{ + body::{ + Body, BodyState, Frame, SizeHint, encode_report::EncodeReport, test::poll_frame_unpin, + }, + either::Either, + test::{PollExt, StaticBody}, + }; + + #[derive(Debug, thiserror::Error, serde::Serialize)] + #[error("test error")] + struct TestError; + + #[test] + fn encode_error() { + let inner = StaticBody::>::new([Err(Report::new(TestError))]); + let mut body = EncodeReport::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + assert_matches!( + frame, + Frame::Control(Either::Right(ResponseKind::Err( + ErrorCode::INTERNAL_SERVER_ERROR + ))) + ); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + insta::assert_debug_snapshot!(frame, @r###" + Data( + Right( + b"\x01[{\"context\":\"test error\",\"attachments\":[],\"sources\":[]}]", + ), + ) + "###); + + let frame = poll_frame_unpin(&mut body).expect("should be ready"); + assert!(frame.is_none()); + } + + #[test] + fn passthrough_data() { + let inner = StaticBody::>::new([Ok(Frame::new_data( + Bytes::from_static(b"test data"), + ))]); + let mut body = EncodeReport::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + assert_matches!(frame, Frame::Data(Either::Left(data)) if data == Bytes::from_static(b"test data")); + + let frame = poll_frame_unpin(&mut body).expect("should be ready"); + assert!(frame.is_none()); + } + + #[test] + fn passthrough_control() { + let inner = + StaticBody::>::new([Ok(Frame::new_control(2_i32))]); + let mut body = EncodeReport::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + + assert_matches!(frame, Frame::Control(Either::Left(2))); + + let frame = poll_frame_unpin(&mut body).expect("should be ready"); + assert!(frame.is_none()); + } + + #[test] + fn passthrough_sizehint() { + const DATA: &[u8] = b"test data"; + + let inner = StaticBody::>::new([Ok(Frame::new_data( + Bytes::from_static(DATA), + ))]); + let mut body = EncodeReport::new(inner, JsonCodec); + + assert_eq!(body.size_hint(), SizeHint::with_exact(DATA.len() as u64)); + + let _frame = poll_frame_unpin(&mut body); + + assert_eq!(body.size_hint(), SizeHint::with_exact(0)); + } + + #[test] + fn size_hint_on_error() { + const DATA: &[u8] = b"test data"; + + let inner = StaticBody::>::new([ + Err(Report::new(TestError)), + Ok(Frame::new_data(Bytes::from_static(DATA))), + ]); + let mut body = EncodeReport::new(inner, JsonCodec); + + // no error yet, so the size hint should be the size of the data + assert_eq!(body.size_hint(), SizeHint::with_exact(DATA.len() as u64)); + + let _frame = poll_frame_unpin(&mut body); + + // we now have an error, therefore the size hint should include the error + // `40` is taken from the serialization in the unit test above + assert_eq!(body.size_hint(), SizeHint::with_exact(57)); + + let _frame = poll_frame_unpin(&mut body); + + // once finished (2nd poll), we've exhausted and should have a size hint of 0 + assert_eq!(body.size_hint(), SizeHint::with_exact(0)); + } + + #[test] + fn passthrough_state() { + let inner = StaticBody::>::new([Ok(Frame::new_data( + Bytes::from_static(b"test data"), + ))]); + let mut body = EncodeReport::new(inner, JsonCodec); + + assert_eq!(body.state(), None); + + let _frame = poll_frame_unpin(&mut body); + + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn state_on_error() { + let inner = StaticBody::>::new([Err(Report::new(TestError))]); + let mut body = EncodeReport::new(inner, JsonCodec); + + assert_eq!(body.state(), None); + + // polling the control frame + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), None); + + // polling the data frame (after that we're finished) + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn state_trailing_data_after_error() { + let inner = StaticBody::>::new([ + Err(Report::new(TestError)), + Ok(Frame::new_data(Bytes::from_static(b"test data"))), + ]); + let mut body = EncodeReport::new(inner, JsonCodec); + + assert_eq!(body.state(), None); + + // polling the control frame + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), None); + + // polling the error data frame, we're finished, because we **cannot** continue after an + // error + let _frame = poll_frame_unpin(&mut body); + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn size_hint_and_state_with_empty_body() { + let inner = StaticBody::>::new([]); + let body = EncodeReport::new(inner, JsonCodec); + + assert_eq!(body.size_hint(), SizeHint::with_exact(0)); + assert_eq!(body.state(), Some(BodyState::Complete)); + } + + #[test] + fn does_not_continue_after_error() { + let inner = StaticBody::>::new([ + Ok(Frame::new_data(Bytes::from_static(b"data1"))), + Err(Report::new(TestError)), + Ok(Frame::new_data(Bytes::from_static(b"data2"))), + Ok(Frame::new_control(())), + Ok(Frame::new_data(Bytes::from_static(b"data3"))), + ]); + let mut body = EncodeReport::new(inner, JsonCodec); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + assert_matches!(frame, Frame::Data(data) if *data.inner() == Bytes::from_static(b"data1")); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + assert_matches!( + frame, + Frame::Control(Either::Right(ResponseKind::Err( + ErrorCode::INTERNAL_SERVER_ERROR + ))) + ); + + let frame = poll_frame_unpin(&mut body) + .expect("should be ready") + .expect("should have an item") + .expect("should be a frame"); + assert_debug_snapshot!(frame, @r###" + Data( + Right( + b"\x01[{\"context\":\"test error\",\"attachments\":[],\"sources\":[]}]", + ), + ) + "###); + + assert!( + poll_frame_unpin(&mut body) + .expect("should be ready") + .is_none() + ); + } +} diff --git a/libs/@local/harpc/tower/src/body/mod.rs b/libs/@local/harpc/tower/src/body/mod.rs index 3405ce355b4..544bd55d6c6 100644 --- a/libs/@local/harpc/tower/src/body/mod.rs +++ b/libs/@local/harpc/tower/src/body/mod.rs @@ -1,6 +1,8 @@ pub mod boxed; pub mod controlled; pub mod empty; +pub mod encode_error; +pub mod encode_report; pub mod full; pub mod limited; pub mod map; diff --git a/libs/@local/harpc/tower/src/body/size_hint.rs b/libs/@local/harpc/tower/src/body/size_hint.rs index 3a9f9ec7758..3d262bf90cc 100644 --- a/libs/@local/harpc/tower/src/body/size_hint.rs +++ b/libs/@local/harpc/tower/src/body/size_hint.rs @@ -1,5 +1,7 @@ //! Adapted and vendored from `http-body` crate () +use core::ops::Add; + /// A `Body` size hint /// /// The default implementation returns: @@ -89,3 +91,17 @@ impl SizeHint { self.upper = Some(value); } } + +impl Add for SizeHint { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + let lower = self.lower + rhs.lower; + let upper = match (self.upper, rhs.upper) { + (Some(lhs), Some(rhs)) => Some(lhs + rhs), + _ => None, + }; + + Self { lower, upper } + } +} diff --git a/libs/@local/harpc/tower/src/either.rs b/libs/@local/harpc/tower/src/either.rs index 5e83b86d315..ad3bd492cb3 100644 --- a/libs/@local/harpc/tower/src/either.rs +++ b/libs/@local/harpc/tower/src/either.rs @@ -109,6 +109,20 @@ where } impl Either { + pub const fn inner(&self) -> &T { + match self { + Self::Left(left) => left, + Self::Right(right) => right, + } + } + + pub fn inner_mut(&mut self) -> &mut T { + match self { + Self::Left(left) => left, + Self::Right(right) => right, + } + } + pub fn into_inner(self) -> T { match self { Self::Left(left) => left, diff --git a/libs/@local/harpc/tower/src/layer/body_error.rs b/libs/@local/harpc/tower/src/layer/body_error.rs new file mode 100644 index 00000000000..4a0cee272f0 --- /dev/null +++ b/libs/@local/harpc/tower/src/layer/body_error.rs @@ -0,0 +1,74 @@ +use core::{ + error::Error, + task::{Context, Poll}, +}; + +use futures::TryFutureExt; +use harpc_codec::encode::ErrorEncoder; +use harpc_types::response_kind::ResponseKind; +use tower::{Layer, Service}; + +use crate::{ + body::{Body, encode_error::EncodeError}, + request::Request, + response::Response, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct HandleBodyErrorLayer { + encoder: E, +} + +impl HandleBodyErrorLayer { + pub const fn new(encoder: E) -> Self { + Self { encoder } + } +} + +impl Layer for HandleBodyErrorLayer +where + E: Clone, +{ + type Service = HandleBodyErrorService; + + fn layer(&self, inner: S) -> Self::Service { + HandleBodyErrorService { + inner, + encoder: self.encoder.clone(), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct HandleBodyErrorService { + inner: S, + + encoder: E, +} + +impl Service> for HandleBodyErrorService +where + S: Service, Response = Response> + Clone + Send, + E: ErrorEncoder + Clone, + ReqBody: Body, + // the extra bounds here are not strictly required, but they help to make the error messages + // more expressive during compilation + ResBody: Body, Error: Error + serde::Serialize>, +{ + type Error = S::Error; + type Response = Response>; + + type Future = impl Future>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let encoder = self.encoder.clone(); + + self.inner + .call(req) + .map_ok(|res| res.map_body(|body| EncodeError::new(body, encoder))) + } +} diff --git a/libs/@local/harpc/tower/src/layer/body_report.rs b/libs/@local/harpc/tower/src/layer/body_report.rs new file mode 100644 index 00000000000..32c2fa644d5 --- /dev/null +++ b/libs/@local/harpc/tower/src/layer/body_report.rs @@ -0,0 +1,73 @@ +use core::task::{Context, Poll}; + +use error_stack::Report; +use futures::TryFutureExt; +use harpc_codec::encode::ErrorEncoder; +use harpc_types::response_kind::ResponseKind; +use tower::{Layer, Service}; + +use crate::{ + body::{Body, encode_report::EncodeReport}, + request::Request, + response::Response, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct HandleBodyReportLayer { + encoder: E, +} + +impl HandleBodyReportLayer { + pub const fn new(encoder: E) -> Self { + Self { encoder } + } +} + +impl Layer for HandleBodyReportLayer +where + E: Clone, +{ + type Service = HandleBodyErrorService; + + fn layer(&self, inner: S) -> Self::Service { + HandleBodyErrorService { + inner, + encoder: self.encoder.clone(), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct HandleBodyErrorService { + inner: S, + + encoder: E, +} + +impl Service> for HandleBodyErrorService +where + S: Service, Response = Response> + Clone + Send, + E: ErrorEncoder + Clone, + ReqBody: Body, + // the extra bounds here are not strictly required, but they help to make the error messages + // more expressive during compilation + ResBody: Body, Error = Report>, + C: error_stack::Context, +{ + type Error = S::Error; + type Response = Response>; + + type Future = impl Future>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let encoder = self.encoder.clone(); + + self.inner + .call(req) + .map_ok(|res| res.map_body(|body| EncodeReport::new(body, encoder))) + } +} diff --git a/libs/@local/harpc/tower/src/layer/error.rs b/libs/@local/harpc/tower/src/layer/error.rs index eff159b0a35..05584cad521 100644 --- a/libs/@local/harpc/tower/src/layer/error.rs +++ b/libs/@local/harpc/tower/src/layer/error.rs @@ -16,6 +16,7 @@ use crate::{ response::{Parts, Response}, }; +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct HandleErrorLayer { encoder: E, } diff --git a/libs/@local/harpc/tower/src/layer/mod.rs b/libs/@local/harpc/tower/src/layer/mod.rs index 9f8a166a9fc..3ae3622126b 100644 --- a/libs/@local/harpc/tower/src/layer/mod.rs +++ b/libs/@local/harpc/tower/src/layer/mod.rs @@ -1,3 +1,5 @@ +pub mod body_error; +pub mod body_report; pub mod boxed; pub mod error; pub mod report; diff --git a/libs/@local/harpc/tower/src/lib.rs b/libs/@local/harpc/tower/src/lib.rs index 447ed33df4c..cce2931cd46 100644 --- a/libs/@local/harpc/tower/src/lib.rs +++ b/libs/@local/harpc/tower/src/lib.rs @@ -4,7 +4,9 @@ type_changing_struct_update, error_generic_member_access )] -#![cfg_attr(test, feature(noop_waker, assert_matches))] +#![cfg_attr(test, feature(noop_waker, assert_matches, macro_metavar_expr))] + +extern crate alloc; pub use self::extensions::Extensions; @@ -15,6 +17,8 @@ pub mod layer; pub mod net; pub mod request; pub mod response; +#[cfg(test)] +pub(crate) mod test; // TODO: server impl of Transaction -> Request/Response stream // TODO: client impl of Transaction -> Request/Response stream diff --git a/libs/@local/harpc/tower/src/net/pack.rs b/libs/@local/harpc/tower/src/net/pack.rs index c72f853aa78..3f6cf24afc5 100644 --- a/libs/@local/harpc/tower/src/net/pack.rs +++ b/libs/@local/harpc/tower/src/net/pack.rs @@ -6,11 +6,16 @@ use core::{ }; use bytes::{Buf, BufMut, Bytes, BytesMut}; -use futures::Stream; +use futures::{FutureExt, Stream}; use harpc_codec::{encode::ErrorEncoder, error::EncodedError}; use harpc_types::{error_code::ErrorCode, response_kind::ResponseKind}; +use tower::{Layer, Service}; -use crate::body::{Body, Frame}; +use crate::{ + body::{Body, Frame}, + request::Request, + response::Response, +}; #[derive( Debug, @@ -56,11 +61,7 @@ pin_project_lite::pin_project! { } } -impl Pack -where - B: Body, Error = !>, - E: ErrorEncoder, -{ +impl Pack { pub const fn new(inner: B, encoder: E) -> Self { Self { inner, @@ -173,6 +174,58 @@ where } } +pub struct PackLayer { + encoder: C, +} + +impl PackLayer { + pub const fn new(encoder: E) -> Self { + Self { encoder } + } +} + +impl Layer for PackLayer +where + E: Clone, +{ + type Service = PackService; + + fn layer(&self, inner: S) -> Self::Service { + PackService { + inner, + encoder: self.encoder.clone(), + } + } +} + +pub struct PackService { + inner: S, + encoder: E, +} + +impl Service> for PackService +where + S: Service, Response = Response>, + C: ErrorEncoder + Clone, +{ + type Error = S::Error; + type Response = Pack; + + type Future = impl Future>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let encoder = self.encoder.clone(); + + self.inner + .call(req) + .map(|result| result.map(|response| Pack::new(response.into_body(), encoder))) + } +} + #[cfg(test)] mod test { use bytes::{BufMut, Bytes}; diff --git a/libs/@local/harpc/tower/src/response.rs b/libs/@local/harpc/tower/src/response.rs index bc510cb8b8a..6a002be5570 100644 --- a/libs/@local/harpc/tower/src/response.rs +++ b/libs/@local/harpc/tower/src/response.rs @@ -39,7 +39,9 @@ where pub const fn from_parts(parts: Parts, body: B) -> Self { Self { head: parts, body } } +} +impl Response { pub const fn session(&self) -> SessionId { self.head.session } diff --git a/libs/@local/harpc/tower/src/test.rs b/libs/@local/harpc/tower/src/test.rs new file mode 100644 index 00000000000..dde9faf22bd --- /dev/null +++ b/libs/@local/harpc/tower/src/test.rs @@ -0,0 +1,77 @@ +use alloc::collections::VecDeque; +use core::{ + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Buf; + +use crate::body::{Body, BodyState, Frame, SizeHint}; + +pin_project_lite::pin_project! { + pub(crate) struct StaticBody { + frames: VecDeque, E>>, + } +} + +impl StaticBody { + pub(crate) fn new(frames: impl IntoIterator, E>>) -> Self { + Self { + frames: frames.into_iter().collect(), + } + } +} + +impl Body for StaticBody +where + D: Buf, +{ + type Control = C; + type Data = D; + type Error = E; + + fn poll_frame( + self: Pin<&mut Self>, + _: &mut Context, + ) -> Poll>> { + let this = self.project(); + let frame = this.frames.pop_front(); + + Poll::Ready(frame) + } + + fn state(&self) -> Option { + self.frames.is_empty().then_some(BodyState::Complete) + } + + fn size_hint(&self) -> SizeHint { + let size: usize = self + .frames + .iter() + .map(Result::as_ref) + .filter_map(Result::ok) + .filter_map(Frame::data) + .map(D::remaining) + .sum(); + + SizeHint::with_exact(size as u64) + } +} + +pub(crate) trait PollExt { + type Item; + + fn expect(self, message: impl AsRef) -> Self::Item; +} + +impl PollExt for Poll { + type Item = T; + + #[track_caller] + fn expect(self, message: impl AsRef) -> Self::Item { + match self { + Self::Ready(val) => val, + Self::Pending => panic!("{}", message.as_ref()), + } + } +} diff --git a/libs/@local/hash-graph-client/typescript/package.json b/libs/@local/hash-graph-client/typescript/package.json index c2108adb180..27061399ca7 100644 --- a/libs/@local/hash-graph-client/typescript/package.json +++ b/libs/@local/hash-graph-client/typescript/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@local/eslint-config": "0.0.0-private", "@local/tsconfig": "0.0.0-private", - "@redocly/cli": "1.25.5", + "@redocly/cli": "1.25.6", "@rust/graph-api": "0.0.0-private", "@types/node": "20.16.11", "eslint": "8.57.0", diff --git a/libs/error-stack/.justfile b/libs/error-stack/.justfile index 5c6ddc5610d..261ba5e5db2 100755 --- a/libs/error-stack/.justfile +++ b/libs/error-stack/.justfile @@ -21,8 +21,8 @@ clippy *arguments: test *arguments: @just install-cargo-nextest - RUST_BACKTRACE=1 cargo nextest run --all-features --all-targets --cargo-profile {{profile}} {{arguments}} - RUST_BACKTRACE=1 cargo nextest run --no-default-features --all-targets --cargo-profile {{profile}} {{arguments}} + RUST_BACKTRACE=1 cargo nextest run --all-features --all-targets {{arguments}} + RUST_BACKTRACE=1 cargo nextest run --no-default-features --all-targets {{arguments}} RUST_BACKTRACE=1 cargo test --all-features --doc {{arguments}} [private] diff --git a/libs/error-stack/CHANGELOG.md b/libs/error-stack/CHANGELOG.md index f7fd6941477..0209a0f64a7 100644 --- a/libs/error-stack/CHANGELOG.md +++ b/libs/error-stack/CHANGELOG.md @@ -5,19 +5,21 @@ All notable changes to `error-stack` will be documented in this file. ## Planned - Support for [`defmt`](https://defmt.ferrous-systems.com) +- Better support for serialization and deserialization of `Report` ## Unreleased ### Features -- Report has been split into `Report` and `Report<[C]>` to distinguish between a group of related errors and a single error. These errors can still be nested. -- Introduce a new `unstable` flag, which is used to enable unstable features, these features are not covered by semver and may be modified or removed at any time. +- Report has been split into `Report` and `Report<[C]>` to distinguish between a group of related errors and a single error. These errors can still be nested. ([#5047](https://github.com/hashintel/hash/pull/5047)) +- Introduce a new `unstable` flag, which is used to enable unstable features, these features are not covered by semver and may be modified or removed at any time. ([#5181](https://github.com/hashintel/hash/pull/5181)) ### Breaking Changes -- `Extend` is no longer implemented by `Report`, instead it is implemented on `Report<[C]>`, either use `From` or `Report::expand` to convert between `Report` into `Report<[C]>`. -- `extend_one` has been renamed to `push` and is only implemented on `Report<[C]>`. -- `bail!(report,)` has been removed, one must now use `bail!(report)`. This is in preparation for the unstable `bail!` macro that allows to construct `Report<[C]>`. +- Set the MSRV to 1.83 ([#5333](https://github.com/hashintel/hash/pull/5333)) +- `Extend` is no longer implemented by `Report`, instead it is implemented on `Report<[C]>`, either use `From` or `Report::expand` to convert between `Report` into `Report<[C]>`. ([#5047](https://github.com/hashintel/hash/pull/5047)) +- `extend_one` has been renamed to `push` and is only implemented on `Report<[C]>`. ([#5047](https://github.com/hashintel/hash/pull/5047)) +- `bail!(report,)` has been removed, one must now use `bail!(report)`. This is in preparation for the unstable `bail!` macro that allows to construct `Report<[C]>`. ([#5047](https://github.com/hashintel/hash/pull/5047)) ## [0.5.0](https://github.com/hashintel/hash/tree/error-stack%400.5.0/libs/error-stack) - 2024-07-12 diff --git a/libs/error-stack/Cargo.toml b/libs/error-stack/Cargo.toml index af6ea9e36d2..a1a25efc372 100644 --- a/libs/error-stack/Cargo.toml +++ b/libs/error-stack/Cargo.toml @@ -3,7 +3,7 @@ name = "error-stack" version = "0.5.0" authors = { workspace = true } edition = "2021" -rust-version = "1.63.0" +rust-version = "1.83.0" license = "MIT OR Apache-2.0" description = "A context-aware error-handling library that supports arbitrary attached user data" documentation = "https://docs.rs/error-stack" diff --git a/libs/error-stack/README.md b/libs/error-stack/README.md index e55b627dc5e..7bf9b83e545 100644 --- a/libs/error-stack/README.md +++ b/libs/error-stack/README.md @@ -7,7 +7,7 @@ [![crates.io](https://img.shields.io/crates/v/error-stack)][crates.io] [![libs.rs](https://img.shields.io/badge/libs.rs-error--stack-orange)][libs.rs] -[![rust-version](https://img.shields.io/static/v1?label=Rust&message=1.63.0/nightly-2024-10-14&color=blue)][rust-version] +[![rust-version](https://img.shields.io/static/v1?label=Rust&message=1.83.0/nightly-2024-10-14&color=blue)][rust-version] [![documentation](https://img.shields.io/docsrs/error-stack)][documentation] [![license](https://img.shields.io/crates/l/error-stack)][license] diff --git a/libs/error-stack/build.rs b/libs/error-stack/build.rs index 52153b99077..fe966eb2787 100644 --- a/libs/error-stack/build.rs +++ b/libs/error-stack/build.rs @@ -1,6 +1,4 @@ -use std::process::exit; - -use rustc_version::{Channel, Version, version_meta}; +use rustc_version::{Channel, version_meta}; fn main() { let version_meta = version_meta().expect("Could not get Rust version"); @@ -10,27 +8,19 @@ fn main() { println!("cargo:rustc-cfg=nightly"); } - let rustc_version = version_meta.semver; - let trimmed_rustc_version = Version::new( - rustc_version.major, - rustc_version.minor, - rustc_version.patch, - ); - - if cfg!(feature = "backtrace") && trimmed_rustc_version < Version::new(1, 65, 0) { - println!("cargo:warning=The `backtrace` feature requires Rust 1.65.0 or later."); - exit(1); - } - - println!("cargo:rustc-check-cfg=cfg(rust_1_80)"); - if trimmed_rustc_version >= Version::new(1, 80, 0) { - println!("cargo:rustc-cfg=rust_1_80"); - } - - println!("cargo:rustc-check-cfg=cfg(rust_1_81)"); - if trimmed_rustc_version >= Version::new(1, 81, 0) { - println!("cargo:rustc-cfg=rust_1_81"); - } + // Currently not used, but helpful for future reference + // + // let rustc_version = version_meta.semver; + // let trimmed_rustc_version = Version::new( + // rustc_version.major, + // rustc_version.minor, + // rustc_version.patch, + // ); + // + // println!("cargo:rustc-check-cfg=cfg(rust_1_81)"); + // if trimmed_rustc_version >= Version::new(1, 81, 0) { + // println!("cargo:rustc-cfg=rust_1_81"); + // } if cfg!(feature = "futures") && !cfg!(feature = "unstable") { println!("cargo:warning=The `futures` feature requires the `unstable` feature."); diff --git a/libs/error-stack/examples/demo.rs b/libs/error-stack/examples/demo.rs index 723e439129e..ce3d00b73ef 100644 --- a/libs/error-stack/examples/demo.rs +++ b/libs/error-stack/examples/demo.rs @@ -16,8 +16,10 @@ impl fmt::Display for ParseExperimentError { impl Context for ParseExperimentError {} -// Reason: false-positive, try_fold is fail-fast, our implementation is fail-slow. -#[expect(clippy::manual_try_fold)] +#[expect( + clippy::manual_try_fold, + reason = "false-positive, try_fold is fail-fast, our implementation is fail-slow" +)] fn parse_experiment(description: &str) -> Result, ParseExperimentError> { let values = description .split(' ') diff --git a/libs/error-stack/macros/Cargo.toml b/libs/error-stack/macros/Cargo.toml index 4c65e0d831a..94dcfecb125 100644 --- a/libs/error-stack/macros/Cargo.toml +++ b/libs/error-stack/macros/Cargo.toml @@ -3,7 +3,7 @@ name = "error-stack-macros" version = "0.0.0-reserved" authors = { workspace = true } edition = "2021" -rust-version = "1.63.0" +rust-version = "1.83.0" license = "MIT OR Apache-2.0" description = "Macros for the `error-stack` crate" documentation = "https://docs.rs/error-stack" diff --git a/libs/error-stack/macros/README.md b/libs/error-stack/macros/README.md index ef6b7136b66..f7040cd3846 100644 --- a/libs/error-stack/macros/README.md +++ b/libs/error-stack/macros/README.md @@ -6,7 +6,7 @@ [![crates.io](https://img.shields.io/crates/v/error-stack-macros)][crates.io] [![libs.rs](https://img.shields.io/badge/libs.rs-error--stack--macros-orange)][libs.rs] -[![rust-version](https://img.shields.io/static/v1?label=Rust&message=1.63.0/nightly-2024-10-14&color=blue)][rust-version] +[![rust-version](https://img.shields.io/static/v1?label=Rust&message=1.83.0/nightly-2024-10-14&color=blue)][rust-version] [![documentation](https://img.shields.io/docsrs/error-stack-macros)][documentation] [![license](https://img.shields.io/crates/l/error-stack)][license] diff --git a/libs/error-stack/src/compat/anyhow.rs b/libs/error-stack/src/compat/anyhow.rs index 0764450ec03..adf2baebe89 100644 --- a/libs/error-stack/src/compat/anyhow.rs +++ b/libs/error-stack/src/compat/anyhow.rs @@ -18,7 +18,7 @@ impl IntoReportCompat for core::result::Result { .map(ToString::to_string) .collect::>(); - #[cfg_attr(not(feature = "std"), allow(unused_mut))] + #[cfg_attr(not(feature = "std"), expect(unused_mut))] let mut report: Report = Report::from_frame(Frame::from_anyhow(anyhow, alloc::boxed::Box::new([]))); diff --git a/libs/error-stack/src/context.rs b/libs/error-stack/src/context.rs index d437b05e485..513c5e92aae 100644 --- a/libs/error-stack/src/context.rs +++ b/libs/error-stack/src/context.rs @@ -1,12 +1,7 @@ -#[cfg(any(feature = "std", rust_1_81))] use alloc::string::{String, ToString}; -#[cfg(rust_1_81)] -use core::error::Error; #[cfg(nightly)] use core::error::Request; -use core::fmt; -#[cfg(all(feature = "std", not(rust_1_81)))] -use std::error::Error; +use core::{error::Error, fmt}; use crate::Report; @@ -64,7 +59,7 @@ use crate::Report; pub trait Context: fmt::Display + fmt::Debug + Send + Sync + 'static { /// Provide values which can then be requested by [`Report`]. #[cfg(nightly)] - #[allow(unused_variables)] + #[expect(unused_variables)] fn provide<'a>(&'a self, request: &mut Request<'a>) {} /// Returns the source of the error, if any. @@ -72,38 +67,32 @@ pub trait Context: fmt::Display + fmt::Debug + Send + Sync + 'static { /// This method only exists to avoid the requirement of specialization and to get the sources /// for `Error`. #[doc(hidden)] - #[cfg(any(feature = "std", rust_1_81))] fn __source(&self) -> Option<&(dyn Error + 'static)> { None } } /// Captures an error message as the context of a [`Report`]. -#[cfg(any(feature = "std", rust_1_81))] pub(crate) struct SourceContext(String); -#[cfg(any(feature = "std", rust_1_81))] impl SourceContext { pub(crate) fn from_error(value: &dyn Error) -> Self { Self(value.to_string()) } } -#[cfg(any(feature = "std", rust_1_81))] impl fmt::Debug for SourceContext { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, fmt) } } -#[cfg(any(feature = "std", rust_1_81))] impl fmt::Display for SourceContext { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, fmt) } } -#[cfg(any(feature = "std", rust_1_81))] impl Context for SourceContext {} impl From for Report @@ -117,7 +106,6 @@ where } } -#[cfg(any(feature = "std", rust_1_81))] impl Context for C { #[cfg(nightly)] fn provide<'a>(&'a self, request: &mut Request<'a>) { diff --git a/libs/error-stack/src/error.rs b/libs/error-stack/src/error.rs index 508968c9858..375478a62fa 100644 --- a/libs/error-stack/src/error.rs +++ b/libs/error-stack/src/error.rs @@ -1,12 +1,6 @@ -#![cfg(any(feature = "std", rust_1_81))] - -#[cfg(rust_1_81)] -use core::error::Error; #[cfg(nightly)] use core::error::Request; -use core::fmt; -#[cfg(all(feature = "std", not(rust_1_81)))] -use std::error::Error; +use core::{error::Error, fmt, ptr}; use crate::Report; @@ -20,7 +14,7 @@ impl ReportError { pub(crate) const fn from_ref(report: &Report) -> &Self { // SAFETY: `ReportError` is a `repr(transparent)` wrapper around `Report`. - unsafe { &*(report as *const Report).cast() } + unsafe { &*ptr::from_ref(report).cast() } } } @@ -36,7 +30,6 @@ impl fmt::Display for ReportError { } } -#[cfg(any(feature = "std", rust_1_81))] impl Error for ReportError { #[cfg(nightly)] fn provide<'a>(&'a self, request: &mut Request<'a>) { diff --git a/libs/error-stack/src/ext/tuple.rs b/libs/error-stack/src/ext/tuple.rs index 340d1744487..a875bc2162d 100644 --- a/libs/error-stack/src/ext/tuple.rs +++ b/libs/error-stack/src/ext/tuple.rs @@ -101,7 +101,7 @@ macro_rules! impl_ext { { type Output = ($($output),*, T); - #[allow(non_snake_case, clippy::min_ident_chars)] + #[expect(non_snake_case, clippy::min_ident_chars)] fn try_collect(self) -> Result { let ($($type),*, result) = self; let prefix = ($($type,)*).try_collect(); diff --git a/libs/error-stack/src/fmt/hook.rs b/libs/error-stack/src/fmt/hook.rs index 7567c1ec3c0..8fdec1a4d9b 100644 --- a/libs/error-stack/src/fmt/hook.rs +++ b/libs/error-stack/src/fmt/hook.rs @@ -3,7 +3,6 @@ // implementation: `pub(crate)` and `pub`. #![cfg_attr(not(feature = "std"), allow(unreachable_pub))] -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::{boxed::Box, string::String, vec::Vec}; use core::{any::TypeId, mem}; @@ -421,7 +420,7 @@ fn into_boxed_hook( /// [`Display`]: core::fmt::Display /// [`Debug`]: core::fmt::Debug /// [`.insert()`]: Hooks::insert -#[allow(clippy::field_scoped_visibility_modifiers)] +#[expect(clippy::field_scoped_visibility_modifiers)] pub(crate) struct Hooks { // We use `Vec`, instead of `HashMap` or `BTreeMap`, so that ordering is consistent with the // insertion order of types. @@ -455,7 +454,6 @@ impl Hooks { mod default { #[cfg(any(feature = "backtrace", feature = "spantrace"))] use alloc::format; - #[cfg_attr(feature = "std", allow(unused_imports))] use alloc::string::ToString; use core::{ panic::Location, diff --git a/libs/error-stack/src/fmt/mod.rs b/libs/error-stack/src/fmt/mod.rs index 267a4adde49..2f12f1dfed4 100644 --- a/libs/error-stack/src/fmt/mod.rs +++ b/libs/error-stack/src/fmt/mod.rs @@ -299,10 +299,9 @@ mod hook; mod location; mod r#override; -use alloc::collections::VecDeque; -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::{ borrow::ToOwned, + collections::VecDeque, format, string::{String, ToString}, vec, @@ -584,9 +583,11 @@ enum PreparedInstruction<'a> { } impl Instruction { - // Reason: the match arms are the same intentionally, this makes it more clean which variant - // emits which and also keeps it nicely formatted. - #[allow(clippy::match_same_arms)] + #[expect( + clippy::match_same_arms, + reason = "the match arms are the same intentionally, this makes it more clean which \ + variant emits which and also keeps it nicely formatted." + )] fn prepare(&self) -> PreparedInstruction { match self { Self::Value { value, style } => PreparedInstruction::Content(value, style), @@ -691,7 +692,7 @@ impl Display for LineDisplay<'_> { struct Lines(VecDeque); impl Lines { - fn new() -> Self { + const fn new() -> Self { Self(VecDeque::new()) } @@ -825,7 +826,10 @@ impl Opaque { } } -#[allow(clippy::needless_pass_by_ref_mut)] +#[cfg_attr( + not(any(feature = "std", feature = "hooks")), + expect(clippy::needless_pass_by_ref_mut) +)] fn debug_attachments_invoke<'a>( frames: impl IntoIterator, config: &mut Config, @@ -1090,7 +1094,7 @@ impl Debug for Report { let color = config.color_mode(); let charset = config.charset(); - #[cfg_attr(not(any(feature = "std", feature = "hooks")), allow(unused_mut))] + #[cfg_attr(not(any(feature = "std", feature = "hooks")), expect(unused_mut))] let mut lines = self .current_frames_unchecked() .iter() diff --git a/libs/error-stack/src/frame/frame_impl.rs b/libs/error-stack/src/frame/frame_impl.rs index 4e0ba08d6e5..4b7459d4d75 100644 --- a/libs/error-stack/src/frame/frame_impl.rs +++ b/libs/error-stack/src/frame/frame_impl.rs @@ -1,4 +1,3 @@ -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::boxed::Box; #[cfg(nightly)] use core::error::{Error, Request}; diff --git a/libs/error-stack/src/frame/mod.rs b/libs/error-stack/src/frame/mod.rs index e34c065b3f1..d710ec05017 100644 --- a/libs/error-stack/src/frame/mod.rs +++ b/libs/error-stack/src/frame/mod.rs @@ -1,7 +1,6 @@ mod frame_impl; mod kind; -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::boxed::Box; #[cfg(nightly)] use core::error::{self, Error}; diff --git a/libs/error-stack/src/hook/context.rs b/libs/error-stack/src/hook/context.rs index cf13b86a616..a52dcdb10f4 100644 --- a/libs/error-stack/src/hook/context.rs +++ b/libs/error-stack/src/hook/context.rs @@ -1,6 +1,4 @@ -#[cfg_attr(feature = "std", allow(unused_imports))] -use alloc::boxed::Box; -use alloc::collections::BTreeMap; +use alloc::{boxed::Box, collections::BTreeMap}; use core::any::{Any, TypeId}; pub(crate) type Storage = BTreeMap>>; @@ -168,7 +166,7 @@ impl HookContext { pub fn cast(&mut self) -> &mut HookContext { // SAFETY: `HookContext` is marked as repr(transparent) and the changed generic is only used // inside of the `PhantomData` - unsafe { &mut *(self as *mut Self).cast::>() } + unsafe { &mut *core::ptr::from_mut(self).cast::>() } } } diff --git a/libs/error-stack/src/hook/mod.rs b/libs/error-stack/src/hook/mod.rs index bac6bc8f568..da872e493ee 100644 --- a/libs/error-stack/src/hook/mod.rs +++ b/libs/error-stack/src/hook/mod.rs @@ -1,6 +1,5 @@ pub(crate) mod context; -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::vec::Vec; use crate::{ diff --git a/libs/error-stack/src/iter.rs b/libs/error-stack/src/iter.rs index 40d058bd6b5..942a628c42c 100644 --- a/libs/error-stack/src/iter.rs +++ b/libs/error-stack/src/iter.rs @@ -1,6 +1,5 @@ //! Iterators over [`Frame`]s. -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::{vec, vec::Vec}; #[cfg(nightly)] use core::marker::PhantomData; diff --git a/libs/error-stack/src/lib.rs b/libs/error-stack/src/lib.rs index 450af14db33..62b758fa0d1 100644 --- a/libs/error-stack/src/lib.rs +++ b/libs/error-stack/src/lib.rs @@ -2,7 +2,7 @@ //! //! [![crates.io](https://img.shields.io/crates/v/error-stack)][crates.io] //! [![libs.rs](https://img.shields.io/badge/libs.rs-error--stack-orange)][libs.rs] -//! [![rust-version](https://img.shields.io/static/v1?label=Rust&message=1.63.0/nightly-2024-10-14&color=blue)][rust-version] +//! [![rust-version](https://img.shields.io/static/v1?label=Rust&message=1.83.0/nightly-2024-10-14&color=blue)][rust-version] //! //! [crates.io]: https://crates.io/crates/error-stack //! [libs.rs]: https://lib.rs/crates/error-stack @@ -486,11 +486,7 @@ //! [`SpanTrace`]: tracing_error::SpanTrace //! [`Stream`]: futures_core::Stream #![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr( - nightly, - feature(error_generic_member_access), - allow(clippy::incompatible_msrv) -)] +#![cfg_attr(nightly, feature(error_generic_member_access))] #![cfg_attr(all(nightly, feature = "unstable"), feature(try_trait_v2))] #![cfg_attr(all(doc, nightly), feature(doc_auto_cfg, doc_cfg))] #![cfg_attr(all(nightly, feature = "std"), feature(backtrace_frames))] @@ -546,7 +542,7 @@ pub use self::{ pub use self::{future::FutureExt, result::ResultExt}; #[cfg(test)] -#[allow(dead_code)] +#[expect(dead_code)] mod tests { use core::mem; diff --git a/libs/error-stack/src/report.rs b/libs/error-stack/src/report.rs index 6b58d69bf6d..29db15524b6 100644 --- a/libs/error-stack/src/report.rs +++ b/libs/error-stack/src/report.rs @@ -1,24 +1,18 @@ -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::{boxed::Box, vec, vec::Vec}; -#[cfg(rust_1_81)] -use core::error::Error; -use core::{fmt, marker::PhantomData, mem, panic::Location}; +use core::{error::Error, fmt, marker::PhantomData, mem, panic::Location}; #[cfg(feature = "backtrace")] use std::backtrace::{Backtrace, BacktraceStatus}; -#[cfg(all(feature = "std", not(rust_1_81)))] -use std::error::Error; #[cfg(feature = "std")] use std::process::ExitCode; #[cfg(feature = "spantrace")] use tracing_error::{SpanTrace, SpanTraceStatus}; -#[cfg(any(feature = "std", rust_1_81))] -use crate::context::SourceContext; #[cfg(nightly)] use crate::iter::{RequestRef, RequestValue}; use crate::{ Context, Frame, + context::SourceContext, iter::{Frames, FramesMut}, }; @@ -246,14 +240,14 @@ use crate::{ /// # } /// ``` #[must_use] -#[allow(clippy::field_scoped_visibility_modifiers)] +#[expect(clippy::field_scoped_visibility_modifiers)] pub struct Report { // The vector is boxed as this implies a memory footprint equal to a single pointer size // instead of three pointer sizes. Even for small `Result::Ok` variants, the `Result` would // still have at least the size of `Report`, even at the happy path. It's unexpected, that // creating or traversing a report will happen in the hot path, so a double indirection is // a good trade-off. - #[allow(clippy::box_collection)] + #[expect(clippy::box_collection)] pub(super) frames: Box>, _context: PhantomData *const C>, } @@ -268,12 +262,11 @@ impl Report { /// [`Backtrace` and `SpanTrace` section]: #backtrace-and-spantrace #[inline] #[track_caller] - #[allow(clippy::missing_panics_doc)] // Reason: No panic possible + #[expect(clippy::missing_panics_doc, reason = "No panic possible")] pub fn new(context: C) -> Self where C: Context, { - #[cfg(any(feature = "std", rust_1_81))] if let Some(mut current_source) = context.__source() { // The sources needs to be applied in reversed order, so we buffer them in a vector let mut sources = vec![SourceContext::from_error(current_source)]; @@ -327,7 +320,6 @@ impl Report { #[cfg(all(not(nightly), feature = "spantrace"))] let span_trace = Some(SpanTrace::capture()); - #[allow(unused_mut)] let mut report = Self { frames: Box::new(vec![frame]), _context: PhantomData, @@ -460,7 +452,6 @@ impl Report { /// Converts this `Report` to an [`Error`]. #[must_use] - #[cfg(any(feature = "std", rust_1_81))] pub fn into_error(self) -> impl Error + Send + Sync + 'static where C: 'static, @@ -470,7 +461,6 @@ impl Report { /// Returns this `Report` as an [`Error`]. #[must_use] - #[cfg(any(feature = "std", rust_1_81))] pub fn as_error(&self) -> &(impl Error + Send + Sync + 'static) where C: 'static, @@ -744,7 +734,6 @@ impl Report<[C]> { /// error1.push(error2); /// error1.push(error3); /// ``` - #[allow(clippy::same_name_method)] pub fn push(&mut self, mut report: Report) { self.frames.append(&mut report.frames); } @@ -868,28 +857,24 @@ impl Report<[C]> { } } -#[cfg(any(feature = "std", rust_1_81))] impl From> for Box { fn from(report: Report) -> Self { Box::new(report.into_error()) } } -#[cfg(any(feature = "std", rust_1_81))] impl From> for Box { fn from(report: Report) -> Self { Box::new(report.into_error()) } } -#[cfg(any(feature = "std", rust_1_81))] impl From> for Box { fn from(report: Report) -> Self { Box::new(report.into_error()) } } -#[cfg(any(feature = "std", rust_1_81))] impl From> for Box { fn from(report: Report) -> Self { Box::new(report.into_error()) diff --git a/libs/error-stack/src/serde.rs b/libs/error-stack/src/serde.rs index 8cb2d33b088..a4fb918b532 100644 --- a/libs/error-stack/src/serde.rs +++ b/libs/error-stack/src/serde.rs @@ -13,7 +13,6 @@ //! } //! ``` -#[cfg_attr(feature = "std", allow(unused_imports))] use alloc::{format, vec, vec::Vec}; use serde::{Serialize, Serializer, ser::SerializeMap}; @@ -29,7 +28,6 @@ impl Serialize for SerializeAttachment<'_> { { let Self(frame) = self; - #[allow(clippy::match_same_arms)] match frame.kind() { FrameKind::Context(_) => { // TODO: for now `Context` is unsupported, upcoming PR will fix via hooks diff --git a/libs/error-stack/src/sink.rs b/libs/error-stack/src/sink.rs index 5bb82e0dad5..d782f387295 100644 --- a/libs/error-stack/src/sink.rs +++ b/libs/error-stack/src/sink.rs @@ -72,7 +72,7 @@ impl Drop for Bomb { match self.0 { BombState::Panic => panic!("ReportSink was dropped without being consumed"), - #[allow(clippy::print_stderr)] + #[cfg_attr(not(feature = "tracing"), expect(clippy::print_stderr))] #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))] BombState::Warn(location) => { #[cfg(feature = "tracing")] @@ -548,7 +548,7 @@ mod test { #[test] #[should_panic(expected = "without being consumed")] fn panic_on_unused() { - #[allow(clippy::unnecessary_wraps)] + #[expect(clippy::unnecessary_wraps)] fn sink() -> Result<(), Report<[TestError]>> { let mut sink = ReportSink::new_armed(); @@ -562,7 +562,6 @@ mod test { #[test] fn panic_on_unused_with_defuse() { - #[allow(clippy::unnecessary_wraps)] fn sink() -> Result<(), Report<[TestError]>> { let mut sink = ReportSink::new_armed(); diff --git a/libs/error-stack/tests/common.rs b/libs/error-stack/tests/common.rs index 69991a4be2b..ffc6cf7c1e0 100644 --- a/libs/error-stack/tests/common.rs +++ b/libs/error-stack/tests/common.rs @@ -12,7 +12,7 @@ pub fn create_report() -> Report { extern crate alloc; use core::{any::TypeId, panic::Location}; -#[allow(unused_imports)] +#[expect(unused_imports)] use core::{ fmt, future::Future, @@ -21,13 +21,14 @@ use core::{ }; #[cfg(feature = "backtrace")] use std::backtrace::Backtrace; -#[allow(unused_imports)] -#[cfg(all(rust_1_80, feature = "std"))] +#[cfg(all(feature = "std", any(feature = "backtrace", feature = "spantrace")))] use std::sync::LazyLock; use error_stack::{AttachmentKind, Context, Frame, FrameKind, Report, Result}; -#[allow(unused_imports)] -#[cfg(not(all(rust_1_80, feature = "std")))] +#[cfg(all( + not(feature = "std"), + any(feature = "backtrace", feature = "spantrace") +))] use once_cell::sync::Lazy as LazyLock; #[cfg(feature = "spantrace")] use tracing_error::SpanTrace; @@ -240,8 +241,7 @@ pub fn remove_builtin_messages>( .into_iter() .filter_map(|message| { let message = message.as_ref(); - // Reason: complexity + readability - #[allow(clippy::if_then_some_else_none)] + #[expect(clippy::if_then_some_else_none, reason = "complexity + readability")] if message != "Location" && message != "Backtrace" && message != "SpanTrace" { Some(message.to_owned()) } else { @@ -267,8 +267,10 @@ pub fn remove_builtin_frames(report: &Report) -> impl Iterator usize { #[cfg(feature = "backtrace")] if supports_backtrace() { @@ -335,7 +337,11 @@ pub fn expect_count(mut count: usize) -> usize { /// ``` /// /// This is simplified pseudo-code to illustrate how the macro works. -#[allow(unused_macros)] +#[expect( + clippy::allow_attributes, + reason = "It's not possible to avoid this warning" +)] +#[allow(unused_macros, reason = "Only used in some tests")] macro_rules! assert_kinds { ($report:ident, [ $($pattern:pat_param),* diff --git a/libs/error-stack/tests/snapshots/test_debug__full__complex.snap b/libs/error-stack/tests/snapshots/test_debug__full__complex.snap index 251d2d3ad0d..b85e69685a2 100644 --- a/libs/error-stack/tests/snapshots/test_debug__full__complex.snap +++ b/libs/error-stack/tests/snapshots/test_debug__full__complex.snap @@ -3,7 +3,7 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:?}\")" --- context A -├╴at libs/error-stack/tests/test_debug.rs:431:14 +├╴at libs/error-stack/tests/test_debug.rs:433:14 ├╴printable A │ ╰┬▶ root error diff --git a/libs/error-stack/tests/snapshots/test_debug__full__hook_provider.snap b/libs/error-stack/tests/snapshots/test_debug__full__hook_provider.snap index a94715e7e13..49cb24ae225 100644 --- a/libs/error-stack/tests/snapshots/test_debug__full__hook_provider.snap +++ b/libs/error-stack/tests/snapshots/test_debug__full__hook_provider.snap @@ -5,7 +5,7 @@ expression: "format!(\"{report:?}\")" context D ├╴usize: 420 ├╴&'static str: Invalid User Input -├╴at libs/error-stack/tests/test_debug.rs:587:38 +├╴at libs/error-stack/tests/test_debug.rs:588:38 │ ╰─▶ root error ├╴at libs/error-stack/tests/common.rs:9:5 diff --git a/libs/error-stack/tests/snapshots/test_debug__full__linear.snap b/libs/error-stack/tests/snapshots/test_debug__full__linear.snap index a629317a329..59ad2beae0e 100644 --- a/libs/error-stack/tests/snapshots/test_debug__full__linear.snap +++ b/libs/error-stack/tests/snapshots/test_debug__full__linear.snap @@ -3,11 +3,11 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:?}\")" --- context B -├╴at libs/error-stack/tests/test_debug.rs:276:14 +├╴at libs/error-stack/tests/test_debug.rs:278:14 ├╴printable C │ ├─▶ context A -│ ├╴at libs/error-stack/tests/test_debug.rs:273:14 +│ ├╴at libs/error-stack/tests/test_debug.rs:275:14 │ ├╴printable B │ ╰╴1 additional opaque attachment │ diff --git a/libs/error-stack/tests/snapshots/test_debug__full__linear_ext.snap b/libs/error-stack/tests/snapshots/test_debug__full__linear_ext.snap index 0f572464c37..a1a78e3f998 100644 --- a/libs/error-stack/tests/snapshots/test_debug__full__linear_ext.snap +++ b/libs/error-stack/tests/snapshots/test_debug__full__linear_ext.snap @@ -3,11 +3,11 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:#?}\")" --- context B -├╴at libs/error-stack/tests/test_debug.rs:293:14 +├╴at libs/error-stack/tests/test_debug.rs:295:14 ├╴printable C │ ├─▶ context A -│ ├╴at libs/error-stack/tests/test_debug.rs:290:14 +│ ├╴at libs/error-stack/tests/test_debug.rs:292:14 │ ├╴printable B │ ╰╴1 additional opaque attachment │ diff --git a/libs/error-stack/tests/snapshots/test_debug__full__multiline_context.snap b/libs/error-stack/tests/snapshots/test_debug__full__multiline_context.snap index fca3a4a3788..b1f993b39ef 100644 --- a/libs/error-stack/tests/snapshots/test_debug__full__multiline_context.snap +++ b/libs/error-stack/tests/snapshots/test_debug__full__multiline_context.snap @@ -3,20 +3,20 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:#?}\")" --- context B -├╴at libs/error-stack/tests/test_debug.rs:318:14 +├╴at libs/error-stack/tests/test_debug.rs:320:14 ├╴printable C │ ├─▶ A multiline │ context that might have │ a bit more info -│ ├╴at libs/error-stack/tests/test_debug.rs:315:14 +│ ├╴at libs/error-stack/tests/test_debug.rs:317:14 │ ├╴printable B │ ╰╴1 additional opaque attachment │ ╰─▶ A multiline context that might have a bit more info - ├╴at libs/error-stack/tests/test_debug.rs:314:22 + ├╴at libs/error-stack/tests/test_debug.rs:316:22 ╰╴backtrace (1) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/libs/error-stack/tests/snapshots/test_debug__full__sources.snap b/libs/error-stack/tests/snapshots/test_debug__full__sources.snap index 1e8e62d140e..c935fd87726 100644 --- a/libs/error-stack/tests/snapshots/test_debug__full__sources.snap +++ b/libs/error-stack/tests/snapshots/test_debug__full__sources.snap @@ -3,7 +3,7 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:?}\")" --- context A -├╴at libs/error-stack/tests/test_debug.rs:363:14 +├╴at libs/error-stack/tests/test_debug.rs:365:14 ├╴1 additional opaque attachment │ ╰┬▶ root error diff --git a/libs/error-stack/tests/snapshots/test_debug__full__sources_transparent.snap b/libs/error-stack/tests/snapshots/test_debug__full__sources_transparent.snap index 549cb7def5e..efb0ec06a9b 100644 --- a/libs/error-stack/tests/snapshots/test_debug__full__sources_transparent.snap +++ b/libs/error-stack/tests/snapshots/test_debug__full__sources_transparent.snap @@ -3,7 +3,7 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:?}\")" --- context A -├╴at libs/error-stack/tests/test_debug.rs:404:18 +├╴at libs/error-stack/tests/test_debug.rs:406:18 ├╴1 additional opaque attachment │ ╰┬▶ root error diff --git a/libs/error-stack/tests/snapshots/test_debug__sources_nested.snap b/libs/error-stack/tests/snapshots/test_debug__sources_nested.snap index 8c86c1c8d9a..e1a1201a27f 100644 --- a/libs/error-stack/tests/snapshots/test_debug__sources_nested.snap +++ b/libs/error-stack/tests/snapshots/test_debug__sources_nested.snap @@ -3,12 +3,12 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:?}\")" --- context A -├╴at libs/error-stack/tests/test_debug.rs:208:10 +├╴at libs/error-stack/tests/test_debug.rs:211:10 ├╴2 ├╴1 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:201:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:204:10 │ ├╴4 │ ├╴3 │ │ @@ -17,16 +17,16 @@ context A │ ╰╴6 │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:196:10 + ├╴at libs/error-stack/tests/test_debug.rs:199:10 ├╴5 ├╴3 │ ├─▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:194:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:197:10 │ ╰╴7 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:185:34 + │ ├╴at libs/error-stack/tests/test_debug.rs:188:34 │ ├╴9 │ ├╴8 │ │ @@ -34,7 +34,7 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:178:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:181:10 │ ├╴13 │ ├╴10 │ ├╴16 @@ -45,7 +45,7 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:164:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:167:10 │ ├╴15 │ ├╴14 │ ├╴10 @@ -57,7 +57,7 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:174:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:177:10 │ ├╴11 │ ├╴9 │ ├╴8 @@ -66,13 +66,13 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:170:10 + ├╴at libs/error-stack/tests/test_debug.rs:173:10 ├╴12 ├╴9 ├╴8 │ ├─▶ context A - │ ╰╴at libs/error-stack/tests/test_debug.rs:169:10 + │ ╰╴at libs/error-stack/tests/test_debug.rs:172:10 │ ╰─▶ root error ╰╴at libs/error-stack/tests/common.rs:9:5 diff --git a/libs/error-stack/tests/snapshots/test_debug__sources_nested@spantrace-backtrace.snap b/libs/error-stack/tests/snapshots/test_debug__sources_nested@spantrace-backtrace.snap index 44103ea61f6..5f483010cc5 100644 --- a/libs/error-stack/tests/snapshots/test_debug__sources_nested@spantrace-backtrace.snap +++ b/libs/error-stack/tests/snapshots/test_debug__sources_nested@spantrace-backtrace.snap @@ -3,12 +3,12 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:?}\")" --- context A -├╴at libs/error-stack/tests/test_debug.rs:208:10 +├╴at libs/error-stack/tests/test_debug.rs:211:10 ├╴2 ├╴1 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:201:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:204:10 │ ├╴4 │ ├╴3 │ │ @@ -19,16 +19,16 @@ context A │ ╰╴6 │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:196:10 + ├╴at libs/error-stack/tests/test_debug.rs:199:10 ├╴5 ├╴3 │ ├─▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:194:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:197:10 │ ╰╴7 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:185:34 + │ ├╴at libs/error-stack/tests/test_debug.rs:188:34 │ ├╴9 │ ├╴8 │ │ @@ -38,7 +38,7 @@ context A │ ╰╴span trace with 2 frames (2) │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:178:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:181:10 │ ├╴13 │ ├╴10 │ ├╴16 @@ -51,7 +51,7 @@ context A │ ╰╴span trace with 2 frames (3) │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:164:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:167:10 │ ├╴15 │ ├╴14 │ ├╴10 @@ -65,7 +65,7 @@ context A │ ╰╴span trace with 2 frames (4) │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:174:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:177:10 │ ├╴11 │ ├╴9 │ ├╴8 @@ -76,13 +76,13 @@ context A │ ╰╴span trace with 2 frames (5) │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:170:10 + ├╴at libs/error-stack/tests/test_debug.rs:173:10 ├╴12 ├╴9 ├╴8 │ ├─▶ context A - │ ╰╴at libs/error-stack/tests/test_debug.rs:169:10 + │ ╰╴at libs/error-stack/tests/test_debug.rs:172:10 │ ╰─▶ root error ├╴at libs/error-stack/tests/common.rs:9:5 diff --git a/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate.snap b/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate.snap index 525ace7cd56..014ffbff719 100644 --- a/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate.snap +++ b/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate.snap @@ -3,12 +3,12 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:#?}\")" --- context A -├╴at libs/error-stack/tests/test_debug.rs:208:10 +├╴at libs/error-stack/tests/test_debug.rs:211:10 ├╴2 ├╴1 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:201:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:204:10 │ ├╴4 │ ├╴3 │ │ @@ -17,16 +17,16 @@ context A │ ╰╴6 │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:196:10 + ├╴at libs/error-stack/tests/test_debug.rs:199:10 ├╴5 ├╴3 │ ├─▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:194:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:197:10 │ ╰╴7 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:185:34 + │ ├╴at libs/error-stack/tests/test_debug.rs:188:34 │ ├╴9 │ ├╴8 │ │ @@ -34,7 +34,7 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:178:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:181:10 │ ├╴13 │ ├╴10 │ ├╴16 @@ -45,7 +45,7 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:164:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:167:10 │ ├╴15 │ ├╴14 │ ├╴10 @@ -57,7 +57,7 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:174:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:177:10 │ ├╴11 │ ├╴9 │ ├╴8 @@ -66,13 +66,13 @@ context A │ ╰╴at libs/error-stack/tests/common.rs:9:5 │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:170:10 + ├╴at libs/error-stack/tests/test_debug.rs:173:10 ├╴12 ├╴9 ├╴8 │ ├─▶ context A - │ ╰╴at libs/error-stack/tests/test_debug.rs:169:10 + │ ╰╴at libs/error-stack/tests/test_debug.rs:172:10 │ ╰─▶ root error ╰╴at libs/error-stack/tests/common.rs:9:5 diff --git a/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate@spantrace-backtrace.snap b/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate@spantrace-backtrace.snap index 2c92af81e2d..01cd341c8fa 100644 --- a/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate@spantrace-backtrace.snap +++ b/libs/error-stack/tests/snapshots/test_debug__sources_nested_alternate@spantrace-backtrace.snap @@ -3,12 +3,12 @@ source: libs/error-stack/tests/test_debug.rs expression: "format!(\"{report:#?}\")" --- context A -├╴at libs/error-stack/tests/test_debug.rs:208:10 +├╴at libs/error-stack/tests/test_debug.rs:211:10 ├╴2 ├╴1 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:201:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:204:10 │ ├╴4 │ ├╴3 │ │ @@ -19,16 +19,16 @@ context A │ ╰╴6 │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:196:10 + ├╴at libs/error-stack/tests/test_debug.rs:199:10 ├╴5 ├╴3 │ ├─▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:194:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:197:10 │ ╰╴7 │ ╰┬▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:185:34 + │ ├╴at libs/error-stack/tests/test_debug.rs:188:34 │ ├╴9 │ ├╴8 │ │ @@ -38,7 +38,7 @@ context A │ ╰╴span trace with 2 frames (2) │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:178:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:181:10 │ ├╴13 │ ├╴10 │ ├╴16 @@ -51,7 +51,7 @@ context A │ ╰╴span trace with 2 frames (3) │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:164:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:167:10 │ ├╴15 │ ├╴14 │ ├╴10 @@ -65,7 +65,7 @@ context A │ ╰╴span trace with 2 frames (4) │ ├▶ context A - │ ├╴at libs/error-stack/tests/test_debug.rs:174:10 + │ ├╴at libs/error-stack/tests/test_debug.rs:177:10 │ ├╴11 │ ├╴9 │ ├╴8 @@ -76,13 +76,13 @@ context A │ ╰╴span trace with 2 frames (5) │ ╰▶ context A - ├╴at libs/error-stack/tests/test_debug.rs:170:10 + ├╴at libs/error-stack/tests/test_debug.rs:173:10 ├╴12 ├╴9 ├╴8 │ ├─▶ context A - │ ╰╴at libs/error-stack/tests/test_debug.rs:169:10 + │ ╰╴at libs/error-stack/tests/test_debug.rs:172:10 │ ╰─▶ root error ├╴at libs/error-stack/tests/common.rs:9:5 diff --git a/libs/error-stack/tests/test_attach.rs b/libs/error-stack/tests/test_attach.rs index c75ba4857d6..ece13a1a3af 100644 --- a/libs/error-stack/tests/test_attach.rs +++ b/libs/error-stack/tests/test_attach.rs @@ -3,7 +3,6 @@ #[macro_use] mod common; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::{AttachmentKind, FrameKind, FutureExt, Report, ResultExt}; diff --git a/libs/error-stack/tests/test_attach_printable.rs b/libs/error-stack/tests/test_attach_printable.rs index ff521e61d79..32d0f4e4717 100644 --- a/libs/error-stack/tests/test_attach_printable.rs +++ b/libs/error-stack/tests/test_attach_printable.rs @@ -3,7 +3,6 @@ #[macro_use] mod common; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::{AttachmentKind, FrameKind, FutureExt, Report, ResultExt}; diff --git a/libs/error-stack/tests/test_backtrace.rs b/libs/error-stack/tests/test_backtrace.rs index 8294d1a43e9..a7239a7ab41 100644 --- a/libs/error-stack/tests/test_backtrace.rs +++ b/libs/error-stack/tests/test_backtrace.rs @@ -9,7 +9,6 @@ mod common; use std::backtrace::Backtrace; -#[allow(clippy::wildcard_imports)] use common::*; #[cfg(nightly)] use error_stack::Report; diff --git a/libs/error-stack/tests/test_change_context.rs b/libs/error-stack/tests/test_change_context.rs index 023ad7b07f4..2f19b8456e7 100644 --- a/libs/error-stack/tests/test_change_context.rs +++ b/libs/error-stack/tests/test_change_context.rs @@ -3,7 +3,6 @@ #[macro_use] mod common; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::{AttachmentKind, FrameKind, FutureExt, Report, ResultExt}; diff --git a/libs/error-stack/tests/test_compatibility.rs b/libs/error-stack/tests/test_compatibility.rs index cb445a6c323..f98ec63a7d1 100644 --- a/libs/error-stack/tests/test_compatibility.rs +++ b/libs/error-stack/tests/test_compatibility.rs @@ -8,7 +8,6 @@ mod common; #[cfg(nightly)] use core::error; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::IntoReportCompat; #[cfg(all(nightly, feature = "backtrace"))] @@ -46,14 +45,13 @@ fn anyhow() { .attach_printable(PrintableA(0)) .attach_printable(PrintableB(0)); - #[allow(unused_mut)] + #[expect(unused_mut)] let mut report_messages = messages(&report); let anyhow_report = anyhow .into_report() .expect_err("should have returned error"); - #[allow(unused_mut)] - let mut anyhow_messages = messages(&anyhow_report); + let anyhow_messages = messages(&anyhow_report); assert_eq!( remove_builtin_messages(anyhow_messages.into_iter().rev()), @@ -157,13 +155,10 @@ fn eyre() { let report = create_report() .attach_printable(PrintableA(0)) .attach_printable(PrintableB(0)); - - #[allow(unused_mut)] - let mut report_messages = messages(&report); + let report_messages = messages(&report); let eyre_report = eyre.into_report().expect_err("should have returned error"); - #[allow(unused_mut)] - let mut eyre_messages = messages(&eyre_report); + let eyre_messages = messages(&eyre_report); assert_eq!( remove_builtin_messages(eyre_messages.into_iter().rev()), diff --git a/libs/error-stack/tests/test_debug.rs b/libs/error-stack/tests/test_debug.rs index 825d3fd360c..b4abad376cf 100644 --- a/libs/error-stack/tests/test_debug.rs +++ b/libs/error-stack/tests/test_debug.rs @@ -3,11 +3,11 @@ #![allow(clippy::std_instead_of_core)] mod common; -#[allow(clippy::wildcard_imports)] use common::*; -#[allow(unused_imports)] -use error_stack::Report; -use error_stack::fmt::{Charset, ColorMode}; +use error_stack::{ + Report, + fmt::{Charset, ColorMode}, +}; use insta::assert_snapshot; #[cfg(feature = "spantrace")] use tracing_error::ErrorLayer; @@ -50,7 +50,10 @@ fn setup() { } fn snap_suffix() -> String { - #[allow(unused_mut)] + #[cfg_attr( + not(any(feature = "backtrace", feature = "spantrace")), + expect(unused_mut) + )] let mut suffix: Vec<&'static str> = vec![]; #[cfg(feature = "spantrace")] @@ -259,7 +262,6 @@ mod full { panic::Location, }; - #[allow(clippy::wildcard_imports)] use super::*; #[test] @@ -571,7 +573,6 @@ mod full { } #[cfg(nightly)] - #[expect(clippy::incompatible_msrv, reason = "This is gated behind nightly")] impl Error for ContextD { fn provide<'a>(&'a self, request: &mut Request<'a>) { request.provide_ref(&self.code); diff --git a/libs/error-stack/tests/test_display.rs b/libs/error-stack/tests/test_display.rs index 01e2251c2b3..5716653f87f 100644 --- a/libs/error-stack/tests/test_display.rs +++ b/libs/error-stack/tests/test_display.rs @@ -2,7 +2,6 @@ mod common; -#[allow(clippy::wildcard_imports)] use common::*; #[test] diff --git a/libs/error-stack/tests/test_downcast.rs b/libs/error-stack/tests/test_downcast.rs index a15c1279f97..002eb71a439 100644 --- a/libs/error-stack/tests/test_downcast.rs +++ b/libs/error-stack/tests/test_downcast.rs @@ -2,7 +2,6 @@ mod common; -#[allow(clippy::wildcard_imports)] use common::*; #[test] diff --git a/libs/error-stack/tests/test_extend.rs b/libs/error-stack/tests/test_extend.rs index f55476e2d98..d0ae0bdf6de 100644 --- a/libs/error-stack/tests/test_extend.rs +++ b/libs/error-stack/tests/test_extend.rs @@ -3,7 +3,6 @@ mod common; use core::fmt::{Display, Formatter}; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::{Context, Report, report}; diff --git a/libs/error-stack/tests/test_frame.rs b/libs/error-stack/tests/test_frame.rs index 7ada93128aa..baadbae1fad 100644 --- a/libs/error-stack/tests/test_frame.rs +++ b/libs/error-stack/tests/test_frame.rs @@ -4,7 +4,6 @@ mod common; use core::{any::TypeId, iter::zip, panic::Location}; -#[allow(clippy::wildcard_imports)] use common::*; #[test] diff --git a/libs/error-stack/tests/test_iter.rs b/libs/error-stack/tests/test_iter.rs index 6cb61490d21..749b3a2dafa 100644 --- a/libs/error-stack/tests/test_iter.rs +++ b/libs/error-stack/tests/test_iter.rs @@ -40,7 +40,6 @@ impl Context for Char {} /// G /// ╰─▶ H /// ``` -#[allow(clippy::many_single_char_names)] fn build() -> Report<[Char]> { let mut report_c = report!(Char('C')).expand(); let report_d = report!(Char('D')); diff --git a/libs/error-stack/tests/test_macros.rs b/libs/error-stack/tests/test_macros.rs index 9b0a30885ec..e1c9ec1a65a 100644 --- a/libs/error-stack/tests/test_macros.rs +++ b/libs/error-stack/tests/test_macros.rs @@ -2,7 +2,6 @@ mod common; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::{bail, ensure, report}; diff --git a/libs/error-stack/tests/test_provision.rs b/libs/error-stack/tests/test_provision.rs index 23be8f431eb..2ac80d5a468 100644 --- a/libs/error-stack/tests/test_provision.rs +++ b/libs/error-stack/tests/test_provision.rs @@ -3,7 +3,6 @@ mod common; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::Report; diff --git a/libs/error-stack/tests/test_span_trace.rs b/libs/error-stack/tests/test_span_trace.rs index 50dafa51679..e8f36bf6234 100644 --- a/libs/error-stack/tests/test_span_trace.rs +++ b/libs/error-stack/tests/test_span_trace.rs @@ -3,7 +3,6 @@ mod common; -#[allow(clippy::wildcard_imports)] use common::*; use error_stack::Result; use tracing_error::{ErrorLayer, SpanTrace}; diff --git a/yarn.lock b/yarn.lock index eb593b2c801..43ada3f981b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7400,12 +7400,12 @@ require-from-string "^2.0.2" uri-js-replace "^1.0.1" -"@redocly/cli@1.25.5": - version "1.25.5" - resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.25.5.tgz#258f6d23e8298814518ec4d24d023c1e21e3b081" - integrity sha512-sFh4A8wqwuig7mF/nYNVIyxSfKKZikWC+uVH6OB1IepYQXNsHFaLAU1VaNI9gS5mMvWmYx5SEuSCVB9LaNFBhw== +"@redocly/cli@1.25.6": + version "1.25.6" + resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.25.6.tgz#71533fe95f5a49ba481eb7cd8a2a4f1c053d8b9c" + integrity sha512-oA/0a4pee8u0nP8Gme2Zz8CuAPRjvdioWY+6Szp9YjdMNgWEag2LCXBiJdABLqnnUeawPeuzAQkBM/fqMCjsUQ== dependencies: - "@redocly/openapi-core" "1.25.5" + "@redocly/openapi-core" "1.25.6" abort-controller "^3.0.0" chokidar "^3.5.1" colorette "^1.2.0" @@ -7430,10 +7430,10 @@ resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.12.1.tgz#7b905a17d710244550ef826542d0db164d5ace02" integrity sha512-RW3rSirfsPdr0uvATijRDU3f55SuZV3m7/ppdTDvGw4IB0cmeZRkFmqTrchxMqWP50Gfg1tpHnjdxUCNo0E2qg== -"@redocly/openapi-core@1.25.5", "@redocly/openapi-core@^1.4.0": - version "1.25.5" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.25.5.tgz#443b1488c8ef1ddcb8f407c3e7dd8cb7b388b427" - integrity sha512-BNgXjqesJu4L5f8F73c2hkkH5IdvjYCKYFgIl+m9oNgqGRIPBJjtiEGOx7jkQ6nElN4311z7Z4aTECtklaaHwg== +"@redocly/openapi-core@1.25.6", "@redocly/openapi-core@^1.4.0": + version "1.25.6" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.25.6.tgz#e5afe8651ed96a88a11c3ccff81b4ab10f55d50c" + integrity sha512-6MolUvqYNepxgXts9xRONvX6I1yq63B/hct1zyRrLgWM2QjmFhhS2yCZxELwWZfGO1OmzqutDaqsoFqB+LYJGg== dependencies: "@redocly/ajv" "^8.11.2" "@redocly/config" "^0.12.1"