Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: matcher for gRPC service name #471

Merged
merged 1 commit into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .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