-
Notifications
You must be signed in to change notification settings - Fork 388
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
impl(generator):
request_id
-like helpers (#13605)
Add some functions to determine if a method has at least one `request_id`-like field, i.e., a non-repeating string field in the request message that expects UUIDV4 values.
- Loading branch information
Showing
6 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
#include "generator/internal/request_id.h" | ||
#include <google/api/field_info.pb.h> | ||
|
||
namespace google { | ||
namespace cloud { | ||
namespace generator_internal { | ||
|
||
bool MeetsRequestIdRequirements( | ||
google::protobuf::FieldDescriptor const& descriptor) { | ||
if (descriptor.type() != google::protobuf::FieldDescriptor::TYPE_STRING || | ||
descriptor.is_repeated() || descriptor.has_presence()) { | ||
return false; | ||
} | ||
if (!descriptor.options().HasExtension(google::api::field_info)) return false; | ||
auto const info = descriptor.options().GetExtension(google::api::field_info); | ||
return info.format() == google::api::FieldInfo::UUID4; | ||
} | ||
|
||
std::string RequestIdFieldName( | ||
YAML::Node const& service_config, | ||
google::protobuf::MethodDescriptor const& descriptor) try { | ||
if (descriptor.input_type() == nullptr) return {}; | ||
auto const& request_descriptor = *descriptor.input_type(); | ||
if (service_config.Type() != YAML::NodeType::Map) return {}; | ||
// This code is fairly defensive. First we need to find the | ||
// `publishing.method_settings` node, which must be a sequence. | ||
auto const& publishing = service_config["publishing"]; | ||
if (publishing.Type() != YAML::NodeType::Map) return {}; | ||
auto const& method_settings = publishing["method_settings"]; | ||
if (method_settings.Type() != YAML::NodeType::Sequence) return {}; | ||
for (auto const& m : method_settings) { | ||
// Each node in the `method_settings` sequence contains a map, the | ||
// `selector` field in the map is a string that may match the name of the | ||
// method we are interested in. | ||
if (m.Type() != YAML::NodeType::Map) continue; | ||
auto const& selector = m["selector"]; | ||
if (selector.Type() != YAML::NodeType::Scalar) continue; | ||
if (selector.as<std::string>() != descriptor.full_name()) continue; | ||
// Once we find the method, we need to find any auto populated field that | ||
// meets the requirements for a request id. | ||
auto const& auto_populated = m["auto_populated_fields"]; | ||
if (auto_populated.Type() != YAML::NodeType::Sequence) continue; | ||
for (auto const& f : auto_populated) { | ||
if (f.Type() != YAML::NodeType::Scalar) continue; | ||
auto const* fd = request_descriptor.FindFieldByName(f.as<std::string>()); | ||
if (fd == nullptr) continue; | ||
if (MeetsRequestIdRequirements(*fd)) return fd->name(); | ||
} | ||
} | ||
return {}; | ||
} catch (YAML::Exception const& ex) { | ||
// Ignore errors in the YAML file. If it is broken just fallback to having | ||
// no field. | ||
return {}; | ||
} | ||
|
||
} // namespace generator_internal | ||
} // namespace cloud | ||
} // namespace google |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
#ifndef GOOGLE_CLOUD_CPP_GENERATOR_INTERNAL_REQUEST_ID_H | ||
#define GOOGLE_CLOUD_CPP_GENERATOR_INTERNAL_REQUEST_ID_H | ||
|
||
#include <google/protobuf/descriptor.h> | ||
#include <yaml-cpp/yaml.h> | ||
#include <string> | ||
|
||
namespace google { | ||
namespace cloud { | ||
namespace generator_internal { | ||
|
||
/// Determine if a field is a request_id-like field. | ||
bool MeetsRequestIdRequirements( | ||
google::protobuf::FieldDescriptor const& descriptor); | ||
|
||
/// Returns the name of the (first) request_id-like field in the request | ||
/// message. | ||
std::string RequestIdFieldName( | ||
YAML::Node const& service_config, | ||
google::protobuf::MethodDescriptor const& descriptor); | ||
|
||
} // namespace generator_internal | ||
} // namespace cloud | ||
} // namespace google | ||
|
||
#endif // GOOGLE_CLOUD_CPP_GENERATOR_INTERNAL_REQUEST_ID_H |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
#include "generator/internal/request_id.h" | ||
#include "generator/testing/error_collectors.h" | ||
#include "generator/testing/fake_source_tree.h" | ||
#include <gmock/gmock.h> | ||
|
||
namespace google { | ||
namespace cloud { | ||
namespace generator_internal { | ||
namespace { | ||
|
||
using ::google::cloud::generator_testing::FakeSourceTree; | ||
using ::google::protobuf::DescriptorPool; | ||
using ::google::protobuf::FileDescriptor; | ||
using ::google::protobuf::FileDescriptorProto; | ||
using ::testing::NotNull; | ||
|
||
// We factor out the protobuf mumbo jumbo to keep the tests concise and | ||
// self-contained. The protobuf objects must be in scope for the duration of a | ||
// test. To achieve this, we pass in a `test` lambda which is invoked in this | ||
// method. | ||
void RunRequestIdTest(std::string const& service_proto, | ||
std::function<void(FileDescriptor const*)> const& test) { | ||
auto constexpr kServiceBoilerPlate = R"""( | ||
syntax = "proto3"; | ||
package google.cloud.test.v1; | ||
import "google/api/field_info.proto"; | ||
)"""; | ||
|
||
auto constexpr kFieldInfoProto = R"""( | ||
syntax = "proto3"; | ||
package google.api; | ||
import "google/protobuf/descriptor.proto"; | ||
extend google.protobuf.FieldOptions { | ||
google.api.FieldInfo field_info = 291403980; | ||
} | ||
message FieldInfo { | ||
enum Format { | ||
FORMAT_UNSPECIFIED = 0; | ||
UUID4 = 1; | ||
IPV4 = 2; | ||
IPV6 = 3; | ||
IPV4_OR_IPV6 = 4; | ||
} | ||
Format format = 1; | ||
} | ||
)"""; | ||
|
||
FakeSourceTree source_tree(std::map<std::string, std::string>{ | ||
{"google/api/field_info.proto", kFieldInfoProto}, | ||
{"google/cloud/foo/service.proto", kServiceBoilerPlate + service_proto}}); | ||
google::protobuf::compiler::SourceTreeDescriptorDatabase source_tree_db( | ||
&source_tree); | ||
google::protobuf::SimpleDescriptorDatabase simple_db; | ||
FileDescriptorProto file_proto; | ||
// We need descriptor.proto to be accessible by the pool | ||
// since our test file imports it. | ||
FileDescriptorProto::descriptor()->file()->CopyTo(&file_proto); | ||
simple_db.Add(file_proto); | ||
google::protobuf::MergedDescriptorDatabase merged_db(&simple_db, | ||
&source_tree_db); | ||
generator_testing::ErrorCollector collector; | ||
DescriptorPool pool(&merged_db, &collector); | ||
|
||
// Run the test. | ||
test(pool.FindFileByName("google/cloud/foo/service.proto")); | ||
} | ||
|
||
TEST(RequestId, FieldIsRequestId) { | ||
auto constexpr kProto = R"""( | ||
message Request { | ||
string treat_as_request_id = 1 [ | ||
(google.api.field_info).format = UUID4 | ||
]; | ||
} | ||
)"""; | ||
|
||
RunRequestIdTest(kProto, [](FileDescriptor const* fd) { | ||
ASSERT_THAT(fd, NotNull()); | ||
auto const& field = *fd->message_type(0)->field(0); | ||
EXPECT_TRUE(MeetsRequestIdRequirements(field)); | ||
}); | ||
} | ||
|
||
TEST(RequestId, FieldBadType) { | ||
auto constexpr kProto = R"""( | ||
message Request { | ||
bytes treat_as_request_id = 1 [ | ||
(google.api.field_info).format = UUID4 | ||
]; | ||
} | ||
)"""; | ||
|
||
RunRequestIdTest(kProto, [](FileDescriptor const* fd) { | ||
ASSERT_THAT(fd, NotNull()); | ||
auto const& field = *fd->message_type(0)->field(0); | ||
EXPECT_FALSE(MeetsRequestIdRequirements(field)); | ||
}); | ||
} | ||
|
||
TEST(RequestId, FieldRepeated) { | ||
auto constexpr kProto = R"""( | ||
message Request { | ||
repeated string treat_as_request_id = 1 [ | ||
(google.api.field_info).format = UUID4 | ||
]; | ||
} | ||
)"""; | ||
|
||
RunRequestIdTest(kProto, [](FileDescriptor const* fd) { | ||
ASSERT_THAT(fd, NotNull()); | ||
auto const& field = *fd->message_type(0)->field(0); | ||
EXPECT_FALSE(MeetsRequestIdRequirements(field)); | ||
}); | ||
} | ||
|
||
TEST(RequestId, FieldNoExtension) { | ||
auto constexpr kProto = R"""( | ||
message Request { | ||
string request_id = 1; | ||
} | ||
)"""; | ||
|
||
RunRequestIdTest(kProto, [](FileDescriptor const* fd) { | ||
ASSERT_THAT(fd, NotNull()); | ||
auto const& field = *fd->message_type(0)->field(0); | ||
EXPECT_FALSE(MeetsRequestIdRequirements(field)); | ||
}); | ||
} | ||
|
||
TEST(RequestId, FieldBadFormat) { | ||
auto constexpr kProto = R"""( | ||
message Request { | ||
string treat_as_request_id = 1 [ | ||
(google.api.field_info).format = IPV4 | ||
]; | ||
} | ||
)"""; | ||
|
||
RunRequestIdTest(kProto, [](FileDescriptor const* fd) { | ||
ASSERT_THAT(fd, NotNull()); | ||
auto const& field = *fd->message_type(0)->field(0); | ||
EXPECT_FALSE(MeetsRequestIdRequirements(field)); | ||
}); | ||
} | ||
|
||
TEST(RequestId, MethodRequestFieldName) { | ||
auto constexpr kProto = R"""( | ||
message M0 { | ||
string f1 = 1; | ||
string f2 = 2; | ||
string request_id = 3 [ (google.api.field_info).format = UUID4 ]; | ||
string another_request_id = 4 [ (google.api.field_info).format = UUID4 ]; | ||
} | ||
message M1 { | ||
string not_in_yaml = 1[ (google.api.field_info).format = UUID4 ]; | ||
} | ||
message M2 { | ||
string alternative = 1[ (google.api.field_info).format = UUID4 ]; | ||
} | ||
message M3 { | ||
string bad_format = 1[ (google.api.field_info).format = IPV4 ]; | ||
} | ||
service Service { | ||
rpc Method0(M0) returns (M0) {} | ||
rpc Method1(M1) returns (M0) {} | ||
rpc Method2(M2) returns (M0) {} | ||
rpc Method3(M3) returns (M0) {} | ||
} | ||
)"""; | ||
|
||
auto constexpr kServiceConfigYaml = R"""(publishing: | ||
method_settings: | ||
- selector: google.cloud.test.v1.Service.Method0 | ||
auto_populated_fields: | ||
- request_id | ||
- selector: google.cloud.test.v1.Service.Method2 | ||
auto_populated_fields: | ||
- alternative | ||
- selector: google.cloud.test.v1.Service.Method3 | ||
auto_populated_fields: | ||
- bad_format | ||
)"""; | ||
|
||
auto const service_config = YAML::Load(kServiceConfigYaml); | ||
|
||
RunRequestIdTest(kProto, [&service_config](FileDescriptor const* fd) { | ||
ASSERT_THAT(fd, NotNull()); | ||
ASSERT_EQ(fd->service_count(), 1); | ||
auto const& sd = *fd->service(0); | ||
ASSERT_EQ(sd.method_count(), 4); | ||
EXPECT_EQ(RequestIdFieldName(service_config, *sd.method(0)), "request_id"); | ||
EXPECT_EQ(RequestIdFieldName(service_config, *sd.method(1)), ""); | ||
EXPECT_EQ(RequestIdFieldName(service_config, *sd.method(2)), "alternative"); | ||
EXPECT_EQ(RequestIdFieldName(service_config, *sd.method(3)), ""); | ||
}); | ||
} | ||
|
||
} // namespace | ||
} // namespace generator_internal | ||
} // namespace cloud | ||
} // namespace google | ||
|
||
int main(int argc, char** argv) { | ||
::testing::InitGoogleTest(&argc, argv); | ||
return RUN_ALL_TESTS(); | ||
} |