Skip to content

Commit

Permalink
feat: fix gRPC method matching and introduce a matcher for the servic…
Browse files Browse the repository at this point in the history
…e name
  • Loading branch information
beltram committed Apr 27, 2023
1 parent 909cbb4 commit b5499c4
Show file tree
Hide file tree
Showing 21 changed files with 332 additions and 139 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
touch actix-consumer/build.rs
touch stub-consumer/build.rs
cargo build
- run: cargo nextest run --verbose
- run: cargo nextest run --verbose --all-features
- run: cargo test --doc

hack:
Expand Down
7 changes: 5 additions & 2 deletions book/src/grpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ The API looks like this:
"protoFile": "path/to/grpc.proto", // protobuf file where gRPC service & protobuf messages are defined
"grpcRequest": {
"message": "Pet", // name of the body's message in 'protoFile'
"path": "createDog", // name of the gRPC service to mock, supports Regex
"service": "PetStore", // (optional) name of the gRPC service to mock, supports Regex
"method": "createDog", // (optional) name of the gRPC method to mock, supports Regex
"bodyPatterns": [
{
"equalToJson": { // literally the same matchers as in http
Expand All @@ -28,7 +29,9 @@ The API looks like this:
"body": { // literally the same as in http, supports templating too
"id": 1234,
"name": "{{jsonPath request.body '$.name'}}",
"race": "{{jsonPath request.body '$.race'}}"
"race": "{{jsonPath request.body '$.race'}}",
"action": "{{request.method}}", // only 2 differences with standard templates
"service": "{{request.service}}"
},
"transformers": [ // required for response templating
"response-template"
Expand Down
2 changes: 2 additions & 0 deletions lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub enum StubrError {
ProtoMessageNotFound(String, std::path::PathBuf),
#[error("A protobuf 'message' has to be defined in stub")]
MissingProtoMessage,
#[error("Unexpected invalid gRPC request")]
InvalidGrpcRequest,
}

impl From<StubrError> for handlebars::RenderError {
Expand Down
58 changes: 58 additions & 0 deletions lib/src/model/grpc/request/method.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::wiremock::{Match, Request};
use crate::{StubrError, StubrResult};

pub struct GrpcMethodMatcher(regex::Regex);

impl GrpcMethodMatcher {
pub fn try_new(path: &str) -> StubrResult<Self> {
Ok(Self(regex::Regex::new(path)?))
}
}

pub struct GrpcSvcMatcher(regex::Regex);

impl GrpcSvcMatcher {
pub fn try_new(path: &str) -> StubrResult<Self> {
Ok(Self(regex::Regex::new(path)?))
}
}

pub(crate) struct GrpcMethod<'a>(pub(crate) &'a str);

impl<'a> From<&'a str> for GrpcMethod<'a> {
fn from(value: &'a str) -> Self {
Self(value)
}
}

pub(crate) struct GrpcSvc<'a>(pub(crate) &'a str);

impl<'a> TryFrom<&'a str> for GrpcSvc<'a> {
type Error = StubrError;

fn try_from(value: &'a str) -> StubrResult<Self> {
let svc = value.split('.').last().ok_or(StubrError::InvalidGrpcRequest)?;
Ok(Self(svc))
}
}

impl Match for GrpcMethodMatcher {
fn matches(&self, request: &Request) -> bool {
parse_path(request)
.map(|(method, _)| self.0.is_match(method.0))
.unwrap_or_default()
}
}

impl Match for GrpcSvcMatcher {
fn matches(&self, request: &Request) -> bool {
parse_path(request).map(|(_, svc)| self.0.is_match(svc.0)).unwrap_or_default()
}
}

pub(crate) fn parse_path(request: &Request) -> StubrResult<(GrpcMethod, GrpcSvc)> {
let mut paths = request.url.path_segments().ok_or(StubrError::InvalidGrpcRequest)?;
let svc: GrpcSvc = paths.next().ok_or(StubrError::InvalidGrpcRequest)?.try_into()?;
let method: GrpcMethod = paths.next().ok_or(StubrError::InvalidGrpcRequest)?.into();
Ok((method, svc))
}
17 changes: 11 additions & 6 deletions lib/src/model/grpc/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::{hash::Hash, path::PathBuf};
use protobuf::reflect::MessageDescriptor;

use crate::{
error::StubrResult,
model::{
grpc::proto::parse_message_descriptor,
request::{
Expand All @@ -12,7 +11,7 @@ use crate::{
},
},
wiremock::MockBuilder,
StubrError,
StubrError, StubrResult,
};

pub mod binary_eq;
Expand All @@ -21,17 +20,20 @@ pub mod eq_relaxed;
pub mod json_path;
pub mod json_path_contains;
pub mod json_path_eq;
pub mod path;
pub mod method;

#[derive(Debug, Clone, Hash, Default, serde::Serialize, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct GrpcRequestStub {
/// Name of the message definition within protobuf
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
/// Name of the gRPC method
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
/// Name of the gRPC service
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
pub service: Option<String>,
/// request body matchers
#[serde(skip_serializing_if = "Option::is_none")]
pub body_patterns: Option<Vec<BodyMatcherStub>>,
Expand All @@ -40,8 +42,11 @@ pub struct GrpcRequestStub {
impl GrpcRequestStub {
pub fn try_new(request: &GrpcRequestStub, proto_file: Option<&PathBuf>) -> StubrResult<MockBuilder> {
let mut mock = MockBuilder::from(&HttpMethodStub(Verb::Post));
if let Some(path) = request.path.as_ref() {
mock = mock.and(path::GrpcPathMatcher::try_new(path)?);
if let Some(method) = request.method.as_ref() {
mock = mock.and(method::GrpcMethodMatcher::try_new(method)?);
}
if let Some(svc) = request.service.as_ref() {
mock = mock.and(method::GrpcSvcMatcher::try_new(svc)?);
}
if let Some(matchers) = request.body_patterns.as_ref() {
let proto_file = proto_file.ok_or(StubrError::MissingProtoFile)?;
Expand Down
30 changes: 0 additions & 30 deletions lib/src/model/grpc/request/path.rs

This file was deleted.

44 changes: 32 additions & 12 deletions lib/src/model/response/template/data.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::{wiremock::Request as WiremockRequest, StubrResult};
use http_types::Method;
use serde_json::Value;

use super::req_ext::{Headers, Queries, RequestExt};
Expand All @@ -12,30 +11,46 @@ pub struct HandlebarsData<'a> {
pub is_verify: bool,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(untagged)]
pub enum MethodData<#[cfg(feature = "grpc")] 'a> {
Http(http_types::Method),
#[cfg(feature = "grpc")]
Grpc(&'a str),
}

#[derive(serde::Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RequestData<'a> {
path: &'a str,
#[cfg(not(feature = "grpc"))]
method: MethodData,
#[cfg(feature = "grpc")]
method: MethodData<'a>,
path_segments: Option<Vec<&'a str>>,
url: &'a str,
port: Option<u16>,
method: Method,
body: Option<Value>,
query: Option<Queries<'a>>,
headers: Option<Headers<'a>>,
#[serde(rename = "service")]
#[cfg(feature = "grpc")]
grpc_service: Option<&'a str>,
}

impl Default for RequestData<'_> {
fn default() -> Self {
Self {
path: "",
method: MethodData::Http(http_types::Method::Get),
path_segments: None,
url: "",
port: None,
method: Method::Get,
body: None,
query: None,
headers: None,
#[cfg(feature = "grpc")]
grpc_service: None,
}
}
}
Expand All @@ -45,15 +60,18 @@ impl<'a> RequestData<'a> {
pub fn try_from_grpc_request(req: &'a WiremockRequest, md: &protobuf::reflect::MessageDescriptor) -> StubrResult<Self> {
let body = crate::model::grpc::request::proto_to_json_str(req.body.as_slice(), md)?;
let body = serde_json::from_str(&body)?;
let (grpc_method, grpc_svc) = crate::model::grpc::request::method::parse_path(req)?;
Ok(Self {
path: crate::model::grpc::request::path::GrpcPathMatcher::parse_svc_name(req),
path: "",
path_segments: None,
method: MethodData::Grpc(grpc_method.0),
url: "",
port: None,
method: Method::Post,
body: Some(body),
query: None,
headers: None,
#[cfg(feature = "grpc")]
grpc_service: Some(grpc_svc.0),
})
}
}
Expand All @@ -65,10 +83,11 @@ impl<'a> From<&'a WiremockRequest> for RequestData<'a> {
path_segments: req.path_segments(),
url: req.uri(),
port: req.url.port(),
method: req.method,
method: MethodData::Http(req.method),
body: req.body(),
query: req.queries(),
headers: req.headers(),
..Default::default()
}
}
}
Expand All @@ -81,16 +100,17 @@ impl<'a> From<&'a mut http_types::Request> for RequestData<'a> {
path_segments: req.path_segments(),
url: req.uri(),
port: req.url().port(),
method: req.method(),
method: MethodData::Http(req.method()),
body,
query: req.queries(),
headers: req.headers(),
..Default::default()
}
}
}

#[cfg(test)]
mod request_data_tests {
mod tests {
use std::{borrow::Cow, collections::HashMap, str::FromStr};

use http_types::{
Expand Down Expand Up @@ -146,9 +166,9 @@ mod request_data_tests {
#[test]
fn should_take_request_method() {
let req = request("https://localhost", Some(Method::Get), &[], None);
assert_eq!(RequestData::from(&req).method, Method::Get);
assert!(matches!(RequestData::from(&req).method, MethodData::Http(Method::Get)));
let req = request("https://localhost", Some(Method::Post), &[], None);
assert_eq!(RequestData::from(&req).method, Method::Post);
assert!(matches!(RequestData::from(&req).method, MethodData::Http(Method::Post)));
}

#[test]
Expand Down Expand Up @@ -292,9 +312,9 @@ mod request_data_tests {
#[test]
fn should_take_request_method() {
let mut req = request("https://localhost", Some(Method::Get), &[], None);
assert_eq!(RequestData::from(&mut req).method, Method::Get);
assert!(matches!(RequestData::from(&mut req).method, MethodData::Http(Method::Get)));
let mut req = request("https://localhost", Some(Method::Post), &[], None);
assert_eq!(RequestData::from(&mut req).method, Method::Post);
assert!(matches!(RequestData::from(&mut req).method, MethodData::Http(Method::Post)));
}

#[test]
Expand Down
5 changes: 5 additions & 0 deletions lib/tests/grpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ pub mod resp;
#[async_trait::async_trait(? Send)]
pub trait GrpcConnect {
async fn connect(&self) -> grpc_client::GrpcClient<tonic::transport::Channel>;
async fn connect_other(&self) -> grpc_other_client::GrpcOtherClient<tonic::transport::Channel>;
}

#[async_trait::async_trait(? Send)]
impl GrpcConnect for stubr::Stubr {
async fn connect(&self) -> grpc_client::GrpcClient<tonic::transport::Channel> {
grpc::grpc_client::GrpcClient::connect(self.uri()).await.unwrap()
}

async fn connect_other(&self) -> grpc_other_client::GrpcOtherClient<tonic::transport::Channel> {
grpc::grpc_other_client::GrpcOtherClient::connect(self.uri()).await.unwrap()
}
}
6 changes: 6 additions & 0 deletions lib/tests/grpc/protos/grpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ service Grpc {
rpc respTemplate (Template) returns (Template) {}
}

service GrpcOther {
rpc reqPathEq (EmptyOther) returns (EmptyOther) {}
rpc reqPathEqRegex (EmptyOther) returns (EmptyOther) {}
}

message Empty {}
message EmptyOther {}

// see https://developers.google.com/protocol-buffers/docs/proto3#scalar
message Scalar {
Expand Down
Loading

0 comments on commit b5499c4

Please sign in to comment.