From a7897756aeea0776136184976182ce9a8e954224 Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:29:46 +0530 Subject: [PATCH 1/2] feat: enable infer_type_names by-default (#3198) --- src/core/config/transformer/preset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/config/transformer/preset.rs b/src/core/config/transformer/preset.rs index be70315674..e7c41e3a13 100644 --- a/src/core/config/transformer/preset.rs +++ b/src/core/config/transformer/preset.rs @@ -18,7 +18,7 @@ impl Preset { Self { merge_type: 0.0, tree_shake: false, - infer_type_names: false, + infer_type_names: true, unwrap_single_field_types: true, } } From 51ebc1f807a21ca2d95f9028032e48a11341e54f Mon Sep 17 00:00:00 2001 From: laststylebender <43403528+laststylebender14@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:30:05 +0530 Subject: [PATCH 2/2] feat: support gRPC with connect-rpc (#3197) Co-authored-by: Tushar Mathur --- src/cli/generator/config.rs | 7 +- src/cli/generator/generator.rs | 4 +- src/core/config/transformer/ambiguous_type.rs | 1 + src/core/generator/generator.rs | 15 +- src/core/generator/proto/connect_rpc.rs | 175 ++++++++++++++++++ src/core/generator/proto/mod.rs | 1 + .../fixtures/generator/proto-connect-rpc.md | 31 ++++ ...ures__generator__proto-connect-rpc.md.snap | 81 ++++++++ 8 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 src/core/generator/proto/connect_rpc.rs create mode 100644 tests/cli/fixtures/generator/proto-connect-rpc.md create mode 100644 tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index e2fba5aafd..26a077906b 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -84,6 +84,9 @@ pub enum Source { Proto { src: Location, url: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "connectRPC")] + connect_rpc: Option, }, Config { src: Location, @@ -217,9 +220,9 @@ impl Source { is_mutation, }) } - Source::Proto { src, url } => { + Source::Proto { src, url, connect_rpc } => { let resolved_path = src.into_resolved(parent_dir); - Ok(Source::Proto { src: resolved_path, url }) + Ok(Source::Proto { src: resolved_path, url, connect_rpc }) } Source::Config { src } => { let resolved_path = src.into_resolved(parent_dir); diff --git a/src/cli/generator/generator.rs b/src/cli/generator/generator.rs index f1b73efda0..b56e162808 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -135,13 +135,13 @@ impl Generator { headers: headers.into_btree_map(), }); } - Source::Proto { src, url } => { + Source::Proto { src, url, connect_rpc } => { let path = src.0; let mut metadata = proto_reader.read(&path).await?; if let Some(relative_path_to_proto) = to_relative_path(output_dir, &path) { metadata.path = relative_path_to_proto; } - input_samples.push(Input::Proto { metadata, url }); + input_samples.push(Input::Proto { metadata, url, connect_rpc }); } Source::Config { src } => { let path = src.0; diff --git a/src/core/config/transformer/ambiguous_type.rs b/src/core/config/transformer/ambiguous_type.rs index 994de6557b..3a3a9dc646 100644 --- a/src/core/config/transformer/ambiguous_type.rs +++ b/src/core/config/transformer/ambiguous_type.rs @@ -252,6 +252,7 @@ mod tests { .inputs(vec![Input::Proto { metadata: ProtoMetadata { descriptor_set: set, path: news_proto.to_string() }, url, + connect_rpc: None, }]) .generate(false)?; diff --git a/src/core/generator/generator.rs b/src/core/generator/generator.rs index 4a06071bbb..98cb81515e 100644 --- a/src/core/generator/generator.rs +++ b/src/core/generator/generator.rs @@ -8,6 +8,7 @@ use tailcall_valid::Validator; use url::Url; use super::from_proto::from_proto; +use super::proto::connect_rpc::ConnectRPC; use super::{FromJsonGenerator, NameGenerator, RequestSample, PREFIX}; use crate::core::config::{self, Config, ConfigModule, Link, LinkType}; use crate::core::http::Method; @@ -42,6 +43,7 @@ pub enum Input { Proto { url: String, metadata: ProtoMetadata, + connect_rpc: Option, }, Config { schema: String, @@ -133,9 +135,14 @@ impl Generator { config = config .merge_right(self.generate_from_json(&type_name_generator, &[req_sample])?); } - Input::Proto { metadata, url } => { - config = - config.merge_right(self.generate_from_proto(metadata, &self.query, url)?); + Input::Proto { metadata, url, connect_rpc } => { + let proto_config = self.generate_from_proto(metadata, &self.query, url)?; + let proto_config = if connect_rpc == &Some(true) { + ConnectRPC.transform(proto_config).to_result()? + } else { + proto_config + }; + config = config.merge_right(proto_config); } } } @@ -264,6 +271,7 @@ pub mod test { path: "../../../tailcall-fixtures/fixtures/protobuf/news.proto".to_string(), }, url: "http://localhost:50051".to_string(), + connect_rpc: None, }]) .generate(false)?; @@ -317,6 +325,7 @@ pub mod test { path: "../../../tailcall-fixtures/fixtures/protobuf/news.proto".to_string(), }, url: "http://localhost:50051".to_string(), + connect_rpc: None, }; // Config input diff --git a/src/core/generator/proto/connect_rpc.rs b/src/core/generator/proto/connect_rpc.rs new file mode 100644 index 0000000000..4363631e2c --- /dev/null +++ b/src/core/generator/proto/connect_rpc.rs @@ -0,0 +1,175 @@ +use tailcall_valid::Valid; + +use crate::core::config::{Config, Grpc, Http, Resolver, ResolverSet}; +use crate::core::Transform; + +pub struct ConnectRPC; + +impl Transform for ConnectRPC { + type Value = Config; + type Error = String; + + fn transform(&self, mut config: Self::Value) -> Valid { + for type_ in config.types.values_mut() { + for field_ in type_.fields.values_mut() { + let new_resolvers = field_ + .resolvers + .0 + .iter() + .map(|resolver| match resolver { + Resolver::Grpc(grpc) => Resolver::Http(Http::from(grpc.clone())), + other => other.clone(), + }) + .collect(); + + field_.resolvers = ResolverSet(new_resolvers); + } + } + + Valid::succeed(config) + } +} + +impl From for Http { + fn from(grpc: Grpc) -> Self { + let url = grpc.url; + let body = grpc.body.or_else(|| { + // if body isn't present while transforming the resolver, we need to provide an + // empty object. + Some(serde_json::Value::Object(serde_json::Map::new())) + }); + + // remove the last + // method: package.service.method + // remove the method from the end. + let parts = grpc.method.split(".").collect::>(); + let method = parts[..parts.len() - 1].join(".").to_string(); + let endpoint = parts[parts.len() - 1].to_string(); + + let new_url = format!("{}/{}/{}", url, method, endpoint); + let headers = grpc.headers; + let batch_key = grpc.batch_key; + let dedupe = grpc.dedupe; + let select = grpc.select; + let on_response_body = grpc.on_response_body; + + Self { + url: new_url, + body: body.map(|b| b.to_string()), + method: crate::core::http::Method::POST, + headers, + batch_key, + dedupe, + select, + on_response_body, + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::{json, Value}; + + use super::*; + use crate::core::config::KeyValue; + + #[test] + fn test_grpc_to_http_basic_conversion() { + let grpc = Grpc { + url: "http://localhost:8080".to_string(), + method: "package.service.method".to_string(), + body: Some(json!({"key": "value"})), + headers: Default::default(), + batch_key: Default::default(), + dedupe: Default::default(), + select: Default::default(), + on_response_body: Default::default(), + }; + + let http = Http::from(grpc); + + assert_eq!(http.url, "http://localhost:8080/package.service/method"); + assert_eq!(http.method, crate::core::http::Method::POST); + assert_eq!(http.body, Some(r#"{"key":"value"}"#.to_string())); + } + + #[test] + fn test_grpc_to_http_empty_body() { + let grpc = Grpc { + url: "http://localhost:8080".to_string(), + method: "package.service.method".to_string(), + body: Default::default(), + headers: Default::default(), + batch_key: Default::default(), + dedupe: Default::default(), + select: Default::default(), + on_response_body: Default::default(), + }; + + let http = Http::from(grpc); + + assert_eq!(http.body, Some("{}".to_string())); + } + + #[test] + fn test_grpc_to_http_with_headers() { + let grpc = Grpc { + url: "http://localhost:8080".to_string(), + method: "a.b.c".to_string(), + body: None, + headers: vec![KeyValue { key: "X-Foo".to_string(), value: "bar".to_string() }], + batch_key: Default::default(), + dedupe: Default::default(), + select: Default::default(), + on_response_body: Default::default(), + }; + + let http = Http::from(grpc); + + assert_eq!(http.url, "http://localhost:8080/a.b/c"); + assert_eq!( + http.headers + .iter() + .find(|h| h.key == "X-Foo") + .unwrap() + .value, + "bar".to_string() + ); + } + + #[test] + fn test_grpc_to_http_all_fields() { + let grpc = Grpc { + url: "http://localhost:8080".to_string(), + method: "package.service.method".to_string(), + body: Some(json!({"key": "value"})), + headers: vec![KeyValue { key: "X-Foo".to_string(), value: "bar".to_string() }], + batch_key: vec!["batch_key_value".to_string()], + dedupe: Some(true), + select: Some(Value::String("select_value".to_string())), + on_response_body: Some("on_response_body_value".to_string()), + }; + + let http = Http::from(grpc); + + assert_eq!(http.url, "http://localhost:8080/package.service/method"); + assert_eq!(http.method, crate::core::http::Method::POST); + assert_eq!(http.body, Some(r#"{"key":"value"}"#.to_string())); + assert_eq!( + http.headers + .iter() + .find(|h| h.key == "X-Foo") + .unwrap() + .value, + "bar".to_string() + ); + assert_eq!(http.batch_key, vec!["batch_key_value".to_string()]); + assert_eq!(http.dedupe, Some(true)); + assert_eq!(http.select, Some(Value::String("select_value".to_string()))); + assert_eq!( + http.on_response_body, + Some("on_response_body_value".to_string()) + ); + } +} diff --git a/src/core/generator/proto/mod.rs b/src/core/generator/proto/mod.rs index c0a20dc3d4..f4ac9fa540 100644 --- a/src/core/generator/proto/mod.rs +++ b/src/core/generator/proto/mod.rs @@ -1,3 +1,4 @@ pub mod comments_builder; +pub mod connect_rpc; pub mod path_builder; pub mod path_field; diff --git a/tests/cli/fixtures/generator/proto-connect-rpc.md b/tests/cli/fixtures/generator/proto-connect-rpc.md new file mode 100644 index 0000000000..cd16f7bc2c --- /dev/null +++ b/tests/cli/fixtures/generator/proto-connect-rpc.md @@ -0,0 +1,31 @@ +```json @config +{ + "inputs": [ + { + "curl": { + "src": "http://jsonplaceholder.typicode.com/users", + "fieldName": "users" + } + }, + { + "proto": { + "src": "tailcall-fixtures/fixtures/protobuf/news.proto", + "url": "http://localhost:50051", + "connectRPC": true + } + } + ], + "preset": { + "mergeType": 1.0, + "inferTypeNames": true, + "treeShake": true + }, + "output": { + "path": "./output.graphql", + "format": "graphQL" + }, + "schema": { + "query": "Query" + } +} +``` diff --git a/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap new file mode 100644 index 0000000000..dfcf3f967f --- /dev/null +++ b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__proto-connect-rpc.md.snap @@ -0,0 +1,81 @@ +--- +source: tests/cli/gen.rs +expression: config.to_sdl() +--- +schema @server @upstream { + query: Query +} + +input GEN__news__MultipleNewsId @addField(name: "ids", path: ["ids", "id"]) { + ids: [Id]@omit +} + +input GEN__news__NewsInput { + body: String + id: Int + postImage: String + status: Status + title: String +} + +input Id { + id: Int +} + +enum Status { + DELETED + DRAFT + PUBLISHED +} + +type Address { + city: String + geo: Geo + street: String + suite: String + zipcode: String +} + +type Company { + bs: String + catchPhrase: String + name: String +} + +type GEN__news__NewsList { + news: [News] +} + +type Geo { + lat: String + lng: String +} + +type News { + body: String + id: Int + postImage: String + status: Status + title: String +} + +type Query { + GEN__news__NewsService__AddNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/AddNews", body: "\"{{.args.news}}\"", method: "POST") + GEN__news__NewsService__DeleteNews(newsId: Id!): Empty @http(url: "http://localhost:50051/news.NewsService/DeleteNews", body: "\"{{.args.newsId}}\"", method: "POST") + GEN__news__NewsService__EditNews(news: GEN__news__NewsInput!): News @http(url: "http://localhost:50051/news.NewsService/EditNews", body: "\"{{.args.news}}\"", method: "POST") + GEN__news__NewsService__GetAllNews: GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetAllNews", body: "{}", method: "POST") + GEN__news__NewsService__GetMultipleNews(multipleNewsId: GEN__news__MultipleNewsId!): GEN__news__NewsList @http(url: "http://localhost:50051/news.NewsService/GetMultipleNews", body: "\"{{.args.multipleNewsId}}\"", method: "POST") + GEN__news__NewsService__GetNews(newsId: Id!): News @http(url: "http://localhost:50051/news.NewsService/GetNews", body: "\"{{.args.newsId}}\"", method: "POST") + users: [User] @http(url: "http://jsonplaceholder.typicode.com/users") +} + +type User { + address: Address + company: Company + email: String + id: Int + name: String + phone: String + username: String + website: String +}