From e9abf6ae4b024afb2a8ac02079cef379eed347f6 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Wed, 7 Oct 2026 17:09:30 -0700 Subject: [PATCH] [cel-types] Add Protobuf definitions and codecs for CEL type definitions and aspects. Define `CelTypeDefSet`, `CelTypeDef`, `CelTypeDefObject`, and `CelTypeDefEnum` messages with extension ranges for aspects, along with proto extension aspects for Protocol Buffers (`proto2` and `proto3`). Provide `AspectProto`, `AspectProtoRegistry`, and C++ encoder/decoder APIs (`TypeDefsToProto`, `TypeDefsFromProto`, and element-level overloads) for converting between `cel::TypeDef` types and their Protobuf representations. PiperOrigin-RevId: 995451531 --- common/typedef/BUILD | 196 +++++++ common/typedef/aspect_proto.cc | 95 ++++ common/typedef/aspect_proto.h | 125 +++++ common/typedef/aspect_proto_test.cc | 126 +++++ 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 | 17 + 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 ++++++++ 18 files changed, 3055 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/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/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 diff --git a/common/typedef/BUILD b/common/typedef/BUILD new file mode 100644 index 000000000..c4b7e0008 --- /dev/null +++ b/common/typedef/BUILD @@ -0,0 +1,196 @@ +# 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", + ], +) 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/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..6ed37d282 --- /dev/null +++ b/common/typedef/testdata/BUILD @@ -0,0 +1,17 @@ +package( + default_testonly = True, + default_visibility = ["//visibility:public"], +) + +filegroup( + name = "typedef_testdata", + srcs = glob([ + "*.txtproto", + ]), +) + +exports_files( + srcs = glob([ + "*.txtproto", + ]), +) 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