diff --git a/common/typedef/BUILD b/common/typedef/BUILD new file mode 100644 index 000000000..e64427d4b --- /dev/null +++ b/common/typedef/BUILD @@ -0,0 +1,526 @@ +# Copyright 2026 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. + +load("@com_google_protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +cc_library( + name = "schema", + hdrs = ["schema.h"], +) + +cc_library( + name = "typedef", + srcs = ["typedef.cc"], + hdrs = ["typedef.h"], + deps = [ + ":schema", + "//common:ast", + "//common:constant", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/functional:overload", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/types:span", + ], +) + +cc_test( + name = "typedef_test", + srcs = ["typedef_test.cc"], + deps = [ + ":schema", + ":typedef", + "//common:ast", + "//common:constant", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + ], +) + +cc_library( + name = "schema_yaml", + srcs = ["schema_yaml.cc"], + hdrs = ["schema_yaml.h"], + deps = [ + ":schema", + ":yaml_helpers", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_test( + name = "schema_yaml_test", + srcs = ["schema_yaml_test.cc"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":schema", + ":schema_yaml", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/strings:string_view", + "@yaml-cpp", + ], +) + +cc_library( + name = "yaml_helpers", + srcs = ["yaml_helpers.cc"], + hdrs = ["yaml_helpers.h"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@yaml-cpp", + ], +) + +cc_library( + name = "typedef_yaml", + srcs = ["typedef_yaml.cc"], + hdrs = [ + "typedef_yaml.h", + "typedef_yaml_internal.h", + ], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":schema", + ":schema_yaml", + ":typedef", + ":yaml_helpers", + "//common:ast", + "//common:constant", + "//common:signature", + "//internal:status_macros", + "//internal:strings", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + "@yaml-cpp", + ], +) + +cc_test( + name = "typedef_yaml_test", + srcs = ["typedef_yaml_test.cc"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":schema", + ":schema_yaml", + ":typedef", + ":typedef_yaml", + ":yaml_helpers", + ":yaml_test_helpers", + "//common:ast", + "//common:constant", + "//internal:status_macros", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/time", + "@yaml-cpp", + ], +) + +cc_library( + name = "yaml_test_helpers", + testonly = True, + srcs = ["yaml_test_helpers.cc"], + hdrs = ["yaml_test_helpers.h"], + deps = [ + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + ], +) + +proto_library( + name = "typedef_protos", + srcs = ["typedef.proto"], + deps = [ + "@com_google_cel_spec//proto/cel/expr:checked_proto", + "@com_google_cel_spec//proto/cel/expr:syntax_proto", + ], +) + +cc_proto_library( + name = "typedef_cc_proto", + deps = [":typedef_protos"], +) + +cc_library( + name = "schema_proto", + srcs = ["schema_proto.cc"], + hdrs = ["schema_proto.h"], + deps = [ + ":schema", + ":typedef_cc_proto", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_test( + name = "schema_proto_test", + srcs = ["schema_proto_test.cc"], + deps = [ + ":schema", + ":schema_proto", + ":typedef_cc_proto", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_library( + name = "typedef_proto", + srcs = ["typedef_proto.cc"], + hdrs = ["typedef_proto.h"], + deps = [ + ":schema", + ":schema_proto", + ":typedef", + ":typedef_cc_proto", + "//common:ast", + "//common/ast:constant_proto", + "//internal:status_macros", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + "@com_google_absl//absl/types:variant", + "@com_google_cel_spec//proto/cel/expr:checked_cc_proto", + "@com_google_protobuf//:protobuf", + "@com_google_protobuf//:struct_cc_proto", + ], +) + +cc_library( + name = "json_schema", + hdrs = ["json_schema.h"], + deps = [ + ":schema", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_library( + name = "json_schema_yaml", + srcs = ["json_schema_yaml.cc"], + hdrs = ["json_schema_yaml.h"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":json_schema", + ":schema", + ":schema_yaml", + ":yaml_helpers", + "//internal:status_macros", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@yaml-cpp", + ], +) + +cc_test( + name = "json_schema_yaml_test", + srcs = ["json_schema_yaml_test.cc"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":json_schema", + ":json_schema_yaml", + ":schema", + ":schema_yaml", + ":typedef", + ":typedef_yaml", + ":yaml_helpers", + ":yaml_test_helpers", + "//internal:status_macros", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings:string_view", + "@yaml-cpp", + ], +) + +proto_library( + name = "json_schema_protos", + srcs = ["json_schema.proto"], + deps = [":typedef_protos"], +) + +cc_proto_library( + name = "json_schema_cc_proto", + deps = [":json_schema_protos"], +) + +cc_library( + name = "json_schema_proto", + srcs = ["json_schema_proto.cc"], + hdrs = ["json_schema_proto.h"], + deps = [ + ":json_schema", + ":json_schema_cc_proto", + ":schema", + ":schema_proto", + ":typedef_cc_proto", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_test( + name = "json_schema_proto_test", + srcs = ["json_schema_proto_test.cc"], + deps = [ + ":json_schema_cc_proto", + ":json_schema_proto", + ":schema", + ":typedef_cc_proto", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_protobuf//:protobuf", + ], +) + +cc_library( + name = "proto_schema", + hdrs = ["proto_schema.h"], + deps = [ + ":schema", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_library( + name = "proto_schema_yaml", + srcs = ["proto_schema_yaml.cc"], + hdrs = ["proto_schema_yaml.h"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":proto_schema", + ":schema", + ":schema_yaml", + ":yaml_helpers", + "//internal:status_macros", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@yaml-cpp", + ], +) + +cc_test( + name = "proto_schema_yaml_test", + srcs = ["proto_schema_yaml_test.cc"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":proto_schema", + ":proto_schema_yaml", + ":schema", + ":schema_yaml", + ":typedef", + ":typedef_yaml", + ":yaml_helpers", + ":yaml_test_helpers", + "//internal:status_macros", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings:string_view", + "@yaml-cpp", + ], +) + +proto_library( + name = "proto_schema_protos", + srcs = ["proto_schema.proto"], + deps = [":typedef_protos"], +) + +cc_proto_library( + name = "proto_schema_cc_proto", + deps = [":proto_schema_protos"], +) + +cc_library( + name = "proto_schema_proto", + srcs = ["proto_schema_proto.cc"], + hdrs = ["proto_schema_proto.h"], + deps = [ + ":proto_schema", + ":proto_schema_cc_proto", + ":schema", + ":schema_proto", + ":typedef_cc_proto", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_test( + name = "proto_schema_proto_test", + srcs = ["proto_schema_proto_test.cc"], + deps = [ + ":proto_schema", + ":proto_schema_cc_proto", + ":proto_schema_proto", + ":schema", + ":schema_proto", + ":typedef_cc_proto", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_protobuf//:protobuf", + ], +) + +cc_test( + name = "typedef_proto_test", + srcs = ["typedef_proto_test.cc"], + copts = ["-fexceptions"], + data = [ + "//common/typedef/testdata:typedef_testdata", + ], + features = ["-use_header_modules"], + deps = [ + ":json_schema", + ":json_schema_cc_proto", + ":json_schema_proto", + ":json_schema_yaml", + ":proto_schema_proto", + ":proto_schema_yaml", + ":schema_proto", + ":schema_yaml", + ":typedef", + ":typedef_cc_proto", + ":typedef_proto", + ":typedef_yaml", + "//common:ast", + "//internal:runfiles", + "//internal:testing", + "@com_google_absl//absl/log:absl_log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@com_google_protobuf//:protobuf", + ], +) + +cc_library( + name = "proto_to_typedef", + srcs = ["proto_to_typedef.cc"], + hdrs = ["proto_to_typedef.h"], + deps = [ + ":proto_schema", + ":schema", + ":typedef", + "//common:ast", + "//common:constant", + "//common:minimal_descriptor_database", + "//internal:status_macros", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf", + ], +) + +cc_test( + name = "proto_to_typedef_test", + srcs = ["proto_to_typedef_test.cc"], + copts = ["-fexceptions"], + data = [ + "//common/typedef/testdata:typedef_testdata", + ], + features = ["-use_header_modules"], + deps = [ + ":proto_schema", + ":proto_schema_yaml", + ":proto_to_typedef", + ":schema_yaml", + ":typedef", + ":typedef_yaml", + ":yaml_test_helpers", + "//common:minimal_descriptor_database", + "//internal:runfiles", + "//internal:testing", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log:absl_log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@com_google_cel_spec//proto/cel/expr/conformance/proto2:test_all_types_cc_proto", + "@com_google_cel_spec//proto/cel/expr/conformance/proto3:test_all_types_cc_proto", + "@com_google_protobuf//:protobuf", + ], +) diff --git a/common/typedef/json_schema.h b/common/typedef/json_schema.h new file mode 100644 index 000000000..1d3e075d8 --- /dev/null +++ b/common/typedef/json_schema.h @@ -0,0 +1,52 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_H_ + +#include +#include + +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" + +namespace cel { + +inline constexpr absl::string_view kJsonSchemaName = "json"; + +class JsonSchemaObjectProperties final : public SchemaObjectProperties { + public: + std::optional name; +}; + +class JsonSchemaFieldProperties final : public SchemaFieldProperties { + public: + std::optional name; +}; + +class JsonSchemaEnumProperties final : public SchemaEnumProperties { + public: + std::optional style; + std::optional omit_unspecified; +}; + +class JsonSchemaEnumConstantProperties final + : public SchemaEnumConstantProperties { + public: + std::optional name; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_H_ diff --git a/common/typedef/json_schema.proto b/common/typedef/json_schema.proto new file mode 100644 index 000000000..c35bbdd52 --- /dev/null +++ b/common/typedef/json_schema.proto @@ -0,0 +1,58 @@ +// Copyright 2026 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. + +edition = "2023"; + +package cel.types; + +import "common/typedef/typedef.proto"; + +option cc_enable_arenas = true; + +// JSON Schema properties attached to an Object. +message JsonSchemaObjectProperties { + string name = 1; +} + +// JSON Schema properties attached to an Object.Field. +message JsonSchemaFieldProperties { + string name = 1; +} + +// JSON Schema properties attached to an Enum. +message JsonSchemaEnumProperties { + string style = 1; + bool omit_unspecified = 2; +} + +// JSON Schema properties attached to an Enum.EnumConstant. +message JsonSchemaEnumConstantProperties { + string name = 1; +} + +extend Object { + JsonSchemaObjectProperties json_object_schema = 1000; +} + +extend Object.Field { + JsonSchemaFieldProperties json_field_schema = 1000; +} + +extend Enum { + JsonSchemaEnumProperties json_enum_schema = 1000; +} + +extend Enum.EnumConstant { + JsonSchemaEnumConstantProperties json_enum_constant_schema = 1000; +} diff --git a/common/typedef/json_schema_proto.cc b/common/typedef/json_schema_proto.cc new file mode 100644 index 000000000..9516608b1 --- /dev/null +++ b/common/typedef/json_schema_proto.cc @@ -0,0 +1,165 @@ +// Copyright 2026 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 "common/typedef/json_schema_proto.h" + +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "common/typedef/json_schema.h" +#include "common/typedef/json_schema.pb.h" +#include "common/typedef/schema.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +absl::StatusOr> +JsonSchemaProto::DecodeObjectProperties(const types::Object& proto) const { + if (!proto.HasExtension(types::json_object_schema)) { + return nullptr; + } + const types::JsonSchemaObjectProperties& ext = + proto.GetExtension(types::json_object_schema); + auto result = std::make_unique(); + if (ext.has_name()) { + result->name = ext.name(); + } + return result; +} + +absl::StatusOr> +JsonSchemaProto::DecodeFieldProperties( + const types::Object::Field& proto) const { + if (!proto.HasExtension(types::json_field_schema)) { + return nullptr; + } + const types::JsonSchemaFieldProperties& ext = + proto.GetExtension(types::json_field_schema); + auto result = std::make_unique(); + if (ext.has_name()) { + result->name = ext.name(); + } + return result; +} + +absl::StatusOr> +JsonSchemaProto::DecodeEnumProperties(const types::Enum& proto) const { + if (!proto.HasExtension(types::json_enum_schema)) { + return nullptr; + } + const types::JsonSchemaEnumProperties& ext = + proto.GetExtension(types::json_enum_schema); + auto result = std::make_unique(); + if (ext.has_style()) { + result->style = ext.style(); + } + if (ext.has_omit_unspecified()) { + result->omit_unspecified = ext.omit_unspecified(); + } + return result; +} + +absl::StatusOr> +JsonSchemaProto::DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const { + if (!proto.HasExtension(types::json_enum_constant_schema)) { + return nullptr; + } + const types::JsonSchemaEnumConstantProperties& ext = + proto.GetExtension(types::json_enum_constant_schema); + auto result = std::make_unique(); + if (ext.has_name()) { + result->name = ext.name(); + } + return result; +} + +absl::Status JsonSchemaProto::EncodeObjectProperties( + const SchemaObjectProperties& properties, + types::Object* absl_nonnull proto) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid object properties type for 'json' schema"); + } + types::JsonSchemaObjectProperties* ext = + proto->MutableExtension(types::json_object_schema); + ext->Clear(); + if (props->name.has_value()) { + ext->set_name(*props->name); + } + return absl::OkStatus(); +} + +absl::Status JsonSchemaProto::EncodeFieldProperties( + const SchemaFieldProperties& properties, + types::Object::Field* absl_nonnull proto) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid field properties type for 'json' schema"); + } + types::JsonSchemaFieldProperties* ext = + proto->MutableExtension(types::json_field_schema); + ext->Clear(); + if (props->name.has_value()) { + ext->set_name(*props->name); + } + return absl::OkStatus(); +} + +absl::Status JsonSchemaProto::EncodeEnumProperties( + const SchemaEnumProperties& properties, + types::Enum* absl_nonnull proto) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid enum properties type for 'json' schema"); + } + types::JsonSchemaEnumProperties* ext = + proto->MutableExtension(types::json_enum_schema); + ext->Clear(); + if (props->style.has_value()) { + ext->set_style(*props->style); + } + if (props->omit_unspecified.has_value()) { + ext->set_omit_unspecified(*props->omit_unspecified); + } + return absl::OkStatus(); +} + +absl::Status JsonSchemaProto::EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + types::Enum::EnumConstant* absl_nonnull proto) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid enum constant properties type for 'json' schema"); + } + types::JsonSchemaEnumConstantProperties* ext = + proto->MutableExtension(types::json_enum_constant_schema); + ext->Clear(); + if (props->name.has_value()) { + ext->set_name(*props->name); + } + return absl::OkStatus(); +} + +} // namespace cel diff --git a/common/typedef/json_schema_proto.h b/common/typedef/json_schema_proto.h new file mode 100644 index 000000000..b48341f3c --- /dev/null +++ b/common/typedef/json_schema_proto.h @@ -0,0 +1,72 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_PROTO_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_PROTO_H_ + +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/json_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_proto.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +// SchemaProto implementation for JSON Schema ("json"). +class JsonSchemaProto : public SchemaProto { + public: + static constexpr absl::string_view kName = kJsonSchemaName; + + JsonSchemaProto() = default; + + absl::string_view name() const override { return kName; } + + absl::StatusOr> + DecodeObjectProperties(const types::Object& proto) const override; + + absl::StatusOr> DecodeFieldProperties( + const types::Object::Field& proto) const override; + + absl::StatusOr> DecodeEnumProperties( + const types::Enum& proto) const override; + + absl::StatusOr> + DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const override; + + absl::Status EncodeObjectProperties( + const SchemaObjectProperties& properties, + types::Object* absl_nonnull proto) const override; + + absl::Status EncodeFieldProperties( + const SchemaFieldProperties& properties, + types::Object::Field* absl_nonnull proto) const override; + + absl::Status EncodeEnumProperties( + const SchemaEnumProperties& properties, + types::Enum* absl_nonnull proto) const override; + + absl::Status EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + types::Enum::EnumConstant* absl_nonnull proto) const override; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_PROTO_H_ diff --git a/common/typedef/json_schema_proto_test.cc b/common/typedef/json_schema_proto_test.cc new file mode 100644 index 000000000..524037f80 --- /dev/null +++ b/common/typedef/json_schema_proto_test.cc @@ -0,0 +1,122 @@ +// Copyright 2026 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 "common/typedef/json_schema_proto.h" + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "common/typedef/json_schema.pb.h" +#include "common/typedef/schema.h" +#include "common/typedef/typedef.pb.h" +#include "internal/testing.h" +#include "google/protobuf/unknown_field_set.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::HasSubstr; +using ::testing::IsNull; +using ::testing::NotNull; + +struct DummyObjectProperties : public SchemaObjectProperties {}; +struct DummyFieldProperties : public SchemaFieldProperties {}; +struct DummyEnumProperties : public SchemaEnumProperties {}; +struct DummyEnumConstantProperties : public SchemaEnumConstantProperties {}; + +TEST(JsonSchemaProtoTest, SchemaName) { + JsonSchemaProto schema; + EXPECT_EQ(schema.name(), "json"); +} + +TEST(JsonSchemaProtoTest, DecodeReturnsNullptrWhenExtensionAbsent) { + JsonSchemaProto schema; + types::Object obj_proto; + types::Object::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant const_proto; + + EXPECT_THAT(schema.DecodeObjectProperties(obj_proto), IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeFieldProperties(field_proto), + IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeEnumProperties(enum_proto), IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeEnumConstantProperties(const_proto), + IsOkAndHolds(IsNull())); +} + +TEST(JsonSchemaProtoTest, EncodeTypeMismatchErrors) { + JsonSchemaProto schema; + types::Object obj_proto; + types::Object::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant const_proto; + + DummyObjectProperties dummy_obj; + EXPECT_THAT( + schema.EncodeObjectProperties(dummy_obj, &obj_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid object properties type for 'json' schema"))); + + DummyFieldProperties dummy_field; + EXPECT_THAT( + schema.EncodeFieldProperties(dummy_field, &field_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid field properties type for 'json' schema"))); + + DummyEnumProperties dummy_enum; + EXPECT_THAT( + schema.EncodeEnumProperties(dummy_enum, &enum_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum properties type for 'json' schema"))); + + DummyEnumConstantProperties dummy_const; + EXPECT_THAT(schema.EncodeEnumConstantProperties(dummy_const, &const_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum constant properties type for " + "'json' schema"))); +} + +TEST(JsonSchemaProtoTest, DecodeIgnoresUnknownFields) { + JsonSchemaProto schema; + types::Object obj_proto; + types::Object::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant const_proto; + + auto* obj_ext = obj_proto.MutableExtension(types::json_object_schema); + obj_ext->GetReflection()->MutableUnknownFields(obj_ext)->AddVarint(999, 1); + EXPECT_THAT(schema.DecodeObjectProperties(obj_proto), + IsOkAndHolds(NotNull())); + + auto* field_ext = field_proto.MutableExtension(types::json_field_schema); + field_ext->GetReflection()->MutableUnknownFields(field_ext)->AddVarint(999, + 1); + EXPECT_THAT(schema.DecodeFieldProperties(field_proto), + IsOkAndHolds(NotNull())); + + auto* enum_ext = enum_proto.MutableExtension(types::json_enum_schema); + enum_ext->GetReflection()->MutableUnknownFields(enum_ext)->AddVarint(999, 1); + EXPECT_THAT(schema.DecodeEnumProperties(enum_proto), IsOkAndHolds(NotNull())); + + auto* const_ext = + const_proto.MutableExtension(types::json_enum_constant_schema); + const_ext->GetReflection()->MutableUnknownFields(const_ext)->AddVarint(999, + 1); + EXPECT_THAT(schema.DecodeEnumConstantProperties(const_proto), + IsOkAndHolds(NotNull())); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/json_schema_yaml.cc b/common/typedef/json_schema_yaml.cc new file mode 100644 index 000000000..306b6d397 --- /dev/null +++ b/common/typedef/json_schema_yaml.cc @@ -0,0 +1,231 @@ +// Copyright 2026 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 "common/typedef/json_schema_yaml.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "common/typedef/json_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/yaml_helpers.h" +#include "internal/status_macros.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/emittermanip.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel { + +absl::StatusOr> +JsonSchemaYaml::DecodeObjectProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError(yaml, node, + "Node 'json' object schema is not a map"); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError( + yaml, kv.first, + "Property key in 'json' object schema is not a string"); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'name' is not a string"); + } + result->name = internal::GetString(yaml, val); + } else { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported 'json' object property: '", key, "'")); + } + } + return result; +} + +absl::StatusOr> +JsonSchemaYaml::DecodeFieldProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError(yaml, node, + "Node 'json' field schema is not a map"); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError( + yaml, kv.first, + "Property key in 'json' field schema is not a string"); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'name' is not a string"); + } + result->name = internal::GetString(yaml, val); + } else { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported 'json' field property: '", key, "'")); + } + } + return result; +} + +absl::StatusOr> +JsonSchemaYaml::DecodeEnumProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError(yaml, node, + "Node 'json' enum schema is not a map"); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError( + yaml, kv.first, "Property key in 'json' enum schema is not a string"); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "style") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'style' is not a string"); + } + result->style = internal::GetString(yaml, val); + } else if (key == "omit_unspecified") { + CEL_ASSIGN_OR_RETURN(result->omit_unspecified, + internal::GetBool(yaml, "omit_unspecified", val)); + } else { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported 'json' enum property: '", key, "'")); + } + } + return result; +} + +absl::StatusOr> +JsonSchemaYaml::DecodeEnumConstantProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError(yaml, node, + "Node 'json' enum constant schema is not a map"); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError( + yaml, kv.first, + "Property key in 'json' enum constant schema is not a string"); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'name' is not a string"); + } + result->name = internal::GetString(yaml, val); + } else { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported 'json' enum constant property: '", key, + "'")); + } + } + return result; +} + +absl::Status JsonSchemaYaml::EncodeObjectProperties( + const SchemaObjectProperties& properties, YAML::Emitter& out) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid object properties type for 'json' schema"); + } + out << YAML::BeginMap; + if (props->name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *props->name; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status JsonSchemaYaml::EncodeFieldProperties( + const SchemaFieldProperties& properties, YAML::Emitter& out) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid field properties type for 'json' schema"); + } + out << YAML::BeginMap; + if (props->name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *props->name; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status JsonSchemaYaml::EncodeEnumProperties( + const SchemaEnumProperties& properties, YAML::Emitter& out) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid enum properties type for 'json' schema"); + } + out << YAML::BeginMap; + if (props->style.has_value()) { + out << YAML::Key << "style" << YAML::Value << YAML::DoubleQuoted + << *props->style; + } + if (props->omit_unspecified.has_value()) { + out << YAML::Key << "omit_unspecified" << YAML::Value + << *props->omit_unspecified; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status JsonSchemaYaml::EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, YAML::Emitter& out) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + "Invalid enum constant properties type for 'json' schema"); + } + out << YAML::BeginMap; + if (props->name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *props->name; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +} // namespace cel diff --git a/common/typedef/json_schema_yaml.h b/common/typedef/json_schema_yaml.h new file mode 100644 index 000000000..e7441cb7b --- /dev/null +++ b/common/typedef/json_schema_yaml.h @@ -0,0 +1,68 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_YAML_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_YAML_H_ + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/json_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_yaml.h" + +namespace cel { + +// SchemaYaml implementation for JSON Schema ("json"). +class JsonSchemaYaml : public SchemaYaml { + public: + static constexpr absl::string_view kName = kJsonSchemaName; + + JsonSchemaYaml() = default; + + absl::string_view name() const override { return kName; } + + absl::StatusOr> + DecodeObjectProperties(absl::string_view yaml, + const YAML::Node& node) const override; + + absl::StatusOr> DecodeFieldProperties( + absl::string_view yaml, const YAML::Node& node) const override; + + absl::StatusOr> DecodeEnumProperties( + absl::string_view yaml, const YAML::Node& node) const override; + + absl::StatusOr> + DecodeEnumConstantProperties(absl::string_view yaml, + const YAML::Node& node) const override; + + absl::Status EncodeObjectProperties(const SchemaObjectProperties& properties, + YAML::Emitter& out) const override; + + absl::Status EncodeFieldProperties(const SchemaFieldProperties& properties, + YAML::Emitter& out) const override; + + absl::Status EncodeEnumProperties(const SchemaEnumProperties& properties, + YAML::Emitter& out) const override; + + absl::Status EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + YAML::Emitter& out) const override; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_JSON_SCHEMA_YAML_H_ diff --git a/common/typedef/json_schema_yaml_test.cc b/common/typedef/json_schema_yaml_test.cc new file mode 100644 index 000000000..2f648f3ae --- /dev/null +++ b/common/typedef/json_schema_yaml_test.cc @@ -0,0 +1,276 @@ +// Copyright 2026 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 "common/typedef/json_schema_yaml.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/json_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef_yaml.h" +#include "common/typedef/yaml_helpers.h" +#include "common/typedef/yaml_test_helpers.h" +#include "internal/status_macros.h" +#include "internal/testing.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/node/parse.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::HasSubstr; +using ::testing::NotNull; +using ::testing::Optional; + +absl::StatusOr RoundTripObject(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, schema.DecodeObjectProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeObjectProperties(*props, out)); + return ss.str(); +} + +absl::StatusOr RoundTripField(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, schema.DecodeFieldProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeFieldProperties(*props, out)); + return ss.str(); +} + +absl::StatusOr RoundTripEnum(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, schema.DecodeEnumProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeEnumProperties(*props, out)); + return ss.str(); +} + +absl::StatusOr RoundTripEnumConstant(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, + schema.DecodeEnumConstantProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeEnumConstantProperties(*props, out)); + return ss.str(); +} + +TEST(JsonSchemaYamlTest, DecodeAndEncodeProperties) { + JsonSchemaYaml schema; + + EXPECT_THAT(RoundTripObject(schema, "name: \"PersonObj\""), + IsOkAndHolds("name: \"PersonObj\"")); + EXPECT_THAT(RoundTripObject(schema, "invalid: val"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + EXPECT_THAT(RoundTripField(schema, "name: \"itemId\""), + IsOkAndHolds("name: \"itemId\"")); + EXPECT_THAT(RoundTripField(schema, "id: 12"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + absl::string_view enum_yaml = + "style: \"string\"\n" + "omit_unspecified: true"; + EXPECT_THAT(RoundTripEnum(schema, enum_yaml), IsOkAndHolds(enum_yaml)); + EXPECT_THAT(RoundTripEnum(schema, "omit_unspecified: not_a_bool"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'omit_unspecified' is not a boolean"))); + EXPECT_THAT(RoundTripEnum(schema, "unknown: val"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + EXPECT_THAT(RoundTripEnumConstant(schema, "name: \"shipped\""), + IsOkAndHolds("name: \"shipped\"")); + EXPECT_THAT(RoundTripEnumConstant(schema, "unknown: val"), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(JsonSchemaYamlTest, EndToEndTypeDefsYamlRoundTrip) { + SchemaYamlRegistry registry(std::make_unique()); + + std::string expected_yaml = Unindent(R"yaml( + - object: + name: "com.example.Item" + schemas: + json: + name: "Item" + fields: + - name: "item_id" + type: "string" + schemas: + json: + name: "itemId" + - enum: + name: "com.example.Status" + schemas: + json: + style: "string" + omit_unspecified: true + constants: + - name: "UNSPECIFIED" + id: 0 + - name: "ACTIVE" + id: 1 + schemas: + json: + name: "active" + )yaml"); + + ASSERT_OK_AND_ASSIGN(std::vector type_defs, + TypeDefsFromYaml(expected_yaml, registry)); + ASSERT_EQ(type_defs.size(), 2); + + const auto* obj_props = dynamic_cast( + type_defs[0].object_type().schema_specific_properties.at("json").get()); + ASSERT_THAT(obj_props, NotNull()); + EXPECT_THAT(obj_props->name, Optional(std::string("Item"))); + + const auto* enum_props = dynamic_cast( + type_defs[1].enum_type().schema_specific_properties.at("json").get()); + ASSERT_THAT(enum_props, NotNull()); + EXPECT_THAT(enum_props->style, Optional(std::string("string"))); + EXPECT_THAT(enum_props->omit_unspecified, Optional(true)); + + std::stringstream ss; + ASSERT_THAT(TypeDefsToYaml(type_defs, registry, ss), IsOk()); + EXPECT_EQ(ss.str(), expected_yaml); +} + +struct DummyObjectProperties : public SchemaObjectProperties {}; +struct DummyFieldProperties : public SchemaFieldProperties {}; +struct DummyEnumProperties : public SchemaEnumProperties {}; +struct DummyEnumConstantProperties : public SchemaEnumConstantProperties {}; + +TEST(JsonSchemaYamlTest, SchemaName) { + JsonSchemaYaml schema; + EXPECT_EQ(schema.name(), "json"); +} + +TEST(JsonSchemaYamlTest, EmptyPropertiesRoundTrip) { + JsonSchemaYaml schema; + EXPECT_THAT(RoundTripObject(schema, "{}"), IsOkAndHolds("{}")); + EXPECT_THAT(RoundTripField(schema, "{}"), IsOkAndHolds("{}")); + EXPECT_THAT(RoundTripEnum(schema, "{}"), IsOkAndHolds("{}")); + EXPECT_THAT(RoundTripEnumConstant(schema, "{}"), IsOkAndHolds("{}")); +} + +TEST(JsonSchemaYamlTest, ValidationErrors) { + JsonSchemaYaml schema; + YAML::Node seq = YAML::Load("[]"); + EXPECT_THAT(schema.DecodeObjectProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'json' object schema is not a map"))); + EXPECT_THAT(schema.DecodeFieldProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'json' field schema is not a map"))); + EXPECT_THAT(schema.DecodeEnumProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'json' enum schema is not a map"))); + EXPECT_THAT( + schema.DecodeEnumConstantProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'json' enum constant schema is not a map"))); + + EXPECT_THAT( + RoundTripObject(schema, "? [1]: val"), + StatusIs( + absl::StatusCode::kInvalidArgument, + HasSubstr("Property key in 'json' object schema is not a string"))); + EXPECT_THAT( + RoundTripField(schema, "? [1]: val"), + StatusIs( + absl::StatusCode::kInvalidArgument, + HasSubstr("Property key in 'json' field schema is not a string"))); + EXPECT_THAT( + RoundTripEnum(schema, "? [1]: val"), + StatusIs( + absl::StatusCode::kInvalidArgument, + HasSubstr("Property key in 'json' enum schema is not a string"))); + EXPECT_THAT( + RoundTripEnumConstant(schema, "? [1]: val"), + StatusIs( + absl::StatusCode::kInvalidArgument, + HasSubstr( + "Property key in 'json' enum constant schema is not a string"))); + + EXPECT_THAT(RoundTripObject(schema, "name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'name' is not a string"))); + EXPECT_THAT(RoundTripField(schema, "name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'name' is not a string"))); + EXPECT_THAT(RoundTripEnum(schema, "style: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'style' is not a string"))); + EXPECT_THAT(RoundTripEnumConstant(schema, "name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'name' is not a string"))); +} + +TEST(JsonSchemaYamlTest, EncodeTypeMismatchErrors) { + JsonSchemaYaml schema; + std::stringstream ss; + YAML::Emitter out(ss); + + DummyObjectProperties dummy_obj; + EXPECT_THAT( + schema.EncodeObjectProperties(dummy_obj, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid object properties type for 'json' schema"))); + + DummyFieldProperties dummy_field; + EXPECT_THAT( + schema.EncodeFieldProperties(dummy_field, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid field properties type for 'json' schema"))); + + DummyEnumProperties dummy_enum; + EXPECT_THAT( + schema.EncodeEnumProperties(dummy_enum, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum properties type for 'json' schema"))); + + DummyEnumConstantProperties dummy_const; + EXPECT_THAT(schema.EncodeEnumConstantProperties(dummy_const, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum constant properties type for " + "'json' schema"))); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/proto_schema.h b/common/typedef/proto_schema.h new file mode 100644 index 000000000..05f415964 --- /dev/null +++ b/common/typedef/proto_schema.h @@ -0,0 +1,71 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_H_ + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" + +namespace cel { + +inline constexpr absl::string_view kProto2SchemaName = "proto2"; +inline constexpr absl::string_view kProto3SchemaName = "proto3"; + +struct ProtoExtensionRange { + int32_t start = 0; + int32_t end = 0; + + friend bool operator==(const ProtoExtensionRange& a, + const ProtoExtensionRange& b) = default; +}; + +class ProtoObjectProperties final : public SchemaObjectProperties { + public: + std::optional name; + std::optional file_name; + std::vector extension_ranges; +}; + +class ProtoFieldProperties final : public SchemaFieldProperties { + public: + std::optional name; + std::optional id; + std::optional json_name; + std::optional type; + std::optional label; + std::optional oneof; + std::optional default_value; +}; + +class ProtoEnumProperties final : public SchemaEnumProperties { + public: + std::optional name; + std::optional allow_alias; + std::optional file_name; +}; + +class ProtoEnumConstantProperties final : public SchemaEnumConstantProperties { + public: + std::optional name; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_H_ diff --git a/common/typedef/proto_schema.proto b/common/typedef/proto_schema.proto new file mode 100644 index 000000000..ee986a356 --- /dev/null +++ b/common/typedef/proto_schema.proto @@ -0,0 +1,77 @@ +// Copyright 2026 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. + +edition = "2023"; + +package cel.types; + +import "common/typedef/typedef.proto"; + +option cc_enable_arenas = true; + +// Represents a declared protobuf extension range [start, end) on a message. +message ProtoExtensionRange { + int32 start = 1; + int32 end = 2; +} + +// Protocol Buffers schema properties attached to an Object. +message ProtoObjectProperties { + string name = 1; + string file_name = 2; + repeated ProtoExtensionRange extension_ranges = 3; +} + +// Protocol Buffers schema properties attached to an Object.Field. +message ProtoFieldProperties { + string name = 1; + int32 id = 2; + string json_name = 3; + string type = 4; + string label = 5; + string oneof = 6; + string default_value = 7; +} + +// Protocol Buffers schema properties attached to an Enum. +message ProtoEnumProperties { + string name = 1; + bool allow_alias = 2; + string file_name = 3; +} + +// Protocol Buffers schema properties attached to an Enum.EnumConstant. +message ProtoEnumConstantProperties { + string name = 1; +} + +extend Object { + ProtoObjectProperties proto2_object_schema = 1001; + ProtoObjectProperties proto3_object_schema = 1002; +} + +extend Object.Field { + ProtoFieldProperties proto2_field_schema = 1001; + ProtoFieldProperties proto3_field_schema = 1002; +} + +extend Enum { + ProtoEnumProperties proto2_enum_schema = 1001; + ProtoEnumProperties proto3_enum_schema = 1002; +} + +extend Enum.EnumConstant { + ProtoEnumConstantProperties proto2_enum_constant_schema = 1001; + ProtoEnumConstantProperties proto3_enum_constant_schema = 1002; +} diff --git a/common/typedef/proto_schema_proto.cc b/common/typedef/proto_schema_proto.cc new file mode 100644 index 000000000..934b81a48 --- /dev/null +++ b/common/typedef/proto_schema_proto.cc @@ -0,0 +1,249 @@ +// Copyright 2026 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 "common/typedef/proto_schema_proto.h" + +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/proto_schema.pb.h" +#include "common/typedef/schema.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +absl::StatusOr> +ProtoSchemaProto::DecodeObjectProperties(const types::Object& proto) const { + const bool has_ext = (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_object_schema) + : proto.HasExtension(types::proto3_object_schema); + if (!has_ext) { + return nullptr; + } + const types::ProtoObjectProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_object_schema) + : proto.GetExtension(types::proto3_object_schema); + auto result = std::make_unique(); + if (ext.has_name()) { + result->name = ext.name(); + } + if (ext.has_file_name()) { + result->file_name = ext.file_name(); + } + for (const types::ProtoExtensionRange& range : ext.extension_ranges()) { + result->extension_ranges.push_back( + ProtoExtensionRange{.start = range.start(), .end = range.end()}); + } + return result; +} + +absl::StatusOr> +ProtoSchemaProto::DecodeFieldProperties( + const types::Object::Field& proto) const { + const bool has_ext = (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_field_schema) + : proto.HasExtension(types::proto3_field_schema); + if (!has_ext) { + return nullptr; + } + const types::ProtoFieldProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_field_schema) + : proto.GetExtension(types::proto3_field_schema); + auto result = std::make_unique(); + if (ext.has_name()) { + result->name = ext.name(); + } + if (ext.has_id()) { + result->id = ext.id(); + } + if (ext.has_json_name()) { + result->json_name = ext.json_name(); + } + if (ext.has_type()) { + result->type = ext.type(); + } + if (ext.has_label()) { + result->label = ext.label(); + } + if (ext.has_oneof()) { + result->oneof = ext.oneof(); + } + if (ext.has_default_value()) { + result->default_value = ext.default_value(); + } + return result; +} + +absl::StatusOr> +ProtoSchemaProto::DecodeEnumProperties(const types::Enum& proto) const { + const bool has_ext = (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_enum_schema) + : proto.HasExtension(types::proto3_enum_schema); + if (!has_ext) { + return nullptr; + } + const types::ProtoEnumProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_enum_schema) + : proto.GetExtension(types::proto3_enum_schema); + auto result = std::make_unique(); + if (ext.has_name()) { + result->name = ext.name(); + } + if (ext.has_allow_alias()) { + result->allow_alias = ext.allow_alias(); + } + if (ext.has_file_name()) { + result->file_name = ext.file_name(); + } + return result; +} + +absl::StatusOr> +ProtoSchemaProto::DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const { + const bool has_ext = + (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_enum_constant_schema) + : proto.HasExtension(types::proto3_enum_constant_schema); + if (!has_ext) { + return nullptr; + } + const types::ProtoEnumConstantProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_enum_constant_schema) + : proto.GetExtension(types::proto3_enum_constant_schema); + auto result = std::make_unique(); + if (ext.has_name()) { + result->name = ext.name(); + } + return result; +} + +absl::Status ProtoSchemaProto::EncodeObjectProperties( + const SchemaObjectProperties& properties, + types::Object* absl_nonnull proto) const { + const auto* props = dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid object properties type for '", name(), "' schema")); + } + types::ProtoObjectProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_object_schema) + : proto->MutableExtension(types::proto3_object_schema); + ext->Clear(); + if (props->name.has_value()) { + ext->set_name(*props->name); + } + if (props->file_name.has_value()) { + ext->set_file_name(*props->file_name); + } + for (const ProtoExtensionRange& range : props->extension_ranges) { + types::ProtoExtensionRange* proto_range = ext->add_extension_ranges(); + proto_range->set_start(range.start); + proto_range->set_end(range.end); + } + return absl::OkStatus(); +} + +absl::Status ProtoSchemaProto::EncodeFieldProperties( + const SchemaFieldProperties& properties, + types::Object::Field* absl_nonnull proto) const { + const auto* props = dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid field properties type for '", name(), "' schema")); + } + types::ProtoFieldProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_field_schema) + : proto->MutableExtension(types::proto3_field_schema); + ext->Clear(); + if (props->name.has_value()) { + ext->set_name(*props->name); + } + if (props->id.has_value()) { + ext->set_id(*props->id); + } + if (props->json_name.has_value()) { + ext->set_json_name(*props->json_name); + } + if (props->type.has_value()) { + ext->set_type(*props->type); + } + if (props->label.has_value()) { + ext->set_label(*props->label); + } + if (props->oneof.has_value()) { + ext->set_oneof(*props->oneof); + } + if (props->default_value.has_value()) { + ext->set_default_value(*props->default_value); + } + return absl::OkStatus(); +} + +absl::Status ProtoSchemaProto::EncodeEnumProperties( + const SchemaEnumProperties& properties, + types::Enum* absl_nonnull proto) const { + const auto* props = dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid enum properties type for '", name(), "' schema")); + } + types::ProtoEnumProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_enum_schema) + : proto->MutableExtension(types::proto3_enum_schema); + ext->Clear(); + if (props->name.has_value()) { + ext->set_name(*props->name); + } + if (props->allow_alias.has_value()) { + ext->set_allow_alias(*props->allow_alias); + } + if (props->file_name.has_value()) { + ext->set_file_name(*props->file_name); + } + return absl::OkStatus(); +} + +absl::Status ProtoSchemaProto::EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + types::Enum::EnumConstant* absl_nonnull proto) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid enum constant properties type for '", name(), "' schema")); + } + types::ProtoEnumConstantProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_enum_constant_schema) + : proto->MutableExtension(types::proto3_enum_constant_schema); + ext->Clear(); + if (props->name.has_value()) { + ext->set_name(*props->name); + } + return absl::OkStatus(); +} + +} // namespace cel diff --git a/common/typedef/proto_schema_proto.h b/common/typedef/proto_schema_proto.h new file mode 100644 index 000000000..a7fece17b --- /dev/null +++ b/common/typedef/proto_schema_proto.h @@ -0,0 +1,97 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_PROTO_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_PROTO_H_ + +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_proto.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +// Base SchemaProto implementation for Protocol Buffers schemas. +class ProtoSchemaProto : public SchemaProto { + public: + absl::StatusOr> + DecodeObjectProperties(const types::Object& proto) const override; + + absl::StatusOr> DecodeFieldProperties( + const types::Object::Field& proto) const override; + + absl::StatusOr> DecodeEnumProperties( + const types::Enum& proto) const override; + + absl::StatusOr> + DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const override; + + absl::Status EncodeObjectProperties( + const SchemaObjectProperties& properties, + types::Object* absl_nonnull proto) const override; + + absl::Status EncodeFieldProperties( + const SchemaFieldProperties& properties, + types::Object::Field* absl_nonnull proto) const override; + + absl::Status EncodeEnumProperties( + const SchemaEnumProperties& properties, + types::Enum* absl_nonnull proto) const override; + + absl::Status EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + types::Enum::EnumConstant* absl_nonnull proto) const override; + + protected: + enum class ProtoVersion { + kProto2, + kProto3, + }; + + explicit ProtoSchemaProto(ProtoVersion version) : version_(version) {} + + private: + ProtoVersion version_; +}; + +// SchemaProto implementation for Protocol Buffers v2 ("proto2"). +class Proto2SchemaProto : public ProtoSchemaProto { + public: + static constexpr absl::string_view kName = kProto2SchemaName; + + Proto2SchemaProto() : ProtoSchemaProto(ProtoVersion::kProto2) {} + + absl::string_view name() const override { return kName; } +}; + +// SchemaProto implementation for Protocol Buffers v3 ("proto3"). +class Proto3SchemaProto : public ProtoSchemaProto { + public: + static constexpr absl::string_view kName = kProto3SchemaName; + + Proto3SchemaProto() : ProtoSchemaProto(ProtoVersion::kProto3) {} + + absl::string_view name() const override { return kName; } +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_PROTO_H_ diff --git a/common/typedef/proto_schema_proto_test.cc b/common/typedef/proto_schema_proto_test.cc new file mode 100644 index 000000000..3a92e60cf --- /dev/null +++ b/common/typedef/proto_schema_proto_test.cc @@ -0,0 +1,208 @@ +// Copyright 2026 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 "common/typedef/proto_schema_proto.h" + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/proto_schema.pb.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_proto.h" +#include "common/typedef/typedef.pb.h" +#include "internal/testing.h" +#include "google/protobuf/unknown_field_set.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::HasSubstr; +using ::testing::IsNull; +using ::testing::NotNull; + +struct DummyObjectProperties : public SchemaObjectProperties {}; +struct DummyFieldProperties : public SchemaFieldProperties {}; +struct DummyEnumProperties : public SchemaEnumProperties {}; +struct DummyEnumConstantProperties : public SchemaEnumConstantProperties {}; + +TEST(ProtoSchemaProtoTest, SchemaNames) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + EXPECT_EQ(p2.name(), kProto2SchemaName); + EXPECT_EQ(p3.name(), kProto3SchemaName); +} + +TEST(ProtoSchemaProtoTest, DecodeReturnsNullptrWhenExtensionAbsent) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + types::Object obj_proto; + types::Object::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant const_proto; + + for (const SchemaProto* schema : {static_cast(&p2), + static_cast(&p3)}) { + EXPECT_THAT(schema->DecodeObjectProperties(obj_proto), + IsOkAndHolds(IsNull())); + EXPECT_THAT(schema->DecodeFieldProperties(field_proto), + IsOkAndHolds(IsNull())); + EXPECT_THAT(schema->DecodeEnumProperties(enum_proto), + IsOkAndHolds(IsNull())); + EXPECT_THAT(schema->DecodeEnumConstantProperties(const_proto), + IsOkAndHolds(IsNull())); + } +} + +TEST(ProtoSchemaProtoTest, EncodeTypeMismatchErrors) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + + for (const SchemaProto* schema : {static_cast(&p2), + static_cast(&p3)}) { + types::Object obj_proto; + types::Object::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant const_proto; + + DummyObjectProperties dummy_obj; + EXPECT_THAT(schema->EncodeObjectProperties(dummy_obj, &obj_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid object properties type for '"))); + + DummyFieldProperties dummy_field; + EXPECT_THAT(schema->EncodeFieldProperties(dummy_field, &field_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid field properties type for '"))); + + DummyEnumProperties dummy_enum; + EXPECT_THAT(schema->EncodeEnumProperties(dummy_enum, &enum_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum properties type for '"))); + + DummyEnumConstantProperties dummy_const; + EXPECT_THAT( + schema->EncodeEnumConstantProperties(dummy_const, &const_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum constant properties type for '"))); + } +} + +TEST(ProtoSchemaProtoTest, DecodeIgnoresUnknownFields) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + + types::Object obj_proto; + auto* obj_ext = obj_proto.MutableExtension(types::proto2_object_schema); + obj_ext->GetReflection()->MutableUnknownFields(obj_ext)->AddVarint(999, 1); + EXPECT_THAT(p2.DecodeObjectProperties(obj_proto), IsOkAndHolds(NotNull())); + + types::Object::Field field_proto; + auto* field_ext = field_proto.MutableExtension(types::proto3_field_schema); + field_ext->GetReflection()->MutableUnknownFields(field_ext)->AddVarint(999, + 1); + EXPECT_THAT(p3.DecodeFieldProperties(field_proto), IsOkAndHolds(NotNull())); + + types::Enum enum_proto; + auto* enum_ext = enum_proto.MutableExtension(types::proto2_enum_schema); + enum_ext->GetReflection()->MutableUnknownFields(enum_ext)->AddVarint(999, 1); + EXPECT_THAT(p2.DecodeEnumProperties(enum_proto), IsOkAndHolds(NotNull())); + + types::Enum::EnumConstant const_proto; + auto* const_ext = + const_proto.MutableExtension(types::proto3_enum_constant_schema); + const_ext->GetReflection()->MutableUnknownFields(const_ext)->AddVarint(999, + 1); + EXPECT_THAT(p3.DecodeEnumConstantProperties(const_proto), + IsOkAndHolds(NotNull())); +} + +TEST(ProtoSchemaProtoTest, EncodeAndDecodeRoundTrip) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + + for (const SchemaProto* schema : {static_cast(&p2), + static_cast(&p3)}) { + ProtoObjectProperties obj_in; + obj_in.name = "com.example.Obj"; + obj_in.file_name = "com/example/obj.proto"; + obj_in.extension_ranges = {{.start = 100, .end = 200}, + {.start = 1000, .end = 536870912}}; + types::Object obj_proto; + ASSERT_THAT(schema->EncodeObjectProperties(obj_in, &obj_proto), IsOk()); + ASSERT_OK_AND_ASSIGN(auto obj_out_base, + schema->DecodeObjectProperties(obj_proto)); + const auto* obj_out = + dynamic_cast(obj_out_base.get()); + ASSERT_THAT(obj_out, NotNull()); + EXPECT_EQ(obj_out->name, obj_in.name); + EXPECT_EQ(obj_out->file_name, obj_in.file_name); + EXPECT_EQ(obj_out->extension_ranges, obj_in.extension_ranges); + + ProtoFieldProperties field_in; + field_in.name = "com.example.ExtField"; + field_in.id = 42; + field_in.json_name = "customJson"; + field_in.type = "sint32"; + field_in.label = "required"; + field_in.oneof = "choice"; + field_in.default_value = "BAR"; + types::Object::Field field_proto; + ASSERT_THAT(schema->EncodeFieldProperties(field_in, &field_proto), IsOk()); + ASSERT_OK_AND_ASSIGN(auto field_out_base, + schema->DecodeFieldProperties(field_proto)); + const auto* field_out = + dynamic_cast(field_out_base.get()); + ASSERT_THAT(field_out, NotNull()); + EXPECT_EQ(field_out->name, field_in.name); + EXPECT_EQ(field_out->id, field_in.id); + EXPECT_EQ(field_out->json_name, field_in.json_name); + EXPECT_EQ(field_out->type, field_in.type); + EXPECT_EQ(field_out->label, field_in.label); + EXPECT_EQ(field_out->oneof, field_in.oneof); + EXPECT_EQ(field_out->default_value, field_in.default_value); + + ProtoEnumProperties enum_in; + enum_in.name = "com.example.Status"; + enum_in.allow_alias = true; + enum_in.file_name = "com/example/status.proto"; + types::Enum enum_proto; + ASSERT_THAT(schema->EncodeEnumProperties(enum_in, &enum_proto), IsOk()); + ASSERT_OK_AND_ASSIGN(auto enum_out_base, + schema->DecodeEnumProperties(enum_proto)); + const auto* enum_out = + dynamic_cast(enum_out_base.get()); + ASSERT_THAT(enum_out, NotNull()); + EXPECT_EQ(enum_out->name, enum_in.name); + EXPECT_EQ(enum_out->allow_alias, enum_in.allow_alias); + EXPECT_EQ(enum_out->file_name, enum_in.file_name); + + ProtoEnumConstantProperties const_in; + const_in.name = "STATUS_OK"; + types::Enum::EnumConstant const_proto; + ASSERT_THAT(schema->EncodeEnumConstantProperties(const_in, &const_proto), + IsOk()); + ASSERT_OK_AND_ASSIGN(auto const_out_base, + schema->DecodeEnumConstantProperties(const_proto)); + const auto* const_out = + dynamic_cast(const_out_base.get()); + ASSERT_THAT(const_out, NotNull()); + EXPECT_EQ(const_out->name, const_in.name); + } +} + +} // namespace +} // namespace cel diff --git a/common/typedef/proto_schema_yaml.cc b/common/typedef/proto_schema_yaml.cc new file mode 100644 index 000000000..5012e2a3b --- /dev/null +++ b/common/typedef/proto_schema_yaml.cc @@ -0,0 +1,361 @@ +// Copyright 2026 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 "common/typedef/proto_schema_yaml.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/yaml_helpers.h" +#include "internal/status_macros.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/emittermanip.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel { + +absl::StatusOr> +ProtoSchemaYaml::DecodeObjectProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError( + yaml, node, + absl::StrCat("Node '", name(), "' object schema is not a map")); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Property key in '", name(), + "' object schema is not a string")); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'name' is not a string"); + } + result->name = internal::GetString(yaml, val); + } else if (key == "file_name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, + "Node 'file_name' is not a string"); + } + result->file_name = internal::GetString(yaml, val); + } else if (key == "extension_ranges") { + if (!val.IsSequence()) { + return internal::YamlError(yaml, val, + "Node 'extension_ranges' is not a sequence"); + } + for (const YAML::Node& range_node : val) { + if (!range_node.IsMap()) { + return internal::YamlError( + yaml, range_node, "Element of 'extension_ranges' is not a map"); + } + ProtoExtensionRange range; + bool has_start = false; + bool has_end = false; + for (const auto& rkv : range_node) { + if (!rkv.first.IsScalar()) { + return internal::YamlError( + yaml, rkv.first, + "Property key in 'extension_ranges' element is not a string"); + } + std::string rkey = internal::GetString(yaml, rkv.first); + if (rkey == "start") { + CEL_ASSIGN_OR_RETURN(range.start, + internal::GetInt32(yaml, "start", rkv.second)); + has_start = true; + } else if (rkey == "end") { + CEL_ASSIGN_OR_RETURN(range.end, + internal::GetInt32(yaml, "end", rkv.second)); + has_end = true; + } else { + return internal::YamlError( + yaml, rkv.first, + absl::StrCat("Unsupported 'extension_ranges' property: '", rkey, + "'")); + } + } + if (!has_start || !has_end) { + return internal::YamlError( + yaml, range_node, + "Element of 'extension_ranges' must specify both 'start' and " + "'end'"); + } + result->extension_ranges.push_back(range); + } + } else { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported '", name(), "' object property: '", key, + "'")); + } + } + return result; +} + +absl::StatusOr> +ProtoSchemaYaml::DecodeFieldProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError( + yaml, node, + absl::StrCat("Node '", name(), "' field schema is not a map")); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Property key in '", name(), + "' field schema is not a string")); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'name' is not a string"); + } + result->name = internal::GetString(yaml, val); + } else if (key == "id") { + CEL_ASSIGN_OR_RETURN(result->id, internal::GetInt32(yaml, "id", val)); + } else if (key == "json_name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, + "Node 'json_name' is not a string"); + } + result->json_name = internal::GetString(yaml, val); + } else if (key == "type") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'type' is not a string"); + } + result->type = internal::GetString(yaml, val); + } else if (key == "label") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'label' is not a string"); + } + result->label = internal::GetString(yaml, val); + } else if (key == "oneof") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'oneof' is not a string"); + } + result->oneof = internal::GetString(yaml, val); + } else if (key == "default_value") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, + "Node 'default_value' is not a string"); + } + result->default_value = internal::GetString(yaml, val); + } else { + return internal::YamlError(yaml, kv.first, + absl::StrCat("Unsupported '", name(), + "' field property: '", key, "'")); + } + } + return result; +} + +absl::StatusOr> +ProtoSchemaYaml::DecodeEnumProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError( + yaml, node, + absl::StrCat("Node '", name(), "' enum schema is not a map")); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError(yaml, kv.first, + absl::StrCat("Property key in '", name(), + "' enum schema is not a string")); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'name' is not a string"); + } + result->name = internal::GetString(yaml, val); + } else if (key == "allow_alias") { + CEL_ASSIGN_OR_RETURN(result->allow_alias, + internal::GetBool(yaml, "allow_alias", val)); + } else if (key == "file_name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, + "Node 'file_name' is not a string"); + } + result->file_name = internal::GetString(yaml, val); + } else { + return internal::YamlError(yaml, kv.first, + absl::StrCat("Unsupported '", name(), + "' enum property: '", key, "'")); + } + } + return result; +} + +absl::StatusOr> +ProtoSchemaYaml::DecodeEnumConstantProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (!node.IsDefined() || !node.IsMap()) { + return internal::YamlError( + yaml, node, + absl::StrCat("Node '", name(), "' enum constant schema is not a map")); + } + auto result = std::make_unique(); + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Property key in '", name(), + "' enum constant schema is not a string")); + } + std::string key = internal::GetString(yaml, kv.first); + const YAML::Node& val = kv.second; + if (key == "name") { + if (!val.IsScalar()) { + return internal::YamlError(yaml, val, "Node 'name' is not a string"); + } + result->name = internal::GetString(yaml, val); + } else { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported '", name(), "' enum constant property: '", + key, "'")); + } + } + return result; +} + +absl::Status ProtoSchemaYaml::EncodeObjectProperties( + const SchemaObjectProperties& properties, YAML::Emitter& out) const { + const auto* props = dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid object properties type for '", name(), "' schema")); + } + out << YAML::BeginMap; + if (props->name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *props->name; + } + if (props->file_name.has_value()) { + out << YAML::Key << "file_name" << YAML::Value << YAML::DoubleQuoted + << *props->file_name; + } + if (!props->extension_ranges.empty()) { + out << YAML::Key << "extension_ranges" << YAML::Value << YAML::BeginSeq; + for (const ProtoExtensionRange& range : props->extension_ranges) { + out << YAML::BeginMap; + out << YAML::Key << "start" << YAML::Value << range.start; + out << YAML::Key << "end" << YAML::Value << range.end; + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status ProtoSchemaYaml::EncodeFieldProperties( + const SchemaFieldProperties& properties, YAML::Emitter& out) const { + const auto* props = dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid field properties type for '", name(), "' schema")); + } + out << YAML::BeginMap; + if (props->name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *props->name; + } + if (props->id.has_value()) { + out << YAML::Key << "id" << YAML::Value << *props->id; + } + if (props->json_name.has_value()) { + out << YAML::Key << "json_name" << YAML::Value << YAML::DoubleQuoted + << *props->json_name; + } + if (props->type.has_value()) { + out << YAML::Key << "type" << YAML::Value << YAML::DoubleQuoted + << *props->type; + } + if (props->label.has_value()) { + out << YAML::Key << "label" << YAML::Value << YAML::DoubleQuoted + << *props->label; + } + if (props->oneof.has_value()) { + out << YAML::Key << "oneof" << YAML::Value << YAML::DoubleQuoted + << *props->oneof; + } + if (props->default_value.has_value()) { + out << YAML::Key << "default_value" << YAML::Value << YAML::DoubleQuoted + << *props->default_value; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status ProtoSchemaYaml::EncodeEnumProperties( + const SchemaEnumProperties& properties, YAML::Emitter& out) const { + const auto* props = dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid enum properties type for '", name(), "' schema")); + } + out << YAML::BeginMap; + if (props->name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *props->name; + } + if (props->allow_alias.has_value()) { + out << YAML::Key << "allow_alias" << YAML::Value << *props->allow_alias; + } + if (props->file_name.has_value()) { + out << YAML::Key << "file_name" << YAML::Value << YAML::DoubleQuoted + << *props->file_name; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status ProtoSchemaYaml::EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, YAML::Emitter& out) const { + const auto* props = + dynamic_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid enum constant properties type for '", name(), "' schema")); + } + out << YAML::BeginMap; + if (props->name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *props->name; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +} // namespace cel diff --git a/common/typedef/proto_schema_yaml.h b/common/typedef/proto_schema_yaml.h new file mode 100644 index 000000000..932b19cfb --- /dev/null +++ b/common/typedef/proto_schema_yaml.h @@ -0,0 +1,82 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_YAML_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_YAML_H_ + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_yaml.h" + +namespace cel { + +// Base SchemaYaml implementation for Protocol Buffers schemas. +class ProtoSchemaYaml : public SchemaYaml { + public: + absl::StatusOr> + DecodeObjectProperties(absl::string_view yaml, + const YAML::Node& node) const override; + + absl::StatusOr> DecodeFieldProperties( + absl::string_view yaml, const YAML::Node& node) const override; + + absl::StatusOr> DecodeEnumProperties( + absl::string_view yaml, const YAML::Node& node) const override; + + absl::StatusOr> + DecodeEnumConstantProperties(absl::string_view yaml, + const YAML::Node& node) const override; + + absl::Status EncodeObjectProperties(const SchemaObjectProperties& properties, + YAML::Emitter& out) const override; + + absl::Status EncodeFieldProperties(const SchemaFieldProperties& properties, + YAML::Emitter& out) const override; + + absl::Status EncodeEnumProperties(const SchemaEnumProperties& properties, + YAML::Emitter& out) const override; + + absl::Status EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + YAML::Emitter& out) const override; +}; + +// SchemaYaml implementation for Protocol Buffers v2 ("proto2"). +class Proto2SchemaYaml : public ProtoSchemaYaml { + public: + static constexpr absl::string_view kName = kProto2SchemaName; + + Proto2SchemaYaml() = default; + + absl::string_view name() const override { return kName; } +}; + +// SchemaYaml implementation for Protocol Buffers v3 ("proto3"). +class Proto3SchemaYaml : public ProtoSchemaYaml { + public: + static constexpr absl::string_view kName = kProto3SchemaName; + + Proto3SchemaYaml() = default; + + absl::string_view name() const override { return kName; } +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_SCHEMA_YAML_H_ diff --git a/common/typedef/proto_schema_yaml_test.cc b/common/typedef/proto_schema_yaml_test.cc new file mode 100644 index 000000000..9e7e4aced --- /dev/null +++ b/common/typedef/proto_schema_yaml_test.cc @@ -0,0 +1,355 @@ +// Copyright 2026 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 "common/typedef/proto_schema_yaml.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef_yaml.h" +#include "common/typedef/yaml_helpers.h" +#include "common/typedef/yaml_test_helpers.h" +#include "internal/status_macros.h" +#include "internal/testing.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/node/parse.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::HasSubstr; +using ::testing::NotNull; +using ::testing::Optional; + +absl::StatusOr RoundTripObject(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, schema.DecodeObjectProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeObjectProperties(*props, out)); + return ss.str(); +} + +absl::StatusOr RoundTripField(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, schema.DecodeFieldProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeFieldProperties(*props, out)); + return ss.str(); +} + +absl::StatusOr RoundTripEnum(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, schema.DecodeEnumProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeEnumProperties(*props, out)); + return ss.str(); +} + +absl::StatusOr RoundTripEnumConstant(const SchemaYaml& schema, + absl::string_view yaml) { + CEL_ASSIGN_OR_RETURN(YAML::Node node, internal::LoadYaml(yaml)); + CEL_ASSIGN_OR_RETURN(auto props, + schema.DecodeEnumConstantProperties(yaml, node)); + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + CEL_RETURN_IF_ERROR(schema.EncodeEnumConstantProperties(*props, out)); + return ss.str(); +} + +TEST(ProtoSchemaYamlTest, DecodeAndEncodeProperties) { + SchemaYamlRegistry registry(std::make_unique(), + std::make_unique()); + + for (absl::string_view schema_name : {kProto2SchemaName, kProto3SchemaName}) { + const SchemaYaml* schema = registry.Find(schema_name); + ASSERT_THAT(schema, NotNull()); + + absl::string_view obj_yaml = + "name: \"com.example.Item\"\n" + "file_name: \"com/example/item.proto\"\n" + "extension_ranges:\n" + " - start: 100\n" + " end: 200"; + EXPECT_THAT(RoundTripObject(*schema, obj_yaml), IsOkAndHolds(obj_yaml)); + EXPECT_THAT(RoundTripObject(*schema, "unknown: val"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + absl::string_view field_yaml = + "name: \"com.example.store.Price\"\n" + "id: 12\n" + "json_name: \"priceVal\"\n" + "type: \"sint32\"\n" + "label: \"required\"\n" + "oneof: \"choice\"\n" + "default_value: \"BAR\""; + EXPECT_THAT(RoundTripField(*schema, field_yaml), IsOkAndHolds(field_yaml)); + EXPECT_THAT(RoundTripField(*schema, "id: not_an_int"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'id' is not an integer"))); + EXPECT_THAT(RoundTripField(*schema, "unknown: val"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + absl::string_view enum_yaml = + "name: \"com.example.Status\"\n" + "allow_alias: true\n" + "file_name: \"com/example/status.proto\""; + EXPECT_THAT(RoundTripEnum(*schema, enum_yaml), IsOkAndHolds(enum_yaml)); + EXPECT_THAT(RoundTripEnum(*schema, "allow_alias: bad_bool"), + StatusIs(absl::StatusCode::kInvalidArgument)); + EXPECT_THAT(RoundTripEnum(*schema, "unknown: val"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + absl::string_view val_yaml = "name: \"SHIPPED\""; + EXPECT_THAT(RoundTripEnumConstant(*schema, val_yaml), + IsOkAndHolds(val_yaml)); + EXPECT_THAT(RoundTripEnumConstant(*schema, "unknown: val"), + StatusIs(absl::StatusCode::kInvalidArgument)); + } +} + +TEST(ProtoSchemaYamlTest, EndToEndTypeDefsYamlRoundTrip) { + SchemaYamlRegistry registry(std::make_unique(), + std::make_unique()); + + std::string expected_yaml = Unindent(R"yaml( + - object: + name: "com.example.Item" + schemas: + proto2: + name: "com.example.proto.Item" + fields: + - name: "item_id" + type: "string" + schemas: + proto2: + name: "com.example.store.Price" + id: 12 + json_name: "itemId" + - enum: + name: "com.example.Status" + schemas: + proto3: + name: "com.example.proto.Status" + allow_alias: false + constants: + - name: "UNSPECIFIED" + id: 0 + - name: "ACTIVE" + id: 1 + schemas: + proto3: + name: "STATUS_ACTIVE" + )yaml"); + + ASSERT_OK_AND_ASSIGN(std::vector type_defs, + TypeDefsFromYaml(expected_yaml, registry)); + ASSERT_EQ(type_defs.size(), 2); + + const auto* obj_props = dynamic_cast( + type_defs[0].object_type().schema_specific_properties.at("proto2").get()); + ASSERT_THAT(obj_props, NotNull()); + EXPECT_THAT(obj_props->name, Optional(std::string("com.example.proto.Item"))); + + const auto* field_props = dynamic_cast( + type_defs[0] + .object_type() + .fields[0] + .schema_specific_properties.at("proto2") + .get()); + ASSERT_THAT(field_props, NotNull()); + EXPECT_THAT(field_props->name, + Optional(std::string("com.example.store.Price"))); + EXPECT_THAT(field_props->id, Optional(12)); + EXPECT_THAT(field_props->json_name, Optional(std::string("itemId"))); + + std::stringstream ss; + ASSERT_THAT(TypeDefsToYaml(type_defs, registry, ss), IsOk()); + EXPECT_EQ(ss.str(), expected_yaml); +} + +struct DummyObjectProperties : public SchemaObjectProperties {}; +struct DummyFieldProperties : public SchemaFieldProperties {}; +struct DummyEnumProperties : public SchemaEnumProperties {}; +struct DummyEnumConstantProperties : public SchemaEnumConstantProperties {}; + +TEST(ProtoSchemaYamlTest, SchemaNames) { + Proto2SchemaYaml p2; + Proto3SchemaYaml p3; + EXPECT_EQ(p2.name(), kProto2SchemaName); + EXPECT_EQ(p3.name(), kProto3SchemaName); +} + +TEST(ProtoSchemaYamlTest, EmptyAndPartialPropertiesRoundTrip) { + Proto2SchemaYaml p2; + Proto3SchemaYaml p3; + for (const SchemaYaml* schema : {static_cast(&p2), + static_cast(&p3)}) { + EXPECT_THAT(RoundTripObject(*schema, "{}"), IsOkAndHolds("{}")); + EXPECT_THAT(RoundTripField(*schema, "{}"), IsOkAndHolds("{}")); + EXPECT_THAT(RoundTripEnum(*schema, "{}"), IsOkAndHolds("{}")); + EXPECT_THAT(RoundTripEnumConstant(*schema, "{}"), IsOkAndHolds("{}")); + + EXPECT_THAT(RoundTripField(*schema, "id: 5"), IsOkAndHolds("id: 5")); + EXPECT_THAT(RoundTripField(*schema, "name: \"msg.Field\""), + IsOkAndHolds("name: \"msg.Field\"")); + EXPECT_THAT(RoundTripField(*schema, "json_name: \"jsonFld\""), + IsOkAndHolds("json_name: \"jsonFld\"")); + + EXPECT_THAT(RoundTripEnum(*schema, "name: \"MyEnum\""), + IsOkAndHolds("name: \"MyEnum\"")); + EXPECT_THAT(RoundTripEnum(*schema, "allow_alias: false"), + IsOkAndHolds("allow_alias: false")); + } +} + +TEST(ProtoSchemaYamlTest, ValidationErrors) { + Proto2SchemaYaml p2; + Proto3SchemaYaml p3; + for (const SchemaYaml* schema : {static_cast(&p2), + static_cast(&p3)}) { + YAML::Node seq = YAML::Load("[]"); + EXPECT_THAT(schema->DecodeObjectProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("object schema is not a map"))); + EXPECT_THAT(schema->DecodeFieldProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("field schema is not a map"))); + EXPECT_THAT(schema->DecodeEnumProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("enum schema is not a map"))); + EXPECT_THAT(schema->DecodeEnumConstantProperties("[]", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("enum constant schema is not a map"))); + + EXPECT_THAT(RoundTripObject(*schema, "? [1]: val"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("object schema is not a string"))); + EXPECT_THAT(RoundTripField(*schema, "? [1]: val"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("field schema is not a string"))); + EXPECT_THAT(RoundTripEnum(*schema, "? [1]: val"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("enum schema is not a string"))); + EXPECT_THAT(RoundTripEnumConstant(*schema, "? [1]: val"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("enum constant schema is not a string"))); + + EXPECT_THAT(RoundTripObject(*schema, "name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'name' is not a string"))); + EXPECT_THAT(RoundTripObject(*schema, "file_name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'file_name' is not a string"))); + EXPECT_THAT( + RoundTripObject(*schema, "extension_ranges: not_a_seq"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'extension_ranges' is not a sequence"))); + EXPECT_THAT( + RoundTripObject(*schema, "extension_ranges: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Element of 'extension_ranges' is not a map"))); + EXPECT_THAT(RoundTripObject(*schema, "extension_ranges: [{start: 1}]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("must specify both 'start' and 'end'"))); + EXPECT_THAT(RoundTripObject( + *schema, "extension_ranges: [{start: 1, end: 2, bad: 3}]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported 'extension_ranges' property"))); + EXPECT_THAT(RoundTripField(*schema, "name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'name' is not a string"))); + EXPECT_THAT(RoundTripField(*schema, "json_name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'json_name' is not a string"))); + EXPECT_THAT(RoundTripField(*schema, "type: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'type' is not a string"))); + EXPECT_THAT(RoundTripField(*schema, "label: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'label' is not a string"))); + EXPECT_THAT(RoundTripField(*schema, "oneof: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'oneof' is not a string"))); + EXPECT_THAT(RoundTripField(*schema, "default_value: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'default_value' is not a string"))); + EXPECT_THAT(RoundTripEnum(*schema, "name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'name' is not a string"))); + EXPECT_THAT(RoundTripEnum(*schema, "file_name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'file_name' is not a string"))); + EXPECT_THAT(RoundTripEnumConstant(*schema, "name: [1]"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'name' is not a string"))); + } +} + +TEST(ProtoSchemaYamlTest, EncodeTypeMismatchErrors) { + Proto2SchemaYaml p2; + Proto3SchemaYaml p3; + for (const SchemaYaml* schema : {static_cast(&p2), + static_cast(&p3)}) { + std::stringstream ss; + YAML::Emitter out(ss); + + DummyObjectProperties dummy_obj; + EXPECT_THAT(schema->EncodeObjectProperties(dummy_obj, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid object properties type for '"))); + + DummyFieldProperties dummy_field; + EXPECT_THAT(schema->EncodeFieldProperties(dummy_field, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid field properties type for '"))); + + DummyEnumProperties dummy_enum; + EXPECT_THAT(schema->EncodeEnumProperties(dummy_enum, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum properties type for '"))); + + DummyEnumConstantProperties dummy_const; + EXPECT_THAT( + schema->EncodeEnumConstantProperties(dummy_const, out), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum constant properties type for '"))); + } +} + +} // namespace +} // namespace cel diff --git a/common/typedef/proto_to_typedef.cc b/common/typedef/proto_to_typedef.cc new file mode 100644 index 000000000..5851ecd75 --- /dev/null +++ b/common/typedef/proto_to_typedef.cc @@ -0,0 +1,1510 @@ +// Copyright 2026 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 "common/typedef/proto_to_typedef.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "google/protobuf/descriptor.pb.h" +#include "absl/container/btree_map.h" +#include "absl/container/btree_set.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_replace.h" +#include "absl/strings/string_view.h" +#include "absl/strings/strip.h" +#include "absl/types/span.h" +#include "common/ast.h" +#include "common/constant.h" +#include "common/minimal_descriptor_database.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/schema.h" +#include "common/typedef/typedef.h" +#include "internal/status_macros.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/descriptor_database.h" +#include "google/protobuf/message.h" + +namespace cel { +namespace { + +class ErrorCollector final : public google::protobuf::DescriptorPool::ErrorCollector { + public: + void RecordError(absl::string_view filename, absl::string_view element_name, + const google::protobuf::Message* /*descriptor*/, + ErrorLocation /*location*/, + absl::string_view message) override { + if (!errors_.empty()) { + absl::StrAppend(&errors_, "; "); + } + absl::StrAppend(&errors_, filename, ": ", element_name, ": ", message); + } + + absl::string_view errors() const { return errors_; } + + private: + std::string errors_; +}; + +absl::StatusOr GetSchemaName( + const google::protobuf::FileDescriptorProto& file_proto) { + if (file_proto.syntax() == "proto3") { + return kProto3SchemaName; + } + if (file_proto.syntax().empty() || file_proto.syntax() == "proto2") { + return kProto2SchemaName; + } + return absl::InvalidArgumentError( + absl::StrCat("Unsupported proto syntax '", file_proto.syntax(), + "' in file '", file_proto.name(), "'.")); +} + +bool IsWellKnownMessageType(const google::protobuf::Descriptor& desc) { + switch (desc.well_known_type()) { + case google::protobuf::Descriptor::WELLKNOWNTYPE_BOOLVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_INT32VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_INT64VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_UINT32VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_UINT64VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_FLOATVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_DOUBLEVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_BYTESVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_STRINGVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_ANY: + case google::protobuf::Descriptor::WELLKNOWNTYPE_DURATION: + case google::protobuf::Descriptor::WELLKNOWNTYPE_TIMESTAMP: + case google::protobuf::Descriptor::WELLKNOWNTYPE_VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_LISTVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_STRUCT: + return true; + default: + return false; + } +} + +bool IsWellKnownEnumType(const google::protobuf::EnumDescriptor& enum_desc) { + return enum_desc.full_name() == "google.protobuf.NullValue"; +} + +TypeSpec MessageDescriptorToTypeSpec(const google::protobuf::Descriptor& desc) { + switch (desc.well_known_type()) { + case google::protobuf::Descriptor::WELLKNOWNTYPE_BOOLVALUE: + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kBool)); + case google::protobuf::Descriptor::WELLKNOWNTYPE_INT32VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_INT64VALUE: + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kInt64)); + case google::protobuf::Descriptor::WELLKNOWNTYPE_UINT32VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_UINT64VALUE: + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kUint64)); + case google::protobuf::Descriptor::WELLKNOWNTYPE_FLOATVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_DOUBLEVALUE: + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kDouble)); + case google::protobuf::Descriptor::WELLKNOWNTYPE_BYTESVALUE: + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kBytes)); + case google::protobuf::Descriptor::WELLKNOWNTYPE_STRINGVALUE: + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kString)); + case google::protobuf::Descriptor::WELLKNOWNTYPE_ANY: + return TypeSpec(WellKnownTypeSpec::kAny); + case google::protobuf::Descriptor::WELLKNOWNTYPE_DURATION: + return TypeSpec(WellKnownTypeSpec::kDuration); + case google::protobuf::Descriptor::WELLKNOWNTYPE_TIMESTAMP: + return TypeSpec(WellKnownTypeSpec::kTimestamp); + case google::protobuf::Descriptor::WELLKNOWNTYPE_VALUE: + return TypeSpec(DynTypeSpec()); + case google::protobuf::Descriptor::WELLKNOWNTYPE_LISTVALUE: + return TypeSpec(ListTypeSpec(std::make_unique(DynTypeSpec()))); + case google::protobuf::Descriptor::WELLKNOWNTYPE_STRUCT: + return TypeSpec( + MapTypeSpec(std::make_unique(PrimitiveType::kString), + std::make_unique(DynTypeSpec()))); + default: + return TypeSpec(MessageTypeSpec(std::string(desc.full_name()))); + } +} + +TypeSpec EnumDescriptorToTypeSpec(const google::protobuf::EnumDescriptor& enum_desc) { + if (IsWellKnownEnumType(enum_desc)) { + return TypeSpec(NullTypeSpec()); + } + return TypeSpec(MessageTypeSpec(std::string(enum_desc.full_name()))); +} + +absl::StatusOr SingularFieldDescriptorToTypeSpec( + const google::protobuf::FieldDescriptor& field) { + switch (field.type()) { + case google::protobuf::FieldDescriptor::TYPE_BOOL: + return TypeSpec(PrimitiveType::kBool); + case google::protobuf::FieldDescriptor::TYPE_INT32: + case google::protobuf::FieldDescriptor::TYPE_SINT32: + case google::protobuf::FieldDescriptor::TYPE_SFIXED32: + case google::protobuf::FieldDescriptor::TYPE_INT64: + case google::protobuf::FieldDescriptor::TYPE_SINT64: + case google::protobuf::FieldDescriptor::TYPE_SFIXED64: + return TypeSpec(PrimitiveType::kInt64); + case google::protobuf::FieldDescriptor::TYPE_UINT32: + case google::protobuf::FieldDescriptor::TYPE_FIXED32: + case google::protobuf::FieldDescriptor::TYPE_UINT64: + case google::protobuf::FieldDescriptor::TYPE_FIXED64: + return TypeSpec(PrimitiveType::kUint64); + case google::protobuf::FieldDescriptor::TYPE_FLOAT: + case google::protobuf::FieldDescriptor::TYPE_DOUBLE: + return TypeSpec(PrimitiveType::kDouble); + case google::protobuf::FieldDescriptor::TYPE_STRING: + return TypeSpec(PrimitiveType::kString); + case google::protobuf::FieldDescriptor::TYPE_BYTES: + return TypeSpec(PrimitiveType::kBytes); + case google::protobuf::FieldDescriptor::TYPE_ENUM: + return EnumDescriptorToTypeSpec(*field.enum_type()); + case google::protobuf::FieldDescriptor::TYPE_MESSAGE: + case google::protobuf::FieldDescriptor::TYPE_GROUP: + return MessageDescriptorToTypeSpec(*field.message_type()); + } + return absl::InvalidArgumentError( + absl::StrCat("Unsupported proto field type: ", field.type_name(), + " on field '", field.full_name(), "'.")); +} + +absl::StatusOr FieldDescriptorToTypeSpec( + const google::protobuf::FieldDescriptor& field) { + if (field.is_map()) { + const google::protobuf::Descriptor* entry = field.message_type(); + const google::protobuf::FieldDescriptor* key_field = entry->map_key(); + const google::protobuf::FieldDescriptor* value_field = entry->map_value(); + if (key_field == nullptr || value_field == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid map entry for field '", field.full_name(), "'.")); + } + CEL_ASSIGN_OR_RETURN(TypeSpec key_type, + SingularFieldDescriptorToTypeSpec(*key_field)); + CEL_ASSIGN_OR_RETURN(TypeSpec value_type, + SingularFieldDescriptorToTypeSpec(*value_field)); + return TypeSpec( + MapTypeSpec(std::make_unique(std::move(key_type)), + std::make_unique(std::move(value_type)))); + } + CEL_ASSIGN_OR_RETURN(TypeSpec elem_type, + SingularFieldDescriptorToTypeSpec(field)); + if (field.is_repeated()) { + return TypeSpec( + ListTypeSpec(std::make_unique(std::move(elem_type)))); + } + return elem_type; +} + +std::string GetSingularProtoTypeName(const google::protobuf::FieldDescriptor& field) { + switch (field.type()) { + case google::protobuf::FieldDescriptor::TYPE_BOOL: + return "bool"; + case google::protobuf::FieldDescriptor::TYPE_INT32: + return "int32"; + case google::protobuf::FieldDescriptor::TYPE_INT64: + return "int64"; + case google::protobuf::FieldDescriptor::TYPE_UINT32: + return "uint32"; + case google::protobuf::FieldDescriptor::TYPE_UINT64: + return "uint64"; + case google::protobuf::FieldDescriptor::TYPE_SINT32: + return "sint32"; + case google::protobuf::FieldDescriptor::TYPE_SINT64: + return "sint64"; + case google::protobuf::FieldDescriptor::TYPE_FIXED32: + return "fixed32"; + case google::protobuf::FieldDescriptor::TYPE_FIXED64: + return "fixed64"; + case google::protobuf::FieldDescriptor::TYPE_SFIXED32: + return "sfixed32"; + case google::protobuf::FieldDescriptor::TYPE_SFIXED64: + return "sfixed64"; + case google::protobuf::FieldDescriptor::TYPE_FLOAT: + return "float"; + case google::protobuf::FieldDescriptor::TYPE_DOUBLE: + return "double"; + case google::protobuf::FieldDescriptor::TYPE_STRING: + return "string"; + case google::protobuf::FieldDescriptor::TYPE_BYTES: + return "bytes"; + case google::protobuf::FieldDescriptor::TYPE_ENUM: + return std::string(field.enum_type()->full_name()); + case google::protobuf::FieldDescriptor::TYPE_GROUP: + return "group"; + case google::protobuf::FieldDescriptor::TYPE_MESSAGE: + return std::string(field.message_type()->full_name()); + } + return ""; +} + +absl::StatusOr DefaultSingularProtoTypeName( + const TypeSpec& type_spec) { + if (type_spec.has_primitive()) { + switch (type_spec.primitive()) { + case PrimitiveType::kBool: + return "bool"; + case PrimitiveType::kInt64: + return "int64"; + case PrimitiveType::kUint64: + return "uint64"; + case PrimitiveType::kDouble: + return "double"; + case PrimitiveType::kString: + return "string"; + case PrimitiveType::kBytes: + return "bytes"; + case PrimitiveType::kPrimitiveTypeUnspecified: + break; + } + return absl::InvalidArgumentError("Unspecified primitive type."); + } + if (type_spec.has_wrapper()) { + switch (type_spec.wrapper()) { + case PrimitiveType::kBool: + return "google.protobuf.BoolValue"; + case PrimitiveType::kInt64: + return "google.protobuf.Int64Value"; + case PrimitiveType::kUint64: + return "google.protobuf.UInt64Value"; + case PrimitiveType::kDouble: + return "google.protobuf.DoubleValue"; + case PrimitiveType::kString: + return "google.protobuf.StringValue"; + case PrimitiveType::kBytes: + return "google.protobuf.BytesValue"; + case PrimitiveType::kPrimitiveTypeUnspecified: + break; + } + return absl::InvalidArgumentError("Unspecified primitive wrapper type."); + } + if (type_spec.has_well_known()) { + switch (type_spec.well_known()) { + case WellKnownTypeSpec::kAny: + return "google.protobuf.Any"; + case WellKnownTypeSpec::kDuration: + return "google.protobuf.Duration"; + case WellKnownTypeSpec::kTimestamp: + return "google.protobuf.Timestamp"; + case WellKnownTypeSpec::kWellKnownTypeUnspecified: + break; + } + return absl::InvalidArgumentError("Unspecified well-known type."); + } + if (type_spec.has_dyn()) { + return "google.protobuf.Value"; + } + if (type_spec.has_null()) { + return "google.protobuf.NullValue"; + } + if (type_spec.has_message_type()) { + return type_spec.message_type().type(); + } + if (type_spec.has_abstract_type() && + type_spec.abstract_type().parameter_types().empty()) { + return type_spec.abstract_type().name(); + } + if (type_spec.has_list_type()) { + if (type_spec.list_type().elem_type().has_dyn()) { + return "google.protobuf.ListValue"; + } + return absl::InvalidArgumentError( + "Nested list type other than list is not representable as a " + "singular protobuf field."); + } + if (type_spec.has_map_type()) { + if (type_spec.map_type().key_type().has_primitive() && + type_spec.map_type().key_type().primitive() == PrimitiveType::kString && + type_spec.map_type().value_type().has_dyn()) { + return "google.protobuf.Struct"; + } + return absl::InvalidArgumentError( + "Nested map type other than map is not representable as a " + "singular protobuf field."); + } + return absl::InvalidArgumentError( + "Unsupported TypeSpec for protobuf field conversion."); +} + +absl::StatusOr> ExtractNonDefaultProtoFieldType( + const google::protobuf::FieldDescriptor& field, const TypeSpec& cel_type) { + if (field.is_map()) { + const google::protobuf::Descriptor* entry = field.message_type(); + const google::protobuf::FieldDescriptor* key_field = entry->map_key(); + const google::protobuf::FieldDescriptor* value_field = entry->map_value(); + std::string actual_key = GetSingularProtoTypeName(*key_field); + std::string actual_val = GetSingularProtoTypeName(*value_field); + CEL_ASSIGN_OR_RETURN( + std::string default_key, + DefaultSingularProtoTypeName(cel_type.map_type().key_type())); + CEL_ASSIGN_OR_RETURN( + std::string default_val, + DefaultSingularProtoTypeName(cel_type.map_type().value_type())); + if (actual_key != default_key || actual_val != default_val) { + return absl::StrCat("map<", actual_key, ", ", actual_val, ">"); + } + return std::nullopt; + } + if (field.is_repeated()) { + std::string actual_elem = GetSingularProtoTypeName(field); + CEL_ASSIGN_OR_RETURN( + std::string default_elem, + DefaultSingularProtoTypeName(cel_type.list_type().elem_type())); + if (actual_elem != default_elem) { + return actual_elem; + } + return std::nullopt; + } + // Singular field: top-level list defaults to repeated Value, and + // top-level map defaults to map, so singular + // ListValue and Struct fields must record their explicit message type. + std::string actual = GetSingularProtoTypeName(field); + if (cel_type.has_list_type() || cel_type.has_map_type()) { + return actual; + } + CEL_ASSIGN_OR_RETURN(std::string default_singular, + DefaultSingularProtoTypeName(cel_type)); + if (actual != default_singular) { + return actual; + } + return std::nullopt; +} + +template +std::string ExtractLeadingComment(const DescriptorT& desc, + const ProtoToTypeDefOptions& options) { + if (!options.include_comments) { + return ""; + } + proto2::SourceLocation location; + if (!desc.GetSourceLocation(&location)) { + return ""; + } + return std::string(absl::StripAsciiWhitespace(location.leading_comments)); +} + +Constant ExtractFieldDefaultValue(const google::protobuf::FieldDescriptor& field, + const ProtoToTypeDefOptions& options) { + if (!options.include_defaults || !field.has_default_value() || + field.is_repeated()) { + return Constant(); + } + switch (field.cpp_type()) { + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: + return Constant(field.default_value_bool()); + case google::protobuf::FieldDescriptor::CPPTYPE_INT32: + return Constant(static_cast(field.default_value_int32())); + case google::protobuf::FieldDescriptor::CPPTYPE_INT64: + return Constant(field.default_value_int64()); + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: + return Constant(static_cast(field.default_value_uint32())); + case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: + return Constant(field.default_value_uint64()); + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: + return Constant(static_cast(field.default_value_float())); + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: + return Constant(field.default_value_double()); + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: + if (field.type() == google::protobuf::FieldDescriptor::TYPE_BYTES) { + return Constant(BytesConstant(field.default_value_string())); + } + return Constant(StringConstant(field.default_value_string())); + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: + // CEL Constant only represents scalar literals; enum defaults are stored + // in ProtoFieldProperties::default_value. + return Constant(); + } + return Constant(); +} + +std::string DefaultJsonName(absl::string_view name) { + std::string result; + result.reserve(name.size()); + bool next_upper = false; + for (char c : name) { + if (c == '_') { + next_upper = true; + } else if (next_upper) { + result.push_back(absl::ascii_toupper(static_cast(c))); + next_upper = false; + } else { + result.push_back(c); + } + } + return result; +} + +absl::Status ConvertEnumDescriptor(const google::protobuf::EnumDescriptor& enum_desc, + absl::string_view schema_name, + const ProtoToTypeDefOptions& options, + TypeDefSet& type_def_set) { + if (IsWellKnownEnumType(enum_desc)) { + return absl::OkStatus(); + } + EnumTypeDef enum_def; + enum_def.name = std::string(enum_desc.full_name()); + enum_def.doc = ExtractLeadingComment(enum_desc, options); + + if (enum_desc.containing_type() == nullptr || + enum_desc.options().has_allow_alias()) { + auto props = std::make_unique(); + if (enum_desc.containing_type() == nullptr) { + props->file_name = std::string(enum_desc.file()->name()); + } + if (enum_desc.options().has_allow_alias()) { + props->allow_alias = enum_desc.options().allow_alias(); + } + enum_def.schema_specific_properties[std::string(schema_name)] = + std::move(props); + } + + for (int i = 0; i < enum_desc.value_count(); ++i) { + const google::protobuf::EnumValueDescriptor* val = enum_desc.value(i); + EnumTypeDef::EnumConstant constant; + constant.name = std::string(val->name()); + constant.id = val->number(); + constant.doc = ExtractLeadingComment(*val, options); + CEL_RETURN_IF_ERROR(enum_def.AddConstant(std::move(constant))); + } + + return type_def_set.AddTypeDef(TypeDef(std::move(enum_def))); +} + +absl::Status ConvertMessageDescriptor(const google::protobuf::Descriptor& desc, + absl::string_view schema_name, + const ProtoToTypeDefOptions& options, + TypeDefSet& type_def_set) { + if (desc.options().map_entry() || IsWellKnownMessageType(desc)) { + return absl::OkStatus(); + } + + ObjectTypeDef object_def; + object_def.name = std::string(desc.full_name()); + object_def.doc = ExtractLeadingComment(desc, options); + + if (desc.containing_type() == nullptr || desc.extension_range_count() > 0) { + auto obj_props = std::make_unique(); + if (desc.containing_type() == nullptr) { + obj_props->file_name = std::string(desc.file()->name()); + } + for (int i = 0; i < desc.extension_range_count(); ++i) { + const google::protobuf::Descriptor::ExtensionRange* range = desc.extension_range(i); + obj_props->extension_ranges.push_back(ProtoExtensionRange{ + .start = range->start_number(), + .end = range->end_number(), + }); + } + object_def.schema_specific_properties[std::string(schema_name)] = + std::move(obj_props); + } + + for (int i = 0; i < desc.field_count(); ++i) { + const google::protobuf::FieldDescriptor* field = desc.field(i); + ObjectTypeDef::Field field_def; + field_def.name = std::string(field->name()); + CEL_ASSIGN_OR_RETURN(field_def.type, FieldDescriptorToTypeSpec(*field)); + field_def.doc = ExtractLeadingComment(*field, options); + field_def.default_value = ExtractFieldDefaultValue(*field, options); + + auto field_props = std::make_unique(); + field_props->id = field->number(); + if (field->has_json_name() && + field->json_name() != DefaultJsonName(field->name())) { + field_props->json_name = std::string(field->json_name()); + } + CEL_ASSIGN_OR_RETURN(field_props->type, ExtractNonDefaultProtoFieldType( + *field, field_def.type)); + if (schema_name == kProto2SchemaName && field->is_required()) { + field_props->label = "required"; + } else if (schema_name == kProto3SchemaName && + field->containing_oneof() != nullptr && + field->real_containing_oneof() == nullptr) { + field_props->label = "optional"; + } + if (field->real_containing_oneof() != nullptr) { + field_props->oneof = std::string(field->real_containing_oneof()->name()); + } + if (options.include_defaults && field->has_default_value() && + !field->is_repeated() && + field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_ENUM) { + field_props->default_value = + std::string(field->default_value_enum()->name()); + } + field_def.schema_specific_properties[std::string(schema_name)] = + std::move(field_props); + + CEL_RETURN_IF_ERROR(object_def.AddField(std::move(field_def))); + } + + CEL_RETURN_IF_ERROR(type_def_set.AddTypeDef(TypeDef(std::move(object_def)))); + + for (int i = 0; i < desc.enum_type_count(); ++i) { + CEL_RETURN_IF_ERROR(ConvertEnumDescriptor(*desc.enum_type(i), schema_name, + options, type_def_set)); + } + + for (int i = 0; i < desc.nested_type_count(); ++i) { + CEL_RETURN_IF_ERROR(ConvertMessageDescriptor( + *desc.nested_type(i), schema_name, options, type_def_set)); + } + + return absl::OkStatus(); +} + +// ============================================================================= +// TypeDefSetToFileDescriptorSet helpers +// ============================================================================= + +const ProtoObjectProperties* GetProtoObjectProperties( + const ObjectTypeDef& obj, absl::string_view schema_name) { + auto it = obj.schema_specific_properties.find(schema_name); + if (it == obj.schema_specific_properties.end()) { + return nullptr; + } + return dynamic_cast(it->second.get()); +} + +const ProtoFieldProperties* GetProtoFieldProperties( + const ObjectTypeDef::Field& field, absl::string_view schema_name) { + auto it = field.schema_specific_properties.find(schema_name); + if (it == field.schema_specific_properties.end()) { + return nullptr; + } + return dynamic_cast(it->second.get()); +} + +const ProtoEnumProperties* GetProtoEnumProperties( + const EnumTypeDef& e, absl::string_view schema_name) { + auto it = e.schema_specific_properties.find(schema_name); + if (it == e.schema_specific_properties.end()) { + return nullptr; + } + return dynamic_cast(it->second.get()); +} + +const ProtoEnumConstantProperties* GetProtoEnumConstantProperties( + const EnumTypeDef::EnumConstant& c, absl::string_view schema_name) { + auto it = c.schema_specific_properties.find(schema_name); + if (it == c.schema_specific_properties.end()) { + return nullptr; + } + return dynamic_cast(it->second.get()); +} + +std::string MapEntryName(absl::string_view field_name) { + std::string result; + result.reserve(field_name.size() + 5); + bool cap_next = true; + for (char c : field_name) { + if (c == '_') { + cap_next = true; + } else if (cap_next) { + result.push_back(absl::ascii_toupper(static_cast(c))); + cap_next = false; + } else { + result.push_back(c); + } + } + absl::StrAppend(&result, "Entry"); + return result; +} + +template +absl::Status CheckSchemaMapForProtoSyntax(const MapT& props_map, + bool* has_proto2, bool* has_proto3) { + if (props_map.contains(kProto2SchemaName)) { + *has_proto2 = true; + } + if (props_map.contains(kProto3SchemaName)) { + *has_proto3 = true; + } + if (*has_proto2 && *has_proto3) { + return absl::InvalidArgumentError( + "TypeDef mixes 'proto2' and 'proto3' schema properties."); + } + return absl::OkStatus(); +} + +absl::Status CollectTreeProtoSyntax( + const TypeDef& td, + const absl::flat_hash_map>& + children_map, + bool* has_proto2, bool* has_proto3) { + if (td.is_object()) { + CEL_RETURN_IF_ERROR(CheckSchemaMapForProtoSyntax( + td.object_type().schema_specific_properties, has_proto2, has_proto3)); + for (const ObjectTypeDef::Field& field : td.object_type().fields) { + CEL_RETURN_IF_ERROR(CheckSchemaMapForProtoSyntax( + field.schema_specific_properties, has_proto2, has_proto3)); + } + } else if (td.is_enum()) { + CEL_RETURN_IF_ERROR(CheckSchemaMapForProtoSyntax( + td.enum_type().schema_specific_properties, has_proto2, has_proto3)); + for (const EnumTypeDef::EnumConstant& c : td.enum_type().constants) { + CEL_RETURN_IF_ERROR(CheckSchemaMapForProtoSyntax( + c.schema_specific_properties, has_proto2, has_proto3)); + } + } + auto it = children_map.find(td.name()); + if (it != children_map.end()) { + for (const TypeDef* child : it->second) { + CEL_RETURN_IF_ERROR( + CollectTreeProtoSyntax(*child, children_map, has_proto2, has_proto3)); + } + } + return absl::OkStatus(); +} + +absl::StatusOr DetermineSchemaNameForTree( + const TypeDef& root_td, + const absl::flat_hash_map>& + children_map) { + bool has_proto2 = false; + bool has_proto3 = false; + CEL_RETURN_IF_ERROR( + CollectTreeProtoSyntax(root_td, children_map, &has_proto2, &has_proto3)); + return has_proto2 ? std::string(kProto2SchemaName) + : std::string(kProto3SchemaName); +} + +std::optional GetWellKnownTypeFileName( + absl::string_view full_type_name) { + if (full_type_name == "google.protobuf.Any") { + return "google/protobuf/any.proto"; + } + if (full_type_name == "google.protobuf.Duration") { + return "google/protobuf/duration.proto"; + } + if (full_type_name == "google.protobuf.Timestamp") { + return "google/protobuf/timestamp.proto"; + } + if (full_type_name == "google.protobuf.Struct" || + full_type_name == "google.protobuf.Value" || + full_type_name == "google.protobuf.ListValue" || + full_type_name == "google.protobuf.NullValue") { + return "google/protobuf/struct.proto"; + } + if (full_type_name == "google.protobuf.BoolValue" || + full_type_name == "google.protobuf.Int32Value" || + full_type_name == "google.protobuf.Int64Value" || + full_type_name == "google.protobuf.UInt32Value" || + full_type_name == "google.protobuf.UInt64Value" || + full_type_name == "google.protobuf.FloatValue" || + full_type_name == "google.protobuf.DoubleValue" || + full_type_name == "google.protobuf.StringValue" || + full_type_name == "google.protobuf.BytesValue") { + return "google/protobuf/wrappers.proto"; + } + return std::nullopt; +} + +void AddCommentLocation(absl::Span path, absl::string_view doc, + const TypeDefToProtoOptions& options, + proto2::SourceCodeInfo* source_code_info) { + if (!options.include_comments || doc.empty()) { + return; + } + proto2::SourceCodeInfo::Location* loc = source_code_info->add_location(); + for (int p : path) { + loc->add_path(p); + } + loc->add_span(0); + loc->add_span(0); + loc->add_span(0); + loc->set_leading_comments(absl::StrCat(" ", doc, "\n")); +} + +absl::Status ParseMapProtoType(absl::string_view map_type_str, + std::string* key_type, std::string* val_type) { + absl::string_view inner = map_type_str; + if (!absl::ConsumePrefix(&inner, "map<") || + !absl::ConsumeSuffix(&inner, ">")) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid protobuf map type override: '", map_type_str, "'.")); + } + size_t comma_pos = inner.find(','); + if (comma_pos == absl::string_view::npos || + inner.find(',', comma_pos + 1) != absl::string_view::npos) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid protobuf map type override: '", map_type_str, "'.")); + } + *key_type = + std::string(absl::StripAsciiWhitespace(inner.substr(0, comma_pos))); + *val_type = + std::string(absl::StripAsciiWhitespace(inner.substr(comma_pos + 1))); + if (key_type->empty() || val_type->empty()) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid protobuf map type override: '", map_type_str, "'.")); + } + return absl::OkStatus(); +} + +absl::Status PopulateSingularFieldProtoType( + const TypeSpec& type_spec, absl::string_view proto_type_str, + const TypeDefSet& type_def_set, google::protobuf::FieldDescriptorProto* field_proto, + absl::flat_hash_set* referenced_types) { + if (proto_type_str == "bool") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_BOOL); + return absl::OkStatus(); + } + if (proto_type_str == "int32") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_INT32); + return absl::OkStatus(); + } + if (proto_type_str == "int64") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_INT64); + return absl::OkStatus(); + } + if (proto_type_str == "uint32") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_UINT32); + return absl::OkStatus(); + } + if (proto_type_str == "uint64") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_UINT64); + return absl::OkStatus(); + } + if (proto_type_str == "sint32") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_SINT32); + return absl::OkStatus(); + } + if (proto_type_str == "sint64") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_SINT64); + return absl::OkStatus(); + } + if (proto_type_str == "fixed32") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_FIXED32); + return absl::OkStatus(); + } + if (proto_type_str == "fixed64") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_FIXED64); + return absl::OkStatus(); + } + if (proto_type_str == "sfixed32") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_SFIXED32); + return absl::OkStatus(); + } + if (proto_type_str == "sfixed64") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_SFIXED64); + return absl::OkStatus(); + } + if (proto_type_str == "float") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_FLOAT); + return absl::OkStatus(); + } + if (proto_type_str == "double") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_DOUBLE); + return absl::OkStatus(); + } + if (proto_type_str == "string") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_STRING); + return absl::OkStatus(); + } + if (proto_type_str == "bytes") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_BYTES); + return absl::OkStatus(); + } + if (proto_type_str == "group") { + std::string group_msg_name; + if (type_spec.has_message_type()) { + group_msg_name = type_spec.message_type().type(); + } else if (type_spec.has_abstract_type() && + type_spec.abstract_type().parameter_types().empty()) { + group_msg_name = type_spec.abstract_type().name(); + } else { + return absl::InvalidArgumentError( + "Proto 'group' field must have a message TypeSpec."); + } + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_GROUP); + field_proto->set_type_name(absl::StrCat(".", group_msg_name)); + referenced_types->insert(group_msg_name); + return absl::OkStatus(); + } + if (proto_type_str == "google.protobuf.NullValue") { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_ENUM); + field_proto->set_type_name(".google.protobuf.NullValue"); + referenced_types->insert("google.protobuf.NullValue"); + return absl::OkStatus(); + } + + const TypeDef* target_td = type_def_set.FindTypeDef(proto_type_str); + if (target_td != nullptr) { + if (target_td->is_enum()) { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_ENUM); + } else { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_MESSAGE); + } + field_proto->set_type_name(absl::StrCat(".", proto_type_str)); + referenced_types->insert(std::string(proto_type_str)); + return absl::OkStatus(); + } + + if (GetWellKnownTypeFileName(proto_type_str).has_value()) { + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_MESSAGE); + field_proto->set_type_name(absl::StrCat(".", proto_type_str)); + referenced_types->insert(std::string(proto_type_str)); + return absl::OkStatus(); + } + + return absl::InvalidArgumentError( + absl::StrCat("Unknown referenced protobuf type '", proto_type_str, "'.")); +} + +absl::StatusOr FormatConstantDefaultValue( + const Constant& constant, google::protobuf::FieldDescriptorProto::Type proto_type) { + switch (proto_type) { + case google::protobuf::FieldDescriptorProto::TYPE_BOOL: + if (!constant.has_bool_value()) { + return absl::InvalidArgumentError( + "Expected bool default value for bool field."); + } + return constant.bool_value() ? "true" : "false"; + case google::protobuf::FieldDescriptorProto::TYPE_INT32: + case google::protobuf::FieldDescriptorProto::TYPE_SINT32: + case google::protobuf::FieldDescriptorProto::TYPE_SFIXED32: + case google::protobuf::FieldDescriptorProto::TYPE_INT64: + case google::protobuf::FieldDescriptorProto::TYPE_SINT64: + case google::protobuf::FieldDescriptorProto::TYPE_SFIXED64: + if (!constant.has_int64_value()) { + return absl::InvalidArgumentError( + "Expected int default value for integer field."); + } + return absl::StrCat(constant.int64_value()); + case google::protobuf::FieldDescriptorProto::TYPE_UINT32: + case google::protobuf::FieldDescriptorProto::TYPE_FIXED32: + case google::protobuf::FieldDescriptorProto::TYPE_UINT64: + case google::protobuf::FieldDescriptorProto::TYPE_FIXED64: + if (!constant.has_uint64_value()) { + return absl::InvalidArgumentError( + "Expected uint default value for unsigned integer field."); + } + return absl::StrCat(constant.uint64_value()); + case google::protobuf::FieldDescriptorProto::TYPE_FLOAT: + if (!constant.has_double_value()) { + return absl::InvalidArgumentError( + "Expected double default value for float field."); + } + return absl::StrCat(static_cast(constant.double_value())); + case google::protobuf::FieldDescriptorProto::TYPE_DOUBLE: + if (!constant.has_double_value()) { + return absl::InvalidArgumentError( + "Expected double default value for double field."); + } + return absl::StrCat(constant.double_value()); + case google::protobuf::FieldDescriptorProto::TYPE_STRING: + if (!constant.has_string_value()) { + return absl::InvalidArgumentError( + "Expected string default value for string field."); + } + return constant.string_value(); + case google::protobuf::FieldDescriptorProto::TYPE_BYTES: + if (!constant.has_bytes_value()) { + return absl::InvalidArgumentError( + "Expected bytes default value for bytes field."); + } + return absl::CEscape(constant.bytes_value()); + default: + return absl::InvalidArgumentError( + "Scalar Constant default value is only supported on primitive " + "fields."); + } +} + +std::string ShortTypeName(absl::string_view full_name) { + size_t pos = full_name.rfind('.'); + if (pos == absl::string_view::npos) { + return std::string(full_name); + } + return std::string(full_name.substr(pos + 1)); +} + +void BuildEnumProto(const EnumTypeDef& enum_def, absl::string_view schema_name, + const TypeDefToProtoOptions& options, + std::vector base_path, + google::protobuf::EnumDescriptorProto* enum_proto, + proto2::SourceCodeInfo* source_code_info) { + enum_proto->set_name(ShortTypeName(enum_def.name)); + AddCommentLocation(base_path, enum_def.doc, options, source_code_info); + + const ProtoEnumProperties* enum_props = + GetProtoEnumProperties(enum_def, schema_name); + if (enum_props != nullptr && enum_props->allow_alias.has_value()) { + enum_proto->mutable_options()->set_allow_alias(*enum_props->allow_alias); + } + + for (size_t i = 0; i < enum_def.constants.size(); ++i) { + const EnumTypeDef::EnumConstant& constant = enum_def.constants[i]; + google::protobuf::EnumValueDescriptorProto* val_proto = enum_proto->add_value(); + const ProtoEnumConstantProperties* const_props = + GetProtoEnumConstantProperties(constant, schema_name); + if (const_props != nullptr && const_props->name.has_value()) { + val_proto->set_name(*const_props->name); + } else { + val_proto->set_name(constant.name); + } + val_proto->set_number(constant.id); + + std::vector val_path = base_path; + val_path.push_back(google::protobuf::EnumDescriptorProto::kValueFieldNumber); + val_path.push_back(static_cast(i)); + AddCommentLocation(val_path, constant.doc, options, source_code_info); + } +} + +absl::Status BuildMessageProto( + const ObjectTypeDef& obj_def, absl::string_view schema_name, + const TypeDefSet& type_def_set, + const absl::flat_hash_map>& + children_map, + const TypeDefToProtoOptions& options, std::vector base_path, + google::protobuf::DescriptorProto* msg_proto, + proto2::SourceCodeInfo* source_code_info, + absl::flat_hash_set* referenced_types) { + msg_proto->set_name(ShortTypeName(obj_def.name)); + AddCommentLocation(base_path, obj_def.doc, options, source_code_info); + + const ProtoObjectProperties* obj_props = + GetProtoObjectProperties(obj_def, schema_name); + if (obj_props != nullptr) { + for (const ProtoExtensionRange& range : obj_props->extension_ranges) { + google::protobuf::DescriptorProto::ExtensionRange* ext_range = + msg_proto->add_extension_range(); + ext_range->set_start(range.start); + ext_range->set_end(range.end); + } + } + + // First pass: register real oneof declarations so all real oneofs precede any + // synthetic proto3_optional oneofs (as required by descriptor.proto). + absl::flat_hash_map real_oneof_indices; + absl::flat_hash_set used_oneof_names; + absl::flat_hash_set used_field_numbers; + for (const ObjectTypeDef::Field& field : obj_def.fields) { + const ProtoFieldProperties* field_props = + GetProtoFieldProperties(field, schema_name); + if (field_props != nullptr) { + if (field_props->id.has_value()) { + used_field_numbers.insert(*field_props->id); + } + if (field_props->oneof.has_value()) { + auto [it, inserted] = real_oneof_indices.try_emplace( + *field_props->oneof, msg_proto->oneof_decl_size()); + if (inserted) { + msg_proto->add_oneof_decl()->set_name(*field_props->oneof); + used_oneof_names.insert(*field_props->oneof); + } + } + } + } + + // Build nested enums and explicit nested messages first so we know their + // names and can avoid collisions when synthesizing map-entry messages. + absl::flat_hash_set used_nested_type_names; + auto children_it = children_map.find(obj_def.name); + if (children_it != children_map.end()) { + for (const TypeDef* child : children_it->second) { + if (child->is_enum()) { + int enum_idx = msg_proto->enum_type_size(); + std::vector enum_path = base_path; + enum_path.push_back(google::protobuf::DescriptorProto::kEnumTypeFieldNumber); + enum_path.push_back(enum_idx); + BuildEnumProto(child->enum_type(), schema_name, options, + std::move(enum_path), msg_proto->add_enum_type(), + source_code_info); + } else if (child->is_object()) { + used_nested_type_names.insert(ShortTypeName(child->name())); + int nested_idx = msg_proto->nested_type_size(); + std::vector nested_path = base_path; + nested_path.push_back(google::protobuf::DescriptorProto::kNestedTypeFieldNumber); + nested_path.push_back(nested_idx); + CEL_RETURN_IF_ERROR(BuildMessageProto( + child->object_type(), schema_name, type_def_set, children_map, + options, std::move(nested_path), msg_proto->add_nested_type(), + source_code_info, referenced_types)); + } + } + } + + int32_t next_auto_field_number = 1; + auto allocate_field_number = [&]() -> int32_t { + while ( + used_field_numbers.contains(next_auto_field_number) || + (next_auto_field_number >= 19000 && next_auto_field_number <= 19999)) { + ++next_auto_field_number; + } + used_field_numbers.insert(next_auto_field_number); + return next_auto_field_number++; + }; + + for (size_t i = 0; i < obj_def.fields.size(); ++i) { + const ObjectTypeDef::Field& field_def = obj_def.fields[i]; + const ProtoFieldProperties* field_props = + GetProtoFieldProperties(field_def, schema_name); + + google::protobuf::FieldDescriptorProto* field_proto = msg_proto->add_field(); + field_proto->set_name(field_def.name); + if (field_props != nullptr && field_props->id.has_value()) { + field_proto->set_number(*field_props->id); + } else { + field_proto->set_number(allocate_field_number()); + } + if (field_props != nullptr && field_props->json_name.has_value()) { + field_proto->set_json_name(*field_props->json_name); + } + + std::vector field_path = base_path; + field_path.push_back(google::protobuf::DescriptorProto::kFieldFieldNumber); + field_path.push_back(static_cast(i)); + AddCommentLocation(field_path, field_def.doc, options, source_code_info); + + const bool is_singular_struct_override = + field_props != nullptr && field_props->type.has_value() && + *field_props->type == "google.protobuf.Struct"; + const bool is_singular_list_value_override = + field_props != nullptr && field_props->type.has_value() && + *field_props->type == "google.protobuf.ListValue"; + + if (field_def.type.has_map_type() && !is_singular_struct_override) { + // Protobuf map field. + CEL_ASSIGN_OR_RETURN( + std::string key_proto_type, + DefaultSingularProtoTypeName(field_def.type.map_type().key_type())); + CEL_ASSIGN_OR_RETURN( + std::string val_proto_type, + DefaultSingularProtoTypeName(field_def.type.map_type().value_type())); + if (field_props != nullptr && field_props->type.has_value()) { + CEL_RETURN_IF_ERROR(ParseMapProtoType( + *field_props->type, &key_proto_type, &val_proto_type)); + } + + std::string entry_name = MapEntryName(field_def.name); + while (used_nested_type_names.contains(entry_name)) { + absl::StrAppend(&entry_name, "_"); + } + used_nested_type_names.insert(entry_name); + + google::protobuf::DescriptorProto* entry_proto = msg_proto->add_nested_type(); + entry_proto->set_name(entry_name); + entry_proto->mutable_options()->set_map_entry(true); + + google::protobuf::FieldDescriptorProto* key_field_proto = entry_proto->add_field(); + key_field_proto->set_name("key"); + key_field_proto->set_number(1); + key_field_proto->set_label(google::protobuf::FieldDescriptorProto::LABEL_OPTIONAL); + CEL_RETURN_IF_ERROR(PopulateSingularFieldProtoType( + field_def.type.map_type().key_type(), key_proto_type, type_def_set, + key_field_proto, referenced_types)); + + google::protobuf::FieldDescriptorProto* val_field_proto = entry_proto->add_field(); + val_field_proto->set_name("value"); + val_field_proto->set_number(2); + val_field_proto->set_label(google::protobuf::FieldDescriptorProto::LABEL_OPTIONAL); + CEL_RETURN_IF_ERROR(PopulateSingularFieldProtoType( + field_def.type.map_type().value_type(), val_proto_type, type_def_set, + val_field_proto, referenced_types)); + + field_proto->set_label(google::protobuf::FieldDescriptorProto::LABEL_REPEATED); + field_proto->set_type(google::protobuf::FieldDescriptorProto::TYPE_MESSAGE); + field_proto->set_type_name( + absl::StrCat(".", obj_def.name, ".", entry_name)); + } else if (field_def.type.has_list_type() && + !is_singular_list_value_override) { + // Repeated field. + field_proto->set_label(google::protobuf::FieldDescriptorProto::LABEL_REPEATED); + const TypeSpec& elem_spec = field_def.type.list_type().elem_type(); + std::string elem_proto_type; + if (field_props != nullptr && field_props->type.has_value()) { + elem_proto_type = *field_props->type; + } else { + CEL_ASSIGN_OR_RETURN(elem_proto_type, + DefaultSingularProtoTypeName(elem_spec)); + } + CEL_RETURN_IF_ERROR(PopulateSingularFieldProtoType( + elem_spec, elem_proto_type, type_def_set, field_proto, + referenced_types)); + } else { + // Singular field. + if (schema_name == kProto2SchemaName && field_props != nullptr && + field_props->label.has_value() && *field_props->label == "required") { + field_proto->set_label(google::protobuf::FieldDescriptorProto::LABEL_REQUIRED); + } else { + field_proto->set_label(google::protobuf::FieldDescriptorProto::LABEL_OPTIONAL); + } + + std::string singular_proto_type; + if (field_props != nullptr && field_props->type.has_value()) { + singular_proto_type = *field_props->type; + } else { + CEL_ASSIGN_OR_RETURN(singular_proto_type, + DefaultSingularProtoTypeName(field_def.type)); + } + CEL_RETURN_IF_ERROR(PopulateSingularFieldProtoType( + field_def.type, singular_proto_type, type_def_set, field_proto, + referenced_types)); + + if (field_props != nullptr && field_props->oneof.has_value()) { + field_proto->set_oneof_index( + real_oneof_indices.at(*field_props->oneof)); + } else if (schema_name == kProto3SchemaName && field_props != nullptr && + field_props->label.has_value() && + *field_props->label == "optional") { + field_proto->set_proto3_optional(true); + std::string synth_oneof_name = absl::StrCat("_", field_def.name); + while (used_oneof_names.contains(synth_oneof_name)) { + synth_oneof_name = absl::StrCat("_", synth_oneof_name); + } + used_oneof_names.insert(synth_oneof_name); + int synth_idx = msg_proto->oneof_decl_size(); + msg_proto->add_oneof_decl()->set_name(synth_oneof_name); + field_proto->set_oneof_index(synth_idx); + } + + if (schema_name == kProto2SchemaName) { + if (field_def.default_value.has_value()) { + CEL_ASSIGN_OR_RETURN( + std::string default_str, + FormatConstantDefaultValue(field_def.default_value, + field_proto->type())); + field_proto->set_default_value(default_str); + } else if (field_props != nullptr && + field_props->default_value.has_value()) { + field_proto->set_default_value(*field_props->default_value); + } + } + } + } + + return absl::OkStatus(); +} + +enum class TopoVisitState { kUnvisited, kVisiting, kVisited }; + +absl::Status TopoSortFile( + const std::string& file_name, + const absl::btree_map& + files_by_name, + absl::flat_hash_map* visit_state, + google::protobuf::FileDescriptorSet* output_set) { + TopoVisitState state = (*visit_state)[file_name]; + if (state == TopoVisitState::kVisited) { + return absl::OkStatus(); + } + if (state == TopoVisitState::kVisiting) { + return absl::InvalidArgumentError( + absl::StrCat("Cyclic file dependency involving '", file_name, "'.")); + } + (*visit_state)[file_name] = TopoVisitState::kVisiting; + auto it = files_by_name.find(file_name); + if (it != files_by_name.end()) { + for (const std::string& dep : it->second.dependency()) { + if (files_by_name.contains(dep)) { + CEL_RETURN_IF_ERROR( + TopoSortFile(dep, files_by_name, visit_state, output_set)); + } + } + *output_set->add_file() = it->second; + } + (*visit_state)[file_name] = TopoVisitState::kVisited; + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr FileDescriptorSetToTypeDefSet( + const google::protobuf::FileDescriptorSet& file_descriptor_set, + const ProtoToTypeDefOptions& options) { + google::protobuf::SimpleDescriptorDatabase ext_db; + for (const google::protobuf::FileDescriptorProto& file_proto : + file_descriptor_set.file()) { + if (file_proto.name().empty()) { + return absl::InvalidArgumentError( + "File descriptor name cannot be empty."); + } + if (!ext_db.Add(file_proto)) { + return absl::InvalidArgumentError(absl::StrCat( + "Duplicate file descriptor: '", file_proto.name(), "'.")); + } + } + + // Place ext_db first so user-provided descriptors (including SourceCodeInfo) + // take precedence over the minimal well-known types fallback database. + google::protobuf::MergedDescriptorDatabase merged_db( + &ext_db, cel::GetMinimalDescriptorDatabase()); + ErrorCollector error_collector; + google::protobuf::DescriptorPool pool(&merged_db, &error_collector); + + TypeDefSet type_def_set; + for (const google::protobuf::FileDescriptorProto& file_proto : + file_descriptor_set.file()) { + CEL_ASSIGN_OR_RETURN(absl::string_view schema_name, + GetSchemaName(file_proto)); + const google::protobuf::FileDescriptor* file_desc = + pool.FindFileByName(file_proto.name()); + if (file_desc == nullptr) { + if (!error_collector.errors().empty()) { + return absl::InvalidArgumentError( + absl::StrCat("Failed to build file descriptor '", file_proto.name(), + "': ", error_collector.errors())); + } + return absl::InvalidArgumentError(absl::StrCat( + "Failed to build file descriptor '", file_proto.name(), "'.")); + } + + for (int i = 0; i < file_desc->enum_type_count(); ++i) { + CEL_RETURN_IF_ERROR(ConvertEnumDescriptor( + *file_desc->enum_type(i), schema_name, options, type_def_set)); + } + + for (int i = 0; i < file_desc->message_type_count(); ++i) { + CEL_RETURN_IF_ERROR(ConvertMessageDescriptor( + *file_desc->message_type(i), schema_name, options, type_def_set)); + } + } + + return type_def_set; +} + +absl::StatusOr TypeDefSetToFileDescriptorSet( + const TypeDefSet& type_def_set, const TypeDefToProtoOptions& options) { + if (type_def_set.types().empty()) { + return google::protobuf::FileDescriptorSet{}; + } + + // Partition TypeDefs into top-level types and nested children of enclosing + // ObjectTypeDefs. + std::vector top_level_types; + absl::flat_hash_map> children_map; + absl::flat_hash_map parent_map; + + for (const TypeDef& td : type_def_set.types()) { + absl::string_view full_name = td.name(); + size_t dot_pos = full_name.rfind('.'); + const TypeDef* parent_td = nullptr; + if (dot_pos != absl::string_view::npos) { + const TypeDef* candidate = + type_def_set.FindTypeDef(full_name.substr(0, dot_pos)); + if (candidate != nullptr && candidate->is_object()) { + parent_td = candidate; + } + } + if (parent_td != nullptr) { + children_map[std::string(parent_td->name())].push_back(&td); + parent_map[std::string(full_name)] = parent_td; + } else { + top_level_types.push_back(&td); + } + } + + struct FileSpec { + std::string file_name; + std::string package; + std::string schema_name; + std::vector top_types; + }; + absl::btree_map file_specs; + absl::flat_hash_map top_type_to_file; + + for (const TypeDef* root_td : top_level_types) { + CEL_ASSIGN_OR_RETURN(std::string schema_name, + DetermineSchemaNameForTree(*root_td, children_map)); + absl::string_view full_name = root_td->name(); + size_t dot_pos = full_name.rfind('.'); + std::string package = (dot_pos == absl::string_view::npos) + ? "" + : std::string(full_name.substr(0, dot_pos)); + + std::optional explicit_file_name; + if (root_td->is_object()) { + const ProtoObjectProperties* props = + GetProtoObjectProperties(root_td->object_type(), schema_name); + if (props != nullptr && props->file_name.has_value() && + !props->file_name->empty()) { + explicit_file_name = *props->file_name; + } + } else if (root_td->is_enum()) { + const ProtoEnumProperties* props = + GetProtoEnumProperties(root_td->enum_type(), schema_name); + if (props != nullptr && props->file_name.has_value() && + !props->file_name->empty()) { + explicit_file_name = *props->file_name; + } + } + + std::string file_name; + if (explicit_file_name.has_value()) { + file_name = *std::move(explicit_file_name); + } else if (package.empty()) { + file_name = absl::StrCat(schema_name, "_types.proto"); + } else { + file_name = absl::StrCat(absl::StrReplaceAll(package, {{".", "/"}}), "/", + schema_name, "_types.proto"); + } + + auto [it, inserted] = + file_specs.try_emplace(file_name, FileSpec{.file_name = file_name, + .package = package, + .schema_name = schema_name}); + if (!inserted) { + if (it->second.package != package) { + return absl::InvalidArgumentError( + absl::StrCat("Conflicting packages '", it->second.package, + "' and '", package, "' in file '", file_name, "'.")); + } + if (it->second.schema_name != schema_name) { + return absl::InvalidArgumentError(absl::StrCat( + "Conflicting proto syntaxes '", it->second.schema_name, "' and '", + schema_name, "' in file '", file_name, "'.")); + } + } + it->second.top_types.push_back(root_td); + top_type_to_file[std::string(root_td->name())] = file_name; + } + + auto resolve_type_file = + [&](absl::string_view full_type_name) -> absl::StatusOr { + const TypeDef* td = type_def_set.FindTypeDef(full_type_name); + if (td != nullptr) { + const TypeDef* curr = td; + while (true) { + auto pit = parent_map.find(curr->name()); + if (pit == parent_map.end()) { + break; + } + curr = pit->second; + } + return top_type_to_file.at(curr->name()); + } + std::optional wkt_file = + GetWellKnownTypeFileName(full_type_name); + if (wkt_file.has_value()) { + return *std::move(wkt_file); + } + return absl::InvalidArgumentError(absl::StrCat( + "Unknown referenced protobuf type '", full_type_name, "'.")); + }; + + absl::btree_map files_by_name; + absl::btree_set required_wkt_files; + + for (const auto& [file_name, spec] : file_specs) { + google::protobuf::FileDescriptorProto file_proto; + file_proto.set_name(spec.file_name); + if (!spec.package.empty()) { + file_proto.set_package(spec.package); + } + file_proto.set_syntax(spec.schema_name); + + proto2::SourceCodeInfo source_code_info; + absl::flat_hash_set referenced_types; + + for (const TypeDef* top_td : spec.top_types) { + if (top_td->is_enum()) { + int enum_idx = file_proto.enum_type_size(); + std::vector path = { + google::protobuf::FileDescriptorProto::kEnumTypeFieldNumber, enum_idx}; + BuildEnumProto(top_td->enum_type(), spec.schema_name, options, + std::move(path), file_proto.add_enum_type(), + &source_code_info); + } else if (top_td->is_object()) { + int msg_idx = file_proto.message_type_size(); + std::vector path = { + google::protobuf::FileDescriptorProto::kMessageTypeFieldNumber, msg_idx}; + CEL_RETURN_IF_ERROR(BuildMessageProto( + top_td->object_type(), spec.schema_name, type_def_set, children_map, + options, std::move(path), file_proto.add_message_type(), + &source_code_info, &referenced_types)); + } + } + + if (source_code_info.location_size() > 0) { + *file_proto.mutable_source_code_info() = std::move(source_code_info); + } + + absl::btree_set deps; + for (const std::string& ref_type : referenced_types) { + CEL_ASSIGN_OR_RETURN(std::string dep_file, resolve_type_file(ref_type)); + if (dep_file != spec.file_name) { + deps.insert(dep_file); + if (!file_specs.contains(dep_file)) { + required_wkt_files.insert(dep_file); + } + } + } + for (const std::string& dep : deps) { + file_proto.add_dependency(dep); + } + + files_by_name[spec.file_name] = std::move(file_proto); + } + + if (options.include_well_known_type_files && !required_wkt_files.empty()) { + google::protobuf::DescriptorDatabase* wkt_db = cel::GetMinimalDescriptorDatabase(); + std::vector worklist(required_wkt_files.begin(), + required_wkt_files.end()); + while (!worklist.empty()) { + std::string wkt_file = std::move(worklist.back()); + worklist.pop_back(); + if (files_by_name.contains(wkt_file)) { + continue; + } + google::protobuf::FileDescriptorProto wkt_proto; + if (!wkt_db->FindFileByName(wkt_file, &wkt_proto)) { + return absl::InvalidArgumentError( + absl::StrCat("Well-known protobuf file '", wkt_file, + "' not found in minimal descriptor database.")); + } + for (const std::string& dep : wkt_proto.dependency()) { + if (!files_by_name.contains(dep)) { + worklist.push_back(dep); + } + } + files_by_name[wkt_file] = std::move(wkt_proto); + } + } + + google::protobuf::FileDescriptorSet result; + absl::flat_hash_map visit_state; + for (const auto& [file_name, _] : files_by_name) { + CEL_RETURN_IF_ERROR( + TopoSortFile(file_name, files_by_name, &visit_state, &result)); + } + + // Validate that the produced FileDescriptorSet builds cleanly in + // DescriptorPool. + google::protobuf::SimpleDescriptorDatabase ext_db; + for (const google::protobuf::FileDescriptorProto& f : result.file()) { + ext_db.Add(f); + } + google::protobuf::MergedDescriptorDatabase merged_db( + &ext_db, cel::GetMinimalDescriptorDatabase()); + ErrorCollector error_collector; + google::protobuf::DescriptorPool pool(&merged_db, &error_collector); + for (const google::protobuf::FileDescriptorProto& f : result.file()) { + if (pool.FindFileByName(f.name()) == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Generated invalid FileDescriptorProto '", f.name(), + "': ", error_collector.errors())); + } + } + + return result; +} + +} // namespace cel diff --git a/common/typedef/proto_to_typedef.h b/common/typedef/proto_to_typedef.h new file mode 100644 index 000000000..752f16d84 --- /dev/null +++ b/common/typedef/proto_to_typedef.h @@ -0,0 +1,67 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_TO_TYPEDEF_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_TO_TYPEDEF_H_ + +#include "google/protobuf/descriptor.pb.h" +#include "absl/status/statusor.h" +#include "common/typedef/typedef.h" + +namespace cel { + +struct ProtoToTypeDefOptions { + // Whether to extract leading source comments (`SourceCodeInfo`) into `doc`. + bool include_comments = true; + // Whether to extract explicit proto2 field default values into + // `ObjectTypeDef::Field::default_value`. + bool include_defaults = true; +}; + +// Converts all non-well-known message and enum descriptors in a +// `google::protobuf::FileDescriptorSet` into a `cel::TypeDefSet`. +// +// Standard CEL well-known types (e.g. `google.protobuf.Timestamp`, +// `google.protobuf.Duration`, `google.protobuf.Struct`, +// `google.protobuf.Value`, `google.protobuf.ListValue`, +// `google.protobuf.NullValue`, `google.protobuf.Any`, and primitive wrappers) +// are mapped to their built-in CEL `TypeSpec` representations on fields and +// omitted from the emitted `TypeDefSet`. Synthetic protobuf map-entry messages +// are also omitted. +// +// Schema-specific properties (`ProtoObjectProperties`, `ProtoFieldProperties`, +// `ProtoEnumProperties`) are populated under `"proto2"` or `"proto3"` according +// to each file's syntax. +absl::StatusOr FileDescriptorSetToTypeDefSet( + const google::protobuf::FileDescriptorSet& file_descriptor_set, + const ProtoToTypeDefOptions& options = {}); + +struct TypeDefToProtoOptions { + // Whether to populate `SourceCodeInfo` leading comments from non-empty `doc` + // fields. + bool include_comments = true; + // Whether to include minimal `FileDescriptorProto`s for referenced standard + // CEL well-known types (e.g. `google/protobuf/timestamp.proto`) when not + // already defined in the `TypeDefSet`. + bool include_well_known_type_files = true; +}; + +// Converts all message and enum `TypeDef`s in a `cel::TypeDefSet` back into a +// valid, topologically ordered `google::protobuf::FileDescriptorSet`. +absl::StatusOr TypeDefSetToFileDescriptorSet( + const TypeDefSet& type_def_set, const TypeDefToProtoOptions& options = {}); + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_PROTO_TO_TYPEDEF_H_ diff --git a/common/typedef/proto_to_typedef_test.cc b/common/typedef/proto_to_typedef_test.cc new file mode 100644 index 000000000..94db15fe8 --- /dev/null +++ b/common/typedef/proto_to_typedef_test.cc @@ -0,0 +1,734 @@ +// Copyright 2026 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 "common/typedef/proto_to_typedef.h" + +#include +#include +#include +#include +#include + +#include "google/protobuf/descriptor.pb.h" +#include "absl/container/flat_hash_set.h" +#include "absl/log/absl_log.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/ascii.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "common/minimal_descriptor_database.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/proto_schema_yaml.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef_yaml.h" +#include "common/typedef/yaml_test_helpers.h" +#include "internal/runfiles.h" +#include "internal/testing.h" +#include "cel/expr/conformance/proto2/test_all_types.pb.h" +#include "cel/expr/conformance/proto3/test_all_types.pb.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/descriptor_database.h" +#include "google/protobuf/text_format.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::NotNull; + +constexpr absl::string_view kTestTypeDefFilePath = +"_main/common/typedef/testdata/"; + +constexpr absl::string_view kBaselineSeparator = + "--------------------------------------------------------------------\n"; + +SchemaYamlRegistry MakeYamlRegistry() { + return SchemaYamlRegistry(std::make_unique(), + std::make_unique()); +} + +google::protobuf::FileDescriptorSet BuildTransitiveFileDescriptorSet( + const google::protobuf::Descriptor* root_descriptor) { + google::protobuf::FileDescriptorSet file_set; + absl::flat_hash_set visited; + std::vector stack = {root_descriptor->file()}; + while (!stack.empty()) { + const google::protobuf::FileDescriptor* file = stack.back(); + stack.pop_back(); + if (!visited.insert(file->name()).second) { + continue; + } + google::protobuf::FileDescriptorProto* file_proto = file_set.add_file(); + file->CopyTo(file_proto); + file->CopySourceCodeInfoTo(file_proto); + file->CopyJsonNameTo(file_proto); + for (int i = file->dependency_count() - 1; i >= 0; --i) { + stack.push_back(file->dependency(i)); + } + } + return file_set; +} + +bool IsWellKnownMessage(const google::protobuf::Descriptor& desc) { + switch (desc.well_known_type()) { + case google::protobuf::Descriptor::WELLKNOWNTYPE_BOOLVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_INT32VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_INT64VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_UINT32VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_UINT64VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_FLOATVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_DOUBLEVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_BYTESVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_STRINGVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_ANY: + case google::protobuf::Descriptor::WELLKNOWNTYPE_DURATION: + case google::protobuf::Descriptor::WELLKNOWNTYPE_TIMESTAMP: + case google::protobuf::Descriptor::WELLKNOWNTYPE_VALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_LISTVALUE: + case google::protobuf::Descriptor::WELLKNOWNTYPE_STRUCT: + return true; + default: + return false; + } +} + +bool IsWellKnownEnum(const google::protobuf::EnumDescriptor& enum_desc) { + return enum_desc.full_name() == "google.protobuf.NullValue"; +} + +template +std::string GetTrimmedLeadingComment(const DescriptorT& desc) { + proto2::SourceLocation loc; + if (!desc.GetSourceLocation(&loc)) { + return ""; + } + return std::string(absl::StripAsciiWhitespace(loc.leading_comments)); +} + +void VerifyEnumDescriptorEquivalence(const google::protobuf::EnumDescriptor& orig_enum, + const google::protobuf::EnumDescriptor& rt_enum) { + SCOPED_TRACE(absl::StrCat("Enum: ", orig_enum.full_name())); + EXPECT_EQ(rt_enum.full_name(), orig_enum.full_name()); + EXPECT_EQ(rt_enum.file()->name(), orig_enum.file()->name()); + EXPECT_EQ(GetTrimmedLeadingComment(rt_enum), + GetTrimmedLeadingComment(orig_enum)); + EXPECT_EQ(rt_enum.options().has_allow_alias(), + orig_enum.options().has_allow_alias()); + if (orig_enum.options().has_allow_alias()) { + EXPECT_EQ(rt_enum.options().allow_alias(), + orig_enum.options().allow_alias()); + } + ASSERT_EQ(rt_enum.value_count(), orig_enum.value_count()); + for (int i = 0; i < orig_enum.value_count(); ++i) { + const google::protobuf::EnumValueDescriptor* orig_val = orig_enum.value(i); + const google::protobuf::EnumValueDescriptor* rt_val = rt_enum.value(i); + EXPECT_EQ(rt_val->name(), orig_val->name()); + EXPECT_EQ(rt_val->number(), orig_val->number()); + EXPECT_EQ(GetTrimmedLeadingComment(*rt_val), + GetTrimmedLeadingComment(*orig_val)); + } +} + +void VerifyFieldDefaultEquivalence(const google::protobuf::FieldDescriptor& orig_field, + const google::protobuf::FieldDescriptor& rt_field) { + ASSERT_EQ(rt_field.has_default_value(), orig_field.has_default_value()); + if (!orig_field.has_default_value()) { + return; + } + switch (orig_field.cpp_type()) { + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: + EXPECT_EQ(rt_field.default_value_bool(), orig_field.default_value_bool()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_INT32: + EXPECT_EQ(rt_field.default_value_int32(), + orig_field.default_value_int32()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_INT64: + EXPECT_EQ(rt_field.default_value_int64(), + orig_field.default_value_int64()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: + EXPECT_EQ(rt_field.default_value_uint32(), + orig_field.default_value_uint32()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: + EXPECT_EQ(rt_field.default_value_uint64(), + orig_field.default_value_uint64()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: + EXPECT_FLOAT_EQ(rt_field.default_value_float(), + orig_field.default_value_float()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: + EXPECT_DOUBLE_EQ(rt_field.default_value_double(), + orig_field.default_value_double()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: + EXPECT_EQ(rt_field.default_value_string(), + orig_field.default_value_string()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: + EXPECT_EQ(rt_field.default_value_enum()->full_name(), + orig_field.default_value_enum()->full_name()); + break; + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: + break; + } +} + +void VerifyFieldDescriptorEquivalence(const google::protobuf::FieldDescriptor& orig_field, + const google::protobuf::FieldDescriptor& rt_field) { + SCOPED_TRACE(absl::StrCat("Field: ", orig_field.full_name())); + EXPECT_EQ(rt_field.name(), orig_field.name()); + EXPECT_EQ(rt_field.number(), orig_field.number()); + EXPECT_EQ(rt_field.type(), orig_field.type()); + EXPECT_EQ(rt_field.is_required(), orig_field.is_required()); + EXPECT_EQ(rt_field.is_repeated(), orig_field.is_repeated()); + EXPECT_EQ(rt_field.is_map(), orig_field.is_map()); + EXPECT_EQ(rt_field.has_presence(), orig_field.has_presence()); + EXPECT_EQ(rt_field.json_name(), orig_field.json_name()); + EXPECT_EQ(GetTrimmedLeadingComment(rt_field), + GetTrimmedLeadingComment(orig_field)); + + if (orig_field.real_containing_oneof() != nullptr) { + ASSERT_THAT(rt_field.real_containing_oneof(), NotNull()); + EXPECT_EQ(rt_field.real_containing_oneof()->name(), + orig_field.real_containing_oneof()->name()); + } else { + EXPECT_EQ(rt_field.real_containing_oneof(), nullptr); + } + + VerifyFieldDefaultEquivalence(orig_field, rt_field); + + if (orig_field.is_map()) { + const google::protobuf::FieldDescriptor* orig_key = + orig_field.message_type()->map_key(); + const google::protobuf::FieldDescriptor* rt_key = rt_field.message_type()->map_key(); + ASSERT_THAT(rt_key, NotNull()); + EXPECT_EQ(rt_key->type(), orig_key->type()); + + const google::protobuf::FieldDescriptor* orig_val = + orig_field.message_type()->map_value(); + const google::protobuf::FieldDescriptor* rt_val = + rt_field.message_type()->map_value(); + ASSERT_THAT(rt_val, NotNull()); + EXPECT_EQ(rt_val->type(), orig_val->type()); + if (orig_val->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { + EXPECT_EQ(rt_val->message_type()->full_name(), + orig_val->message_type()->full_name()); + } else if (orig_val->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_ENUM) { + EXPECT_EQ(rt_val->enum_type()->full_name(), + orig_val->enum_type()->full_name()); + } + } else if (orig_field.cpp_type() == + google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) { + ASSERT_THAT(rt_field.message_type(), NotNull()); + EXPECT_EQ(rt_field.message_type()->full_name(), + orig_field.message_type()->full_name()); + } else if (orig_field.cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_ENUM) { + ASSERT_THAT(rt_field.enum_type(), NotNull()); + EXPECT_EQ(rt_field.enum_type()->full_name(), + orig_field.enum_type()->full_name()); + } +} + +void VerifyMessageDescriptorEquivalence(const google::protobuf::Descriptor& orig_msg, + const google::protobuf::Descriptor& rt_msg) { + SCOPED_TRACE(absl::StrCat("Message: ", orig_msg.full_name())); + EXPECT_EQ(rt_msg.full_name(), orig_msg.full_name()); + EXPECT_EQ(rt_msg.file()->name(), orig_msg.file()->name()); + EXPECT_EQ(GetTrimmedLeadingComment(rt_msg), + GetTrimmedLeadingComment(orig_msg)); + + ASSERT_EQ(rt_msg.extension_range_count(), orig_msg.extension_range_count()); + for (int i = 0; i < orig_msg.extension_range_count(); ++i) { + EXPECT_EQ(rt_msg.extension_range(i)->start_number(), + orig_msg.extension_range(i)->start_number()); + EXPECT_EQ(rt_msg.extension_range(i)->end_number(), + orig_msg.extension_range(i)->end_number()); + } + + ASSERT_EQ(rt_msg.real_oneof_decl_count(), orig_msg.real_oneof_decl_count()); + for (int i = 0; i < orig_msg.real_oneof_decl_count(); ++i) { + const google::protobuf::OneofDescriptor* orig_oneof = orig_msg.real_oneof_decl(i); + const google::protobuf::OneofDescriptor* rt_oneof = + rt_msg.FindOneofByName(orig_oneof->name()); + ASSERT_THAT(rt_oneof, NotNull()); + EXPECT_EQ(rt_oneof->field_count(), orig_oneof->field_count()); + } + + ASSERT_EQ(rt_msg.field_count(), orig_msg.field_count()); + for (int i = 0; i < orig_msg.field_count(); ++i) { + const google::protobuf::FieldDescriptor* orig_field = orig_msg.field(i); + const google::protobuf::FieldDescriptor* rt_field = + rt_msg.FindFieldByName(orig_field->name()); + ASSERT_THAT(rt_field, NotNull()) << orig_field->full_name(); + VerifyFieldDescriptorEquivalence(*orig_field, *rt_field); + } + + for (int i = 0; i < orig_msg.enum_type_count(); ++i) { + const google::protobuf::EnumDescriptor* orig_enum = orig_msg.enum_type(i); + if (IsWellKnownEnum(*orig_enum)) { + continue; + } + const google::protobuf::EnumDescriptor* rt_enum = + rt_msg.FindEnumTypeByName(orig_enum->name()); + ASSERT_THAT(rt_enum, NotNull()) << orig_enum->full_name(); + VerifyEnumDescriptorEquivalence(*orig_enum, *rt_enum); + } + + for (int i = 0; i < orig_msg.nested_type_count(); ++i) { + const google::protobuf::Descriptor* orig_nested = orig_msg.nested_type(i); + if (orig_nested->options().map_entry() || + IsWellKnownMessage(*orig_nested)) { + continue; + } + const google::protobuf::Descriptor* rt_nested = + rt_msg.FindNestedTypeByName(orig_nested->name()); + ASSERT_THAT(rt_nested, NotNull()) << orig_nested->full_name(); + VerifyMessageDescriptorEquivalence(*orig_nested, *rt_nested); + } +} + +void VerifyFileDescriptorSetEquivalence( + const google::protobuf::FileDescriptorSet& orig_fds, + const google::protobuf::FileDescriptorSet& rt_fds) { + google::protobuf::SimpleDescriptorDatabase orig_ext_db; + for (const google::protobuf::FileDescriptorProto& f : orig_fds.file()) { + ASSERT_TRUE(orig_ext_db.Add(f)); + } + google::protobuf::MergedDescriptorDatabase orig_db(&orig_ext_db, + cel::GetMinimalDescriptorDatabase()); + google::protobuf::DescriptorPool orig_pool(&orig_db); + + google::protobuf::SimpleDescriptorDatabase rt_ext_db; + for (const google::protobuf::FileDescriptorProto& f : rt_fds.file()) { + ASSERT_TRUE(rt_ext_db.Add(f)); + } + google::protobuf::MergedDescriptorDatabase rt_db(&rt_ext_db, + cel::GetMinimalDescriptorDatabase()); + google::protobuf::DescriptorPool rt_pool(&rt_db); + + for (const google::protobuf::FileDescriptorProto& orig_file_proto : orig_fds.file()) { + const google::protobuf::FileDescriptor* orig_file = + orig_pool.FindFileByName(orig_file_proto.name()); + ASSERT_THAT(orig_file, NotNull()); + + int non_wkt_enums = 0; + for (int i = 0; i < orig_file->enum_type_count(); ++i) { + if (!IsWellKnownEnum(*orig_file->enum_type(i))) { + ++non_wkt_enums; + } + } + int non_wkt_msgs = 0; + for (int i = 0; i < orig_file->message_type_count(); ++i) { + if (!IsWellKnownMessage(*orig_file->message_type(i))) { + ++non_wkt_msgs; + } + } + if (non_wkt_enums == 0 && non_wkt_msgs == 0) { + // Pure well-known type file (e.g. timestamp.proto, struct.proto). + continue; + } + + const google::protobuf::FileDescriptor* rt_file = + rt_pool.FindFileByName(orig_file->name()); + ASSERT_THAT(rt_file, NotNull()) << "Missing file: " << orig_file->name(); + EXPECT_EQ(rt_file->package(), orig_file->package()); + EXPECT_EQ(rt_file->enum_type_count(), non_wkt_enums); + EXPECT_EQ(rt_file->message_type_count(), non_wkt_msgs); + + for (int i = 0; i < orig_file->enum_type_count(); ++i) { + const google::protobuf::EnumDescriptor* orig_enum = orig_file->enum_type(i); + if (IsWellKnownEnum(*orig_enum)) { + continue; + } + const google::protobuf::EnumDescriptor* rt_enum = + rt_pool.FindEnumTypeByName(orig_enum->full_name()); + ASSERT_THAT(rt_enum, NotNull()); + VerifyEnumDescriptorEquivalence(*orig_enum, *rt_enum); + } + + for (int i = 0; i < orig_file->message_type_count(); ++i) { + const google::protobuf::Descriptor* orig_msg = orig_file->message_type(i); + if (IsWellKnownMessage(*orig_msg)) { + continue; + } + const google::protobuf::Descriptor* rt_msg = + rt_pool.FindMessageTypeByName(orig_msg->full_name()); + ASSERT_THAT(rt_msg, NotNull()); + VerifyMessageDescriptorEquivalence(*orig_msg, *rt_msg); + } + } +} + +struct ProtoToTypeDefBaselineTestCase { + const google::protobuf::Descriptor* root_descriptor; + std::string baseline_file; +}; + +using ProtoToTypeDefBaselineTest = + testing::TestWithParam; + +TEST_P(ProtoToTypeDefBaselineTest, ConvertToYamlBaselineAndRoundTrip) { + std::string baseline; + std::string baseline_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, GetParam().baseline_file)); + ASSERT_THAT(cel::internal::GetFileContents(baseline_file, &baseline), IsOk()); + baseline = absl::StripAsciiWhitespace(baseline); + + google::protobuf::FileDescriptorSet file_set = + BuildTransitiveFileDescriptorSet(GetParam().root_descriptor); + ASSERT_OK_AND_ASSIGN(TypeDefSet type_def_set, + FileDescriptorSetToTypeDefSet(file_set)); + + SchemaYamlRegistry yaml_registry = MakeYamlRegistry(); + std::ostringstream yaml_out; + ASSERT_THAT(TypeDefSetToYaml(type_def_set, yaml_registry, yaml_out), IsOk()); + + // Verify the emitted YAML parses cleanly back into a TypeDefSet. + ASSERT_OK_AND_ASSIGN(TypeDefSet roundtrip_set, + TypeDefSetFromYaml(yaml_out.str(), yaml_registry)); + EXPECT_EQ(roundtrip_set.types().size(), type_def_set.types().size()); + + // Verify round-tripping TypeDefSet -> FileDescriptorSet produces an + // equivalent FileDescriptorSet. + ASSERT_OK_AND_ASSIGN(google::protobuf::FileDescriptorSet roundtrip_file_set, + TypeDefSetToFileDescriptorSet(roundtrip_set)); + VerifyFileDescriptorSetEquivalence(file_set, roundtrip_file_set); + + // Verify idempotence: FileDescriptorSet -> TypeDefSet -> FileDescriptorSet + // -> TypeDefSet produces the exact same YAML. + ASSERT_OK_AND_ASSIGN(TypeDefSet second_type_def_set, + FileDescriptorSetToTypeDefSet(roundtrip_file_set)); + std::ostringstream second_yaml_out; + ASSERT_THAT( + TypeDefSetToYaml(second_type_def_set, yaml_registry, second_yaml_out), + IsOk()); + EXPECT_EQ(second_yaml_out.str(), yaml_out.str()); + + std::ostringstream out; + out << "PROTO DESCRIPTOR: " << GetParam().root_descriptor->full_name() + << "\n"; + out << kBaselineSeparator; + out << "TYPEDEF YAML:\n"; + out << yaml_out.str(); + + std::string actual(absl::StripAsciiWhitespace(out.str())); + if (actual != baseline) { + // Log the actual result to make it easier to copy/paste into the baseline + // file when updating the tests. + ABSL_LOG(INFO) << "Actual:\n" << actual; + EXPECT_EQ(actual, baseline); + } +} + +INSTANTIATE_TEST_SUITE_P( + TestAllTypes, ProtoToTypeDefBaselineTest, + testing::ValuesIn({ + ProtoToTypeDefBaselineTestCase{ + .root_descriptor = + cel::expr::conformance::proto2::TestAllTypes::descriptor(), + .baseline_file = "proto2_test_all_types_to_typedef.baseline", + }, + ProtoToTypeDefBaselineTestCase{ + .root_descriptor = + cel::expr::conformance::proto3::TestAllTypes::descriptor(), + .baseline_file = "proto3_test_all_types_to_typedef.baseline", + }, + })); + +TEST(ProtoToTypeDefTest, CommentsCustomJsonNameAndAllowAlias) { + google::protobuf::FileDescriptorSet file_set; + ASSERT_TRUE(google::protobuf::TextFormat::ParseFromString( + R"pb( + name: "custom.proto" + package: "com.example" + syntax: "proto2" + message_type { + name: "CustomMsg" + field { + name: "custom_field" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + default_value: "hello" + json_name: "customJson" + } + field { + name: "normal_field" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_INT32 + json_name: "normalField" + } + } + enum_type { + name: "CustomEnum" + value { name: "UNKNOWN" number: 0 } + value { name: "FIRST" number: 1 } + value { name: "ALIAS_FIRST" number: 1 } + options { allow_alias: true } + } + source_code_info { + location { + path: [ 4, 0 ] + span: [ 0, 0, 0 ] + leading_comments: " Message doc comment.\n" + } + location { + path: [ 4, 0, 2, 0 ] + span: [ 0, 0, 0 ] + leading_comments: " Field doc comment.\n" + } + location { + path: [ 5, 0 ] + span: [ 0, 0, 0 ] + leading_comments: " Enum doc comment.\n" + } + location { + path: [ 5, 0, 2, 0 ] + span: [ 0, 0, 0 ] + leading_comments: " Enum constant doc comment.\n" + } + } + )pb", + file_set.add_file())); + + ASSERT_OK_AND_ASSIGN(TypeDefSet set, FileDescriptorSetToTypeDefSet(file_set)); + EXPECT_EQ(set.types().size(), 2); + + SchemaYamlRegistry yaml_registry = MakeYamlRegistry(); + std::ostringstream yaml_out; + ASSERT_THAT(TypeDefSetToYaml(set, yaml_registry, yaml_out), IsOk()); + EXPECT_EQ(yaml_out.str(), Unindent(R"yaml( + - enum: + name: "com.example.CustomEnum" + doc: "Enum doc comment." + schemas: + proto2: + allow_alias: true + file_name: "custom.proto" + constants: + - name: "UNKNOWN" + id: 0 + doc: "Enum constant doc comment." + - name: "FIRST" + id: 1 + - name: "ALIAS_FIRST" + id: 1 + - object: + name: "com.example.CustomMsg" + doc: "Message doc comment." + schemas: + proto2: + file_name: "custom.proto" + fields: + - name: "custom_field" + type: "string" + doc: "Field doc comment." + default: "hello" + schemas: + proto2: + id: 1 + json_name: "customJson" + - name: "normal_field" + type: "int" + schemas: + proto2: + id: 2 + type: "int32" + )yaml")); + + ASSERT_OK_AND_ASSIGN(google::protobuf::FileDescriptorSet rt_file_set, + TypeDefSetToFileDescriptorSet(set)); + VerifyFileDescriptorSetEquivalence(file_set, rt_file_set); + + // Disabling comments and defaults omits `doc` and `default`. + ASSERT_OK_AND_ASSIGN( + TypeDefSet stripped_set, + FileDescriptorSetToTypeDefSet( + file_set, {.include_comments = false, .include_defaults = false})); + const TypeDef* msg_def = stripped_set.FindTypeDef("com.example.CustomMsg"); + ASSERT_THAT(msg_def, NotNull()); + EXPECT_THAT(msg_def->doc(), Eq("")); + const ObjectTypeDef::Field* f = + msg_def->object_type().FindField("custom_field"); + ASSERT_THAT(f, NotNull()); + EXPECT_THAT(f->doc, Eq("")); + EXPECT_FALSE(f->default_value.has_value()); +} + +TEST(ProtoToTypeDefTest, MinimalWktFallbackDatabaseResolvesWktImports) { + // Verify that a FileDescriptorSet importing google/protobuf/timestamp.proto + // without bundling timestamp.proto in the FileDescriptorSet still builds via + // cel::GetMinimalDescriptorDatabase(), and round-trips back to a + // FileDescriptorSet with or without bundling WKT files. + google::protobuf::FileDescriptorSet file_set; + ASSERT_TRUE(google::protobuf::TextFormat::ParseFromString( + R"pb( + name: "event.proto" + package: "com.example" + syntax: "proto3" + dependency: "google/protobuf/timestamp.proto" + message_type { + name: "Event" + field { + name: "created_at" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".google.protobuf.Timestamp" + } + } + )pb", + file_set.add_file())); + + ASSERT_OK_AND_ASSIGN(TypeDefSet set, FileDescriptorSetToTypeDefSet(file_set)); + EXPECT_EQ(set.types().size(), 1); + const TypeDef* event_def = set.FindTypeDef("com.example.Event"); + ASSERT_THAT(event_def, NotNull()); + const ObjectTypeDef::Field* created_at = + event_def->object_type().FindField("created_at"); + ASSERT_THAT(created_at, NotNull()); + EXPECT_TRUE(created_at->type.has_well_known()); + + ASSERT_OK_AND_ASSIGN(google::protobuf::FileDescriptorSet rt_with_wkt, + TypeDefSetToFileDescriptorSet( + set, {.include_well_known_type_files = true})); + EXPECT_EQ(rt_with_wkt.file_size(), 2); + VerifyFileDescriptorSetEquivalence(file_set, rt_with_wkt); + + ASSERT_OK_AND_ASSIGN(google::protobuf::FileDescriptorSet rt_without_wkt, + TypeDefSetToFileDescriptorSet( + set, {.include_well_known_type_files = false})); + EXPECT_EQ(rt_without_wkt.file_size(), 1); + EXPECT_EQ(rt_without_wkt.file(0).name(), "event.proto"); + VerifyFileDescriptorSetEquivalence(file_set, rt_without_wkt); +} + +TEST(ProtoToTypeDefTest, HandConstructedTypeDefSetRoundTrip) { + SchemaYamlRegistry yaml_registry = MakeYamlRegistry(); + std::string yaml = Unindent(R"yaml( + - object: + name: "com.example.Order" + doc: "Order message." + fields: + - name: "order_id" + type: "string" + - name: "count" + type: "int" + )yaml"); + ASSERT_OK_AND_ASSIGN(TypeDefSet set, TypeDefSetFromYaml(yaml, yaml_registry)); + ASSERT_OK_AND_ASSIGN(google::protobuf::FileDescriptorSet fds, + TypeDefSetToFileDescriptorSet(set)); + ASSERT_EQ(fds.file_size(), 1); + EXPECT_EQ(fds.file(0).name(), "com/example/proto3_types.proto"); + EXPECT_EQ(fds.file(0).package(), "com.example"); + EXPECT_EQ(fds.file(0).syntax(), "proto3"); + + ASSERT_OK_AND_ASSIGN(TypeDefSet empty_fds_check, + FileDescriptorSetToTypeDefSet(fds)); + EXPECT_NE(empty_fds_check.FindTypeDef("com.example.Order"), nullptr); + + TypeDefSet empty_set; + ASSERT_OK_AND_ASSIGN(google::protobuf::FileDescriptorSet empty_fds, + TypeDefSetToFileDescriptorSet(empty_set)); + EXPECT_EQ(empty_fds.file_size(), 0); +} + +TEST(ProtoToTypeDefTest, ValidationErrors) { + // Empty file name. + { + google::protobuf::FileDescriptorSet file_set; + file_set.add_file(); + EXPECT_THAT(FileDescriptorSetToTypeDefSet(file_set), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("File descriptor name cannot be empty."))); + } + + // Duplicate file name. + { + google::protobuf::FileDescriptorSet file_set; + file_set.add_file()->set_name("dup.proto"); + file_set.add_file()->set_name("dup.proto"); + EXPECT_THAT(FileDescriptorSetToTypeDefSet(file_set), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Duplicate file descriptor: 'dup.proto'."))); + } + + // Unsupported syntax (e.g. editions). + { + google::protobuf::FileDescriptorSet file_set; + google::protobuf::FileDescriptorProto* f = file_set.add_file(); + f->set_name("editions.proto"); + f->set_syntax("editions"); + EXPECT_THAT(FileDescriptorSetToTypeDefSet(file_set), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported proto syntax 'editions' in " + "file 'editions.proto'."))); + } + + // Unresolved dependency. + { + google::protobuf::FileDescriptorSet file_set; + google::protobuf::FileDescriptorProto* f = file_set.add_file(); + f->set_name("broken.proto"); + f->add_dependency("nonexistent.proto"); + EXPECT_THAT( + FileDescriptorSetToTypeDefSet(file_set), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to build file descriptor 'broken.proto'"))); + } + + // Unknown referenced type in TypeDefSetToFileDescriptorSet. + { + SchemaYamlRegistry yaml_registry = MakeYamlRegistry(); + ASSERT_OK_AND_ASSIGN( + TypeDefSet set, + TypeDefSetFromYaml("- object:\n" + " name: \"com.example.Bad\"\n" + " fields:\n" + " - name: \"ref\"\n" + " type: \"com.example.Missing\"\n", + yaml_registry)); + EXPECT_THAT(TypeDefSetToFileDescriptorSet(set), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unknown referenced protobuf type " + "'com.example.Missing'"))); + } + + // Conflicting proto2 and proto3 schemas on a single TypeDef. + { + ObjectTypeDef obj; + obj.name = "com.example.Mixed"; + obj.schema_specific_properties["proto2"] = + std::make_unique(); + obj.schema_specific_properties["proto3"] = + std::make_unique(); + TypeDefSet set; + ASSERT_THAT(set.AddTypeDef(TypeDef(std::move(obj))), IsOk()); + EXPECT_THAT(TypeDefSetToFileDescriptorSet(set), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("mixes 'proto2' and 'proto3'"))); + } +} + +} // namespace +} // namespace cel diff --git a/common/typedef/schema.h b/common/typedef/schema.h new file mode 100644 index 000000000..3b46d95a2 --- /dev/null +++ b/common/typedef/schema.h @@ -0,0 +1,56 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_H_ + +namespace cel { + +// A schema (e.g. "proto2", "proto3", "json") represents an external schema +// or data representation that a CEL TypeDef maps to or from. While TypeDef +// describes types in a schema-agnostic way, schema properties store the +// schema-specific metadata (such as protobuf field IDs or JSON property names) +// attached to objects, fields, enums, and enum constants under `schemas`. + +// Opaque base class for schema-specific properties attached to an +// ObjectTypeDef. +class SchemaObjectProperties { + public: + virtual ~SchemaObjectProperties() = default; +}; + +// Opaque base class for schema-specific properties attached to an +// ObjectTypeDef::Field. +class SchemaFieldProperties { + public: + virtual ~SchemaFieldProperties() = default; +}; + +// Opaque base class for schema-specific properties attached to an +// EnumTypeDef. +class SchemaEnumProperties { + public: + virtual ~SchemaEnumProperties() = default; +}; + +// Opaque base class for schema-specific properties attached to an +// EnumTypeDef::EnumConstant. +class SchemaEnumConstantProperties { + public: + virtual ~SchemaEnumConstantProperties() = default; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_H_ diff --git a/common/typedef/schema_proto.cc b/common/typedef/schema_proto.cc new file mode 100644 index 000000000..d486d2939 --- /dev/null +++ b/common/typedef/schema_proto.cc @@ -0,0 +1,98 @@ +// Copyright 2026 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 "common/typedef/schema_proto.h" + +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +absl::StatusOr> +SchemaProto::DecodeObjectProperties(const types::Object& proto) const { + return nullptr; +} + +absl::StatusOr> +SchemaProto::DecodeFieldProperties(const types::Object::Field& proto) const { + return nullptr; +} + +absl::StatusOr> +SchemaProto::DecodeEnumProperties(const types::Enum& proto) const { + return nullptr; +} + +absl::StatusOr> +SchemaProto::DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const { + return nullptr; +} + +absl::Status SchemaProto::EncodeObjectProperties( + const SchemaObjectProperties& properties, + types::Object* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom object properties are not supported for schema '", name(), "'")); +} + +absl::Status SchemaProto::EncodeFieldProperties( + const SchemaFieldProperties& properties, + types::Object::Field* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom field properties are not supported for schema '", name(), "'")); +} + +absl::Status SchemaProto::EncodeEnumProperties( + const SchemaEnumProperties& properties, + types::Enum* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum properties are not supported for schema '", name(), "'")); +} + +absl::Status SchemaProto::EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + types::Enum::EnumConstant* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum constant properties are not supported for schema '", name(), + "'")); +} + +SchemaProtoRegistry::SchemaProtoRegistry( + std::vector> schemas) { + schemas_.reserve(schemas.size()); + for (std::unique_ptr& schema : schemas) { + Add(std::move(schema)); + } +} + +const SchemaProto* absl_nullable SchemaProtoRegistry::Find( + absl::string_view schema_name) const { + auto it = schemas_.find(schema_name); + if (it == schemas_.end()) { + return nullptr; + } + return it->second.get(); +} + +} // namespace cel diff --git a/common/typedef/schema_proto.h b/common/typedef/schema_proto.h new file mode 100644 index 000000000..2ee2d6684 --- /dev/null +++ b/common/typedef/schema_proto.h @@ -0,0 +1,122 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_PROTO_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_PROTO_H_ + +#include +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +// Interface representing a Protobuf codec for a schema (e.g. "json", +// "proto2", "proto3") capable of decoding and encoding schema-specific property +// objects via proto extensions on cel.types messages. +class SchemaProto { + public: + virtual ~SchemaProto() = default; + + // Returns the unique schema identifier (e.g. "json", "proto2", "proto3"). + virtual absl::string_view name() const = 0; + + // Decodes schema-specific object properties from extensions on `proto`. + // Returns nullptr if this schema has no properties present on `proto`. + virtual absl::StatusOr> + DecodeObjectProperties(const types::Object& proto) const; + + // Decodes schema-specific field properties from extensions on `proto`. + // Returns nullptr if this schema has no properties present on `proto`. + virtual absl::StatusOr> + DecodeFieldProperties(const types::Object::Field& proto) const; + + // Decodes schema-specific enum properties from extensions on `proto`. + // Returns nullptr if this schema has no properties present on `proto`. + virtual absl::StatusOr> + DecodeEnumProperties(const types::Enum& proto) const; + + // Decodes schema-specific enum constant properties from extensions on + // `proto`. Returns nullptr if this schema has no properties present on + // `proto`. + virtual absl::StatusOr> + DecodeEnumConstantProperties(const types::Enum::EnumConstant& proto) const; + + virtual absl::Status EncodeObjectProperties( + const SchemaObjectProperties& properties, + types::Object* absl_nonnull proto) const; + + virtual absl::Status EncodeFieldProperties( + const SchemaFieldProperties& properties, + types::Object::Field* absl_nonnull proto) const; + + virtual absl::Status EncodeEnumProperties( + const SchemaEnumProperties& properties, + types::Enum* absl_nonnull proto) const; + + virtual absl::Status EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + types::Enum::EnumConstant* absl_nonnull proto) const; +}; + +// Read-only registry of SchemaProto codecs keyed by schema name. +class SchemaProtoRegistry { + public: + SchemaProtoRegistry() = default; + + explicit SchemaProtoRegistry( + std::vector> schemas); + + template < + typename... Schemas, + typename = std::enable_if_t< + (sizeof...(Schemas) > 0) && + (std::is_convertible_v> && + ...)>> + explicit SchemaProtoRegistry(Schemas&&... schemas) { + schemas_.reserve(sizeof...(Schemas)); + (Add(std::forward(schemas)), ...); + } + + // Finds a registered SchemaProto by name, or returns nullptr if not found. + const SchemaProto* absl_nullable Find(absl::string_view schema_name) const; + + const absl::flat_hash_map>& + schemas() const { + return schemas_; + } + + private: + void Add(std::unique_ptr schema) { + if (schema != nullptr) { + absl::string_view name = schema->name(); + schemas_[name] = std::move(schema); + } + } + + absl::flat_hash_map> schemas_; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_PROTO_H_ diff --git a/common/typedef/schema_proto_test.cc b/common/typedef/schema_proto_test.cc new file mode 100644 index 000000000..bb70233eb --- /dev/null +++ b/common/typedef/schema_proto_test.cc @@ -0,0 +1,128 @@ +// Copyright 2026 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 "common/typedef/schema_proto.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" +#include "common/typedef/typedef.pb.h" +#include "internal/testing.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::NotNull; + +class StubSchemaProto final : public SchemaProto { + public: + explicit StubSchemaProto(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + private: + std::string name_; +}; + +TEST(SchemaProtoTest, DefaultDecodeReturnsNullptr) { + StubSchemaProto schema("stub"); + + types::Object object_proto; + types::Object::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant enum_const_proto; + + EXPECT_THAT(schema.DecodeObjectProperties(object_proto), + IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeFieldProperties(field_proto), + IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeEnumProperties(enum_proto), IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeEnumConstantProperties(enum_const_proto), + IsOkAndHolds(IsNull())); +} + +TEST(SchemaProtoTest, DefaultEncodeReturnsUnimplementedError) { + StubSchemaProto schema("stub"); + types::Object object_proto; + types::Object::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant enum_const_proto; + + SchemaObjectProperties obj_props; + SchemaFieldProperties field_props; + SchemaEnumProperties enum_props; + SchemaEnumConstantProperties enum_const_props; + + EXPECT_THAT(schema.EncodeObjectProperties(obj_props, &object_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom object properties are not supported for " + "schema 'stub'"))); + EXPECT_THAT(schema.EncodeFieldProperties(field_props, &field_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom field properties are not supported for " + "schema 'stub'"))); + EXPECT_THAT(schema.EncodeEnumProperties(enum_props, &enum_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum properties are not supported for " + "schema 'stub'"))); + EXPECT_THAT( + schema.EncodeEnumConstantProperties(enum_const_props, &enum_const_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum constant properties are not supported for " + "schema 'stub'"))); +} + +TEST(SchemaProtoRegistryTest, ConstructAndFindSchemas) { + SchemaProtoRegistry empty_registry; + EXPECT_THAT(empty_registry.Find("schema_a"), IsNull()); + EXPECT_TRUE(empty_registry.schemas().empty()); + + SchemaProtoRegistry registry(std::make_unique("schema_a"), + std::make_unique("schema_b")); + + const SchemaProto* schema_a = registry.Find("schema_a"); + ASSERT_THAT(schema_a, NotNull()); + EXPECT_EQ(schema_a->name(), "schema_a"); + + const SchemaProto* schema_b = registry.Find("schema_b"); + ASSERT_THAT(schema_b, NotNull()); + EXPECT_EQ(schema_b->name(), "schema_b"); + + EXPECT_THAT(registry.Find("unknown_schema"), IsNull()); + EXPECT_EQ(registry.schemas().size(), 2); +} + +TEST(SchemaProtoRegistryTest, ConstructFromVector) { + std::vector> schemas; + schemas.push_back(std::make_unique("schema_a")); + schemas.push_back(nullptr); + SchemaProtoRegistry registry(std::move(schemas)); + + EXPECT_THAT(registry.Find("schema_a"), NotNull()); + EXPECT_THAT(registry.Find("schema_b"), IsNull()); + EXPECT_EQ(registry.schemas().size(), 1); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/schema_yaml.cc b/common/typedef/schema_yaml.cc new file mode 100644 index 000000000..3e4c7e786 --- /dev/null +++ b/common/typedef/schema_yaml.cc @@ -0,0 +1,122 @@ +// Copyright 2026 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 "common/typedef/schema_yaml.h" + +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" +#include "common/typedef/yaml_helpers.h" + +namespace cel { + +absl::StatusOr> +SchemaYaml::DecodeObjectProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (internal::IsEmptyYamlNode(node)) { + return nullptr; + } + return internal::YamlError( + yaml, node, + absl::StrCat("Custom object properties are not supported for schema '", + name(), "'")); +} + +absl::StatusOr> +SchemaYaml::DecodeFieldProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (internal::IsEmptyYamlNode(node)) { + return nullptr; + } + return internal::YamlError( + yaml, node, + absl::StrCat("Custom field properties are not supported for schema '", + name(), "'")); +} + +absl::StatusOr> +SchemaYaml::DecodeEnumProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (internal::IsEmptyYamlNode(node)) { + return nullptr; + } + return internal::YamlError( + yaml, node, + absl::StrCat("Custom enum properties are not supported for schema '", + name(), "'")); +} + +absl::StatusOr> +SchemaYaml::DecodeEnumConstantProperties(absl::string_view yaml, + const YAML::Node& node) const { + if (internal::IsEmptyYamlNode(node)) { + return nullptr; + } + return internal::YamlError( + yaml, node, + absl::StrCat( + "Custom enum constant properties are not supported for schema '", + name(), "'")); +} + +absl::Status SchemaYaml::EncodeObjectProperties( + const SchemaObjectProperties& properties, YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom object properties are not supported for schema '", name(), "'")); +} + +absl::Status SchemaYaml::EncodeFieldProperties( + const SchemaFieldProperties& properties, YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom field properties are not supported for schema '", name(), "'")); +} + +absl::Status SchemaYaml::EncodeEnumProperties( + const SchemaEnumProperties& properties, YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum properties are not supported for schema '", name(), "'")); +} + +absl::Status SchemaYaml::EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum constant properties are not supported for schema '", name(), + "'")); +} + +SchemaYamlRegistry::SchemaYamlRegistry( + std::vector> schemas) { + schemas_.reserve(schemas.size()); + for (std::unique_ptr& schema : schemas) { + Add(std::move(schema)); + } +} + +const SchemaYaml* absl_nullable SchemaYamlRegistry::Find( + absl::string_view schema_name) const { + auto it = schemas_.find(schema_name); + if (it == schemas_.end()) { + return nullptr; + } + return it->second.get(); +} + +} // namespace cel diff --git a/common/typedef/schema_yaml.h b/common/typedef/schema_yaml.h new file mode 100644 index 000000000..3740b0c50 --- /dev/null +++ b/common/typedef/schema_yaml.h @@ -0,0 +1,110 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_YAML_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_YAML_H_ + +#include +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" + +namespace YAML { +class Emitter; +class Node; +} // namespace YAML + +namespace cel { + +// Interface representing a YAML codec for a schema (e.g. "json", +// "proto2", "proto3") capable of decoding and encoding schema-specific property +// objects. +class SchemaYaml { + public: + virtual ~SchemaYaml() = default; + + // Returns the unique schema identifier (e.g. "json", "proto2", "proto3"). + virtual absl::string_view name() const = 0; + + virtual absl::StatusOr> + DecodeObjectProperties(absl::string_view yaml, const YAML::Node& node) const; + + virtual absl::StatusOr> + DecodeFieldProperties(absl::string_view yaml, const YAML::Node& node) const; + + virtual absl::StatusOr> + DecodeEnumProperties(absl::string_view yaml, const YAML::Node& node) const; + + virtual absl::StatusOr> + DecodeEnumConstantProperties(absl::string_view yaml, + const YAML::Node& node) const; + + virtual absl::Status EncodeObjectProperties( + const SchemaObjectProperties& properties, YAML::Emitter& out) const; + + virtual absl::Status EncodeFieldProperties( + const SchemaFieldProperties& properties, YAML::Emitter& out) const; + + virtual absl::Status EncodeEnumProperties( + const SchemaEnumProperties& properties, YAML::Emitter& out) const; + + virtual absl::Status EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, YAML::Emitter& out) const; +}; + +// Read-only registry of SchemaYaml codecs keyed by schema name. +class SchemaYamlRegistry { + public: + SchemaYamlRegistry() = default; + + explicit SchemaYamlRegistry( + std::vector> schemas); + + template < + typename... Schemas, + typename = std::enable_if_t< + (sizeof...(Schemas) > 0) && + (std::is_convertible_v> && + ...)>> + explicit SchemaYamlRegistry(Schemas&&... schemas) { + schemas_.reserve(sizeof...(Schemas)); + (Add(std::forward(schemas)), ...); + } + + // Finds a registered SchemaYaml by name, or returns nullptr if not + // found. + const SchemaYaml* absl_nullable Find(absl::string_view schema_name) const; + + private: + void Add(std::unique_ptr schema) { + if (schema != nullptr) { + absl::string_view name = schema->name(); + schemas_[name] = std::move(schema); + } + } + + absl::flat_hash_map> schemas_; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_SCHEMA_YAML_H_ diff --git a/common/typedef/schema_yaml_test.cc b/common/typedef/schema_yaml_test.cc new file mode 100644 index 000000000..76a947fb1 --- /dev/null +++ b/common/typedef/schema_yaml_test.cc @@ -0,0 +1,153 @@ +// Copyright 2026 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 "common/typedef/schema_yaml.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/string_view.h" +#include "common/typedef/schema.h" +#include "internal/testing.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/node/parse.h" +#include "yaml-cpp/node/type.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel { +namespace { + +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::NotNull; + +class StubSchemaYaml final : public SchemaYaml { + public: + explicit StubSchemaYaml(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + private: + std::string name_; +}; + +TEST(SchemaYamlTest, DefaultDecodeSucceedsOnEmptyNodes) { + StubSchemaYaml schema("stub"); + + std::vector empty_nodes = { + YAML::Node(YAML::NodeType::Undefined), + YAML::Load("~"), + YAML::Load("\"\""), + YAML::Load("{}"), + YAML::Load("[]"), + }; + + for (const YAML::Node& node : empty_nodes) { + EXPECT_THAT(schema.DecodeObjectProperties("", node), + IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeFieldProperties("", node), IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeEnumProperties("", node), IsOkAndHolds(IsNull())); + EXPECT_THAT(schema.DecodeEnumConstantProperties("", node), + IsOkAndHolds(IsNull())); + } +} + +TEST(SchemaYamlTest, DefaultDecodeFailsWithYamlErrorOnNonEmptyNode) { + StubSchemaYaml schema("stub"); + absl::string_view yaml = "prop: 123"; + YAML::Node node = YAML::Load(std::string(yaml)); + + EXPECT_THAT(schema.DecodeObjectProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom object properties are not supported for " + "schema 'stub'\n|prop: 123\n|^"))); + EXPECT_THAT(schema.DecodeFieldProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom field properties are not supported for " + "schema 'stub'\n|prop: 123\n|^"))); + EXPECT_THAT(schema.DecodeEnumProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom enum properties are not supported for " + "schema 'stub'\n|prop: 123\n|^"))); + EXPECT_THAT( + schema.DecodeEnumConstantProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom enum constant properties are not supported for " + "schema 'stub'\n|prop: 123\n|^"))); +} + +TEST(SchemaYamlTest, DefaultEncodeReturnsUnimplementedError) { + StubSchemaYaml schema("stub"); + YAML::Emitter emitter; + SchemaObjectProperties obj_props; + SchemaFieldProperties field_props; + SchemaEnumProperties enum_props; + SchemaEnumConstantProperties enum_const_props; + + EXPECT_THAT(schema.EncodeObjectProperties(obj_props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom object properties are not supported for " + "schema 'stub'"))); + EXPECT_THAT(schema.EncodeFieldProperties(field_props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom field properties are not supported for " + "schema 'stub'"))); + EXPECT_THAT(schema.EncodeEnumProperties(enum_props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum properties are not supported for " + "schema 'stub'"))); + EXPECT_THAT( + schema.EncodeEnumConstantProperties(enum_const_props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum constant properties are not supported for " + "schema 'stub'"))); +} + +TEST(SchemaYamlRegistryTest, ConstructAndFindSchemas) { + SchemaYamlRegistry empty_registry; + EXPECT_THAT(empty_registry.Find("schema_a"), IsNull()); + + SchemaYamlRegistry registry(std::make_unique("schema_a"), + std::make_unique("schema_b")); + + const SchemaYaml* schema_a = registry.Find("schema_a"); + ASSERT_THAT(schema_a, NotNull()); + EXPECT_EQ(schema_a->name(), "schema_a"); + + const SchemaYaml* schema_b = registry.Find("schema_b"); + ASSERT_THAT(schema_b, NotNull()); + EXPECT_EQ(schema_b->name(), "schema_b"); + + EXPECT_THAT(registry.Find("unknown_schema"), IsNull()); +} + +TEST(SchemaYamlRegistryTest, ConstructFromVector) { + std::vector> schemas; + schemas.push_back(std::make_unique("schema_a")); + schemas.push_back(nullptr); + SchemaYamlRegistry registry(std::move(schemas)); + + EXPECT_THAT(registry.Find("schema_a"), NotNull()); + EXPECT_THAT(registry.Find("schema_b"), IsNull()); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/testdata/BUILD b/common/typedef/testdata/BUILD new file mode 100644 index 000000000..f85ec9d7d --- /dev/null +++ b/common/typedef/testdata/BUILD @@ -0,0 +1,21 @@ +package( + default_testonly = True, + default_visibility = ["//visibility:public"], +) + +filegroup( + name = "typedef_testdata", + srcs = glob([ + "*.yaml", + "*.txtproto", + "*.baseline", + ]), +) + +exports_files( + srcs = glob([ + "*.yaml", + "*.txtproto", + "*.baseline", + ]), +) diff --git a/common/typedef/testdata/proto2_test_all_types_to_typedef.baseline b/common/typedef/testdata/proto2_test_all_types_to_typedef.baseline new file mode 100644 index 000000000..1f563402e --- /dev/null +++ b/common/typedef/testdata/proto2_test_all_types_to_typedef.baseline @@ -0,0 +1,1516 @@ +PROTO DESCRIPTOR: cel.expr.conformance.proto2.TestAllTypes +-------------------------------------------------------------------- +TYPEDEF YAML: +- enum: + name: "cel.expr.conformance.proto2.GlobalEnum" + schemas: + proto2: + file_name: "third_party/cel/spec/proto/cel/expr/conformance/proto2/test_all_types.proto" + constants: + - name: "GOO" + id: 0 + - name: "GAR" + id: 1 + - name: "GAZ" + id: 2 +- object: + name: "cel.expr.conformance.proto2.NestedTestAllTypes" + schemas: + proto2: + file_name: "third_party/cel/spec/proto/cel/expr/conformance/proto2/test_all_types.proto" + fields: + - name: "child" + type: "cel.expr.conformance.proto2.NestedTestAllTypes" + schemas: + proto2: + id: 1 + - name: "payload" + type: "cel.expr.conformance.proto2.TestAllTypes" + schemas: + proto2: + id: 2 +- object: + name: "cel.expr.conformance.proto2.TestAllTypes" + schemas: + proto2: + file_name: "third_party/cel/spec/proto/cel/expr/conformance/proto2/test_all_types.proto" + extension_ranges: + - start: 1000 + end: 536870912 + fields: + - name: "single_int32" + type: "int" + default: -32 + schemas: + proto2: + id: 1 + type: "int32" + - name: "single_int64" + type: "int" + default: -64 + schemas: + proto2: + id: 2 + - name: "single_uint32" + type: "uint" + default: 32 + schemas: + proto2: + id: 3 + type: "uint32" + - name: "single_uint64" + type: "uint" + default: 64 + schemas: + proto2: + id: 4 + - name: "single_sint32" + type: "int" + schemas: + proto2: + id: 5 + type: "sint32" + - name: "single_sint64" + type: "int" + schemas: + proto2: + id: 6 + type: "sint64" + - name: "single_fixed32" + type: "uint" + schemas: + proto2: + id: 7 + type: "fixed32" + - name: "single_fixed64" + type: "uint" + schemas: + proto2: + id: 8 + type: "fixed64" + - name: "single_sfixed32" + type: "int" + schemas: + proto2: + id: 9 + type: "sfixed32" + - name: "single_sfixed64" + type: "int" + schemas: + proto2: + id: 10 + type: "sfixed64" + - name: "single_float" + type: "double" + default: 3 + schemas: + proto2: + id: 11 + type: "float" + - name: "single_double" + type: "double" + default: 6.4000000000000004 + schemas: + proto2: + id: 12 + - name: "single_bool" + type: "bool" + default: true + schemas: + proto2: + id: 13 + - name: "single_string" + type: "string" + default: "empty" + schemas: + proto2: + id: 14 + - name: "single_bytes" + type: "bytes" + default: b"\x6e\x6f\x6e\x65" + schemas: + proto2: + id: 15 + - name: "in" + type: "bool" + schemas: + proto2: + id: 18 + - name: "single_any" + type: "any" + schemas: + proto2: + id: 100 + - name: "single_duration" + type: "duration" + schemas: + proto2: + id: 101 + - name: "single_timestamp" + type: "timestamp" + schemas: + proto2: + id: 102 + - name: "single_struct" + type: "map" + schemas: + proto2: + id: 103 + type: "google.protobuf.Struct" + - name: "single_value" + type: "dyn" + schemas: + proto2: + id: 104 + - name: "single_int64_wrapper" + type: "int_wrapper" + schemas: + proto2: + id: 105 + - name: "single_int32_wrapper" + type: "int_wrapper" + schemas: + proto2: + id: 106 + type: "google.protobuf.Int32Value" + - name: "single_double_wrapper" + type: "double_wrapper" + schemas: + proto2: + id: 107 + - name: "single_float_wrapper" + type: "double_wrapper" + schemas: + proto2: + id: 108 + type: "google.protobuf.FloatValue" + - name: "single_uint64_wrapper" + type: "uint_wrapper" + schemas: + proto2: + id: 109 + - name: "single_uint32_wrapper" + type: "uint_wrapper" + schemas: + proto2: + id: 110 + type: "google.protobuf.UInt32Value" + - name: "single_string_wrapper" + type: "string_wrapper" + schemas: + proto2: + id: 111 + - name: "single_bool_wrapper" + type: "bool_wrapper" + schemas: + proto2: + id: 112 + - name: "single_bytes_wrapper" + type: "bytes_wrapper" + schemas: + proto2: + id: 113 + - name: "list_value" + type: "list" + schemas: + proto2: + id: 114 + type: "google.protobuf.ListValue" + - name: "null_value" + type: "null" + schemas: + proto2: + id: 115 + - name: "optional_null_value" + type: "null" + schemas: + proto2: + id: 116 + - name: "field_mask" + type: "google.protobuf.FieldMask" + schemas: + proto2: + id: 117 + - name: "empty" + type: "google.protobuf.Empty" + schemas: + proto2: + id: 118 + - name: "single_nested_message" + type: "cel.expr.conformance.proto2.TestAllTypes.NestedMessage" + schemas: + proto2: + id: 21 + oneof: "nested_type" + - name: "single_nested_enum" + type: "cel.expr.conformance.proto2.TestAllTypes.NestedEnum" + schemas: + proto2: + id: 22 + oneof: "nested_type" + default_value: "BAR" + - name: "standalone_message" + type: "cel.expr.conformance.proto2.TestAllTypes.NestedMessage" + schemas: + proto2: + id: 23 + - name: "standalone_enum" + type: "cel.expr.conformance.proto2.TestAllTypes.NestedEnum" + schemas: + proto2: + id: 24 + - name: "repeated_int32" + type: "list" + schemas: + proto2: + id: 31 + type: "int32" + - name: "repeated_int64" + type: "list" + schemas: + proto2: + id: 32 + - name: "repeated_uint32" + type: "list" + schemas: + proto2: + id: 33 + type: "uint32" + - name: "repeated_uint64" + type: "list" + schemas: + proto2: + id: 34 + - name: "repeated_sint32" + type: "list" + schemas: + proto2: + id: 35 + type: "sint32" + - name: "repeated_sint64" + type: "list" + schemas: + proto2: + id: 36 + type: "sint64" + - name: "repeated_fixed32" + type: "list" + schemas: + proto2: + id: 37 + type: "fixed32" + - name: "repeated_fixed64" + type: "list" + schemas: + proto2: + id: 38 + type: "fixed64" + - name: "repeated_sfixed32" + type: "list" + schemas: + proto2: + id: 39 + type: "sfixed32" + - name: "repeated_sfixed64" + type: "list" + schemas: + proto2: + id: 40 + type: "sfixed64" + - name: "repeated_float" + type: "list" + schemas: + proto2: + id: 41 + type: "float" + - name: "repeated_double" + type: "list" + schemas: + proto2: + id: 42 + - name: "repeated_bool" + type: "list" + schemas: + proto2: + id: 43 + - name: "repeated_string" + type: "list" + schemas: + proto2: + id: 44 + - name: "repeated_bytes" + type: "list" + schemas: + proto2: + id: 45 + - name: "repeated_nested_message" + type: "list" + schemas: + proto2: + id: 51 + - name: "repeated_nested_enum" + type: "list" + schemas: + proto2: + id: 52 + - name: "repeated_string_piece" + type: "list" + schemas: + proto2: + id: 53 + - name: "repeated_cord" + type: "list" + schemas: + proto2: + id: 54 + - name: "repeated_lazy_message" + type: "list" + schemas: + proto2: + id: 55 + - name: "repeated_any" + type: "list" + schemas: + proto2: + id: 120 + - name: "repeated_duration" + type: "list" + schemas: + proto2: + id: 121 + - name: "repeated_timestamp" + type: "list" + schemas: + proto2: + id: 122 + - name: "repeated_struct" + type: "list>" + schemas: + proto2: + id: 123 + - name: "repeated_value" + type: "list" + schemas: + proto2: + id: 124 + - name: "repeated_int64_wrapper" + type: "list" + schemas: + proto2: + id: 125 + - name: "repeated_int32_wrapper" + type: "list" + schemas: + proto2: + id: 126 + type: "google.protobuf.Int32Value" + - name: "repeated_double_wrapper" + type: "list" + schemas: + proto2: + id: 127 + - name: "repeated_float_wrapper" + type: "list" + schemas: + proto2: + id: 128 + type: "google.protobuf.FloatValue" + - name: "repeated_uint64_wrapper" + type: "list" + schemas: + proto2: + id: 129 + - name: "repeated_uint32_wrapper" + type: "list" + schemas: + proto2: + id: 130 + type: "google.protobuf.UInt32Value" + - name: "repeated_string_wrapper" + type: "list" + schemas: + proto2: + id: 131 + - name: "repeated_bool_wrapper" + type: "list" + schemas: + proto2: + id: 132 + - name: "repeated_bytes_wrapper" + type: "list" + schemas: + proto2: + id: 133 + - name: "repeated_list_value" + type: "list>" + schemas: + proto2: + id: 134 + - name: "repeated_null_value" + type: "list" + schemas: + proto2: + id: 135 + - name: "map_int64_nested_type" + type: "map" + schemas: + proto2: + id: 62 + - name: "map_bool_bool" + type: "map" + schemas: + proto2: + id: 63 + - name: "map_bool_string" + type: "map" + schemas: + proto2: + id: 64 + - name: "map_bool_bytes" + type: "map" + schemas: + proto2: + id: 65 + - name: "map_bool_int32" + type: "map" + schemas: + proto2: + id: 66 + type: "map" + - name: "map_bool_int64" + type: "map" + schemas: + proto2: + id: 67 + - name: "map_bool_uint32" + type: "map" + schemas: + proto2: + id: 68 + type: "map" + - name: "map_bool_uint64" + type: "map" + schemas: + proto2: + id: 69 + - name: "map_bool_float" + type: "map" + schemas: + proto2: + id: 70 + type: "map" + - name: "map_bool_double" + type: "map" + schemas: + proto2: + id: 71 + - name: "map_bool_enum" + type: "map" + schemas: + proto2: + id: 72 + - name: "map_bool_message" + type: "map" + schemas: + proto2: + id: 73 + - name: "map_bool_duration" + type: "map" + schemas: + proto2: + id: 228 + - name: "map_bool_timestamp" + type: "map" + schemas: + proto2: + id: 229 + - name: "map_bool_null_value" + type: "map" + schemas: + proto2: + id: 230 + - name: "map_bool_any" + type: "map" + schemas: + proto2: + id: 246 + - name: "map_bool_struct" + type: "map>" + schemas: + proto2: + id: 247 + - name: "map_bool_value" + type: "map" + schemas: + proto2: + id: 248 + - name: "map_bool_list_value" + type: "map>" + schemas: + proto2: + id: 249 + - name: "map_bool_int64_wrapper" + type: "map" + schemas: + proto2: + id: 250 + - name: "map_bool_int32_wrapper" + type: "map" + schemas: + proto2: + id: 251 + type: "map" + - name: "map_bool_double_wrapper" + type: "map" + schemas: + proto2: + id: 252 + - name: "map_bool_float_wrapper" + type: "map" + schemas: + proto2: + id: 253 + type: "map" + - name: "map_bool_uint64_wrapper" + type: "map" + schemas: + proto2: + id: 254 + - name: "map_bool_uint32_wrapper" + type: "map" + schemas: + proto2: + id: 255 + type: "map" + - name: "map_bool_string_wrapper" + type: "map" + schemas: + proto2: + id: 256 + - name: "map_bool_bool_wrapper" + type: "map" + schemas: + proto2: + id: 257 + - name: "map_bool_bytes_wrapper" + type: "map" + schemas: + proto2: + id: 258 + - name: "map_int32_bool" + type: "map" + schemas: + proto2: + id: 74 + type: "map" + - name: "map_int32_string" + type: "map" + schemas: + proto2: + id: 75 + type: "map" + - name: "map_int32_bytes" + type: "map" + schemas: + proto2: + id: 76 + type: "map" + - name: "map_int32_int32" + type: "map" + schemas: + proto2: + id: 77 + type: "map" + - name: "map_int32_int64" + type: "map" + schemas: + proto2: + id: 78 + type: "map" + - name: "map_int32_uint32" + type: "map" + schemas: + proto2: + id: 79 + type: "map" + - name: "map_int32_uint64" + type: "map" + schemas: + proto2: + id: 80 + type: "map" + - name: "map_int32_float" + type: "map" + schemas: + proto2: + id: 81 + type: "map" + - name: "map_int32_double" + type: "map" + schemas: + proto2: + id: 82 + type: "map" + - name: "map_int32_enum" + type: "map" + schemas: + proto2: + id: 83 + type: "map" + - name: "map_int32_message" + type: "map" + schemas: + proto2: + id: 84 + type: "map" + - name: "map_int32_duration" + type: "map" + schemas: + proto2: + id: 231 + type: "map" + - name: "map_int32_timestamp" + type: "map" + schemas: + proto2: + id: 232 + type: "map" + - name: "map_int32_null_value" + type: "map" + schemas: + proto2: + id: 233 + type: "map" + - name: "map_int32_any" + type: "map" + schemas: + proto2: + id: 259 + type: "map" + - name: "map_int32_struct" + type: "map>" + schemas: + proto2: + id: 260 + type: "map" + - name: "map_int32_value" + type: "map" + schemas: + proto2: + id: 261 + type: "map" + - name: "map_int32_list_value" + type: "map>" + schemas: + proto2: + id: 262 + type: "map" + - name: "map_int32_int64_wrapper" + type: "map" + schemas: + proto2: + id: 263 + type: "map" + - name: "map_int32_int32_wrapper" + type: "map" + schemas: + proto2: + id: 264 + type: "map" + - name: "map_int32_double_wrapper" + type: "map" + schemas: + proto2: + id: 265 + type: "map" + - name: "map_int32_float_wrapper" + type: "map" + schemas: + proto2: + id: 266 + type: "map" + - name: "map_int32_uint64_wrapper" + type: "map" + schemas: + proto2: + id: 267 + type: "map" + - name: "map_int32_uint32_wrapper" + type: "map" + schemas: + proto2: + id: 268 + type: "map" + - name: "map_int32_string_wrapper" + type: "map" + schemas: + proto2: + id: 269 + type: "map" + - name: "map_int32_bool_wrapper" + type: "map" + schemas: + proto2: + id: 270 + type: "map" + - name: "map_int32_bytes_wrapper" + type: "map" + schemas: + proto2: + id: 271 + type: "map" + - name: "map_int64_bool" + type: "map" + schemas: + proto2: + id: 85 + - name: "map_int64_string" + type: "map" + schemas: + proto2: + id: 86 + - name: "map_int64_bytes" + type: "map" + schemas: + proto2: + id: 87 + - name: "map_int64_int32" + type: "map" + schemas: + proto2: + id: 88 + type: "map" + - name: "map_int64_int64" + type: "map" + schemas: + proto2: + id: 89 + - name: "map_int64_uint32" + type: "map" + schemas: + proto2: + id: 90 + type: "map" + - name: "map_int64_uint64" + type: "map" + schemas: + proto2: + id: 91 + - name: "map_int64_float" + type: "map" + schemas: + proto2: + id: 92 + type: "map" + - name: "map_int64_double" + type: "map" + schemas: + proto2: + id: 93 + - name: "map_int64_enum" + type: "map" + schemas: + proto2: + id: 94 + - name: "map_int64_message" + type: "map" + schemas: + proto2: + id: 95 + - name: "map_int64_duration" + type: "map" + schemas: + proto2: + id: 234 + - name: "map_int64_timestamp" + type: "map" + schemas: + proto2: + id: 235 + - name: "map_int64_null_value" + type: "map" + schemas: + proto2: + id: 236 + - name: "map_int64_any" + type: "map" + schemas: + proto2: + id: 272 + - name: "map_int64_struct" + type: "map>" + schemas: + proto2: + id: 273 + - name: "map_int64_value" + type: "map" + schemas: + proto2: + id: 274 + - name: "map_int64_list_value" + type: "map>" + schemas: + proto2: + id: 275 + - name: "map_int64_int64_wrapper" + type: "map" + schemas: + proto2: + id: 276 + - name: "map_int64_int32_wrapper" + type: "map" + schemas: + proto2: + id: 277 + type: "map" + - name: "map_int64_double_wrapper" + type: "map" + schemas: + proto2: + id: 278 + - name: "map_int64_float_wrapper" + type: "map" + schemas: + proto2: + id: 279 + type: "map" + - name: "map_int64_uint64_wrapper" + type: "map" + schemas: + proto2: + id: 280 + - name: "map_int64_uint32_wrapper" + type: "map" + schemas: + proto2: + id: 281 + type: "map" + - name: "map_int64_string_wrapper" + type: "map" + schemas: + proto2: + id: 282 + - name: "map_int64_bool_wrapper" + type: "map" + schemas: + proto2: + id: 283 + - name: "map_int64_bytes_wrapper" + type: "map" + schemas: + proto2: + id: 284 + - name: "map_uint32_bool" + type: "map" + schemas: + proto2: + id: 96 + type: "map" + - name: "map_uint32_string" + type: "map" + schemas: + proto2: + id: 97 + type: "map" + - name: "map_uint32_bytes" + type: "map" + schemas: + proto2: + id: 98 + type: "map" + - name: "map_uint32_int32" + type: "map" + schemas: + proto2: + id: 99 + type: "map" + - name: "map_uint32_int64" + type: "map" + schemas: + proto2: + id: 200 + type: "map" + - name: "map_uint32_uint32" + type: "map" + schemas: + proto2: + id: 201 + type: "map" + - name: "map_uint32_uint64" + type: "map" + schemas: + proto2: + id: 202 + type: "map" + - name: "map_uint32_float" + type: "map" + schemas: + proto2: + id: 203 + type: "map" + - name: "map_uint32_double" + type: "map" + schemas: + proto2: + id: 204 + type: "map" + - name: "map_uint32_enum" + type: "map" + schemas: + proto2: + id: 205 + type: "map" + - name: "map_uint32_message" + type: "map" + schemas: + proto2: + id: 206 + type: "map" + - name: "map_uint32_duration" + type: "map" + schemas: + proto2: + id: 237 + type: "map" + - name: "map_uint32_timestamp" + type: "map" + schemas: + proto2: + id: 238 + type: "map" + - name: "map_uint32_null_value" + type: "map" + schemas: + proto2: + id: 239 + type: "map" + - name: "map_uint32_any" + type: "map" + schemas: + proto2: + id: 285 + type: "map" + - name: "map_uint32_struct" + type: "map>" + schemas: + proto2: + id: 286 + type: "map" + - name: "map_uint32_value" + type: "map" + schemas: + proto2: + id: 287 + type: "map" + - name: "map_uint32_list_value" + type: "map>" + schemas: + proto2: + id: 288 + type: "map" + - name: "map_uint32_int64_wrapper" + type: "map" + schemas: + proto2: + id: 289 + type: "map" + - name: "map_uint32_int32_wrapper" + type: "map" + schemas: + proto2: + id: 290 + type: "map" + - name: "map_uint32_double_wrapper" + type: "map" + schemas: + proto2: + id: 291 + type: "map" + - name: "map_uint32_float_wrapper" + type: "map" + schemas: + proto2: + id: 292 + type: "map" + - name: "map_uint32_uint64_wrapper" + type: "map" + schemas: + proto2: + id: 293 + type: "map" + - name: "map_uint32_uint32_wrapper" + type: "map" + schemas: + proto2: + id: 294 + type: "map" + - name: "map_uint32_string_wrapper" + type: "map" + schemas: + proto2: + id: 295 + type: "map" + - name: "map_uint32_bool_wrapper" + type: "map" + schemas: + proto2: + id: 296 + type: "map" + - name: "map_uint32_bytes_wrapper" + type: "map" + schemas: + proto2: + id: 297 + type: "map" + - name: "map_uint64_bool" + type: "map" + schemas: + proto2: + id: 207 + - name: "map_uint64_string" + type: "map" + schemas: + proto2: + id: 208 + - name: "map_uint64_bytes" + type: "map" + schemas: + proto2: + id: 209 + - name: "map_uint64_int32" + type: "map" + schemas: + proto2: + id: 210 + type: "map" + - name: "map_uint64_int64" + type: "map" + schemas: + proto2: + id: 211 + - name: "map_uint64_uint32" + type: "map" + schemas: + proto2: + id: 212 + type: "map" + - name: "map_uint64_uint64" + type: "map" + schemas: + proto2: + id: 213 + - name: "map_uint64_float" + type: "map" + schemas: + proto2: + id: 214 + type: "map" + - name: "map_uint64_double" + type: "map" + schemas: + proto2: + id: 215 + - name: "map_uint64_enum" + type: "map" + schemas: + proto2: + id: 216 + - name: "map_uint64_message" + type: "map" + schemas: + proto2: + id: 217 + - name: "map_uint64_duration" + type: "map" + schemas: + proto2: + id: 240 + - name: "map_uint64_timestamp" + type: "map" + schemas: + proto2: + id: 241 + - name: "map_uint64_null_value" + type: "map" + schemas: + proto2: + id: 242 + - name: "map_uint64_any" + type: "map" + schemas: + proto2: + id: 298 + - name: "map_uint64_struct" + type: "map>" + schemas: + proto2: + id: 299 + - name: "map_uint64_value" + type: "map" + schemas: + proto2: + id: 300 + - name: "map_uint64_list_value" + type: "map>" + schemas: + proto2: + id: 301 + - name: "map_uint64_int64_wrapper" + type: "map" + schemas: + proto2: + id: 302 + - name: "map_uint64_int32_wrapper" + type: "map" + schemas: + proto2: + id: 303 + type: "map" + - name: "map_uint64_double_wrapper" + type: "map" + schemas: + proto2: + id: 304 + - name: "map_uint64_float_wrapper" + type: "map" + schemas: + proto2: + id: 305 + type: "map" + - name: "map_uint64_uint64_wrapper" + type: "map" + schemas: + proto2: + id: 306 + - name: "map_uint64_uint32_wrapper" + type: "map" + schemas: + proto2: + id: 307 + type: "map" + - name: "map_uint64_string_wrapper" + type: "map" + schemas: + proto2: + id: 308 + - name: "map_uint64_bool_wrapper" + type: "map" + schemas: + proto2: + id: 309 + - name: "map_uint64_bytes_wrapper" + type: "map" + schemas: + proto2: + id: 310 + - name: "map_string_bool" + type: "map" + schemas: + proto2: + id: 218 + - name: "map_string_string" + type: "map" + schemas: + proto2: + id: 61 + - name: "map_string_bytes" + type: "map" + schemas: + proto2: + id: 219 + - name: "map_string_int32" + type: "map" + schemas: + proto2: + id: 220 + type: "map" + - name: "map_string_int64" + type: "map" + schemas: + proto2: + id: 221 + - name: "map_string_uint32" + type: "map" + schemas: + proto2: + id: 222 + type: "map" + - name: "map_string_uint64" + type: "map" + schemas: + proto2: + id: 223 + - name: "map_string_float" + type: "map" + schemas: + proto2: + id: 224 + type: "map" + - name: "map_string_double" + type: "map" + schemas: + proto2: + id: 225 + - name: "map_string_enum" + type: "map" + schemas: + proto2: + id: 226 + - name: "map_string_message" + type: "map" + schemas: + proto2: + id: 227 + - name: "map_string_duration" + type: "map" + schemas: + proto2: + id: 243 + - name: "map_string_timestamp" + type: "map" + schemas: + proto2: + id: 244 + - name: "map_string_null_value" + type: "map" + schemas: + proto2: + id: 245 + - name: "map_string_any" + type: "map" + schemas: + proto2: + id: 311 + - name: "map_string_struct" + type: "map>" + schemas: + proto2: + id: 312 + - name: "map_string_value" + type: "map" + schemas: + proto2: + id: 313 + - name: "map_string_list_value" + type: "map>" + schemas: + proto2: + id: 314 + - name: "map_string_int64_wrapper" + type: "map" + schemas: + proto2: + id: 315 + - name: "map_string_int32_wrapper" + type: "map" + schemas: + proto2: + id: 316 + type: "map" + - name: "map_string_double_wrapper" + type: "map" + schemas: + proto2: + id: 317 + - name: "map_string_float_wrapper" + type: "map" + schemas: + proto2: + id: 318 + type: "map" + - name: "map_string_uint64_wrapper" + type: "map" + schemas: + proto2: + id: 319 + - name: "map_string_uint32_wrapper" + type: "map" + schemas: + proto2: + id: 320 + type: "map" + - name: "map_string_string_wrapper" + type: "map" + schemas: + proto2: + id: 321 + - name: "map_string_bool_wrapper" + type: "map" + schemas: + proto2: + id: 322 + - name: "map_string_bytes_wrapper" + type: "map" + schemas: + proto2: + id: 323 + - name: "oneof_type" + type: "cel.expr.conformance.proto2.NestedTestAllTypes" + schemas: + proto2: + id: 400 + oneof: "kind" + - name: "oneof_msg" + type: "cel.expr.conformance.proto2.TestAllTypes.NestedMessage" + schemas: + proto2: + id: 401 + oneof: "kind" + - name: "oneof_bool" + type: "bool" + schemas: + proto2: + id: 402 + oneof: "kind" + - name: "nestedgroup" + type: "cel.expr.conformance.proto2.TestAllTypes.NestedGroup" + schemas: + proto2: + id: 403 + type: "group" + - name: "as" + type: "bool" + schemas: + proto2: + id: 500 + - name: "break" + type: "bool" + schemas: + proto2: + id: 501 + - name: "const" + type: "bool" + schemas: + proto2: + id: 502 + - name: "continue" + type: "bool" + schemas: + proto2: + id: 503 + - name: "else" + type: "bool" + schemas: + proto2: + id: 504 + - name: "for" + type: "bool" + schemas: + proto2: + id: 505 + - name: "function" + type: "bool" + schemas: + proto2: + id: 506 + - name: "if" + type: "bool" + schemas: + proto2: + id: 507 + - name: "import" + type: "bool" + schemas: + proto2: + id: 508 + - name: "let" + type: "bool" + schemas: + proto2: + id: 509 + - name: "loop" + type: "bool" + schemas: + proto2: + id: 510 + - name: "package" + type: "bool" + schemas: + proto2: + id: 511 + - name: "namespace" + type: "bool" + schemas: + proto2: + id: 512 + - name: "return" + type: "bool" + schemas: + proto2: + id: 513 + - name: "var" + type: "bool" + schemas: + proto2: + id: 514 + - name: "void" + type: "bool" + schemas: + proto2: + id: 515 + - name: "while" + type: "bool" + schemas: + proto2: + id: 516 +- enum: + name: "cel.expr.conformance.proto2.TestAllTypes.NestedEnum" + constants: + - name: "FOO" + id: 0 + - name: "BAR" + id: 1 + - name: "BAZ" + id: 2 +- object: + name: "cel.expr.conformance.proto2.TestAllTypes.NestedGroup" + fields: + - name: "single_id" + type: "int" + schemas: + proto2: + id: 404 + type: "int32" + - name: "single_name" + type: "string" + schemas: + proto2: + id: 405 +- object: + name: "cel.expr.conformance.proto2.TestAllTypes.NestedMessage" + fields: + - name: "bb" + type: "int" + schemas: + proto2: + id: 1 + type: "int32" +- object: + name: "cel.expr.conformance.proto2.TestRequired" + schemas: + proto2: + file_name: "third_party/cel/spec/proto/cel/expr/conformance/proto2/test_all_types.proto" + fields: + - name: "required_int32" + type: "int" + schemas: + proto2: + id: 1 + type: "int32" + label: "required" +- object: + name: "google.protobuf.Empty" + schemas: + proto3: + file_name: "google/protobuf/empty.proto" +- object: + name: "google.protobuf.FieldMask" + schemas: + proto3: + file_name: "google/protobuf/field_mask.proto" + fields: + - name: "paths" + type: "list" + schemas: + proto3: + id: 1 diff --git a/common/typedef/testdata/proto3_test_all_types_to_typedef.baseline b/common/typedef/testdata/proto3_test_all_types_to_typedef.baseline new file mode 100644 index 000000000..dea3298b9 --- /dev/null +++ b/common/typedef/testdata/proto3_test_all_types_to_typedef.baseline @@ -0,0 +1,1483 @@ +PROTO DESCRIPTOR: cel.expr.conformance.proto3.TestAllTypes +-------------------------------------------------------------------- +TYPEDEF YAML: +- enum: + name: "cel.expr.conformance.proto3.GlobalEnum" + schemas: + proto3: + file_name: "third_party/cel/spec/proto/cel/expr/conformance/proto3/test_all_types.proto" + constants: + - name: "GOO" + id: 0 + - name: "GAR" + id: 1 + - name: "GAZ" + id: 2 +- object: + name: "cel.expr.conformance.proto3.NestedTestAllTypes" + schemas: + proto3: + file_name: "third_party/cel/spec/proto/cel/expr/conformance/proto3/test_all_types.proto" + fields: + - name: "child" + type: "cel.expr.conformance.proto3.NestedTestAllTypes" + schemas: + proto3: + id: 1 + - name: "payload" + type: "cel.expr.conformance.proto3.TestAllTypes" + schemas: + proto3: + id: 2 +- object: + name: "cel.expr.conformance.proto3.TestAllTypes" + schemas: + proto3: + file_name: "third_party/cel/spec/proto/cel/expr/conformance/proto3/test_all_types.proto" + fields: + - name: "single_int32" + type: "int" + schemas: + proto3: + id: 1 + type: "int32" + - name: "single_int64" + type: "int" + schemas: + proto3: + id: 2 + - name: "single_uint32" + type: "uint" + schemas: + proto3: + id: 3 + type: "uint32" + - name: "single_uint64" + type: "uint" + schemas: + proto3: + id: 4 + - name: "single_sint32" + type: "int" + schemas: + proto3: + id: 5 + type: "sint32" + - name: "single_sint64" + type: "int" + schemas: + proto3: + id: 6 + type: "sint64" + - name: "single_fixed32" + type: "uint" + schemas: + proto3: + id: 7 + type: "fixed32" + - name: "single_fixed64" + type: "uint" + schemas: + proto3: + id: 8 + type: "fixed64" + - name: "single_sfixed32" + type: "int" + schemas: + proto3: + id: 9 + type: "sfixed32" + - name: "single_sfixed64" + type: "int" + schemas: + proto3: + id: 10 + type: "sfixed64" + - name: "single_float" + type: "double" + schemas: + proto3: + id: 11 + type: "float" + - name: "single_double" + type: "double" + schemas: + proto3: + id: 12 + - name: "single_bool" + type: "bool" + schemas: + proto3: + id: 13 + - name: "single_string" + type: "string" + schemas: + proto3: + id: 14 + - name: "single_bytes" + type: "bytes" + schemas: + proto3: + id: 15 + - name: "optional_bool" + type: "bool" + schemas: + proto3: + id: 16 + label: "optional" + - name: "optional_string" + type: "string" + schemas: + proto3: + id: 17 + label: "optional" + - name: "in" + type: "bool" + schemas: + proto3: + id: 18 + - name: "single_any" + type: "any" + schemas: + proto3: + id: 100 + - name: "single_duration" + type: "duration" + schemas: + proto3: + id: 101 + - name: "single_timestamp" + type: "timestamp" + schemas: + proto3: + id: 102 + - name: "single_struct" + type: "map" + schemas: + proto3: + id: 103 + type: "google.protobuf.Struct" + - name: "single_value" + type: "dyn" + schemas: + proto3: + id: 104 + - name: "single_int64_wrapper" + type: "int_wrapper" + schemas: + proto3: + id: 105 + - name: "single_int32_wrapper" + type: "int_wrapper" + schemas: + proto3: + id: 106 + type: "google.protobuf.Int32Value" + - name: "single_double_wrapper" + type: "double_wrapper" + schemas: + proto3: + id: 107 + - name: "single_float_wrapper" + type: "double_wrapper" + schemas: + proto3: + id: 108 + type: "google.protobuf.FloatValue" + - name: "single_uint64_wrapper" + type: "uint_wrapper" + schemas: + proto3: + id: 109 + - name: "single_uint32_wrapper" + type: "uint_wrapper" + schemas: + proto3: + id: 110 + type: "google.protobuf.UInt32Value" + - name: "single_string_wrapper" + type: "string_wrapper" + schemas: + proto3: + id: 111 + - name: "single_bool_wrapper" + type: "bool_wrapper" + schemas: + proto3: + id: 112 + - name: "single_bytes_wrapper" + type: "bytes_wrapper" + schemas: + proto3: + id: 113 + - name: "list_value" + type: "list" + schemas: + proto3: + id: 114 + type: "google.protobuf.ListValue" + - name: "null_value" + type: "null" + schemas: + proto3: + id: 115 + - name: "optional_null_value" + type: "null" + schemas: + proto3: + id: 116 + label: "optional" + - name: "field_mask" + type: "google.protobuf.FieldMask" + schemas: + proto3: + id: 117 + - name: "empty" + type: "google.protobuf.Empty" + schemas: + proto3: + id: 118 + - name: "single_nested_message" + type: "cel.expr.conformance.proto3.TestAllTypes.NestedMessage" + schemas: + proto3: + id: 21 + oneof: "nested_type" + - name: "single_nested_enum" + type: "cel.expr.conformance.proto3.TestAllTypes.NestedEnum" + schemas: + proto3: + id: 22 + oneof: "nested_type" + - name: "standalone_message" + type: "cel.expr.conformance.proto3.TestAllTypes.NestedMessage" + schemas: + proto3: + id: 23 + - name: "standalone_enum" + type: "cel.expr.conformance.proto3.TestAllTypes.NestedEnum" + schemas: + proto3: + id: 24 + - name: "repeated_int32" + type: "list" + schemas: + proto3: + id: 31 + type: "int32" + - name: "repeated_int64" + type: "list" + schemas: + proto3: + id: 32 + - name: "repeated_uint32" + type: "list" + schemas: + proto3: + id: 33 + type: "uint32" + - name: "repeated_uint64" + type: "list" + schemas: + proto3: + id: 34 + - name: "repeated_sint32" + type: "list" + schemas: + proto3: + id: 35 + type: "sint32" + - name: "repeated_sint64" + type: "list" + schemas: + proto3: + id: 36 + type: "sint64" + - name: "repeated_fixed32" + type: "list" + schemas: + proto3: + id: 37 + type: "fixed32" + - name: "repeated_fixed64" + type: "list" + schemas: + proto3: + id: 38 + type: "fixed64" + - name: "repeated_sfixed32" + type: "list" + schemas: + proto3: + id: 39 + type: "sfixed32" + - name: "repeated_sfixed64" + type: "list" + schemas: + proto3: + id: 40 + type: "sfixed64" + - name: "repeated_float" + type: "list" + schemas: + proto3: + id: 41 + type: "float" + - name: "repeated_double" + type: "list" + schemas: + proto3: + id: 42 + - name: "repeated_bool" + type: "list" + schemas: + proto3: + id: 43 + - name: "repeated_string" + type: "list" + schemas: + proto3: + id: 44 + - name: "repeated_bytes" + type: "list" + schemas: + proto3: + id: 45 + - name: "repeated_nested_message" + type: "list" + schemas: + proto3: + id: 51 + - name: "repeated_nested_enum" + type: "list" + schemas: + proto3: + id: 52 + - name: "repeated_string_piece" + type: "list" + schemas: + proto3: + id: 53 + - name: "repeated_cord" + type: "list" + schemas: + proto3: + id: 54 + - name: "repeated_lazy_message" + type: "list" + schemas: + proto3: + id: 55 + - name: "repeated_any" + type: "list" + schemas: + proto3: + id: 120 + - name: "repeated_duration" + type: "list" + schemas: + proto3: + id: 121 + - name: "repeated_timestamp" + type: "list" + schemas: + proto3: + id: 122 + - name: "repeated_struct" + type: "list>" + schemas: + proto3: + id: 123 + - name: "repeated_value" + type: "list" + schemas: + proto3: + id: 124 + - name: "repeated_int64_wrapper" + type: "list" + schemas: + proto3: + id: 125 + - name: "repeated_int32_wrapper" + type: "list" + schemas: + proto3: + id: 126 + type: "google.protobuf.Int32Value" + - name: "repeated_double_wrapper" + type: "list" + schemas: + proto3: + id: 127 + - name: "repeated_float_wrapper" + type: "list" + schemas: + proto3: + id: 128 + type: "google.protobuf.FloatValue" + - name: "repeated_uint64_wrapper" + type: "list" + schemas: + proto3: + id: 129 + - name: "repeated_uint32_wrapper" + type: "list" + schemas: + proto3: + id: 130 + type: "google.protobuf.UInt32Value" + - name: "repeated_string_wrapper" + type: "list" + schemas: + proto3: + id: 131 + - name: "repeated_bool_wrapper" + type: "list" + schemas: + proto3: + id: 132 + - name: "repeated_bytes_wrapper" + type: "list" + schemas: + proto3: + id: 133 + - name: "repeated_list_value" + type: "list>" + schemas: + proto3: + id: 134 + - name: "repeated_null_value" + type: "list" + schemas: + proto3: + id: 135 + - name: "map_int64_nested_type" + type: "map" + schemas: + proto3: + id: 62 + - name: "map_bool_bool" + type: "map" + schemas: + proto3: + id: 63 + - name: "map_bool_string" + type: "map" + schemas: + proto3: + id: 64 + - name: "map_bool_bytes" + type: "map" + schemas: + proto3: + id: 65 + - name: "map_bool_int32" + type: "map" + schemas: + proto3: + id: 66 + type: "map" + - name: "map_bool_int64" + type: "map" + schemas: + proto3: + id: 67 + - name: "map_bool_uint32" + type: "map" + schemas: + proto3: + id: 68 + type: "map" + - name: "map_bool_uint64" + type: "map" + schemas: + proto3: + id: 69 + - name: "map_bool_float" + type: "map" + schemas: + proto3: + id: 70 + type: "map" + - name: "map_bool_double" + type: "map" + schemas: + proto3: + id: 71 + - name: "map_bool_enum" + type: "map" + schemas: + proto3: + id: 72 + - name: "map_bool_message" + type: "map" + schemas: + proto3: + id: 73 + - name: "map_bool_duration" + type: "map" + schemas: + proto3: + id: 228 + - name: "map_bool_timestamp" + type: "map" + schemas: + proto3: + id: 229 + - name: "map_bool_null_value" + type: "map" + schemas: + proto3: + id: 230 + - name: "map_bool_any" + type: "map" + schemas: + proto3: + id: 246 + - name: "map_bool_struct" + type: "map>" + schemas: + proto3: + id: 247 + - name: "map_bool_value" + type: "map" + schemas: + proto3: + id: 248 + - name: "map_bool_list_value" + type: "map>" + schemas: + proto3: + id: 249 + - name: "map_bool_int64_wrapper" + type: "map" + schemas: + proto3: + id: 250 + - name: "map_bool_int32_wrapper" + type: "map" + schemas: + proto3: + id: 251 + type: "map" + - name: "map_bool_double_wrapper" + type: "map" + schemas: + proto3: + id: 252 + - name: "map_bool_float_wrapper" + type: "map" + schemas: + proto3: + id: 253 + type: "map" + - name: "map_bool_uint64_wrapper" + type: "map" + schemas: + proto3: + id: 254 + - name: "map_bool_uint32_wrapper" + type: "map" + schemas: + proto3: + id: 255 + type: "map" + - name: "map_bool_string_wrapper" + type: "map" + schemas: + proto3: + id: 256 + - name: "map_bool_bool_wrapper" + type: "map" + schemas: + proto3: + id: 257 + - name: "map_bool_bytes_wrapper" + type: "map" + schemas: + proto3: + id: 258 + - name: "map_int32_bool" + type: "map" + schemas: + proto3: + id: 74 + type: "map" + - name: "map_int32_string" + type: "map" + schemas: + proto3: + id: 75 + type: "map" + - name: "map_int32_bytes" + type: "map" + schemas: + proto3: + id: 76 + type: "map" + - name: "map_int32_int32" + type: "map" + schemas: + proto3: + id: 77 + type: "map" + - name: "map_int32_int64" + type: "map" + schemas: + proto3: + id: 78 + type: "map" + - name: "map_int32_uint32" + type: "map" + schemas: + proto3: + id: 79 + type: "map" + - name: "map_int32_uint64" + type: "map" + schemas: + proto3: + id: 80 + type: "map" + - name: "map_int32_float" + type: "map" + schemas: + proto3: + id: 81 + type: "map" + - name: "map_int32_double" + type: "map" + schemas: + proto3: + id: 82 + type: "map" + - name: "map_int32_enum" + type: "map" + schemas: + proto3: + id: 83 + type: "map" + - name: "map_int32_message" + type: "map" + schemas: + proto3: + id: 84 + type: "map" + - name: "map_int32_duration" + type: "map" + schemas: + proto3: + id: 231 + type: "map" + - name: "map_int32_timestamp" + type: "map" + schemas: + proto3: + id: 232 + type: "map" + - name: "map_int32_null_value" + type: "map" + schemas: + proto3: + id: 233 + type: "map" + - name: "map_int32_any" + type: "map" + schemas: + proto3: + id: 259 + type: "map" + - name: "map_int32_struct" + type: "map>" + schemas: + proto3: + id: 260 + type: "map" + - name: "map_int32_value" + type: "map" + schemas: + proto3: + id: 261 + type: "map" + - name: "map_int32_list_value" + type: "map>" + schemas: + proto3: + id: 262 + type: "map" + - name: "map_int32_int64_wrapper" + type: "map" + schemas: + proto3: + id: 263 + type: "map" + - name: "map_int32_int32_wrapper" + type: "map" + schemas: + proto3: + id: 264 + type: "map" + - name: "map_int32_double_wrapper" + type: "map" + schemas: + proto3: + id: 265 + type: "map" + - name: "map_int32_float_wrapper" + type: "map" + schemas: + proto3: + id: 266 + type: "map" + - name: "map_int32_uint64_wrapper" + type: "map" + schemas: + proto3: + id: 267 + type: "map" + - name: "map_int32_uint32_wrapper" + type: "map" + schemas: + proto3: + id: 268 + type: "map" + - name: "map_int32_string_wrapper" + type: "map" + schemas: + proto3: + id: 269 + type: "map" + - name: "map_int32_bool_wrapper" + type: "map" + schemas: + proto3: + id: 270 + type: "map" + - name: "map_int32_bytes_wrapper" + type: "map" + schemas: + proto3: + id: 271 + type: "map" + - name: "map_int64_bool" + type: "map" + schemas: + proto3: + id: 85 + - name: "map_int64_string" + type: "map" + schemas: + proto3: + id: 86 + - name: "map_int64_bytes" + type: "map" + schemas: + proto3: + id: 87 + - name: "map_int64_int32" + type: "map" + schemas: + proto3: + id: 88 + type: "map" + - name: "map_int64_int64" + type: "map" + schemas: + proto3: + id: 89 + - name: "map_int64_uint32" + type: "map" + schemas: + proto3: + id: 90 + type: "map" + - name: "map_int64_uint64" + type: "map" + schemas: + proto3: + id: 91 + - name: "map_int64_float" + type: "map" + schemas: + proto3: + id: 92 + type: "map" + - name: "map_int64_double" + type: "map" + schemas: + proto3: + id: 93 + - name: "map_int64_enum" + type: "map" + schemas: + proto3: + id: 94 + - name: "map_int64_message" + type: "map" + schemas: + proto3: + id: 95 + - name: "map_int64_duration" + type: "map" + schemas: + proto3: + id: 234 + - name: "map_int64_timestamp" + type: "map" + schemas: + proto3: + id: 235 + - name: "map_int64_null_value" + type: "map" + schemas: + proto3: + id: 236 + - name: "map_int64_any" + type: "map" + schemas: + proto3: + id: 272 + - name: "map_int64_struct" + type: "map>" + schemas: + proto3: + id: 273 + - name: "map_int64_value" + type: "map" + schemas: + proto3: + id: 274 + - name: "map_int64_list_value" + type: "map>" + schemas: + proto3: + id: 275 + - name: "map_int64_int64_wrapper" + type: "map" + schemas: + proto3: + id: 276 + - name: "map_int64_int32_wrapper" + type: "map" + schemas: + proto3: + id: 277 + type: "map" + - name: "map_int64_double_wrapper" + type: "map" + schemas: + proto3: + id: 278 + - name: "map_int64_float_wrapper" + type: "map" + schemas: + proto3: + id: 279 + type: "map" + - name: "map_int64_uint64_wrapper" + type: "map" + schemas: + proto3: + id: 280 + - name: "map_int64_uint32_wrapper" + type: "map" + schemas: + proto3: + id: 281 + type: "map" + - name: "map_int64_string_wrapper" + type: "map" + schemas: + proto3: + id: 282 + - name: "map_int64_bool_wrapper" + type: "map" + schemas: + proto3: + id: 283 + - name: "map_int64_bytes_wrapper" + type: "map" + schemas: + proto3: + id: 284 + - name: "map_uint32_bool" + type: "map" + schemas: + proto3: + id: 96 + type: "map" + - name: "map_uint32_string" + type: "map" + schemas: + proto3: + id: 97 + type: "map" + - name: "map_uint32_bytes" + type: "map" + schemas: + proto3: + id: 98 + type: "map" + - name: "map_uint32_int32" + type: "map" + schemas: + proto3: + id: 99 + type: "map" + - name: "map_uint32_int64" + type: "map" + schemas: + proto3: + id: 200 + type: "map" + - name: "map_uint32_uint32" + type: "map" + schemas: + proto3: + id: 201 + type: "map" + - name: "map_uint32_uint64" + type: "map" + schemas: + proto3: + id: 202 + type: "map" + - name: "map_uint32_float" + type: "map" + schemas: + proto3: + id: 203 + type: "map" + - name: "map_uint32_double" + type: "map" + schemas: + proto3: + id: 204 + type: "map" + - name: "map_uint32_enum" + type: "map" + schemas: + proto3: + id: 205 + type: "map" + - name: "map_uint32_message" + type: "map" + schemas: + proto3: + id: 206 + type: "map" + - name: "map_uint32_duration" + type: "map" + schemas: + proto3: + id: 237 + type: "map" + - name: "map_uint32_timestamp" + type: "map" + schemas: + proto3: + id: 238 + type: "map" + - name: "map_uint32_null_value" + type: "map" + schemas: + proto3: + id: 239 + type: "map" + - name: "map_uint32_any" + type: "map" + schemas: + proto3: + id: 285 + type: "map" + - name: "map_uint32_struct" + type: "map>" + schemas: + proto3: + id: 286 + type: "map" + - name: "map_uint32_value" + type: "map" + schemas: + proto3: + id: 287 + type: "map" + - name: "map_uint32_list_value" + type: "map>" + schemas: + proto3: + id: 288 + type: "map" + - name: "map_uint32_int64_wrapper" + type: "map" + schemas: + proto3: + id: 289 + type: "map" + - name: "map_uint32_int32_wrapper" + type: "map" + schemas: + proto3: + id: 290 + type: "map" + - name: "map_uint32_double_wrapper" + type: "map" + schemas: + proto3: + id: 291 + type: "map" + - name: "map_uint32_float_wrapper" + type: "map" + schemas: + proto3: + id: 292 + type: "map" + - name: "map_uint32_uint64_wrapper" + type: "map" + schemas: + proto3: + id: 293 + type: "map" + - name: "map_uint32_uint32_wrapper" + type: "map" + schemas: + proto3: + id: 294 + type: "map" + - name: "map_uint32_string_wrapper" + type: "map" + schemas: + proto3: + id: 295 + type: "map" + - name: "map_uint32_bool_wrapper" + type: "map" + schemas: + proto3: + id: 296 + type: "map" + - name: "map_uint32_bytes_wrapper" + type: "map" + schemas: + proto3: + id: 297 + type: "map" + - name: "map_uint64_bool" + type: "map" + schemas: + proto3: + id: 207 + - name: "map_uint64_string" + type: "map" + schemas: + proto3: + id: 208 + - name: "map_uint64_bytes" + type: "map" + schemas: + proto3: + id: 209 + - name: "map_uint64_int32" + type: "map" + schemas: + proto3: + id: 210 + type: "map" + - name: "map_uint64_int64" + type: "map" + schemas: + proto3: + id: 211 + - name: "map_uint64_uint32" + type: "map" + schemas: + proto3: + id: 212 + type: "map" + - name: "map_uint64_uint64" + type: "map" + schemas: + proto3: + id: 213 + - name: "map_uint64_float" + type: "map" + schemas: + proto3: + id: 214 + type: "map" + - name: "map_uint64_double" + type: "map" + schemas: + proto3: + id: 215 + - name: "map_uint64_enum" + type: "map" + schemas: + proto3: + id: 216 + - name: "map_uint64_message" + type: "map" + schemas: + proto3: + id: 217 + - name: "map_uint64_duration" + type: "map" + schemas: + proto3: + id: 240 + - name: "map_uint64_timestamp" + type: "map" + schemas: + proto3: + id: 241 + - name: "map_uint64_null_value" + type: "map" + schemas: + proto3: + id: 242 + - name: "map_uint64_any" + type: "map" + schemas: + proto3: + id: 298 + - name: "map_uint64_struct" + type: "map>" + schemas: + proto3: + id: 299 + - name: "map_uint64_value" + type: "map" + schemas: + proto3: + id: 300 + - name: "map_uint64_list_value" + type: "map>" + schemas: + proto3: + id: 301 + - name: "map_uint64_int64_wrapper" + type: "map" + schemas: + proto3: + id: 302 + - name: "map_uint64_int32_wrapper" + type: "map" + schemas: + proto3: + id: 303 + type: "map" + - name: "map_uint64_double_wrapper" + type: "map" + schemas: + proto3: + id: 304 + - name: "map_uint64_float_wrapper" + type: "map" + schemas: + proto3: + id: 305 + type: "map" + - name: "map_uint64_uint64_wrapper" + type: "map" + schemas: + proto3: + id: 306 + - name: "map_uint64_uint32_wrapper" + type: "map" + schemas: + proto3: + id: 307 + type: "map" + - name: "map_uint64_string_wrapper" + type: "map" + schemas: + proto3: + id: 308 + - name: "map_uint64_bool_wrapper" + type: "map" + schemas: + proto3: + id: 309 + - name: "map_uint64_bytes_wrapper" + type: "map" + schemas: + proto3: + id: 310 + - name: "map_string_bool" + type: "map" + schemas: + proto3: + id: 218 + - name: "map_string_string" + type: "map" + schemas: + proto3: + id: 61 + - name: "map_string_bytes" + type: "map" + schemas: + proto3: + id: 219 + - name: "map_string_int32" + type: "map" + schemas: + proto3: + id: 220 + type: "map" + - name: "map_string_int64" + type: "map" + schemas: + proto3: + id: 221 + - name: "map_string_uint32" + type: "map" + schemas: + proto3: + id: 222 + type: "map" + - name: "map_string_uint64" + type: "map" + schemas: + proto3: + id: 223 + - name: "map_string_float" + type: "map" + schemas: + proto3: + id: 224 + type: "map" + - name: "map_string_double" + type: "map" + schemas: + proto3: + id: 225 + - name: "map_string_enum" + type: "map" + schemas: + proto3: + id: 226 + - name: "map_string_message" + type: "map" + schemas: + proto3: + id: 227 + - name: "map_string_duration" + type: "map" + schemas: + proto3: + id: 243 + - name: "map_string_timestamp" + type: "map" + schemas: + proto3: + id: 244 + - name: "map_string_null_value" + type: "map" + schemas: + proto3: + id: 245 + - name: "map_string_any" + type: "map" + schemas: + proto3: + id: 311 + - name: "map_string_struct" + type: "map>" + schemas: + proto3: + id: 312 + - name: "map_string_value" + type: "map" + schemas: + proto3: + id: 313 + - name: "map_string_list_value" + type: "map>" + schemas: + proto3: + id: 314 + - name: "map_string_int64_wrapper" + type: "map" + schemas: + proto3: + id: 315 + - name: "map_string_int32_wrapper" + type: "map" + schemas: + proto3: + id: 316 + type: "map" + - name: "map_string_double_wrapper" + type: "map" + schemas: + proto3: + id: 317 + - name: "map_string_float_wrapper" + type: "map" + schemas: + proto3: + id: 318 + type: "map" + - name: "map_string_uint64_wrapper" + type: "map" + schemas: + proto3: + id: 319 + - name: "map_string_uint32_wrapper" + type: "map" + schemas: + proto3: + id: 320 + type: "map" + - name: "map_string_string_wrapper" + type: "map" + schemas: + proto3: + id: 321 + - name: "map_string_bool_wrapper" + type: "map" + schemas: + proto3: + id: 322 + - name: "map_string_bytes_wrapper" + type: "map" + schemas: + proto3: + id: 323 + - name: "oneof_type" + type: "cel.expr.conformance.proto3.NestedTestAllTypes" + schemas: + proto3: + id: 400 + oneof: "kind" + - name: "oneof_msg" + type: "cel.expr.conformance.proto3.TestAllTypes.NestedMessage" + schemas: + proto3: + id: 401 + oneof: "kind" + - name: "oneof_bool" + type: "bool" + schemas: + proto3: + id: 402 + oneof: "kind" + - name: "as" + type: "bool" + schemas: + proto3: + id: 500 + - name: "break" + type: "bool" + schemas: + proto3: + id: 501 + - name: "const" + type: "bool" + schemas: + proto3: + id: 502 + - name: "continue" + type: "bool" + schemas: + proto3: + id: 503 + - name: "else" + type: "bool" + schemas: + proto3: + id: 504 + - name: "for" + type: "bool" + schemas: + proto3: + id: 505 + - name: "function" + type: "bool" + schemas: + proto3: + id: 506 + - name: "if" + type: "bool" + schemas: + proto3: + id: 507 + - name: "import" + type: "bool" + schemas: + proto3: + id: 508 + - name: "let" + type: "bool" + schemas: + proto3: + id: 509 + - name: "loop" + type: "bool" + schemas: + proto3: + id: 510 + - name: "package" + type: "bool" + schemas: + proto3: + id: 511 + - name: "namespace" + type: "bool" + schemas: + proto3: + id: 512 + - name: "return" + type: "bool" + schemas: + proto3: + id: 513 + - name: "var" + type: "bool" + schemas: + proto3: + id: 514 + - name: "void" + type: "bool" + schemas: + proto3: + id: 515 + - name: "while" + type: "bool" + schemas: + proto3: + id: 516 +- enum: + name: "cel.expr.conformance.proto3.TestAllTypes.NestedEnum" + constants: + - name: "FOO" + id: 0 + - name: "BAR" + id: 1 + - name: "BAZ" + id: 2 +- object: + name: "cel.expr.conformance.proto3.TestAllTypes.NestedMessage" + fields: + - name: "bb" + type: "int" + schemas: + proto3: + id: 1 + type: "int32" +- object: + name: "google.protobuf.Empty" + schemas: + proto3: + file_name: "google/protobuf/empty.proto" +- object: + name: "google.protobuf.FieldMask" + schemas: + proto3: + file_name: "google/protobuf/field_mask.proto" + fields: + - name: "paths" + type: "list" + schemas: + proto3: + id: 1 diff --git a/common/typedef/testdata/typedefs.txtproto b/common/typedef/testdata/typedefs.txtproto new file mode 100644 index 000000000..ec79285f2 --- /dev/null +++ b/common/typedef/testdata/typedefs.txtproto @@ -0,0 +1,282 @@ +# proto-file: common/typedef/typedef.proto +# proto-message: cel.types.TypeDefSet + +types { + object_type { + name: "com.example.Order" + doc: "Customer order entity." + fields { + name: "order_id" + type { + primitive: STRING + } + doc: "Unique order ID." + default_value { + string_value: "ord-001" + } + [cel.types.json_field_schema] { + name: "orderId" + } + [cel.types.proto2_field_schema] { + name: "order_id_v2" + id: 1 + json_name: "orderId" + } + [cel.types.proto3_field_schema] { + id: 1 + json_name: "orderId" + } + } + fields { + name: "is_active" + type { + primitive: BOOL + } + default_value { + bool_value: true + } + } + fields { + name: "item_count" + type { + primitive: INT64 + } + default_value { + int64_value: -1 + } + } + fields { + name: "total_cents" + type { + primitive: UINT64 + } + default_value { + uint64_value: 500 + } + } + fields { + name: "discount_rate" + type { + primitive: DOUBLE + } + default_value { + double_value: 0.25 + } + } + fields { + name: "raw_token" + type { + primitive: BYTES + } + default_value { + bytes_value: "\001\002a" + } + } + fields { + name: "created_at" + type { + well_known: TIMESTAMP + } + default_value { + timestamp_value { + seconds: 1767225600 + } + } + } + fields { + name: "ttl" + type { + well_known: DURATION + } + default_value { + duration_value { + seconds: 90 + } + } + } + fields { + name: "nothing" + type { + null: NULL_VALUE + } + } + fields { + name: "anything" + type { + dyn {} + } + } + fields { + name: "payload" + type { + well_known: ANY + } + } + fields { + name: "opt_bool" + type { + wrapper: BOOL + } + } + fields { + name: "opt_int" + type { + wrapper: INT64 + } + } + fields { + name: "opt_uint" + type { + wrapper: UINT64 + } + } + fields { + name: "opt_double" + type { + wrapper: DOUBLE + } + } + fields { + name: "opt_string" + type { + wrapper: STRING + } + } + fields { + name: "opt_bytes" + type { + wrapper: BYTES + } + } + fields { + name: "tags" + type { + list_type { + elem_type { + primitive: STRING + } + } + } + } + fields { + name: "metadata" + type { + map_type { + key_type { + primitive: STRING + } + value_type { + primitive: INT64 + } + } + } + } + fields { + name: "predicate" + type { + function { + result_type { + primitive: BOOL + } + arg_types { + primitive: INT64 + } + arg_types { + primitive: STRING + } + } + } + } + fields { + name: "meta_type" + type { + type { + primitive: STRING + } + } + } + fields { + name: "param_val" + type { + type_param: "T" + } + } + fields { + name: "maybe_note" + type { + abstract_type { + name: "optional" + parameter_types { + primitive: STRING + } + } + } + } + fields { + name: "shipping_addr" + type { + message_type: "com.example.Address" + } + } + fields { + name: "empty_schema_field" + type { + primitive: STRING + } + [cel.types.json_field_schema] {} + [cel.types.proto2_field_schema] {} + [cel.types.proto3_field_schema] {} + } + [cel.types.json_object_schema] { + name: "Order" + } + [cel.types.proto2_object_schema] { + name: "com.example.proto2.Order" + } + [cel.types.proto3_object_schema] { + name: "com.example.proto3.Order" + } + } +} +types { + enum_type { + name: "com.example.OrderStatus" + doc: "Lifecycle status of an order." + constants { + name: "ORDER_STATUS_UNSPECIFIED" + id: 0 + doc: "Unspecified status." + } + constants { + name: "ORDER_STATUS_ACTIVE" + id: 1 + doc: "Active status." + [cel.types.json_enum_constant_schema] { + name: "active" + } + [cel.types.proto2_enum_constant_schema] { + name: "STATUS_ACTIVE_V2" + } + [cel.types.proto3_enum_constant_schema] { + name: "STATUS_ACTIVE_V3" + } + } + constants { + name: "ORDER_STATUS_LEGACY" + id: -1 + [cel.types.json_enum_constant_schema] {} + } + [cel.types.json_enum_schema] { + style: "string" + omit_unspecified: true + } + [cel.types.proto2_enum_schema] { + name: "com.example.proto2.OrderStatus" + allow_alias: true + } + [cel.types.proto3_enum_schema] { + name: "com.example.proto3.OrderStatus" + allow_alias: false + } + } +} diff --git a/common/typedef/testdata/typedefs.yaml b/common/typedef/testdata/typedefs.yaml new file mode 100644 index 000000000..bfdc0db42 --- /dev/null +++ b/common/typedef/testdata/typedefs.yaml @@ -0,0 +1,127 @@ +# Copyright 2026 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. + +- object: + name: "com.example.Order" + doc: "Customer order entity." + schemas: + json: + name: "Order" + proto2: + name: "com.example.proto2.Order" + proto3: + name: "com.example.proto3.Order" + fields: + - name: "order_id" + type: "string" + doc: "Unique order ID." + default: "ord-001" + schemas: + json: + name: "orderId" + proto2: + name: "order_id_v2" + id: 1 + json_name: "orderId" + proto3: + id: 1 + json_name: "orderId" + - name: "is_active" + type: "bool" + default: true + - name: "item_count" + type: "int" + default: -1 + - name: "total_cents" + type: "uint" + default: 500 + - name: "discount_rate" + type: "double" + default: 0.25 + - name: "raw_token" + type: "bytes" + default: b"\x01\x02\x61" + - name: "created_at" + type: "timestamp" + default: "2026-01-01T00:00:00Z" + - name: "ttl" + type: "duration" + default: "90s" + - name: "nothing" + type: "null" + - name: "anything" + type: "dyn" + - name: "payload" + type: "any" + - name: "opt_bool" + type: "bool_wrapper" + - name: "opt_int" + type: "int_wrapper" + - name: "opt_uint" + type: "uint_wrapper" + - name: "opt_double" + type: "double_wrapper" + - name: "opt_string" + type: "string_wrapper" + - name: "opt_bytes" + type: "bytes_wrapper" + - name: "tags" + type: "list" + - name: "metadata" + type: "map" + - name: "predicate" + type: "function" + - name: "meta_type" + type: "type" + - name: "param_val" + type: "~T" + - name: "maybe_note" + type: "optional" + - name: "empty_schema_field" + type: "string" + schemas: + json: {} + proto2: {} + proto3: {} +- enum: + name: "com.example.OrderStatus" + doc: "Lifecycle status of an order." + schemas: + json: + style: "string" + omit_unspecified: true + proto2: + name: "com.example.proto2.OrderStatus" + allow_alias: true + proto3: + name: "com.example.proto3.OrderStatus" + allow_alias: false + constants: + - name: "ORDER_STATUS_UNSPECIFIED" + id: 0 + doc: "Unspecified status." + - name: "ORDER_STATUS_ACTIVE" + id: 1 + doc: "Active status." + schemas: + json: + name: "active" + proto2: + name: "STATUS_ACTIVE_V2" + proto3: + name: "STATUS_ACTIVE_V3" + - name: "ORDER_STATUS_LEGACY" + id: -1 + schemas: + json: {} diff --git a/common/typedef/testdata/typedefs_proto_to_yaml.baseline b/common/typedef/testdata/typedefs_proto_to_yaml.baseline new file mode 100644 index 000000000..1edd9efbb --- /dev/null +++ b/common/typedef/testdata/typedefs_proto_to_yaml.baseline @@ -0,0 +1,122 @@ +PROTO SOURCE: typedefs.txtproto +-------------------------------------------------------------------- +TYPEDEF YAML: +- object: + name: "com.example.Order" + doc: "Customer order entity." + schemas: + json: + name: "Order" + proto2: + name: "com.example.proto2.Order" + proto3: + name: "com.example.proto3.Order" + fields: + - name: "order_id" + type: "string" + doc: "Unique order ID." + default: "ord-001" + schemas: + json: + name: "orderId" + proto2: + name: "order_id_v2" + id: 1 + json_name: "orderId" + proto3: + id: 1 + json_name: "orderId" + - name: "is_active" + type: "bool" + default: true + - name: "item_count" + type: "int" + default: -1 + - name: "total_cents" + type: "uint" + default: 500 + - name: "discount_rate" + type: "double" + default: 0.25 + - name: "raw_token" + type: "bytes" + default: b"\x01\x02\x61" + - name: "created_at" + type: "timestamp" + default: 2026-01-01T00:00:00Z + - name: "ttl" + type: "duration" + default: 1m30s + - name: "nothing" + type: "null" + - name: "anything" + type: "dyn" + - name: "payload" + type: "any" + - name: "opt_bool" + type: "bool_wrapper" + - name: "opt_int" + type: "int_wrapper" + - name: "opt_uint" + type: "uint_wrapper" + - name: "opt_double" + type: "double_wrapper" + - name: "opt_string" + type: "string_wrapper" + - name: "opt_bytes" + type: "bytes_wrapper" + - name: "tags" + type: "list" + - name: "metadata" + type: "map" + - name: "predicate" + type: "function" + - name: "meta_type" + type: "type" + - name: "param_val" + type: "~T" + - name: "maybe_note" + type: "optional" + - name: "shipping_addr" + type: "com.example.Address" + - name: "empty_schema_field" + type: "string" + schemas: + json: + {} + proto2: + {} + proto3: + {} +- enum: + name: "com.example.OrderStatus" + doc: "Lifecycle status of an order." + schemas: + json: + style: "string" + omit_unspecified: true + proto2: + name: "com.example.proto2.OrderStatus" + allow_alias: true + proto3: + name: "com.example.proto3.OrderStatus" + allow_alias: false + constants: + - name: "ORDER_STATUS_UNSPECIFIED" + id: 0 + doc: "Unspecified status." + - name: "ORDER_STATUS_ACTIVE" + id: 1 + doc: "Active status." + schemas: + json: + name: "active" + proto2: + name: "STATUS_ACTIVE_V2" + proto3: + name: "STATUS_ACTIVE_V3" + - name: "ORDER_STATUS_LEGACY" + id: -1 + schemas: + json: + {} diff --git a/common/typedef/testdata/typedefs_yaml_to_proto.baseline b/common/typedef/testdata/typedefs_yaml_to_proto.baseline new file mode 100644 index 000000000..f2502672a --- /dev/null +++ b/common/typedef/testdata/typedefs_yaml_to_proto.baseline @@ -0,0 +1,281 @@ +YAML SOURCE: typedefs.yaml +-------------------------------------------------------------------- +TYPEDEF PROTO: +types { + object_type { + name: "com.example.Order" + doc: "Customer order entity." + fields { + name: "order_id" + type { + primitive: STRING + } + doc: "Unique order ID." + default_value { + string_value: "ord-001" + } + [cel.types.json_field_schema] { + name: "orderId" + } + [cel.types.proto2_field_schema] { + name: "order_id_v2" + id: 1 + json_name: "orderId" + } + [cel.types.proto3_field_schema] { + id: 1 + json_name: "orderId" + } + } + fields { + name: "is_active" + type { + primitive: BOOL + } + default_value { + bool_value: true + } + } + fields { + name: "item_count" + type { + primitive: INT64 + } + default_value { + int64_value: -1 + } + } + fields { + name: "total_cents" + type { + primitive: UINT64 + } + default_value { + uint64_value: 500 + } + } + fields { + name: "discount_rate" + type { + primitive: DOUBLE + } + default_value { + double_value: 0.25 + } + } + fields { + name: "raw_token" + type { + primitive: BYTES + } + default_value { + bytes_value: "\001\002a" + } + } + fields { + name: "created_at" + type { + well_known: TIMESTAMP + } + default_value { + timestamp_value { + seconds: 1767225600 + } + } + } + fields { + name: "ttl" + type { + well_known: DURATION + } + default_value { + duration_value { + seconds: 90 + } + } + } + fields { + name: "nothing" + type { + null: NULL_VALUE + } + } + fields { + name: "anything" + type { + dyn { + } + } + } + fields { + name: "payload" + type { + well_known: ANY + } + } + fields { + name: "opt_bool" + type { + wrapper: BOOL + } + } + fields { + name: "opt_int" + type { + wrapper: INT64 + } + } + fields { + name: "opt_uint" + type { + wrapper: UINT64 + } + } + fields { + name: "opt_double" + type { + wrapper: DOUBLE + } + } + fields { + name: "opt_string" + type { + wrapper: STRING + } + } + fields { + name: "opt_bytes" + type { + wrapper: BYTES + } + } + fields { + name: "tags" + type { + list_type { + elem_type { + primitive: STRING + } + } + } + } + fields { + name: "metadata" + type { + map_type { + key_type { + primitive: STRING + } + value_type { + primitive: INT64 + } + } + } + } + fields { + name: "predicate" + type { + function { + result_type { + primitive: BOOL + } + arg_types { + primitive: INT64 + } + arg_types { + primitive: STRING + } + } + } + } + fields { + name: "meta_type" + type { + type { + primitive: STRING + } + } + } + fields { + name: "param_val" + type { + type_param: "T" + } + } + fields { + name: "maybe_note" + type { + abstract_type { + name: "optional" + parameter_types { + primitive: STRING + } + } + } + } + fields { + name: "empty_schema_field" + type { + primitive: STRING + } + [cel.types.json_field_schema] { + } + [cel.types.proto2_field_schema] { + } + [cel.types.proto3_field_schema] { + } + } + [cel.types.json_object_schema] { + name: "Order" + } + [cel.types.proto2_object_schema] { + name: "com.example.proto2.Order" + } + [cel.types.proto3_object_schema] { + name: "com.example.proto3.Order" + } + } +} +types { + enum_type { + name: "com.example.OrderStatus" + doc: "Lifecycle status of an order." + constants { + name: "ORDER_STATUS_UNSPECIFIED" + id: 0 + doc: "Unspecified status." + } + constants { + name: "ORDER_STATUS_ACTIVE" + id: 1 + doc: "Active status." + [cel.types.json_enum_constant_schema] { + name: "active" + } + [cel.types.proto2_enum_constant_schema] { + name: "STATUS_ACTIVE_V2" + } + [cel.types.proto3_enum_constant_schema] { + name: "STATUS_ACTIVE_V3" + } + } + constants { + name: "ORDER_STATUS_LEGACY" + id: -1 + [cel.types.json_enum_constant_schema] { + } + } + [cel.types.json_enum_schema] { + style: "string" + omit_unspecified: true + } + [cel.types.proto2_enum_schema] { + name: "com.example.proto2.OrderStatus" + allow_alias: true + } + [cel.types.proto3_enum_schema] { + name: "com.example.proto3.OrderStatus" + allow_alias: false + } + } +} diff --git a/common/typedef/typedef.cc b/common/typedef/typedef.cc new file mode 100644 index 000000000..0bebe241d --- /dev/null +++ b/common/typedef/typedef.cc @@ -0,0 +1,118 @@ +// Copyright 2026 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 "common/typedef/typedef.h" + +#include +#include + +#include "absl/base/nullability.h" +#include "absl/functional/overload.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + +namespace cel { + +absl::Status ObjectTypeDef::AddField(Field field) { + if (FindField(field.name) != nullptr) { + return absl::AlreadyExistsError( + absl::StrCat("Field '", field.name, "' is already defined.")); + } + fields.push_back(std::move(field)); + return absl::OkStatus(); +} + +const ObjectTypeDef::Field* absl_nullable ObjectTypeDef::FindField( + absl::string_view field_name) const { + for (const Field& field : fields) { + if (field.name == field_name) { + return &field; + } + } + return nullptr; +} + +absl::Status EnumTypeDef::AddConstant(EnumConstant constant) { + if (FindConstant(constant.name) != nullptr) { + return absl::AlreadyExistsError(absl::StrCat( + "Enum constant '", constant.name, "' is already defined.")); + } + constants.push_back(std::move(constant)); + return absl::OkStatus(); +} + +const EnumTypeDef::EnumConstant* absl_nullable EnumTypeDef::FindConstant( + absl::string_view constant_name) const { + for (const EnumConstant& constant : constants) { + if (constant.name == constant_name) { + return &constant; + } + } + return nullptr; +} + +TypeDefKindCase TypeDef::kind_case() const { + return std::visit( + absl::Overload{ + [](const ObjectTypeDef&) { return TypeDefKindCase::kObject; }, + [](const EnumTypeDef&) { return TypeDefKindCase::kEnum; }, + }, + variant_); +} + +absl::string_view TypeDef::name() const { + return std::visit( + absl::Overload{ + [](const ObjectTypeDef& obj) -> absl::string_view { + return obj.name; + }, + [](const EnumTypeDef& e) -> absl::string_view { return e.name; }, + }, + variant_); +} + +absl::string_view TypeDef::doc() const { + return std::visit( + absl::Overload{ + [](const ObjectTypeDef& obj) -> absl::string_view { return obj.doc; }, + [](const EnumTypeDef& e) -> absl::string_view { return e.doc; }, + }, + variant_); +} + +absl::Status TypeDefSet::AddTypeDef(TypeDef type_def) { + absl::string_view type_name = type_def.name(); + if (type_name.empty()) { + return absl::InvalidArgumentError("Type name cannot be empty."); + } + auto [it, inserted] = index_by_name_.emplace(type_name, types_.size()); + if (!inserted) { + return absl::AlreadyExistsError( + absl::StrCat("Type '", type_name, "' is already defined.")); + } + types_.push_back(std::move(type_def)); + return absl::OkStatus(); +} + +const TypeDef* absl_nullable TypeDefSet::FindTypeDef( + absl::string_view type_name) const { + auto it = index_by_name_.find(type_name); + if (it == index_by_name_.end()) { + return nullptr; + } + return &types_[it->second]; +} + +} // namespace cel diff --git a/common/typedef/typedef.h b/common/typedef/typedef.h new file mode 100644 index 000000000..42b1a149b --- /dev/null +++ b/common/typedef/typedef.h @@ -0,0 +1,151 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "common/ast.h" +#include "common/constant.h" +#include "common/typedef/schema.h" + +namespace cel { + +using ObjectSchemaSpecificProperties = + absl::flat_hash_map>; +using FieldSchemaSpecificProperties = + absl::flat_hash_map>; +using EnumSchemaSpecificProperties = + absl::flat_hash_map>; +using EnumConstantSchemaSpecificProperties = + absl::flat_hash_map>; + +// Represents a structured object type (`object:`) with typed fields. +struct ObjectTypeDef { + struct Field { + std::string name; + TypeSpec type; + std::string doc; + // Optional scalar default value; check `default_value.has_value()`. + Constant default_value; + // Target schema-specific properties keyed by target schema name. + FieldSchemaSpecificProperties schema_specific_properties; + }; + + std::string name; + std::string doc; + std::vector fields; + // Target schema-specific properties keyed by target schema name. + ObjectSchemaSpecificProperties schema_specific_properties; + + absl::Status AddField(Field field); + const Field* absl_nullable FindField(absl::string_view field_name) const; +}; + +// Represents a named enumeration type (`enum:`). +struct EnumTypeDef { + struct EnumConstant { + std::string name; + int32_t id = 0; + std::string doc; + // Target schema-specific properties keyed by target schema name. + EnumConstantSchemaSpecificProperties schema_specific_properties; + }; + + std::string name; + std::string doc; + std::vector constants; + // Target schema-specific properties keyed by target schema name. + EnumSchemaSpecificProperties schema_specific_properties; + + absl::Status AddConstant(EnumConstant constant); + const EnumConstant* absl_nullable FindConstant( + absl::string_view constant_name) const; +}; + +enum class TypeDefKindCase { + kObject, + kEnum, +}; + +// Variant wrapper holding either an ObjectTypeDef or EnumTypeDef. +class TypeDef { + public: + TypeDef() = default; + explicit TypeDef(ObjectTypeDef object_type) + : variant_(std::move(object_type)) {} + explicit TypeDef(EnumTypeDef enum_type) : variant_(std::move(enum_type)) {} + + TypeDefKindCase kind_case() const; + absl::string_view name() const; + absl::string_view doc() const; + + bool is_object() const { + return std::holds_alternative(variant_); + } + const ObjectTypeDef& object_type() const { + return std::get(variant_); + } + + bool is_enum() const { return std::holds_alternative(variant_); } + const EnumTypeDef& enum_type() const { + return std::get(variant_); + } + + private: + std::variant variant_; +}; + +// Represents a collection of unique TypeDefs (objects and enums) indexed by +// their fully-qualified CEL type name. +class TypeDefSet { + public: + TypeDefSet() = default; + + // Adds a TypeDef to the set. Returns InvalidArgumentError if the type name + // is empty, or AlreadyExistsError if a TypeDef with the same name is already + // present. + absl::Status AddTypeDef(TypeDef type_def); + + // Looks up a TypeDef by its fully-qualified name, or returns nullptr if not + // found. + const TypeDef* absl_nullable FindTypeDef(absl::string_view type_name) const; + + // Returns the TypeDefs in the order they were added. + absl::Span types() const { return types_; } + + private: + std::vector types_; + absl::flat_hash_map index_by_name_; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_H_ diff --git a/common/typedef/typedef.proto b/common/typedef/typedef.proto new file mode 100644 index 000000000..52d945e5b --- /dev/null +++ b/common/typedef/typedef.proto @@ -0,0 +1,70 @@ +// Copyright 2026 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. + +edition = "2023"; + +package cel.types; + +import "cel/expr/checked.proto"; +import "cel/expr/syntax.proto"; + +option cc_enable_arenas = true; + +// Represents a collection of CEL type definitions. +message TypeDefSet { + repeated TypeDef types = 1; +} + +// Represents a single CEL type definition (either an object or an enum). +message TypeDef { + oneof kind { + Object object_type = 1; + Enum enum_type = 2; + } +} + +// Represents a structured object type with typed fields. +message Object { + message Field { + string name = 1; + cel.expr.Type type = 2; + string doc = 3; + cel.expr.Constant default_value = 4; + + extensions 1000 to max; + } + + string name = 1; + string doc = 2; + repeated Field fields = 3; + + extensions 1000 to max; +} + +// Represents a named enumeration type. +message Enum { + message EnumConstant { + string name = 1; + int32 id = 2; + string doc = 3; + + extensions 1000 to max; + } + + string name = 1; + string doc = 2; + repeated EnumConstant constants = 3; + + extensions 1000 to max; +} diff --git a/common/typedef/typedef_proto.cc b/common/typedef/typedef_proto.cc new file mode 100644 index 000000000..d06ab363d --- /dev/null +++ b/common/typedef/typedef_proto.cc @@ -0,0 +1,726 @@ +// Copyright 2026 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 "common/typedef/typedef_proto.h" + +#include +#include +#include +#include +#include + +#include "cel/expr/checked.pb.h" +#include "google/protobuf/struct.pb.h" +#include "absl/algorithm/container.h" +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "absl/types/variant.h" +#include "common/ast.h" +#include "common/ast/constant_proto.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_proto.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef.pb.h" +#include "internal/status_macros.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/message.h" + +namespace cel { +namespace { + +using TypePb = cel::expr::Type; + +absl::StatusOr TypeSpecFromProto(const TypePb& type); + +absl::StatusOr PrimitiveFromProto( + TypePb::PrimitiveType primitive_type) { + switch (primitive_type) { + case TypePb::PRIMITIVE_TYPE_UNSPECIFIED: + return PrimitiveType::kPrimitiveTypeUnspecified; + case TypePb::BOOL: + return PrimitiveType::kBool; + case TypePb::INT64: + return PrimitiveType::kInt64; + case TypePb::UINT64: + return PrimitiveType::kUint64; + case TypePb::DOUBLE: + return PrimitiveType::kDouble; + case TypePb::STRING: + return PrimitiveType::kString; + case TypePb::BYTES: + return PrimitiveType::kBytes; + default: + return absl::InvalidArgumentError( + "Illegal type specified for cel::expr::Type::PrimitiveType."); + } +} + +absl::StatusOr WellKnownFromProto( + TypePb::WellKnownType well_known_type) { + switch (well_known_type) { + case TypePb::WELL_KNOWN_TYPE_UNSPECIFIED: + return WellKnownTypeSpec::kWellKnownTypeUnspecified; + case TypePb::ANY: + return WellKnownTypeSpec::kAny; + case TypePb::TIMESTAMP: + return WellKnownTypeSpec::kTimestamp; + case TypePb::DURATION: + return WellKnownTypeSpec::kDuration; + default: + return absl::InvalidArgumentError( + "Illegal type specified for cel::expr::Type::WellKnownType."); + } +} + +absl::StatusOr ListTypeFromProto( + const TypePb::ListType& list_type) { + CEL_ASSIGN_OR_RETURN(TypeSpec elem_type, + TypeSpecFromProto(list_type.elem_type())); + return ListTypeSpec(std::make_unique(std::move(elem_type))); +} + +absl::StatusOr MapTypeFromProto(const TypePb::MapType& map_type) { + CEL_ASSIGN_OR_RETURN(TypeSpec key_type, + TypeSpecFromProto(map_type.key_type())); + CEL_ASSIGN_OR_RETURN(TypeSpec value_type, + TypeSpecFromProto(map_type.value_type())); + return MapTypeSpec(std::make_unique(std::move(key_type)), + std::make_unique(std::move(value_type))); +} + +absl::StatusOr FunctionTypeFromProto( + const TypePb::FunctionType& function_type) { + std::vector arg_types; + arg_types.reserve(function_type.arg_types_size()); + for (const auto& arg_type : function_type.arg_types()) { + CEL_ASSIGN_OR_RETURN(TypeSpec native_arg, TypeSpecFromProto(arg_type)); + arg_types.push_back(std::move(native_arg)); + } + CEL_ASSIGN_OR_RETURN(TypeSpec native_result, + TypeSpecFromProto(function_type.result_type())); + return FunctionTypeSpec(std::make_unique(std::move(native_result)), + std::move(arg_types)); +} + +absl::StatusOr AbstractTypeFromProto( + const TypePb::AbstractType& abstract_type) { + std::vector parameter_types; + parameter_types.reserve(abstract_type.parameter_types_size()); + for (const auto& parameter_type : abstract_type.parameter_types()) { + CEL_ASSIGN_OR_RETURN(TypeSpec native_param, + TypeSpecFromProto(parameter_type)); + parameter_types.push_back(std::move(native_param)); + } + return AbstractType(abstract_type.name(), std::move(parameter_types)); +} + +absl::StatusOr TypeSpecFromProto(const TypePb& type) { + switch (type.type_kind_case()) { + case TypePb::kDyn: + return TypeSpec(DynTypeSpec()); + case TypePb::kNull: + return TypeSpec(NullTypeSpec()); + case TypePb::kPrimitive: { + CEL_ASSIGN_OR_RETURN(PrimitiveType p, + PrimitiveFromProto(type.primitive())); + return TypeSpec(p); + } + case TypePb::kWrapper: { + CEL_ASSIGN_OR_RETURN(PrimitiveType w, PrimitiveFromProto(type.wrapper())); + return TypeSpec(PrimitiveTypeWrapper(w)); + } + case TypePb::kWellKnown: { + CEL_ASSIGN_OR_RETURN(WellKnownTypeSpec wk, + WellKnownFromProto(type.well_known())); + return TypeSpec(wk); + } + case TypePb::kListType: { + CEL_ASSIGN_OR_RETURN(ListTypeSpec lt, + ListTypeFromProto(type.list_type())); + return TypeSpec(std::move(lt)); + } + case TypePb::kMapType: { + CEL_ASSIGN_OR_RETURN(MapTypeSpec mt, MapTypeFromProto(type.map_type())); + return TypeSpec(std::move(mt)); + } + case TypePb::kFunction: { + CEL_ASSIGN_OR_RETURN(FunctionTypeSpec ft, + FunctionTypeFromProto(type.function())); + return TypeSpec(std::move(ft)); + } + case TypePb::kMessageType: + return TypeSpec(MessageTypeSpec(type.message_type())); + case TypePb::kTypeParam: + return TypeSpec(ParamTypeSpec(type.type_param())); + case TypePb::kType: { + CEL_ASSIGN_OR_RETURN(TypeSpec inner, TypeSpecFromProto(type.type())); + return TypeSpec(std::make_unique(std::move(inner))); + } + case TypePb::kError: + return TypeSpec(ErrorTypeSpec::kValue); + case TypePb::kAbstractType: { + CEL_ASSIGN_OR_RETURN(AbstractType at, + AbstractTypeFromProto(type.abstract_type())); + return TypeSpec(std::move(at)); + } + case TypePb::TYPE_KIND_NOT_SET: + return TypeSpec(UnsetTypeSpec()); + default: + return absl::InvalidArgumentError( + "Illegal type specified for cel::expr::Type."); + } +} + +absl::Status TypeSpecToProto(const TypeSpec& type, TypePb* absl_nonnull result); + +struct TypeSpecKindToProtoVisitor { + absl::Status operator()(PrimitiveType primitive) const { + switch (primitive) { + case PrimitiveType::kPrimitiveTypeUnspecified: + result->set_primitive(TypePb::PRIMITIVE_TYPE_UNSPECIFIED); + return absl::OkStatus(); + case PrimitiveType::kBool: + result->set_primitive(TypePb::BOOL); + return absl::OkStatus(); + case PrimitiveType::kInt64: + result->set_primitive(TypePb::INT64); + return absl::OkStatus(); + case PrimitiveType::kUint64: + result->set_primitive(TypePb::UINT64); + return absl::OkStatus(); + case PrimitiveType::kDouble: + result->set_primitive(TypePb::DOUBLE); + return absl::OkStatus(); + case PrimitiveType::kString: + result->set_primitive(TypePb::STRING); + return absl::OkStatus(); + case PrimitiveType::kBytes: + result->set_primitive(TypePb::BYTES); + return absl::OkStatus(); + default: + return absl::InvalidArgumentError("Unsupported primitive type"); + } + } + + absl::Status operator()(PrimitiveTypeWrapper wrapper) const { + CEL_RETURN_IF_ERROR((*this)(wrapper.type())); + auto wrapped = result->primitive(); + result->set_wrapper(wrapped); + return absl::OkStatus(); + } + + absl::Status operator()(UnsetTypeSpec) const { + result->clear_type_kind(); + return absl::OkStatus(); + } + + absl::Status operator()(DynTypeSpec) const { + result->mutable_dyn(); + return absl::OkStatus(); + } + + absl::Status operator()(ErrorTypeSpec) const { + result->mutable_error(); + return absl::OkStatus(); + } + + absl::Status operator()(NullTypeSpec) const { + result->set_null(google::protobuf::NULL_VALUE); + return absl::OkStatus(); + } + + absl::Status operator()(const ListTypeSpec& list_type) const { + return TypeSpecToProto(list_type.elem_type(), + result->mutable_list_type()->mutable_elem_type()); + } + + absl::Status operator()(const MapTypeSpec& map_type) const { + CEL_RETURN_IF_ERROR(TypeSpecToProto( + map_type.key_type(), result->mutable_map_type()->mutable_key_type())); + return TypeSpecToProto(map_type.value_type(), + result->mutable_map_type()->mutable_value_type()); + } + + absl::Status operator()(const MessageTypeSpec& message_type) const { + result->set_message_type(message_type.type()); + return absl::OkStatus(); + } + + absl::Status operator()(const WellKnownTypeSpec& well_known_type) const { + switch (well_known_type) { + case WellKnownTypeSpec::kWellKnownTypeUnspecified: + result->set_well_known(TypePb::WELL_KNOWN_TYPE_UNSPECIFIED); + return absl::OkStatus(); + case WellKnownTypeSpec::kAny: + result->set_well_known(TypePb::ANY); + return absl::OkStatus(); + case WellKnownTypeSpec::kDuration: + result->set_well_known(TypePb::DURATION); + return absl::OkStatus(); + case WellKnownTypeSpec::kTimestamp: + result->set_well_known(TypePb::TIMESTAMP); + return absl::OkStatus(); + default: + return absl::InvalidArgumentError("Unsupported well-known type"); + } + } + + absl::Status operator()(const FunctionTypeSpec& function_type) const { + CEL_RETURN_IF_ERROR( + TypeSpecToProto(function_type.result_type(), + result->mutable_function()->mutable_result_type())); + for (const TypeSpec& arg_type : function_type.arg_types()) { + CEL_RETURN_IF_ERROR(TypeSpecToProto( + arg_type, result->mutable_function()->add_arg_types())); + } + return absl::OkStatus(); + } + + absl::Status operator()(const AbstractType& type) const { + auto* abstract_type_pb = result->mutable_abstract_type(); + abstract_type_pb->set_name(type.name()); + for (const TypeSpec& type_param : type.parameter_types()) { + CEL_RETURN_IF_ERROR( + TypeSpecToProto(type_param, abstract_type_pb->add_parameter_types())); + } + return absl::OkStatus(); + } + + absl::Status operator()(const std::unique_ptr& type_type) const { + return TypeSpecToProto((type_type != nullptr) ? *type_type : TypeSpec(), + result->mutable_type()); + } + + absl::Status operator()(const ParamTypeSpec& param_type) const { + result->set_type_param(param_type.type()); + return absl::OkStatus(); + } + + TypePb* absl_nonnull result; +}; + +absl::Status TypeSpecToProto(const TypeSpec& type, + TypePb* absl_nonnull result) { + return absl::visit(TypeSpecKindToProtoVisitor{result}, type.type_kind()); +} + +size_t CountExtensions(const google::protobuf::Message& message) { + std::vector fields; + message.GetReflection()->ListFields(message, &fields); + size_t extension_count = 0; + for (const google::protobuf::FieldDescriptor* field : fields) { + if (field->is_extension()) { + ++extension_count; + } + } + return extension_count; +} + +std::vector SortedRegistrySchemas( + const SchemaProtoRegistry& registry) { + std::vector schemas; + schemas.reserve(registry.schemas().size()); + for (const auto& [_, schema] : registry.schemas()) { + if (schema != nullptr) { + schemas.push_back(schema.get()); + } + } + absl::c_sort(schemas, [](const SchemaProto* absl_nonnull lhs, + const SchemaProto* absl_nonnull rhs) { + return lhs->name() < rhs->name(); + }); + return schemas; +} + +template +std::vector SortedPropertyKeys(const PropertiesMap& properties) { + std::vector keys; + keys.reserve(properties.size()); + for (const auto& [key, props] : properties) { + if (props != nullptr) { + keys.push_back(key); + } + } + absl::c_sort(keys); + return keys; +} + +absl::StatusOr +DecodeObjectSchemaSpecificProperties(const types::Object& proto, + const SchemaProtoRegistry& registry) { + const size_t extension_count = CountExtensions(proto); + ObjectSchemaSpecificProperties result; + for (const SchemaProto* absl_nonnull schema : + SortedRegistrySchemas(registry)) { + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeObjectProperties(proto)); + if (props != nullptr) { + result[schema->name()] = std::move(props); + } + } + if (result.size() < extension_count) { + return absl::InvalidArgumentError( + "Unsupported schema extension on cel.types.Object"); + } + return result; +} + +absl::StatusOr +DecodeFieldSchemaSpecificProperties(const types::Object::Field& proto, + const SchemaProtoRegistry& registry) { + const size_t extension_count = CountExtensions(proto); + FieldSchemaSpecificProperties result; + for (const SchemaProto* absl_nonnull schema : + SortedRegistrySchemas(registry)) { + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeFieldProperties(proto)); + if (props != nullptr) { + result[schema->name()] = std::move(props); + } + } + if (result.size() < extension_count) { + return absl::InvalidArgumentError( + "Unsupported schema extension on cel.types.Object.Field"); + } + return result; +} + +absl::StatusOr DecodeEnumSchemaSpecificProperties( + const types::Enum& proto, const SchemaProtoRegistry& registry) { + const size_t extension_count = CountExtensions(proto); + EnumSchemaSpecificProperties result; + for (const SchemaProto* absl_nonnull schema : + SortedRegistrySchemas(registry)) { + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeEnumProperties(proto)); + if (props != nullptr) { + result[schema->name()] = std::move(props); + } + } + if (result.size() < extension_count) { + return absl::InvalidArgumentError( + "Unsupported schema extension on cel.types.Enum"); + } + return result; +} + +absl::StatusOr +DecodeEnumConstantSchemaSpecificProperties( + const types::Enum::EnumConstant& proto, + const SchemaProtoRegistry& registry) { + const size_t extension_count = CountExtensions(proto); + EnumConstantSchemaSpecificProperties result; + for (const SchemaProto* absl_nonnull schema : + SortedRegistrySchemas(registry)) { + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeEnumConstantProperties(proto)); + if (props != nullptr) { + result[schema->name()] = std::move(props); + } + } + if (result.size() < extension_count) { + return absl::InvalidArgumentError( + "Unsupported schema extension on cel.types.Enum.EnumConstant"); + } + return result; +} + +absl::Status EncodeObjectSchemaSpecificProperties( + const ObjectSchemaSpecificProperties& properties, + types::Object* absl_nonnull out, const SchemaProtoRegistry& registry) { + for (const std::string& schema_name : SortedPropertyKeys(properties)) { + const SchemaProto* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_RETURN_IF_ERROR( + schema->EncodeObjectProperties(*properties.at(schema_name), out)); + } + return absl::OkStatus(); +} + +absl::Status EncodeFieldSchemaSpecificProperties( + const FieldSchemaSpecificProperties& properties, + types::Object::Field* absl_nonnull out, + const SchemaProtoRegistry& registry) { + for (const std::string& schema_name : SortedPropertyKeys(properties)) { + const SchemaProto* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_RETURN_IF_ERROR( + schema->EncodeFieldProperties(*properties.at(schema_name), out)); + } + return absl::OkStatus(); +} + +absl::Status EncodeEnumSchemaSpecificProperties( + const EnumSchemaSpecificProperties& properties, + types::Enum* absl_nonnull out, const SchemaProtoRegistry& registry) { + for (const std::string& schema_name : SortedPropertyKeys(properties)) { + const SchemaProto* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_RETURN_IF_ERROR( + schema->EncodeEnumProperties(*properties.at(schema_name), out)); + } + return absl::OkStatus(); +} + +absl::Status EncodeEnumConstantSchemaSpecificProperties( + const EnumConstantSchemaSpecificProperties& properties, + types::Enum::EnumConstant* absl_nonnull out, + const SchemaProtoRegistry& registry) { + for (const std::string& schema_name : SortedPropertyKeys(properties)) { + const SchemaProto* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_RETURN_IF_ERROR( + schema->EncodeEnumConstantProperties(*properties.at(schema_name), out)); + } + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr ObjectTypeDefFromProto( + const types::Object& proto, const SchemaProtoRegistry& registry) { + ObjectTypeDef object_def; + object_def.name = proto.name(); + object_def.doc = proto.doc(); + CEL_ASSIGN_OR_RETURN(object_def.schema_specific_properties, + DecodeObjectSchemaSpecificProperties(proto, registry)); + + for (const types::Object::Field& field_proto : proto.fields()) { + ObjectTypeDef::Field field; + field.name = field_proto.name(); + if (field_proto.has_type()) { + CEL_ASSIGN_OR_RETURN(field.type, TypeSpecFromProto(field_proto.type())); + } + field.doc = field_proto.doc(); + if (field_proto.has_default_value()) { + CEL_RETURN_IF_ERROR(ast_internal::ConstantFromProto( + field_proto.default_value(), field.default_value)); + } + CEL_ASSIGN_OR_RETURN( + field.schema_specific_properties, + DecodeFieldSchemaSpecificProperties(field_proto, registry)); + CEL_RETURN_IF_ERROR(object_def.AddField(std::move(field))); + } + + return object_def; +} + +absl::Status ObjectTypeDefToProto(const ObjectTypeDef& object_def, + types::Object* absl_nonnull out, + const SchemaProtoRegistry& registry) { + out->Clear(); + if (!object_def.name.empty()) { + out->set_name(object_def.name); + } + if (!object_def.doc.empty()) { + out->set_doc(object_def.doc); + } + CEL_RETURN_IF_ERROR(EncodeObjectSchemaSpecificProperties( + object_def.schema_specific_properties, out, registry)); + + out->mutable_fields()->Reserve(object_def.fields.size()); + for (const ObjectTypeDef::Field& field : object_def.fields) { + types::Object::Field* field_proto = out->add_fields(); + if (!field.name.empty()) { + field_proto->set_name(field.name); + } + if (field.type.is_specified()) { + CEL_RETURN_IF_ERROR( + TypeSpecToProto(field.type, field_proto->mutable_type())); + } + if (!field.doc.empty()) { + field_proto->set_doc(field.doc); + } + if (field.default_value.has_value()) { + CEL_RETURN_IF_ERROR(ast_internal::ConstantToProto( + field.default_value, field_proto->mutable_default_value())); + } + CEL_RETURN_IF_ERROR(EncodeFieldSchemaSpecificProperties( + field.schema_specific_properties, field_proto, registry)); + } + return absl::OkStatus(); +} + +absl::StatusOr ObjectTypeDefToProto( + const ObjectTypeDef& object_def, const SchemaProtoRegistry& registry) { + types::Object out; + CEL_RETURN_IF_ERROR(ObjectTypeDefToProto(object_def, &out, registry)); + return out; +} + +absl::StatusOr EnumTypeDefFromProto( + const types::Enum& proto, const SchemaProtoRegistry& registry) { + EnumTypeDef enum_def; + enum_def.name = proto.name(); + enum_def.doc = proto.doc(); + CEL_ASSIGN_OR_RETURN(enum_def.schema_specific_properties, + DecodeEnumSchemaSpecificProperties(proto, registry)); + + for (const types::Enum::EnumConstant& constant_proto : proto.constants()) { + EnumTypeDef::EnumConstant constant; + constant.name = constant_proto.name(); + constant.id = constant_proto.id(); + constant.doc = constant_proto.doc(); + CEL_ASSIGN_OR_RETURN( + constant.schema_specific_properties, + DecodeEnumConstantSchemaSpecificProperties(constant_proto, registry)); + CEL_RETURN_IF_ERROR(enum_def.AddConstant(std::move(constant))); + } + + return enum_def; +} + +absl::Status EnumTypeDefToProto(const EnumTypeDef& enum_def, + types::Enum* absl_nonnull out, + const SchemaProtoRegistry& registry) { + out->Clear(); + if (!enum_def.name.empty()) { + out->set_name(enum_def.name); + } + if (!enum_def.doc.empty()) { + out->set_doc(enum_def.doc); + } + CEL_RETURN_IF_ERROR(EncodeEnumSchemaSpecificProperties( + enum_def.schema_specific_properties, out, registry)); + + out->mutable_constants()->Reserve(enum_def.constants.size()); + for (const EnumTypeDef::EnumConstant& constant : enum_def.constants) { + types::Enum::EnumConstant* constant_proto = out->add_constants(); + if (!constant.name.empty()) { + constant_proto->set_name(constant.name); + } + constant_proto->set_id(constant.id); + if (!constant.doc.empty()) { + constant_proto->set_doc(constant.doc); + } + CEL_RETURN_IF_ERROR(EncodeEnumConstantSchemaSpecificProperties( + constant.schema_specific_properties, constant_proto, registry)); + } + return absl::OkStatus(); +} + +absl::StatusOr EnumTypeDefToProto( + const EnumTypeDef& enum_def, const SchemaProtoRegistry& registry) { + types::Enum out; + CEL_RETURN_IF_ERROR(EnumTypeDefToProto(enum_def, &out, registry)); + return out; +} + +absl::StatusOr TypeDefFromProto(const types::TypeDef& proto, + const SchemaProtoRegistry& registry) { + switch (proto.kind_case()) { + case types::TypeDef::kObjectType: { + CEL_ASSIGN_OR_RETURN( + ObjectTypeDef obj, + ObjectTypeDefFromProto(proto.object_type(), registry)); + return TypeDef(std::move(obj)); + } + case types::TypeDef::kEnumType: { + CEL_ASSIGN_OR_RETURN(EnumTypeDef e, + EnumTypeDefFromProto(proto.enum_type(), registry)); + return TypeDef(std::move(e)); + } + case types::TypeDef::KIND_NOT_SET: + return absl::InvalidArgumentError( + "cel.types.TypeDef must specify 'object_type' or 'enum_type'"); + } + return absl::InvalidArgumentError( + "cel.types.TypeDef must specify 'object_type' or 'enum_type'"); +} + +absl::Status TypeDefToProto(const TypeDef& type_def, + types::TypeDef* absl_nonnull out, + const SchemaProtoRegistry& registry) { + out->Clear(); + if (type_def.is_object()) { + return ObjectTypeDefToProto(type_def.object_type(), + out->mutable_object_type(), registry); + } + return EnumTypeDefToProto(type_def.enum_type(), out->mutable_enum_type(), + registry); +} + +absl::StatusOr TypeDefToProto( + const TypeDef& type_def, const SchemaProtoRegistry& registry) { + types::TypeDef out; + CEL_RETURN_IF_ERROR(TypeDefToProto(type_def, &out, registry)); + return out; +} + +absl::StatusOr> TypeDefsFromProto( + const types::TypeDefSet& proto, const SchemaProtoRegistry& registry) { + std::vector result; + result.reserve(proto.types_size()); + for (const types::TypeDef& item : proto.types()) { + CEL_ASSIGN_OR_RETURN(TypeDef type_def, TypeDefFromProto(item, registry)); + result.push_back(std::move(type_def)); + } + return result; +} + +absl::Status TypeDefsToProto(absl::Span type_defs, + types::TypeDefSet* absl_nonnull out, + const SchemaProtoRegistry& registry) { + out->Clear(); + out->mutable_types()->Reserve(type_defs.size()); + for (const TypeDef& type_def : type_defs) { + CEL_RETURN_IF_ERROR(TypeDefToProto(type_def, out->add_types(), registry)); + } + return absl::OkStatus(); +} + +absl::StatusOr TypeDefsToProto( + absl::Span type_defs, const SchemaProtoRegistry& registry) { + types::TypeDefSet out; + CEL_RETURN_IF_ERROR(TypeDefsToProto(type_defs, &out, registry)); + return out; +} + +absl::StatusOr TypeDefSetFromProto( + const types::TypeDefSet& proto, const SchemaProtoRegistry& registry) { + TypeDefSet result; + for (const types::TypeDef& item : proto.types()) { + CEL_ASSIGN_OR_RETURN(TypeDef type_def, TypeDefFromProto(item, registry)); + CEL_RETURN_IF_ERROR(result.AddTypeDef(std::move(type_def))); + } + return result; +} + +absl::Status TypeDefSetToProto(const TypeDefSet& type_def_set, + types::TypeDefSet* absl_nonnull out, + const SchemaProtoRegistry& registry) { + return TypeDefsToProto(type_def_set.types(), out, registry); +} + +absl::StatusOr TypeDefSetToProto( + const TypeDefSet& type_def_set, const SchemaProtoRegistry& registry) { + return TypeDefsToProto(type_def_set.types(), registry); +} + +} // namespace cel diff --git a/common/typedef/typedef_proto.h b/common/typedef/typedef_proto.h new file mode 100644 index 000000000..c8520cc2c --- /dev/null +++ b/common/typedef/typedef_proto.h @@ -0,0 +1,121 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_PROTO_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_PROTO_H_ + +#include + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/types/span.h" +#include "common/typedef/schema_proto.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +// Converts a cel.types.Object proto message to a native ObjectTypeDef. +// +// Schema-specific extension properties on the object and its fields are decoded +// by delegating to the corresponding `SchemaProto` codecs in `registry`. +absl::StatusOr ObjectTypeDefFromProto( + const types::Object& proto, const SchemaProtoRegistry& registry = {}); + +// Converts a native ObjectTypeDef to a cel.types.Object proto message. +absl::Status ObjectTypeDefToProto(const ObjectTypeDef& object_def, + types::Object* absl_nonnull out, + const SchemaProtoRegistry& registry = {}); + +absl::StatusOr ObjectTypeDefToProto( + const ObjectTypeDef& object_def, const SchemaProtoRegistry& registry = {}); + +// Converts a cel.types.Enum proto message to a native EnumTypeDef. +// +// Schema-specific extension properties on the enum and its constants are +// decoded by delegating to the corresponding `SchemaProto` codecs in +// `registry`. +absl::StatusOr EnumTypeDefFromProto( + const types::Enum& proto, const SchemaProtoRegistry& registry = {}); + +// Converts a native EnumTypeDef to a cel.types.Enum proto message. +absl::Status EnumTypeDefToProto(const EnumTypeDef& enum_def, + types::Enum* absl_nonnull out, + const SchemaProtoRegistry& registry = {}); + +absl::StatusOr EnumTypeDefToProto( + const EnumTypeDef& enum_def, const SchemaProtoRegistry& registry = {}); + +// Converts a cel.types.TypeDef proto message to a native TypeDef. +// +// Schema-specific extension properties on object, field, enum, and enum +// constant messages are decoded by delegating to the corresponding +// `SchemaProto` codecs in `registry`. +absl::StatusOr TypeDefFromProto( + const types::TypeDef& proto, const SchemaProtoRegistry& registry = {}); + +// Converts a native TypeDef to a cel.types.TypeDef proto message. +// +// Schema-specific properties are encoded as proto extensions by delegating to +// the corresponding `SchemaProto` codecs looked up in `registry`. +absl::Status TypeDefToProto(const TypeDef& type_def, + types::TypeDef* absl_nonnull out, + const SchemaProtoRegistry& registry = {}); + +absl::StatusOr TypeDefToProto( + const TypeDef& type_def, const SchemaProtoRegistry& registry = {}); + +// Converts a cel.types.TypeDefSet proto message to a sequence of native +// TypeDefs. +// +// Schema-specific extension properties are decoded by delegating to the +// corresponding `SchemaProto` codecs in `registry`. +absl::StatusOr> TypeDefsFromProto( + const types::TypeDefSet& proto, const SchemaProtoRegistry& registry = {}); + +// Converts a sequence of native TypeDefs to a cel.types.TypeDefSet proto +// message. +// +// Schema-specific properties are encoded as proto extensions by delegating to +// the corresponding `SchemaProto` codecs looked up in `registry`. +absl::Status TypeDefsToProto(absl::Span type_defs, + types::TypeDefSet* absl_nonnull out, + const SchemaProtoRegistry& registry = {}); + +absl::StatusOr TypeDefsToProto( + absl::Span type_defs, + const SchemaProtoRegistry& registry = {}); + +// Converts a cel.types.TypeDefSet proto message to a native TypeDefSet. +// +// Schema-specific extension properties are decoded by delegating to the +// corresponding `SchemaProto` codecs in `registry`. +absl::StatusOr TypeDefSetFromProto( + const types::TypeDefSet& proto, const SchemaProtoRegistry& registry = {}); + +// Converts a native TypeDefSet to a cel.types.TypeDefSet proto message. +// +// Schema-specific properties are encoded as proto extensions by delegating to +// the corresponding `SchemaProto` codecs looked up in `registry`. +absl::Status TypeDefSetToProto(const TypeDefSet& type_def_set, + types::TypeDefSet* absl_nonnull out, + const SchemaProtoRegistry& registry = {}); + +absl::StatusOr TypeDefSetToProto( + const TypeDefSet& type_def_set, const SchemaProtoRegistry& registry = {}); + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_PROTO_H_ diff --git a/common/typedef/typedef_proto_test.cc b/common/typedef/typedef_proto_test.cc new file mode 100644 index 000000000..642288286 --- /dev/null +++ b/common/typedef/typedef_proto_test.cc @@ -0,0 +1,354 @@ +// Copyright 2026 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 "common/typedef/typedef_proto.h" + +#include +#include +#include +#include +#include + +#include "absl/log/absl_log.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/ascii.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "common/ast.h" +#include "common/typedef/json_schema.h" +#include "common/typedef/json_schema.pb.h" +#include "common/typedef/json_schema_proto.h" +#include "common/typedef/json_schema_yaml.h" +#include "common/typedef/proto_schema_proto.h" +#include "common/typedef/proto_schema_yaml.h" +#include "common/typedef/schema_proto.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef.pb.h" +#include "common/typedef/typedef_yaml.h" +#include "internal/runfiles.h" +#include "internal/testing.h" +#include "google/protobuf/text_format.h" +#include "google/protobuf/unknown_field_set.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::SizeIs; + +constexpr absl::string_view kTestTypeDefFilePath = +"_main/common/typedef/testdata/"; + +constexpr absl::string_view kBaselineSeparator = + "--------------------------------------------------------------------\n"; + +SchemaYamlRegistry MakeYamlRegistry() { + return SchemaYamlRegistry(std::make_unique(), + std::make_unique(), + std::make_unique()); +} + +SchemaProtoRegistry MakeProtoRegistry() { + return SchemaProtoRegistry(std::make_unique(), + std::make_unique(), + std::make_unique()); +} + +struct YamlToProtoTestCase { + std::string yaml_source_file; + std::string baseline_file; +}; + +using YamlToProtoTest = testing::TestWithParam; + +TEST_P(YamlToProtoTest, Convert) { + std::string contents; + std::string test_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, GetParam().yaml_source_file)); + ASSERT_THAT(cel::internal::GetFileContents(test_file, &contents), IsOk()); + + std::string baseline; + std::string baseline_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, GetParam().baseline_file)); + ASSERT_THAT(cel::internal::GetFileContents(baseline_file, &baseline), IsOk()); + baseline = absl::StripAsciiWhitespace(baseline); + + SchemaYamlRegistry yaml_registry = MakeYamlRegistry(); + SchemaProtoRegistry proto_registry = MakeProtoRegistry(); + + ASSERT_OK_AND_ASSIGN(std::vector type_defs, + TypeDefsFromYaml(contents, yaml_registry)); + ASSERT_OK_AND_ASSIGN(types::TypeDefSet proto_set, + TypeDefsToProto(type_defs, proto_registry)); + + std::string textproto; + ASSERT_TRUE(google::protobuf::TextFormat::PrintToString(proto_set, &textproto)); + + std::ostringstream out; + out << "YAML SOURCE: " << GetParam().yaml_source_file << "\n"; + out << kBaselineSeparator; + out << "TYPEDEF PROTO:\n"; + out << textproto; + + std::string actual(absl::StripAsciiWhitespace(out.str())); + if (actual != baseline) { + // Log the actual result to make it easier to copy/paste into the baseline + // file when updating the tests. + ABSL_LOG(INFO) << "Actual:\n" << actual; + EXPECT_EQ(actual, baseline); + } +} + +INSTANTIATE_TEST_SUITE_P( + Formats, YamlToProtoTest, + testing::ValuesIn({ + YamlToProtoTestCase{ + .yaml_source_file = "typedefs.yaml", + .baseline_file = "typedefs_yaml_to_proto.baseline", + }, + })); + +struct ProtoToYamlTestCase { + std::string proto_source_file; + std::string baseline_file; +}; + +using ProtoToYamlTest = testing::TestWithParam; + +TEST_P(ProtoToYamlTest, Convert) { + std::string contents; + std::string test_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, GetParam().proto_source_file)); + ASSERT_THAT(cel::internal::GetFileContents(test_file, &contents), IsOk()); + + std::string baseline; + std::string baseline_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, GetParam().baseline_file)); + ASSERT_THAT(cel::internal::GetFileContents(baseline_file, &baseline), IsOk()); + baseline = absl::StripAsciiWhitespace(baseline); + + SchemaYamlRegistry yaml_registry = MakeYamlRegistry(); + SchemaProtoRegistry proto_registry = MakeProtoRegistry(); + + types::TypeDefSet proto_set; + ASSERT_TRUE(google::protobuf::TextFormat::ParseFromString(contents, &proto_set)); + ASSERT_OK_AND_ASSIGN(std::vector type_defs, + TypeDefsFromProto(proto_set, proto_registry)); + + std::ostringstream out; + out << "PROTO SOURCE: " << GetParam().proto_source_file << "\n"; + out << kBaselineSeparator; + out << "TYPEDEF YAML:\n"; + ASSERT_THAT(TypeDefsToYaml(type_defs, yaml_registry, out), IsOk()); + + std::string actual(absl::StripAsciiWhitespace(out.str())); + if (actual != baseline) { + // Log the actual result to make it easier to copy/paste into the baseline + // file when updating the tests. + ABSL_LOG(INFO) << "Actual:\n" << actual; + EXPECT_EQ(actual, baseline); + } +} + +INSTANTIATE_TEST_SUITE_P( + Formats, ProtoToYamlTest, + testing::ValuesIn({ + ProtoToYamlTestCase{ + .proto_source_file = "typedefs.txtproto", + .baseline_file = "typedefs_proto_to_yaml.baseline", + }, + })); + +TEST(TypeDefProtoTest, DirectObjectAndEnumOverloads) { + ObjectTypeDef obj; + obj.name = "com.example.DirectObj"; + types::Object obj_proto; + ASSERT_THAT(ObjectTypeDefToProto(obj, &obj_proto), IsOk()); + ASSERT_OK_AND_ASSIGN(ObjectTypeDef decoded_obj, + ObjectTypeDefFromProto(obj_proto)); + EXPECT_EQ(decoded_obj.name, "com.example.DirectObj"); + + EnumTypeDef enm; + enm.name = "com.example.DirectEnum"; + ASSERT_THAT(enm.AddConstant(EnumTypeDef::EnumConstant{ + .name = "NEG_ONE", + .id = -1, + }), + IsOk()); + ASSERT_OK_AND_ASSIGN(types::Enum enum_proto, EnumTypeDefToProto(enm)); + ASSERT_OK_AND_ASSIGN(EnumTypeDef decoded_enum, + EnumTypeDefFromProto(enum_proto)); + EXPECT_EQ(decoded_enum.name, "com.example.DirectEnum"); + ASSERT_THAT(decoded_enum.constants, SizeIs(1)); + EXPECT_EQ(decoded_enum.constants[0].name, "NEG_ONE"); + EXPECT_EQ(decoded_enum.constants[0].id, -1); +} + +TEST(TypeDefProtoTest, TypeDefSetRoundTripAndDuplicateError) { + TypeDefSet set; + ObjectTypeDef obj; + obj.name = "com.example.Obj"; + ASSERT_THAT(set.AddTypeDef(TypeDef(std::move(obj))), IsOk()); + EnumTypeDef enm; + enm.name = "com.example.Enum"; + ASSERT_THAT(set.AddTypeDef(TypeDef(std::move(enm))), IsOk()); + + ASSERT_OK_AND_ASSIGN(types::TypeDefSet proto_set, TypeDefSetToProto(set)); + types::TypeDefSet proto_set_out; + ASSERT_THAT(TypeDefSetToProto(set, &proto_set_out), IsOk()); + EXPECT_EQ(proto_set_out.types_size(), 2); + + ASSERT_OK_AND_ASSIGN(TypeDefSet decoded, TypeDefSetFromProto(proto_set)); + EXPECT_EQ(decoded.types().size(), 2); + EXPECT_NE(decoded.FindTypeDef("com.example.Obj"), nullptr); + EXPECT_NE(decoded.FindTypeDef("com.example.Enum"), nullptr); + + // Duplicate type name in proto is rejected by TypeDefSetFromProto. + proto_set.add_types()->mutable_object_type()->set_name("com.example.Obj"); + EXPECT_THAT( + TypeDefSetFromProto(proto_set), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Type 'com.example.Obj' is already defined."))); +} + +TEST(TypeDefProtoTest, ValidationAndErrorCases) { + // Empty cel.types.TypeDef (neither object_type nor enum_type set). + types::TypeDef empty_td_proto; + EXPECT_THAT(TypeDefFromProto(empty_td_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("cel.types.TypeDef must specify " + "'object_type' or 'enum_type'"))); + + // Duplicate field names on cel.types.Object. + types::Object dup_fields_proto; + dup_fields_proto.set_name("com.example.DupFields"); + dup_fields_proto.add_fields()->set_name("dup"); + dup_fields_proto.add_fields()->set_name("dup"); + EXPECT_THAT(ObjectTypeDefFromProto(dup_fields_proto), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Field 'dup' is already defined."))); + + // Duplicate enum constant names on cel.types.Enum. + types::Enum dup_constants_proto; + dup_constants_proto.set_name("com.example.DupConstants"); + dup_constants_proto.add_constants()->set_name("DUP_VAL"); + dup_constants_proto.add_constants()->set_name("DUP_VAL"); + EXPECT_THAT( + EnumTypeDefFromProto(dup_constants_proto), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Enum constant 'DUP_VAL' is already defined."))); + + // Unsupported schema during encoding (empty registry). + ObjectTypeDef obj_with_unregistered; + obj_with_unregistered.name = "com.example.Obj"; + obj_with_unregistered.schema_specific_properties["json"] = + std::make_unique(); + EXPECT_THAT(ObjectTypeDefToProto(obj_with_unregistered), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("Unsupported schema: 'json'"))); + + ObjectTypeDef obj_field_unregistered; + obj_field_unregistered.name = "com.example.Obj"; + ObjectTypeDef::Field f{.name = "f", .type = TypeSpec(PrimitiveType::kString)}; + f.schema_specific_properties["json"] = + std::make_unique(); + ASSERT_THAT(obj_field_unregistered.AddField(std::move(f)), IsOk()); + EXPECT_THAT(ObjectTypeDefToProto(obj_field_unregistered), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("Unsupported schema: 'json'"))); + + EnumTypeDef enum_with_unregistered; + enum_with_unregistered.name = "com.example.Enum"; + enum_with_unregistered.schema_specific_properties["json"] = + std::make_unique(); + EXPECT_THAT(EnumTypeDefToProto(enum_with_unregistered), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("Unsupported schema: 'json'"))); + + EnumTypeDef enum_const_unregistered; + enum_const_unregistered.name = "com.example.Enum"; + EnumTypeDef::EnumConstant ec{.name = "C", .id = 0}; + ec.schema_specific_properties["json"] = + std::make_unique(); + ASSERT_THAT(enum_const_unregistered.AddConstant(std::move(ec)), IsOk()); + EXPECT_THAT(EnumTypeDefToProto(enum_const_unregistered), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("Unsupported schema: 'json'"))); + + // Unregistered extension during decoding (empty registry). + types::Object obj_ext_proto; + obj_ext_proto.set_name("com.example.Obj"); + obj_ext_proto.MutableExtension(types::json_object_schema)->set_name("x"); + EXPECT_THAT( + ObjectTypeDefFromProto(obj_ext_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema extension on cel.types.Object"))); + + types::Object field_ext_proto; + field_ext_proto.set_name("com.example.Obj"); + auto* f_proto = field_ext_proto.add_fields(); + f_proto->set_name("f"); + f_proto->MutableExtension(types::json_field_schema)->set_name("x"); + EXPECT_THAT(ObjectTypeDefFromProto(field_ext_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema extension on " + "cel.types.Object.Field"))); + + types::Enum enum_ext_proto; + enum_ext_proto.set_name("com.example.Enum"); + enum_ext_proto.MutableExtension(types::json_enum_schema)->set_style("string"); + EXPECT_THAT( + EnumTypeDefFromProto(enum_ext_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema extension on cel.types.Enum"))); + + types::Enum const_ext_proto; + const_ext_proto.set_name("com.example.Enum"); + auto* c_proto = const_ext_proto.add_constants(); + c_proto->set_name("C"); + c_proto->MutableExtension(types::json_enum_constant_schema)->set_name("x"); + EXPECT_THAT(EnumTypeDefFromProto(const_ext_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema extension on " + "cel.types.Enum.EnumConstant"))); + + // Unknown fields on messages are ignored. + types::TypeDefSet unknown_set; + unknown_set.GetReflection() + ->MutableUnknownFields(&unknown_set) + ->AddVarint(9999, 1); + EXPECT_THAT(TypeDefsFromProto(unknown_set), IsOk()); + + types::TypeDef unknown_td; + unknown_td.mutable_object_type()->set_name("com.example.Obj"); + unknown_td.GetReflection() + ->MutableUnknownFields(&unknown_td) + ->AddVarint(9999, 1); + EXPECT_THAT(TypeDefFromProto(unknown_td), IsOk()); + + types::Object unknown_obj; + unknown_obj.set_name("com.example.Obj"); + unknown_obj.GetReflection() + ->MutableUnknownFields(&unknown_obj) + ->AddVarint(9999, 1); + EXPECT_THAT(ObjectTypeDefFromProto(unknown_obj), IsOk()); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/typedef_test.cc b/common/typedef/typedef_test.cc new file mode 100644 index 000000000..2f90410df --- /dev/null +++ b/common/typedef/typedef_test.cc @@ -0,0 +1,263 @@ +// Copyright 2026 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 "common/typedef/typedef.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "common/ast.h" +#include "common/constant.h" +#include "common/typedef/schema.h" +#include "internal/testing.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::IsNull; +using ::testing::NotNull; + +class TestObjectProperties final : public SchemaObjectProperties {}; +class TestFieldProperties final : public SchemaFieldProperties {}; +class TestEnumProperties final : public SchemaEnumProperties {}; +class TestEnumConstantProperties final : public SchemaEnumConstantProperties {}; + +TEST(TypeDefTest, DefaultTypeDefIsObject) { + TypeDef def; + EXPECT_TRUE(def.is_object()); + EXPECT_FALSE(def.is_enum()); + EXPECT_EQ(def.kind_case(), TypeDefKindCase::kObject); + EXPECT_THAT(def.name(), Eq("")); + EXPECT_THAT(def.doc(), Eq("")); + EXPECT_THAT(def.object_type().schema_specific_properties, IsEmpty()); + EXPECT_TRUE(def.object_type().fields.empty()); + EXPECT_THAT(def.object_type().FindField("id"), IsNull()); +} + +TEST(TypeDefTest, ObjectTypeDefConstructionAndLookup) { + ObjectTypeDef person; + person.name = "com.example.Person"; + person.doc = "User account profile entity."; + person.schema_specific_properties["json"] = + std::make_unique(); + + ASSERT_THAT(person.AddField(ObjectTypeDef::Field{ + .name = "id", + .type = TypeSpec(PrimitiveType::kInt64), + .doc = "Unique user identifier.", + }), + IsOk()); + + ObjectTypeDef::Field display_name_field{ + .name = "display_name", + .type = TypeSpec(PrimitiveType::kString), + .doc = "Display name.", + .default_value = Constant(StringConstant("Anonymous")), + }; + display_name_field.schema_specific_properties["json"] = + std::make_unique(); + display_name_field.schema_specific_properties["proto3"] = + std::make_unique(); + ASSERT_THAT(person.AddField(std::move(display_name_field)), IsOk()); + + ASSERT_THAT(person.AddField(ObjectTypeDef::Field{ + .name = "tags", + .type = TypeSpec(ListTypeSpec( + std::make_unique(PrimitiveType::kString))), + }), + IsOk()); + + EXPECT_THAT(person.AddField(ObjectTypeDef::Field{ + .name = "id", + .type = TypeSpec(PrimitiveType::kString), + }), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Field 'id' is already defined."))); + + const ObjectTypeDef::Field* id_field = person.FindField("id"); + ASSERT_THAT(id_field, NotNull()); + EXPECT_EQ(id_field->name, "id"); + EXPECT_EQ(id_field->type, TypeSpec(PrimitiveType::kInt64)); + EXPECT_EQ(id_field->doc, "Unique user identifier."); + EXPECT_FALSE(id_field->default_value.has_value()); + EXPECT_THAT(id_field->schema_specific_properties, IsEmpty()); + + const ObjectTypeDef::Field* name_field = person.FindField("display_name"); + ASSERT_THAT(name_field, NotNull()); + EXPECT_EQ(name_field->name, "display_name"); + EXPECT_TRUE(name_field->default_value.has_string_value()); + EXPECT_EQ(name_field->default_value.string_value(), "Anonymous"); + EXPECT_EQ(name_field->schema_specific_properties.size(), 2); + EXPECT_TRUE(name_field->schema_specific_properties.contains("json")); + EXPECT_TRUE(name_field->schema_specific_properties.contains("proto3")); + + const ObjectTypeDef::Field* tags_field = person.FindField("tags"); + ASSERT_THAT(tags_field, NotNull()); + EXPECT_EQ(tags_field->name, "tags"); + EXPECT_EQ(tags_field->type, TypeSpec(ListTypeSpec(std::make_unique( + PrimitiveType::kString)))); + + EXPECT_EQ(person.schema_specific_properties.size(), 1); + EXPECT_TRUE(person.schema_specific_properties.contains("json")); + + EXPECT_THAT(person.FindField("nonexistent"), IsNull()); + + TypeDef type_def(std::move(person)); + EXPECT_TRUE(type_def.is_object()); + EXPECT_FALSE(type_def.is_enum()); + EXPECT_EQ(type_def.kind_case(), TypeDefKindCase::kObject); + EXPECT_EQ(type_def.name(), "com.example.Person"); + EXPECT_EQ(type_def.doc(), "User account profile entity."); + EXPECT_EQ(type_def.object_type().fields.size(), 3); + EXPECT_EQ(type_def.object_type().schema_specific_properties.size(), 1); + EXPECT_TRUE( + type_def.object_type().schema_specific_properties.contains("json")); +} + +TEST(TypeDefTest, EnumTypeDefConstructionAndLookup) { + EnumTypeDef status_enum; + status_enum.name = "com.example.OrderStatus"; + status_enum.doc = "Lifecycle state of a customer order."; + status_enum.schema_specific_properties["json"] = + std::make_unique(); + + ASSERT_THAT(status_enum.AddConstant(EnumTypeDef::EnumConstant{ + .name = "ORDER_STATUS_UNSPECIFIED", + .doc = "Default unspecified state.", + }), + IsOk()); + + EnumTypeDef::EnumConstant shipped_val{ + .name = "SHIPPED", + .id = 2, + .doc = "Order has been dispatched.", + }; + shipped_val.schema_specific_properties["json"] = + std::make_unique(); + ASSERT_THAT(status_enum.AddConstant(std::move(shipped_val)), IsOk()); + + EXPECT_THAT( + status_enum.AddConstant(EnumTypeDef::EnumConstant{.name = "SHIPPED"}), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Enum constant 'SHIPPED' is already defined."))); + + const EnumTypeDef::EnumConstant* shipped = + status_enum.FindConstant("SHIPPED"); + ASSERT_THAT(shipped, NotNull()); + EXPECT_EQ(shipped->name, "SHIPPED"); + EXPECT_EQ(shipped->id, 2); + EXPECT_EQ(shipped->doc, "Order has been dispatched."); + EXPECT_TRUE(shipped->schema_specific_properties.contains("json")); + + EnumTypeDef empty_enum; + EXPECT_THAT(empty_enum.FindConstant("ORDER_STATUS_UNSPECIFIED"), IsNull()); + + EXPECT_EQ(status_enum.schema_specific_properties.size(), 1); + EXPECT_TRUE(status_enum.schema_specific_properties.contains("json")); + + const EnumTypeDef::EnumConstant* unspecified = + status_enum.FindConstant("ORDER_STATUS_UNSPECIFIED"); + ASSERT_THAT(unspecified, NotNull()); + EXPECT_EQ(unspecified->name, "ORDER_STATUS_UNSPECIFIED"); + EXPECT_EQ(unspecified->id, 0); + EXPECT_EQ(unspecified->doc, "Default unspecified state."); + EXPECT_THAT(unspecified->schema_specific_properties, IsEmpty()); + + EXPECT_THAT(status_enum.FindConstant("UNKNOWN"), IsNull()); + + TypeDef type_def(std::move(status_enum)); + EXPECT_FALSE(type_def.is_object()); + EXPECT_TRUE(type_def.is_enum()); + EXPECT_EQ(type_def.kind_case(), TypeDefKindCase::kEnum); + EXPECT_EQ(type_def.name(), "com.example.OrderStatus"); + EXPECT_EQ(type_def.doc(), "Lifecycle state of a customer order."); + EXPECT_EQ(type_def.enum_type().constants.size(), 2); + EXPECT_EQ(type_def.enum_type().schema_specific_properties.size(), 1); + EXPECT_TRUE(type_def.enum_type().schema_specific_properties.contains("json")); +} + +TEST(TypeDefTest, DefaultEnumTypeDef) { + TypeDef def((EnumTypeDef())); + EXPECT_FALSE(def.is_object()); + EXPECT_TRUE(def.is_enum()); + EXPECT_EQ(def.kind_case(), TypeDefKindCase::kEnum); + EXPECT_THAT(def.name(), Eq("")); + EXPECT_THAT(def.doc(), Eq("")); + EXPECT_THAT(def.enum_type().schema_specific_properties, IsEmpty()); + EXPECT_TRUE(def.enum_type().constants.empty()); + EXPECT_THAT(def.enum_type().FindConstant("ANY"), IsNull()); +} + +TEST(TypeDefSetTest, ConstructionLookupAndErrors) { + TypeDefSet set; + EXPECT_THAT(set.types(), IsEmpty()); + EXPECT_THAT(set.FindTypeDef("com.example.Person"), IsNull()); + + // Empty name is rejected. + EXPECT_THAT(set.AddTypeDef(TypeDef()), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type name cannot be empty."))); + + ObjectTypeDef person; + person.name = "com.example.Person"; + ASSERT_THAT(set.AddTypeDef(TypeDef(std::move(person))), IsOk()); + + EnumTypeDef status; + status.name = "com.example.Status"; + ASSERT_THAT(set.AddTypeDef(TypeDef(std::move(status))), IsOk()); + + ASSERT_EQ(set.types().size(), 2); + EXPECT_EQ(set.types()[0].name(), "com.example.Person"); + EXPECT_EQ(set.types()[1].name(), "com.example.Status"); + + // Duplicate name is rejected (even if switching between object and enum). + EnumTypeDef dup_person; + dup_person.name = "com.example.Person"; + EXPECT_THAT( + set.AddTypeDef(TypeDef(std::move(dup_person))), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Type 'com.example.Person' is already defined."))); + + const TypeDef* found_person = set.FindTypeDef("com.example.Person"); + ASSERT_THAT(found_person, NotNull()); + EXPECT_TRUE(found_person->is_object()); + EXPECT_EQ(found_person->name(), "com.example.Person"); + + const TypeDef* found_status = set.FindTypeDef("com.example.Status"); + ASSERT_THAT(found_status, NotNull()); + EXPECT_TRUE(found_status->is_enum()); + EXPECT_EQ(found_status->name(), "com.example.Status"); + + EXPECT_THAT(set.FindTypeDef("com.example.Missing"), IsNull()); + + // Copy and move preserve lookup integrity. + TypeDefSet copy_set = set; + ASSERT_THAT(copy_set.FindTypeDef("com.example.Person"), NotNull()); + EXPECT_EQ(copy_set.FindTypeDef("com.example.Person"), ©_set.types()[0]); + + TypeDefSet move_set = std::move(copy_set); + ASSERT_THAT(move_set.FindTypeDef("com.example.Status"), NotNull()); + EXPECT_EQ(move_set.FindTypeDef("com.example.Status"), &move_set.types()[1]); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/typedef_yaml.cc b/common/typedef/typedef_yaml.cc new file mode 100644 index 000000000..02f483a58 --- /dev/null +++ b/common/typedef/typedef_yaml.cc @@ -0,0 +1,1208 @@ +// Copyright 2026 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 "common/typedef/typedef_yaml.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "common/ast.h" +#include "common/constant.h" +#include "common/signature.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef_yaml_internal.h" +#include "common/typedef/yaml_helpers.h" +#include "internal/status_macros.h" +#include "internal/strings.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/emittermanip.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel { +namespace { + +using ::cel::internal::FormatYamlErrorMessage; +using ::cel::internal::GetBinary; +using ::cel::internal::GetBool; +using ::cel::internal::GetContextNodeForKeyValue; +using ::cel::internal::GetInt32; +using ::cel::internal::GetString; +using ::cel::internal::IsBinary; +using ::cel::internal::LoadYaml; +using ::cel::internal::YamlError; + +absl::StatusOr BuildTypeSpecFromStructured( + const YAML::Node& node, absl::string_view yaml, std::string name_str, + bool is_type_param, std::vector params) { + if (is_type_param) { + return TypeSpec(ParamTypeSpec(std::move(name_str))); + } + + auto read_param_or_dyn = [¶ms](size_t index) { + auto spec = std::make_unique(DynTypeSpec()); + if (params.size() > index) { + *spec = std::move(params[index]); + } + return spec; + }; + + if (params.empty()) { + if (name_str == "null") return TypeSpec(NullTypeSpec()); + if (name_str == "bool") return TypeSpec(PrimitiveType::kBool); + if (name_str == "int") return TypeSpec(PrimitiveType::kInt64); + if (name_str == "uint") return TypeSpec(PrimitiveType::kUint64); + if (name_str == "double") return TypeSpec(PrimitiveType::kDouble); + if (name_str == "string") return TypeSpec(PrimitiveType::kString); + if (name_str == "bytes") return TypeSpec(PrimitiveType::kBytes); + if (name_str == "any" || name_str == "google.protobuf.Any") { + return TypeSpec(WellKnownTypeSpec::kAny); + } + if (name_str == "timestamp" || name_str == "google.protobuf.Timestamp") { + return TypeSpec(WellKnownTypeSpec::kTimestamp); + } + if (name_str == "duration" || name_str == "google.protobuf.Duration") { + return TypeSpec(WellKnownTypeSpec::kDuration); + } + if (name_str == "dyn" || name_str == "google.protobuf.Value") { + return TypeSpec(DynTypeSpec()); + } + if (name_str == "bool_wrapper" || name_str == "google.protobuf.BoolValue") { + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kBool)); + } + if (name_str == "int_wrapper" || name_str == "google.protobuf.Int64Value" || + name_str == "google.protobuf.Int32Value") { + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kInt64)); + } + if (name_str == "uint_wrapper" || + name_str == "google.protobuf.UInt64Value" || + name_str == "google.protobuf.UInt32Value") { + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kUint64)); + } + if (name_str == "double_wrapper" || + name_str == "google.protobuf.DoubleValue" || + name_str == "google.protobuf.FloatValue") { + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kDouble)); + } + if (name_str == "string_wrapper" || + name_str == "google.protobuf.StringValue") { + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kString)); + } + if (name_str == "bytes_wrapper" || + name_str == "google.protobuf.BytesValue") { + return TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kBytes)); + } + if (name_str == "google.protobuf.ListValue") { + return TypeSpec(ListTypeSpec(std::make_unique(DynTypeSpec()))); + } + if (name_str == "google.protobuf.Struct") { + return TypeSpec( + MapTypeSpec(std::make_unique(PrimitiveType::kString), + std::make_unique(DynTypeSpec()))); + } + } + + if (name_str == "type") { + return TypeSpec(read_param_or_dyn(0)); + } + if (name_str == "list") { + return TypeSpec(ListTypeSpec(read_param_or_dyn(0))); + } + if (name_str == "map") { + auto key = read_param_or_dyn(0); + auto value = read_param_or_dyn(1); + return TypeSpec(MapTypeSpec(std::move(key), std::move(value))); + } + if (name_str == "function") { + auto result_type = read_param_or_dyn(0); + std::vector arg_types; + for (size_t i = 1; i < params.size(); ++i) { + arg_types.push_back(std::move(params[i])); + } + return TypeSpec( + FunctionTypeSpec(std::move(result_type), std::move(arg_types))); + } + + return TypeSpec(AbstractType(std::move(name_str), std::move(params))); +} + +absl::StatusOr ParseStructuredTypeSpec(const YAML::Node& node, + absl::string_view yaml) { + const YAML::Node type_name = node["type_name"]; + if (!type_name.IsDefined()) { + return YamlError(yaml, node, "Node 'type_name' is not specified"); + } + if (!type_name || !type_name.IsScalar()) { + return YamlError(yaml, type_name, "Node 'type_name' is not a string"); + } + std::string name_str = GetString(yaml, type_name); + if (name_str.empty()) { + return YamlError(yaml, node, "Field 'type' is not specified"); + } + + bool is_type_param = false; + const YAML::Node is_type_param_node = node["is_type_param"]; + if (is_type_param_node.IsDefined()) { + if (!is_type_param_node.IsScalar()) { + return YamlError(yaml, is_type_param_node, + "Node 'is_type_param' is not a boolean"); + } + CEL_ASSIGN_OR_RETURN(is_type_param, + GetBool(yaml, "is_type_param", is_type_param_node)); + } + + std::vector param_specs; + const YAML::Node params = node["params"]; + if (params.IsDefined()) { + if (!params.IsSequence()) { + return YamlError(yaml, params, "Node 'params' is not a sequence"); + } + for (const YAML::Node& param : params) { + if (!param || !param.IsMap()) { + return YamlError(yaml, param, "Type parameter is not a map"); + } + CEL_ASSIGN_OR_RETURN(TypeSpec param_spec, + ParseStructuredTypeSpec(param, yaml)); + param_specs.push_back(std::move(param_spec)); + } + } + + return BuildTypeSpecFromStructured(node, yaml, std::move(name_str), + is_type_param, std::move(param_specs)); +} + +ConstantKindCase GetConstantKindCase(const TypeSpec& type_spec) { + if (type_spec.has_null()) { + return ConstantKindCase::kNull; + } + if (type_spec.has_primitive()) { + switch (type_spec.primitive()) { + case PrimitiveType::kBool: + return ConstantKindCase::kBool; + case PrimitiveType::kInt64: + return ConstantKindCase::kInt; + case PrimitiveType::kUint64: + return ConstantKindCase::kUint; + case PrimitiveType::kDouble: + return ConstantKindCase::kDouble; + case PrimitiveType::kString: + return ConstantKindCase::kString; + case PrimitiveType::kBytes: + return ConstantKindCase::kBytes; + default: + return ConstantKindCase::kUnspecified; + } + } + if (type_spec.has_well_known()) { + switch (type_spec.well_known()) { + case WellKnownTypeSpec::kDuration: + return ConstantKindCase::kDuration; + case WellKnownTypeSpec::kTimestamp: + return ConstantKindCase::kTimestamp; + default: + return ConstantKindCase::kUnspecified; + } + } + return ConstantKindCase::kUnspecified; +} + +absl::StatusOr ParseConstantValue(absl::string_view yaml, + const YAML::Node& node, + ConstantKindCase constant_kind_case, + absl::string_view value) { + switch (constant_kind_case) { + case ConstantKindCase::kNull: + if (!value.empty()) { + return YamlError(yaml, node, "Failed to parse null constant"); + } + return Constant(nullptr); + case ConstantKindCase::kBool: + if (absl::EqualsIgnoreCase(value, "true")) { + return Constant(true); + } else if (absl::EqualsIgnoreCase(value, "false")) { + return Constant(false); + } else { + return YamlError(yaml, node, "Failed to parse bool constant"); + } + case ConstantKindCase::kInt: + int64_t int_value; + if (!absl::SimpleAtoi(value, &int_value)) { + return YamlError(yaml, node, "Failed to parse int constant"); + } + return Constant(int_value); + case ConstantKindCase::kUint: + uint64_t uint_value; + if (absl::EndsWith(value, "u")) { + value = value.substr(0, value.size() - 1); + } + if (!absl::SimpleAtoi(value, &uint_value)) { + return YamlError(yaml, node, "Failed to parse uint constant"); + } + return Constant(uint_value); + case ConstantKindCase::kDouble: + double double_value; + if (!absl::SimpleAtod(value, &double_value)) { + return YamlError(yaml, node, "Failed to parse double constant"); + } + return Constant(double_value); + case ConstantKindCase::kBytes: { + if (!IsBinary(node)) { + absl::StatusOr bytes_literal = + internal::ParseBytesLiteral(value); + if (bytes_literal.ok()) { + return Constant(BytesConstant(*bytes_literal)); + } + } + return Constant(BytesConstant(value)); + } + case ConstantKindCase::kString: + return Constant(StringConstant(value)); + case ConstantKindCase::kDuration: { + absl::Duration duration_value; + if (!absl::ParseDuration(value, &duration_value)) { + return YamlError(yaml, node, "Failed to parse duration constant"); + } + return Constant(duration_value); + } + case ConstantKindCase::kTimestamp: { + absl::Time timestamp_value; + std::string error; + if (!absl::ParseTime("%Y-%m-%d%ET%H:%M:%E*SZ", value, ×tamp_value, + &error)) { + return YamlError( + yaml, node, + absl::StrCat("Failed to parse timestamp constant: ", error, + " supported format: YYYY-MM-DDThh:mm:ssZ")); + } + return Constant(timestamp_value); + } + default: + return YamlError(yaml, node, "Constant type is not supported"); + } +} + +void EmitStructuredTypeSpec(const TypeSpec& type_spec, YAML::Emitter& out) { + out << YAML::BeginMap; + out << YAML::Key << "type_name" << YAML::Value << YAML::DoubleQuoted; + if (type_spec.has_null()) { + out << "null"; + } else if (type_spec.has_primitive()) { + switch (type_spec.primitive()) { + case PrimitiveType::kBool: + out << "bool"; + break; + case PrimitiveType::kInt64: + out << "int"; + break; + case PrimitiveType::kUint64: + out << "uint"; + break; + case PrimitiveType::kDouble: + out << "double"; + break; + case PrimitiveType::kString: + out << "string"; + break; + case PrimitiveType::kBytes: + out << "bytes"; + break; + default: + out << "dyn"; + break; + } + } else if (type_spec.has_wrapper()) { + switch (type_spec.wrapper()) { + case PrimitiveType::kBool: + out << "bool_wrapper"; + break; + case PrimitiveType::kInt64: + out << "int_wrapper"; + break; + case PrimitiveType::kUint64: + out << "uint_wrapper"; + break; + case PrimitiveType::kDouble: + out << "double_wrapper"; + break; + case PrimitiveType::kString: + out << "string_wrapper"; + break; + case PrimitiveType::kBytes: + out << "bytes_wrapper"; + break; + default: + out << "dyn"; + break; + } + } else if (type_spec.has_well_known()) { + switch (type_spec.well_known()) { + case WellKnownTypeSpec::kAny: + out << "any"; + break; + case WellKnownTypeSpec::kTimestamp: + out << "timestamp"; + break; + case WellKnownTypeSpec::kDuration: + out << "duration"; + break; + default: + out << "dyn"; + break; + } + } else if (type_spec.has_list_type()) { + out << "list"; + if (type_spec.list_type().has_elem_type()) { + out << YAML::Key << "params" << YAML::Value << YAML::BeginSeq; + EmitStructuredTypeSpec(type_spec.list_type().elem_type(), out); + out << YAML::EndSeq; + } + } else if (type_spec.has_map_type()) { + out << "map"; + if (type_spec.map_type().has_key_type() || + type_spec.map_type().has_value_type()) { + out << YAML::Key << "params" << YAML::Value << YAML::BeginSeq; + EmitStructuredTypeSpec(type_spec.map_type().key_type(), out); + EmitStructuredTypeSpec(type_spec.map_type().value_type(), out); + out << YAML::EndSeq; + } + } else if (type_spec.has_type()) { + out << "type"; + if (type_spec.type().is_specified()) { + out << YAML::Key << "params" << YAML::Value << YAML::BeginSeq; + EmitStructuredTypeSpec(type_spec.type(), out); + out << YAML::EndSeq; + } + } else if (type_spec.has_type_param()) { + out << type_spec.type_param().type(); + out << YAML::Key << "is_type_param" << YAML::Value << true; + } else if (type_spec.has_abstract_type()) { + out << type_spec.abstract_type().name(); + if (!type_spec.abstract_type().parameter_types().empty()) { + out << YAML::Key << "params" << YAML::Value << YAML::BeginSeq; + for (const TypeSpec& param : + type_spec.abstract_type().parameter_types()) { + EmitStructuredTypeSpec(param, out); + } + out << YAML::EndSeq; + } + } else if (type_spec.has_message_type()) { + out << type_spec.message_type().type(); + } else if (type_spec.has_function()) { + out << "function"; + out << YAML::Key << "params" << YAML::Value << YAML::BeginSeq; + EmitStructuredTypeSpec(type_spec.function().result_type(), out); + for (const TypeSpec& arg : type_spec.function().arg_types()) { + EmitStructuredTypeSpec(arg, out); + } + out << YAML::EndSeq; + } else { + out << "dyn"; + } + out << YAML::EndMap; +} + +void EmitTypeSpec(const TypeSpec& type_spec, YAML::Emitter& out, + const TypeDefToYamlOptions& options) { + out << YAML::Key << "type" << YAML::Value; + if (options.use_type_signatures) { + absl::StatusOr signature = MakeTypeSpecSignature(type_spec); + if (signature.ok()) { + out << YAML::DoubleQuoted << *signature; + return; + } + } + EmitStructuredTypeSpec(type_spec, out); +} + +void EmitConstantValue(absl::string_view key, const Constant& constant, + YAML::Emitter& out) { + if (!constant.has_value()) { + return; + } + switch (constant.kind_case()) { + case ConstantKindCase::kUnspecified: + case ConstantKindCase::kNull: + break; + case ConstantKindCase::kBool: + out << YAML::Key << std::string(key) << YAML::Value + << constant.bool_value(); + break; + case ConstantKindCase::kInt: + out << YAML::Key << std::string(key) << YAML::Value + << constant.int_value(); + break; + case ConstantKindCase::kUint: + out << YAML::Key << std::string(key) << YAML::Value + << constant.uint_value(); + break; + case ConstantKindCase::kDouble: + out << YAML::Key << std::string(key) << YAML::Value + << constant.double_value(); + break; + case ConstantKindCase::kBytes: { + out << YAML::Key << std::string(key); + const std::string& bytes_value = constant.bytes_value(); + std::string hex_escaped = "b\""; + for (unsigned char byte : bytes_value) { + absl::StrAppend(&hex_escaped, "\\x"); + absl::StrAppendFormat(&hex_escaped, "%02x", byte); + } + absl::StrAppend(&hex_escaped, "\""); + out << YAML::Value << hex_escaped; + break; + } + case ConstantKindCase::kString: + out << YAML::Key << std::string(key); + out << YAML::Value << YAML::DoubleQuoted << constant.string_value(); + break; + case ConstantKindCase::kDuration: + out << YAML::Key << std::string(key) << YAML::Value; + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) + out << absl::FormatDuration(constant.duration_value()); + break; + case ConstantKindCase::kTimestamp: + out << YAML::Key << std::string(key) << YAML::Value; + out << absl::FormatTime( + "%Y-%m-%d%ET%H:%M:%E*SZ", + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) + constant.timestamp_value(), absl::UTCTimeZone()); + break; + } +} + +absl::StatusOr +ParseObjectSchemaSpecificProperties(absl::string_view yaml, + const YAML::Node& parent_node, + const SchemaYamlRegistry& registry) { + ObjectSchemaSpecificProperties result; + const YAML::Node schemas = parent_node["schemas"]; + if (!schemas.IsDefined()) { + return result; + } + if (!schemas.IsMap()) { + return YamlError(yaml, schemas, "Node 'schemas' is not a map"); + } + for (const auto& kv : schemas) { + if (!kv.first.IsScalar()) { + return YamlError(yaml, kv.first, "Target schema name is not a string"); + } + std::string schema_name = GetString(yaml, kv.first); + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return YamlError(yaml, kv.first, + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeObjectProperties(yaml, kv.second)); + if (props != nullptr) { + result[schema_name] = std::move(props); + } + } + return result; +} + +absl::StatusOr +ParseFieldSchemaSpecificProperties(absl::string_view yaml, + const YAML::Node& parent_node, + const SchemaYamlRegistry& registry) { + FieldSchemaSpecificProperties result; + const YAML::Node schemas = parent_node["schemas"]; + if (!schemas.IsDefined()) { + return result; + } + if (!schemas.IsMap()) { + return YamlError(yaml, schemas, "Node 'schemas' is not a map"); + } + for (const auto& kv : schemas) { + if (!kv.first.IsScalar()) { + return YamlError(yaml, kv.first, "Target schema name is not a string"); + } + std::string schema_name = GetString(yaml, kv.first); + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return YamlError(yaml, kv.first, + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeFieldProperties(yaml, kv.second)); + if (props != nullptr) { + result[schema_name] = std::move(props); + } + } + return result; +} + +absl::StatusOr ParseEnumSchemaSpecificProperties( + absl::string_view yaml, const YAML::Node& parent_node, + const SchemaYamlRegistry& registry) { + EnumSchemaSpecificProperties result; + const YAML::Node schemas = parent_node["schemas"]; + if (!schemas.IsDefined()) { + return result; + } + if (!schemas.IsMap()) { + return YamlError(yaml, schemas, "Node 'schemas' is not a map"); + } + for (const auto& kv : schemas) { + if (!kv.first.IsScalar()) { + return YamlError(yaml, kv.first, "Target schema name is not a string"); + } + std::string schema_name = GetString(yaml, kv.first); + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return YamlError(yaml, kv.first, + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeEnumProperties(yaml, kv.second)); + if (props != nullptr) { + result[schema_name] = std::move(props); + } + } + return result; +} + +absl::StatusOr +ParseEnumConstantSchemaSpecificProperties(absl::string_view yaml, + const YAML::Node& parent_node, + const SchemaYamlRegistry& registry) { + EnumConstantSchemaSpecificProperties result; + const YAML::Node schemas = parent_node["schemas"]; + if (!schemas.IsDefined()) { + return result; + } + if (!schemas.IsMap()) { + return YamlError(yaml, schemas, "Node 'schemas' is not a map"); + } + for (const auto& kv : schemas) { + if (!kv.first.IsScalar()) { + return YamlError(yaml, kv.first, "Target schema name is not a string"); + } + std::string schema_name = GetString(yaml, kv.first); + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return YamlError(yaml, kv.first, + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + CEL_ASSIGN_OR_RETURN(std::unique_ptr props, + schema->DecodeEnumConstantProperties(yaml, kv.second)); + if (props != nullptr) { + result[schema_name] = std::move(props); + } + } + return result; +} + +absl::Status EmitObjectSchemaSpecificProperties( + const ObjectSchemaSpecificProperties& properties, YAML::Emitter& out, + const SchemaYamlRegistry& registry) { + if (properties.empty()) { + return absl::OkStatus(); + } + std::vector sorted_keys; + sorted_keys.reserve(properties.size()); + for (const auto& [key, props] : properties) { + if (props != nullptr) { + sorted_keys.push_back(key); + } + } + if (sorted_keys.empty()) { + return absl::OkStatus(); + } + absl::c_sort(sorted_keys); + + out << YAML::Key << "schemas" << YAML::Value << YAML::BeginMap; + for (const std::string& schema_name : sorted_keys) { + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + out << YAML::Key << schema_name << YAML::Value; + CEL_RETURN_IF_ERROR( + schema->EncodeObjectProperties(*properties.at(schema_name), out)); + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status EmitFieldSchemaSpecificProperties( + const FieldSchemaSpecificProperties& properties, YAML::Emitter& out, + const SchemaYamlRegistry& registry) { + if (properties.empty()) { + return absl::OkStatus(); + } + std::vector sorted_keys; + sorted_keys.reserve(properties.size()); + for (const auto& [key, props] : properties) { + if (props != nullptr) { + sorted_keys.push_back(key); + } + } + if (sorted_keys.empty()) { + return absl::OkStatus(); + } + absl::c_sort(sorted_keys); + + out << YAML::Key << "schemas" << YAML::Value << YAML::BeginMap; + for (const std::string& schema_name : sorted_keys) { + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + out << YAML::Key << schema_name << YAML::Value; + CEL_RETURN_IF_ERROR( + schema->EncodeFieldProperties(*properties.at(schema_name), out)); + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status EmitEnumSchemaSpecificProperties( + const EnumSchemaSpecificProperties& properties, YAML::Emitter& out, + const SchemaYamlRegistry& registry) { + if (properties.empty()) { + return absl::OkStatus(); + } + std::vector sorted_keys; + sorted_keys.reserve(properties.size()); + for (const auto& [key, props] : properties) { + if (props != nullptr) { + sorted_keys.push_back(key); + } + } + if (sorted_keys.empty()) { + return absl::OkStatus(); + } + absl::c_sort(sorted_keys); + + out << YAML::Key << "schemas" << YAML::Value << YAML::BeginMap; + for (const std::string& schema_name : sorted_keys) { + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + out << YAML::Key << schema_name << YAML::Value; + CEL_RETURN_IF_ERROR( + schema->EncodeEnumProperties(*properties.at(schema_name), out)); + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status EmitEnumConstantSchemaSpecificProperties( + const EnumConstantSchemaSpecificProperties& properties, YAML::Emitter& out, + const SchemaYamlRegistry& registry) { + if (properties.empty()) { + return absl::OkStatus(); + } + std::vector sorted_keys; + sorted_keys.reserve(properties.size()); + for (const auto& [key, props] : properties) { + if (props != nullptr) { + sorted_keys.push_back(key); + } + } + if (sorted_keys.empty()) { + return absl::OkStatus(); + } + absl::c_sort(sorted_keys); + + out << YAML::Key << "schemas" << YAML::Value << YAML::BeginMap; + for (const std::string& schema_name : sorted_keys) { + const SchemaYaml* schema = registry.Find(schema_name); + if (schema == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported schema: '", schema_name, "'")); + } + out << YAML::Key << schema_name << YAML::Value; + CEL_RETURN_IF_ERROR( + schema->EncodeEnumConstantProperties(*properties.at(schema_name), out)); + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::StatusOr ParseObjectTypeDef( + absl::string_view yaml, const YAML::Node& node, + const SchemaYamlRegistry& registry) { + if (!node || !node.IsMap()) { + return YamlError(yaml, node, "Object type definition is not a map"); + } + + const YAML::Node name = node["name"]; + if (!name || !name.IsScalar()) { + return YamlError(yaml, name.IsDefined() ? name : node, + "Type definition 'name' is not a string"); + } + std::string name_str = GetString(yaml, name); + + const YAML::Node doc = node["doc"]; + std::string doc_str; + if (doc.IsDefined()) { + if (!doc.IsScalar()) { + return YamlError(yaml, doc, "Type definition 'doc' is not a string"); + } + doc_str = GetString(yaml, doc); + } + + const YAML::Node constants = node["constants"]; + if (constants.IsDefined()) { + return YamlError(yaml, GetContextNodeForKeyValue(node, constants), + "Object type definition cannot have 'constants'"); + } + + ObjectTypeDef object_def; + object_def.name = std::move(name_str); + object_def.doc = std::move(doc_str); + CEL_ASSIGN_OR_RETURN( + object_def.schema_specific_properties, + ParseObjectSchemaSpecificProperties(yaml, node, registry)); + + const YAML::Node fields = node["fields"]; + if (!fields.IsDefined()) { + return object_def; + } + if (!fields.IsSequence()) { + return YamlError(yaml, fields, "Node 'fields' is not a sequence"); + } + + for (const YAML::Node& field_node : fields) { + if (!field_node || !field_node.IsMap()) { + return YamlError(yaml, field_node, "Field is not a map"); + } + ObjectTypeDef::Field field; + + const YAML::Node field_name = field_node["name"]; + if (!field_name || !field_name.IsScalar()) { + return YamlError(yaml, field_name.IsDefined() ? field_name : field_node, + "Field name is not a string"); + } + field.name = GetString(yaml, field_name); + + const YAML::Node type = field_node["type"]; + if (!type.IsDefined()) { + return YamlError(yaml, field_node, "Field 'type' is not specified"); + } + if (type.IsScalar()) { + CEL_ASSIGN_OR_RETURN(field.type, ParseTypeSpec(GetString(yaml, type)), + YamlError(yaml, type, absl::Status(_).message())); + } else if (type.IsMap()) { + CEL_ASSIGN_OR_RETURN(field.type, ParseStructuredTypeSpec(type, yaml)); + } else { + return YamlError(yaml, type, + "Field 'type' is neither a string nor a map"); + } + + const YAML::Node field_doc = field_node["doc"]; + if (field_doc.IsDefined()) { + if (!field_doc.IsScalar()) { + return YamlError(yaml, field_doc, "Field 'doc' is not a string"); + } + field.doc = GetString(yaml, field_doc); + } + + const YAML::Node default_node = field_node["default"]; + if (default_node.IsDefined()) { + ConstantKindCase constant_kind_case = GetConstantKindCase(field.type); + if (constant_kind_case == ConstantKindCase::kUnspecified) { + std::string type_name = MakeTypeSpecSignature(field.type) + .value_or(FormatTypeSpec(field.type)); + return YamlError( + yaml, default_node, + absl::StrCat("Constant type '", type_name, "' is not supported")); + } + if (default_node.IsNull() && + constant_kind_case == ConstantKindCase::kNull) { + field.default_value = Constant(nullptr); + } else { + if (!default_node.IsScalar()) { + return YamlError(yaml, default_node, + "Field 'default' is not a scalar"); + } + std::string value_str; + if (IsBinary(default_node)) { + CEL_ASSIGN_OR_RETURN(value_str, GetBinary(yaml, default_node)); + } else { + value_str = GetString(yaml, default_node); + } + CEL_ASSIGN_OR_RETURN(field.default_value, + ParseConstantValue(yaml, default_node, + constant_kind_case, value_str)); + } + } + + CEL_ASSIGN_OR_RETURN( + field.schema_specific_properties, + ParseFieldSchemaSpecificProperties(yaml, field_node, registry)); + + absl::Status add_status = object_def.AddField(std::move(field)); + if (!add_status.ok()) { + return YamlError(yaml, field_name, add_status.message()); + } + } + + return object_def; +} + +absl::StatusOr ParseEnumTypeDef( + absl::string_view yaml, const YAML::Node& node, + const SchemaYamlRegistry& registry) { + if (!node || !node.IsMap()) { + return YamlError(yaml, node, "Enum type definition is not a map"); + } + + const YAML::Node name = node["name"]; + if (!name || !name.IsScalar()) { + return YamlError(yaml, name.IsDefined() ? name : node, + "Type definition 'name' is not a string"); + } + std::string name_str = GetString(yaml, name); + + const YAML::Node doc = node["doc"]; + std::string doc_str; + if (doc.IsDefined()) { + if (!doc.IsScalar()) { + return YamlError(yaml, doc, "Type definition 'doc' is not a string"); + } + doc_str = GetString(yaml, doc); + } + + const YAML::Node fields = node["fields"]; + if (fields.IsDefined()) { + return YamlError(yaml, GetContextNodeForKeyValue(node, fields), + "Enum type definition cannot have 'fields'"); + } + + EnumTypeDef enum_def; + enum_def.name = std::move(name_str); + enum_def.doc = std::move(doc_str); + CEL_ASSIGN_OR_RETURN(enum_def.schema_specific_properties, + ParseEnumSchemaSpecificProperties(yaml, node, registry)); + + const YAML::Node constants = node["constants"]; + if (!constants.IsDefined()) { + return enum_def; + } + if (!constants.IsSequence()) { + return YamlError(yaml, constants, "Node 'constants' is not a sequence"); + } + + for (const YAML::Node& constant_node : constants) { + if (!constant_node || !constant_node.IsMap()) { + return YamlError(yaml, constant_node, "Enum constant is not a map"); + } + EnumTypeDef::EnumConstant enum_constant; + + const YAML::Node constant_name = constant_node["name"]; + if (!constant_name || !constant_name.IsScalar()) { + return YamlError( + yaml, constant_name.IsDefined() ? constant_name : constant_node, + "Enum constant name is not a string"); + } + enum_constant.name = GetString(yaml, constant_name); + + const YAML::Node constant_id = constant_node["id"]; + if (constant_id.IsDefined()) { + CEL_ASSIGN_OR_RETURN(enum_constant.id, GetInt32(yaml, "id", constant_id)); + } else { + enum_constant.id = static_cast(enum_def.constants.size()); + } + + const YAML::Node constant_doc = constant_node["doc"]; + if (constant_doc.IsDefined()) { + if (!constant_doc.IsScalar()) { + return YamlError(yaml, constant_doc, + "Enum constant 'doc' is not a string"); + } + enum_constant.doc = GetString(yaml, constant_doc); + } + + CEL_ASSIGN_OR_RETURN(enum_constant.schema_specific_properties, + ParseEnumConstantSchemaSpecificProperties( + yaml, constant_node, registry)); + + absl::Status add_status = enum_def.AddConstant(std::move(enum_constant)); + if (!add_status.ok()) { + return YamlError(yaml, constant_name, add_status.message()); + } + } + + return enum_def; +} + +} // namespace + +namespace internal { + +absl::StatusOr ParseTypeDefNode(absl::string_view yaml, + const YAML::Node& node, + const SchemaYamlRegistry& registry) { + if (!node || !node.IsMap()) { + return YamlError(yaml, node, "Type definition is not a map"); + } + + const YAML::Node object_node = node["object"]; + const YAML::Node enum_node = node["enum"]; + if (object_node.IsDefined() && enum_node.IsDefined()) { + return YamlError(yaml, GetContextNodeForKeyValue(node, enum_node), + "Node 'object' and 'enum' are mutually exclusive"); + } + + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + return YamlError(yaml, kv.first, "Type definition kind is not a string"); + } + std::string kind = GetString(yaml, kv.first); + if (kind != "object" && kind != "enum") { + return YamlError( + yaml, kv.first, + absl::StrCat("Unsupported type definition kind: '", kind, "'")); + } + } + + if (object_node.IsDefined()) { + CEL_ASSIGN_OR_RETURN(ObjectTypeDef object_def, + ParseObjectTypeDef(yaml, object_node, registry)); + return TypeDef(std::move(object_def)); + } + if (enum_node.IsDefined()) { + CEL_ASSIGN_OR_RETURN(EnumTypeDef enum_def, + ParseEnumTypeDef(yaml, enum_node, registry)); + return TypeDef(std::move(enum_def)); + } + return YamlError(yaml, node, + "Type definition must specify 'object' or 'enum'"); +} + +absl::StatusOr> ParseTypeDefsNode( + absl::string_view yaml, const YAML::Node& seq_node, + const SchemaYamlRegistry& registry) { + std::vector result; + if (!seq_node.IsDefined() || seq_node.IsNull()) { + return result; + } + if (!seq_node.IsSequence()) { + return YamlError(yaml, seq_node, "Node 'types' is not a sequence"); + } + for (const YAML::Node& item : seq_node) { + CEL_ASSIGN_OR_RETURN(TypeDef type_def, + ParseTypeDefNode(yaml, item, registry)); + result.push_back(std::move(type_def)); + } + return result; +} + +absl::Status EmitTypeDefNode(const TypeDef& type_def, YAML::Emitter& out, + const TypeDefToYamlOptions& options, + const SchemaYamlRegistry& registry) { + out << YAML::BeginMap; + if (type_def.is_object()) { + const ObjectTypeDef& object_def = type_def.object_type(); + out << YAML::Key << "object" << YAML::Value << YAML::BeginMap; + if (!object_def.name.empty()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << object_def.name; + } + if (!object_def.doc.empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << object_def.doc; + } + CEL_RETURN_IF_ERROR(EmitObjectSchemaSpecificProperties( + object_def.schema_specific_properties, out, registry)); + if (!object_def.fields.empty()) { + out << YAML::Key << "fields" << YAML::Value << YAML::BeginSeq; + for (const ObjectTypeDef::Field& field : object_def.fields) { + out << YAML::BeginMap; + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << field.name; + EmitTypeSpec(field.type, out, options); + if (!field.doc.empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << field.doc; + } + EmitConstantValue("default", field.default_value, out); + CEL_RETURN_IF_ERROR(EmitFieldSchemaSpecificProperties( + field.schema_specific_properties, out, registry)); + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + out << YAML::EndMap; + } else { + const EnumTypeDef& enum_def = type_def.enum_type(); + out << YAML::Key << "enum" << YAML::Value << YAML::BeginMap; + if (!enum_def.name.empty()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << enum_def.name; + } + if (!enum_def.doc.empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << enum_def.doc; + } + CEL_RETURN_IF_ERROR(EmitEnumSchemaSpecificProperties( + enum_def.schema_specific_properties, out, registry)); + if (!enum_def.constants.empty()) { + out << YAML::Key << "constants" << YAML::Value << YAML::BeginSeq; + for (const EnumTypeDef::EnumConstant& constant : enum_def.constants) { + out << YAML::BeginMap; + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << constant.name; + out << YAML::Key << "id" << YAML::Value << constant.id; + if (!constant.doc.empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << constant.doc; + } + CEL_RETURN_IF_ERROR(EmitEnumConstantSchemaSpecificProperties( + constant.schema_specific_properties, out, registry)); + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + out << YAML::EndMap; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::Status EmitTypeDefsSeq(absl::Span type_defs, + YAML::Emitter& out, + const TypeDefToYamlOptions& options, + const SchemaYamlRegistry& registry) { + std::vector sorted_type_defs(type_defs.begin(), type_defs.end()); + absl::c_sort(sorted_type_defs, [](const TypeDef& a, const TypeDef& b) { + return a.name() < b.name(); + }); + + out << YAML::BeginSeq; + for (const TypeDef& type_def : sorted_type_defs) { + CEL_RETURN_IF_ERROR(EmitTypeDefNode(type_def, out, options, registry)); + } + out << YAML::EndSeq; + return absl::OkStatus(); +} + +} // namespace internal + +absl::StatusOr TypeDefFromYaml(absl::string_view yaml, + const SchemaYamlRegistry& registry) { + CEL_ASSIGN_OR_RETURN(YAML::Node root, LoadYaml(yaml)); + if (!root.IsDefined() || root.IsNull()) { + return TypeDef(); + } + if (!root.IsMap()) { + return absl::InvalidArgumentError(FormatYamlErrorMessage( + yaml, "Invalid CEL type definition YAML", root.Mark())); + } + return internal::ParseTypeDefNode(yaml, root, registry); +} + +absl::StatusOr> TypeDefsFromYaml( + absl::string_view yaml, const SchemaYamlRegistry& registry) { + CEL_ASSIGN_OR_RETURN(YAML::Node root, LoadYaml(yaml)); + if (!root.IsDefined() || root.IsNull()) { + return std::vector(); + } + if (root.IsSequence()) { + return internal::ParseTypeDefsNode(yaml, root, registry); + } + if (root.IsMap()) { + const YAML::Node types = root["types"]; + if (!types.IsDefined()) { + return absl::InvalidArgumentError(FormatYamlErrorMessage( + yaml, "Invalid CEL type definitions YAML", root.Mark())); + } + return internal::ParseTypeDefsNode(yaml, types, registry); + } + return absl::InvalidArgumentError(FormatYamlErrorMessage( + yaml, "Invalid CEL type definitions YAML", root.Mark())); +} + +absl::Status TypeDefToYaml(const TypeDef& type_def, std::ostream& os, + const TypeDefToYamlOptions& options, + const SchemaYamlRegistry& registry) { + YAML::Emitter out(os); + out.SetIndent(2); + return internal::EmitTypeDefNode(type_def, out, options, registry); +} + +absl::Status TypeDefToYaml(const TypeDef& type_def, + const SchemaYamlRegistry& registry, std::ostream& os, + const TypeDefToYamlOptions& options) { + return TypeDefToYaml(type_def, os, options, registry); +} + +absl::Status TypeDefsToYaml(absl::Span type_defs, + std::ostream& os, + const TypeDefToYamlOptions& options, + const SchemaYamlRegistry& registry) { + if (type_defs.empty()) { + return absl::OkStatus(); + } + YAML::Emitter out(os); + out.SetIndent(2); + return internal::EmitTypeDefsSeq(type_defs, out, options, registry); +} + +absl::Status TypeDefsToYaml(absl::Span type_defs, + const SchemaYamlRegistry& registry, + std::ostream& os, + const TypeDefToYamlOptions& options) { + return TypeDefsToYaml(type_defs, os, options, registry); +} + +absl::StatusOr TypeDefSetFromYaml( + absl::string_view yaml, const SchemaYamlRegistry& registry) { + CEL_ASSIGN_OR_RETURN(std::vector type_defs, + TypeDefsFromYaml(yaml, registry)); + TypeDefSet result; + for (TypeDef& type_def : type_defs) { + CEL_RETURN_IF_ERROR(result.AddTypeDef(std::move(type_def))); + } + return result; +} + +absl::Status TypeDefSetToYaml(const TypeDefSet& type_def_set, std::ostream& os, + const TypeDefToYamlOptions& options, + const SchemaYamlRegistry& registry) { + return TypeDefsToYaml(type_def_set.types(), os, options, registry); +} + +absl::Status TypeDefSetToYaml(const TypeDefSet& type_def_set, + const SchemaYamlRegistry& registry, + std::ostream& os, + const TypeDefToYamlOptions& options) { + return TypeDefsToYaml(type_def_set.types(), os, options, registry); +} + +} // namespace cel diff --git a/common/typedef/typedef_yaml.h b/common/typedef/typedef_yaml.h new file mode 100644 index 000000000..df949331d --- /dev/null +++ b/common/typedef/typedef_yaml.h @@ -0,0 +1,103 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_YAML_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_YAML_H_ + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" + +namespace cel { + +struct TypeDefToYamlOptions { + // Whether to use concise type signatures (e.g., `type: "list"`) + // instead of structured type definitions (`type_name: "list"`, `params: ...`) + // in the output YAML. + bool use_type_signatures = true; +}; + +// TypeDefFromYaml creates a TypeDef from a YAML mapping string. +// +// Schema-specific properties under `schemas` are decoded by +// delegating to the corresponding `SchemaYaml` looked up in `registry`. +absl::StatusOr TypeDefFromYaml( + absl::string_view yaml, const SchemaYamlRegistry& registry = {}); + +// TypeDefsFromYaml creates a sequence of TypeDefs from a YAML string. +// +// Supports either a top-level YAML sequence of type definitions or a YAML map +// containing a `types` sequence key. Schema-specific properties +// under `schemas` are decoded by delegating to the corresponding +// `SchemaYaml` looked up in `registry`. +absl::StatusOr> TypeDefsFromYaml( + absl::string_view yaml, const SchemaYamlRegistry& registry = {}); + +// TypeDefSetFromYaml creates a TypeDefSet from a YAML string. +// +// Supports either a top-level YAML sequence of type definitions or a YAML map +// containing a `types` sequence key. Schema-specific properties +// under `schemas` are decoded by delegating to the corresponding +// `SchemaYaml` looked up in `registry`. +absl::StatusOr TypeDefSetFromYaml( + absl::string_view yaml, const SchemaYamlRegistry& registry = {}); + +// TypeDefToYaml serializes a single TypeDef as a YAML mapping to `os`. +// +// Schema-specific properties under `schemas` are encoded by +// delegating to the corresponding `SchemaYaml` looked up in `registry`. +absl::Status TypeDefToYaml(const TypeDef& type_def, std::ostream& os, + const TypeDefToYamlOptions& options = {}, + const SchemaYamlRegistry& registry = {}); + +absl::Status TypeDefToYaml(const TypeDef& type_def, + const SchemaYamlRegistry& registry, std::ostream& os, + const TypeDefToYamlOptions& options = {}); + +// TypeDefsToYaml serializes a sequence of TypeDefs as a YAML sequence to `os`. +// +// Schema-specific properties under `schemas` are encoded by +// delegating to the corresponding `SchemaYaml` looked up in `registry`. +absl::Status TypeDefsToYaml(absl::Span type_defs, + std::ostream& os, + const TypeDefToYamlOptions& options = {}, + const SchemaYamlRegistry& registry = {}); + +absl::Status TypeDefsToYaml(absl::Span type_defs, + const SchemaYamlRegistry& registry, + std::ostream& os, + const TypeDefToYamlOptions& options = {}); + +// TypeDefSetToYaml serializes a TypeDefSet as a YAML sequence to `os`. +// +// Schema-specific properties under `schemas` are encoded by +// delegating to the corresponding `SchemaYaml` looked up in `registry`. +absl::Status TypeDefSetToYaml(const TypeDefSet& type_def_set, std::ostream& os, + const TypeDefToYamlOptions& options = {}, + const SchemaYamlRegistry& registry = {}); + +absl::Status TypeDefSetToYaml(const TypeDefSet& type_def_set, + const SchemaYamlRegistry& registry, + std::ostream& os, + const TypeDefToYamlOptions& options = {}); + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_YAML_H_ diff --git a/common/typedef/typedef_yaml_internal.h b/common/typedef/typedef_yaml_internal.h new file mode 100644 index 000000000..989b27673 --- /dev/null +++ b/common/typedef/typedef_yaml_internal.h @@ -0,0 +1,51 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_YAML_INTERNAL_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_YAML_INTERNAL_H_ + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef_yaml.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/node/node.h" + +namespace cel::internal { + +absl::StatusOr ParseTypeDefNode( + absl::string_view yaml, const YAML::Node& node, + const SchemaYamlRegistry& registry = {}); + +absl::StatusOr> ParseTypeDefsNode( + absl::string_view yaml, const YAML::Node& seq_node, + const SchemaYamlRegistry& registry = {}); + +absl::Status EmitTypeDefNode(const TypeDef& type_def, YAML::Emitter& out, + const TypeDefToYamlOptions& options, + const SchemaYamlRegistry& registry = {}); + +absl::Status EmitTypeDefsSeq(absl::Span type_defs, + YAML::Emitter& out, + const TypeDefToYamlOptions& options, + const SchemaYamlRegistry& registry = {}); + +} // namespace cel::internal + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_YAML_INTERNAL_H_ diff --git a/common/typedef/typedef_yaml_test.cc b/common/typedef/typedef_yaml_test.cc new file mode 100644 index 000000000..74722fc22 --- /dev/null +++ b/common/typedef/typedef_yaml_test.cc @@ -0,0 +1,1576 @@ +// Copyright 2026 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 "common/typedef/typedef_yaml.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/time/civil_time.h" +#include "absl/time/time.h" +#include "common/ast.h" +#include "common/constant.h" +#include "common/typedef/schema.h" +#include "common/typedef/schema_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/yaml_helpers.h" +#include "common/typedef/yaml_test_helpers.h" +#include "internal/status_macros.h" +#include "internal/testing.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/emittermanip.h" +#include "yaml-cpp/mark.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/node/parse.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::SizeIs; + +struct TestPropsData { + std::optional name; + std::optional id; +}; + +absl::StatusOr DecodeTestProps(absl::string_view schema_name, + absl::string_view context, + absl::string_view yaml, + const YAML::Node& node) { + if (!node || !node.IsMap()) { + return internal::YamlError(yaml, node, "Properties node is not a map"); + } + TestPropsData data; + for (const auto& kv : node) { + std::string key = internal::GetString(yaml, kv.first); + if (key == "name") { + data.name = internal::GetString(yaml, kv.second); + } else if (key == "id") { + CEL_ASSIGN_OR_RETURN(data.id, internal::GetInt32(yaml, "id", kv.second)); + } else { + return internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported '", schema_name, "' ", context, + " property: '", key, "'")); + } + } + return data; +} + +void EncodeTestProps(const TestPropsData& data, YAML::Emitter& out) { + out << YAML::BeginMap; + if (data.name.has_value()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << *data.name; + } + if (data.id.has_value()) { + out << YAML::Key << "id" << YAML::Value << *data.id; + } + out << YAML::EndMap; +} + +class TestObjectProps final : public SchemaObjectProperties { + public: + explicit TestObjectProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestFieldProps final : public SchemaFieldProperties { + public: + explicit TestFieldProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestEnumProps final : public SchemaEnumProperties { + public: + explicit TestEnumProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestEnumConstantProps final : public SchemaEnumConstantProperties { + public: + explicit TestEnumConstantProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestSchema final : public SchemaYaml { + public: + explicit TestSchema(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + absl::StatusOr> + DecodeObjectProperties(absl::string_view yaml, + const YAML::Node& node) const override { + CEL_ASSIGN_OR_RETURN(TestPropsData d, + DecodeTestProps(name_, "object", yaml, node)); + return std::make_unique(std::move(d)); + } + + absl::StatusOr> DecodeFieldProperties( + absl::string_view yaml, const YAML::Node& node) const override { + CEL_ASSIGN_OR_RETURN(TestPropsData d, + DecodeTestProps(name_, "field", yaml, node)); + return std::make_unique(std::move(d)); + } + + absl::StatusOr> DecodeEnumProperties( + absl::string_view yaml, const YAML::Node& node) const override { + CEL_ASSIGN_OR_RETURN(TestPropsData d, + DecodeTestProps(name_, "enum", yaml, node)); + return std::make_unique(std::move(d)); + } + + absl::StatusOr> + DecodeEnumConstantProperties(absl::string_view yaml, + const YAML::Node& node) const override { + CEL_ASSIGN_OR_RETURN(TestPropsData d, + DecodeTestProps(name_, "enum constant", yaml, node)); + return std::make_unique(std::move(d)); + } + + absl::Status EncodeObjectProperties(const SchemaObjectProperties& properties, + YAML::Emitter& out) const override { + EncodeTestProps(static_cast(properties).data(), + out); + return absl::OkStatus(); + } + + absl::Status EncodeFieldProperties(const SchemaFieldProperties& properties, + YAML::Emitter& out) const override { + EncodeTestProps(static_cast(properties).data(), out); + return absl::OkStatus(); + } + + absl::Status EncodeEnumProperties(const SchemaEnumProperties& properties, + YAML::Emitter& out) const override { + EncodeTestProps(static_cast(properties).data(), out); + return absl::OkStatus(); + } + + absl::Status EncodeEnumConstantProperties( + const SchemaEnumConstantProperties& properties, + YAML::Emitter& out) const override { + EncodeTestProps( + static_cast(properties).data(), out); + return absl::OkStatus(); + } + + private: + std::string name_; +}; + +class StubSchema final : public SchemaYaml { + public: + explicit StubSchema(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + private: + std::string name_; +}; + +SchemaYamlRegistry MakeTestRegistry() { + return SchemaYamlRegistry(std::make_unique("schema_a"), + std::make_unique("schema_b"), + std::make_unique("stub_schema")); +} + +TEST(TypeDefYamlTest, ParseObjectTypeDefWithSchemasAndDefaults) { + SchemaYamlRegistry registry = MakeTestRegistry(); + + ASSERT_OK_AND_ASSIGN(TypeDef type_def, TypeDefFromYaml(R"yaml( + object: + name: "com.example.Item" + doc: "Store item entity." + schemas: + schema_a: + name: "ItemObj" + schema_b: + name: "com.example.store.ItemProto" + fields: + - name: "item_id" + type: "string" + doc: "Unique item identifier." + default: "default_id" + schemas: + schema_a: + name: "itemId" + schema_b: + name: "com.example.store.Price" + id: 12 + - name: "tags" + type: "list" + - name: "attributes" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "int" + - name: "nested_type" + type: + type_name: "list" + params: + - type_name: "T" + is_type_param: true + )yaml", + registry)); + + ASSERT_TRUE(type_def.is_object()); + const ObjectTypeDef& obj = type_def.object_type(); + EXPECT_EQ(obj.name, "com.example.Item"); + EXPECT_EQ(obj.doc, "Store item entity."); + + ASSERT_TRUE(obj.schema_specific_properties.contains("schema_a")); + EXPECT_NE(obj.schema_specific_properties.at("schema_a"), nullptr); + ASSERT_TRUE(obj.schema_specific_properties.contains("schema_b")); + EXPECT_NE(obj.schema_specific_properties.at("schema_b"), nullptr); + + ASSERT_THAT(obj.fields, SizeIs(4)); + const ObjectTypeDef::Field& f0 = obj.fields[0]; + EXPECT_EQ(f0.name, "item_id"); + EXPECT_EQ(f0.type, TypeSpec(PrimitiveType::kString)); + EXPECT_EQ(f0.doc, "Unique item identifier."); + EXPECT_EQ(f0.default_value, Constant(StringConstant("default_id"))); + + ASSERT_TRUE(f0.schema_specific_properties.contains("schema_a")); + EXPECT_NE(f0.schema_specific_properties.at("schema_a"), nullptr); + ASSERT_TRUE(f0.schema_specific_properties.contains("schema_b")); + EXPECT_NE(f0.schema_specific_properties.at("schema_b"), nullptr); + + EXPECT_EQ(obj.fields[1].name, "tags"); + EXPECT_EQ(obj.fields[1].type, + TypeSpec(ListTypeSpec( + std::make_unique(PrimitiveType::kString)))); + + EXPECT_EQ(obj.fields[2].name, "attributes"); + EXPECT_EQ( + obj.fields[2].type, + TypeSpec(MapTypeSpec(std::make_unique(PrimitiveType::kString), + std::make_unique(PrimitiveType::kInt64)))); + + EXPECT_EQ(obj.fields[3].name, "nested_type"); + EXPECT_EQ( + obj.fields[3].type, + TypeSpec(ListTypeSpec(std::make_unique(ParamTypeSpec("T"))))); +} + +TEST(TypeDefYamlTest, ConciseAndStructuredTypeSyntax) { + absl::string_view concise_yaml = R"yaml( + object: + name: com.example.store.Order + fields: + - name: items + type: list + - name: metadata + type: map + - name: notes + type: string_wrapper + )yaml"; + + absl::string_view structured_yaml = R"yaml( + object: + name: com.example.store.Order + fields: + - name: items + type: + type_name: list + params: + - type_name: com.example.store.Item + - name: metadata + type: + type_name: map + params: + - type_name: string + - type_name: dyn + - name: notes + type: + type_name: string_wrapper + )yaml"; + + ASSERT_OK_AND_ASSIGN(TypeDef from_concise, TypeDefFromYaml(concise_yaml)); + ASSERT_OK_AND_ASSIGN(TypeDef from_structured, + TypeDefFromYaml(structured_yaml)); + + for (const TypeDef* def : {&from_concise, &from_structured}) { + ASSERT_TRUE(def->is_object()); + const ObjectTypeDef& obj = def->object_type(); + EXPECT_EQ(obj.name, "com.example.store.Order"); + ASSERT_THAT(obj.fields, SizeIs(3)); + EXPECT_EQ(obj.fields[0].name, "items"); + EXPECT_EQ(obj.fields[0].type, + TypeSpec(ListTypeSpec(std::make_unique( + AbstractType("com.example.store.Item", {}))))); + EXPECT_EQ(obj.fields[1].name, "metadata"); + EXPECT_EQ( + obj.fields[1].type, + TypeSpec(MapTypeSpec(std::make_unique(PrimitiveType::kString), + std::make_unique(DynTypeSpec())))); + EXPECT_EQ(obj.fields[2].name, "notes"); + EXPECT_EQ(obj.fields[2].type, + TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kString))); + } + + std::stringstream emitted_concise; + ASSERT_THAT(TypeDefToYaml(from_structured, emitted_concise, + {.use_type_signatures = true}), + IsOk()); + EXPECT_EQ(emitted_concise.str(), Unindent(R"yaml( + object: + name: "com.example.store.Order" + fields: + - name: "items" + type: "list" + - name: "metadata" + type: "map" + - name: "notes" + type: "string_wrapper" + )yaml")); + + std::stringstream emitted_structured; + ASSERT_THAT(TypeDefToYaml(from_concise, emitted_structured, + {.use_type_signatures = false}), + IsOk()); + EXPECT_EQ(emitted_structured.str(), Unindent(R"yaml( + object: + name: "com.example.store.Order" + fields: + - name: "items" + type: + type_name: "list" + params: + - type_name: "com.example.store.Item" + - name: "metadata" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" + - name: "notes" + type: + type_name: "string_wrapper" + )yaml")); +} + +TEST(TypeDefYamlTest, ParseAllConstantDefaultTypes) { + ASSERT_OK_AND_ASSIGN(TypeDef type_def, TypeDefFromYaml(R"yaml( + object: + name: "ConstantsHolder" + fields: + - name: "f_null" + type: "null" + default: ~ + - name: "f_bool" + type: "bool" + default: true + - name: "f_int" + type: "int" + default: -42 + - name: "f_uint" + type: "uint" + default: 100u + - name: "f_double" + type: "double" + default: 3.25 + - name: "f_bytes" + type: "bytes" + default: b"\xff\x00\x01" + - name: "f_bytes_b64" + type: "bytes" + default: !!binary "AQID" + - name: "f_string" + type: "string" + default: "hello" + - name: "f_duration" + type: "duration" + default: "1h2m3s" + - name: "f_timestamp" + type: "timestamp" + default: "2026-01-02T03:04:05Z" + )yaml")); + + ASSERT_TRUE(type_def.is_object()); + const ObjectTypeDef& obj = type_def.object_type(); + ASSERT_THAT(obj.fields, SizeIs(10)); + EXPECT_EQ(obj.fields[0].default_value, Constant(nullptr)); + EXPECT_EQ(obj.fields[1].default_value, Constant(true)); + EXPECT_EQ(obj.fields[2].default_value, Constant(int64_t{-42})); + EXPECT_EQ(obj.fields[3].default_value, Constant(uint64_t{100})); + EXPECT_EQ(obj.fields[4].default_value, Constant(3.25)); + EXPECT_EQ(obj.fields[5].default_value, + Constant(BytesConstant(absl::string_view("\xff\x00\x01", 3)))); + EXPECT_EQ(obj.fields[6].default_value, + Constant(BytesConstant(absl::string_view("\x01\x02\x03", 3)))); + EXPECT_EQ(obj.fields[7].default_value, Constant(StringConstant("hello"))); + EXPECT_EQ(obj.fields[8].default_value, + Constant(absl::Hours(1) + absl::Minutes(2) + absl::Seconds(3))); + EXPECT_EQ(obj.fields[9].default_value, + Constant(absl::FromUnixSeconds(1767323045))); +} + +TEST(TypeDefYamlTest, ParseEnumTypeDef) { + SchemaYamlRegistry registry = MakeTestRegistry(); + + ASSERT_OK_AND_ASSIGN(TypeDef type_def, TypeDefFromYaml(R"yaml( + enum: + name: "com.example.OrderStatus" + doc: "Order lifecycle state." + schemas: + schema_a: + name: "OrderStatusA" + schema_b: + name: "com.example.proto.OrderStatus" + constants: + - name: "UNSPECIFIED" + doc: "Default value." + - name: "PENDING" + - name: "SHIPPED" + id: 10 + doc: "Shipped to customer." + schemas: + schema_a: + name: "shipped" + schema_b: + name: "ORDER_STATUS_SHIPPED" + - name: "DELIVERED" + )yaml", + registry)); + + ASSERT_TRUE(type_def.is_enum()); + const EnumTypeDef& enum_def = type_def.enum_type(); + EXPECT_EQ(enum_def.name, "com.example.OrderStatus"); + EXPECT_EQ(enum_def.doc, "Order lifecycle state."); + + ASSERT_TRUE(enum_def.schema_specific_properties.contains("schema_a")); + EXPECT_NE(enum_def.schema_specific_properties.at("schema_a"), nullptr); + ASSERT_TRUE(enum_def.schema_specific_properties.contains("schema_b")); + EXPECT_NE(enum_def.schema_specific_properties.at("schema_b"), nullptr); + + ASSERT_THAT(enum_def.constants, SizeIs(4)); + EXPECT_EQ(enum_def.constants[0].name, "UNSPECIFIED"); + EXPECT_EQ(enum_def.constants[0].id, 0); + EXPECT_EQ(enum_def.constants[0].doc, "Default value."); + + EXPECT_EQ(enum_def.constants[1].name, "PENDING"); + EXPECT_EQ(enum_def.constants[1].id, 1); + + EXPECT_EQ(enum_def.constants[2].name, "SHIPPED"); + EXPECT_EQ(enum_def.constants[2].id, 10); + EXPECT_EQ(enum_def.constants[2].doc, "Shipped to customer."); + ASSERT_TRUE( + enum_def.constants[2].schema_specific_properties.contains("schema_a")); + EXPECT_NE(enum_def.constants[2].schema_specific_properties.at("schema_a"), + nullptr); + ASSERT_TRUE( + enum_def.constants[2].schema_specific_properties.contains("schema_b")); + EXPECT_NE(enum_def.constants[2].schema_specific_properties.at("schema_b"), + nullptr); + + EXPECT_EQ(enum_def.constants[3].name, "DELIVERED"); + EXPECT_EQ(enum_def.constants[3].id, 3); +} + +TEST(TypeDefYamlTest, TypeDefsFromYamlSequenceAndTypesMap) { + SchemaYamlRegistry registry = MakeTestRegistry(); + + absl::string_view types_map_yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "item_id" + type: "string" + schemas: + schema_a: + name: "itemId" + schema_b: + name: "com.example.store.Price" + id: 12 + - enum: + name: "Status" + constants: + - name: "UNKNOWN" + )yaml"; + + absl::string_view seq_yaml = R"yaml( + - object: + name: "Item" + fields: + - name: "item_id" + type: "string" + schemas: + schema_a: + name: "itemId" + schema_b: + name: "com.example.store.Price" + id: 12 + - enum: + name: "Status" + constants: + - name: "UNKNOWN" + )yaml"; + + ASSERT_OK_AND_ASSIGN(std::vector from_map, + TypeDefsFromYaml(types_map_yaml, registry)); + ASSERT_OK_AND_ASSIGN(std::vector from_seq, + TypeDefsFromYaml(seq_yaml, registry)); + ASSERT_THAT(from_map, SizeIs(2)); + ASSERT_THAT(from_seq, SizeIs(2)); + std::stringstream map_ss; + std::stringstream seq_ss; + ASSERT_THAT(TypeDefsToYaml(from_map, registry, map_ss), IsOk()); + ASSERT_THAT(TypeDefsToYaml(from_seq, registry, seq_ss), IsOk()); + EXPECT_EQ(map_ss.str(), seq_ss.str()); +} + +TEST(TypeDefYamlTest, ExportAndRoundTripObjectAndEnum) { + SchemaYamlRegistry registry = MakeTestRegistry(); + + std::string expected_signature_yaml = Unindent(R"yaml( + - object: + name: "com.example.Item" + doc: "Store item." + schemas: + schema_a: + name: "Item" + schema_b: + name: "com.example.proto.Item" + fields: + - name: "item_id" + type: "string" + doc: "ID of the item." + default: "abc" + schemas: + schema_a: + name: "itemId" + schema_b: + name: "com.example.store.Price" + id: 12 + - name: "tags" + type: "list" + - enum: + name: "com.example.Status" + doc: "Status enum." + schemas: + schema_a: + name: "StatusA" + schema_b: + name: "com.example.proto.Status" + constants: + - name: "UNSPECIFIED" + id: 0 + - name: "ACTIVE" + id: 1 + doc: "Active state." + schemas: + schema_a: + name: "active" + schema_b: + name: "STATUS_ACTIVE" + )yaml"); + + std::string expected_structured_yaml = Unindent(R"yaml( + - object: + name: "com.example.Item" + doc: "Store item." + schemas: + schema_a: + name: "Item" + schema_b: + name: "com.example.proto.Item" + fields: + - name: "item_id" + type: + type_name: "string" + doc: "ID of the item." + default: "abc" + schemas: + schema_a: + name: "itemId" + schema_b: + name: "com.example.store.Price" + id: 12 + - name: "tags" + type: + type_name: "list" + params: + - type_name: "string" + - enum: + name: "com.example.Status" + doc: "Status enum." + schemas: + schema_a: + name: "StatusA" + schema_b: + name: "com.example.proto.Status" + constants: + - name: "UNSPECIFIED" + id: 0 + - name: "ACTIVE" + id: 1 + doc: "Active state." + schemas: + schema_a: + name: "active" + schema_b: + name: "STATUS_ACTIVE" + )yaml"); + + ASSERT_OK_AND_ASSIGN(std::vector type_defs, + TypeDefsFromYaml(expected_signature_yaml, registry)); + + std::stringstream sig_ss; + ASSERT_THAT(TypeDefsToYaml(type_defs, registry, sig_ss, + {.use_type_signatures = true}), + IsOk()); + EXPECT_EQ(sig_ss.str(), expected_signature_yaml); + + std::stringstream struct_ss; + ASSERT_THAT(TypeDefsToYaml(type_defs, registry, struct_ss, + {.use_type_signatures = false}), + IsOk()); + EXPECT_EQ(struct_ss.str(), expected_structured_yaml); + + ASSERT_OK_AND_ASSIGN(std::vector roundtrip_defs, + TypeDefsFromYaml(struct_ss.str(), registry)); + std::stringstream roundtrip_ss; + ASSERT_THAT(TypeDefsToYaml(roundtrip_defs, registry, roundtrip_ss, + {.use_type_signatures = true}), + IsOk()); + EXPECT_EQ(roundtrip_ss.str(), expected_signature_yaml); +} + +struct MalformedYamlTestCase { + std::string yaml; + std::string expected_error; +}; + +class TypeDefYamlErrorTest + : public testing::TestWithParam {}; + +TEST_P(TypeDefYamlErrorTest, ReportsFormattedErrorWithMark) { + const MalformedYamlTestCase& test_case = GetParam(); + SchemaYamlRegistry registry = MakeTestRegistry(); + EXPECT_THAT(TypeDefsFromYaml(test_case.yaml, registry), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq(test_case.expected_error))); +} + +INSTANTIATE_TEST_SUITE_P( + TypeDefYamlErrorTest, TypeDefYamlErrorTest, + ::testing::Values( + MalformedYamlTestCase{ + .yaml = R"yaml( + types: "not_a_seq" + )yaml", + .expected_error = "2:22: Node 'types' is not a sequence\n" + "| types: \"not_a_seq\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - "not_a_map" + )yaml", + .expected_error = "3:19: Type definition is not a map\n" + "| - \"not_a_map\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: + - "bad_name" + )yaml", + .expected_error = "5:23: Type definition 'name' is not a string\n" + "| - \"bad_name\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - union: + name: "Item" + )yaml", + .expected_error = + "3:19: Unsupported type definition kind: 'union'\n" + "| - union:\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + constants: [] + )yaml", + .expected_error = + "5:21: Object type definition cannot have 'constants'\n" + "| constants: []\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - enum: + name: "Status" + fields: [] + )yaml", + .expected_error = + "5:21: Enum type definition cannot have 'fields'\n" + "| fields: []\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "f" + type: "" + )yaml", + .expected_error = "7:31: Empty type signature\n" + "| type: \"\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "f" + type: {} + )yaml", + .expected_error = "7:31: Node 'type_name' is not specified\n" + "| type: {}\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "f" + type: "uint" + default: -1 + )yaml", + .expected_error = "8:34: Failed to parse uint constant\n" + "| default: -1\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + schemas: + unknown_schema: + name: "Foo" + )yaml", + .expected_error = "6:23: Unsupported schema: 'unknown_schema'\n" + "| unknown_schema:\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "f" + type: "string" + schemas: + schema_a: + unknown_prop: 12 + )yaml", + .expected_error = + "10:29: Unsupported 'schema_a' field property: 'unknown_prop'\n" + "| unknown_prop: 12\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "f" + type: "string" + schemas: + schema_b: + id: "not_an_int" + )yaml", + .expected_error = + "10:33: Node 'id' is not an integer\n" + "| id: \"not_an_int\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "f" + type: "string" + - name: "f" + type: "int" + )yaml", + .expected_error = "8:31: Field 'f' is already defined.\n" + "| - name: \"f\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - enum: + name: "Status" + constants: + - name: "ACTIVE" + id: "not_an_int" + )yaml", + .expected_error = "7:29: Node 'id' is not an integer\n" + "| id: \"not_an_int\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - enum: + name: "Status" + constants: + - name: "ACTIVE" + - name: "ACTIVE" + )yaml", + .expected_error = + "7:31: Enum constant 'ACTIVE' is already defined.\n" + "| - name: \"ACTIVE\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + schemas: + stub_schema: + name: "Foo" + )yaml", + .expected_error = + "7:25: Custom object properties are not supported for schema " + "'stub_schema'\n" + "| name: \"Foo\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - object: + name: "Item" + fields: + - name: "f" + type: "string" + schemas: + stub_schema: + name: "Foo" + )yaml", + .expected_error = + "10:29: Custom field properties are not supported for schema " + "'stub_schema'\n" + "| name: \"Foo\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - enum: + name: "Status" + schemas: + stub_schema: + name: "Foo" + constants: + - name: "ACTIVE" + )yaml", + .expected_error = + "7:25: Custom enum properties are not supported for schema " + "'stub_schema'\n" + "| name: \"Foo\"\n" + "| ^", + }, + MalformedYamlTestCase{ + .yaml = R"yaml( + types: + - enum: + name: "Status" + constants: + - name: "ACTIVE" + schemas: + stub_schema: + name: "Foo" + )yaml", + .expected_error = + "9:29: Custom enum constant properties are not supported for " + "schema 'stub_schema'\n" + "| name: \"Foo\"\n" + "| ^", + })); + +TEST(TypeDefYamlTest, EmptySchemaPropertiesOnUnimplementedSchemaSucceeds) { + SchemaYamlRegistry registry = MakeTestRegistry(); + ASSERT_OK_AND_ASSIGN(std::vector defs, TypeDefsFromYaml(R"yaml( + types: + - object: + name: "Item" + schemas: + stub_schema: {} + fields: + - name: "f" + type: "int" + schemas: + stub_schema: ~ + - enum: + name: "Status" + schemas: + stub_schema: {} + constants: + - name: "A" + schemas: + stub_schema: ~ + )yaml", + registry)); + ASSERT_THAT(defs, SizeIs(2)); + EXPECT_TRUE(defs[0].object_type().schema_specific_properties.empty()); + EXPECT_TRUE( + defs[0].object_type().fields[0].schema_specific_properties.empty()); + EXPECT_TRUE(defs[1].enum_type().schema_specific_properties.empty()); + EXPECT_TRUE( + defs[1].enum_type().constants[0].schema_specific_properties.empty()); +} + +TEST(TypeDefYamlTest, EncodeErrorOnUnregisteredSchema) { + SchemaYamlRegistry registry = MakeTestRegistry(); + ASSERT_OK_AND_ASSIGN(TypeDef type_def, TypeDefFromYaml(R"yaml( + object: + name: "Item" + schemas: + schema_a: + name: "Item" + fields: + - name: "f" + type: "int" + schemas: + schema_a: + name: "f" + )yaml", + registry)); + + SchemaYamlRegistry empty_registry; + std::stringstream ss; + EXPECT_THAT(TypeDefToYaml(type_def, empty_registry, ss), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema: 'schema_a'"))); + + ASSERT_OK_AND_ASSIGN(TypeDef enum_def, TypeDefFromYaml(R"yaml( + enum: + name: "Status" + constants: + - name: "A" + schemas: + schema_a: + name: "A" + )yaml", + registry)); + std::stringstream ss_enum; + EXPECT_THAT(TypeDefToYaml(enum_def, empty_registry, ss_enum), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema: 'schema_a'"))); +} + +TEST(TypeDefYamlTest, ConstantDefaultValuesRoundTrip) { + SchemaYamlRegistry registry = MakeTestRegistry(); + std::string yaml = Unindent(R"yaml( + object: + name: "com.example.Defaults" + fields: + - name: "bool_field" + type: "bool" + default: true + - name: "int_field" + type: "int" + default: 123 + - name: "uint_field" + type: "uint" + default: 456u + - name: "double_field" + type: "double" + default: 3.14 + - name: "bytes_field" + type: "bytes" + default: b"\x01\x02\x03" + - name: "duration_field" + type: "duration" + default: "10s" + - name: "timestamp_field" + type: "timestamp" + default: "2026-10-05T00:00:00Z" + )yaml"); + + ASSERT_OK_AND_ASSIGN(TypeDef def, TypeDefFromYaml(yaml, registry)); + ASSERT_TRUE(def.is_object()); + const auto& fields = def.object_type().fields; + ASSERT_THAT(fields, SizeIs(7)); + EXPECT_TRUE(fields[0].default_value.has_bool_value()); + EXPECT_EQ(fields[0].default_value.bool_value(), true); + EXPECT_TRUE(fields[1].default_value.has_int_value()); + EXPECT_EQ(fields[1].default_value.int_value(), 123); + EXPECT_TRUE(fields[2].default_value.has_uint_value()); + EXPECT_EQ(fields[2].default_value.uint_value(), 456u); + EXPECT_TRUE(fields[3].default_value.has_double_value()); + EXPECT_DOUBLE_EQ(fields[3].default_value.double_value(), 3.14); + EXPECT_TRUE(fields[4].default_value.has_bytes_value()); + EXPECT_EQ(fields[4].default_value.bytes_value(), "\x01\x02\x03"); + EXPECT_TRUE(fields[5].default_value.has_duration_value()); + EXPECT_EQ(fields[5].default_value.duration_value(), absl::Seconds(10)); + EXPECT_TRUE(fields[6].default_value.has_timestamp_value()); + EXPECT_EQ(fields[6].default_value.timestamp_value(), + absl::FromCivil(absl::CivilSecond(2026, 10, 5, 0, 0, 0), + absl::UTCTimeZone())); + + std::stringstream ss; + ASSERT_THAT(TypeDefToYaml(def, registry, ss, {.use_type_signatures = true}), + IsOk()); + ASSERT_OK_AND_ASSIGN(TypeDef def2, TypeDefFromYaml(ss.str(), registry)); + ASSERT_TRUE(def2.is_object()); + const auto& fields2 = def2.object_type().fields; + ASSERT_THAT(fields2, SizeIs(7)); + for (size_t i = 0; i < 7; ++i) { + EXPECT_EQ(fields[i].name, fields2[i].name); + EXPECT_EQ(fields[i].type, fields2[i].type); + EXPECT_EQ(fields[i].default_value, fields2[i].default_value); + } +} + +TEST(TypeDefYamlTest, YamlHelpersCoverage) { + EXPECT_EQ(internal::FormatYamlErrorMessage("yaml", "error", + YAML::Mark::null_mark()), + "error"); + + YAML::Mark mark; + mark.pos = 7; + mark.line = 0; + mark.column = 7; + EXPECT_THAT(internal::FormatYamlErrorMessage("single line", "err", mark), + HasSubstr("single line\n| ^")); + + EXPECT_THAT(internal::LoadYaml("{ invalid: [ }"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + YAML::Node seq = YAML::Load("[1, 2]"); + EXPECT_EQ(internal::GetString("yaml", seq), ""); + + YAML::Node str_node = YAML::Load("hello"); + EXPECT_THAT(internal::GetBinary("hello", str_node), IsOkAndHolds("")); + YAML::Node bad_b64 = YAML::Load("!!binary '%%%'"); + EXPECT_THAT(internal::GetBinary("!!binary '%%%'", bad_b64), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("not a valid Base64 encoded binary"))); + + EXPECT_THAT(internal::GetBool("[1]", "k", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'k' is not a boolean"))); + YAML::Node not_bool = YAML::Load("not_a_bool"); + EXPECT_THAT(internal::GetBool("not_a_bool", "k", not_bool), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'k' is not a boolean"))); + + EXPECT_THAT(internal::GetInt32("[1]", "k", seq), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'k' is not an integer"))); + YAML::Node not_int = YAML::Load("not_an_int"); + EXPECT_THAT(internal::GetInt32("not_an_int", "k", not_int), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'k' is not an integer"))); + + YAML::Node map_node = YAML::Load("a: 1\nb: 2"); + YAML::Node missing_node = YAML::Load("3"); + YAML::Node ctx = internal::GetContextNodeForKeyValue(map_node, missing_node); + EXPECT_TRUE(ctx.is(missing_node)); +} + +TEST(TypeDefYamlTest, MoreValidationErrors) { + SchemaYamlRegistry registry = MakeTestRegistry(); + + EXPECT_THAT(TypeDefFromYaml("object: [1, 2]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Object type definition is not a map"))); + EXPECT_THAT(TypeDefFromYaml("object:\n name: [1]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition 'name' is not a string"))); + EXPECT_THAT(TypeDefFromYaml("object:\n name: 'A'\n doc: [1]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition 'doc' is not a string"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n constants: []", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Object type definition cannot have 'constants'"))); + EXPECT_THAT(TypeDefFromYaml("object:\n name: 'A'\n fields: 123", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'fields' is not a sequence"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields: [123]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field is not a map"))); + EXPECT_THAT(TypeDefFromYaml( + "object:\n name: 'A'\n fields:\n - name: [1]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field name is not a string"))); + EXPECT_THAT(TypeDefFromYaml( + "object:\n name: 'A'\n fields:\n - name: 'f'", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field 'type' is not specified"))); + EXPECT_THAT( + TypeDefFromYaml( + "object:\n name: 'A'\n fields:\n - name: 'f'\n type: [1]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field 'type' is neither a string nor a map"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: ''", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Empty type signature"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'int'\n doc: [1]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field 'doc' is not a string"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'bad_type'\n default: 123", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Constant type 'bad_type' is not supported"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'int'\n default: [1]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field 'default' is not a scalar"))); + + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type:\n type_name: [1]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'type_name' is not a string"))); + EXPECT_THAT(TypeDefFromYaml( + "object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type:\n type_name: 'int'\n is_type_param: [1]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'is_type_param' is not a boolean"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type:\n type_name: 'map'\n params: 123", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'params' is not a sequence"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type:\n type_name: 'map'\n params: [123]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type parameter is not a map"))); + + EXPECT_THAT(TypeDefFromYaml("enum: [1, 2]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Enum type definition is not a map"))); + EXPECT_THAT(TypeDefFromYaml("enum:\n name: 'E'\n constants: 123", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'constants' is not a sequence"))); + EXPECT_THAT( + TypeDefFromYaml("enum:\n name: 'E'\n constants: [123]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Enum constant is not a map"))); + EXPECT_THAT( + TypeDefFromYaml("enum:\n name: 'E'\n constants:\n - name: [1]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Enum constant name is not a string"))); + EXPECT_THAT(TypeDefFromYaml( + "enum:\n name: 'E'\n constants:\n - name: 'C'\n " + "doc: [1]", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Enum constant 'doc' is not a string"))); + EXPECT_THAT(TypeDefFromYaml("enum:\n name: 'E'\n fields: []", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Enum type definition cannot have 'fields'"))); + + EXPECT_THAT(TypeDefFromYaml("[1]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid CEL type definition YAML"))); + EXPECT_THAT(TypeDefsFromYaml("[1]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition is not a map"))); + EXPECT_THAT(TypeDefsFromYaml("123", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid CEL type definitions YAML"))); + EXPECT_THAT(TypeDefsFromYaml("not_types: 1", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid CEL type definitions YAML"))); + EXPECT_THAT(TypeDefsFromYaml("types: 123", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'types' is not a sequence"))); + EXPECT_THAT( + TypeDefFromYaml("object: {}\nenum: {}", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'object' and 'enum' are mutually exclusive"))); + EXPECT_THAT(TypeDefFromYaml("foo: 1", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported type definition kind: 'foo'"))); + EXPECT_THAT( + TypeDefFromYaml("{}", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition must specify 'object' or 'enum'"))); + EXPECT_THAT(TypeDefFromYaml("? [1]: 2", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition kind is not a string"))); + + EXPECT_THAT(TypeDefFromYaml("object:\n name: 'O'\n schemas: 123", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'schemas' is not a map"))); + EXPECT_THAT(TypeDefFromYaml( + "object:\n name: 'O'\n schemas:\n ? [1]: val", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Target schema name is not a string"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'O'\n schemas:\n unsupported: {}", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema: 'unsupported'"))); + + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'O'\n fields:\n - name: 'f'\n " + "type: 'int'\n schemas: 123", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'schemas' is not a map"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'O'\n fields:\n - name: 'f'\n " + "type: 'int'\n schemas:\n ? [1]: val", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Target schema name is not a string"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'O'\n fields:\n - name: 'f'\n " + "type: 'int'\n schemas:\n unsupported: {}", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema: 'unsupported'"))); + + EXPECT_THAT( + TypeDefFromYaml("enum:\n name: 'E'\n schemas: 123\n constants: []", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'schemas' is not a map"))); + EXPECT_THAT( + TypeDefFromYaml("enum:\n name: 'E'\n schemas:\n ? [1]: val\n " + "constants: []", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Target schema name is not a string"))); + EXPECT_THAT( + TypeDefFromYaml("enum:\n name: 'E'\n schemas:\n unsupported: {}\n " + "constants: []", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema: 'unsupported'"))); + + EXPECT_THAT(TypeDefFromYaml( + "enum:\n name: 'E'\n constants:\n - name: 'C'\n " + "schemas: 123", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'schemas' is not a map"))); + EXPECT_THAT(TypeDefFromYaml( + "enum:\n name: 'E'\n constants:\n - name: 'C'\n " + "schemas:\n ? [1]: val", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Target schema name is not a string"))); + EXPECT_THAT(TypeDefFromYaml( + "enum:\n name: 'E'\n constants:\n - name: 'C'\n " + "schemas:\n unsupported: {}", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported schema: 'unsupported'"))); +} + +TEST(TypeDefYamlTest, MoreConstantParsingAndErrorBranches) { + SchemaYamlRegistry registry = MakeTestRegistry(); + + ASSERT_OK_AND_ASSIGN(TypeDef empty_def, TypeDefFromYaml("")); + EXPECT_TRUE(empty_def.name().empty()); + ASSERT_OK_AND_ASSIGN(std::vector empty_defs, TypeDefsFromYaml("")); + EXPECT_TRUE(empty_defs.empty()); + + EXPECT_THAT(TypeDefFromYaml("object:\n name: 'NoFields'", registry), IsOk()); + EXPECT_THAT(TypeDefFromYaml("enum:\n name: 'NoConstants'", registry), + IsOk()); + + EXPECT_THAT(TypeDefFromYaml("enum: {}", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition 'name' is not a string"))); + EXPECT_THAT(TypeDefFromYaml("enum:\n name: 'E'\n doc: [1]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition 'doc' is not a string"))); + + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type:\n type_name: ''", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field 'type' is not specified"))); + + // Null constant + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'null'\n default: null", + registry), + IsOk()); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'null'\n default: 'abc'", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to parse null constant"))); + + // False bool + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'bool'\n default: false", + registry), + IsOk()); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'bool'\n default: 'bad'", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to parse bool constant"))); + + // Invalid int/uint/double/duration/timestamp + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'int'\n default: 'bad'", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to parse int constant"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'uint'\n default: 'bad'", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to parse uint constant"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'double'\n default: 'bad'", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to parse double constant"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'duration'\n default: 'bad'", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to parse duration constant"))); + EXPECT_THAT( + TypeDefFromYaml("object:\n name: 'A'\n fields:\n - name: 'f'\n " + "type: 'timestamp'\n default: 'bad'", + registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Failed to parse timestamp constant"))); + + // TypeParam serialization + ObjectTypeDef type_param_def; + type_param_def.name = "ParamHolder"; + type_param_def.fields.push_back(ObjectTypeDef::Field{ + .name = "p", + .type = TypeSpec(ParamTypeSpec("T")), + }); + std::stringstream ss; + EXPECT_THAT(TypeDefToYaml(TypeDef(type_param_def), ss, + {.use_type_signatures = false}), + IsOk()); + EXPECT_THAT(ss.str(), HasSubstr("is_type_param: true")); +} + +TEST(TypeDefYamlTest, StructuredWellKnownStructAndFunctionTypesRoundTrip) { + absl::string_view yaml = R"yaml( + object: + name: "StructuredSpecialTypes" + fields: + - name: "struct_field" + type: + type_name: "google.protobuf.Struct" + - name: "list_value_field" + type: + type_name: "google.protobuf.ListValue" + - name: "any_field" + type: + type_name: "any" + - name: "timestamp_field" + type: + type_name: "timestamp" + - name: "duration_field" + type: + type_name: "duration" + - name: "func_field" + type: + type_name: "function" + params: + - type_name: "bool" + - type_name: "int" + - type_name: "string" + )yaml"; + + ASSERT_OK_AND_ASSIGN(TypeDef def, TypeDefFromYaml(yaml)); + ASSERT_TRUE(def.is_object()); + const auto& fields = def.object_type().fields; + ASSERT_THAT(fields, SizeIs(6)); + + EXPECT_EQ( + fields[0].type, + TypeSpec(MapTypeSpec(std::make_unique(PrimitiveType::kString), + std::make_unique(DynTypeSpec())))); + EXPECT_EQ(fields[1].type, + TypeSpec(ListTypeSpec(std::make_unique(DynTypeSpec())))); + EXPECT_EQ(fields[2].type, TypeSpec(WellKnownTypeSpec::kAny)); + EXPECT_EQ(fields[3].type, TypeSpec(WellKnownTypeSpec::kTimestamp)); + EXPECT_EQ(fields[4].type, TypeSpec(WellKnownTypeSpec::kDuration)); + EXPECT_EQ(fields[5].type, + TypeSpec(FunctionTypeSpec( + std::make_unique(PrimitiveType::kBool), + {TypeSpec(PrimitiveType::kInt64), + TypeSpec(PrimitiveType::kString)}))); + + std::stringstream emitted; + ASSERT_THAT(TypeDefToYaml(def, emitted, {.use_type_signatures = false}), + IsOk()); + EXPECT_EQ(emitted.str(), Unindent(R"yaml( + object: + name: "StructuredSpecialTypes" + fields: + - name: "struct_field" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "dyn" + - name: "list_value_field" + type: + type_name: "list" + params: + - type_name: "dyn" + - name: "any_field" + type: + type_name: "any" + - name: "timestamp_field" + type: + type_name: "timestamp" + - name: "duration_field" + type: + type_name: "duration" + - name: "func_field" + type: + type_name: "function" + params: + - type_name: "bool" + - type_name: "int" + - type_name: "string" + )yaml")); +} + +TEST(TypeDefYamlTest, TypeDefSetRoundTripAndDuplicateError) { + SchemaYamlRegistry registry = MakeTestRegistry(); + absl::string_view yaml = R"yaml( + - object: + name: "com.example.Obj" + - enum: + name: "com.example.Enum" + )yaml"; + ASSERT_OK_AND_ASSIGN(TypeDefSet set, TypeDefSetFromYaml(yaml, registry)); + EXPECT_EQ(set.types().size(), 2); + EXPECT_NE(set.FindTypeDef("com.example.Obj"), nullptr); + EXPECT_NE(set.FindTypeDef("com.example.Enum"), nullptr); + + std::stringstream ss1; + ASSERT_THAT(TypeDefSetToYaml(set, ss1, {}, registry), IsOk()); + std::stringstream ss2; + ASSERT_THAT(TypeDefSetToYaml(set, registry, ss2), IsOk()); + EXPECT_EQ(ss1.str(), ss2.str()); + + ASSERT_OK_AND_ASSIGN(TypeDefSet roundtrip, + TypeDefSetFromYaml(ss1.str(), registry)); + EXPECT_EQ(roundtrip.types().size(), 2); + + absl::string_view dup_yaml = R"yaml( + - object: + name: "com.example.Dup" + - object: + name: "com.example.Dup" + )yaml"; + EXPECT_THAT( + TypeDefSetFromYaml(dup_yaml, registry), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Type 'com.example.Dup' is already defined."))); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/yaml_helpers.cc b/common/typedef/yaml_helpers.cc new file mode 100644 index 000000000..0ccf2066f --- /dev/null +++ b/common/typedef/yaml_helpers.cc @@ -0,0 +1,153 @@ +// Copyright 2026 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 "common/typedef/yaml_helpers.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/escaping.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "yaml-cpp/exceptions.h" +#include "yaml-cpp/mark.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/node/parse.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel::internal { + +std::string FormatYamlErrorMessage(absl::string_view yaml, + absl::string_view error, + const YAML::Mark& mark) { + if (mark.is_null()) { + return std::string(error); + } + std::string message; + absl::StrAppend(&message, mark.line + 1, ":", mark.column + 1, ": ", error, + "\n|"); + size_t start = mark.pos - mark.column; + size_t end = yaml.find('\n', mark.pos); + if (end == std::string::npos) { + end = yaml.size(); + } + + absl::StrAppend(&message, yaml.substr(start, end - start), "\n|", + std::string(mark.column, ' '), "^"); + + return message; +} + +absl::StatusOr LoadYaml(absl::string_view yaml) { + try { + return YAML::Load(std::string(yaml)); + } catch (YAML::ParserException& e) { + return absl::InvalidArgumentError( + FormatYamlErrorMessage(yaml, e.msg, e.mark)); + } +} + +absl::Status YamlError(absl::string_view yaml, const YAML::Node& node, + absl::string_view error) { + return absl::InvalidArgumentError( + FormatYamlErrorMessage(yaml, error, node.Mark())); +} + +bool IsEmptyYamlNode(const YAML::Node& node) { + if (!node.IsDefined() || node.IsNull()) { + return true; + } + if (node.IsScalar()) { + return node.Scalar().empty(); + } + return node.size() == 0; +} + +std::string GetString(absl::string_view yaml, const YAML::Node& node) { + if (!node.IsDefined() || !node.IsScalar()) { + return ""; + } + try { + return node.as(); + } catch (YAML::Exception& e) { + // This should never happen since we already checked that the node is a + // scalar and all scalars can be converted to strings. + return ""; + } +} + +bool IsBinary(const YAML::Node& node) { + return node.Tag() == "!!binary" || node.Tag() == "tag:yaml.org,2002:binary"; +} + +absl::StatusOr GetBinary(absl::string_view yaml, + const YAML::Node& node) { + if (!node.IsDefined() || !node.IsScalar() || !IsBinary(node)) { + return ""; + } + std::string binary; + // Instead of using the YAML::Binary type, we use absl::Base64Unescape + // because YAML::Binary is lenient to Base64 decoding errors. + if (absl::Base64Unescape(GetString(yaml, node), &binary)) { + return binary; + } else { + return YamlError(yaml, node, + absl::StrCat("Node '", GetString(yaml, node), + "' is not a valid Base64 encoded binary")); + } +} + +absl::StatusOr GetBool(absl::string_view yaml, absl::string_view key, + const YAML::Node& node) { + if (!node.IsDefined() || !node.IsScalar()) { + return YamlError(yaml, node, + absl::StrCat("Node '", key, "' is not a boolean")); + } + try { + return node.as(); + } catch (YAML::Exception& e) { + return YamlError(yaml, node, + absl::StrCat("Node '", key, "' is not a boolean")); + } +} + +absl::StatusOr GetInt32(absl::string_view yaml, absl::string_view key, + const YAML::Node& node) { + if (!node.IsDefined() || !node.IsScalar()) { + return YamlError(yaml, node, + absl::StrCat("Node '", key, "' is not an integer")); + } + int32_t value = 0; + if (!absl::SimpleAtoi(GetString(yaml, node), &value)) { + return YamlError(yaml, node, + absl::StrCat("Node '", key, "' is not an integer")); + } + return value; +} + +YAML::Node GetContextNodeForKeyValue(const YAML::Node& node, + const YAML::Node& value_node) { + for (const auto& kv : node) { + if (kv.second.IsDefined() && kv.second.is(value_node)) { + return kv.first; + } + } + return value_node; +} + +} // namespace cel::internal diff --git a/common/typedef/yaml_helpers.h b/common/typedef/yaml_helpers.h new file mode 100644 index 000000000..46d04afc6 --- /dev/null +++ b/common/typedef/yaml_helpers.h @@ -0,0 +1,61 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_YAML_HELPERS_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_YAML_HELPERS_H_ + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "yaml-cpp/mark.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel::internal { + +std::string FormatYamlErrorMessage(absl::string_view yaml, + absl::string_view error, + const YAML::Mark& mark); + +absl::StatusOr LoadYaml(absl::string_view yaml); + +absl::Status YamlError(absl::string_view yaml, const YAML::Node& node, + absl::string_view error); + +bool IsEmptyYamlNode(const YAML::Node& node); + +std::string GetString(absl::string_view yaml, const YAML::Node& node); + +bool IsBinary(const YAML::Node& node); + +absl::StatusOr GetBinary(absl::string_view yaml, + const YAML::Node& node); + +absl::StatusOr GetBool(absl::string_view yaml, absl::string_view key, + const YAML::Node& node); + +absl::StatusOr GetInt32(absl::string_view yaml, absl::string_view key, + const YAML::Node& node); + +// Returns the key in the map `node` that has the given `value_node` as its +// value. If no such key exists, returns `value_node` itself. +YAML::Node GetContextNodeForKeyValue(const YAML::Node& node, + const YAML::Node& value_node); + +} // namespace cel::internal + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_YAML_HELPERS_H_ diff --git a/common/typedef/yaml_test_helpers.cc b/common/typedef/yaml_test_helpers.cc new file mode 100644 index 000000000..f24bf0025 --- /dev/null +++ b/common/typedef/yaml_test_helpers.cc @@ -0,0 +1,48 @@ +// Copyright 2026 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 "common/typedef/yaml_test_helpers.h" + +#include +#include +#include + +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" + +namespace cel { + +std::string Unindent(absl::string_view yaml) { + std::vector lines = absl::StrSplit(yaml, '\n'); + int indent = -1; + std::vector unindented_lines; + for (const auto& line : lines) { + std::size_t pos = line.find_first_not_of(" \t"); + if (pos == std::string::npos) { + continue; + } + if (indent == -1) { + indent = pos; + } + if (static_cast(pos) >= indent) { + unindented_lines.push_back(line.substr(indent)); + } else { + unindented_lines.push_back(line); + } + } + return absl::StrJoin(unindented_lines, "\n"); +} + +} // namespace cel diff --git a/common/typedef/yaml_test_helpers.h b/common/typedef/yaml_test_helpers.h new file mode 100644 index 000000000..76eb870bd --- /dev/null +++ b/common/typedef/yaml_test_helpers.h @@ -0,0 +1,30 @@ +// Copyright 2026 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 THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_YAML_TEST_HELPERS_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_YAML_TEST_HELPERS_H_ + +#include + +#include "absl/strings/string_view.h" + +namespace cel { + +// Strips common leading whitespace indentation from non-empty lines in `yaml` +// and trims leading and trailing empty or whitespace-only lines. +std::string Unindent(absl::string_view yaml); + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_YAML_TEST_HELPERS_H_