Skip to content

Commit

Permalink
fix(grpc): allow users to pass value along with mustache template (#2383
Browse files Browse the repository at this point in the history
)
  • Loading branch information
ssddOnTop authored Jul 9, 2024
1 parent e90f961 commit afde464
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 27 deletions.
4 changes: 2 additions & 2 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ directive @grpc(
or use Mustache template for dynamic parameters. These parameters will be added in
the body in `protobuf` format.
"""
body: String
body: JSON
"""
The `headers` parameter allows you to customize the headers of the HTTP request made
by the `@grpc` operator. It is used by specifying a key-value map of header names
Expand Down Expand Up @@ -754,7 +754,7 @@ input Grpc {
or use Mustache template for dynamic parameters. These parameters will be added in
the body in `protobuf` format.
"""
body: String
body: JSON
"""
The `headers` parameter allows you to customize the headers of the HTTP request made
by the `@grpc` operator. It is used by specifying a key-value map of header names
Expand Down
6 changes: 1 addition & 5 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,11 +608,7 @@
}
},
"body": {
"description": "This refers to the arguments of your gRPC call. You can pass it as a static object or use Mustache template for dynamic parameters. These parameters will be added in the body in `protobuf` format.",
"type": [
"string",
"null"
]
"description": "This refers to the arguments of your gRPC call. You can pass it as a static object or use Mustache template for dynamic parameters. These parameters will be added in the body in `protobuf` format."
},
"headers": {
"description": "The `headers` parameter allows you to customize the headers of the HTTP request made by the `@grpc` operator. It is used by specifying a key-value map of header names and their values. Note: content-type is automatically set to application/grpc",
Expand Down
11 changes: 8 additions & 3 deletions src/core/blueprint/mustache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,14 @@ impl FieldDefinition {
)
.and_then(|_| {
if let Some(body) = &req_template.body {
Valid::from_iter(body.expression_segments(), |parts| {
parts_validator.validate(parts, true).trace("body")
})
if let Some(mustache) = &body.mustache {
Valid::from_iter(mustache.expression_segments(), |parts| {
parts_validator.validate(parts, true).trace("body")
})
} else {
// TODO: needs review
Valid::succeed(Default::default())
}
} else {
Valid::succeed(Default::default())
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/blueprint/operators/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pub fn compile_grpc(inputs: CompileGrpc) -> Valid<IR, String> {
to_operation(&method, file_descriptor_set)
.fuse(to_url(grpc, &method, config_module))
.fuse(helpers::headers::to_mustache_headers(&grpc.headers))
.fuse(helpers::body::to_body(grpc.body.as_deref()))
.fuse(helpers::body::to_body(grpc.body.as_ref()))
.into()
})
.and_then(|(operation, url, headers, body)| {
Expand Down
2 changes: 1 addition & 1 deletion src/core/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ pub struct Grpc {
/// This refers to the arguments of your gRPC call. You can pass it as a
/// static object or use Mustache template for dynamic parameters. These
/// parameters will be added in the body in `protobuf` format.
pub body: Option<String>,
pub body: Option<Value>,
#[serde(rename = "batchKey", default, skip_serializing_if = "is_default")]
/// The key path in the response which should be used to group multiple requests. For instance `["news","id"]`. For more details please refer out [n + 1 guide](https://tailcall.run/docs/guides/n+1#solving-using-batching).
pub group_by: Vec<String>,
Expand Down
3 changes: 2 additions & 1 deletion src/core/generator/from_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use prost_reflect::prost_types::field_descriptor_proto::Label;
use prost_reflect::prost_types::{
DescriptorProto, EnumDescriptorProto, FileDescriptorSet, ServiceDescriptorProto, SourceCodeInfo,
};
use serde_json::Value;

use super::graphql_type::{GraphQLType, Unparsed};
use super::proto::comments_builder::CommentsBuilder;
Expand Down Expand Up @@ -355,7 +356,7 @@ impl Context {
default_value: None,
};

body = Some(format!("{{{{.args.{key}}}}}"));
body = Some(Value::String(format!("{{{{.args.{key}}}}}")));
cfg_field.args.insert(key, val);
}

Expand Down
30 changes: 26 additions & 4 deletions src/core/grpc/request_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,27 @@ static GRPC_MIME_TYPE: HeaderValue = HeaderValue::from_static("application/grpc"
pub struct RequestTemplate {
pub url: Mustache,
pub headers: MustacheHeaders,
pub body: Option<Mustache>,
pub body: Option<RequestBody>,
pub operation: ProtobufOperation,
pub operation_type: GraphQLOperationType,
}

#[derive(Default, Debug, Clone, PartialEq, Setters)]
pub struct RequestBody {
pub mustache: Option<Mustache>,
pub value: String,
}

impl RequestBody {
pub fn render<C: PathString>(&self, ctx: &C) -> String {
if let Some(mustache) = &self.mustache {
mustache.render(ctx)
} else {
self.value.to_string()
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedRequestTemplate {
pub url: Url,
Expand Down Expand Up @@ -123,7 +139,7 @@ mod tests {
use pretty_assertions::assert_eq;
use tailcall_fixtures::protobuf;

use super::RequestTemplate;
use super::{RequestBody, RequestTemplate};
use crate::core::blueprint::GrpcMethod;
use crate::core::config::reader::ConfigReader;
use crate::core::config::{Config, Field, GraphQLOperationType, Grpc, Link, LinkType, Type};
Expand Down Expand Up @@ -237,7 +253,10 @@ mod tests {
url: Mustache::parse("http://localhost:3000/").unwrap(),
headers: vec![],
operation: get_protobuf_op().await,
body: Some(Mustache::parse(r#"{ "name": "test" }"#).unwrap()),
body: Some(RequestBody {
mustache: Some(Mustache::parse(r#"{ "name": "test" }"#).unwrap()),
value: Default::default(),
}),
operation_type: GraphQLOperationType::Query,
};
let ctx = Context::default();
Expand All @@ -254,7 +273,10 @@ mod tests {
url: Mustache::parse("http://localhost:3000/").unwrap(),
headers: vec![],
operation: get_protobuf_op().await,
body: Some(Mustache::parse(body_str).unwrap()),
body: Some(RequestBody {
mustache: Some(Mustache::parse(body_str).unwrap()),
value: Default::default(),
}),
operation_type: GraphQLOperationType::Query,
}
}
Expand Down
28 changes: 19 additions & 9 deletions src/core/helpers/body.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
use serde_json::Value;

use crate::core::grpc::request_template::RequestBody;
use crate::core::mustache::Mustache;
use crate::core::valid::{Valid, ValidationError};
use crate::core::valid::Valid;

pub fn to_body(body: Option<&str>) -> Valid<Option<Mustache>, String> {
pub fn to_body(body: Option<&Value>) -> Valid<Option<RequestBody>, String> {
let Some(body) = body else {
return Valid::succeed(None);
};

Valid::from(
Mustache::parse(body)
.map(Some)
.map_err(|e| ValidationError::new(e.to_string())),
)
let mut req_body = RequestBody::default();

let value = body.to_string();
if let Ok(mustache) = Mustache::parse(&value) {
req_body = req_body.mustache(Some(mustache));
}
Valid::succeed(Some(req_body.value(value)))
}

#[cfg(test)]
mod tests {
use super::to_body;
use crate::core::grpc::request_template::RequestBody;
use crate::core::mustache::Mustache;
use crate::core::valid::Valid;

Expand All @@ -28,11 +34,15 @@ mod tests {

#[test]
fn body_parse_success() {
let result = to_body(Some("content"));
let value = serde_json::Value::String("content".to_string());
let result = to_body(Some(&value));

assert_eq!(
result,
Valid::succeed(Some(Mustache::parse("content").unwrap()))
Valid::succeed(Some(RequestBody {
mustache: Some(Mustache::parse(value.to_string().as_str()).unwrap()),
value: value.to_string()
}))
);
}
}
6 changes: 5 additions & 1 deletion src/core/proto_reader/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde_json::json;
use crate::core::blueprint::GrpcMethod;
use crate::core::config::ConfigReaderContext;
use crate::core::grpc::protobuf::ProtobufSet;
use crate::core::grpc::request_template::RequestBody;
use crate::core::grpc::RequestTemplate;
use crate::core::mustache::Mustache;
use crate::core::runtime::TargetRuntime;
Expand Down Expand Up @@ -133,7 +134,10 @@ impl GrpcReflection {
HeaderName::from_static("content-type"),
Mustache::parse("application/grpc+proto")?,
)],
body: Mustache::parse(body.to_string().as_str()).ok(),
body: Some(RequestBody {
mustache: Mustache::parse(body.to_string().as_str()).ok(),
value: Default::default(),
}),
operation: operation.clone(),
operation_type: Default::default(),
};
Expand Down
17 changes: 17 additions & 0 deletions tests/core/snapshots/grpc-json.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"newsById": {
"id": 2
}
}
}
}
17 changes: 17 additions & 0 deletions tests/core/snapshots/grpc-json.md_1.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"newsByIdMustache": {
"id": 2
}
}
}
}
17 changes: 17 additions & 0 deletions tests/core/snapshots/grpc-json.md_2.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"newsByIdMustacheAndJson": {
"id": 2
}
}
}
}
58 changes: 58 additions & 0 deletions tests/core/snapshots/grpc-json.md_client.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
source: tests/core/spec.rs
expression: formatted
---
scalar Bytes

scalar Date

scalar Email

scalar Empty

scalar Int128

scalar Int16

scalar Int32

scalar Int64

scalar Int8

scalar JSON

type News {
body: String
id: Int
postImage: String
title: String
}

input NewsInput {
id: Int
}

scalar PhoneNumber

type Query {
newsById: News!
newsByIdMustache(news: NewsInput!): News!
newsByIdMustacheAndJson(news: NewsInput!): News!
}

scalar UInt128

scalar UInt16

scalar UInt32

scalar UInt64

scalar UInt8

scalar Url

schema {
query: Query
}
28 changes: 28 additions & 0 deletions tests/core/snapshots/grpc-json.md_merged.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: tests/core/spec.rs
expression: formatter
---
schema
@server(port: 8000)
@upstream(baseURL: "http://localhost:50051")
@link(id: "news", src: "news.proto", type: Protobuf) {
query: Query
}

input NewsInput {
id: Int
}

type News {
body: String
id: Int
postImage: String
title: String
}

type Query {
newsById: News! @grpc(body: {id: 2}, method: "news.NewsService.GetNews")
newsByIdMustache(news: NewsInput!): News! @grpc(body: "{{.args.news}}", method: "news.NewsService.GetNews")
newsByIdMustacheAndJson(news: NewsInput!): News!
@grpc(body: {id: "{{.args.news.id}}"}, method: "news.NewsService.GetNews")
}
Loading

1 comment on commit afde464

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 7.41ms 3.31ms 85.76ms 72.05%
Req/Sec 3.41k 181.61 3.75k 91.42%

407739 requests in 30.00s, 2.04GB read

Requests/sec: 13589.39

Transfer/sec: 69.75MB

Please sign in to comment.