From b81a5f44184298a0b9d01a53f98288d34b2d980f Mon Sep 17 00:00:00 2001 From: Justin King Date: Thu, 24 Sep 2026 10:07:16 -0700 Subject: [PATCH] Introduce `AttributeMatcher` PiperOrigin-RevId: 987583628 --- base/BUILD | 32 ++ base/attribute.cc | 51 +++ base/attribute.h | 31 +- base/attribute_matcher.cc | 688 +++++++++++++++++++++++++++++++++ base/attribute_matcher.h | 480 +++++++++++++++++++++++ base/attribute_matcher_test.cc | 462 ++++++++++++++++++++++ base/attribute_test.cc | 19 + 7 files changed, 1761 insertions(+), 2 deletions(-) create mode 100644 base/attribute_matcher.cc create mode 100644 base/attribute_matcher.h create mode 100644 base/attribute_matcher_test.cc diff --git a/base/BUILD b/base/BUILD index 739fc96de..97994353b 100644 --- a/base/BUILD +++ b/base/BUILD @@ -34,6 +34,8 @@ cc_library( deps = [ ":kind", "//internal:status_macros", + "//internal:strings", + "@com_google_absl//absl/algorithm:container", "@com_google_absl//absl/base:core_headers", "@com_google_absl//absl/base:no_destructor", "@com_google_absl//absl/base:nullability", @@ -59,6 +61,36 @@ cc_test( ], ) +cc_library( + name = "attribute_matcher", + srcs = ["attribute_matcher.cc"], + hdrs = ["attribute_matcher.h"], + deps = [ + ":attributes", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/base:no_destructor", + "@com_google_absl//absl/functional:function_ref", + "@com_google_absl//absl/log:absl_check", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/types:source_location", + "@com_google_absl//absl/types:span", + ], +) + +cc_test( + name = "attribute_matcher_test", + srcs = ["attribute_matcher_test.cc"], + deps = [ + ":attribute_matcher", + ":attributes", + "//internal:testing", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:status_matchers", + ], +) + cc_library( name = "kind", hdrs = ["kind.h"], diff --git a/base/attribute.cc b/base/attribute.cc index 97fa75fa7..768e7c4f4 100644 --- a/base/attribute.cc +++ b/base/attribute.cc @@ -20,6 +20,7 @@ #include #include +#include "absl/algorithm/container.h" #include "absl/base/macros.h" #include "absl/base/nullability.h" #include "absl/log/absl_check.h" @@ -30,6 +31,7 @@ #include "absl/strings/string_view.h" #include "base/kind.h" #include "internal/status_macros.h" +#include "internal/strings.h" namespace cel { @@ -119,6 +121,26 @@ struct AttributeQualifierTypeVisitor final { Kind operator()(bool) const { return Kind::kBool; } }; +struct AttributeQualifierToString { + void operator()(std::monostate) const {} + + void operator()(bool value) const { output.append(value ? "true" : "false"); } + + void operator()(int64_t value) const { absl::StrAppend(&output, value); } + + void operator()(uint64_t value) const { + absl::StrAppend(&output, value, "u"); + } + + void operator()(absl::string_view value) const { + absl::StrAppend(&output, internal::FormatStringLiteral(value)); + } + + void operator()(common_internal::WildcardType) const { output.append("*"); } + + std::string& output; +}; + } // namespace Kind AttributeQualifier::kind() const { @@ -193,6 +215,30 @@ absl::StatusOr Attribute::AsString() const { return result; } +std::string Attribute::ToString() const { + std::string result; + result.append(variable_name()); + for (const auto& qualifier : qualifier_path()) { + result.push_back('['); + std::visit(AttributeQualifierToString{result}, + common_internal::AsVariant(qualifier)); + result.push_back(']'); + } + return result; +} + +std::string AttributePattern::ToString() const { + std::string result; + result.append(variable()); + for (const auto& qualifier : qualifier_path()) { + result.push_back('['); + std::visit(AttributeQualifierToString{result}, + common_internal::AsVariant(qualifier)); + result.push_back(']'); + } + return result; +} + std::string AttributeQualifier::ToString() const { std::string result; absl::Status status = @@ -517,4 +563,9 @@ bool operator<(const AttributeQualifierPattern& lhs, common_internal::AsVariant(rhs)); } +bool operator==(const AttributePattern& lhs, const AttributePattern& rhs) { + return lhs.variable() == rhs.variable() && + absl::c_equal(lhs.qualifier_path(), rhs.qualifier_path()); +} + } // namespace cel diff --git a/base/attribute.h b/base/attribute.h index a0877fc96..1febe9a27 100644 --- a/base/attribute.h +++ b/base/attribute.h @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -840,6 +841,9 @@ class Attribute { absl::StatusOr AsString() const; + [[nodiscard]] + std::string ToString() const; + private: struct Impl final { Impl(std::string variable_name, @@ -854,6 +858,11 @@ class Attribute { std::shared_ptr impl_; }; +template +void AbslStringify(S& sink, const Attribute& attribute) { + sink.Append(attribute.ToString()); +} + // AttributePattern is a fully-qualified absolute attribute path pattern. // Supported segments steps in the path are: // - field selection; @@ -868,8 +877,9 @@ class AttributePattern { FULL // Pattern matches an attribute itself. }; - AttributePattern(std::string variable, - std::vector qualifier_path) + explicit AttributePattern( + std::string variable, + std::vector qualifier_path = {}) : variable_(std::move(variable)), qualifier_path_(std::move(qualifier_path)) {} @@ -902,11 +912,28 @@ class AttributePattern { return result; } + [[nodiscard]] + std::string ToString() const; + private: std::string variable_; std::vector qualifier_path_; }; +template +void AbslStringify(S& sink, const AttributePattern& pattern) { + sink.Append(pattern.ToString()); +} + +[[nodiscard]] +bool operator==(const AttributePattern& lhs, const AttributePattern& rhs); + +[[nodiscard]] +inline bool operator!=(const AttributePattern& lhs, + const AttributePattern& rhs) { + return !operator==(lhs, rhs); +} + struct FieldSpecifier { int64_t number; std::string name; diff --git a/base/attribute_matcher.cc b/base/attribute_matcher.cc new file mode 100644 index 000000000..3820c9dde --- /dev/null +++ b/base/attribute_matcher.cc @@ -0,0 +1,688 @@ +// 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 "base/attribute_matcher.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/no_destructor.h" +#include "absl/functional/function_ref.h" +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/source_location.h" +#include "absl/types/span.h" +#include "base/attribute.h" + +namespace cel { + +namespace common_internal { + +std::pair AttributeMatcherNode::AddMissing( + const AttributeQualifier& pattern, bool prev_set) { + ABSL_DCHECK(IsMissing()); + const bool set_missing_children = SetMissingChildren(); + if (set_missing_children && !prev_set) { + ClearMissingChildren(); + return {nullptr, false}; + } + if (!set_missing_children && wildcard_ != nullptr && wildcard_->IsMissing()) { + return {nullptr, false}; + } + if (children_ == nullptr) { + children_ = std::make_unique>(); + } + AttributeMatcherNode* child = const_cast( + &*children_->emplace(this, pattern).first); + return {child, child->SetMissing()}; +} + +std::pair AttributeMatcherNode::AddMissing( + const AttributeQualifierPattern& pattern, bool prev_set) { + if (auto qualifier = pattern.ToQualifier(); qualifier.has_value()) { + return AddMissing(*qualifier, prev_set); + } + ABSL_DCHECK(IsMissing()); + const bool set_missing_children = SetMissingChildren(); + if (set_missing_children && !prev_set) { + ClearMissingChildren(); + return {nullptr, false}; + } + if (!set_missing_children) { + return {nullptr, false}; + } + if (wildcard_ == nullptr) { + wildcard_ = std::make_unique( + this, AttributeQualifierPattern::Wildcard()); + } + return {wildcard_.get(), wildcard_->SetMissing()}; +} + +std::pair AttributeMatcherNode::AddUnknown( + const AttributeQualifier& pattern, bool prev_set) { + ABSL_DCHECK(IsUnknown()); + const bool set_unknown_children = SetUnknownChildren(); + if (set_unknown_children && !prev_set) { + ClearUnknownChildren(); + return {nullptr, false}; + } + if (!set_unknown_children && wildcard_ != nullptr && wildcard_->IsUnknown()) { + return {nullptr, false}; + } + if (children_ == nullptr) { + children_ = std::make_unique>(); + } + AttributeMatcherNode* child = const_cast( + &*children_->emplace(this, pattern).first); + return {child, child->SetUnknown()}; +} + +std::pair AttributeMatcherNode::AddUnknown( + const AttributeQualifierPattern& pattern, bool prev_set) { + if (auto qualifier = pattern.ToQualifier(); qualifier.has_value()) { + return AddUnknown(*qualifier, prev_set); + } + ABSL_DCHECK(IsUnknown()); + const bool set_unknown_children = SetUnknownChildren(); + if (set_unknown_children && !prev_set) { + ClearUnknownChildren(); + return {nullptr, false}; + } + if (!set_unknown_children) { + return {nullptr, false}; + } + if (wildcard_ == nullptr) { + wildcard_ = std::make_unique( + this, AttributeQualifierPattern::Wildcard()); + } + return {wildcard_.get(), wildcard_->SetUnknown()}; +} + +struct MissingAttributeClearer { + [[nodiscard]] + static std::pair Clear(AttributeMatcherNode* node) { + if (node->IsUnknown() && node->IsFound()) { + // Only unknown, skip. + return {0, false}; + } + // Must have missing. + ABSL_DCHECK(node->IsMissing()); + if (!node->HasMissingChildren()) { + // Leaf. + node->ClearMissing(); + return {1, node->IsKnown()}; + } + // Must have children. + size_t count = 0; + if (node->children_ != nullptr) { + auto children_begin = node->children_->begin(); + const auto children_end = node->children_->end(); + while (children_begin != children_end) { + auto [removed, erase] = + Clear(const_cast(&*children_begin)); + count += removed; + if (erase) { + children_begin = node->children_->erase(children_begin); + } else { + ++children_begin; + } + } + } + if (node->wildcard_ != nullptr) { + auto [removed, erase] = Clear(node->wildcard_.get()); + count += removed; + if (erase) { + node->wildcard_.reset(); + } + } + node->ClearMissing(); + return {count, node->IsKnown()}; + } +}; + +struct UnknownAttributeClearer { + [[nodiscard]] + static std::pair Clear(AttributeMatcherNode* node) { + if (node->IsKnown() && node->IsMissing()) { + // Only missing, skip. + return {0, false}; + } + // Must have missing. + ABSL_DCHECK(node->IsUnknown()); + if (!node->HasUnknownChildren()) { + // Leaf. + node->ClearUnknown(); + return {1, node->IsFound()}; + } + // Must have children. + size_t count = 0; + if (node->children_ != nullptr) { + auto children_begin = node->children_->begin(); + const auto children_end = node->children_->end(); + while (children_begin != children_end) { + auto [removed, erase] = + Clear(const_cast(&*children_begin)); + count += removed; + if (erase) { + children_begin = node->children_->erase(children_begin); + } else { + ++children_begin; + } + } + } + if (node->wildcard_ != nullptr) { + auto [removed, erase] = Clear(node->wildcard_.get()); + count += removed; + if (erase) { + node->wildcard_.reset(); + } + } + node->ClearUnknown(); + return {count, node->IsFound()}; + } +}; + +bool AttributeMatcherNode::RemoveUnknown() { + ABSL_DCHECK(IsUnknown()); + ABSL_DCHECK(!HasUnknownChildren()); + ClearUnknown(); + AttributeMatcherNode* parent = this->parent(); + if (parent == nullptr) { + return IsFound(); + } + if (IsFound()) { + if (key_.IsWildcard()) { + ABSL_DCHECK_EQ(parent->wildcard_.get(), this); + parent->wildcard_.reset(); + } else { + parent->children_->erase(*this); + if (parent->children_->empty()) { + parent->children_.reset(); + } + } + } + return parent->UnknownChildRemoved(); +} + +bool AttributeMatcherNode::UnknownChildRemoved() { + ABSL_DCHECK(IsUnknown()); + ABSL_DCHECK(HasUnknownChildren()); + bool unknown_children = false; + if (children_ != nullptr) { + for (auto& child : *children_) { + if (child.IsUnknown()) { + unknown_children = true; + break; + } + } + } + if (!unknown_children && wildcard_ != nullptr && wildcard_->IsUnknown()) { + unknown_children = true; + } + if (unknown_children) { + return false; + } + ClearUnknownChildren(); + return RemoveUnknown(); +} + +bool AttributeMatcherNode::RemoveMissing() { + ABSL_DCHECK(IsMissing()); + ABSL_DCHECK(!HasMissingChildren()); + ClearMissing(); + AttributeMatcherNode* parent = this->parent(); + if (parent == nullptr) { + return IsKnown(); + } + if (IsKnown()) { + if (key_.IsWildcard()) { + ABSL_DCHECK_EQ(parent->wildcard_.get(), this); + parent->wildcard_.reset(); + } else { + parent->children_->erase(*this); + if (parent->children_->empty()) { + parent->children_.reset(); + } + } + } + return parent->MissingChildRemoved(); +} + +bool AttributeMatcherNode::MissingChildRemoved() { + ABSL_DCHECK(IsMissing()); + ABSL_DCHECK(HasMissingChildren()); + bool missing_children = false; + if (children_ != nullptr) { + for (auto& child : *children_) { + if (child.IsMissing()) { + missing_children = true; + break; + } + } + } + if (!missing_children && wildcard_ != nullptr && wildcard_->IsMissing()) { + missing_children = true; + } + if (missing_children) { + return false; + } + ClearMissingChildren(); + return RemoveMissing(); +} + +} // namespace common_internal + +void AttributeMatcher::ClearMissingAttributes() { + auto children_begin = children_.begin(); + const auto children_end = children_.end(); + while (children_begin != children_end) { + if (children_begin->IsUnknown() && children_begin->IsFound()) { + // Only unknown, skip. + ++children_begin; + continue; + } + // Must have missing. + ABSL_DCHECK(children_begin->IsMissing()); + auto [removed, erase] = common_internal::MissingAttributeClearer::Clear( + &const_cast(*children_begin)); + if (erase) { + children_begin = children_.erase(children_begin); + } else { + ++children_begin; + } + missing_attributes_ -= removed; + } +} + +void AttributeMatcher::ClearUnknownAttributes() { + auto children_begin = children_.begin(); + const auto children_end = children_.end(); + while (children_begin != children_end) { + if (children_begin->IsKnown() && children_begin->IsMissing()) { + // Only missing, skip. + ++children_begin; + continue; + } + // Must have unknown. + ABSL_DCHECK(children_begin->IsUnknown()); + auto [removed, erase] = common_internal::UnknownAttributeClearer::Clear( + &const_cast(*children_begin)); + if (erase) { + children_begin = children_.erase(children_begin); + } else { + ++children_begin; + } + unknown_attributes_ -= removed; + } +} + +namespace { + +absl::Status AttributePatternNotFound( + absl::string_view type, absl::string_view pattern, + absl::SourceLocation source_location = absl::SourceLocation::current()) { + return absl::NotFoundError( + absl::StrCat(type, " attribute pattern ", pattern, " not found"), + source_location); +} + +bool IsWildcard(const AttributeQualifierPattern& qualifier) { + return qualifier.IsWildcard(); +} + +bool IsWildcard(const AttributeQualifier&) { return false; } + +} // namespace + +template +absl::Status AttributeMatcher::AddUnknownAttribute( + absl::string_view variable, absl::Span qualifiers, + absl::FunctionRef to_string) { + if (variable.empty()) { + return absl::InvalidArgumentError( + "attribute pattern variable name is required"); + } + for (size_t i = 0; i < qualifiers.size(); ++i) { + if (!qualifiers[i]) { + return absl::InvalidArgumentError("bad attribute qualifier pattern"); + } + if (IsWildcard(qualifiers[i]) && i != qualifiers.size() - 1) { + return absl::InvalidArgumentError( + "wildcard qualifier in attribute pattern must be last"); + } + } + common_internal::AttributeMatcherNode* node = + const_cast( + &*children_ + .emplace(nullptr, AttributeQualifierPattern::OfString(variable)) + .first); + bool mutated = node->SetUnknown(); + for (const auto& qualifier : qualifiers) { + std::tie(node, mutated) = node->AddUnknown(qualifier, mutated); + if (node == nullptr) { + return absl::AlreadyExistsError( + absl::StrCat("attribute pattern which is a superset of ", to_string(), + " already exists")); + } + } + if (mutated) { + ++unknown_attributes_; + } else { + return absl::AlreadyExistsError(absl::StrCat( + "attribute pattern covering ", to_string(), " already exists")); + } + return absl::OkStatus(); +} + +template +absl::Status AttributeMatcher::AddMissingAttribute( + absl::string_view variable, absl::Span qualifiers, + absl::FunctionRef to_string) { + if (variable.empty()) { + return absl::InvalidArgumentError( + "attribute pattern variable name is required"); + } + for (size_t i = 0; i < qualifiers.size(); ++i) { + if (!qualifiers[i]) { + return absl::InvalidArgumentError("bad attribute qualifier pattern"); + } + if (IsWildcard(qualifiers[i]) && i != qualifiers.size() - 1) { + return absl::InvalidArgumentError( + "wildcard qualifier in attribute pattern must be last"); + } + } + common_internal::AttributeMatcherNode* node = + const_cast( + &*children_ + .emplace(nullptr, AttributeQualifierPattern::OfString(variable)) + .first); + bool mutated = node->SetMissing(); + for (const auto& qualifier : qualifiers) { + std::tie(node, mutated) = node->AddMissing(qualifier, mutated); + if (node == nullptr) { + return absl::AlreadyExistsError( + absl::StrCat("attribute pattern which is a superset of ", to_string(), + " already exists")); + } + } + if (mutated) { + ++missing_attributes_; + } else { + return absl::AlreadyExistsError(absl::StrCat( + "attribute pattern covering ", to_string(), " already exists")); + } + return absl::OkStatus(); +} + +absl::Status AttributeMatcher::AddUnknownAttribute( + const AttributePattern& pattern) { + return AddUnknownAttribute( + pattern.variable(), pattern.qualifier_path(), + [&pattern]() -> std::string { return pattern.ToString(); }); +} + +absl::Status AttributeMatcher::AddUnknownAttribute(const Attribute& attribute) { + return AddUnknownAttribute( + attribute.variable_name(), attribute.qualifier_path(), + [&attribute]() -> std::string { return attribute.ToString(); }); +} + +absl::Status AttributeMatcher::AddMissingAttribute( + const AttributePattern& pattern) { + return AddMissingAttribute( + pattern.variable(), pattern.qualifier_path(), + [&pattern]() -> std::string { return pattern.ToString(); }); +} + +absl::Status AttributeMatcher::AddMissingAttribute(const Attribute& attribute) { + return AddMissingAttribute( + attribute.variable_name(), attribute.qualifier_path(), + [&attribute]() -> std::string { return attribute.ToString(); }); +} + +template +absl::Status AttributeMatcher::RemoveUnknownAttribute( + absl::string_view variable, absl::Span qualifiers, + absl::FunctionRef to_string) { + auto children_it = children_.find(AttributeQualifierView::OfString(variable)); + if (children_it == children_.end() || children_it->IsKnown()) { + return AttributePatternNotFound("unknown", to_string()); + } + Node* variable_node = const_cast(&*children_it); + Node* node = variable_node; + for (const auto& qualifier : qualifiers) { + if (node->children_ == nullptr || !node->HasUnknownChildren()) { + return AttributePatternNotFound("unknown", to_string()); + } + if (IsWildcard(qualifier)) { + if (node->wildcard_ == nullptr || node->wildcard_->IsKnown()) { + return AttributePatternNotFound("unknown", to_string()); + } + node = node->wildcard_.get(); + } else { + children_it = node->children_->find(qualifier); + if (children_it == node->children_->end() || children_it->IsKnown()) { + return AttributePatternNotFound("unknown", to_string()); + } + node = const_cast(&*children_it); + } + } + if (node->HasUnknownChildren()) { + return AttributePatternNotFound("unknown", to_string()); + } + --unknown_attributes_; + if (node->RemoveUnknown()) { + children_.erase(*variable_node); + } + return absl::OkStatus(); +} + +template +absl::Status AttributeMatcher::RemoveMissingAttribute( + absl::string_view variable, absl::Span qualifiers, + absl::FunctionRef to_string) { + auto children_it = children_.find(AttributeQualifierView::OfString(variable)); + if (children_it == children_.end() || children_it->IsFound()) { + return AttributePatternNotFound("missing", to_string()); + } + Node* variable_node = const_cast(&*children_it); + Node* node = variable_node; + for (const auto& qualifier : qualifiers) { + if (node->children_ == nullptr || !node->HasMissingChildren()) { + return AttributePatternNotFound("missing", to_string()); + } + if (IsWildcard(qualifier)) { + if (node->wildcard_ == nullptr || node->wildcard_->IsFound()) { + return AttributePatternNotFound("missing", to_string()); + } + node = node->wildcard_.get(); + } else { + children_it = node->children_->find(qualifier); + if (children_it == node->children_->end() || children_it->IsFound()) { + return AttributePatternNotFound("missing", to_string()); + } + node = const_cast(&*children_it); + } + } + if (node->HasMissingChildren()) { + return AttributePatternNotFound("missing", to_string()); + } + --missing_attributes_; + if (node->RemoveMissing()) { + children_.erase(*variable_node); + } + return absl::OkStatus(); +} + +absl::Status AttributeMatcher::RemoveUnknownAttribute( + const AttributePattern& pattern) { + return RemoveUnknownAttribute( + pattern.variable(), pattern.qualifier_path(), + [&pattern]() -> std::string { return pattern.ToString(); }); +} + +absl::Status AttributeMatcher::RemoveUnknownAttribute( + const Attribute& attribute) { + return RemoveUnknownAttribute( + attribute.variable_name(), attribute.qualifier_path(), + [&attribute]() -> std::string { return attribute.ToString(); }); +} + +absl::Status AttributeMatcher::RemoveMissingAttribute( + const AttributePattern& pattern) { + return RemoveMissingAttribute( + pattern.variable(), pattern.qualifier_path(), + [&pattern]() -> std::string { return pattern.ToString(); }); +} + +absl::Status AttributeMatcher::RemoveMissingAttribute( + const Attribute& attribute) { + return RemoveMissingAttribute( + attribute.variable_name(), attribute.qualifier_path(), + [&attribute]() -> std::string { return attribute.ToString(); }); +} + +struct AttributeMatcher::MissingAttributeCollector { + std::vector result; + absl::string_view variable; + std::vector qualifiers; + + explicit MissingAttributeCollector(size_t capacity) { + result.reserve(capacity); + } + + void CollectVariable(absl::string_view variable, + const common_internal::AttributeMatcherNode* node) { + if (!node->IsMissing()) { + return; + } + if (!node->HasMissingChildren()) { + result.push_back(AttributePattern(std::string(variable), {})); + return; + } + this->variable = variable; + qualifiers.clear(); + for (const auto& child : *node->children_) { + CollectQualifier(child.key(), &child); + } + if (node->wildcard_ != nullptr) { + CollectQualifier(AttributeQualifierPattern::Wildcard(), + node->wildcard_.get()); + } + } + + void CollectQualifier(const AttributeQualifierPattern& qualifier, + const common_internal::AttributeMatcherNode* node) { + if (!node->IsMissing()) { + return; + } + qualifiers.emplace_back(qualifier); + if (node->HasMissingChildren()) { + for (const auto& child : *node->children_) { + CollectQualifier(child.key(), &child); + } + if (node->wildcard_ != nullptr) { + CollectQualifier(AttributeQualifierPattern::Wildcard(), + node->wildcard_.get()); + } + } else { + result.push_back(AttributePattern(std::string(variable), qualifiers)); + } + qualifiers.pop_back(); + } +}; + +struct AttributeMatcher::UnknownAttributeCollector { + std::vector result; + absl::string_view variable; + std::vector qualifiers; + + explicit UnknownAttributeCollector(size_t capacity) { + result.reserve(capacity); + } + + void CollectVariable(absl::string_view variable, + const common_internal::AttributeMatcherNode* node) { + if (!node->IsUnknown()) { + return; + } + if (!node->HasUnknownChildren()) { + result.push_back(AttributePattern(std::string(variable), {})); + return; + } + this->variable = variable; + qualifiers.clear(); + for (const auto& child : *node->children_) { + CollectQualifier(child.key(), &child); + } + if (node->wildcard_ != nullptr) { + CollectQualifier(AttributeQualifierPattern::Wildcard(), + node->wildcard_.get()); + } + } + + void CollectQualifier(const AttributeQualifierPattern& qualifier, + const common_internal::AttributeMatcherNode* node) { + if (!node->IsUnknown()) { + return; + } + qualifiers.emplace_back(qualifier); + if (node->HasUnknownChildren()) { + for (const auto& child : *node->children_) { + CollectQualifier(child.key(), &child); + } + if (node->wildcard_ != nullptr) { + CollectQualifier(AttributeQualifierPattern::Wildcard(), + node->wildcard_.get()); + } + } else { + result.push_back(AttributePattern(std::string(variable), qualifiers)); + } + qualifiers.pop_back(); + } +}; + +[[nodiscard]] +std::vector AttributeMatcher::GetMissingAttributes() const { + MissingAttributeCollector collector(missing_attributes_); + for (const auto& child : children_) { + // Guaranteed to be string. + collector.CollectVariable(child.key().GetString(), &child); + } + return std::move(collector.result); +} + +[[nodiscard]] +std::vector AttributeMatcher::GetUnknownAttributes() const { + UnknownAttributeCollector collector(unknown_attributes_); + for (const auto& child : children_) { + // Guaranteed to be string. + collector.CollectVariable(child.key().GetString(), &child); + } + return std::move(collector.result); +} + +const AttributeMatcher& EmptyAttributeMatcher() { + static const absl::NoDestructor empty; + return *empty; +} + +} // namespace cel diff --git a/base/attribute_matcher.h b/base/attribute_matcher.h new file mode 100644 index 000000000..827b0b04b --- /dev/null +++ b/base/attribute_matcher.h @@ -0,0 +1,480 @@ +// 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_BASE_ATTRIBUTE_MATCHER_H_ +#define THIRD_PARTY_CEL_CPP_BASE_ATTRIBUTE_MATCHER_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/attributes.h" +#include "absl/functional/function_ref.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "base/attribute.h" + +namespace cel { + +class AttributeMatcher; +class AttributeMatch; + +namespace common_internal { + +struct MissingAttributeClearer; +struct UnknownAttributeClearer; + +class AttributeMatcherNode { + public: + struct Less { + using is_transparent = void; + + bool operator()(const AttributeMatcherNode& lhs, + const AttributeMatcherNode& rhs) const { + return lhs.key() < rhs.key(); + } + + bool operator()(const AttributeMatcherNode& lhs, + const AttributeQualifierPattern& rhs) const { + return lhs.key() < rhs; + } + + bool operator()(const AttributeQualifierPattern& lhs, + const AttributeMatcherNode& rhs) const { + return lhs < rhs.key(); + } + + bool operator()(const AttributeMatcherNode& lhs, + const AttributeQualifierView& rhs) const { + return lhs.key() < rhs; + } + + bool operator()(const AttributeQualifierView& lhs, + const AttributeMatcherNode& rhs) const { + return lhs < rhs.key(); + } + + bool operator()(const AttributeMatcherNode& lhs, + const AttributeQualifier& rhs) const { + return lhs.key() < rhs; + } + + bool operator()(const AttributeQualifier& lhs, + const AttributeMatcherNode& rhs) const { + return lhs < rhs.key(); + } + }; + + AttributeMatcherNode(AttributeMatcherNode* parent, + const AttributeQualifierPattern& key) + : parent_(parent), + key_(key), + depth_(parent != nullptr ? parent->depth_ + 1 : 0) {} + + [[nodiscard]] + bool IsUnknown() const { + return unknown_; + } + + [[nodiscard]] + bool IsMissing() const { + return missing_; + } + + [[nodiscard]] + bool HasUnknownChildren() const { + return unknown_children_; + } + + [[nodiscard]] + bool HasMissingChildren() const { + return missing_children_; + } + + [[nodiscard]] + bool IsKnown() const { + return !IsUnknown(); + } + + [[nodiscard]] + bool IsFound() const { + return !IsMissing(); + } + + [[nodiscard]] + bool IsFullyUnknown() const { + return IsUnknown() && !HasUnknownChildren(); + } + + [[nodiscard]] + bool IsFullyMissing() const { + return IsMissing() && !HasMissingChildren(); + } + + [[nodiscard]] + bool IsPartiallyUnknown() const { + return IsUnknown() && HasUnknownChildren(); + } + + [[nodiscard]] + bool IsPartiallyMissing() const { + return IsMissing() && HasMissingChildren(); + } + + [[nodiscard]] + const AttributeQualifierPattern& key() const { + return key_; + } + + [[nodiscard]] + AttributeMatcherNode* parent() const { + return parent_; + } + + [[nodiscard]] + uint32_t depth() const { + return depth_; + } + + [[nodiscard]] + const AttributeMatcherNode* MatchQualifier( + const cel::AttributeQualifierView& qualifier) const { + if (children_ != nullptr) { + auto it = children_->find(qualifier); + if (it != children_->end()) { + return &*it; + } + } + if (wildcard_ != nullptr) { + return wildcard_.get(); + } + return nullptr; + } + + [[nodiscard]] + std::pair AddMissing( + const AttributeQualifier& pattern, bool prev_set); + [[nodiscard]] + std::pair AddMissing( + const AttributeQualifierPattern& pattern, bool prev_set); + + [[nodiscard]] + std::pair AddUnknown( + const AttributeQualifier& pattern, bool prev_set); + [[nodiscard]] + std::pair AddUnknown( + const AttributeQualifierPattern& pattern, bool prev_set); + + void ClearMissing() { + missing_ = false; + ClearMissingChildren(); + } + + void ClearUnknown() { + unknown_ = false; + ClearUnknownChildren(); + } + + [[nodiscard]] + bool RemoveUnknown(); + + [[nodiscard]] + bool RemoveMissing(); + + private: + friend class cel::AttributeMatcher; + friend struct MissingAttributeClearer; + friend struct UnknownAttributeClearer; + + [[nodiscard]] + bool UnknownChildRemoved(); + + [[nodiscard]] + bool MissingChildRemoved(); + + bool SetMissing() { + if (missing_) { + return false; + } + missing_ = true; + return true; + } + + bool SetUnknown() { + if (unknown_) { + return false; + } + unknown_ = true; + return true; + } + + bool SetMissingChildren() { + if (missing_children_) { + return false; + } + missing_children_ = true; + return true; + } + + bool SetUnknownChildren() { + if (unknown_children_) { + return false; + } + unknown_children_ = true; + return true; + } + + void ClearMissingChildren() { missing_children_ = false; } + + void ClearUnknownChildren() { unknown_children_ = false; } + + AttributeMatcherNode* const parent_; + const AttributeQualifierPattern key_; + const uint32_t depth_; + bool unknown_ = false; + bool missing_ = false; + bool unknown_children_ = false; + bool missing_children_ = false; + // Pointer stability is required. + std::unique_ptr> children_; + std::unique_ptr wildcard_; +}; + +AttributeMatch MakeAttributeMatch( + const common_internal::AttributeMatcherNode* node); + +[[nodiscard]] +const AttributeMatcherNode* GetAttributeMatcherNode( + const AttributeMatch& match); + +} // namespace common_internal + +class [[nodiscard]] AttributeMatch { + public: + using Type = AttributePattern::MatchType; + + AttributeMatch() = default; + AttributeMatch(const AttributeMatch&) = default; + AttributeMatch& operator=(const AttributeMatch&) = default; + + [[nodiscard]] + Type GetUnknownType() const { + return node_ == nullptr || !node_->IsUnknown() ? Type::NONE + : node_->HasUnknownChildren() ? Type::PARTIAL + : Type::FULL; + } + + [[nodiscard]] + Type GetMissingType() const { + return node_ == nullptr || !node_->IsMissing() ? Type::NONE + : node_->HasMissingChildren() ? Type::PARTIAL + : Type::FULL; + } + + [[nodiscard]] + bool IsNone() const { + return node_ == nullptr; + } + + [[nodiscard]] + bool IsKnown() const { + return IsNone() || node_->IsKnown(); + } + + [[nodiscard]] + bool IsFound() const { + return IsNone() || node_->IsFound(); + } + + [[nodiscard]] + bool IsFullyUnknown() const { + return !IsNone() && node_->IsFullyUnknown(); + } + + [[nodiscard]] + bool IsFullyMissing() const { + return !IsNone() && node_->IsFullyMissing(); + } + + [[nodiscard]] + bool IsPartiallyUnknown() const { + return !IsNone() && node_->IsPartiallyUnknown(); + } + + [[nodiscard]] + bool IsPartiallyMissing() const { + return !IsNone() && node_->IsPartiallyMissing(); + } + + AttributeMatch MatchQualifier(const AttributeQualifierView& qualifier) const { + if (IsNone()) { + return AttributeMatch(); + } + return AttributeMatch(node_->MatchQualifier(qualifier)); + } + + private: + friend AttributeMatch common_internal::MakeAttributeMatch( + const common_internal::AttributeMatcherNode* node); + friend const common_internal::AttributeMatcherNode* + common_internal::GetAttributeMatcherNode(const AttributeMatch& match); + + explicit AttributeMatch(const common_internal::AttributeMatcherNode* node) + : node_(node) {} + + const common_internal::AttributeMatcherNode* node_ = nullptr; +}; + +namespace common_internal { + +inline AttributeMatch MakeAttributeMatch( + const common_internal::AttributeMatcherNode* node) { + return AttributeMatch(node); +} + +[[nodiscard]] +inline const AttributeMatcherNode* GetAttributeMatcherNode( + const AttributeMatch& match) { + return match.node_; +} + +} // namespace common_internal + +// AttributeMatcher allows for efficiently checking for unknown or missing +// attributes at runtime in a scalable manner. AttributeMatcher is +// thread-compatible however it must not be modified during any evaluation where +// it is being used. +class AttributeMatcher { + private: + using Node = common_internal::AttributeMatcherNode; + + public: + AttributeMatcher() = default; + AttributeMatcher(const AttributeMatcher&) = delete; + AttributeMatcher(AttributeMatcher&&) = default; + AttributeMatcher& operator=(const AttributeMatcher&) = delete; + AttributeMatcher& operator=(AttributeMatcher&&) = default; + + // Adds an unknown attribute to the matcher. Returns an error if the attribute + // or attribute pattern being added overlaps with a previously added unknown + // attribute. + absl::Status AddUnknownAttribute(const AttributePattern& pattern); + absl::Status AddUnknownAttribute(const Attribute& attribute); + + // Adds a missing attribute to the matcher. Returns an error if the attribute + // or attribute pattern being added overlaps with a previously added missing + // attribute. + absl::Status AddMissingAttribute(const AttributePattern& pattern); + absl::Status AddMissingAttribute(const Attribute& attribute); + + // Removes a previously added unknown attribute from the matcher. + absl::Status RemoveUnknownAttribute(const AttributePattern& pattern); + absl::Status RemoveUnknownAttribute(const Attribute& attribute); + + // Removes a previously added missing attribute from the matcher. + absl::Status RemoveMissingAttribute(const AttributePattern& pattern); + absl::Status RemoveMissingAttribute(const Attribute& attribute); + + // Resets the matcher such that all observable behavior is equivalent to a + // default constructed matcher. + void ClearAttributes() { + children_.clear(); + missing_attributes_ = 0; + unknown_attributes_ = 0; + } + + // Removes all previously added missing attributes. + void ClearMissingAttributes(); + + // Removes all previously added unknown attributes. + void ClearUnknownAttributes(); + + AttributeMatch MatchVariable(absl::string_view variable) const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + auto it = children_.find(AttributeQualifierView::OfString(variable)); + if (it != children_.end()) { + return common_internal::MakeAttributeMatch(&*it); + } + return AttributeMatch(); + } + + // Returns an array of missing attribute patterns corresponding to the + // previously added missing attribute patterns. The order of the attribute + // patterns in the array is not guaranteed. + [[nodiscard]] + std::vector GetMissingAttributes() const; + + // Returns an array of unknown attribute patterns corresponding to the + // previously added unknown attribute patterns. The order of the attribute + // patterns in the array is not guaranteed. + [[nodiscard]] + std::vector GetUnknownAttributes() const; + + // Tests whether any missing attribute patterns have been added to this + // matcher. + [[nodiscard]] + bool HasMissingAttributes() const { + return missing_attributes_ != 0; + } + + // Tests whether any unknown attribute patterns have been added to this + // matcher. + [[nodiscard]] + bool HasUnknownAttributes() const { + return unknown_attributes_ != 0; + } + + private: + struct MissingAttributeCollector; + struct UnknownAttributeCollector; + friend struct MissingAttributeClearer; + friend struct UnknownAttributeClearer; + + template + absl::Status AddUnknownAttribute(absl::string_view variable, + absl::Span qualifiers, + absl::FunctionRef to_string); + + template + absl::Status AddMissingAttribute(absl::string_view variable, + absl::Span qualifiers, + absl::FunctionRef to_string); + + template + absl::Status RemoveUnknownAttribute( + absl::string_view variable, absl::Span qualifiers, + absl::FunctionRef to_string); + + template + absl::Status RemoveMissingAttribute( + absl::string_view variable, absl::Span qualifiers, + absl::FunctionRef to_string); + + // Pointer stability is required. + std::set children_; + size_t missing_attributes_ = 0; + size_t unknown_attributes_ = 0; +}; + +[[nodiscard]] +const AttributeMatcher& EmptyAttributeMatcher(); + +} // namespace cel + +#endif // THIRD_PARTY_CEL_CPP_BASE_ATTRIBUTE_MATCHER_H_ diff --git a/base/attribute_matcher_test.cc b/base/attribute_matcher_test.cc new file mode 100644 index 000000000..8a18ba558 --- /dev/null +++ b/base/attribute_matcher_test.cc @@ -0,0 +1,462 @@ +// Copyright 2022 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 "base/attribute_matcher.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "base/attribute.h" +#include "internal/testing.h" + +namespace cel { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; + +using MatchType = AttributePattern::MatchType; + +MATCHER_P2(AttributeMatchIs, missing_type, unknown_type, "") { + return ::testing::ExplainMatchResult(::testing::Eq(missing_type), + arg.GetMissingType(), result_listener) && + ::testing::ExplainMatchResult(::testing::Eq(unknown_type), + arg.GetUnknownType(), result_listener); +} + +using ::testing::IsEmpty; + +TEST(AttributeMatcher, Empty) { + AttributeMatcher matcher; + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_FALSE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), IsEmpty()); + EXPECT_THAT(matcher.MatchVariable("foo"), + AttributeMatchIs(MatchType::NONE, MatchType::NONE)); +} + +TEST(AttributeMatcher, Unknown_Attribute_OneVariable_ZeroQualifier_Subset) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute(Attribute("foo")), IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute(Attribute("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::Wildcard()})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern("foo"))); +} + +TEST(AttributeMatcher, + Unknown_AttributePattern_OneVariable_ZeroQualifier_Subset) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern("foo")), IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute(Attribute("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::Wildcard()})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern("foo"))); +} + +TEST(AttributeMatcher, Unknown_Attribute_OneVariable_TwoQualifier_Superset) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar"), + AttributeQualifier::OfString("baz")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute(Attribute("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar"), + AttributeQualifierPattern::OfString("baz")}))); +} + +TEST(AttributeMatcher, + Unknown_AttributePattern_OneVariable_TwoQualifier_Superset) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar"), + AttributeQualifierPattern::OfString("baz")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute(Attribute("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern("foo")), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar"), + AttributeQualifierPattern::OfString("baz")}))); +} + +TEST(AttributeMatcher, Unknown_Attribute_OneVariable_ZeroQualifier) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute(Attribute("foo")), IsOk()); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern("foo"))); + EXPECT_THAT(matcher.MatchVariable("foo"), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); +} + +TEST(AttributeMatcher, Unknown_Attribute_OneVariable_OneQualifier) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + IsOk()); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")}))); + EXPECT_THAT(matcher.MatchVariable("foo"), + AttributeMatchIs(MatchType::NONE, MatchType::PARTIAL)); + EXPECT_THAT(matcher.MatchVariable("foo").MatchQualifier( + AttributeQualifierView::OfString("bar")), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); +} + +TEST(AttributeMatcher, Unknown_Attribute_OneVariable_TwoQualifier) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("baz")})), + IsOk()); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT( + matcher.GetUnknownAttributes(), + UnorderedElementsAre( + AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("foo", + {AttributeQualifierPattern::OfString("baz")}))); + EXPECT_THAT(matcher.MatchVariable("foo"), + AttributeMatchIs(MatchType::NONE, MatchType::PARTIAL)); + EXPECT_THAT(matcher.MatchVariable("foo").MatchQualifier( + AttributeQualifierView::OfString("bar")), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); + EXPECT_THAT(matcher.MatchVariable("foo").MatchQualifier( + AttributeQualifierView::OfString("baz")), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); +} + +TEST(AttributeMatcher, Unknown_AttributePattern_OneVariable_ZeroQualifier) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern("foo")), IsOk()); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern("foo"))); + EXPECT_THAT(matcher.MatchVariable("foo"), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); +} + +TEST(AttributeMatcher, Unknown_AttributePattern_OneVariable_OneQualifier) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")})), + IsOk()); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")}))); + EXPECT_THAT(matcher.MatchVariable("foo"), + AttributeMatchIs(MatchType::NONE, MatchType::PARTIAL)); + EXPECT_THAT(matcher.MatchVariable("foo").MatchQualifier( + AttributeQualifierView::OfString("bar")), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); +} + +TEST(AttributeMatcher, Unknown_AttributePattern_OneVariable_TwoQualifier) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("bar")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::OfString("baz")})), + IsOk()); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_THAT( + matcher.GetUnknownAttributes(), + UnorderedElementsAre( + AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("foo", + {AttributeQualifierPattern::OfString("baz")}))); + EXPECT_THAT(matcher.MatchVariable("foo"), + AttributeMatchIs(MatchType::NONE, MatchType::PARTIAL)); + EXPECT_THAT(matcher.MatchVariable("foo").MatchQualifier( + AttributeQualifierView::OfString("bar")), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); + EXPECT_THAT(matcher.MatchVariable("foo").MatchQualifier( + AttributeQualifierView::OfString("baz")), + AttributeMatchIs(MatchType::NONE, MatchType::FULL)); +} + +TEST(AttributeMatcher, ClearMissingAttributes) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddMissingAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + IsOk()); + EXPECT_THAT(matcher.AddMissingAttribute( + Attribute("bar", {AttributeQualifier::OfString("foo")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute(Attribute("bar")), IsOk()); + EXPECT_THAT(matcher.AddMissingAttribute( + Attribute("baz", {AttributeQualifier::OfString("foo")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("baz", {AttributeQualifier::OfString("foo"), + AttributeQualifier::OfString("bar")})), + IsOk()); + matcher.ClearMissingAttributes(); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT( + matcher.GetUnknownAttributes(), + UnorderedElementsAre( + AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("bar"), + AttributePattern("baz", + {AttributeQualifierPattern::OfString("foo"), + AttributeQualifierPattern::OfString("bar")}))); +} + +TEST(AttributeMatcher, ClearUnknownAttributes) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + IsOk()); + EXPECT_THAT(matcher.AddMissingAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("bar", {AttributeQualifier::OfString("foo")})), + IsOk()); + EXPECT_THAT(matcher.AddMissingAttribute(Attribute("bar")), IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("baz", {AttributeQualifier::OfString("foo")})), + IsOk()); + EXPECT_THAT(matcher.AddMissingAttribute( + Attribute("baz", {AttributeQualifier::OfString("foo"), + AttributeQualifier::OfString("bar")})), + IsOk()); + matcher.ClearUnknownAttributes(); + EXPECT_FALSE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetUnknownAttributes(), IsEmpty()); + EXPECT_TRUE(matcher.HasMissingAttributes()); + EXPECT_THAT( + matcher.GetMissingAttributes(), + UnorderedElementsAre( + AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("bar"), + AttributePattern("baz", + {AttributeQualifierPattern::OfString("foo"), + AttributeQualifierPattern::OfString("bar")}))); +} + +TEST(AttributeMatcher, RemoveMissingAttribute) { + const std::vector unknown_attributes( + {AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("bar", {AttributeQualifierPattern::OfString("foo")}), + AttributePattern("baz", {AttributeQualifierPattern::OfString("foo")})}); + const std::vector missing_attributes( + {AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("bar"), + AttributePattern("baz", {AttributeQualifierPattern::OfString("foo"), + AttributeQualifierPattern::OfString("bar")})}); + AttributeMatcher matcher; + for (const auto& attribute : unknown_attributes) { + EXPECT_THAT(matcher.AddUnknownAttribute(attribute), IsOk()); + } + std::vector removal_order({0, 1, 2}); + do { + for (const auto& attribute : missing_attributes) { + EXPECT_THAT(matcher.AddMissingAttribute(attribute), IsOk()); + } + // 0 + EXPECT_THAT( + matcher.RemoveMissingAttribute(missing_attributes[removal_order[0]]), + IsOk()); + EXPECT_TRUE(matcher.HasMissingAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), + UnorderedElementsAre(missing_attributes[removal_order[1]], + missing_attributes[removal_order[2]])); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAreArray(unknown_attributes)); + // 1 + EXPECT_THAT( + matcher.RemoveMissingAttribute(missing_attributes[removal_order[1]]), + IsOk()); + EXPECT_TRUE(matcher.HasMissingAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), + UnorderedElementsAre(missing_attributes[removal_order[2]])); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAreArray(unknown_attributes)); + // 2 + EXPECT_THAT( + matcher.RemoveMissingAttribute(missing_attributes[removal_order[2]]), + IsOk()); + EXPECT_FALSE(matcher.HasMissingAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), IsEmpty()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAreArray(unknown_attributes)); + } while (std::next_permutation(removal_order.begin(), removal_order.end())); +} + +TEST(AttributeMatcher, RemoveUnknownAttribute) { + const std::vector missing_attributes( + {AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("bar", {AttributeQualifierPattern::OfString("foo")}), + AttributePattern("baz", {AttributeQualifierPattern::OfString("foo")})}); + const std::vector unknown_attributes( + {AttributePattern("foo", {AttributeQualifierPattern::OfString("bar")}), + AttributePattern("bar"), + AttributePattern("baz", {AttributeQualifierPattern::OfString("foo"), + AttributeQualifierPattern::OfString("bar")})}); + AttributeMatcher matcher; + for (const auto& attribute : missing_attributes) { + EXPECT_THAT(matcher.AddMissingAttribute(attribute), IsOk()); + } + std::vector removal_order({0, 1, 2}); + do { + for (const auto& attribute : unknown_attributes) { + EXPECT_THAT(matcher.AddUnknownAttribute(attribute), IsOk()); + } + // 0 + EXPECT_THAT( + matcher.RemoveUnknownAttribute(unknown_attributes[removal_order[0]]), + IsOk()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(unknown_attributes[removal_order[1]], + unknown_attributes[removal_order[2]])); + EXPECT_TRUE(matcher.HasMissingAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), + UnorderedElementsAreArray(missing_attributes)); + // 1 + EXPECT_THAT( + matcher.RemoveUnknownAttribute(unknown_attributes[removal_order[1]]), + IsOk()); + EXPECT_TRUE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetUnknownAttributes(), + UnorderedElementsAre(unknown_attributes[removal_order[2]])); + EXPECT_TRUE(matcher.HasMissingAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), + UnorderedElementsAreArray(missing_attributes)); + // 2 + EXPECT_THAT( + matcher.RemoveUnknownAttribute(unknown_attributes[removal_order[2]]), + IsOk()); + EXPECT_FALSE(matcher.HasUnknownAttributes()); + EXPECT_THAT(matcher.GetUnknownAttributes(), IsEmpty()); + EXPECT_TRUE(matcher.HasMissingAttributes()); + EXPECT_THAT(matcher.GetMissingAttributes(), + UnorderedElementsAreArray(missing_attributes)); + } while (std::next_permutation(removal_order.begin(), removal_order.end())); +} + +TEST(AttributeMatcher, Wildcards) { + AttributeMatcher matcher; + EXPECT_THAT(matcher.AddMissingAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::Wildcard(), + AttributeQualifierPattern::Wildcard()})), + StatusIs(absl::StatusCode::kInvalidArgument)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::Wildcard(), + AttributeQualifierPattern::Wildcard()})), + StatusIs(absl::StatusCode::kInvalidArgument)); + EXPECT_THAT(matcher.AddMissingAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::Wildcard()})), + IsOk()); + EXPECT_THAT(matcher.AddMissingAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(matcher.AddUnknownAttribute(AttributePattern( + "foo", {AttributeQualifierPattern::Wildcard()})), + IsOk()); + EXPECT_THAT(matcher.AddUnknownAttribute( + Attribute("foo", {AttributeQualifier::OfString("bar")})), + StatusIs(absl::StatusCode::kAlreadyExists)); +} + +} // namespace +} // namespace cel diff --git a/base/attribute_test.cc b/base/attribute_test.cc index d4b36f9f5..0adff75ad 100644 --- a/base/attribute_test.cc +++ b/base/attribute_test.cc @@ -76,5 +76,24 @@ TEST(AttributeQualifierView, String) { EXPECT_THAT(qualifier.AsString(), Optional(absl::string_view("foo"))); } +TEST(Attribute, ToString) { + EXPECT_EQ(Attribute("foo", {AttributeQualifier::OfBool(true), + AttributeQualifier::OfInt(2), + AttributeQualifier::OfUint(3), + AttributeQualifier::OfString("bar")}) + .ToString(), + "foo[true][2][3u][\"bar\"]"); +} + +TEST(AttributePattern, ToString) { + EXPECT_EQ(AttributePattern("foo", {AttributeQualifierPattern::OfBool(true), + AttributeQualifierPattern::OfInt(2), + AttributeQualifierPattern::OfUint(3), + AttributeQualifierPattern::OfString("bar"), + AttributeQualifierPattern::Wildcard()}) + .ToString(), + "foo[true][2][3u][\"bar\"][*]"); +} + } // namespace } // namespace cel