From 97cda1a5c7299d564ee5d3a369987b574830df8e Mon Sep 17 00:00:00 2001 From: declark1 <44146800+declark1@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:07:53 -0700 Subject: [PATCH 1/2] Add Code, apply gRPC service changes to drop tonic, drop CodeExt Signed-off-by: declark1 <44146800+declark1@users.noreply.github.com> --- mocktail/Cargo.toml | 1 - mocktail/src/ext.rs | 34 ---- mocktail/src/headers.rs | 6 - mocktail/src/lib.rs | 7 +- mocktail/src/mock_builder/then.rs | 3 +- mocktail/src/response.rs | 181 +---------------- mocktail/src/service/grpc.rs | 13 +- mocktail/src/service/http.rs | 7 +- mocktail/src/status.rs | 309 ++++++++++++++++++++++++++++++ 9 files changed, 328 insertions(+), 233 deletions(-) create mode 100644 mocktail/src/status.rs diff --git a/mocktail/Cargo.toml b/mocktail/Cargo.toml index f7aec6e..c72f564 100644 --- a/mocktail/Cargo.toml +++ b/mocktail/Cargo.toml @@ -31,7 +31,6 @@ serde_json = "1" thiserror = "2" tokio = "1" tokio-stream = "0" -tonic = "0.12" tracing = "0" url = "2" uuid = { version = "1.16.0", features = ["fast-rng", "v7"] } diff --git a/mocktail/src/ext.rs b/mocktail/src/ext.rs index fd1e57b..22d86f3 100644 --- a/mocktail/src/ext.rs +++ b/mocktail/src/ext.rs @@ -1,41 +1,7 @@ //! Extension traits use bytes::{BufMut, Bytes, BytesMut}; -use http::status::InvalidStatusCode; use prost::Message; -pub trait CodeExt { - /// Creates a gRPC status code from an equivalent HTTP status code. - fn from_u16(code: u16) -> Result; - /// Creates a gRPC status code from an equivalent [`http::StatusCode`]. - fn from_http(status_code: http::StatusCode) -> tonic::Code; -} - -impl CodeExt for tonic::Code { - fn from_u16(code: u16) -> Result { - let status_code = http::StatusCode::from_u16(code)?; - Ok(tonic::Code::from_http(status_code)) - } - - fn from_http(status_code: http::StatusCode) -> tonic::Code { - match status_code { - http::StatusCode::INTERNAL_SERVER_ERROR => tonic::Code::Internal, - http::StatusCode::UNPROCESSABLE_ENTITY | http::StatusCode::BAD_REQUEST => { - tonic::Code::InvalidArgument - } - http::StatusCode::UNAUTHORIZED => tonic::Code::Unauthenticated, - http::StatusCode::FORBIDDEN => tonic::Code::PermissionDenied, - http::StatusCode::NOT_FOUND => tonic::Code::NotFound, - http::StatusCode::NOT_IMPLEMENTED => tonic::Code::Unimplemented, - http::StatusCode::TOO_MANY_REQUESTS - | http::StatusCode::BAD_GATEWAY - | http::StatusCode::SERVICE_UNAVAILABLE - | http::StatusCode::GATEWAY_TIMEOUT => tonic::Code::Unavailable, - http::StatusCode::OK => tonic::Code::Ok, - _ => tonic::Code::Unknown, - } - } -} - pub trait MessageExt { /// Encodes the messages to bytes for a HTTP body. fn to_bytes(&self) -> Bytes; diff --git a/mocktail/src/headers.rs b/mocktail/src/headers.rs index 434971c..aa061af 100644 --- a/mocktail/src/headers.rs +++ b/mocktail/src/headers.rs @@ -132,12 +132,6 @@ impl From<&http::HeaderMap> for Headers { } } -impl From for tonic::metadata::MetadataMap { - fn from(value: Headers) -> Self { - tonic::metadata::MetadataMap::from_headers(value.into()) - } -} - /// Represents a HTTP header name. #[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct HeaderName(String); diff --git a/mocktail/src/lib.rs b/mocktail/src/lib.rs index a3fb185..add8bdc 100644 --- a/mocktail/src/lib.rs +++ b/mocktail/src/lib.rs @@ -11,8 +11,10 @@ pub use mock_set::MockSet; mod request; pub use request::{Method, Request}; mod response; -pub use response::{Response, StatusCode}; +pub use response::Response; pub mod server; +mod status; +pub use status::{Code, StatusCode}; pub mod prelude { pub use crate::{ body::Body, @@ -21,8 +23,9 @@ pub mod prelude { mock::Mock, mock_set::MockSet, request::{Method, Request}, - response::{Response, StatusCode}, + response::Response, server::MockServer, + status::{Code, StatusCode}, }; } mod ext; diff --git a/mocktail/src/mock_builder/then.rs b/mocktail/src/mock_builder/then.rs index f1d3f3e..3f6b269 100644 --- a/mocktail/src/mock_builder/then.rs +++ b/mocktail/src/mock_builder/then.rs @@ -6,7 +6,8 @@ use bytes::Bytes; use crate::{ body::Body, headers::{HeaderName, HeaderValue, Headers}, - response::{Response, StatusCode}, + response::Response, + status::StatusCode, }; /// A response builder. diff --git a/mocktail/src/response.rs b/mocktail/src/response.rs index 89e1d21..80ec72f 100644 --- a/mocktail/src/response.rs +++ b/mocktail/src/response.rs @@ -1,8 +1,5 @@ //! Mock response -use std::num::NonZeroU16; - -use super::{body::Body, headers::Headers}; -use crate::{ext::CodeExt, Error}; +use super::{body::Body, headers::Headers, status::StatusCode}; /// Represents a HTTP response. #[derive(Debug, Clone, PartialEq)] @@ -73,179 +70,3 @@ impl Default for Response { } } } - -/// Represents a HTTP status code. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct StatusCode(NonZeroU16); - -impl StatusCode { - pub fn from_u16(code: u16) -> Result { - if !(100..1000).contains(&code) { - return Err(Error::Invalid("invalid status code".into())); - } - Ok(Self(NonZeroU16::new(code).unwrap())) - } - - pub fn as_u16(&self) -> u16 { - self.0.get() - } - - pub fn is_informational(&self) -> bool { - (100..200).contains(&self.as_u16()) - } - - pub fn is_success(&self) -> bool { - (200..300).contains(&self.as_u16()) - } - - pub fn is_redirection(&self) -> bool { - (300..400).contains(&self.as_u16()) - } - - pub fn is_error(&self) -> bool { - (400..600).contains(&self.as_u16()) - } - - pub fn is_ok(&self) -> bool { - self.is_success() - } - - pub fn as_http(&self) -> http::StatusCode { - http::StatusCode::from_u16(self.as_u16()).unwrap() - } - - pub fn as_grpc(&self) -> tonic::Code { - tonic::Code::from_u16(self.as_u16()).unwrap() - } - - pub fn as_grpc_i32(&self) -> i32 { - self.as_grpc() as i32 - } -} - -impl StatusCode { - pub const CONTINUE: StatusCode = StatusCode(NonZeroU16::new(100).unwrap()); - pub const SWITCHING_PROTOCOLS: StatusCode = StatusCode(NonZeroU16::new(101).unwrap()); - pub const PROCESSING: StatusCode = StatusCode(NonZeroU16::new(102).unwrap()); - pub const EARLY_HINTS: StatusCode = StatusCode(NonZeroU16::new(103).unwrap()); - - pub const OK: StatusCode = StatusCode(NonZeroU16::new(200).unwrap()); - pub const CREATED: StatusCode = StatusCode(NonZeroU16::new(201).unwrap()); - pub const ACCEPTED: StatusCode = StatusCode(NonZeroU16::new(202).unwrap()); - pub const NON_AUTHORITATIVE_INFORMATION: StatusCode = StatusCode(NonZeroU16::new(203).unwrap()); - pub const NO_CONTENT: StatusCode = StatusCode(NonZeroU16::new(204).unwrap()); - pub const RESET_CONTENT: StatusCode = StatusCode(NonZeroU16::new(205).unwrap()); - pub const PARTIAL_CONTENT: StatusCode = StatusCode(NonZeroU16::new(206).unwrap()); - pub const MULTI_STATUS: StatusCode = StatusCode(NonZeroU16::new(207).unwrap()); - pub const ALREADY_REPORTED: StatusCode = StatusCode(NonZeroU16::new(208).unwrap()); - pub const IM_USED: StatusCode = StatusCode(NonZeroU16::new(226).unwrap()); - - pub const MULTIPLE_CHOICES: StatusCode = StatusCode(NonZeroU16::new(300).unwrap()); - pub const MOVED_PERMANENTLY: StatusCode = StatusCode(NonZeroU16::new(301).unwrap()); - pub const FOUND: StatusCode = StatusCode(NonZeroU16::new(302).unwrap()); - pub const SEE_OTHER: StatusCode = StatusCode(NonZeroU16::new(303).unwrap()); - pub const NOT_MODIFIED: StatusCode = StatusCode(NonZeroU16::new(304).unwrap()); - pub const USE_PROXY: StatusCode = StatusCode(NonZeroU16::new(305).unwrap()); - pub const TEMPORARY_REDIRECT: StatusCode = StatusCode(NonZeroU16::new(307).unwrap()); - pub const PERMANENT_REDIRECT: StatusCode = StatusCode(NonZeroU16::new(308).unwrap()); - - pub const BAD_REQUEST: StatusCode = StatusCode(NonZeroU16::new(400).unwrap()); - pub const UNAUTHORIZED: StatusCode = StatusCode(NonZeroU16::new(401).unwrap()); - pub const PAYMENT_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(402).unwrap()); - pub const FORBIDDEN: StatusCode = StatusCode(NonZeroU16::new(403).unwrap()); - pub const NOT_FOUND: StatusCode = StatusCode(NonZeroU16::new(404).unwrap()); - pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(NonZeroU16::new(405).unwrap()); - pub const NOT_ACCEPTABLE: StatusCode = StatusCode(NonZeroU16::new(406).unwrap()); - pub const PROXY_AUTHENTICATION_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(407).unwrap()); - pub const REQUEST_TIMEOUT: StatusCode = StatusCode(NonZeroU16::new(408).unwrap()); - pub const CONFLICT: StatusCode = StatusCode(NonZeroU16::new(409).unwrap()); - pub const GONE: StatusCode = StatusCode(NonZeroU16::new(410).unwrap()); - pub const LENGTH_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(411).unwrap()); - pub const PRECONDITION_FAILED: StatusCode = StatusCode(NonZeroU16::new(412).unwrap()); - pub const PAYLOAD_TOO_LARGE: StatusCode = StatusCode(NonZeroU16::new(413).unwrap()); - pub const URI_TOO_LONG: StatusCode = StatusCode(NonZeroU16::new(414).unwrap()); - pub const UNSUPPORTED_MEDIA_TYPE: StatusCode = StatusCode(NonZeroU16::new(415).unwrap()); - pub const RANGE_NOT_SATISFIABLE: StatusCode = StatusCode(NonZeroU16::new(416).unwrap()); - pub const EXPECTATION_FAILED: StatusCode = StatusCode(NonZeroU16::new(417).unwrap()); - pub const IM_A_TEAPOT: StatusCode = StatusCode(NonZeroU16::new(418).unwrap()); - pub const MISDIRECTED_REQUEST: StatusCode = StatusCode(NonZeroU16::new(421).unwrap()); - pub const UNPROCESSABLE_ENTITY: StatusCode = StatusCode(NonZeroU16::new(422).unwrap()); - pub const LOCKED: StatusCode = StatusCode(NonZeroU16::new(423).unwrap()); - pub const FAILED_DEPENDENCY: StatusCode = StatusCode(NonZeroU16::new(424).unwrap()); - pub const TOO_EARLY: StatusCode = StatusCode(NonZeroU16::new(425).unwrap()); - pub const UPGRADE_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(426).unwrap()); - pub const PRECONDITION_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(428).unwrap()); - pub const TOO_MANY_REQUESTS: StatusCode = StatusCode(NonZeroU16::new(429).unwrap()); - pub const REQUEST_HEADER_FIELDS_TOO_LARGE: StatusCode = - StatusCode(NonZeroU16::new(431).unwrap()); - pub const UNAVAILABLE_FOR_LEGAL_REASONS: StatusCode = StatusCode(NonZeroU16::new(451).unwrap()); - - pub const INTERNAL_SERVER_ERROR: StatusCode = StatusCode(NonZeroU16::new(500).unwrap()); - pub const NOT_IMPLEMENTED: StatusCode = StatusCode(NonZeroU16::new(501).unwrap()); - pub const BAD_GATEWAY: StatusCode = StatusCode(NonZeroU16::new(502).unwrap()); - pub const SERVICE_UNAVAILABLE: StatusCode = StatusCode(NonZeroU16::new(503).unwrap()); - pub const GATEWAY_TIMEOUT: StatusCode = StatusCode(NonZeroU16::new(504).unwrap()); - pub const HTTP_VERSION_NOT_SUPPORTED: StatusCode = StatusCode(NonZeroU16::new(505).unwrap()); - pub const VARIANT_ALSO_NEGOTIATES: StatusCode = StatusCode(NonZeroU16::new(506).unwrap()); - pub const INSUFFICIENT_STORAGE: StatusCode = StatusCode(NonZeroU16::new(507).unwrap()); - pub const LOOP_DETECTED: StatusCode = StatusCode(NonZeroU16::new(508).unwrap()); - pub const NOT_EXTENDED: StatusCode = StatusCode(NonZeroU16::new(510).unwrap()); - pub const NETWORK_AUTHENTICATION_REQUIRED: StatusCode = - StatusCode(NonZeroU16::new(511).unwrap()); -} - -impl Default for StatusCode { - fn default() -> Self { - Self::OK - } -} - -impl PartialEq for StatusCode { - fn eq(&self, other: &u16) -> bool { - self.as_u16() == *other - } -} - -impl PartialEq for u16 { - fn eq(&self, other: &StatusCode) -> bool { - *self == other.as_u16() - } -} - -impl PartialEq for http::StatusCode { - fn eq(&self, other: &StatusCode) -> bool { - *self == other.as_u16() - } -} - -impl TryFrom for StatusCode { - type Error = Error; - - fn try_from(value: u16) -> Result { - Self::from_u16(value) - } -} - -impl From for u16 { - fn from(status: StatusCode) -> u16 { - status.0.get() - } -} - -impl From for StatusCode { - fn from(value: http::StatusCode) -> Self { - Self::from_u16(value.as_u16()).unwrap() - } -} - -impl From for http::StatusCode { - fn from(value: StatusCode) -> Self { - Self::from_u16(value.0.into()).unwrap() - } -} - -impl From for tonic::Code { - fn from(value: StatusCode) -> Self { - tonic::Code::from_u16(value.as_u16()).unwrap() - } -} diff --git a/mocktail/src/service/grpc.rs b/mocktail/src/service/grpc.rs index cb93d40..4981eed 100644 --- a/mocktail/src/service/grpc.rs +++ b/mocktail/src/service/grpc.rs @@ -9,10 +9,11 @@ use http_body_util::{BodyExt, StreamBody}; use hyper::{body::Incoming, service::Service}; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; -use tonic::body::BoxBody; use tracing::debug; -use crate::{request::Request, server::MockServerState}; +use crate::{request::Request, server::MockServerState, service::http::empty, Code}; + +use super::http::BoxBody; /// Mock gRPC service. #[derive(Debug, Clone)] @@ -40,7 +41,7 @@ impl Service> for GrpcMockService { return Ok(http::Response::builder() .status(http::StatusCode::METHOD_NOT_ALLOWED) .header("Allow", "POST") - .body(tonic::body::empty_body()) + .body(empty()) .unwrap()); } let content_type = req.headers().get("content-type"); @@ -52,7 +53,7 @@ impl Service> for GrpcMockService { return Ok(http::Response::builder() .status(http::StatusCode::UNSUPPORTED_MEDIA_TYPE) .header("Accept-Post", "application/grpc") - .body(tonic::body::empty_body()) + .body(empty()) .unwrap()); } @@ -61,7 +62,7 @@ impl Service> for GrpcMockService { // Create response stream let (response_tx, response_rx) = - mpsc::channel::, tonic::Status>>(32); + mpsc::channel::, hyper::Error>>(32); let response_stream = ReceiverStream::new(response_rx); let response_body = BoxBody::new(StreamBody::new(response_stream)); let response = http::Response::builder() @@ -122,7 +123,7 @@ impl Service> for GrpcMockService { fn mock_not_found_trailer() -> HeaderMap { let mut headers = HeaderMap::new(); - headers.insert("grpc-status", (tonic::Code::NotFound as i32).into()); + headers.insert("grpc-status", (Code::NotFound as i32).into()); headers.insert("grpc-message", HeaderValue::from_static("mock not found")); headers } diff --git a/mocktail/src/service/http.rs b/mocktail/src/service/http.rs index 2217c82..e734578 100644 --- a/mocktail/src/service/http.rs +++ b/mocktail/src/service/http.rs @@ -13,7 +13,8 @@ use tracing::debug; use crate::{request::Request, server::MockServerState}; -type BoxBody = http_body_util::combinators::BoxBody; +/// A type-erased HTTP body. +pub type BoxBody = http_body_util::combinators::BoxBody; const ALLOWED_METHODS: [http::Method; 5] = [ http::Method::GET, @@ -157,10 +158,10 @@ impl Service> for HttpMockService { } } -fn full(data: Bytes) -> BoxBody { +pub fn full(data: Bytes) -> BoxBody { Full::new(data).map_err(|err| match err {}).boxed() } -fn empty() -> BoxBody { +pub fn empty() -> BoxBody { Empty::new().map_err(|err| match err {}).boxed() } diff --git a/mocktail/src/status.rs b/mocktail/src/status.rs new file mode 100644 index 0000000..c7a972c --- /dev/null +++ b/mocktail/src/status.rs @@ -0,0 +1,309 @@ +use std::num::NonZeroU16; + +use crate::Error; + +/// Represents a HTTP status code. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct StatusCode(NonZeroU16); + +impl StatusCode { + pub fn from_u16(code: u16) -> Result { + if !(100..1000).contains(&code) { + return Err(Error::Invalid("invalid status code".into())); + } + Ok(Self(NonZeroU16::new(code).unwrap())) + } + + pub fn as_u16(&self) -> u16 { + self.0.get() + } + + pub fn is_informational(&self) -> bool { + (100..200).contains(&self.as_u16()) + } + + pub fn is_success(&self) -> bool { + (200..300).contains(&self.as_u16()) + } + + pub fn is_redirection(&self) -> bool { + (300..400).contains(&self.as_u16()) + } + + pub fn is_error(&self) -> bool { + (400..600).contains(&self.as_u16()) + } + + pub fn is_ok(&self) -> bool { + self.is_success() + } + + pub fn as_http(&self) -> http::StatusCode { + http::StatusCode::from_u16(self.as_u16()).unwrap() + } + + pub fn as_grpc(&self) -> Code { + Code::from_http_u16(self.as_u16()).unwrap() + } + + pub fn as_grpc_i32(&self) -> i32 { + self.as_grpc() as i32 + } +} + +impl StatusCode { + pub const CONTINUE: StatusCode = StatusCode(NonZeroU16::new(100).unwrap()); + pub const SWITCHING_PROTOCOLS: StatusCode = StatusCode(NonZeroU16::new(101).unwrap()); + pub const PROCESSING: StatusCode = StatusCode(NonZeroU16::new(102).unwrap()); + pub const EARLY_HINTS: StatusCode = StatusCode(NonZeroU16::new(103).unwrap()); + + pub const OK: StatusCode = StatusCode(NonZeroU16::new(200).unwrap()); + pub const CREATED: StatusCode = StatusCode(NonZeroU16::new(201).unwrap()); + pub const ACCEPTED: StatusCode = StatusCode(NonZeroU16::new(202).unwrap()); + pub const NON_AUTHORITATIVE_INFORMATION: StatusCode = StatusCode(NonZeroU16::new(203).unwrap()); + pub const NO_CONTENT: StatusCode = StatusCode(NonZeroU16::new(204).unwrap()); + pub const RESET_CONTENT: StatusCode = StatusCode(NonZeroU16::new(205).unwrap()); + pub const PARTIAL_CONTENT: StatusCode = StatusCode(NonZeroU16::new(206).unwrap()); + pub const MULTI_STATUS: StatusCode = StatusCode(NonZeroU16::new(207).unwrap()); + pub const ALREADY_REPORTED: StatusCode = StatusCode(NonZeroU16::new(208).unwrap()); + pub const IM_USED: StatusCode = StatusCode(NonZeroU16::new(226).unwrap()); + + pub const MULTIPLE_CHOICES: StatusCode = StatusCode(NonZeroU16::new(300).unwrap()); + pub const MOVED_PERMANENTLY: StatusCode = StatusCode(NonZeroU16::new(301).unwrap()); + pub const FOUND: StatusCode = StatusCode(NonZeroU16::new(302).unwrap()); + pub const SEE_OTHER: StatusCode = StatusCode(NonZeroU16::new(303).unwrap()); + pub const NOT_MODIFIED: StatusCode = StatusCode(NonZeroU16::new(304).unwrap()); + pub const USE_PROXY: StatusCode = StatusCode(NonZeroU16::new(305).unwrap()); + pub const TEMPORARY_REDIRECT: StatusCode = StatusCode(NonZeroU16::new(307).unwrap()); + pub const PERMANENT_REDIRECT: StatusCode = StatusCode(NonZeroU16::new(308).unwrap()); + + pub const BAD_REQUEST: StatusCode = StatusCode(NonZeroU16::new(400).unwrap()); + pub const UNAUTHORIZED: StatusCode = StatusCode(NonZeroU16::new(401).unwrap()); + pub const PAYMENT_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(402).unwrap()); + pub const FORBIDDEN: StatusCode = StatusCode(NonZeroU16::new(403).unwrap()); + pub const NOT_FOUND: StatusCode = StatusCode(NonZeroU16::new(404).unwrap()); + pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(NonZeroU16::new(405).unwrap()); + pub const NOT_ACCEPTABLE: StatusCode = StatusCode(NonZeroU16::new(406).unwrap()); + pub const PROXY_AUTHENTICATION_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(407).unwrap()); + pub const REQUEST_TIMEOUT: StatusCode = StatusCode(NonZeroU16::new(408).unwrap()); + pub const CONFLICT: StatusCode = StatusCode(NonZeroU16::new(409).unwrap()); + pub const GONE: StatusCode = StatusCode(NonZeroU16::new(410).unwrap()); + pub const LENGTH_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(411).unwrap()); + pub const PRECONDITION_FAILED: StatusCode = StatusCode(NonZeroU16::new(412).unwrap()); + pub const PAYLOAD_TOO_LARGE: StatusCode = StatusCode(NonZeroU16::new(413).unwrap()); + pub const URI_TOO_LONG: StatusCode = StatusCode(NonZeroU16::new(414).unwrap()); + pub const UNSUPPORTED_MEDIA_TYPE: StatusCode = StatusCode(NonZeroU16::new(415).unwrap()); + pub const RANGE_NOT_SATISFIABLE: StatusCode = StatusCode(NonZeroU16::new(416).unwrap()); + pub const EXPECTATION_FAILED: StatusCode = StatusCode(NonZeroU16::new(417).unwrap()); + pub const IM_A_TEAPOT: StatusCode = StatusCode(NonZeroU16::new(418).unwrap()); + pub const MISDIRECTED_REQUEST: StatusCode = StatusCode(NonZeroU16::new(421).unwrap()); + pub const UNPROCESSABLE_ENTITY: StatusCode = StatusCode(NonZeroU16::new(422).unwrap()); + pub const LOCKED: StatusCode = StatusCode(NonZeroU16::new(423).unwrap()); + pub const FAILED_DEPENDENCY: StatusCode = StatusCode(NonZeroU16::new(424).unwrap()); + pub const TOO_EARLY: StatusCode = StatusCode(NonZeroU16::new(425).unwrap()); + pub const UPGRADE_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(426).unwrap()); + pub const PRECONDITION_REQUIRED: StatusCode = StatusCode(NonZeroU16::new(428).unwrap()); + pub const TOO_MANY_REQUESTS: StatusCode = StatusCode(NonZeroU16::new(429).unwrap()); + pub const REQUEST_HEADER_FIELDS_TOO_LARGE: StatusCode = + StatusCode(NonZeroU16::new(431).unwrap()); + pub const UNAVAILABLE_FOR_LEGAL_REASONS: StatusCode = StatusCode(NonZeroU16::new(451).unwrap()); + + pub const INTERNAL_SERVER_ERROR: StatusCode = StatusCode(NonZeroU16::new(500).unwrap()); + pub const NOT_IMPLEMENTED: StatusCode = StatusCode(NonZeroU16::new(501).unwrap()); + pub const BAD_GATEWAY: StatusCode = StatusCode(NonZeroU16::new(502).unwrap()); + pub const SERVICE_UNAVAILABLE: StatusCode = StatusCode(NonZeroU16::new(503).unwrap()); + pub const GATEWAY_TIMEOUT: StatusCode = StatusCode(NonZeroU16::new(504).unwrap()); + pub const HTTP_VERSION_NOT_SUPPORTED: StatusCode = StatusCode(NonZeroU16::new(505).unwrap()); + pub const VARIANT_ALSO_NEGOTIATES: StatusCode = StatusCode(NonZeroU16::new(506).unwrap()); + pub const INSUFFICIENT_STORAGE: StatusCode = StatusCode(NonZeroU16::new(507).unwrap()); + pub const LOOP_DETECTED: StatusCode = StatusCode(NonZeroU16::new(508).unwrap()); + pub const NOT_EXTENDED: StatusCode = StatusCode(NonZeroU16::new(510).unwrap()); + pub const NETWORK_AUTHENTICATION_REQUIRED: StatusCode = + StatusCode(NonZeroU16::new(511).unwrap()); +} + +impl Default for StatusCode { + fn default() -> Self { + Self::OK + } +} + +impl PartialEq for StatusCode { + fn eq(&self, other: &u16) -> bool { + self.as_u16() == *other + } +} + +impl PartialEq for u16 { + fn eq(&self, other: &StatusCode) -> bool { + *self == other.as_u16() + } +} + +impl PartialEq for http::StatusCode { + fn eq(&self, other: &StatusCode) -> bool { + *self == other.as_u16() + } +} + +impl TryFrom for StatusCode { + type Error = Error; + + fn try_from(value: u16) -> Result { + Self::from_u16(value) + } +} + +impl From for u16 { + fn from(status: StatusCode) -> u16 { + status.0.get() + } +} + +impl From for StatusCode { + fn from(value: http::StatusCode) -> Self { + Self::from_u16(value.as_u16()).unwrap() + } +} + +impl From for http::StatusCode { + fn from(value: StatusCode) -> Self { + Self::from_u16(value.0.into()).unwrap() + } +} + +impl From for Code { + fn from(value: StatusCode) -> Self { + Code::from_http_u16(value.as_u16()).unwrap() + } +} + +/// Represents a gRPC status code. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Code { + /// The operation completed successfully. + Ok = 0, + /// The operation was cancelled. + Cancelled = 1, + /// Unknown error. + Unknown = 2, + /// Client specified an invalid argument. + InvalidArgument = 3, + /// Deadline expired before operation could complete. + DeadlineExceeded = 4, + /// Some requested entity was not found. + NotFound = 5, + /// Some entity that we attempted to create already exists. + AlreadyExists = 6, + /// The caller does not have permission to execute the specified operation. + PermissionDenied = 7, + /// Some resource has been exhausted. + ResourceExhausted = 8, + /// The system is not in a state required for the operation's execution. + FailedPrecondition = 9, + /// The operation was aborted. + Aborted = 10, + /// Operation was attempted past the valid range. + OutOfRange = 11, + /// Operation is not implemented or not supported. + Unimplemented = 12, + /// Internal error. + Internal = 13, + /// The service is currently unavailable. + Unavailable = 14, + /// Unrecoverable data loss or corruption. + DataLoss = 15, + /// The request does not have valid authentication credentials + Unauthenticated = 16, +} + +impl Code { + /// Returns the gRPC equivalent of [`http::StatusCode`]. + /// + /// Loosely based on https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + /// with the following changes: + /// + /// - http::StatusCode::NOT_FOUND => Code::NotFound (instead of Code::Unimplemented) + /// - http::StatusCode::UNPROCESSABLE_ENTITY => Code::InvalidArgument (instead of Code::Unknown) + /// - http::StatusCode::NOT_IMPLEMENTED => Code::Unimplemented (instead of Code::Unknown) + /// - http::StatusCode::INTERNAL_SERVER_ERROR => Code::Internal (instead of Code::Unknown) + /// - http::StatusCode::OK => Code::Ok (instead of Code::Unknown) + pub fn from_http(http_code: http::StatusCode) -> Code { + match http_code { + http::StatusCode::OK => Code::Ok, // Code::Unknown + http::StatusCode::BAD_REQUEST => Code::Internal, + http::StatusCode::UNPROCESSABLE_ENTITY => Code::InvalidArgument, // Code::Unknown + http::StatusCode::UNAUTHORIZED => Code::Unauthenticated, + http::StatusCode::FORBIDDEN => Code::PermissionDenied, + http::StatusCode::NOT_FOUND => Code::NotFound, // Code::Unimplemented, + http::StatusCode::TOO_MANY_REQUESTS + | http::StatusCode::BAD_GATEWAY + | http::StatusCode::SERVICE_UNAVAILABLE + | http::StatusCode::GATEWAY_TIMEOUT => Code::Unavailable, + http::StatusCode::INTERNAL_SERVER_ERROR => Code::Internal, // Code::Unknown, + http::StatusCode::NOT_IMPLEMENTED => Code::Unimplemented, // Code::Unknown, + _ => Code::Unknown, + } + } + + fn from_http_u16(code: u16) -> Result { + let status_code = http::StatusCode::from_u16(code) + .map_err(|_| Error::Invalid("invalid status code".into()))?; + Ok(Code::from_http(status_code)) + } + + /// Get description of this `Code`. + pub fn description(&self) -> &'static str { + match self { + Code::Ok => "The operation completed successfully", + Code::Cancelled => "The operation was cancelled", + Code::Unknown => "Unknown error", + Code::InvalidArgument => "Client specified an invalid argument", + Code::DeadlineExceeded => "Deadline expired before operation could complete", + Code::NotFound => "Some requested entity was not found", + Code::AlreadyExists => "Some entity that we attempted to create already exists", + Code::PermissionDenied => { + "The caller does not have permission to execute the specified operation" + } + Code::ResourceExhausted => "Some resource has been exhausted", + Code::FailedPrecondition => { + "The system is not in a state required for the operation's execution" + } + Code::Aborted => "The operation was aborted", + Code::OutOfRange => "Operation was attempted past the valid range", + Code::Unimplemented => "Operation is not implemented or not supported", + Code::Internal => "Internal error", + Code::Unavailable => "The service is currently unavailable", + Code::DataLoss => "Unrecoverable data loss or corruption", + Code::Unauthenticated => "The request does not have valid authentication credentials", + } + } + + // pub fn to_header_value(self) -> HeaderValue { + // match self { + // Code::Ok => HeaderValue::from_static("0"), + // Code::Cancelled => HeaderValue::from_static("1"), + // Code::Unknown => HeaderValue::from_static("2"), + // Code::InvalidArgument => HeaderValue::from_static("3"), + // Code::DeadlineExceeded => HeaderValue::from_static("4"), + // Code::NotFound => HeaderValue::from_static("5"), + // Code::AlreadyExists => HeaderValue::from_static("6"), + // Code::PermissionDenied => HeaderValue::from_static("7"), + // Code::ResourceExhausted => HeaderValue::from_static("8"), + // Code::FailedPrecondition => HeaderValue::from_static("9"), + // Code::Aborted => HeaderValue::from_static("10"), + // Code::OutOfRange => HeaderValue::from_static("11"), + // Code::Unimplemented => HeaderValue::from_static("12"), + // Code::Internal => HeaderValue::from_static("13"), + // Code::Unavailable => HeaderValue::from_static("14"), + // Code::DataLoss => HeaderValue::from_static("15"), + // Code::Unauthenticated => HeaderValue::from_static("16"), + // } + // } +} + +impl std::fmt::Display for Code { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.description(), f) + } +} From e5f6dfb594424e3d8ef0cea04f56cf2cbe48b3d1 Mon Sep 17 00:00:00 2001 From: declark1 <44146800+declark1@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:14:50 -0700 Subject: [PATCH 2/2] Add Code::to_header_value() method Signed-off-by: declark1 <44146800+declark1@users.noreply.github.com> --- mocktail/src/service/grpc.rs | 5 +++-- mocktail/src/status.rs | 43 ++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/mocktail/src/service/grpc.rs b/mocktail/src/service/grpc.rs index 4981eed..43105fc 100644 --- a/mocktail/src/service/grpc.rs +++ b/mocktail/src/service/grpc.rs @@ -96,7 +96,8 @@ impl Service> for GrpcMockService { } // Send trailers frame let mut trailers = HeaderMap::from(response.headers().clone()); - trailers.insert("grpc-status", response.status().as_grpc_i32().into()); + trailers + .insert("grpc-status", response.status().as_grpc().to_header_value()); if let Some(message) = response.message() { trailers .insert("grpc-message", HeaderValue::from_str(message).unwrap()); @@ -123,7 +124,7 @@ impl Service> for GrpcMockService { fn mock_not_found_trailer() -> HeaderMap { let mut headers = HeaderMap::new(); - headers.insert("grpc-status", (Code::NotFound as i32).into()); + headers.insert("grpc-status", Code::NotFound.to_header_value()); headers.insert("grpc-message", HeaderValue::from_static("mock not found")); headers } diff --git a/mocktail/src/status.rs b/mocktail/src/status.rs index c7a972c..9cfe917 100644 --- a/mocktail/src/status.rs +++ b/mocktail/src/status.rs @@ -279,27 +279,28 @@ impl Code { } } - // pub fn to_header_value(self) -> HeaderValue { - // match self { - // Code::Ok => HeaderValue::from_static("0"), - // Code::Cancelled => HeaderValue::from_static("1"), - // Code::Unknown => HeaderValue::from_static("2"), - // Code::InvalidArgument => HeaderValue::from_static("3"), - // Code::DeadlineExceeded => HeaderValue::from_static("4"), - // Code::NotFound => HeaderValue::from_static("5"), - // Code::AlreadyExists => HeaderValue::from_static("6"), - // Code::PermissionDenied => HeaderValue::from_static("7"), - // Code::ResourceExhausted => HeaderValue::from_static("8"), - // Code::FailedPrecondition => HeaderValue::from_static("9"), - // Code::Aborted => HeaderValue::from_static("10"), - // Code::OutOfRange => HeaderValue::from_static("11"), - // Code::Unimplemented => HeaderValue::from_static("12"), - // Code::Internal => HeaderValue::from_static("13"), - // Code::Unavailable => HeaderValue::from_static("14"), - // Code::DataLoss => HeaderValue::from_static("15"), - // Code::Unauthenticated => HeaderValue::from_static("16"), - // } - // } + /// Returns [`http::HeaderValue`] representation. + pub fn to_header_value(self) -> http::HeaderValue { + match self { + Code::Ok => http::HeaderValue::from_static("0"), + Code::Cancelled => http::HeaderValue::from_static("1"), + Code::Unknown => http::HeaderValue::from_static("2"), + Code::InvalidArgument => http::HeaderValue::from_static("3"), + Code::DeadlineExceeded => http::HeaderValue::from_static("4"), + Code::NotFound => http::HeaderValue::from_static("5"), + Code::AlreadyExists => http::HeaderValue::from_static("6"), + Code::PermissionDenied => http::HeaderValue::from_static("7"), + Code::ResourceExhausted => http::HeaderValue::from_static("8"), + Code::FailedPrecondition => http::HeaderValue::from_static("9"), + Code::Aborted => http::HeaderValue::from_static("10"), + Code::OutOfRange => http::HeaderValue::from_static("11"), + Code::Unimplemented => http::HeaderValue::from_static("12"), + Code::Internal => http::HeaderValue::from_static("13"), + Code::Unavailable => http::HeaderValue::from_static("14"), + Code::DataLoss => http::HeaderValue::from_static("15"), + Code::Unauthenticated => http::HeaderValue::from_static("16"), + } + } } impl std::fmt::Display for Code {