Skip to content

Commit 51ebc1f

Browse files
feat: support gRPC with connect-rpc (#3197)
Co-authored-by: Tushar Mathur <[email protected]>
1 parent a789775 commit 51ebc1f

File tree

8 files changed

+308
-7
lines changed

8 files changed

+308
-7
lines changed

src/cli/generator/config.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ pub enum Source<Status = UnResolved> {
8484
Proto {
8585
src: Location<Status>,
8686
url: String,
87+
#[serde(skip_serializing_if = "Option::is_none")]
88+
#[serde(rename = "connectRPC")]
89+
connect_rpc: Option<bool>,
8790
},
8891
Config {
8992
src: Location<Status>,
@@ -217,9 +220,9 @@ impl Source<UnResolved> {
217220
is_mutation,
218221
})
219222
}
220-
Source::Proto { src, url } => {
223+
Source::Proto { src, url, connect_rpc } => {
221224
let resolved_path = src.into_resolved(parent_dir);
222-
Ok(Source::Proto { src: resolved_path, url })
225+
Ok(Source::Proto { src: resolved_path, url, connect_rpc })
223226
}
224227
Source::Config { src } => {
225228
let resolved_path = src.into_resolved(parent_dir);

src/cli/generator/generator.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,13 @@ impl Generator {
135135
headers: headers.into_btree_map(),
136136
});
137137
}
138-
Source::Proto { src, url } => {
138+
Source::Proto { src, url, connect_rpc } => {
139139
let path = src.0;
140140
let mut metadata = proto_reader.read(&path).await?;
141141
if let Some(relative_path_to_proto) = to_relative_path(output_dir, &path) {
142142
metadata.path = relative_path_to_proto;
143143
}
144-
input_samples.push(Input::Proto { metadata, url });
144+
input_samples.push(Input::Proto { metadata, url, connect_rpc });
145145
}
146146
Source::Config { src } => {
147147
let path = src.0;

src/core/config/transformer/ambiguous_type.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ mod tests {
252252
.inputs(vec![Input::Proto {
253253
metadata: ProtoMetadata { descriptor_set: set, path: news_proto.to_string() },
254254
url,
255+
connect_rpc: None,
255256
}])
256257
.generate(false)?;
257258

src/core/generator/generator.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use tailcall_valid::Validator;
88
use url::Url;
99

1010
use super::from_proto::from_proto;
11+
use super::proto::connect_rpc::ConnectRPC;
1112
use super::{FromJsonGenerator, NameGenerator, RequestSample, PREFIX};
1213
use crate::core::config::{self, Config, ConfigModule, Link, LinkType};
1314
use crate::core::http::Method;
@@ -42,6 +43,7 @@ pub enum Input {
4243
Proto {
4344
url: String,
4445
metadata: ProtoMetadata,
46+
connect_rpc: Option<bool>,
4547
},
4648
Config {
4749
schema: String,
@@ -133,9 +135,14 @@ impl Generator {
133135
config = config
134136
.merge_right(self.generate_from_json(&type_name_generator, &[req_sample])?);
135137
}
136-
Input::Proto { metadata, url } => {
137-
config =
138-
config.merge_right(self.generate_from_proto(metadata, &self.query, url)?);
138+
Input::Proto { metadata, url, connect_rpc } => {
139+
let proto_config = self.generate_from_proto(metadata, &self.query, url)?;
140+
let proto_config = if connect_rpc == &Some(true) {
141+
ConnectRPC.transform(proto_config).to_result()?
142+
} else {
143+
proto_config
144+
};
145+
config = config.merge_right(proto_config);
139146
}
140147
}
141148
}
@@ -264,6 +271,7 @@ pub mod test {
264271
path: "../../../tailcall-fixtures/fixtures/protobuf/news.proto".to_string(),
265272
},
266273
url: "http://localhost:50051".to_string(),
274+
connect_rpc: None,
267275
}])
268276
.generate(false)?;
269277

@@ -317,6 +325,7 @@ pub mod test {
317325
path: "../../../tailcall-fixtures/fixtures/protobuf/news.proto".to_string(),
318326
},
319327
url: "http://localhost:50051".to_string(),
328+
connect_rpc: None,
320329
};
321330

322331
// Config input
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use tailcall_valid::Valid;
2+
3+
use crate::core::config::{Config, Grpc, Http, Resolver, ResolverSet};
4+
use crate::core::Transform;
5+
6+
pub struct ConnectRPC;
7+
8+
impl Transform for ConnectRPC {
9+
type Value = Config;
10+
type Error = String;
11+
12+
fn transform(&self, mut config: Self::Value) -> Valid<Self::Value, Self::Error> {
13+
for type_ in config.types.values_mut() {
14+
for field_ in type_.fields.values_mut() {
15+
let new_resolvers = field_
16+
.resolvers
17+
.0
18+
.iter()
19+
.map(|resolver| match resolver {
20+
Resolver::Grpc(grpc) => Resolver::Http(Http::from(grpc.clone())),
21+
other => other.clone(),
22+
})
23+
.collect();
24+
25+
field_.resolvers = ResolverSet(new_resolvers);
26+
}
27+
}
28+
29+
Valid::succeed(config)
30+
}
31+
}
32+
33+
impl From<Grpc> for Http {
34+
fn from(grpc: Grpc) -> Self {
35+
let url = grpc.url;
36+
let body = grpc.body.or_else(|| {
37+
// if body isn't present while transforming the resolver, we need to provide an
38+
// empty object.
39+
Some(serde_json::Value::Object(serde_json::Map::new()))
40+
});
41+
42+
// remove the last
43+
// method: package.service.method
44+
// remove the method from the end.
45+
let parts = grpc.method.split(".").collect::<Vec<_>>();
46+
let method = parts[..parts.len() - 1].join(".").to_string();
47+
let endpoint = parts[parts.len() - 1].to_string();
48+
49+
let new_url = format!("{}/{}/{}", url, method, endpoint);
50+
let headers = grpc.headers;
51+
let batch_key = grpc.batch_key;
52+
let dedupe = grpc.dedupe;
53+
let select = grpc.select;
54+
let on_response_body = grpc.on_response_body;
55+
56+
Self {
57+
url: new_url,
58+
body: body.map(|b| b.to_string()),
59+
method: crate::core::http::Method::POST,
60+
headers,
61+
batch_key,
62+
dedupe,
63+
select,
64+
on_response_body,
65+
..Default::default()
66+
}
67+
}
68+
}
69+
70+
#[cfg(test)]
71+
mod tests {
72+
use serde_json::{json, Value};
73+
74+
use super::*;
75+
use crate::core::config::KeyValue;
76+
77+
#[test]
78+
fn test_grpc_to_http_basic_conversion() {
79+
let grpc = Grpc {
80+
url: "http://localhost:8080".to_string(),
81+
method: "package.service.method".to_string(),
82+
body: Some(json!({"key": "value"})),
83+
headers: Default::default(),
84+
batch_key: Default::default(),
85+
dedupe: Default::default(),
86+
select: Default::default(),
87+
on_response_body: Default::default(),
88+
};
89+
90+
let http = Http::from(grpc);
91+
92+
assert_eq!(http.url, "http://localhost:8080/package.service/method");
93+
assert_eq!(http.method, crate::core::http::Method::POST);
94+
assert_eq!(http.body, Some(r#"{"key":"value"}"#.to_string()));
95+
}
96+
97+
#[test]
98+
fn test_grpc_to_http_empty_body() {
99+
let grpc = Grpc {
100+
url: "http://localhost:8080".to_string(),
101+
method: "package.service.method".to_string(),
102+
body: Default::default(),
103+
headers: Default::default(),
104+
batch_key: Default::default(),
105+
dedupe: Default::default(),
106+
select: Default::default(),
107+
on_response_body: Default::default(),
108+
};
109+
110+
let http = Http::from(grpc);
111+
112+
assert_eq!(http.body, Some("{}".to_string()));
113+
}
114+
115+
#[test]
116+
fn test_grpc_to_http_with_headers() {
117+
let grpc = Grpc {
118+
url: "http://localhost:8080".to_string(),
119+
method: "a.b.c".to_string(),
120+
body: None,
121+
headers: vec![KeyValue { key: "X-Foo".to_string(), value: "bar".to_string() }],
122+
batch_key: Default::default(),
123+
dedupe: Default::default(),
124+
select: Default::default(),
125+
on_response_body: Default::default(),
126+
};
127+
128+
let http = Http::from(grpc);
129+
130+
assert_eq!(http.url, "http://localhost:8080/a.b/c");
131+
assert_eq!(
132+
http.headers
133+
.iter()
134+
.find(|h| h.key == "X-Foo")
135+
.unwrap()
136+
.value,
137+
"bar".to_string()
138+
);
139+
}
140+
141+
#[test]
142+
fn test_grpc_to_http_all_fields() {
143+
let grpc = Grpc {
144+
url: "http://localhost:8080".to_string(),
145+
method: "package.service.method".to_string(),
146+
body: Some(json!({"key": "value"})),
147+
headers: vec![KeyValue { key: "X-Foo".to_string(), value: "bar".to_string() }],
148+
batch_key: vec!["batch_key_value".to_string()],
149+
dedupe: Some(true),
150+
select: Some(Value::String("select_value".to_string())),
151+
on_response_body: Some("on_response_body_value".to_string()),
152+
};
153+
154+
let http = Http::from(grpc);
155+
156+
assert_eq!(http.url, "http://localhost:8080/package.service/method");
157+
assert_eq!(http.method, crate::core::http::Method::POST);
158+
assert_eq!(http.body, Some(r#"{"key":"value"}"#.to_string()));
159+
assert_eq!(
160+
http.headers
161+
.iter()
162+
.find(|h| h.key == "X-Foo")
163+
.unwrap()
164+
.value,
165+
"bar".to_string()
166+
);
167+
assert_eq!(http.batch_key, vec!["batch_key_value".to_string()]);
168+
assert_eq!(http.dedupe, Some(true));
169+
assert_eq!(http.select, Some(Value::String("select_value".to_string())));
170+
assert_eq!(
171+
http.on_response_body,
172+
Some("on_response_body_value".to_string())
173+
);
174+
}
175+
}

src/core/generator/proto/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod comments_builder;
2+
pub mod connect_rpc;
23
pub mod path_builder;
34
pub mod path_field;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
```json @config
2+
{
3+
"inputs": [
4+
{
5+
"curl": {
6+
"src": "http://jsonplaceholder.typicode.com/users",
7+
"fieldName": "users"
8+
}
9+
},
10+
{
11+
"proto": {
12+
"src": "tailcall-fixtures/fixtures/protobuf/news.proto",
13+
"url": "http://localhost:50051",
14+
"connectRPC": true
15+
}
16+
}
17+
],
18+
"preset": {
19+
"mergeType": 1.0,
20+
"inferTypeNames": true,
21+
"treeShake": true
22+
},
23+
"output": {
24+
"path": "./output.graphql",
25+
"format": "graphQL"
26+
},
27+
"schema": {
28+
"query": "Query"
29+
}
30+
}
31+
```
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
source: tests/cli/gen.rs
3+
expression: config.to_sdl()
4+
---
5+
schema @server @upstream {
6+
query: Query
7+
}
8+
9+
input GEN__news__MultipleNewsId @addField(name: "ids", path: ["ids", "id"]) {
10+
ids: [Id]@omit
11+
}
12+
13+
input GEN__news__NewsInput {
14+
body: String
15+
id: Int
16+
postImage: String
17+
status: Status
18+
title: String
19+
}
20+
21+
input Id {
22+
id: Int
23+
}
24+
25+
enum Status {
26+
DELETED
27+
DRAFT
28+
PUBLISHED
29+
}
30+
31+
type Address {
32+
city: String
33+
geo: Geo
34+
street: String
35+
suite: String
36+
zipcode: String
37+
}
38+
39+
type Company {
40+
bs: String
41+
catchPhrase: String
42+
name: String
43+
}
44+
45+
type GEN__news__NewsList {
46+
news: [News]
47+
}
48+
49+
type Geo {
50+
lat: String
51+
lng: String
52+
}
53+
54+
type News {
55+
body: String
56+
id: Int
57+
postImage: String
58+
status: Status
59+
title: String
60+
}
61+
62+
type Query {
63+
GEN__news__NewsService__AddNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/AddNews", body: "\"{{.args.news}}\"", method: "POST")
64+
GEN__news__NewsService__DeleteNews(newsId: Id!): Empty @http(url: "http://localhost:50051/news.NewsService/DeleteNews", body: "\"{{.args.newsId}}\"", method: "POST")
65+
GEN__news__NewsService__EditNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/EditNews", body: "\"{{.args.news}}\"", method: "POST")
66+
GEN__news__NewsService__GetAllNews: GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetAllNews", body: "{}", method: "POST")
67+
GEN__news__NewsService__GetMultipleNews(multipleNewsId: GEN__news__MultipleNewsId!): GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetMultipleNews", body: "\"{{.args.multipleNewsId}}\"", method: "POST")
68+
GEN__news__NewsService__GetNews(newsId: Id!): News @http(url: "http://localhost:50051/news.NewsService/GetNews", body: "\"{{.args.newsId}}\"", method: "POST")
69+
users: [User] @http(url: "http://jsonplaceholder.typicode.com/users")
70+
}
71+
72+
type User {
73+
address: Address
74+
company: Company
75+
email: String
76+
id: Int
77+
name: String
78+
phone: String
79+
username: String
80+
website: String
81+
}

0 commit comments

Comments
 (0)