From 6f28ac172afa6c2696ca2690ec16da25797d7acb Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Mon, 5 Oct 2026 15:53:20 -0700 Subject: [PATCH] [cel-types] Add YAML encoding and decoding for CEL TypeDef and aspects. Introduce `AspectYaml` and `AspectYamlRegistry` in `common/typedef/aspect_yaml` for pluggable YAML encoding and decoding of aspects (e.g. target-schema-specific properties). Implement `TypeDefFromYaml` and `TypeDefToYaml` in `common/typedef/typedef_yaml`, supporting both concise type signatures (`list`) and structured `TypeSpec` representations as well as delegating `aspects` blocks to registered `AspectYaml` codecs. PiperOrigin-RevId: 993955156 --- common/typedef/BUILD | 322 ++++++ common/typedef/aspect_proto.cc | 95 ++ common/typedef/aspect_proto.h | 125 ++ common/typedef/aspect_proto_test.cc | 126 +++ common/typedef/aspect_yaml.cc | 118 ++ common/typedef/aspect_yaml.h | 111 ++ common/typedef/aspect_yaml_test.cc | 153 +++ common/typedef/proto_schema.h | 130 +++ common/typedef/proto_schema.proto | 64 ++ common/typedef/proto_schema_proto.cc | 205 ++++ common/typedef/proto_schema_proto.h | 95 ++ common/typedef/proto_schema_proto_test.cc | 168 +++ common/typedef/testdata/BUILD | 21 + .../typedef/testdata/cel_type_errors.baseline | 920 +++++++++++++++ common/typedef/testdata/cel_type_errors.yaml | 498 ++++++++ common/typedef/testdata/cel_types.txtproto | 217 ++++ common/typedef/testdata/cel_types.yaml | 257 +++++ .../testdata/cel_types_from_yaml.baseline | 477 ++++++++ .../testdata/cel_types_to_yaml.baseline | 244 ++++ common/typedef/testdata/typedefs.txtproto | 273 +++++ common/typedef/typedef.cc | 81 ++ common/typedef/typedef.h | 209 ++++ common/typedef/typedef.proto | 70 ++ common/typedef/typedef_proto.cc | 625 ++++++++++ common/typedef/typedef_proto.h | 103 ++ common/typedef/typedef_proto_test.cc | 245 ++++ common/typedef/typedef_test.cc | 228 ++++ common/typedef/typedef_yaml.cc | 1002 +++++++++++++++++ common/typedef/typedef_yaml.h | 66 ++ common/typedef/typedef_yaml_test.cc | 573 ++++++++++ common/typedef/yaml_helpers.cc | 154 +++ common/typedef/yaml_helpers.h | 61 + common/typedef/yaml_test_helpers.cc | 48 + common/typedef/yaml_test_helpers.h | 30 + 34 files changed, 8114 insertions(+) create mode 100644 common/typedef/BUILD create mode 100644 common/typedef/aspect_proto.cc create mode 100644 common/typedef/aspect_proto.h create mode 100644 common/typedef/aspect_proto_test.cc create mode 100644 common/typedef/aspect_yaml.cc create mode 100644 common/typedef/aspect_yaml.h create mode 100644 common/typedef/aspect_yaml_test.cc create mode 100644 common/typedef/proto_schema.h create mode 100644 common/typedef/proto_schema.proto create mode 100644 common/typedef/proto_schema_proto.cc create mode 100644 common/typedef/proto_schema_proto.h create mode 100644 common/typedef/proto_schema_proto_test.cc create mode 100644 common/typedef/testdata/BUILD create mode 100644 common/typedef/testdata/cel_type_errors.baseline create mode 100644 common/typedef/testdata/cel_type_errors.yaml create mode 100644 common/typedef/testdata/cel_types.txtproto create mode 100644 common/typedef/testdata/cel_types.yaml create mode 100644 common/typedef/testdata/cel_types_from_yaml.baseline create mode 100644 common/typedef/testdata/cel_types_to_yaml.baseline create mode 100644 common/typedef/testdata/typedefs.txtproto create mode 100644 common/typedef/typedef.cc create mode 100644 common/typedef/typedef.h create mode 100644 common/typedef/typedef.proto create mode 100644 common/typedef/typedef_proto.cc create mode 100644 common/typedef/typedef_proto.h create mode 100644 common/typedef/typedef_proto_test.cc create mode 100644 common/typedef/typedef_test.cc create mode 100644 common/typedef/typedef_yaml.cc create mode 100644 common/typedef/typedef_yaml.h create mode 100644 common/typedef/typedef_yaml_test.cc create mode 100644 common/typedef/yaml_helpers.cc create mode 100644 common/typedef/yaml_helpers.h create mode 100644 common/typedef/yaml_test_helpers.cc create mode 100644 common/typedef/yaml_test_helpers.h diff --git a/common/typedef/BUILD b/common/typedef/BUILD new file mode 100644 index 000000000..ebec3564f --- /dev/null +++ b/common/typedef/BUILD @@ -0,0 +1,322 @@ +# 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 = "typedef", + srcs = ["typedef.cc"], + hdrs = ["typedef.h"], + deps = [ + "//common:ast", + "//common:constant", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/base:nullability", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/functional:overload", + "@com_google_absl//absl/log:absl_check", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_test( + name = "typedef_test", + srcs = ["typedef_test.cc"], + deps = [ + ":typedef", + "//common:ast", + "//common:constant", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + ], +) + +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 = "aspect_proto", + srcs = ["aspect_proto.cc"], + hdrs = ["aspect_proto.h"], + deps = [ + ":typedef_cc_proto", + "@com_google_absl//absl/base:core_headers", + "@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 = "aspect_proto_test", + srcs = ["aspect_proto_test.cc"], + deps = [ + ":aspect_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 = [ + ":aspect_proto", + ":typedef", + ":typedef_cc_proto", + "//common:ast", + "//common:constant", + "//common/ast:constant_proto", + "//internal:status_macros", + "@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/types:span", + "@com_google_absl//absl/types:variant", + "@com_google_cel_spec//proto/cel/expr:checked_cc_proto", + "@com_google_protobuf//:struct_cc_proto", + ], +) + +cc_library( + name = "proto_schema", + hdrs = ["proto_schema.h"], + deps = [ + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/strings:string_view", + ], +) + +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 = [ + ":aspect_proto", + ":proto_schema", + ":proto_schema_cc_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 = [ + ":aspect_proto", + ":proto_schema", + ":proto_schema_cc_proto", + ":proto_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"], + data = [ + "//common/typedef/testdata:typedef_testdata", + ], + deps = [ + ":aspect_proto", + ":proto_schema_proto", + ":typedef", + ":typedef_cc_proto", + ":typedef_proto", + "//internal:proto_matchers", + "//internal:runfiles", + "//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_protobuf//:protobuf", + ], +) + +# NOTE: The YAML targets below depend on yaml-cpp (//third_party/yamlcpp), +# which is used for open-source CEL compatibility (and therefore cannot be +# trivially migrated to Crubit/Rust YAML parsers) and requires C++ exceptions. +# The implementation using //third_party/yamlcpp carefully catches and handles +# exceptions, so code using these targets does not need the -fexceptions flag. + +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 = "aspect_yaml", + srcs = ["aspect_yaml.cc"], + hdrs = ["aspect_yaml.h"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":yaml_helpers", + "@com_google_absl//absl/base:core_headers", + "@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 = "aspect_yaml_test", + srcs = ["aspect_yaml_test.cc"], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":aspect_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 = "typedef_yaml", + srcs = ["typedef_yaml.cc"], + hdrs = [ + "typedef_yaml.h", + ], + copts = ["-fexceptions"], + features = ["-use_header_modules"], + deps = [ + ":aspect_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/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:str_format", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/time", + "@yaml-cpp", + ], +) + +cc_test( + name = "typedef_yaml_test", + srcs = ["typedef_yaml_test.cc"], + copts = ["-fexceptions"], + data = [ + "//common/typedef/testdata:typedef_testdata", + ], + features = ["-use_header_modules"], + deps = [ + ":aspect_yaml", + ":typedef", + ":typedef_cc_proto", + ":typedef_proto", + ":typedef_yaml", + ":yaml_helpers", + ":yaml_test_helpers", + "//internal:proto_matchers", + "//internal:runfiles", + "//internal:status_macros", + "//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/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@com_google_protobuf//:protobuf", + "@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", + ], +) diff --git a/common/typedef/aspect_proto.cc b/common/typedef/aspect_proto.cc new file mode 100644 index 000000000..ddc23d370 --- /dev/null +++ b/common/typedef/aspect_proto.cc @@ -0,0 +1,95 @@ +// 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/aspect_proto.h" + +#include +#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/typedef.pb.h" + +namespace cel { + +absl::StatusOr AspectProto::DecodeStructProperties( + const types::Struct& proto) const { + return std::any(); +} + +absl::StatusOr AspectProto::DecodeFieldProperties( + const types::Struct::Field& proto) const { + return std::any(); +} + +absl::StatusOr AspectProto::DecodeEnumProperties( + const types::Enum& proto) const { + return std::any(); +} + +absl::StatusOr AspectProto::DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const { + return std::any(); +} + +absl::Status AspectProto::EncodeStructProperties( + const std::any& properties, types::Struct* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom struct properties are not supported for aspect '", name(), "'")); +} + +absl::Status AspectProto::EncodeFieldProperties( + const std::any& properties, + types::Struct::Field* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom field properties are not supported for aspect '", name(), "'")); +} + +absl::Status AspectProto::EncodeEnumProperties( + const std::any& properties, types::Enum* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum properties are not supported for aspect '", name(), "'")); +} + +absl::Status AspectProto::EncodeEnumConstantProperties( + const std::any& properties, + types::Enum::EnumConstant* absl_nonnull proto) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum constant properties are not supported for aspect '", name(), + "'")); +} + +AspectProtoRegistry::AspectProtoRegistry( + std::vector> aspects) { + aspects_.reserve(aspects.size()); + for (std::unique_ptr& aspect : aspects) { + Add(std::move(aspect)); + } +} + +const AspectProto* absl_nullable AspectProtoRegistry::Find( + absl::string_view aspect_name) const { + auto it = aspects_.find(aspect_name); + if (it == aspects_.end()) { + return nullptr; + } + return it->second.get(); +} + +} // namespace cel diff --git a/common/typedef/aspect_proto.h b/common/typedef/aspect_proto.h new file mode 100644 index 000000000..c618989da --- /dev/null +++ b/common/typedef/aspect_proto.h @@ -0,0 +1,125 @@ +// 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_ASPECT_PROTO_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_ASPECT_PROTO_H_ + +#include +#include +#include +#include +#include +#include + +#include "absl/base/attributes.h" +#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/typedef.pb.h" + +namespace cel { + +// Interface representing a Protobuf codec for an aspect (e.g. "json", +// "proto2", "proto3") capable of decoding and encoding aspect property +// objects via proto extensions on cel.types messages. +class AspectProto { + public: + virtual ~AspectProto() = default; + + // Returns the unique aspect identifier (e.g. "json", "proto2", "proto3"). + virtual absl::string_view name() const = 0; + + // Decodes struct aspect properties from extensions on `proto`. + // Returns an empty std::any if this aspect has no properties present on + // `proto`. + virtual absl::StatusOr DecodeStructProperties( + const types::Struct& proto) const; + + // Decodes field aspect properties from extensions on `proto`. + // Returns an empty std::any if this aspect has no properties present on + // `proto`. + virtual absl::StatusOr DecodeFieldProperties( + const types::Struct::Field& proto) const; + + // Decodes enum aspect properties from extensions on `proto`. + // Returns an empty std::any if this aspect has no properties present on + // `proto`. + virtual absl::StatusOr DecodeEnumProperties( + const types::Enum& proto) const; + + // Decodes enum constant aspect properties from extensions on + // `proto`. Returns an empty std::any if this aspect has no properties present + // on `proto`. + virtual absl::StatusOr DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const; + + virtual absl::Status EncodeStructProperties( + const std::any& properties, types::Struct* absl_nonnull proto) const; + + virtual absl::Status EncodeFieldProperties( + const std::any& properties, + types::Struct::Field* absl_nonnull proto) const; + + virtual absl::Status EncodeEnumProperties( + const std::any& properties, types::Enum* absl_nonnull proto) const; + + virtual absl::Status EncodeEnumConstantProperties( + const std::any& properties, + types::Enum::EnumConstant* absl_nonnull proto) const; +}; + +// Read-only registry of AspectProto codecs keyed by aspect name. +class AspectProtoRegistry { + public: + AspectProtoRegistry() = default; + + explicit AspectProtoRegistry( + std::vector> aspects); + + template < + typename... Aspects, + typename = std::enable_if_t< + (sizeof...(Aspects) > 0) && + (std::is_convertible_v> && + ...)>> + explicit AspectProtoRegistry(Aspects&&... aspects) { + aspects_.reserve(sizeof...(Aspects)); + (Add(std::forward(aspects)), ...); + } + + // Finds a registered AspectProto by name, or returns nullptr if not found. + const AspectProto* absl_nullable Find(absl::string_view aspect_name) const + ABSL_ATTRIBUTE_LIFETIME_BOUND; + + const absl::flat_hash_map>& + aspects() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return aspects_; + } + + private: + void Add(std::unique_ptr aspect) { + if (aspect != nullptr) { + absl::string_view name = aspect->name(); + aspects_[name] = std::move(aspect); + } + } + + absl::flat_hash_map> aspects_; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_ASPECT_PROTO_H_ diff --git a/common/typedef/aspect_proto_test.cc b/common/typedef/aspect_proto_test.cc new file mode 100644 index 000000000..d9777f3d6 --- /dev/null +++ b/common/typedef/aspect_proto_test.cc @@ -0,0 +1,126 @@ +// 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/aspect_proto.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/string_view.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; +using ::testing::Property; + +class StubAspectProto final : public AspectProto { + public: + explicit StubAspectProto(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + private: + std::string name_; +}; + +TEST(AspectProtoTest, DefaultDecodeReturnsEmptyAny) { + StubAspectProto aspect("stub"); + + types::Struct struct_proto; + types::Struct::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant enum_const_proto; + + EXPECT_THAT(aspect.DecodeStructProperties(struct_proto), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect.DecodeFieldProperties(field_proto), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect.DecodeEnumProperties(enum_proto), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect.DecodeEnumConstantProperties(enum_const_proto), + IsOkAndHolds(Property(&std::any::has_value, false))); +} + +TEST(AspectProtoTest, DefaultEncodeReturnsUnimplementedError) { + StubAspectProto aspect("stub"); + types::Struct struct_proto; + types::Struct::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant enum_const_proto; + + std::any props = 123; + + EXPECT_THAT(aspect.EncodeStructProperties(props, &struct_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom struct properties are not supported for " + "aspect 'stub'"))); + EXPECT_THAT(aspect.EncodeFieldProperties(props, &field_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom field properties are not supported for " + "aspect 'stub'"))); + EXPECT_THAT(aspect.EncodeEnumProperties(props, &enum_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum properties are not supported for " + "aspect 'stub'"))); + EXPECT_THAT(aspect.EncodeEnumConstantProperties(props, &enum_const_proto), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum constant properties are not supported " + "for aspect 'stub'"))); +} + +TEST(AspectProtoRegistryTest, ConstructAndFindAspects) { + AspectProtoRegistry empty_registry; + EXPECT_THAT(empty_registry.Find("aspect_a"), IsNull()); + EXPECT_TRUE(empty_registry.aspects().empty()); + + AspectProtoRegistry registry(std::make_unique("aspect_a"), + std::make_unique("aspect_b")); + + const AspectProto* aspect_a = registry.Find("aspect_a"); + ASSERT_THAT(aspect_a, NotNull()); + EXPECT_EQ(aspect_a->name(), "aspect_a"); + + const AspectProto* aspect_b = registry.Find("aspect_b"); + ASSERT_THAT(aspect_b, NotNull()); + EXPECT_EQ(aspect_b->name(), "aspect_b"); + + EXPECT_THAT(registry.Find("unknown_aspect"), IsNull()); + EXPECT_EQ(registry.aspects().size(), 2); +} + +TEST(AspectProtoRegistryTest, ConstructFromVector) { + std::vector> aspects; + aspects.push_back(std::make_unique("aspect_a")); + aspects.push_back(nullptr); + AspectProtoRegistry registry(std::move(aspects)); + + EXPECT_THAT(registry.Find("aspect_a"), NotNull()); + EXPECT_THAT(registry.Find("aspect_b"), IsNull()); + EXPECT_EQ(registry.aspects().size(), 1); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/aspect_yaml.cc b/common/typedef/aspect_yaml.cc new file mode 100644 index 000000000..8eb6cb6bd --- /dev/null +++ b/common/typedef/aspect_yaml.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/aspect_yaml.h" + +#include +#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/yaml_helpers.h" + +namespace cel { + +absl::StatusOr AspectYaml::DecodeStructProperties( + absl::string_view yaml, const YAML::Node& node) const { + if (typedef_internal::IsEmptyYamlNode(node)) { + return std::any(); + } + return typedef_internal::YamlError( + yaml, node, + absl::StrCat("Custom struct properties are not supported for aspect '", + name(), "'")); +} + +absl::StatusOr AspectYaml::DecodeFieldProperties( + absl::string_view yaml, const YAML::Node& node) const { + if (typedef_internal::IsEmptyYamlNode(node)) { + return std::any(); + } + return typedef_internal::YamlError( + yaml, node, + absl::StrCat("Custom field properties are not supported for aspect '", + name(), "'")); +} + +absl::StatusOr AspectYaml::DecodeEnumProperties( + absl::string_view yaml, const YAML::Node& node) const { + if (typedef_internal::IsEmptyYamlNode(node)) { + return std::any(); + } + return typedef_internal::YamlError( + yaml, node, + absl::StrCat("Custom enum properties are not supported for aspect '", + name(), "'")); +} + +absl::StatusOr AspectYaml::DecodeEnumConstantProperties( + absl::string_view yaml, const YAML::Node& node) const { + if (typedef_internal::IsEmptyYamlNode(node)) { + return std::any(); + } + return typedef_internal::YamlError( + yaml, node, + absl::StrCat( + "Custom enum constant properties are not supported for aspect '", + name(), "'")); +} + +absl::Status AspectYaml::EncodeStructProperties(const std::any& properties, + YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom struct properties are not supported for aspect '", name(), "'")); +} + +absl::Status AspectYaml::EncodeFieldProperties(const std::any& properties, + YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom field properties are not supported for aspect '", name(), "'")); +} + +absl::Status AspectYaml::EncodeEnumProperties(const std::any& properties, + YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum properties are not supported for aspect '", name(), "'")); +} + +absl::Status AspectYaml::EncodeEnumConstantProperties( + const std::any& properties, YAML::Emitter& out) const { + return absl::UnimplementedError(absl::StrCat( + "Custom enum constant properties are not supported for aspect '", name(), + "'")); +} + +AspectYamlRegistry::AspectYamlRegistry( + std::vector> aspects) { + aspects_.reserve(aspects.size()); + for (std::unique_ptr& aspect : aspects) { + Add(std::move(aspect)); + } +} + +const AspectYaml* absl_nullable AspectYamlRegistry::Find( + absl::string_view aspect_name) const { + auto it = aspects_.find(aspect_name); + if (it == aspects_.end()) { + return nullptr; + } + return it->second.get(); +} + +} // namespace cel diff --git a/common/typedef/aspect_yaml.h b/common/typedef/aspect_yaml.h new file mode 100644 index 000000000..dd7680d16 --- /dev/null +++ b/common/typedef/aspect_yaml.h @@ -0,0 +1,111 @@ +// 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_ASPECT_YAML_H_ +#define THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_ASPECT_YAML_H_ + +#include +#include +#include +#include +#include +#include + +#include "absl/base/attributes.h" +#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" + +namespace YAML { +class Emitter; +class Node; +} // namespace YAML + +namespace cel { + +// Interface representing a YAML codec for an aspect (e.g. "json", +// "proto2", "proto3") capable of decoding and encoding aspect property +// objects. +class AspectYaml { + public: + virtual ~AspectYaml() = default; + + // Returns the unique aspect identifier (e.g. "json", "proto2", "proto3"). + virtual absl::string_view name() const = 0; + + virtual absl::StatusOr DecodeStructProperties( + 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 EncodeStructProperties(const std::any& properties, + YAML::Emitter& out) const; + + virtual absl::Status EncodeFieldProperties(const std::any& properties, + YAML::Emitter& out) const; + + virtual absl::Status EncodeEnumProperties(const std::any& properties, + YAML::Emitter& out) const; + + virtual absl::Status EncodeEnumConstantProperties(const std::any& properties, + YAML::Emitter& out) const; +}; + +// Read-only registry of AspectYaml codecs keyed by aspect name. +class AspectYamlRegistry { + public: + AspectYamlRegistry() = default; + + explicit AspectYamlRegistry( + std::vector> aspects); + + template < + typename... Aspects, + typename = std::enable_if_t< + (sizeof...(Aspects) > 0) && + (std::is_convertible_v> && + ...)>> + explicit AspectYamlRegistry(Aspects&&... aspects) { + aspects_.reserve(sizeof...(Aspects)); + (Add(std::forward(aspects)), ...); + } + + // Finds a registered AspectYaml by name, or returns nullptr if not + // found. + const AspectYaml* absl_nullable Find(absl::string_view aspect_name) const + ABSL_ATTRIBUTE_LIFETIME_BOUND; + + private: + void Add(std::unique_ptr aspect) { + if (aspect != nullptr) { + absl::string_view name = aspect->name(); + aspects_[name] = std::move(aspect); + } + } + + absl::flat_hash_map> aspects_; +}; + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_ASPECT_YAML_H_ diff --git a/common/typedef/aspect_yaml_test.cc b/common/typedef/aspect_yaml_test.cc new file mode 100644 index 000000000..0e9cd56f9 --- /dev/null +++ b/common/typedef/aspect_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/aspect_yaml.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/string_view.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; +using ::testing::Property; + +class StubAspectYaml final : public AspectYaml { + public: + explicit StubAspectYaml(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + private: + std::string name_; +}; + +TEST(AspectYamlTest, DefaultDecodeSucceedsOnEmptyNodes) { + StubAspectYaml aspect("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(aspect.DecodeStructProperties("", node), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect.DecodeFieldProperties("", node), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect.DecodeEnumProperties("", node), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect.DecodeEnumConstantProperties("", node), + IsOkAndHolds(Property(&std::any::has_value, false))); + } +} + +TEST(AspectYamlTest, DefaultDecodeFailsWithYamlErrorOnNonEmptyNode) { + StubAspectYaml aspect("stub"); + absl::string_view yaml = "prop: 123"; + YAML::Node node = YAML::Load(std::string(yaml)); + + EXPECT_THAT(aspect.DecodeStructProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom struct properties are not supported for " + "aspect 'stub'\n|prop: 123\n|^"))); + EXPECT_THAT(aspect.DecodeFieldProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom field properties are not supported for " + "aspect 'stub'\n|prop: 123\n|^"))); + EXPECT_THAT(aspect.DecodeEnumProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom enum properties are not supported for " + "aspect 'stub'\n|prop: 123\n|^"))); + EXPECT_THAT( + aspect.DecodeEnumConstantProperties(yaml, node), + StatusIs(absl::StatusCode::kInvalidArgument, + Eq("1:1: Custom enum constant properties are not supported for " + "aspect 'stub'\n|prop: 123\n|^"))); +} + +TEST(AspectYamlTest, DefaultEncodeReturnsUnimplementedError) { + StubAspectYaml aspect("stub"); + YAML::Emitter emitter; + std::any props = 123; + + EXPECT_THAT(aspect.EncodeStructProperties(props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom struct properties are not supported for " + "aspect 'stub'"))); + EXPECT_THAT(aspect.EncodeFieldProperties(props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom field properties are not supported for " + "aspect 'stub'"))); + EXPECT_THAT(aspect.EncodeEnumProperties(props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum properties are not supported for " + "aspect 'stub'"))); + EXPECT_THAT( + aspect.EncodeEnumConstantProperties(props, emitter), + StatusIs(absl::StatusCode::kUnimplemented, + Eq("Custom enum constant properties are not supported for " + "aspect 'stub'"))); +} + +TEST(AspectYamlRegistryTest, ConstructAndFindAspects) { + AspectYamlRegistry empty_registry; + EXPECT_THAT(empty_registry.Find("aspect_a"), IsNull()); + + AspectYamlRegistry registry(std::make_unique("aspect_a"), + std::make_unique("aspect_b")); + + const AspectYaml* aspect_a = registry.Find("aspect_a"); + ASSERT_THAT(aspect_a, NotNull()); + EXPECT_EQ(aspect_a->name(), "aspect_a"); + + const AspectYaml* aspect_b = registry.Find("aspect_b"); + ASSERT_THAT(aspect_b, NotNull()); + EXPECT_EQ(aspect_b->name(), "aspect_b"); + + EXPECT_THAT(registry.Find("unknown_aspect"), IsNull()); +} + +TEST(AspectYamlRegistryTest, ConstructFromVector) { + std::vector> aspects; + aspects.push_back(std::make_unique("aspect_a")); + aspects.push_back(nullptr); + AspectYamlRegistry registry(std::move(aspects)); + + EXPECT_THAT(registry.Find("aspect_a"), NotNull()); + EXPECT_THAT(registry.Find("aspect_b"), IsNull()); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/proto_schema.h b/common/typedef/proto_schema.h new file mode 100644 index 000000000..a4d671edb --- /dev/null +++ b/common/typedef/proto_schema.h @@ -0,0 +1,130 @@ +// 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/base/attributes.h" +#include "absl/strings/string_view.h" + +namespace cel { + +inline constexpr absl::string_view kProto2AspectName = "proto2"; +inline constexpr absl::string_view kProto3AspectName = "proto3"; + +// Protocol Buffers schema properties attached to a StructTypeDef. +class ProtoStructProperties final { + public: + ProtoStructProperties() = default; + explicit ProtoStructProperties(std::optional name) + : name_(std::move(name)) {} + + // Overrides the canonical CEL struct name with a target-specific protobuf + // message name when they differ (e.g., mapping a canonical CEL type + // "com.example.Order" to "com.example.proto2.Order" in proto2 and + // "com.example.proto3.Order" in proto3). + const std::optional& name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return name_; + } + + private: + std::optional name_; +}; + +// Protocol Buffers schema properties attached to a StructTypeDef::Field. +class ProtoFieldProperties final { + public: + ProtoFieldProperties() = default; + explicit ProtoFieldProperties( + std::optional name, std::optional id = std::nullopt, + std::optional json_name = std::nullopt) + : name_(std::move(name)), id_(id), json_name_(std::move(json_name)) {} + + // Overrides the canonical CEL field name with the protobuf field name when + // they differ (e.g., mapping a CEL field "order_id" to a legacy proto2 field + // "order_id_v2", or mapping a camelCase CEL field "userId" to "user_id"). + const std::optional& name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return name_; + } + + // The protobuf field number (tag) in the message definition (e.g., `1` in + // `string order_id = 1;`), required for wire serialization and descriptor + // generation. + std::optional id() const { return id_; } + + // Custom JSON field name specified via the `[json_name = "..."]` field + // option when it differs from the default lowerCamelCase of the proto field + // name (e.g., `[json_name = "order_id_legacy"]` on field `order_id`). + const std::optional& json_name() const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + return json_name_; + } + + private: + std::optional name_; + std::optional id_; + std::optional json_name_; +}; + +// Protocol Buffers schema properties attached to an EnumTypeDef. +class ProtoEnumProperties final { + public: + ProtoEnumProperties() = default; + explicit ProtoEnumProperties(std::optional name, + std::optional allow_alias = std::nullopt) + : name_(std::move(name)), allow_alias_(allow_alias) {} + + // Overrides the canonical CEL enum name with a target-specific protobuf enum + // name when they differ (e.g., mapping "com.example.OrderStatus" to + // "com.example.proto2.OrderStatus"). + const std::optional& name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return name_; + } + + // Whether the protobuf enum sets `option allow_alias = true;`, permitting + // multiple enum constants to share the same numeric value (e.g., + // `STARTED = 1;` and `RUNNING = 1;`). + std::optional allow_alias() const { return allow_alias_; } + + private: + std::optional name_; + std::optional allow_alias_; +}; + +// Protocol Buffers schema properties attached to an EnumTypeDef::EnumConstant. +class ProtoEnumConstantProperties final { + public: + ProtoEnumConstantProperties() = default; + explicit ProtoEnumConstantProperties(std::optional name) + : name_(std::move(name)) {} + + // Overrides the canonical CEL enum constant name with the protobuf enum value + // name when they differ (e.g., mapping a CEL constant "ORDER_STATUS_ACTIVE" + // to "STATUS_ACTIVE_V2" in a legacy proto2 enum). + const std::optional& name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return name_; + } + + private: + 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..ed4e61013 --- /dev/null +++ b/common/typedef/proto_schema.proto @@ -0,0 +1,64 @@ +// 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; + +// Protocol Buffers schema properties attached to a Struct. +message ProtoStructProperties { + string name = 1; +} + +// Protocol Buffers schema properties attached to a Struct.Field. +message ProtoFieldProperties { + string name = 1; + int32 id = 2; + string json_name = 3; +} + +// Protocol Buffers schema properties attached to an Enum. +message ProtoEnumProperties { + string name = 1; + bool allow_alias = 2; +} + +// Protocol Buffers schema properties attached to an Enum.EnumConstant. +message ProtoEnumConstantProperties { + string name = 1; +} + +extend Struct { + ProtoStructProperties proto2_struct_aspect = 1001; + ProtoStructProperties proto3_struct_aspect = 1002; +} + +extend Struct.Field { + ProtoFieldProperties proto2_field_aspect = 1001; + ProtoFieldProperties proto3_field_aspect = 1002; +} + +extend Enum { + ProtoEnumProperties proto2_enum_aspect = 1001; + ProtoEnumProperties proto3_enum_aspect = 1002; +} + +extend Enum.EnumConstant { + ProtoEnumConstantProperties proto2_enum_constant_aspect = 1001; + ProtoEnumConstantProperties proto3_enum_constant_aspect = 1002; +} diff --git a/common/typedef/proto_schema_proto.cc b/common/typedef/proto_schema_proto.cc new file mode 100644 index 000000000..2bba54e58 --- /dev/null +++ b/common/typedef/proto_schema_proto.cc @@ -0,0 +1,205 @@ +// 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 +#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 "common/typedef/proto_schema.h" +#include "common/typedef/proto_schema.pb.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +absl::StatusOr ProtoSchemaProto::DecodeStructProperties( + const types::Struct& proto) const { + const bool has_ext = (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_struct_aspect) + : proto.HasExtension(types::proto3_struct_aspect); + if (!has_ext) { + return std::any(); + } + const types::ProtoStructProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_struct_aspect) + : proto.GetExtension(types::proto3_struct_aspect); + std::optional name; + if (ext.has_name()) { + name = ext.name(); + } + return ProtoStructProperties(std::move(name)); +} + +absl::StatusOr ProtoSchemaProto::DecodeFieldProperties( + const types::Struct::Field& proto) const { + const bool has_ext = (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_field_aspect) + : proto.HasExtension(types::proto3_field_aspect); + if (!has_ext) { + return std::any(); + } + const types::ProtoFieldProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_field_aspect) + : proto.GetExtension(types::proto3_field_aspect); + std::optional name; + std::optional id; + std::optional json_name; + if (ext.has_name()) { + name = ext.name(); + } + if (ext.has_id()) { + id = ext.id(); + } + if (ext.has_json_name()) { + json_name = ext.json_name(); + } + return ProtoFieldProperties(std::move(name), id, std::move(json_name)); +} + +absl::StatusOr ProtoSchemaProto::DecodeEnumProperties( + const types::Enum& proto) const { + const bool has_ext = (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_enum_aspect) + : proto.HasExtension(types::proto3_enum_aspect); + if (!has_ext) { + return std::any(); + } + const types::ProtoEnumProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_enum_aspect) + : proto.GetExtension(types::proto3_enum_aspect); + std::optional name; + std::optional allow_alias; + if (ext.has_name()) { + name = ext.name(); + } + if (ext.has_allow_alias()) { + allow_alias = ext.allow_alias(); + } + return ProtoEnumProperties(std::move(name), allow_alias); +} + +absl::StatusOr ProtoSchemaProto::DecodeEnumConstantProperties( + const types::Enum::EnumConstant& proto) const { + const bool has_ext = + (version_ == ProtoVersion::kProto2) + ? proto.HasExtension(types::proto2_enum_constant_aspect) + : proto.HasExtension(types::proto3_enum_constant_aspect); + if (!has_ext) { + return std::any(); + } + const types::ProtoEnumConstantProperties& ext = + (version_ == ProtoVersion::kProto2) + ? proto.GetExtension(types::proto2_enum_constant_aspect) + : proto.GetExtension(types::proto3_enum_constant_aspect); + std::optional name; + if (ext.has_name()) { + name = ext.name(); + } + return ProtoEnumConstantProperties(std::move(name)); +} + +absl::Status ProtoSchemaProto::EncodeStructProperties( + const std::any& properties, types::Struct* absl_nonnull proto) const { + const auto* props = std::any_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid struct properties type for '", name(), "' aspect")); + } + types::ProtoStructProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_struct_aspect) + : proto->MutableExtension(types::proto3_struct_aspect); + ext->Clear(); + if (props->name().has_value()) { + ext->set_name(*props->name()); + } + return absl::OkStatus(); +} + +absl::Status ProtoSchemaProto::EncodeFieldProperties( + const std::any& properties, + types::Struct::Field* absl_nonnull proto) const { + const auto* props = std::any_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid field properties type for '", name(), "' aspect")); + } + types::ProtoFieldProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_field_aspect) + : proto->MutableExtension(types::proto3_field_aspect); + 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()); + } + return absl::OkStatus(); +} + +absl::Status ProtoSchemaProto::EncodeEnumProperties( + const std::any& properties, types::Enum* absl_nonnull proto) const { + const auto* props = std::any_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid enum properties type for '", name(), "' aspect")); + } + types::ProtoEnumProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_enum_aspect) + : proto->MutableExtension(types::proto3_enum_aspect); + 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()); + } + return absl::OkStatus(); +} + +absl::Status ProtoSchemaProto::EncodeEnumConstantProperties( + const std::any& properties, + types::Enum::EnumConstant* absl_nonnull proto) const { + const auto* props = std::any_cast(&properties); + if (props == nullptr) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid enum constant properties type for '", name(), "' aspect")); + } + types::ProtoEnumConstantProperties* ext = + (version_ == ProtoVersion::kProto2) + ? proto->MutableExtension(types::proto2_enum_constant_aspect) + : proto->MutableExtension(types::proto3_enum_constant_aspect); + 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..a91962b38 --- /dev/null +++ b/common/typedef/proto_schema_proto.h @@ -0,0 +1,95 @@ +// 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/aspect_proto.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +// Base AspectProto implementation for Protocol Buffers schemas. +class ProtoSchemaProto : public AspectProto { + public: + absl::StatusOr DecodeStructProperties( + const types::Struct& proto) const override; + + absl::StatusOr DecodeFieldProperties( + const types::Struct::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 EncodeStructProperties( + const std::any& properties, + types::Struct* absl_nonnull proto) const override; + + absl::Status EncodeFieldProperties( + const std::any& properties, + types::Struct::Field* absl_nonnull proto) const override; + + absl::Status EncodeEnumProperties( + const std::any& properties, + types::Enum* absl_nonnull proto) const override; + + absl::Status EncodeEnumConstantProperties( + const std::any& properties, + types::Enum::EnumConstant* absl_nonnull proto) const override; + + protected: + enum class ProtoVersion { + kProto2, + kProto3, + }; + + explicit ProtoSchemaProto(ProtoVersion version) : version_(version) {} + + private: + ProtoVersion version_; +}; + +// AspectProto implementation for Protocol Buffers v2 ("proto2"). +class Proto2SchemaProto : public ProtoSchemaProto { + public: + static constexpr absl::string_view kName = kProto2AspectName; + + Proto2SchemaProto() : ProtoSchemaProto(ProtoVersion::kProto2) {} + + absl::string_view name() const override { return kName; } +}; + +// AspectProto implementation for Protocol Buffers v3 ("proto3"). +class Proto3SchemaProto : public ProtoSchemaProto { + public: + static constexpr absl::string_view kName = kProto3AspectName; + + 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..5b8e51218 --- /dev/null +++ b/common/typedef/proto_schema_proto_test.cc @@ -0,0 +1,168 @@ +// 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/status/status.h" +#include "absl/status/status_matchers.h" +#include "common/typedef/aspect_proto.h" +#include "common/typedef/proto_schema.h" +#include "common/typedef/proto_schema.pb.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::Property; + +TEST(ProtoSchemaProtoTest, AspectNames) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + EXPECT_EQ(p2.name(), kProto2AspectName); + EXPECT_EQ(p3.name(), kProto3AspectName); +} + +TEST(ProtoSchemaProtoTest, DecodeReturnsEmptyAnyWhenExtensionAbsent) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + types::Struct empty_struct; + types::Struct::Field empty_field; + types::Enum empty_enum; + types::Enum::EnumConstant empty_const; + + for (const AspectProto* aspect : {static_cast(&p2), + static_cast(&p3)}) { + EXPECT_THAT(aspect->DecodeStructProperties(empty_struct), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect->DecodeFieldProperties(empty_field), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect->DecodeEnumProperties(empty_enum), + IsOkAndHolds(Property(&std::any::has_value, false))); + EXPECT_THAT(aspect->DecodeEnumConstantProperties(empty_const), + IsOkAndHolds(Property(&std::any::has_value, false))); + } + + // Cross-version isolation: Proto3 decoder returns empty std::any when only + // Proto2 extensions are present. + types::Struct proto2_struct; + proto2_struct.MutableExtension(types::proto2_struct_aspect); + EXPECT_THAT(p3.DecodeStructProperties(proto2_struct), + IsOkAndHolds(Property(&std::any::has_value, false))); + types::Struct::Field proto2_field; + proto2_field.MutableExtension(types::proto2_field_aspect); + EXPECT_THAT(p3.DecodeFieldProperties(proto2_field), + IsOkAndHolds(Property(&std::any::has_value, false))); + types::Enum proto2_enum; + proto2_enum.MutableExtension(types::proto2_enum_aspect); + EXPECT_THAT(p3.DecodeEnumProperties(proto2_enum), + IsOkAndHolds(Property(&std::any::has_value, false))); + types::Enum::EnumConstant proto2_const; + proto2_const.MutableExtension(types::proto2_enum_constant_aspect); + EXPECT_THAT(p3.DecodeEnumConstantProperties(proto2_const), + IsOkAndHolds(Property(&std::any::has_value, false))); + + // Cross-version isolation: Proto2 decoder returns empty std::any when only + // Proto3 extensions are present. + types::Struct proto3_struct; + proto3_struct.MutableExtension(types::proto3_struct_aspect); + EXPECT_THAT(p2.DecodeStructProperties(proto3_struct), + IsOkAndHolds(Property(&std::any::has_value, false))); + types::Struct::Field proto3_field; + proto3_field.MutableExtension(types::proto3_field_aspect); + EXPECT_THAT(p2.DecodeFieldProperties(proto3_field), + IsOkAndHolds(Property(&std::any::has_value, false))); + types::Enum proto3_enum; + proto3_enum.MutableExtension(types::proto3_enum_aspect); + EXPECT_THAT(p2.DecodeEnumProperties(proto3_enum), + IsOkAndHolds(Property(&std::any::has_value, false))); + types::Enum::EnumConstant proto3_const; + proto3_const.MutableExtension(types::proto3_enum_constant_aspect); + EXPECT_THAT(p2.DecodeEnumConstantProperties(proto3_const), + IsOkAndHolds(Property(&std::any::has_value, false))); +} + +TEST(ProtoSchemaProtoTest, EncodeTypeMismatchErrors) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + + for (const AspectProto* aspect : {static_cast(&p2), + static_cast(&p3)}) { + types::Struct struct_proto; + types::Struct::Field field_proto; + types::Enum enum_proto; + types::Enum::EnumConstant const_proto; + + std::any wrong_type = 123; + EXPECT_THAT(aspect->EncodeStructProperties(wrong_type, &struct_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid struct properties type for '"))); + + EXPECT_THAT(aspect->EncodeFieldProperties(wrong_type, &field_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid field properties type for '"))); + + EXPECT_THAT(aspect->EncodeEnumProperties(wrong_type, &enum_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum properties type for '"))); + + EXPECT_THAT( + aspect->EncodeEnumConstantProperties(wrong_type, &const_proto), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid enum constant properties type for '"))); + } +} + +TEST(ProtoSchemaProtoTest, DecodeIgnoresUnknownFields) { + Proto2SchemaProto p2; + Proto3SchemaProto p3; + + types::Struct struct_proto; + auto* struct_ext = struct_proto.MutableExtension(types::proto2_struct_aspect); + struct_ext->GetReflection() + ->MutableUnknownFields(struct_ext) + ->AddVarint(999, 1); + EXPECT_THAT(p2.DecodeStructProperties(struct_proto), + IsOkAndHolds(Property(&std::any::has_value, true))); + + types::Struct::Field field_proto; + auto* field_ext = field_proto.MutableExtension(types::proto3_field_aspect); + field_ext->GetReflection()->MutableUnknownFields(field_ext)->AddVarint(999, + 1); + EXPECT_THAT(p3.DecodeFieldProperties(field_proto), + IsOkAndHolds(Property(&std::any::has_value, true))); + + types::Enum enum_proto; + auto* enum_ext = enum_proto.MutableExtension(types::proto2_enum_aspect); + enum_ext->GetReflection()->MutableUnknownFields(enum_ext)->AddVarint(999, 1); + EXPECT_THAT(p2.DecodeEnumProperties(enum_proto), + IsOkAndHolds(Property(&std::any::has_value, true))); + + types::Enum::EnumConstant const_proto; + auto* const_ext = + const_proto.MutableExtension(types::proto3_enum_constant_aspect); + const_ext->GetReflection()->MutableUnknownFields(const_ext)->AddVarint(999, + 1); + EXPECT_THAT(p3.DecodeEnumConstantProperties(const_proto), + IsOkAndHolds(Property(&std::any::has_value, true))); +} + +} // 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/cel_type_errors.baseline b/common/typedef/testdata/cel_type_errors.baseline new file mode 100644 index 000000000..a933704a5 --- /dev/null +++ b/common/typedef/testdata/cel_type_errors.baseline @@ -0,0 +1,920 @@ +YAML SOURCE: cel_type_errors.yaml +-------------------------------------------------------------------- +# struct: +# name: +# - bad_name + +17:7: Type definition 'name' is not a string +| - "bad_name" +| ^ +-------------------------------------------------------------------- +# union: +# name: Item + +18:3: Unsupported type definition kind: 'union' +|- union: +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# constants: [] + +22:5: Struct type definition cannot have 'constants' +| constants: [] +| ^ +-------------------------------------------------------------------- +# enum: +# name: Status +# fields: [] + +25:5: Enum type definition cannot have 'fields' +| fields: [] +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# fields: +# - name: f +# type: "" + +30:15: Empty type signature +| type: "" +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# fields: +# - name: f +# type: {} + +35:15: Node 'type_name' is not specified +| type: {} +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# fields: +# - name: f +# type: uint +# default: -1 + +41:18: Failed to parse uint constant +| default: -1 +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# fields: +# - name: f +# type: string +# aspects: +# aspect_a: +# unknown_prop: 12 + +49:13: Unsupported 'aspect_a' field property: 'unknown_prop' +| unknown_prop: 12 +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# fields: +# - name: f +# type: string +# aspects: +# aspect_b: +# id: not_an_int + +57:17: Node 'id' is not an integer +| id: "not_an_int" +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# fields: +# - name: f +# type: string +# - name: f +# type: int + +63:15: Field 'f' is already defined. +| - name: "f" +| ^ +-------------------------------------------------------------------- +# enum: +# name: Status +# constants: +# - name: ACTIVE +# id: not_an_int + +69:13: Node 'id' is not an integer +| id: "not_an_int" +| ^ +-------------------------------------------------------------------- +# enum: +# name: Status +# constants: +# - name: ACTIVE +# - name: ACTIVE + +74:15: Enum constant 'ACTIVE' is already defined. +| - name: "ACTIVE" +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# aspects: +# stub_aspect: +# name: Foo + +79:9: Custom struct properties are not supported for aspect 'stub_aspect' +| name: "Foo" +| ^ +-------------------------------------------------------------------- +# struct: +# name: Item +# fields: +# - name: f +# type: string +# aspects: +# stub_aspect: +# name: Foo + +87:13: Custom field properties are not supported for aspect 'stub_aspect' +| name: "Foo" +| ^ +-------------------------------------------------------------------- +# enum: +# name: Status +# aspects: +# stub_aspect: +# name: Foo +# constants: +# - name: ACTIVE + +92:9: Custom enum properties are not supported for aspect 'stub_aspect' +| name: "Foo" +| ^ +-------------------------------------------------------------------- +# enum: +# name: Status +# constants: +# - name: ACTIVE +# aspects: +# stub_aspect: +# name: Foo + +101:13: Custom enum constant properties are not supported for aspect 'stub_aspect' +| name: "Foo" +| ^ +-------------------------------------------------------------------- +# struct: [1, 2] + +102:11: Struct type definition is not a map +|- struct: [1, 2] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# doc: [1] + +105:10: Type definition 'doc' is not a string +| doc: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: 123 + +108:13: Node 'fields' is not a sequence +| fields: 123 +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: [123] + +111:14: Field is not a map +| fields: [123] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: [1] + +115:15: Field name is not a string +| - name: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f + +119:9: Field 'type' is not specified +| - name: "f" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: [1] + +124:15: Field 'type' is neither a string nor a map +| type: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: int +# doc: [1] + +130:14: Field 'doc' is not a string +| doc: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: list +# default: 123 + +136:18: Constant type 'list' is not supported +| default: 123 +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: int +# default: [1] + +142:18: Field 'default' is not a scalar +| default: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: [1] + +148:22: Node 'type_name' is not a string +| type_name: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: int +# is_type_param: [1] + +155:26: Node 'is_type_param' is not a boolean +| is_type_param: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: map +# params: 123 + +162:19: Node 'params' is not a sequence +| params: 123 +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: map +# params: [123] + +169:20: Type parameter is not a map +| params: [123] +| ^ +-------------------------------------------------------------------- +# enum: [1, 2] + +170:9: Enum type definition is not a map +|- enum: [1, 2] +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# constants: 123 + +173:16: Node 'constants' is not a sequence +| constants: 123 +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# constants: [123] + +176:17: Enum constant is not a map +| constants: [123] +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# constants: +# - name: [1] + +180:15: Enum constant name is not a string +| - name: [1] +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# constants: +# - name: C +# doc: [1] + +185:14: Enum constant 'doc' is not a string +| doc: [1] +| ^ +-------------------------------------------------------------------- +# [1] + +186:3: Type definition is not a map +|- [1] +| ^ +-------------------------------------------------------------------- +# struct: {} +# enum: {} + +188:3: Node 'struct' and 'enum' are mutually exclusive +| enum: {} +| ^ +-------------------------------------------------------------------- +# {} + +189:3: Type definition must specify 'struct' or 'enum' +|- {} +| ^ +-------------------------------------------------------------------- +# ? [1]: 2 +# : ~ + +190:5: Type definition kind is not a string +|- ? [1]: 2 +| ^ +-------------------------------------------------------------------- +# struct: +# name: O +# aspects: 123 + +193:14: Node 'aspects' is not a map +| aspects: 123 +| ^ +-------------------------------------------------------------------- +# struct: +# name: O +# aspects: +# ? [1]: val +# : ~ + +197:9: Target aspect name is not a string +| ? [1]: val +| ^ +-------------------------------------------------------------------- +# struct: +# name: O +# fields: +# - name: f +# type: int +# aspects: 123 + +203:18: Node 'aspects' is not a map +| aspects: 123 +| ^ +-------------------------------------------------------------------- +# struct: +# name: O +# fields: +# - name: f +# type: int +# aspects: +# ? [1]: val +# : ~ + +210:13: Target aspect name is not a string +| ? [1]: val +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# aspects: 123 +# constants: [] + +213:14: Node 'aspects' is not a map +| aspects: 123 +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# aspects: +# ? [1]: val +# : ~ +# constants: [] + +218:9: Target aspect name is not a string +| ? [1]: val +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# constants: +# - name: C +# aspects: 123 + +224:18: Node 'aspects' is not a map +| aspects: 123 +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# constants: +# - name: C +# aspects: +# ? [1]: val +# : ~ + +230:13: Target aspect name is not a string +| ? [1]: val +| ^ +-------------------------------------------------------------------- +# enum: {} + +231:9: Type definition 'name' is not a string +|- enum: {} +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# doc: [1] + +234:10: Type definition 'doc' is not a string +| doc: [1] +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: "" + +240:22: Node 'type_name' cannot be empty +| type_name: "" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: "null" +# default: abc + +246:18: Failed to parse null constant +| default: "abc" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: any +# default: 123 + +252:18: Constant type 'any' is not supported +| default: 123 +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: bool +# default: bad + +258:18: Failed to parse bool constant +| default: "bad" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: int +# default: bad + +264:18: Failed to parse int constant +| default: "bad" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: uint +# default: bad + +270:18: Failed to parse uint constant +| default: "bad" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: double +# default: bad + +276:18: Failed to parse double constant +| default: "bad" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: duration +# default: bad + +282:18: Failed to parse duration constant +| default: "bad" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: timestamp +# default: bad + +288:18: Failed to parse timestamp constant: Failed to parse input supported format: YYYY-MM-DDThh:mm:ssZ +| default: "bad" +| ^ +-------------------------------------------------------------------- +# struct: +# name: "" + +290:11: Type definition 'name' cannot be empty +| name: "" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: "" +# type: int + +294:15: Field name cannot be empty +| - name: "" +| ^ +-------------------------------------------------------------------- +# enum: +# name: "" + +297:11: Type definition 'name' cannot be empty +| name: "" +| ^ +-------------------------------------------------------------------- +# enum: +# name: E +# constants: +# - name: "" + +301:15: Enum constant name cannot be empty +| - name: "" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: T +# is_type_param: true +# params: +# - type_name: int + +307:11: Type parameter cannot have type parameters +| type_name: "T" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: "null" +# params: +# - type_name: int + +316:11: Type 'null' cannot have type parameters +| type_name: "null" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: bool +# params: +# - type_name: int + +324:11: Type 'bool' cannot have type parameters +| type_name: "bool" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: int +# params: +# - type_name: int + +332:11: Type 'int' cannot have type parameters +| type_name: "int" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: uint +# params: +# - type_name: int + +340:11: Type 'uint' cannot have type parameters +| type_name: "uint" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: double +# params: +# - type_name: int + +348:11: Type 'double' cannot have type parameters +| type_name: "double" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: string +# params: +# - type_name: int + +356:11: Type 'string' cannot have type parameters +| type_name: "string" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: bytes +# params: +# - type_name: int + +364:11: Type 'bytes' cannot have type parameters +| type_name: "bytes" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: any +# params: +# - type_name: int + +372:11: Type 'any' cannot have type parameters +| type_name: "any" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: timestamp +# params: +# - type_name: int + +380:11: Type 'timestamp' cannot have type parameters +| type_name: "timestamp" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: duration +# params: +# - type_name: int + +388:11: Type 'duration' cannot have type parameters +| type_name: "duration" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: dyn +# params: +# - type_name: int + +396:11: Type 'dyn' cannot have type parameters +| type_name: "dyn" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: bool_wrapper +# params: +# - type_name: int + +404:11: Type 'bool_wrapper' cannot have type parameters +| type_name: "bool_wrapper" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: int_wrapper +# params: +# - type_name: int + +412:11: Type 'int_wrapper' cannot have type parameters +| type_name: "int_wrapper" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: uint_wrapper +# params: +# - type_name: int + +420:11: Type 'uint_wrapper' cannot have type parameters +| type_name: "uint_wrapper" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: double_wrapper +# params: +# - type_name: int + +428:11: Type 'double_wrapper' cannot have type parameters +| type_name: "double_wrapper" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: string_wrapper +# params: +# - type_name: int + +436:11: Type 'string_wrapper' cannot have type parameters +| type_name: "string_wrapper" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: bytes_wrapper +# params: +# - type_name: int + +444:11: Type 'bytes_wrapper' cannot have type parameters +| type_name: "bytes_wrapper" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: google.protobuf.ListValue +# params: +# - type_name: int + +452:11: Type 'google.protobuf.ListValue' cannot have type parameters +| type_name: "google.protobuf.ListValue" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: google.protobuf.Struct +# params: +# - type_name: int + +460:11: Type 'google.protobuf.Struct' cannot have type parameters +| type_name: "google.protobuf.Struct" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: type +# params: +# - type_name: int +# - type_name: string + +468:11: Type 'type' expects at most 1 parameter +| type_name: "type" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: list +# params: +# - type_name: int +# - type_name: string + +477:11: Type 'list' expects at most 1 parameter +| type_name: "list" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: map +# params: +# - type_name: string + +486:11: Type 'map' expects 0 or 2 parameters +| type_name: "map" +| ^ +-------------------------------------------------------------------- +# struct: +# name: A +# fields: +# - name: f +# type: +# type_name: map +# params: +# - type_name: string +# - type_name: int +# - type_name: bool + +494:11: Type 'map' expects 0 or 2 parameters +| type_name: "map" +| ^ diff --git a/common/typedef/testdata/cel_type_errors.yaml b/common/typedef/testdata/cel_type_errors.yaml new file mode 100644 index 000000000..049cc1310 --- /dev/null +++ b/common/typedef/testdata/cel_type_errors.yaml @@ -0,0 +1,498 @@ +# 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. + +- struct: + name: + - "bad_name" +- union: + name: "Item" +- struct: + name: "Item" + constants: [] +- enum: + name: "Status" + fields: [] +- struct: + name: "Item" + fields: + - name: "f" + type: "" +- struct: + name: "Item" + fields: + - name: "f" + type: {} +- struct: + name: "Item" + fields: + - name: "f" + type: "uint" + default: -1 +- struct: + name: "Item" + fields: + - name: "f" + type: "string" + aspects: + aspect_a: + unknown_prop: 12 +- struct: + name: "Item" + fields: + - name: "f" + type: "string" + aspects: + aspect_b: + id: "not_an_int" +- struct: + name: "Item" + fields: + - name: "f" + type: "string" + - name: "f" + type: "int" +- enum: + name: "Status" + constants: + - name: "ACTIVE" + id: "not_an_int" +- enum: + name: "Status" + constants: + - name: "ACTIVE" + - name: "ACTIVE" +- struct: + name: "Item" + aspects: + stub_aspect: + name: "Foo" +- struct: + name: "Item" + fields: + - name: "f" + type: "string" + aspects: + stub_aspect: + name: "Foo" +- enum: + name: "Status" + aspects: + stub_aspect: + name: "Foo" + constants: + - name: "ACTIVE" +- enum: + name: "Status" + constants: + - name: "ACTIVE" + aspects: + stub_aspect: + name: "Foo" +- struct: [1, 2] +- struct: + name: "A" + doc: [1] +- struct: + name: "A" + fields: 123 +- struct: + name: "A" + fields: [123] +- struct: + name: "A" + fields: + - name: [1] +- struct: + name: "A" + fields: + - name: "f" +- struct: + name: "A" + fields: + - name: "f" + type: [1] +- struct: + name: "A" + fields: + - name: "f" + type: "int" + doc: [1] +- struct: + name: "A" + fields: + - name: "f" + type: "list" + default: 123 +- struct: + name: "A" + fields: + - name: "f" + type: "int" + default: [1] +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: [1] +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "int" + is_type_param: [1] +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "map" + params: 123 +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "map" + params: [123] +- enum: [1, 2] +- enum: + name: "E" + constants: 123 +- enum: + name: "E" + constants: [123] +- enum: + name: "E" + constants: + - name: [1] +- enum: + name: "E" + constants: + - name: "C" + doc: [1] +- [1] +- struct: {} + enum: {} +- {} +- ? [1]: 2 +- struct: + name: "O" + aspects: 123 +- struct: + name: "O" + aspects: + ? [1]: val +- struct: + name: "O" + fields: + - name: "f" + type: "int" + aspects: 123 +- struct: + name: "O" + fields: + - name: "f" + type: "int" + aspects: + ? [1]: val +- enum: + name: "E" + aspects: 123 + constants: [] +- enum: + name: "E" + aspects: + ? [1]: val + constants: [] +- enum: + name: "E" + constants: + - name: "C" + aspects: 123 +- enum: + name: "E" + constants: + - name: "C" + aspects: + ? [1]: val +- enum: {} +- enum: + name: "E" + doc: [1] +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "" +- struct: + name: "A" + fields: + - name: "f" + type: "null" + default: "abc" +- struct: + name: "A" + fields: + - name: "f" + type: "any" + default: 123 +- struct: + name: "A" + fields: + - name: "f" + type: "bool" + default: "bad" +- struct: + name: "A" + fields: + - name: "f" + type: "int" + default: "bad" +- struct: + name: "A" + fields: + - name: "f" + type: "uint" + default: "bad" +- struct: + name: "A" + fields: + - name: "f" + type: "double" + default: "bad" +- struct: + name: "A" + fields: + - name: "f" + type: "duration" + default: "bad" +- struct: + name: "A" + fields: + - name: "f" + type: "timestamp" + default: "bad" +- struct: + name: "" +- struct: + name: "A" + fields: + - name: "" + type: "int" +- enum: + name: "" +- enum: + name: "E" + constants: + - name: "" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "T" + is_type_param: true + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "null" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "bool" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "int" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "uint" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "double" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "string" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "bytes" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "any" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "timestamp" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "duration" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "dyn" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "bool_wrapper" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "int_wrapper" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "uint_wrapper" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "double_wrapper" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "string_wrapper" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "bytes_wrapper" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "google.protobuf.ListValue" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "google.protobuf.Struct" + params: + - type_name: "int" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "type" + params: + - type_name: "int" + - type_name: "string" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "list" + params: + - type_name: "int" + - type_name: "string" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "map" + params: + - type_name: "string" +- struct: + name: "A" + fields: + - name: "f" + type: + type_name: "map" + params: + - type_name: "string" + - type_name: "int" + - type_name: "bool" diff --git a/common/typedef/testdata/cel_types.txtproto b/common/typedef/testdata/cel_types.txtproto new file mode 100644 index 000000000..74f500c9a --- /dev/null +++ b/common/typedef/testdata/cel_types.txtproto @@ -0,0 +1,217 @@ +# proto-file: common/typedef/typedef.proto +# proto-message: cel.types.TypeDefSet + +# Empty struct +types { + struct_type {} +} + +# Struct with name and doc +types { + struct_type { + name: "NoFields" + doc: "Empty struct." + } +} + +# Enum with constants and doc +types { + enum_type { + name: "com.example.OrderStatus" + doc: "Order lifecycle state." + constants { name: "UNSPECIFIED" id: 0 doc: "Default value." } + constants { name: "PENDING" id: 1 } + constants { name: "SHIPPED" id: 10 doc: "Shipped to customer." } + } +} + +# Struct with fields +types { + struct_type { + name: "com.example.store.Order" + fields { + name: "items" + type { + list_type { + elem_type { + abstract_type { name: "com.example.store.Item" } + } + } + } + } + fields { + name: "metadata" + type { + map_type { + key_type { primitive: STRING } + value_type { dyn {} } + } + } + } + fields { + name: "notes" + type { wrapper: STRING } + } + } +} + +# All supported field types +types { + struct_type { + name: "AllSupportedFieldTypes" + fields { + name: "null_field" + type { null: NULL_VALUE } + default_value { null_value: NULL_VALUE } + } + fields { + name: "bool_field" + type { primitive: BOOL } + default_value { bool_value: true } + } + fields { + name: "int_field" + type { primitive: INT64 } + default_value { int64_value: 123 } + } + fields { + name: "uint_field" + type { primitive: UINT64 } + default_value { uint64_value: 456 } + } + fields { + name: "double_field" + type { primitive: DOUBLE } + default_value { double_value: 3.25 } + } + fields { + name: "bytes_field" + type { primitive: BYTES } + default_value { bytes_value: "\001\002\003" } + } + fields { + name: "string_field" + type { primitive: STRING } + default_value { string_value: "hello" } + } + fields { + name: "duration_field" + type { well_known: DURATION } + default_value { duration_value { seconds: 10 } } + } + fields { + name: "timestamp_field" + type { well_known: TIMESTAMP } + default_value { timestamp_value { seconds: 1791158400 } } + } + fields { + name: "enum_field" + type { abstract_type { name: "com.example.Status" } } + default_value { string_value: "ACTIVE" } + } + fields { + name: "struct_field" + type { + map_type { + key_type { primitive: STRING } + value_type { dyn {} } + } + } + } + fields { + name: "list_value_field" + type { list_type { elem_type { dyn {} } } } + } + fields { + name: "any_field" + type { well_known: ANY } + } + fields { + name: "func_field" + type { + function { + result_type { primitive: BOOL } + arg_types { primitive: INT64 } + arg_types { primitive: STRING } + } + } + } + fields { + name: "type_param_f" + type { type_param: "T" } + } + fields { + name: "unspec_prim_f" + type { primitive: PRIMITIVE_TYPE_UNSPECIFIED } + } + fields { + name: "bool_w_f" + type { wrapper: BOOL } + } + fields { + name: "int_w_f" + type { wrapper: INT64 } + } + fields { + name: "uint_w_f" + type { wrapper: UINT64 } + } + fields { + name: "double_w_f" + type { wrapper: DOUBLE } + } + fields { + name: "bytes_w_f" + type { wrapper: BYTES } + } + fields { + name: "unspec_w_f" + type { wrapper: PRIMITIVE_TYPE_UNSPECIFIED } + } + fields { + name: "unspec_wk_f" + type { well_known: WELL_KNOWN_TYPE_UNSPECIFIED } + } + fields { + name: "map_val_only_f" + type { + map_type { + value_type { primitive: INT64 } + } + } + } + fields { + name: "type_int_f" + type { + type { primitive: INT64 } + } + } + fields { + name: "type_unspec_f" + type { + type {} + } + } + fields { + name: "abstract_params_f" + type { + abstract_type { + name: "MyGeneric" + parameter_types { primitive: INT64 } + } + } + } + fields { + name: "msg_f" + type { + abstract_type { + name: "com.example.Msg" + } + } + } + fields { + name: "error_f" + type { error {} } + } + } +} diff --git a/common/typedef/testdata/cel_types.yaml b/common/typedef/testdata/cel_types.yaml new file mode 100644 index 000000000..0bafd0008 --- /dev/null +++ b/common/typedef/testdata/cel_types.yaml @@ -0,0 +1,257 @@ +# 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. + +# Struct with no fields +- struct: + name: "NoFields" + +# Enum with no constants +- enum: + name: "NoConstants" + +# Struct with aspects and defaults +- struct: + name: "com.example.Item" + doc: "Store item entity." + aspects: + aspect_a: + name: "ItemObj" + aspect_b: + name: "com.example.store.ItemProto" + fields: + - name: "item_id" + type: "string" + doc: "Unique item identifier." + default: "default_id" + aspects: + aspect_a: + name: "itemId" + aspect_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 + +# Concise type syntax +- struct: + name: "com.example.store.Order" + fields: + - name: "items" + type: "list" + - name: "metadata" + type: "map" + - name: "notes" + type: "string_wrapper" + +# Structured type syntax +- struct: + 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" + +# All supported field types +- struct: + name: "AllSupportedFieldTypes" + fields: + - name: "f_null" + type: "null" + default: ~ + - name: "f_null_empty" + type: "null" + default: "" + - name: "f_null_word" + type: "null" + default: null + - name: "f_bool" + type: "bool" + default: true + - name: "f_bool_false" + type: "bool" + default: false + - 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" + - name: "f_enum" + type: "com.example.Status" + default: "ACTIVE" + - 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" + - name: "null_f" + type: + type_name: "null" + - name: "uint_f" + type: + type_name: "uint" + - name: "double_f" + type: + type_name: "double" + - name: "bytes_f" + type: + type_name: "bytes" + - name: "bool_w_f" + type: + type_name: "bool_wrapper" + - name: "int_w_f" + type: + type_name: "int_wrapper" + - name: "uint_w_f" + type: + type_name: "uint_wrapper" + - name: "double_w_f" + type: + type_name: "double_wrapper" + - name: "bytes_w_f" + type: + type_name: "bytes_wrapper" + - name: "type_int_f" + type: + type_name: "type" + params: + - type_name: "int" + - name: "type_unspec_f" + type: + type_name: "type" + - name: "abstract_params_f" + type: + type_name: "MyGeneric" + params: + - type_name: "int" + - name: "msg_f" + type: + type_name: "com.example.Msg" + +# Enum type definition +- enum: + name: "com.example.OrderStatus" + doc: "Order lifecycle state." + aspects: + aspect_a: + name: "OrderStatusA" + aspect_b: + name: "com.example.proto.OrderStatus" + constants: + - name: "UNSPECIFIED" + doc: "Default value." + - name: "PENDING" + - name: "SHIPPED" + id: 10 + doc: "Shipped to customer." + aspects: + aspect_a: + name: "shipped" + aspect_b: + name: "ORDER_STATUS_SHIPPED" + - name: "DELIVERED" + +# An aspect may not support custom properties on all schema elements (e.g., it +# may only define field properties, or none at all like stub_aspect). If a YAML +# file still includes an empty entry for that aspect (`{}` or `~`), the base +# AspectYaml::Decode*Properties implementation returns an empty std::any() +# rather than failing. +- struct: + name: "Item" + aspects: + stub_aspect: {} + fields: + - name: "f" + type: "int" + aspects: + stub_aspect: ~ +- enum: + name: "Status" + aspects: + stub_aspect: {} + constants: + - name: "A" + aspects: + stub_aspect: ~ + +# Unsupported aspects are ignored +- struct: + name: "Item" + aspects: + unknown_aspect: + name: "Foo" + fields: + - name: "f" + type: "int" + aspects: + unknown_aspect: + name: "Bar" diff --git a/common/typedef/testdata/cel_types_from_yaml.baseline b/common/typedef/testdata/cel_types_from_yaml.baseline new file mode 100644 index 000000000..b776ab9f5 --- /dev/null +++ b/common/typedef/testdata/cel_types_from_yaml.baseline @@ -0,0 +1,477 @@ +YAML SOURCE: cel_types.yaml +-------------------------------------------------------------------- +TYPEDEF PROTO: +types { + struct_type { + name: "NoFields" + } +} +types { + enum_type { + name: "NoConstants" + } +} +types { + struct_type { + name: "com.example.Item" + doc: "Store item entity." + fields { + name: "item_id" + type { + primitive: STRING + } + doc: "Unique item identifier." + default_value { + string_value: "default_id" + } + } + fields { + name: "tags" + type { + list_type { + elem_type { + primitive: STRING + } + } + } + } + fields { + name: "attributes" + type { + map_type { + key_type { + primitive: STRING + } + value_type { + primitive: INT64 + } + } + } + } + fields { + name: "nested_type" + type { + list_type { + elem_type { + type_param: "T" + } + } + } + } + } +} +types { + struct_type { + name: "com.example.store.Order" + fields { + name: "items" + type { + list_type { + elem_type { + abstract_type { + name: "com.example.store.Item" + } + } + } + } + } + fields { + name: "metadata" + type { + map_type { + key_type { + primitive: STRING + } + value_type { + dyn { + } + } + } + } + } + fields { + name: "notes" + type { + wrapper: STRING + } + } + } +} +types { + struct_type { + name: "com.example.store.Order" + fields { + name: "items" + type { + list_type { + elem_type { + abstract_type { + name: "com.example.store.Item" + } + } + } + } + } + fields { + name: "metadata" + type { + map_type { + key_type { + primitive: STRING + } + value_type { + dyn { + } + } + } + } + } + fields { + name: "notes" + type { + wrapper: STRING + } + } + } +} +types { + struct_type { + name: "AllSupportedFieldTypes" + fields { + name: "f_null" + type { + null: NULL_VALUE + } + default_value { + null_value: NULL_VALUE + } + } + fields { + name: "f_null_empty" + type { + null: NULL_VALUE + } + default_value { + null_value: NULL_VALUE + } + } + fields { + name: "f_null_word" + type { + null: NULL_VALUE + } + default_value { + null_value: NULL_VALUE + } + } + fields { + name: "f_bool" + type { + primitive: BOOL + } + default_value { + bool_value: true + } + } + fields { + name: "f_bool_false" + type { + primitive: BOOL + } + default_value { + bool_value: false + } + } + fields { + name: "f_int" + type { + primitive: INT64 + } + default_value { + int64_value: -42 + } + } + fields { + name: "f_uint" + type { + primitive: UINT64 + } + default_value { + uint64_value: 100 + } + } + fields { + name: "f_double" + type { + primitive: DOUBLE + } + default_value { + double_value: 3.25 + } + } + fields { + name: "f_bytes" + type { + primitive: BYTES + } + default_value { + bytes_value: "\377\000\001" + } + } + fields { + name: "f_bytes_b64" + type { + primitive: BYTES + } + default_value { + bytes_value: "\001\002\003" + } + } + fields { + name: "f_string" + type { + primitive: STRING + } + default_value { + string_value: "hello" + } + } + fields { + name: "f_duration" + type { + well_known: DURATION + } + default_value { + duration_value { + seconds: 3723 + } + } + } + fields { + name: "f_timestamp" + type { + well_known: TIMESTAMP + } + default_value { + timestamp_value { + seconds: 1767323045 + } + } + } + fields { + name: "f_enum" + type { + abstract_type { + name: "com.example.Status" + } + } + default_value { + string_value: "ACTIVE" + } + } + fields { + name: "struct_field" + type { + map_type { + key_type { + primitive: STRING + } + value_type { + dyn { + } + } + } + } + } + fields { + name: "list_value_field" + type { + list_type { + elem_type { + dyn { + } + } + } + } + } + fields { + name: "any_field" + type { + well_known: ANY + } + } + fields { + name: "timestamp_field" + type { + well_known: TIMESTAMP + } + } + fields { + name: "duration_field" + type { + well_known: DURATION + } + } + fields { + name: "func_field" + type { + function { + result_type { + primitive: BOOL + } + arg_types { + primitive: INT64 + } + arg_types { + primitive: STRING + } + } + } + } + fields { + name: "null_f" + type { + null: NULL_VALUE + } + } + fields { + name: "uint_f" + type { + primitive: UINT64 + } + } + fields { + name: "double_f" + type { + primitive: DOUBLE + } + } + fields { + name: "bytes_f" + type { + primitive: BYTES + } + } + fields { + name: "bool_w_f" + type { + wrapper: BOOL + } + } + fields { + name: "int_w_f" + type { + wrapper: INT64 + } + } + fields { + name: "uint_w_f" + type { + wrapper: UINT64 + } + } + fields { + name: "double_w_f" + type { + wrapper: DOUBLE + } + } + fields { + name: "bytes_w_f" + type { + wrapper: BYTES + } + } + fields { + name: "type_int_f" + type { + type { + primitive: INT64 + } + } + } + fields { + name: "type_unspec_f" + type { + type { + dyn { + } + } + } + } + fields { + name: "abstract_params_f" + type { + abstract_type { + name: "MyGeneric" + parameter_types { + primitive: INT64 + } + } + } + } + fields { + name: "msg_f" + type { + abstract_type { + name: "com.example.Msg" + } + } + } + } +} +types { + enum_type { + name: "com.example.OrderStatus" + doc: "Order lifecycle state." + constants { + name: "UNSPECIFIED" + id: 0 + doc: "Default value." + } + constants { + name: "PENDING" + id: 1 + } + constants { + name: "SHIPPED" + id: 10 + doc: "Shipped to customer." + } + constants { + name: "DELIVERED" + id: 3 + } + } +} +types { + struct_type { + name: "Item" + fields { + name: "f" + type { + primitive: INT64 + } + } + } +} +types { + enum_type { + name: "Status" + constants { + name: "A" + id: 0 + } + } +} +types { + struct_type { + name: "Item" + fields { + name: "f" + type { + primitive: INT64 + } + } + } +} diff --git a/common/typedef/testdata/cel_types_to_yaml.baseline b/common/typedef/testdata/cel_types_to_yaml.baseline new file mode 100644 index 000000000..3cdb9a957 --- /dev/null +++ b/common/typedef/testdata/cel_types_to_yaml.baseline @@ -0,0 +1,244 @@ +PROTO SOURCE: cel_types.txtproto +-------------------------------------------------------------------- +struct: + {} +-------------------------------------------------------------------- +struct: + name: "NoFields" + doc: "Empty struct." +-------------------------------------------------------------------- +enum: + name: "com.example.OrderStatus" + doc: "Order lifecycle state." + constants: + - name: "UNSPECIFIED" + id: 0 + doc: "Default value." + - name: "PENDING" + id: 1 + - name: "SHIPPED" + id: 10 + doc: "Shipped to customer." +-------------------------------------------------------------------- +struct: + name: "com.example.store.Order" + fields: + - name: "items" + type: "list" + - name: "metadata" + type: "map" + - name: "notes" + type: "string_wrapper" +# use_type_signatures = false +struct: + 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" +-------------------------------------------------------------------- +struct: + name: "AllSupportedFieldTypes" + fields: + - name: "null_field" + type: "null" + default: ~ + - name: "bool_field" + type: "bool" + default: true + - name: "int_field" + type: "int" + default: 123 + - name: "uint_field" + type: "uint" + default: 456 + - name: "double_field" + type: "double" + default: 3.25 + - name: "bytes_field" + type: "bytes" + default: b"\x01\x02\x03" + - name: "string_field" + type: "string" + default: "hello" + - name: "duration_field" + type: "duration" + default: 10s + - name: "timestamp_field" + type: "timestamp" + default: 2026-10-05T00:00:00Z + - name: "enum_field" + type: "com.example.Status" + default: "ACTIVE" + - name: "struct_field" + type: "map" + - name: "list_value_field" + type: "list" + - name: "any_field" + type: "any" + - name: "func_field" + type: "function" + - name: "type_param_f" + type: "~T" + - name: "unspec_prim_f" + type: + type_name: "dyn" + - name: "bool_w_f" + type: "bool_wrapper" + - name: "int_w_f" + type: "int_wrapper" + - name: "uint_w_f" + type: "uint_wrapper" + - name: "double_w_f" + type: "double_wrapper" + - name: "bytes_w_f" + type: "bytes_wrapper" + - name: "unspec_w_f" + type: + type_name: "dyn" + - name: "unspec_wk_f" + type: + type_name: "dyn" + - name: "map_val_only_f" + type: "map" + - name: "type_int_f" + type: "type" + - name: "type_unspec_f" + type: + type_name: "type" + - name: "abstract_params_f" + type: "MyGeneric" + - name: "msg_f" + type: "com.example.Msg" + - name: "error_f" + type: + type_name: "dyn" +# use_type_signatures = false +struct: + name: "AllSupportedFieldTypes" + fields: + - name: "null_field" + type: + type_name: "null" + default: ~ + - name: "bool_field" + type: + type_name: "bool" + default: true + - name: "int_field" + type: + type_name: "int" + default: 123 + - name: "uint_field" + type: + type_name: "uint" + default: 456 + - name: "double_field" + type: + type_name: "double" + default: 3.25 + - name: "bytes_field" + type: + type_name: "bytes" + default: b"\x01\x02\x03" + - name: "string_field" + type: + type_name: "string" + default: "hello" + - name: "duration_field" + type: + type_name: "duration" + default: 10s + - name: "timestamp_field" + type: + type_name: "timestamp" + default: 2026-10-05T00:00:00Z + - name: "enum_field" + type: + type_name: "com.example.Status" + default: "ACTIVE" + - 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: "func_field" + type: + type_name: "function" + params: + - type_name: "bool" + - type_name: "int" + - type_name: "string" + - name: "type_param_f" + type: + type_name: "T" + is_type_param: true + - name: "unspec_prim_f" + type: + type_name: "dyn" + - name: "bool_w_f" + type: + type_name: "bool_wrapper" + - name: "int_w_f" + type: + type_name: "int_wrapper" + - name: "uint_w_f" + type: + type_name: "uint_wrapper" + - name: "double_w_f" + type: + type_name: "double_wrapper" + - name: "bytes_w_f" + type: + type_name: "bytes_wrapper" + - name: "unspec_w_f" + type: + type_name: "dyn" + - name: "unspec_wk_f" + type: + type_name: "dyn" + - name: "map_val_only_f" + type: + type_name: "map" + params: + - type_name: "dyn" + - type_name: "int" + - name: "type_int_f" + type: + type_name: "type" + params: + - type_name: "int" + - name: "type_unspec_f" + type: + type_name: "type" + - name: "abstract_params_f" + type: + type_name: "MyGeneric" + params: + - type_name: "int" + - name: "msg_f" + type: + type_name: "com.example.Msg" + - name: "error_f" + type: + type_name: "dyn" diff --git a/common/typedef/testdata/typedefs.txtproto b/common/typedef/testdata/typedefs.txtproto new file mode 100644 index 000000000..1bf57c720 --- /dev/null +++ b/common/typedef/testdata/typedefs.txtproto @@ -0,0 +1,273 @@ +# proto-file: common/typedef/typedef.proto +# proto-message: cel.types.TypeDefSet + +types { + struct_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.proto2_field_aspect] { + name: "order_id_v2" + id: 1 + json_name: "orderId" + } + [cel.types.proto3_field_aspect] { + 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: "unparameterized_meta_type" + type { + type {} + } + } + 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_aspect_field" + type { + primitive: STRING + } + [cel.types.proto2_field_aspect] {} + [cel.types.proto3_field_aspect] {} + } + [cel.types.proto2_struct_aspect] { + name: "com.example.proto2.Order" + } + [cel.types.proto3_struct_aspect] { + 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.proto2_enum_constant_aspect] { + name: "STATUS_ACTIVE_V2" + } + [cel.types.proto3_enum_constant_aspect] { + name: "STATUS_ACTIVE_V3" + } + } + constants { + name: "ORDER_STATUS_LEGACY" + id: -1 + } + [cel.types.proto2_enum_aspect] { + name: "com.example.proto2.OrderStatus" + allow_alias: true + } + [cel.types.proto3_enum_aspect] { + 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..6e111d0e0 --- /dev/null +++ b/common/typedef/typedef.cc @@ -0,0 +1,81 @@ +// 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 StructTypeDef::AddField(Field field) { + if (field.name().empty()) { + return absl::InvalidArgumentError("Field name cannot be empty."); + } + 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 StructTypeDef::Field* absl_nullable StructTypeDef::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 (constant.name().empty()) { + return absl::InvalidArgumentError("Enum constant name cannot be empty."); + } + 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 StructTypeDef&) { return TypeDefKindCase::kStruct; }, + [](const EnumTypeDef&) { return TypeDefKindCase::kEnum; }, + }, + variant_); +} + +} // namespace cel diff --git a/common/typedef/typedef.h b/common/typedef/typedef.h new file mode 100644 index 000000000..5850f190b --- /dev/null +++ b/common/typedef/typedef.h @@ -0,0 +1,209 @@ +// 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 "absl/base/attributes.h" +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "common/ast.h" +#include "common/constant.h" + +namespace cel { + +// Represents a struct type with typed fields. +class StructTypeDef { + public: + class Field { + public: + Field() = default; + Field(std::string name, TypeSpec type, std::string doc = "", + Constant default_value = {}, + absl::flat_hash_map aspects = {}) + : name_(std::move(name)), + type_(std::move(type)), + doc_(std::move(doc)), + default_value_(std::move(default_value)), + aspects_(std::move(aspects)) {} + + absl::string_view name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return name_; + } + const TypeSpec& type() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return type_; } + absl::string_view doc() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return doc_; } + // Optional scalar default value; check `default_value().has_value()`. + // When the field type is an enum type, `default_value` is the name of the + // corresponding `EnumConstant`. + const Constant& default_value() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return default_value_; + } + // Aspects (e.g. schema-specific properties) keyed by aspect name. + const absl::flat_hash_map& aspects() const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + return aspects_; + } + + private: + std::string name_; + TypeSpec type_; + std::string doc_; + Constant default_value_; + absl::flat_hash_map aspects_; + }; + + StructTypeDef() = default; + explicit StructTypeDef( + std::string name, std::string doc = "", + absl::flat_hash_map aspects = {}) + : name_(std::move(name)), + doc_(std::move(doc)), + aspects_(std::move(aspects)) {} + + absl::string_view name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return name_; } + absl::string_view doc() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return doc_; } + const std::vector& fields() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return fields_; + } + // Aspects (e.g. schema-specific properties) keyed by aspect name. + const absl::flat_hash_map& aspects() const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + return aspects_; + } + + // Adds a field to the struct definition. Field names must be non-empty and + // unique. + absl::Status AddField(Field field); + const Field* absl_nullable FindField(absl::string_view field_name) const + ABSL_ATTRIBUTE_LIFETIME_BOUND; + + private: + std::string name_; + std::string doc_; + std::vector fields_; + absl::flat_hash_map aspects_; +}; + +// Represents a named enumeration type with named and numbered constants. +class EnumTypeDef { + public: + class EnumConstant { + public: + EnumConstant() = default; + explicit EnumConstant( + std::string name, int32_t id = 0, std::string doc = "", + absl::flat_hash_map aspects = {}) + : name_(std::move(name)), + id_(id), + doc_(std::move(doc)), + aspects_(std::move(aspects)) {} + + absl::string_view name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return name_; + } + int32_t id() const { return id_; } + absl::string_view doc() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return doc_; } + // Aspects (e.g. schema-specific properties) keyed by aspect name. + const absl::flat_hash_map& aspects() const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + return aspects_; + } + + private: + std::string name_; + int32_t id_ = 0; + std::string doc_; + absl::flat_hash_map aspects_; + }; + + EnumTypeDef() = default; + explicit EnumTypeDef(std::string name, std::string doc = "", + absl::flat_hash_map aspects = {}) + : name_(std::move(name)), + doc_(std::move(doc)), + aspects_(std::move(aspects)) {} + + absl::string_view name() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return name_; } + absl::string_view doc() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return doc_; } + const std::vector& constants() const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + return constants_; + } + // Aspects (e.g. schema-specific properties) keyed by aspect name. + const absl::flat_hash_map& aspects() const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + return aspects_; + } + + // Adds a constant to the enum definition. Constant names must be non-empty + // and unique; numeric IDs may alias. + absl::Status AddConstant(EnumConstant constant); + const EnumConstant* absl_nullable FindConstant( + absl::string_view constant_name) const ABSL_ATTRIBUTE_LIFETIME_BOUND; + + private: + std::string name_; + std::string doc_; + std::vector constants_; + absl::flat_hash_map aspects_; +}; + +enum class TypeDefKindCase { + kStruct, + kEnum, +}; + +// Variant wrapper holding either a StructTypeDef or EnumTypeDef. +class TypeDef { + public: + TypeDef() = default; + explicit TypeDef(StructTypeDef struct_type) + : variant_(std::move(struct_type)) {} + explicit TypeDef(EnumTypeDef enum_type) : variant_(std::move(enum_type)) {} + + TypeDefKindCase kind_case() const; + + bool is_struct() const { + return std::holds_alternative(variant_); + } + // REQUIRES: `is_struct()` is true. + const StructTypeDef& struct_type() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + ABSL_DCHECK(is_struct()); + return std::get(variant_); + } + + bool is_enum() const { return std::holds_alternative(variant_); } + // REQUIRES: `is_enum()` is true. + const EnumTypeDef& enum_type() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + ABSL_DCHECK(is_enum()); + return std::get(variant_); + } + + private: + std::variant variant_; +}; + +} // 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..179bae429 --- /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 a struct or an enum). +message TypeDef { + oneof kind { + Struct struct_type = 1; + Enum enum_type = 2; + } +} + +// Represents a structured type with typed fields. +message Struct { + 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..d75facc5c --- /dev/null +++ b/common/typedef/typedef_proto.cc @@ -0,0 +1,625 @@ +// 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/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/types/span.h" +#include "absl/types/variant.h" +#include "common/ast.h" +#include "common/ast/constant_proto.h" +#include "common/constant.h" +#include "common/typedef/aspect_proto.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef.pb.h" +#include "internal/status_macros.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: { + if (type.type().type_kind_case() == TypePb::TYPE_KIND_NOT_SET) { + return TypeSpec(std::unique_ptr()); + } + 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()); +} + +absl::StatusOr> DecodeStructAspects( + const types::Struct& proto, const AspectProtoRegistry& registry) { + absl::flat_hash_map result; + for (const auto& [name, aspect] : registry.aspects()) { + CEL_ASSIGN_OR_RETURN(std::any props, aspect->DecodeStructProperties(proto)); + if (props.has_value()) { + result[aspect->name()] = std::move(props); + } + } + return result; +} + +absl::StatusOr> DecodeFieldAspects( + const types::Struct::Field& proto, const AspectProtoRegistry& registry) { + absl::flat_hash_map result; + for (const auto& [name, aspect] : registry.aspects()) { + CEL_ASSIGN_OR_RETURN(std::any props, aspect->DecodeFieldProperties(proto)); + if (props.has_value()) { + result[aspect->name()] = std::move(props); + } + } + return result; +} + +absl::StatusOr> DecodeEnumAspects( + const types::Enum& proto, const AspectProtoRegistry& registry) { + absl::flat_hash_map result; + for (const auto& [name, aspect] : registry.aspects()) { + CEL_ASSIGN_OR_RETURN(std::any props, aspect->DecodeEnumProperties(proto)); + if (props.has_value()) { + result[aspect->name()] = std::move(props); + } + } + return result; +} + +absl::StatusOr> +DecodeEnumConstantAspects(const types::Enum::EnumConstant& proto, + const AspectProtoRegistry& registry) { + absl::flat_hash_map result; + for (const auto& [name, aspect] : registry.aspects()) { + CEL_ASSIGN_OR_RETURN(std::any props, + aspect->DecodeEnumConstantProperties(proto)); + if (props.has_value()) { + result[aspect->name()] = std::move(props); + } + } + return result; +} + +absl::Status EncodeStructAspects( + const absl::flat_hash_map& properties, + types::Struct* absl_nonnull out, const AspectProtoRegistry& registry) { + for (const auto& [aspect_name, props] : properties) { + if (!props.has_value()) { + continue; + } + const AspectProto* aspect = registry.Find(aspect_name); + if (aspect == nullptr) { + continue; + } + CEL_RETURN_IF_ERROR(aspect->EncodeStructProperties(props, out)); + } + return absl::OkStatus(); +} + +absl::Status EncodeFieldAspects( + const absl::flat_hash_map& properties, + types::Struct::Field* absl_nonnull out, + const AspectProtoRegistry& registry) { + for (const auto& [aspect_name, props] : properties) { + if (!props.has_value()) { + continue; + } + const AspectProto* aspect = registry.Find(aspect_name); + if (aspect == nullptr) { + continue; + } + CEL_RETURN_IF_ERROR(aspect->EncodeFieldProperties(props, out)); + } + return absl::OkStatus(); +} + +absl::Status EncodeEnumAspects( + const absl::flat_hash_map& properties, + types::Enum* absl_nonnull out, const AspectProtoRegistry& registry) { + for (const auto& [aspect_name, props] : properties) { + if (!props.has_value()) { + continue; + } + const AspectProto* aspect = registry.Find(aspect_name); + if (aspect == nullptr) { + continue; + } + CEL_RETURN_IF_ERROR(aspect->EncodeEnumProperties(props, out)); + } + return absl::OkStatus(); +} + +absl::Status EncodeEnumConstantAspects( + const absl::flat_hash_map& properties, + types::Enum::EnumConstant* absl_nonnull out, + const AspectProtoRegistry& registry) { + for (const auto& [aspect_name, props] : properties) { + if (!props.has_value()) { + continue; + } + const AspectProto* aspect = registry.Find(aspect_name); + if (aspect == nullptr) { + continue; + } + CEL_RETURN_IF_ERROR(aspect->EncodeEnumConstantProperties(props, out)); + } + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr StructTypeDefFromProto( + const types::Struct& proto, const AspectProtoRegistry& registry) { + CEL_ASSIGN_OR_RETURN(auto aspect_props, DecodeStructAspects(proto, registry)); + StructTypeDef struct_def(proto.name(), proto.doc(), std::move(aspect_props)); + + for (const types::Struct::Field& field_proto : proto.fields()) { + TypeSpec field_type; + if (field_proto.has_type()) { + CEL_ASSIGN_OR_RETURN(field_type, TypeSpecFromProto(field_proto.type())); + } + Constant default_value; + if (field_proto.has_default_value()) { + CEL_RETURN_IF_ERROR(ast_internal::ConstantFromProto( + field_proto.default_value(), default_value)); + } + CEL_ASSIGN_OR_RETURN(auto field_aspect_props, + DecodeFieldAspects(field_proto, registry)); + CEL_RETURN_IF_ERROR(struct_def.AddField(StructTypeDef::Field( + field_proto.name(), std::move(field_type), field_proto.doc(), + std::move(default_value), std::move(field_aspect_props)))); + } + + return struct_def; +} + +absl::Status StructTypeDefToProto(const StructTypeDef& struct_def, + types::Struct* absl_nonnull out, + const AspectProtoRegistry& registry) { + out->Clear(); + if (!struct_def.name().empty()) { + out->set_name(struct_def.name()); + } + if (!struct_def.doc().empty()) { + out->set_doc(struct_def.doc()); + } + CEL_RETURN_IF_ERROR(EncodeStructAspects(struct_def.aspects(), out, registry)); + + out->mutable_fields()->Reserve(struct_def.fields().size()); + for (const StructTypeDef::Field& field : struct_def.fields()) { + types::Struct::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( + EncodeFieldAspects(field.aspects(), field_proto, registry)); + } + return absl::OkStatus(); +} + +absl::StatusOr StructTypeDefToProto( + const StructTypeDef& struct_def, const AspectProtoRegistry& registry) { + types::Struct out; + CEL_RETURN_IF_ERROR(StructTypeDefToProto(struct_def, &out, registry)); + return out; +} + +absl::StatusOr EnumTypeDefFromProto( + const types::Enum& proto, const AspectProtoRegistry& registry) { + CEL_ASSIGN_OR_RETURN(auto aspect_props, DecodeEnumAspects(proto, registry)); + EnumTypeDef enum_def(proto.name(), proto.doc(), std::move(aspect_props)); + + for (const types::Enum::EnumConstant& constant_proto : proto.constants()) { + CEL_ASSIGN_OR_RETURN(auto constant_aspect_props, + DecodeEnumConstantAspects(constant_proto, registry)); + CEL_RETURN_IF_ERROR(enum_def.AddConstant(EnumTypeDef::EnumConstant( + constant_proto.name(), constant_proto.id(), constant_proto.doc(), + std::move(constant_aspect_props)))); + } + + return enum_def; +} + +absl::Status EnumTypeDefToProto(const EnumTypeDef& enum_def, + types::Enum* absl_nonnull out, + const AspectProtoRegistry& 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(EncodeEnumAspects(enum_def.aspects(), 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(EncodeEnumConstantAspects(constant.aspects(), + constant_proto, registry)); + } + return absl::OkStatus(); +} + +absl::StatusOr EnumTypeDefToProto( + const EnumTypeDef& enum_def, const AspectProtoRegistry& registry) { + types::Enum out; + CEL_RETURN_IF_ERROR(EnumTypeDefToProto(enum_def, &out, registry)); + return out; +} + +absl::StatusOr TypeDefFromProto(const types::TypeDef& proto, + const AspectProtoRegistry& registry) { + switch (proto.kind_case()) { + case types::TypeDef::kStructType: { + CEL_ASSIGN_OR_RETURN(StructTypeDef s, StructTypeDefFromProto( + proto.struct_type(), registry)); + return TypeDef(std::move(s)); + } + 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: + break; + } + return absl::InvalidArgumentError( + "cel.types.TypeDef must specify 'struct_type' or 'enum_type'"); +} + +absl::Status TypeDefToProto(const TypeDef& type_def, + types::TypeDef* absl_nonnull out, + const AspectProtoRegistry& registry) { + out->Clear(); + if (type_def.is_struct()) { + return StructTypeDefToProto(type_def.struct_type(), + out->mutable_struct_type(), registry); + } + return EnumTypeDefToProto(type_def.enum_type(), out->mutable_enum_type(), + registry); +} + +absl::StatusOr TypeDefToProto( + const TypeDef& type_def, const AspectProtoRegistry& registry) { + types::TypeDef out; + CEL_RETURN_IF_ERROR(TypeDefToProto(type_def, &out, registry)); + return out; +} + +absl::StatusOr> TypeDefsFromProto( + const types::TypeDefSet& proto, const AspectProtoRegistry& 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 AspectProtoRegistry& 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 AspectProtoRegistry& registry) { + types::TypeDefSet out; + CEL_RETURN_IF_ERROR(TypeDefsToProto(type_defs, &out, registry)); + return out; +} + +} // namespace cel diff --git a/common/typedef/typedef_proto.h b/common/typedef/typedef_proto.h new file mode 100644 index 000000000..c5ff90f81 --- /dev/null +++ b/common/typedef/typedef_proto.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_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/aspect_proto.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef.pb.h" + +namespace cel { + +// Converts a cel.types.Struct proto message to a native StructTypeDef. +// +// Aspects on the struct and its fields are decoded +// by delegating to the corresponding `AspectProto` codecs in `registry`. +absl::StatusOr StructTypeDefFromProto( + const types::Struct& proto, const AspectProtoRegistry& registry = {}); + +// Converts a native StructTypeDef to a cel.types.Struct proto message. +absl::Status StructTypeDefToProto(const StructTypeDef& struct_def, + types::Struct* absl_nonnull out, + const AspectProtoRegistry& registry = {}); + +absl::StatusOr StructTypeDefToProto( + const StructTypeDef& struct_def, const AspectProtoRegistry& registry = {}); + +// Converts a cel.types.Enum proto message to a native EnumTypeDef. +// +// Aspects on the enum and its constants are +// decoded by delegating to the corresponding `AspectProto` codecs in +// `registry`. +absl::StatusOr EnumTypeDefFromProto( + const types::Enum& proto, const AspectProtoRegistry& 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 AspectProtoRegistry& registry = {}); + +absl::StatusOr EnumTypeDefToProto( + const EnumTypeDef& enum_def, const AspectProtoRegistry& registry = {}); + +// Converts a cel.types.TypeDef proto message to a native TypeDef. +// +// Aspects on struct, field, enum, and enum +// constant messages are decoded by delegating to the corresponding +// `AspectProto` codecs in `registry`. +absl::StatusOr TypeDefFromProto( + const types::TypeDef& proto, const AspectProtoRegistry& registry = {}); + +// Converts a native TypeDef to a cel.types.TypeDef proto message. +// +// Aspects are encoded as proto extensions by delegating to +// the corresponding `AspectProto` codecs looked up in `registry`. +absl::Status TypeDefToProto(const TypeDef& type_def, + types::TypeDef* absl_nonnull out, + const AspectProtoRegistry& registry = {}); + +absl::StatusOr TypeDefToProto( + const TypeDef& type_def, const AspectProtoRegistry& registry = {}); + +// Converts a cel.types.TypeDefSet proto message to a sequence of native +// TypeDefs. +// +// Aspects are decoded by delegating to the +// corresponding `AspectProto` codecs in `registry`. +absl::StatusOr> TypeDefsFromProto( + const types::TypeDefSet& proto, const AspectProtoRegistry& registry = {}); + +// Converts a sequence of native TypeDefs to a cel.types.TypeDefSet proto +// message. +// +// Aspects are encoded as proto extensions by delegating to +// the corresponding `AspectProto` codecs looked up in `registry`. +absl::Status TypeDefsToProto(absl::Span type_defs, + types::TypeDefSet* absl_nonnull out, + const AspectProtoRegistry& registry = {}); + +absl::StatusOr TypeDefsToProto( + absl::Span type_defs, + const AspectProtoRegistry& 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..f256df1e6 --- /dev/null +++ b/common/typedef/typedef_proto_test.cc @@ -0,0 +1,245 @@ +// 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 "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 "common/typedef/aspect_proto.h" +#include "common/typedef/proto_schema_proto.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef.pb.h" +#include "internal/proto_matchers.h" +#include "internal/runfiles.h" +#include "internal/testing.h" +#include "google/protobuf/text_format.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::cel::internal::test::EqualsProto; +using ::testing::HasSubstr; + +constexpr absl::string_view kTestTypeDefFilePath = +"_main/common/typedef/testdata/"; + +AspectProtoRegistry MakeProtoRegistry() { + return AspectProtoRegistry(std::make_unique(), + std::make_unique()); +} + +TEST(TypeDefProtoTest, TextProtoRoundTrip) { + std::string contents; + std::string test_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, "typedefs.txtproto")); + ASSERT_THAT(cel::internal::GetFileContents(test_file, &contents), IsOk()); + + AspectProtoRegistry 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)); + ASSERT_OK_AND_ASSIGN(types::TypeDefSet roundtrip_proto_set, + TypeDefsToProto(type_defs, proto_registry)); + + std::string expected_text, roundtrip_text; + ASSERT_TRUE(google::protobuf::TextFormat::PrintToString(proto_set, &expected_text)); + ASSERT_TRUE( + google::protobuf::TextFormat::PrintToString(roundtrip_proto_set, &roundtrip_text)); + EXPECT_EQ(roundtrip_text, expected_text); +} + +struct TypeDefProtoTestCase { + std::string textproto; + absl::StatusCode expected_code = absl::StatusCode::kOk; + std::string expected_error; + std::string expected_typedef; +}; + +class TypeDefProtoTest : public testing::TestWithParam {}; + +TEST_P(TypeDefProtoTest, TypeDefProto) { + const TypeDefProtoTestCase& param = GetParam(); + types::TypeDef proto; + ASSERT_TRUE(google::protobuf::TextFormat::ParseFromString(param.textproto, &proto)); + absl::StatusOr type_def = + TypeDefFromProto(proto, MakeProtoRegistry()); + ASSERT_THAT(type_def, + StatusIs(param.expected_code, HasSubstr(param.expected_error))); + if (type_def.ok()) { + types::TypeDef expected_proto; + ASSERT_TRUE(google::protobuf::TextFormat::ParseFromString(param.expected_typedef, + &expected_proto)); + EXPECT_THAT(TypeDefToProto(*type_def), + IsOkAndHolds(EqualsProto(expected_proto))); + if (type_def->is_struct()) { + EXPECT_THAT(StructTypeDefToProto(type_def->struct_type()), + IsOkAndHolds(EqualsProto(expected_proto.struct_type()))); + } else { + EXPECT_THAT(EnumTypeDefToProto(type_def->enum_type()), + IsOkAndHolds(EqualsProto(expected_proto.enum_type()))); + } + } +} + +INSTANTIATE_TEST_SUITE_P( + TypeDefProtoTest, TypeDefProtoTest, + ::testing::Values( + TypeDefProtoTestCase{ + .textproto = "", + .expected_code = absl::StatusCode::kInvalidArgument, + .expected_error = + "cel.types.TypeDef must specify 'struct_type' or 'enum_type'", + }, + TypeDefProtoTestCase{ + .textproto = R"pb( + struct_type { + name: "com.example.SpecialTypes" + fields { + name: "err_field" + type { error {} } + } + fields { + name: "unspec_prim_field" + type { primitive: PRIMITIVE_TYPE_UNSPECIFIED } + } + fields { + name: "unspec_wk_field" + type { well_known: WELL_KNOWN_TYPE_UNSPECIFIED } + } + fields { + name: "unset_type_field" + type {} + } + } + )pb", + .expected_typedef = R"pb( + struct_type { + name: "com.example.SpecialTypes" + fields { + name: "err_field" + type { error {} } + } + fields { + name: "unspec_prim_field" + type { primitive: PRIMITIVE_TYPE_UNSPECIFIED } + } + fields { + name: "unspec_wk_field" + type { well_known: WELL_KNOWN_TYPE_UNSPECIFIED } + } + fields { name: "unset_type_field" } + } + )pb", + }, + TypeDefProtoTestCase{ + .textproto = R"pb( + struct_type { + name: "com.example.DupFields" + fields { name: "dup" } + fields { name: "dup" } + } + )pb", + .expected_code = absl::StatusCode::kAlreadyExists, + .expected_error = "Field 'dup' is already defined.", + }, + TypeDefProtoTestCase{ + .textproto = R"pb( + enum_type { + name: "com.example.DupConstants" + constants { name: "DUP_VAL" } + constants { name: "DUP_VAL" } + } + )pb", + .expected_code = absl::StatusCode::kAlreadyExists, + .expected_error = "Enum constant 'DUP_VAL' is already defined.", + }, + // Aspects for an unregistered aspect on + // cel.types.Struct are ignored. + TypeDefProtoTestCase{ + .textproto = R"pb( + struct_type { + name: "com.example.Struct" + [cel.types.proto2_struct_aspect] { name: "x" } + } + )pb", + .expected_typedef = R"pb( + struct_type { name: "com.example.Struct" } + )pb", + }, + // Aspects for an unregistered aspect on + // cel.types.Struct.Field are ignored. + TypeDefProtoTestCase{ + .textproto = R"pb( + struct_type { + name: "com.example.Struct" + fields { + name: "f" + [cel.types.proto2_field_aspect] { name: "x" } + } + } + )pb", + .expected_typedef = R"pb( + struct_type { + name: "com.example.Struct" + fields { name: "f" } + } + )pb", + }, + // Aspects for an unregistered aspect on + // cel.types.Enum are ignored. + TypeDefProtoTestCase{ + .textproto = R"pb( + enum_type { + name: "com.example.Enum" + [cel.types.proto2_enum_aspect] { name: "x" } + } + )pb", + .expected_typedef = R"pb( + enum_type { name: "com.example.Enum" } + )pb", + }, + // Aspects for an unregistered aspect on + // cel.types.Enum.EnumConstant are ignored. + TypeDefProtoTestCase{ + .textproto = R"pb( + enum_type { + name: "com.example.Enum" + constants { + name: "C" + [cel.types.proto2_enum_constant_aspect] { name: "x" } + } + } + )pb", + .expected_typedef = R"pb( + enum_type { + name: "com.example.Enum" + constants { name: "C" id: 0 } + } + )pb", + })); + +} // namespace +} // namespace cel diff --git a/common/typedef/typedef_test.cc b/common/typedef/typedef_test.cc new file mode 100644 index 000000000..466bae0cc --- /dev/null +++ b/common/typedef/typedef_test.cc @@ -0,0 +1,228 @@ +// 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 + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "common/ast.h" +#include "common/constant.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; + +struct TestStructProperties { + std::string custom_name; +}; +struct TestFieldProperties { + std::string json_name; +}; +struct TestEnumProperties { + bool allow_alias = false; +}; +struct TestEnumConstantProperties { + std::string custom_name; +}; + +TEST(TypeDefTest, DefaultTypeDefIsStruct) { + TypeDef def; + EXPECT_TRUE(def.is_struct()); + EXPECT_FALSE(def.is_enum()); + EXPECT_EQ(def.kind_case(), TypeDefKindCase::kStruct); + EXPECT_THAT(def.struct_type().name(), Eq("")); + EXPECT_THAT(def.struct_type().doc(), Eq("")); + EXPECT_THAT(def.struct_type().aspects(), IsEmpty()); + EXPECT_TRUE(def.struct_type().fields().empty()); + EXPECT_THAT(def.struct_type().FindField("id"), IsNull()); +} + +TEST(TypeDefTest, StructTypeDefConstructionAndLookup) { + StructTypeDef person( + "com.example.Person", "User account profile entity.", + {{"json", TestStructProperties{.custom_name = "Person"}}}); + + ASSERT_THAT( + person.AddField(StructTypeDef::Field( + "id", TypeSpec(PrimitiveType::kInt64), "Unique user identifier.")), + IsOk()); + + StructTypeDef::Field display_name_field( + "display_name", TypeSpec(PrimitiveType::kString), "Display name.", + Constant(StringConstant("Anonymous")), + { + {"json", TestFieldProperties{.json_name = "displayName"}}, + {"proto3", TestFieldProperties{.json_name = "display_name"}}, + }); + ASSERT_THAT(person.AddField(std::move(display_name_field)), IsOk()); + + ASSERT_THAT(person.AddField(StructTypeDef::Field( + "tags", TypeSpec(ListTypeSpec(std::make_unique( + PrimitiveType::kString))))), + IsOk()); + + EXPECT_THAT(person.AddField( + StructTypeDef::Field("", TypeSpec(PrimitiveType::kString))), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Field name cannot be empty."))); + + EXPECT_THAT(person.AddField( + StructTypeDef::Field("id", TypeSpec(PrimitiveType::kString))), + StatusIs(absl::StatusCode::kAlreadyExists, + HasSubstr("Field 'id' is already defined."))); + + const StructTypeDef::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->aspects(), IsEmpty()); + + const StructTypeDef::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->aspects().size(), 2); + ASSERT_TRUE(name_field->aspects().contains("json")); + EXPECT_EQ(std::any_cast( + name_field->aspects().at("json")) + .json_name, + "displayName"); + ASSERT_TRUE(name_field->aspects().contains("proto3")); + EXPECT_EQ(std::any_cast( + name_field->aspects().at("proto3")) + .json_name, + "display_name"); + + const StructTypeDef::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.aspects().size(), 1); + ASSERT_TRUE(person.aspects().contains("json")); + EXPECT_EQ( + std::any_cast(person.aspects().at("json")) + .custom_name, + "Person"); + + EXPECT_THAT(person.FindField("nonexistent"), IsNull()); + + TypeDef type_def(std::move(person)); + EXPECT_TRUE(type_def.is_struct()); + EXPECT_FALSE(type_def.is_enum()); + EXPECT_EQ(type_def.kind_case(), TypeDefKindCase::kStruct); + EXPECT_EQ(type_def.struct_type().name(), "com.example.Person"); + EXPECT_EQ(type_def.struct_type().doc(), "User account profile entity."); + EXPECT_EQ(type_def.struct_type().fields().size(), 3); + EXPECT_EQ(type_def.struct_type().aspects().size(), 1); + EXPECT_TRUE(type_def.struct_type().aspects().contains("json")); +} + +TEST(TypeDefTest, EnumTypeDefConstructionAndLookup) { + EnumTypeDef status_enum("com.example.OrderStatus", + "Lifecycle state of a customer order.", + {{"json", TestEnumProperties{.allow_alias = true}}}); + + ASSERT_THAT(status_enum.AddConstant(EnumTypeDef::EnumConstant( + "ORDER_STATUS_UNSPECIFIED", 0, "Default unspecified state.")), + IsOk()); + + EnumTypeDef::EnumConstant shipped_val( + "SHIPPED", 2, "Order has been dispatched.", + {{"json", TestEnumConstantProperties{.custom_name = "shipped"}}}); + ASSERT_THAT(status_enum.AddConstant(std::move(shipped_val)), IsOk()); + + EXPECT_THAT(status_enum.AddConstant(EnumTypeDef::EnumConstant("")), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Enum constant name cannot be empty."))); + + EXPECT_THAT( + status_enum.AddConstant(EnumTypeDef::EnumConstant("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."); + ASSERT_TRUE(shipped->aspects().contains("json")); + EXPECT_EQ(std::any_cast( + shipped->aspects().at("json")) + .custom_name, + "shipped"); + + EnumTypeDef empty_enum; + EXPECT_THAT(empty_enum.FindConstant("ORDER_STATUS_UNSPECIFIED"), IsNull()); + + EXPECT_EQ(status_enum.aspects().size(), 1); + ASSERT_TRUE(status_enum.aspects().contains("json")); + EXPECT_TRUE( + std::any_cast(status_enum.aspects().at("json")) + .allow_alias); + + 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->aspects(), IsEmpty()); + + EXPECT_THAT(status_enum.FindConstant("UNKNOWN"), IsNull()); + + TypeDef type_def(std::move(status_enum)); + EXPECT_FALSE(type_def.is_struct()); + EXPECT_TRUE(type_def.is_enum()); + EXPECT_EQ(type_def.kind_case(), TypeDefKindCase::kEnum); + EXPECT_EQ(type_def.enum_type().name(), "com.example.OrderStatus"); + EXPECT_EQ(type_def.enum_type().doc(), "Lifecycle state of a customer order."); + EXPECT_EQ(type_def.enum_type().constants().size(), 2); + EXPECT_EQ(type_def.enum_type().aspects().size(), 1); + EXPECT_TRUE(type_def.enum_type().aspects().contains("json")); +} + +TEST(TypeDefTest, DefaultEnumTypeDef) { + TypeDef def((EnumTypeDef())); + EXPECT_FALSE(def.is_struct()); + EXPECT_TRUE(def.is_enum()); + EXPECT_EQ(def.kind_case(), TypeDefKindCase::kEnum); + EXPECT_THAT(def.enum_type().name(), Eq("")); + EXPECT_THAT(def.enum_type().doc(), Eq("")); + EXPECT_THAT(def.enum_type().aspects(), IsEmpty()); + EXPECT_TRUE(def.enum_type().constants().empty()); + EXPECT_THAT(def.enum_type().FindConstant("ANY"), IsNull()); +} + +} // namespace +} // namespace cel diff --git a/common/typedef/typedef_yaml.cc b/common/typedef/typedef_yaml.cc new file mode 100644 index 000000000..2f58dada0 --- /dev/null +++ b/common/typedef/typedef_yaml.cc @@ -0,0 +1,1002 @@ +// 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 + +#include "absl/algorithm/container.h" +#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/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 "common/ast.h" +#include "common/constant.h" +#include "common/signature.h" +#include "common/typedef/aspect_yaml.h" +#include "common/typedef/typedef.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/exceptions.h" +#include "yaml-cpp/node/node.h" +#include "yaml-cpp/null.h" +#include "yaml-cpp/yaml.h" // IWYU pragma: keep + +namespace cel { +namespace { + +using ::cel::typedef_internal::FormatYamlErrorMessage; +using ::cel::typedef_internal::GetBinary; +using ::cel::typedef_internal::GetBool; +using ::cel::typedef_internal::GetContextNodeForKeyValue; +using ::cel::typedef_internal::GetInt32; +using ::cel::typedef_internal::GetString; +using ::cel::typedef_internal::IsBinary; +using ::cel::typedef_internal::LoadYaml; +using ::cel::typedef_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) { + if (!params.empty()) { + return YamlError(yaml, node, + "Type parameter cannot have type parameters"); + } + 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; + }; + + auto make_no_param_type = [&](TypeSpec spec) -> absl::StatusOr { + if (!params.empty()) { + return YamlError( + yaml, node, + absl::StrCat("Type '", name_str, "' cannot have type parameters")); + } + return spec; + }; + + if (name_str == "null") return make_no_param_type(TypeSpec(NullTypeSpec())); + if (name_str == "bool") { + return make_no_param_type(TypeSpec(PrimitiveType::kBool)); + } + if (name_str == "int") { + return make_no_param_type(TypeSpec(PrimitiveType::kInt64)); + } + if (name_str == "uint") { + return make_no_param_type(TypeSpec(PrimitiveType::kUint64)); + } + if (name_str == "double") { + return make_no_param_type(TypeSpec(PrimitiveType::kDouble)); + } + if (name_str == "string") { + return make_no_param_type(TypeSpec(PrimitiveType::kString)); + } + if (name_str == "bytes") { + return make_no_param_type(TypeSpec(PrimitiveType::kBytes)); + } + if (name_str == "any" || name_str == "google.protobuf.Any") { + return make_no_param_type(TypeSpec(WellKnownTypeSpec::kAny)); + } + if (name_str == "timestamp" || name_str == "google.protobuf.Timestamp") { + return make_no_param_type(TypeSpec(WellKnownTypeSpec::kTimestamp)); + } + if (name_str == "duration" || name_str == "google.protobuf.Duration") { + return make_no_param_type(TypeSpec(WellKnownTypeSpec::kDuration)); + } + if (name_str == "dyn" || name_str == "google.protobuf.Value") { + return make_no_param_type(TypeSpec(DynTypeSpec())); + } + if (name_str == "bool_wrapper" || name_str == "google.protobuf.BoolValue") { + return make_no_param_type( + TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kBool))); + } + if (name_str == "int_wrapper" || name_str == "google.protobuf.Int64Value" || + name_str == "google.protobuf.Int32Value") { + return make_no_param_type( + TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kInt64))); + } + if (name_str == "uint_wrapper" || name_str == "google.protobuf.UInt64Value" || + name_str == "google.protobuf.UInt32Value") { + return make_no_param_type( + TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kUint64))); + } + if (name_str == "double_wrapper" || + name_str == "google.protobuf.DoubleValue" || + name_str == "google.protobuf.FloatValue") { + return make_no_param_type( + TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kDouble))); + } + if (name_str == "string_wrapper" || + name_str == "google.protobuf.StringValue") { + return make_no_param_type( + TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kString))); + } + if (name_str == "bytes_wrapper" || name_str == "google.protobuf.BytesValue") { + return make_no_param_type( + TypeSpec(PrimitiveTypeWrapper(PrimitiveType::kBytes))); + } + if (name_str == "google.protobuf.ListValue") { + return make_no_param_type( + TypeSpec(ListTypeSpec(std::make_unique(DynTypeSpec())))); + } + if (name_str == "google.protobuf.Struct") { + return make_no_param_type( + TypeSpec(MapTypeSpec(std::make_unique(PrimitiveType::kString), + std::make_unique(DynTypeSpec())))); + } + + if (name_str == "type") { + if (params.size() > 1) { + return YamlError(yaml, node, "Type 'type' expects at most 1 parameter"); + } + return TypeSpec(read_param_or_dyn(0)); + } + if (name_str == "list") { + if (params.size() > 1) { + return YamlError(yaml, node, "Type 'list' expects at most 1 parameter"); + } + return TypeSpec(ListTypeSpec(read_param_or_dyn(0))); + } + if (name_str == "map") { + if (!params.empty() && params.size() != 2) { + return YamlError(yaml, node, "Type 'map' expects 0 or 2 parameters"); + } + 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(type_name); + if (name_str.empty()) { + return YamlError(yaml, type_name, "Node 'type_name' cannot be empty"); + } + + bool is_type_param = false; + const YAML::Node is_type_param_node = node["is_type_param"]; + if (is_type_param_node.IsDefined()) { + 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; + } + } + // What looks like a message or an abstract type with no parameters can only + // have a default value if it is an enum, in which case the default value + // is the name of the enum constant. + if (type_spec.has_message_type() || + (type_spec.has_abstract_type() && + type_spec.abstract_type().parameter_types().empty())) { + return ConstantKindCase::kString; + } + 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); + } + case ConstantKindCase::kUnspecified: + 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; + } + out << YAML::Key << std::string(key) << YAML::Value; + switch (constant.kind_case()) { + case ConstantKindCase::kUnspecified: + break; + case ConstantKindCase::kNull: + out << YAML::Null; + break; + case ConstantKindCase::kBool: + out << constant.bool_value(); + break; + case ConstantKindCase::kInt: + out << constant.int_value(); + break; + case ConstantKindCase::kUint: + out << constant.uint_value(); + break; + case ConstantKindCase::kDouble: + out << constant.double_value(); + break; + case ConstantKindCase::kBytes: { + 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 << hex_escaped; + break; + } + case ConstantKindCase::kString: + out << YAML::DoubleQuoted << constant.string_value(); + break; + case ConstantKindCase::kDuration: + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) + out << absl::FormatDuration(constant.duration_value()); + break; + case ConstantKindCase::kTimestamp: + out << absl::FormatTime( + "%Y-%m-%d%ET%H:%M:%E*SZ", + // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) + constant.timestamp_value(), absl::UTCTimeZone()); + break; + } +} + +using DecodePropertiesFn = absl::StatusOr (AspectYaml::*)( + absl::string_view yaml, const YAML::Node& node) const; + +absl::StatusOr> ParseAspects( + absl::string_view yaml, const YAML::Node& parent_node, + const AspectYamlRegistry& registry, DecodePropertiesFn decode_fn) { + absl::flat_hash_map result; + const YAML::Node aspects = parent_node["aspects"]; + if (!aspects.IsDefined()) { + return result; + } + if (!aspects.IsMap()) { + return YamlError(yaml, aspects, "Node 'aspects' is not a map"); + } + for (const auto& kv : aspects) { + if (!kv.first.IsScalar()) { + return YamlError(yaml, kv.first, "Target aspect name is not a string"); + } + std::string aspect_name = GetString(kv.first); + const AspectYaml* aspect = registry.Find(aspect_name); + if (aspect == nullptr) { + // We may want to add support for "unknown aspects" for the purposes of + // roundtripping. + continue; + } + CEL_ASSIGN_OR_RETURN(std::any props, (aspect->*decode_fn)(yaml, kv.second)); + if (props.has_value()) { + result[aspect_name] = std::move(props); + } + } + return result; +} + +using EncodePropertiesFn = absl::Status (AspectYaml::*)( + const std::any& properties, YAML::Emitter& out) const; + +absl::Status EmitAspects( + const absl::flat_hash_map& properties, + YAML::Emitter& out, const AspectYamlRegistry* absl_nullable registry, + EncodePropertiesFn encode_fn) { + if (properties.empty()) { + return absl::OkStatus(); + } + std::vector sorted_keys; + sorted_keys.reserve(properties.size()); + for (const auto& [key, props] : properties) { + if (props.has_value()) { + sorted_keys.push_back(key); + } + } + if (sorted_keys.empty()) { + return absl::OkStatus(); + } + absl::c_sort(sorted_keys); + + out << YAML::Key << "aspects" << YAML::Value << YAML::BeginMap; + for (const std::string& aspect_name : sorted_keys) { + const AspectYaml* aspect = + (registry != nullptr) ? registry->Find(aspect_name) : nullptr; + if (aspect == nullptr) { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported aspect: '", aspect_name, "'")); + } + out << YAML::Key << aspect_name << YAML::Value; + CEL_RETURN_IF_ERROR((aspect->*encode_fn)(properties.at(aspect_name), out)); + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +absl::StatusOr ParseStructTypeDef( + absl::string_view yaml, const YAML::Node& node, + const AspectYamlRegistry& registry) { + if (!node || !node.IsMap()) { + return YamlError(yaml, node, "Struct 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(name); + if (name_str.empty()) { + return YamlError(yaml, name, "Type definition 'name' cannot be empty"); + } + + 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(doc); + } + + const YAML::Node constants = node["constants"]; + if (constants.IsDefined()) { + return YamlError(yaml, GetContextNodeForKeyValue(node, constants), + "Struct type definition cannot have 'constants'"); + } + + CEL_ASSIGN_OR_RETURN( + auto struct_aspect_props, + ParseAspects(yaml, node, registry, &AspectYaml::DecodeStructProperties)); + StructTypeDef struct_def(std::move(name_str), std::move(doc_str), + std::move(struct_aspect_props)); + + const YAML::Node fields = node["fields"]; + if (!fields.IsDefined()) { + return struct_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"); + } + + 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"); + } + std::string field_name_str = GetString(field_name); + if (field_name_str.empty()) { + return YamlError(yaml, field_name, "Field name cannot be empty"); + } + + const YAML::Node type = field_node["type"]; + if (!type.IsDefined()) { + return YamlError(yaml, field_node, "Field 'type' is not specified"); + } + TypeSpec field_type; + if (type.IsScalar()) { + CEL_ASSIGN_OR_RETURN(field_type, ParseTypeSpec(GetString(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"); + } + + std::string field_doc_str; + 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_str = GetString(field_doc); + } + + Constant default_value; + 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) { + 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(default_node); + } + CEL_ASSIGN_OR_RETURN(default_value, + ParseConstantValue(yaml, default_node, + constant_kind_case, value_str)); + } + } + + CEL_ASSIGN_OR_RETURN(auto field_aspect_props, + ParseAspects(yaml, field_node, registry, + &AspectYaml::DecodeFieldProperties)); + + StructTypeDef::Field field(std::move(field_name_str), std::move(field_type), + std::move(field_doc_str), + std::move(default_value), + std::move(field_aspect_props)); + absl::Status add_status = struct_def.AddField(std::move(field)); + if (!add_status.ok()) { + return YamlError(yaml, field_name, add_status.message()); + } + } + + return struct_def; +} + +absl::StatusOr ParseEnumTypeDef( + absl::string_view yaml, const YAML::Node& node, + const AspectYamlRegistry& 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(name); + if (name_str.empty()) { + return YamlError(yaml, name, "Type definition 'name' cannot be empty"); + } + + 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(doc); + } + + const YAML::Node fields = node["fields"]; + if (fields.IsDefined()) { + return YamlError(yaml, GetContextNodeForKeyValue(node, fields), + "Enum type definition cannot have 'fields'"); + } + + CEL_ASSIGN_OR_RETURN( + auto enum_aspect_props, + ParseAspects(yaml, node, registry, &AspectYaml::DecodeEnumProperties)); + EnumTypeDef enum_def(std::move(name_str), std::move(doc_str), + std::move(enum_aspect_props)); + + 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"); + } + + 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"); + } + std::string constant_name_str = GetString(constant_name); + if (constant_name_str.empty()) { + return YamlError(yaml, constant_name, + "Enum constant name cannot be empty"); + } + + int32_t constant_id_val; + const YAML::Node constant_id = constant_node["id"]; + if (constant_id.IsDefined()) { + CEL_ASSIGN_OR_RETURN(constant_id_val, GetInt32(yaml, "id", constant_id)); + } else { + constant_id_val = static_cast(enum_def.constants().size()); + } + + std::string constant_doc_str; + 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"); + } + constant_doc_str = GetString(constant_doc); + } + + CEL_ASSIGN_OR_RETURN( + auto constant_aspect_props, + ParseAspects(yaml, constant_node, registry, + &AspectYaml::DecodeEnumConstantProperties)); + + EnumTypeDef::EnumConstant enum_constant( + std::move(constant_name_str), constant_id_val, + std::move(constant_doc_str), std::move(constant_aspect_props)); + 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 typedef_internal { + +absl::StatusOr ParseTypeDefNode(absl::string_view yaml, + const YAML::Node& node, + const AspectYamlRegistry& registry) { + if (!node || !node.IsMap()) { + return YamlError(yaml, node, "Type definition is not a map"); + } + + const YAML::Node struct_node = node["struct"]; + const YAML::Node enum_node = node["enum"]; + if (struct_node.IsDefined() && enum_node.IsDefined()) { + return YamlError(yaml, GetContextNodeForKeyValue(node, enum_node), + "Node 'struct' 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(kv.first); + if (kind != "struct" && kind != "enum") { + return YamlError( + yaml, kv.first, + absl::StrCat("Unsupported type definition kind: '", kind, "'")); + } + } + + if (struct_node.IsDefined()) { + CEL_ASSIGN_OR_RETURN(StructTypeDef struct_def, + ParseStructTypeDef(yaml, struct_node, registry)); + return TypeDef(std::move(struct_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 'struct' or 'enum'"); +} + +absl::Status EmitTypeDefNode(const TypeDef& type_def, YAML::Emitter& out, + const TypeDefToYamlOptions& options) { + out << YAML::BeginMap; + if (type_def.is_struct()) { + const StructTypeDef& struct_def = type_def.struct_type(); + out << YAML::Key << "struct" << YAML::Value << YAML::BeginMap; + if (!struct_def.name().empty()) { + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << std::string(struct_def.name()); + } + if (!struct_def.doc().empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << std::string(struct_def.doc()); + } + CEL_RETURN_IF_ERROR(EmitAspects(struct_def.aspects(), out, options.registry, + &AspectYaml::EncodeStructProperties)); + if (!struct_def.fields().empty()) { + out << YAML::Key << "fields" << YAML::Value << YAML::BeginSeq; + for (const StructTypeDef::Field& field : struct_def.fields()) { + out << YAML::BeginMap; + out << YAML::Key << "name" << YAML::Value << YAML::DoubleQuoted + << std::string(field.name()); + EmitTypeSpec(field.type(), out, options); + if (!field.doc().empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << std::string(field.doc()); + } + EmitConstantValue("default", field.default_value(), out); + CEL_RETURN_IF_ERROR(EmitAspects(field.aspects(), out, options.registry, + &AspectYaml::EncodeFieldProperties)); + 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 + << std::string(enum_def.name()); + } + if (!enum_def.doc().empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << std::string(enum_def.doc()); + } + CEL_RETURN_IF_ERROR(EmitAspects(enum_def.aspects(), out, options.registry, + &AspectYaml::EncodeEnumProperties)); + 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 + << std::string(constant.name()); + out << YAML::Key << "id" << YAML::Value << constant.id(); + if (!constant.doc().empty()) { + out << YAML::Key << "doc" << YAML::Value << YAML::DoubleQuoted + << std::string(constant.doc()); + } + CEL_RETURN_IF_ERROR( + EmitAspects(constant.aspects(), out, options.registry, + &AspectYaml::EncodeEnumConstantProperties)); + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + out << YAML::EndMap; + } + out << YAML::EndMap; + return absl::OkStatus(); +} + +} // namespace typedef_internal + +absl::StatusOr TypeDefFromYaml(absl::string_view yaml, + const AspectYamlRegistry& registry) { + try { + 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 typedef_internal::ParseTypeDefNode(yaml, root, registry); + } catch (const YAML::Exception& e) { + return absl::InvalidArgumentError( + FormatYamlErrorMessage(yaml, e.msg, e.mark)); + } catch (const std::exception& e) { + return absl::InternalError(e.what()); + } catch (...) { + return absl::InternalError("Unknown exception occurred while parsing YAML"); + } +} + +absl::Status TypeDefToYaml(const TypeDef& type_def, std::ostream& os, + const TypeDefToYamlOptions& options) { + try { + YAML::Emitter out(os); + out.SetIndent(2); + CEL_RETURN_IF_ERROR( + typedef_internal::EmitTypeDefNode(type_def, out, options)); + if (!out.good()) { + return absl::InternalError(out.GetLastError()); + } + return absl::OkStatus(); + } catch (const YAML::Exception& e) { + return absl::InternalError(e.msg); + } catch (const std::exception& e) { + return absl::InternalError(e.what()); + } catch (...) { + return absl::InternalError( + "Unknown exception occurred while emitting YAML"); + } +} + +} // namespace cel diff --git a/common/typedef/typedef_yaml.h b/common/typedef/typedef_yaml.h new file mode 100644 index 000000000..ae71a5804 --- /dev/null +++ b/common/typedef/typedef_yaml.h @@ -0,0 +1,66 @@ +// 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 "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "common/typedef/aspect_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; + // Optional registry of AspectYaml codecs used to encode aspect + // properties under `aspects`. + const AspectYamlRegistry* absl_nullable registry = nullptr; +}; + +// TypeDefFromYaml creates a TypeDef from a YAML mapping string. +// +// Aspects under `aspects` are decoded by +// delegating to the corresponding `AspectYaml` looked up in `registry`. +absl::StatusOr TypeDefFromYaml( + absl::string_view yaml, const AspectYamlRegistry& registry = {}); + +// TypeDefToYaml serializes a single TypeDef as a YAML mapping to `os`. +// +// Aspects under `aspects` are encoded by +// delegating to the corresponding `AspectYaml` looked up in `options.registry`. +absl::Status TypeDefToYaml(const TypeDef& type_def, std::ostream& os, + const TypeDefToYamlOptions& options = {}); + +namespace typedef_internal { + +absl::StatusOr ParseTypeDefNode( + absl::string_view yaml, const YAML::Node& node, + const AspectYamlRegistry& registry = {}); + +absl::Status EmitTypeDefNode(const TypeDef& type_def, YAML::Emitter& out, + const TypeDefToYamlOptions& options = {}); + +} // namespace typedef_internal + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_COMMON_TYPEDEF_TYPEDEF_YAML_H_ diff --git a/common/typedef/typedef_yaml_test.cc b/common/typedef/typedef_yaml_test.cc new file mode 100644 index 000000000..24bdeecea --- /dev/null +++ b/common/typedef/typedef_yaml_test.cc @@ -0,0 +1,573 @@ +// 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 + +#include "absl/log/absl_log.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "common/typedef/aspect_yaml.h" +#include "common/typedef/typedef.h" +#include "common/typedef/typedef.pb.h" +#include "common/typedef/typedef_proto.h" +#include "common/typedef/yaml_helpers.h" +#include "common/typedef/yaml_test_helpers.h" +#include "internal/proto_matchers.h" +#include "internal/runfiles.h" +#include "internal/status_macros.h" +#include "internal/testing.h" +#include "google/protobuf/text_format.h" +#include "yaml-cpp/emitter.h" +#include "yaml-cpp/emittermanip.h" +#include "yaml-cpp/exceptions.h" +#include "yaml-cpp/mark.h" +#include "yaml-cpp/node/emit.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 ::cel::internal::test::EqualsProto; +using ::testing::HasSubstr; + +constexpr absl::string_view kTestTypeDefFilePath = +"_main/common/typedef/testdata/"; + +constexpr absl::string_view kBaselineSeparator = + "--------------------------------------------------------------------\n"; + +struct TestPropsData { + std::optional name; + std::optional id; +}; + +absl::StatusOr DecodeTestProps(absl::string_view aspect_name, + absl::string_view context, + absl::string_view yaml, + const YAML::Node& node) { + if (!node || !node.IsMap()) { + return typedef_internal::YamlError(yaml, node, + "Properties node is not a map"); + } + TestPropsData data; + for (const auto& kv : node) { + std::string key = typedef_internal::GetString(kv.first); + if (key == "name") { + data.name = typedef_internal::GetString(kv.second); + } else if (key == "id") { + CEL_ASSIGN_OR_RETURN(data.id, + typedef_internal::GetInt32(yaml, "id", kv.second)); + } else { + return typedef_internal::YamlError( + yaml, kv.first, + absl::StrCat("Unsupported '", aspect_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 TestStructProps final { + public: + explicit TestStructProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestFieldProps final { + public: + explicit TestFieldProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestEnumProps final { + public: + explicit TestEnumProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestEnumConstantProps final { + public: + explicit TestEnumConstantProps(TestPropsData d) : data_(std::move(d)) {} + const TestPropsData& data() const { return data_; } + + private: + TestPropsData data_; +}; + +class TestAspect final : public AspectYaml { + public: + explicit TestAspect(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + absl::StatusOr DecodeStructProperties( + absl::string_view yaml, const YAML::Node& node) const override { + CEL_ASSIGN_OR_RETURN(TestPropsData d, + DecodeTestProps(name_, "struct", yaml, node)); + return std::make_any(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_any(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_any(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_any(std::move(d)); + } + + absl::Status EncodeStructProperties(const std::any& properties, + YAML::Emitter& out) const override { + EncodeTestProps(std::any_cast(properties).data(), + out); + return absl::OkStatus(); + } + + absl::Status EncodeFieldProperties(const std::any& properties, + YAML::Emitter& out) const override { + EncodeTestProps(std::any_cast(properties).data(), + out); + return absl::OkStatus(); + } + + absl::Status EncodeEnumProperties(const std::any& properties, + YAML::Emitter& out) const override { + EncodeTestProps(std::any_cast(properties).data(), + out); + return absl::OkStatus(); + } + + absl::Status EncodeEnumConstantProperties(const std::any& properties, + YAML::Emitter& out) const override { + EncodeTestProps( + std::any_cast(properties).data(), out); + return absl::OkStatus(); + } + + private: + std::string name_; +}; + +class StubAspect final : public AspectYaml { + public: + explicit StubAspect(absl::string_view name) : name_(name) {} + + absl::string_view name() const override { return name_; } + + private: + std::string name_; +}; + +AspectYamlRegistry MakeTestRegistry() { + return AspectYamlRegistry(std::make_unique("aspect_a"), + std::make_unique("aspect_b"), + std::make_unique("stub_aspect")); +} + +TEST(TypeDefYamlTest, CelTypesFromYaml) { + std::string contents; + std::string test_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, "cel_types.yaml")); + ASSERT_THAT(cel::internal::GetFileContents(test_file, &contents), IsOk()); + + std::string baseline; + std::string baseline_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, "cel_types_from_yaml.baseline")); + ASSERT_THAT(cel::internal::GetFileContents(baseline_file, &baseline), IsOk()); + baseline = absl::StripAsciiWhitespace(baseline); + + AspectYamlRegistry registry = MakeTestRegistry(); + ASSERT_OK_AND_ASSIGN(YAML::Node root, typedef_internal::LoadYaml(contents)); + ASSERT_TRUE(root.IsSequence()); + + std::vector type_defs; + for (const YAML::Node& item : root) { + ASSERT_OK_AND_ASSIGN(TypeDef type_def, typedef_internal::ParseTypeDefNode( + contents, item, registry)); + type_defs.push_back(std::move(type_def)); + } + + ASSERT_OK_AND_ASSIGN(types::TypeDefSet proto_set, TypeDefsToProto(type_defs)); + std::string textproto; + ASSERT_TRUE(google::protobuf::TextFormat::PrintToString(proto_set, &textproto)); + + std::ostringstream out; + out << "YAML SOURCE: cel_types.yaml\n"; + out << kBaselineSeparator; + out << "TYPEDEF PROTO:\n"; + out << textproto; + + std::string actual(absl::StripAsciiWhitespace(out.str())); + if (actual != baseline) { + ABSL_LOG(INFO) << "Actual:\n" << actual; + EXPECT_EQ(actual, baseline); + } +} + +TEST(TypeDefYamlTest, CelTypeErrors) { + std::string contents; + std::string test_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, "cel_type_errors.yaml")); + ASSERT_THAT(cel::internal::GetFileContents(test_file, &contents), IsOk()); + + std::string baseline; + std::string baseline_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, "cel_type_errors.baseline")); + ASSERT_THAT(cel::internal::GetFileContents(baseline_file, &baseline), IsOk()); + baseline = absl::StripAsciiWhitespace(baseline); + + AspectYamlRegistry registry = MakeTestRegistry(); + ASSERT_OK_AND_ASSIGN(YAML::Node root, typedef_internal::LoadYaml(contents)); + ASSERT_TRUE(root.IsSequence()); + + std::ostringstream out; + out << "YAML SOURCE: cel_type_errors.yaml\n"; + for (const YAML::Node& item : root) { + out << kBaselineSeparator; + for (absl::string_view line : absl::StrSplit(YAML::Dump(item), '\n')) { + out << "# " << line << "\n"; + } + out << "\n"; + absl::StatusOr type_def = + typedef_internal::ParseTypeDefNode(contents, item, registry); + ASSERT_THAT(type_def, StatusIs(absl::StatusCode::kInvalidArgument)); + out << type_def.status().message() << "\n"; + } + + std::string actual(absl::StripAsciiWhitespace(out.str())); + if (actual != baseline) { + ABSL_LOG(INFO) << "Actual:\n" << actual; + EXPECT_EQ(actual, baseline); + } +} + +TEST(TypeDefYamlTest, CelTypesToYaml) { + std::string contents; + std::string test_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, "cel_types.txtproto")); + ASSERT_THAT(cel::internal::GetFileContents(test_file, &contents), IsOk()); + + std::string baseline; + std::string baseline_file = cel::internal::ResolveRunfilesPath( + absl::StrCat(kTestTypeDefFilePath, "cel_types_to_yaml.baseline")); + ASSERT_THAT(cel::internal::GetFileContents(baseline_file, &baseline), IsOk()); + baseline = absl::StripAsciiWhitespace(baseline); + + types::TypeDefSet proto_set; + ASSERT_TRUE(google::protobuf::TextFormat::ParseFromString(contents, &proto_set)); + ASSERT_OK_AND_ASSIGN(std::vector type_defs, + TypeDefsFromProto(proto_set)); + + std::ostringstream out; + out << "PROTO SOURCE: cel_types.txtproto\n"; + for (const TypeDef& type_def : type_defs) { + out << kBaselineSeparator; + std::ostringstream concise; + ASSERT_THAT(TypeDefToYaml(type_def, concise, {.use_type_signatures = true}), + IsOk()); + out << concise.str() << "\n"; + + std::ostringstream verbose; + ASSERT_THAT( + TypeDefToYaml(type_def, verbose, {.use_type_signatures = false}), + IsOk()); + if (verbose.str() != concise.str()) { + out << "# use_type_signatures = false\n"; + out << verbose.str() << "\n"; + } + } + + std::string actual(absl::StripAsciiWhitespace(out.str())); + if (actual != baseline) { + ABSL_LOG(INFO) << "Actual:\n" << actual; + EXPECT_EQ(actual, baseline); + } +} + +TEST(TypeDefYamlTest, TypeDefFromYamlTopLevel) { + AspectYamlRegistry registry = MakeTestRegistry(); + + ASSERT_OK_AND_ASSIGN(TypeDef empty_def, TypeDefFromYaml("", registry)); + EXPECT_THAT(TypeDefToProto(empty_def), + IsOkAndHolds(EqualsProto("struct_type {}"))); + + ASSERT_OK_AND_ASSIGN( + TypeDef struct_def, + TypeDefFromYaml("struct:\n name: \"NoFields\"", registry)); + EXPECT_THAT(TypeDefToProto(struct_def), + IsOkAndHolds(EqualsProto("struct_type { name: \"NoFields\" }"))); + + EXPECT_THAT(TypeDefFromYaml("[1]", registry), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Invalid CEL type definition YAML"))); +} + +TEST(TypeDefYamlTest, EncodeErrorOnUnregisteredAspect) { + AspectYamlRegistry registry = MakeTestRegistry(); + ASSERT_OK_AND_ASSIGN(TypeDef type_def, TypeDefFromYaml(R"yaml( + struct: + name: "Item" + aspects: + aspect_a: + name: "Item" + fields: + - name: "f" + type: "int" + aspects: + aspect_a: + name: "f" + )yaml", + registry)); + + AspectYamlRegistry empty_registry; + std::stringstream ss; + EXPECT_THAT( + TypeDefToYaml(type_def, ss, + {.use_type_signatures = true, .registry = &empty_registry}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported aspect: 'aspect_a'"))); + + ASSERT_OK_AND_ASSIGN(TypeDef enum_def, TypeDefFromYaml(R"yaml( + enum: + name: "Status" + constants: + - name: "A" + aspects: + aspect_a: + name: "A" + )yaml", + registry)); + std::stringstream ss_enum; + EXPECT_THAT( + TypeDefToYaml(enum_def, ss_enum, + {.use_type_signatures = true, .registry = &empty_registry}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unsupported aspect: 'aspect_a'"))); +} + +TEST(TypeDefYamlTest, YamlHelpersCoverage) { + EXPECT_EQ(typedef_internal::FormatYamlErrorMessage("yaml", "error", + YAML::Mark::null_mark()), + "error"); + + YAML::Mark mark; + mark.pos = 7; + mark.line = 0; + mark.column = 7; + EXPECT_THAT( + typedef_internal::FormatYamlErrorMessage("single line", "err", mark), + HasSubstr("single line\n| ^")); + EXPECT_EQ(typedef_internal::FormatYamlErrorMessage("", "err", mark), "err"); + + EXPECT_THAT(typedef_internal::LoadYaml("{ invalid: [ }"), + StatusIs(absl::StatusCode::kInvalidArgument)); + + YAML::Node seq = YAML::Load("[1, 2]"); + EXPECT_EQ(typedef_internal::GetString(seq), ""); + + YAML::Node str_node = YAML::Load("hello"); + EXPECT_THAT(typedef_internal::GetBinary("hello", str_node), IsOkAndHolds("")); + YAML::Node bad_b64 = YAML::Load("!!binary '%%%'"); + EXPECT_THAT(typedef_internal::GetBinary("!!binary '%%%'", bad_b64), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("not a valid Base64 encoded binary"))); + + EXPECT_THAT(typedef_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(typedef_internal::GetBool("not_a_bool", "k", not_bool), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'k' is not a boolean"))); + + EXPECT_THAT(typedef_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(typedef_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 = + typedef_internal::GetContextNodeForKeyValue(map_node, missing_node); + EXPECT_TRUE(ctx.is(missing_node)); + + EXPECT_TRUE(typedef_internal::IsEmptyYamlNode(YAML::Load("\"\""))); + EXPECT_FALSE(typedef_internal::IsEmptyYamlNode(YAML::Load("'a'"))); + EXPECT_EQ(Unindent(" a\nb"), "a\nb"); + + EXPECT_THAT(typedef_internal::ParseTypeDefNode("1", YAML::Load("1")), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Type definition is not a map"))); +} + +enum class ThrowKind { kYaml, kStd, kInt, kBadEmitter }; + +class ThrowingAspect final : public AspectYaml { + public: + ThrowingAspect(absl::string_view name, ThrowKind kind) + : name_(name), kind_(kind) {} + + absl::string_view name() const override { return name_; } + + absl::StatusOr DecodeStructProperties( + absl::string_view, const YAML::Node&) const override { + if (kind_ == ThrowKind::kYaml) { + throw YAML::Exception(YAML::Mark::null_mark(), "yaml err"); + } + if (kind_ == ThrowKind::kStd) { + throw std::runtime_error("std err"); + } + throw 42; + } + + absl::Status EncodeStructProperties(const std::any&, + YAML::Emitter& out) const override { + if (kind_ == ThrowKind::kYaml) { + throw YAML::Exception(YAML::Mark::null_mark(), "yaml err"); + } + if (kind_ == ThrowKind::kBadEmitter) { + out << YAML::EndMap; + return absl::OkStatus(); + } + throw 42; + } + + private: + std::string name_; + ThrowKind kind_; +}; + +TEST(TypeDefYamlTest, EncodePropertiesErrorsAndExceptionHandling) { + AspectYamlRegistry registry = MakeTestRegistry(); + + // Empty std::any in aspects is skipped without emitting. + StructTypeDef empty_any_struct("Item", "", {{"aspect_a", std::any()}}); + std::stringstream empty_any_ss; + EXPECT_THAT( + TypeDefToYaml(TypeDef(empty_any_struct), empty_any_ss, + {.use_type_signatures = true, .registry = ®istry}), + IsOk()); + + // StubAspect returns UnimplementedError on Encode*Properties. + StructTypeDef unimplemented_struct( + "Item", "", {{"stub_aspect", std::make_any(123)}}); + std::stringstream ss1; + EXPECT_THAT( + TypeDefToYaml(TypeDef(unimplemented_struct), ss1, + {.use_type_signatures = true, .registry = ®istry}), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("Custom struct properties are not supported " + "for aspect 'stub_aspect'"))); + + // TestAspect uses std::any_cast, which throws + // std::bad_any_cast (a std::exception) if given the wrong std::any payload. + StructTypeDef wrong_any_struct("Item", "", + {{"aspect_a", std::make_any(123)}}); + std::stringstream ss3; + EXPECT_THAT( + TypeDefToYaml(TypeDef(wrong_any_struct), ss3, + {.use_type_signatures = true, .registry = ®istry}), + StatusIs(absl::StatusCode::kInternal)); + + AspectYamlRegistry throw_reg( + std::make_unique("yaml_ex", ThrowKind::kYaml), + std::make_unique("std_ex", ThrowKind::kStd), + std::make_unique("int_ex", ThrowKind::kInt), + std::make_unique("bad_emit", ThrowKind::kBadEmitter)); + + EXPECT_THAT( + TypeDefFromYaml("struct:\n name: 'A'\n aspects:\n yaml_ex: {}", + throw_reg), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("yaml err"))); + EXPECT_THAT( + TypeDefFromYaml("struct:\n name: 'A'\n aspects:\n std_ex: {}", + throw_reg), + StatusIs(absl::StatusCode::kInternal, HasSubstr("std err"))); + EXPECT_THAT( + TypeDefFromYaml("struct:\n name: 'A'\n aspects:\n int_ex: {}", + throw_reg), + StatusIs(absl::StatusCode::kInternal, HasSubstr("Unknown exception"))); + + for (auto [aspect_name, substr] : + std::vector>{ + {"yaml_ex", "yaml err"}, + {"int_ex", "Unknown exception"}, + {"bad_emit", ""}}) { + StructTypeDef s("Item", "", {{aspect_name, std::make_any(1)}}); + std::stringstream out1; + EXPECT_THAT( + TypeDefToYaml(TypeDef(s), out1, + {.use_type_signatures = true, .registry = &throw_reg}), + StatusIs(absl::StatusCode::kInternal, HasSubstr(substr))); + } +} + +} // namespace +} // namespace cel diff --git a/common/typedef/yaml_helpers.cc b/common/typedef/yaml_helpers.cc new file mode 100644 index 000000000..670434641 --- /dev/null +++ b/common/typedef/yaml_helpers.cc @@ -0,0 +1,154 @@ +// 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::typedef_internal { + +std::string FormatYamlErrorMessage(absl::string_view yaml, + absl::string_view error, + const YAML::Mark& mark) { + if (mark.is_null() || mark.line < 0 || mark.column < 0 || + mark.pos < mark.column || static_cast(mark.pos) > yaml.size()) { + return std::string(error); + } + std::string message; + absl::StrAppend(&message, mark.line + 1, ":", mark.column + 1, ": ", error, + "\n|"); + size_t start = static_cast(mark.pos - mark.column); + size_t end = yaml.find('\n', static_cast(mark.pos)); + if (end == absl::string_view::npos) { + end = yaml.size(); + } + + absl::StrAppend(&message, yaml.substr(start, end - start), "\n|", + std::string(static_cast(mark.column), ' '), "^"); + + return message; +} + +absl::StatusOr LoadYaml(absl::string_view yaml) { + try { + return YAML::Load(std::string(yaml)); + } catch (const YAML::Exception& 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(const YAML::Node& node) { + if (!node.IsDefined() || !node.IsScalar()) { + return ""; + } + try { + return node.Scalar(); + } catch (const 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(node), &binary)) { + return binary; + } else { + return YamlError(yaml, node, + absl::StrCat("Node '", GetString(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 (const 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(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::typedef_internal diff --git a/common/typedef/yaml_helpers.h b/common/typedef/yaml_helpers.h new file mode 100644 index 000000000..a45299a29 --- /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::typedef_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(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::typedef_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_