Skip to content

Commit

Permalink
impl(generator): request_id-like helpers (#13605)
Browse files Browse the repository at this point in the history
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
coryan authored Feb 14, 2024
1 parent 2d2a391 commit b82de9d
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 0 deletions.
3 changes: 3 additions & 0 deletions generator/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ add_library(
internal/predicate_utils.h
internal/printer.h
internal/proto_definition_location.h
internal/request_id.cc
internal/request_id.h
internal/resolve_comment_references.cc
internal/resolve_comment_references.h
internal/resolve_method_return.cc
Expand Down Expand Up @@ -246,6 +248,7 @@ function (google_cloud_cpp_generator_define_tests)
internal/pagination_test.cc
internal/predicate_utils_test.cc
internal/printer_test.cc
internal/request_id_test.cc
internal/resolve_comment_references_test.cc
internal/resolve_method_return_test.cc
internal/routing_test.cc
Expand Down
2 changes: 2 additions & 0 deletions generator/google_cloud_cpp_generator.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ google_cloud_cpp_generator_hdrs = [
"internal/predicate_utils.h",
"internal/printer.h",
"internal/proto_definition_location.h",
"internal/request_id.h",
"internal/resolve_comment_references.h",
"internal/resolve_method_return.h",
"internal/retry_traits_generator.h",
Expand Down Expand Up @@ -111,6 +112,7 @@ google_cloud_cpp_generator_srcs = [
"internal/options_generator.cc",
"internal/pagination.cc",
"internal/predicate_utils.cc",
"internal/request_id.cc",
"internal/resolve_comment_references.cc",
"internal/resolve_method_return.cc",
"internal/retry_traits_generator.cc",
Expand Down
1 change: 1 addition & 0 deletions generator/google_cloud_cpp_generator_unit_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ google_cloud_cpp_generator_unit_tests = [
"internal/pagination_test.cc",
"internal/predicate_utils_test.cc",
"internal/printer_test.cc",
"internal/request_id_test.cc",
"internal/resolve_comment_references_test.cc",
"internal/resolve_method_return_test.cc",
"internal/routing_test.cc",
Expand Down
73 changes: 73 additions & 0 deletions generator/internal/request_id.cc
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
40 changes: 40 additions & 0 deletions generator/internal/request_id.h
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
226 changes: 226 additions & 0 deletions generator/internal/request_id_test.cc
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();
}

0 comments on commit b82de9d

Please sign in to comment.