diff --git a/common/typedef/BUILD b/common/typedef/BUILD new file mode 100644 index 000000000..cd38c5677 --- /dev/null +++ b/common/typedef/BUILD @@ -0,0 +1,51 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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", + ], +) 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..2833e7aa2 --- /dev/null +++ b/common/typedef/typedef.h @@ -0,0 +1,212 @@ +// 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 + schema_specific_properties = {}) + : name_(std::move(name)), + type_(std::move(type)), + doc_(std::move(doc)), + default_value_(std::move(default_value)), + schema_specific_properties_(std::move(schema_specific_properties)) {} + + 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_; + } + // Target schema-specific properties keyed by target schema name. + const absl::flat_hash_map& + schema_specific_properties() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return schema_specific_properties_; + } + + private: + std::string name_; + TypeSpec type_; + std::string doc_; + Constant default_value_; + absl::flat_hash_map schema_specific_properties_; + }; + + StructTypeDef() = default; + explicit StructTypeDef(std::string name, std::string doc = "", + absl::flat_hash_map + schema_specific_properties = {}) + : name_(std::move(name)), + doc_(std::move(doc)), + schema_specific_properties_(std::move(schema_specific_properties)) {} + + 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_; + } + // Target schema-specific properties keyed by target schema name. + const absl::flat_hash_map& schema_specific_properties() + const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return schema_specific_properties_; + } + + // 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 schema_specific_properties_; +}; + +// 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 + schema_specific_properties = {}) + : name_(std::move(name)), + id_(id), + doc_(std::move(doc)), + schema_specific_properties_(std::move(schema_specific_properties)) {} + + 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_; } + // Target schema-specific properties keyed by target schema name. + const absl::flat_hash_map& + schema_specific_properties() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return schema_specific_properties_; + } + + private: + std::string name_; + int32_t id_ = 0; + std::string doc_; + absl::flat_hash_map schema_specific_properties_; + }; + + EnumTypeDef() = default; + explicit EnumTypeDef(std::string name, std::string doc = "", + absl::flat_hash_map + schema_specific_properties = {}) + : name_(std::move(name)), + doc_(std::move(doc)), + schema_specific_properties_(std::move(schema_specific_properties)) {} + + 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_; + } + // Target schema-specific properties keyed by target schema name. + const absl::flat_hash_map& schema_specific_properties() + const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return schema_specific_properties_; + } + + // 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 schema_specific_properties_; +}; + +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_test.cc b/common/typedef/typedef_test.cc new file mode 100644 index 000000000..51bac3670 --- /dev/null +++ b/common/typedef/typedef_test.cc @@ -0,0 +1,230 @@ +// 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().schema_specific_properties(), 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->schema_specific_properties(), 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->schema_specific_properties().size(), 2); + ASSERT_TRUE(name_field->schema_specific_properties().contains("json")); + EXPECT_EQ(std::any_cast( + name_field->schema_specific_properties().at("json")) + .json_name, + "displayName"); + ASSERT_TRUE(name_field->schema_specific_properties().contains("proto3")); + EXPECT_EQ(std::any_cast( + name_field->schema_specific_properties().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.schema_specific_properties().size(), 1); + ASSERT_TRUE(person.schema_specific_properties().contains("json")); + EXPECT_EQ(std::any_cast( + person.schema_specific_properties().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().schema_specific_properties().size(), 1); + EXPECT_TRUE( + type_def.struct_type().schema_specific_properties().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->schema_specific_properties().contains("json")); + EXPECT_EQ(std::any_cast( + shipped->schema_specific_properties().at("json")) + .custom_name, + "shipped"); + + EnumTypeDef empty_enum; + EXPECT_THAT(empty_enum.FindConstant("ORDER_STATUS_UNSPECIFIED"), IsNull()); + + EXPECT_EQ(status_enum.schema_specific_properties().size(), 1); + ASSERT_TRUE(status_enum.schema_specific_properties().contains("json")); + EXPECT_TRUE(std::any_cast( + status_enum.schema_specific_properties().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->schema_specific_properties(), 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().schema_specific_properties().size(), 1); + EXPECT_TRUE( + type_def.enum_type().schema_specific_properties().contains("json")); +} + +TEST(TypeDefTest, DefaultEnumTypeDef) { + TypeDef def((EnumTypeDef())); + EXPECT_FALSE(def.is_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().schema_specific_properties(), IsEmpty()); + EXPECT_TRUE(def.enum_type().constants().empty()); + EXPECT_THAT(def.enum_type().FindConstant("ANY"), IsNull()); +} + +} // namespace +} // namespace cel