diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index 26a077906b..1e6e8f119c 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -81,10 +81,13 @@ pub enum Source { is_mutation: Option, field_name: String, }, + #[serde(rename_all = "camelCase")] Proto { src: Location, url: String, #[serde(skip_serializing_if = "Option::is_none")] + proto_paths: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "connectRPC")] connect_rpc: Option, }, @@ -220,9 +223,20 @@ impl Source { is_mutation, }) } - Source::Proto { src, url, connect_rpc } => { + Source::Proto { src, url, proto_paths, connect_rpc } => { let resolved_path = src.into_resolved(parent_dir); - Ok(Source::Proto { src: resolved_path, url, connect_rpc }) + let resolved_proto_paths = proto_paths.map(|paths| { + paths + .into_iter() + .map(|path| path.into_resolved(parent_dir)) + .collect() + }); + Ok(Source::Proto { + src: resolved_path, + url, + proto_paths: resolved_proto_paths, + 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 b56e162808..94ef7319cd 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -135,9 +135,11 @@ impl Generator { headers: headers.into_btree_map(), }); } - Source::Proto { src, url, connect_rpc } => { + Source::Proto { src, url, proto_paths, connect_rpc } => { let path = src.0; - let mut metadata = proto_reader.read(&path).await?; + let proto_paths = + proto_paths.map(|paths| paths.into_iter().map(|l| l.0).collect::>()); + let mut metadata = proto_reader.read(&path, proto_paths.as_deref()).await?; if let Some(relative_path_to_proto) = to_relative_path(output_dir, &path) { metadata.path = relative_path_to_proto; } diff --git a/src/core/config/reader.rs b/src/core/config/reader.rs index 609cb0eb62..f6d539360f 100644 --- a/src/core/config/reader.rs +++ b/src/core/config/reader.rs @@ -87,7 +87,7 @@ impl ConfigReader { } } LinkType::Protobuf => { - let meta = self.proto_reader.read(path).await?; + let meta = self.proto_reader.read(path, None).await?; extensions.add_proto(meta); } LinkType::Script => { diff --git a/src/core/proto_reader/reader.rs b/src/core/proto_reader/reader.rs index 81c65cc16a..6fdd0cd476 100644 --- a/src/core/proto_reader/reader.rs +++ b/src/core/proto_reader/reader.rs @@ -65,7 +65,7 @@ impl ProtoReader { /// Asynchronously reads all proto files from a list of paths pub async fn read_all>(&self, paths: &[T]) -> anyhow::Result> { - let resolved_protos = join_all(paths.iter().map(|v| self.read(v.as_ref()))) + let resolved_protos = join_all(paths.iter().map(|v| self.read(v.as_ref(), None))) .await .into_iter() .collect::>>()?; @@ -73,12 +73,20 @@ impl ProtoReader { } /// Reads a proto file from a path - pub async fn read>(&self, path: T) -> anyhow::Result { - let file_read = self.read_proto(path.as_ref(), None).await?; + pub async fn read>( + &self, + path: T, + proto_paths: Option<&[String]>, + ) -> anyhow::Result { + let file_read = self.read_proto(path.as_ref(), None, None).await?; Self::check_package(&file_read)?; let descriptors = self - .file_resolve(file_read, PathBuf::from(path.as_ref()).parent()) + .file_resolve( + file_read, + PathBuf::from(path.as_ref()).parent(), + proto_paths, + ) .await?; let metadata = ProtoMetadata { descriptor_set: FileDescriptorSet { file: descriptors }, @@ -130,12 +138,22 @@ impl ProtoReader { &self, parent_proto: FileDescriptorProto, parent_path: Option<&Path>, + proto_paths: Option<&[String]>, ) -> anyhow::Result> { self.resolve_dependencies(parent_proto, |import| { let parent_path = parent_path.map(|p| p.to_path_buf()); let this = self.clone(); - - async move { this.read_proto(import, parent_path.as_deref()).await }.boxed() + let proto_paths = proto_paths.map(|paths| { + paths + .iter() + .map(|p| Path::new(p).to_path_buf()) + .collect::>() + }); + async move { + this.read_proto(import, parent_path.as_deref(), proto_paths.as_deref()) + .await + } + .boxed() }) .await } @@ -159,27 +177,39 @@ impl ProtoReader { &self, path: T, parent_dir: Option<&Path>, + proto_paths: Option<&[PathBuf]>, ) -> anyhow::Result { let content = if let Ok(file) = GoogleFileResolver::new().open_file(path.as_ref()) { file.source() .context("Unable to extract content of google well-known proto file")? .to_string() } else { - let path = Self::resolve_path(path.as_ref(), parent_dir); + let path = Self::resolve_path(path.as_ref(), parent_dir, proto_paths); self.reader.read_file(path).await?.content }; Ok(protox_parse::parse(path.as_ref(), &content)?) } /// Checks if path is absolute else it joins file path with relative dir /// path - fn resolve_path(src: &str, root_dir: Option<&Path>) -> String { + fn resolve_path(src: &str, root_dir: Option<&Path>, proto_paths: Option<&[PathBuf]>) -> String { if src.starts_with("http") { return src.to_string(); } if Path::new(&src).is_absolute() { - src.to_string() - } else if let Some(path) = root_dir { + return src.to_string(); + } + + if let Some(proto_paths) = proto_paths { + for proto_path in proto_paths { + let path = proto_path.join(src); + if path.exists() { + return path.to_string_lossy().to_string(); + } + } + } + + if let Some(path) = root_dir { path.join(src).to_string_lossy().to_string() } else { src.to_string() @@ -210,7 +240,7 @@ mod test_proto_config { let runtime = crate::core::runtime::test::init(None); let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); reader - .read_proto("google/protobuf/empty.proto", None) + .read_proto("google/protobuf/empty.proto", None, None) .await .unwrap(); } @@ -225,7 +255,11 @@ mod test_proto_config { let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); let file_descriptors = reader - .file_resolve(reader.read_proto(&test_file, None).await?, Some(test_dir)) + .file_resolve( + reader.read_proto(&test_file, None, None).await?, + Some(test_dir), + None, + ) .await?; for file in file_descriptors .iter() @@ -248,7 +282,7 @@ mod test_proto_config { let reader = ProtoReader::init(ResourceReader::::cached(runtime.clone()), runtime); let proto_no_pkg = PathBuf::from(tailcall_fixtures::configs::SELF).join("proto_no_pkg.graphql"); - let config_module = reader.read(proto_no_pkg.to_str().unwrap()).await; + let config_module = reader.read(proto_no_pkg.to_str().unwrap(), None).await; assert!(config_module.is_err()); Ok(()) } diff --git a/tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto b/tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto new file mode 100644 index 0000000000..e4d9b6c72b --- /dev/null +++ b/tailcall-fixtures/fixtures/protobuf/news_proto_paths.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package news; + +import "protobuf/news_dto.proto"; +import "google/protobuf/empty.proto"; + +service NewsService { + rpc GetAllNews(google.protobuf.Empty) returns (NewsList) {} + rpc GetNews(NewsId) returns (News) {} + rpc GetMultipleNews(MultipleNewsId) returns (NewsList) {} + rpc DeleteNews(NewsId) returns (google.protobuf.Empty) {} + rpc EditNews(News) returns (News) {} + rpc AddNews(News) returns (News) {} +} \ No newline at end of file diff --git a/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md b/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.md new file mode 100644 index 0000000000..618da86ab5 --- /dev/null +++ b/tests/cli/fixtures/generator/gen_proto_with_proto_paths_config.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_paths.proto", + "url": "http://localhost:50051", + "protoPaths": ["tailcall-fixtures/fixtures/"] + } + } + ], + "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__gen_proto_with_proto_paths_config.md.snap b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_proto_with_proto_paths_config.md.snap new file mode 100644 index 0000000000..3e176c6dfd --- /dev/null +++ b/tests/cli/snapshots/cli_spec__test__generator_spec__tests__cli__fixtures__generator__gen_proto_with_proto_paths_config.md.snap @@ -0,0 +1,81 @@ +--- +source: tests/cli/gen.rs +expression: config.to_sdl() +--- +schema @server @upstream { + query: Query +} + +input GEN__news__MultipleNewsId { + ids: [Id] +} + +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 @grpc(url: "http://localhost:50051", body: "{{.args.news}}", method: "news.NewsService.AddNews") + GEN__news__NewsService__DeleteNews(newsId: Id!): Empty @grpc(url: "http://localhost:50051", body: "{{.args.newsId}}", method: "news.NewsService.DeleteNews") + GEN__news__NewsService__EditNews(news: GEN__news__NewsInput!): News @grpc(url: "http://localhost:50051", body: "{{.args.news}}", method: "news.NewsService.EditNews") + GEN__news__NewsService__GetAllNews: GEN__news__NewsList @grpc(url: "http://localhost:50051", method: "news.NewsService.GetAllNews") + GEN__news__NewsService__GetMultipleNews(multipleNewsId: GEN__news__MultipleNewsId!): GEN__news__NewsList @grpc(url: "http://localhost:50051", body: "{{.args.multipleNewsId}}", method: "news.NewsService.GetMultipleNews") + GEN__news__NewsService__GetNews(newsId: Id!): News @grpc(url: "http://localhost:50051", body: "{{.args.newsId}}", method: "news.NewsService.GetNews") + 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 +}