Drop the AGPL license in favor of a source-available license. See the blog post [1] for details. [1] https://www.scylladb.com/2024/12/18/why-were-moving-to-a-source-available-license/
757 lines
32 KiB
C++
757 lines
32 KiB
C++
/*
|
|
* Copyright 2019-present ScyllaDB
|
|
*/
|
|
|
|
/*
|
|
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
*/
|
|
|
|
#include <string_view>
|
|
#include "alternator/conditions.hh"
|
|
#include "alternator/error.hh"
|
|
#include <unordered_map>
|
|
#include "utils/rjson.hh"
|
|
#include "serialization.hh"
|
|
#include "utils/base64.hh"
|
|
#include "utils/rjson.hh"
|
|
#include <stdexcept>
|
|
#include "utils/overloaded_functor.hh"
|
|
|
|
#include "expressions.hh"
|
|
|
|
namespace alternator {
|
|
|
|
static logging::logger clogger("alternator-conditions");
|
|
|
|
comparison_operator_type get_comparison_operator(const rjson::value& comparison_operator) {
|
|
static std::unordered_map<std::string, comparison_operator_type> ops = {
|
|
{"EQ", comparison_operator_type::EQ},
|
|
{"NE", comparison_operator_type::NE},
|
|
{"LE", comparison_operator_type::LE},
|
|
{"LT", comparison_operator_type::LT},
|
|
{"GE", comparison_operator_type::GE},
|
|
{"GT", comparison_operator_type::GT},
|
|
{"IN", comparison_operator_type::IN},
|
|
{"NULL", comparison_operator_type::IS_NULL},
|
|
{"NOT_NULL", comparison_operator_type::NOT_NULL},
|
|
{"BETWEEN", comparison_operator_type::BETWEEN},
|
|
{"BEGINS_WITH", comparison_operator_type::BEGINS_WITH},
|
|
{"CONTAINS", comparison_operator_type::CONTAINS},
|
|
{"NOT_CONTAINS", comparison_operator_type::NOT_CONTAINS},
|
|
};
|
|
if (!comparison_operator.IsString()) {
|
|
throw api_error::validation(fmt::format("Invalid comparison operator definition {}", rjson::print(comparison_operator)));
|
|
}
|
|
std::string op = comparison_operator.GetString();
|
|
auto it = ops.find(op);
|
|
if (it == ops.end()) {
|
|
throw api_error::validation(fmt::format("Unsupported comparison operator {}", op));
|
|
}
|
|
return it->second;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct size_check {
|
|
// True iff size passes this check.
|
|
virtual bool operator()(rapidjson::SizeType size) const = 0;
|
|
// Check description, such that format("expected array {}", check.what()) is human-readable.
|
|
virtual sstring what() const = 0;
|
|
};
|
|
|
|
class exact_size : public size_check {
|
|
rapidjson::SizeType _expected;
|
|
public:
|
|
explicit exact_size(rapidjson::SizeType expected) : _expected(expected) {}
|
|
bool operator()(rapidjson::SizeType size) const override { return size == _expected; }
|
|
sstring what() const override { return format("of size {}", _expected); }
|
|
};
|
|
|
|
struct empty : public size_check {
|
|
bool operator()(rapidjson::SizeType size) const override { return size < 1; }
|
|
sstring what() const override { return "to be empty"; }
|
|
};
|
|
|
|
struct nonempty : public size_check {
|
|
bool operator()(rapidjson::SizeType size) const override { return size > 0; }
|
|
sstring what() const override { return "to be non-empty"; }
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
// Check that array has the expected number of elements
|
|
static void verify_operand_count(const rjson::value* array, const size_check& expected, const rjson::value& op) {
|
|
if (!array && expected(0)) {
|
|
// If expected() allows an empty AttributeValueList, it is also fine
|
|
// that it is missing.
|
|
return;
|
|
}
|
|
if (!array || !array->IsArray()) {
|
|
throw api_error::validation("With ComparisonOperator, AttributeValueList must be given and an array");
|
|
}
|
|
if (!expected(array->Size())) {
|
|
throw api_error::validation(
|
|
format("{} operator requires AttributeValueList {}, instead found list size {}",
|
|
op, expected.what(), array->Size()));
|
|
}
|
|
}
|
|
|
|
struct rjson_engaged_ptr_comp {
|
|
bool operator()(const rjson::value* p1, const rjson::value* p2) const {
|
|
return rjson::single_value_comp()(*p1, *p2);
|
|
}
|
|
};
|
|
|
|
// It's not enough to compare underlying JSON objects when comparing sets,
|
|
// as internally they're stored in an array, and the order of elements is
|
|
// not important in set equality. See issue #5021
|
|
static bool check_EQ_for_sets(const rjson::value& set1, const rjson::value& set2) {
|
|
if (!set1.IsArray() || !set2.IsArray() || set1.Size() != set2.Size()) {
|
|
return false;
|
|
}
|
|
std::set<const rjson::value*, rjson_engaged_ptr_comp> set1_raw;
|
|
for (auto it = set1.Begin(); it != set1.End(); ++it) {
|
|
set1_raw.insert(&*it);
|
|
}
|
|
for (const auto& a : set2.GetArray()) {
|
|
if (!set1_raw.contains(&a)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
// Moreover, the JSON being compared can be a nested document with outer
|
|
// layers of lists and maps and some inner set - and we need to get to that
|
|
// inner set to compare it correctly with check_EQ_for_sets() (issue #8514).
|
|
static bool check_EQ(const rjson::value* v1, const rjson::value& v2);
|
|
static bool check_EQ_for_lists(const rjson::value& list1, const rjson::value& list2) {
|
|
if (!list1.IsArray() || !list2.IsArray() || list1.Size() != list2.Size()) {
|
|
return false;
|
|
}
|
|
auto it1 = list1.Begin();
|
|
auto it2 = list2.Begin();
|
|
while (it1 != list1.End()) {
|
|
// Note: Alternator limits an item's depth (rjson::parse() limits
|
|
// it to around 37 levels), so this recursion is safe.
|
|
if (!check_EQ(&*it1, *it2)) {
|
|
return false;
|
|
}
|
|
++it1;
|
|
++it2;
|
|
}
|
|
return true;
|
|
}
|
|
static bool check_EQ_for_maps(const rjson::value& list1, const rjson::value& list2) {
|
|
if (!list1.IsObject() || !list2.IsObject() || list1.MemberCount() != list2.MemberCount()) {
|
|
return false;
|
|
}
|
|
for (auto it1 = list1.MemberBegin(); it1 != list1.MemberEnd(); ++it1) {
|
|
auto it2 = list2.FindMember(it1->name);
|
|
if (it2 == list2.MemberEnd() || !check_EQ(&it1->value, it2->value)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the EQ relation
|
|
static bool check_EQ(const rjson::value* v1, const rjson::value& v2) {
|
|
if (v1 && v1->IsObject() && v1->MemberCount() == 1 && v2.IsObject() && v2.MemberCount() == 1) {
|
|
auto it1 = v1->MemberBegin();
|
|
auto it2 = v2.MemberBegin();
|
|
if (it1->name != it2->name) {
|
|
return false;
|
|
}
|
|
if (it1->name == "SS" || it1->name == "NS" || it1->name == "BS") {
|
|
return check_EQ_for_sets(it1->value, it2->value);
|
|
} else if(it1->name == "L") {
|
|
return check_EQ_for_lists(it1->value, it2->value);
|
|
} else if(it1->name == "M") {
|
|
return check_EQ_for_maps(it1->value, it2->value);
|
|
} else {
|
|
// Other, non-nested types (number, string, etc.) can be compared
|
|
// literally, comparing their JSON representation.
|
|
return it1->value == it2->value;
|
|
}
|
|
} else {
|
|
// If v1 and/or v2 are missing (IsNull()) the result should be false.
|
|
// In the unlikely case that the object is malformed (issue #8070),
|
|
// let's also return false.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the NE relation
|
|
static bool check_NE(const rjson::value* v1, const rjson::value& v2) {
|
|
return !check_EQ(v1, v2);
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the BEGINS_WITH relation
|
|
bool check_BEGINS_WITH(const rjson::value* v1, const rjson::value& v2,
|
|
bool v1_from_query, bool v2_from_query) {
|
|
bool bad = false;
|
|
if (!v1 || !v1->IsObject() || v1->MemberCount() != 1) {
|
|
if (v1_from_query) {
|
|
throw api_error::validation("begins_with() encountered malformed argument");
|
|
} else {
|
|
bad = true;
|
|
}
|
|
} else if (v1->MemberBegin()->name != "S" && v1->MemberBegin()->name != "B") {
|
|
if (v1_from_query) {
|
|
throw api_error::validation(format("begins_with supports only string or binary type, got: {}", *v1));
|
|
} else {
|
|
bad = true;
|
|
}
|
|
}
|
|
if (!v2.IsObject() || v2.MemberCount() != 1) {
|
|
if (v2_from_query) {
|
|
throw api_error::validation("begins_with() encountered malformed argument");
|
|
} else {
|
|
bad = true;
|
|
}
|
|
} else if (v2.MemberBegin()->name != "S" && v2.MemberBegin()->name != "B") {
|
|
if (v2_from_query) {
|
|
throw api_error::validation(format("begins_with() supports only string or binary type, got: {}", v2));
|
|
} else {
|
|
bad = true;
|
|
}
|
|
}
|
|
if (bad) {
|
|
return false;
|
|
}
|
|
auto it1 = v1->MemberBegin();
|
|
auto it2 = v2.MemberBegin();
|
|
if (it1->name != it2->name) {
|
|
return false;
|
|
}
|
|
if (it2->name == "S") {
|
|
return rjson::to_string_view(it1->value).starts_with(rjson::to_string_view(it2->value));
|
|
} else /* it2->name == "B" */ {
|
|
try {
|
|
return base64_begins_with(rjson::to_string_view(it1->value), rjson::to_string_view(it2->value));
|
|
} catch(std::invalid_argument&) {
|
|
// determine if any of the malformed values is from query and raise an exception if so
|
|
unwrap_bytes(it1->value, v1_from_query);
|
|
unwrap_bytes(it2->value, v2_from_query);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool is_set_of(const rjson::value& type1, const rjson::value& type2) {
|
|
return (type2 == "S" && type1 == "SS") || (type2 == "N" && type1 == "NS") || (type2 == "B" && type1 == "BS");
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the CONTAINS relation
|
|
bool check_CONTAINS(const rjson::value* v1, const rjson::value& v2, bool v1_from_query, bool v2_from_query) {
|
|
if (!v1) {
|
|
return false;
|
|
}
|
|
const auto& kv1 = *v1->MemberBegin();
|
|
const auto& kv2 = *v2.MemberBegin();
|
|
if (kv1.name == "S" && kv2.name == "S") {
|
|
return rjson::to_string_view(kv1.value).find(rjson::to_string_view(kv2.value)) != std::string_view::npos;
|
|
} else if (kv1.name == "B" && kv2.name == "B") {
|
|
auto d_kv1 = unwrap_bytes(kv1.value, v1_from_query);
|
|
auto d_kv2 = unwrap_bytes(kv2.value, v2_from_query);
|
|
if (!d_kv1 || !d_kv2) {
|
|
return false;
|
|
}
|
|
return d_kv1->find(*d_kv2) != bytes::npos;
|
|
} else if (is_set_of(kv1.name, kv2.name)) {
|
|
for (auto i = kv1.value.Begin(); i != kv1.value.End(); ++i) {
|
|
if (*i == kv2.value) {
|
|
return true;
|
|
}
|
|
}
|
|
} else if (kv1.name == "L") {
|
|
for (auto i = kv1.value.Begin(); i != kv1.value.End(); ++i) {
|
|
if (!i->IsObject() || i->MemberCount() != 1) {
|
|
clogger.error("check_CONTAINS received a list whose element is malformed");
|
|
return false;
|
|
}
|
|
const auto& el = *i->MemberBegin();
|
|
if (el.name == kv2.name && el.value == kv2.value) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with the NOT_CONTAINS relation
|
|
static bool check_NOT_CONTAINS(const rjson::value* v1, const rjson::value& v2, bool v1_from_query, bool v2_from_query) {
|
|
if (!v1) {
|
|
return false;
|
|
}
|
|
return !check_CONTAINS(v1, v2, v1_from_query, v2_from_query);
|
|
}
|
|
|
|
// Check if a JSON-encoded value equals any element of an array, which must have at least one element.
|
|
static bool check_IN(const rjson::value* val, const rjson::value& array) {
|
|
if (!array[0].IsObject() || array[0].MemberCount() != 1) {
|
|
throw api_error::validation(
|
|
format("IN operator encountered malformed AttributeValue: {}", array[0]));
|
|
}
|
|
const auto& type = array[0].MemberBegin()->name;
|
|
if (type != "S" && type != "N" && type != "B") {
|
|
throw api_error::validation(
|
|
"IN operator requires AttributeValueList elements to be of type String, Number, or Binary ");
|
|
}
|
|
if (!val) {
|
|
return false;
|
|
}
|
|
bool have_match = false;
|
|
for (const auto& elem : array.GetArray()) {
|
|
if (!elem.IsObject() || elem.MemberCount() != 1 || elem.MemberBegin()->name != type) {
|
|
throw api_error::validation(
|
|
"IN operator requires all AttributeValueList elements to have the same type ");
|
|
}
|
|
if (!have_match && *val == elem) {
|
|
// Can't return yet, must check types of all array elements. <sigh>
|
|
have_match = true;
|
|
}
|
|
}
|
|
return have_match;
|
|
}
|
|
|
|
// Another variant of check_IN, this one for ConditionExpression. It needs to
|
|
// check whether the first element in the given vector is equal to any of the
|
|
// others.
|
|
static bool check_IN(const std::vector<rjson::value>& array) {
|
|
const rjson::value* first = &array[0];
|
|
for (unsigned i = 1; i < array.size(); i++) {
|
|
if (check_EQ(first, array[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool check_NULL(const rjson::value* val) {
|
|
return val == nullptr;
|
|
}
|
|
|
|
static bool check_NOT_NULL(const rjson::value* val) {
|
|
return val != nullptr;
|
|
}
|
|
|
|
// Only types S, N or B (string, number or bytes) may be compared by the
|
|
// various comparison operators - lt, le, gt, ge, and between.
|
|
// Note that in particular, if the value is missing (v->IsNull()), this
|
|
// check returns false.
|
|
static bool check_comparable_type(const rjson::value& v) {
|
|
if (!v.IsObject() || v.MemberCount() != 1) {
|
|
return false;
|
|
}
|
|
const rjson::value& type = v.MemberBegin()->name;
|
|
return type == "S" || type == "N" || type == "B";
|
|
}
|
|
|
|
// Check if two JSON-encoded values match with cmp.
|
|
template <typename Comparator>
|
|
bool check_compare(const rjson::value* v1, const rjson::value& v2, const Comparator& cmp,
|
|
bool v1_from_query, bool v2_from_query) {
|
|
bool bad = false;
|
|
if (!v1 || !check_comparable_type(*v1)) {
|
|
if (v1_from_query) {
|
|
throw api_error::validation(format("{} allow only the types String, Number, or Binary", cmp.diagnostic));
|
|
}
|
|
bad = true;
|
|
}
|
|
if (!check_comparable_type(v2)) {
|
|
if (v2_from_query) {
|
|
throw api_error::validation(format("{} allow only the types String, Number, or Binary", cmp.diagnostic));
|
|
}
|
|
bad = true;
|
|
}
|
|
if (bad) {
|
|
return false;
|
|
}
|
|
const auto& kv1 = *v1->MemberBegin();
|
|
const auto& kv2 = *v2.MemberBegin();
|
|
if (kv1.name != kv2.name) {
|
|
return false;
|
|
}
|
|
if (kv1.name == "N") {
|
|
return cmp(unwrap_number(*v1, cmp.diagnostic), unwrap_number(v2, cmp.diagnostic));
|
|
}
|
|
if (kv1.name == "S") {
|
|
return cmp(std::string_view(kv1.value.GetString(), kv1.value.GetStringLength()),
|
|
std::string_view(kv2.value.GetString(), kv2.value.GetStringLength()));
|
|
}
|
|
if (kv1.name == "B") {
|
|
auto d_kv1 = unwrap_bytes(kv1.value, v1_from_query);
|
|
auto d_kv2 = unwrap_bytes(kv2.value, v2_from_query);
|
|
if(!d_kv1 || !d_kv2) {
|
|
return false;
|
|
}
|
|
return cmp(*d_kv1, *d_kv2);
|
|
}
|
|
// cannot reach here, as check_comparable_type() verifies the type is one
|
|
// of the above options.
|
|
return false;
|
|
}
|
|
|
|
struct cmp_lt {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs; }
|
|
// We cannot use the normal comparison operators like "<" on the bytes
|
|
// type, because they treat individual bytes as signed but we need to
|
|
// compare them as *unsigned*. So we need a specialization for bytes.
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) < 0; }
|
|
static constexpr const char* diagnostic = "LT operator";
|
|
};
|
|
|
|
struct cmp_le {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs <= rhs; }
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) <= 0; }
|
|
static constexpr const char* diagnostic = "LE operator";
|
|
};
|
|
|
|
struct cmp_ge {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs >= rhs; }
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) >= 0; }
|
|
static constexpr const char* diagnostic = "GE operator";
|
|
};
|
|
|
|
struct cmp_gt {
|
|
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs > rhs; }
|
|
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) > 0; }
|
|
static constexpr const char* diagnostic = "GT operator";
|
|
};
|
|
|
|
// True if v is between lb and ub, inclusive. Throws or returns false
|
|
// (depending on bounds_from_query parameter) if lb > ub.
|
|
template <typename T>
|
|
static bool check_BETWEEN(const T& v, const T& lb, const T& ub, bool bounds_from_query) {
|
|
if (cmp_lt()(ub, lb)) {
|
|
if (bounds_from_query) {
|
|
throw api_error::validation(
|
|
fmt::format("BETWEEN operator requires lower_bound <= upper_bound, but {} > {}", lb, ub));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return cmp_ge()(v, lb) && cmp_le()(v, ub);
|
|
}
|
|
|
|
static bool check_BETWEEN(const rjson::value* v, const rjson::value& lb, const rjson::value& ub,
|
|
bool v_from_query, bool lb_from_query, bool ub_from_query) {
|
|
if ((v && v_from_query && !check_comparable_type(*v)) ||
|
|
(lb_from_query && !check_comparable_type(lb)) ||
|
|
(ub_from_query && !check_comparable_type(ub))) {
|
|
throw api_error::validation("between allow only the types String, Number, or Binary");
|
|
|
|
}
|
|
if (!v || !v->IsObject() || v->MemberCount() != 1 ||
|
|
!lb.IsObject() || lb.MemberCount() != 1 ||
|
|
!ub.IsObject() || ub.MemberCount() != 1) {
|
|
return false;
|
|
}
|
|
|
|
const auto& kv_v = *v->MemberBegin();
|
|
const auto& kv_lb = *lb.MemberBegin();
|
|
const auto& kv_ub = *ub.MemberBegin();
|
|
bool bounds_from_query = lb_from_query && ub_from_query;
|
|
if (kv_lb.name != kv_ub.name) {
|
|
if (bounds_from_query) {
|
|
throw api_error::validation(
|
|
format("BETWEEN operator requires the same type for lower and upper bound; instead got {} and {}",
|
|
kv_lb.name, kv_ub.name));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
if (kv_v.name != kv_lb.name) { // Cannot compare different types, so v is NOT between lb and ub.
|
|
return false;
|
|
}
|
|
if (kv_v.name == "N") {
|
|
const char* diag = "BETWEEN operator";
|
|
return check_BETWEEN(unwrap_number(*v, diag), unwrap_number(lb, diag), unwrap_number(ub, diag), bounds_from_query);
|
|
}
|
|
if (kv_v.name == "S") {
|
|
return check_BETWEEN(std::string_view(kv_v.value.GetString(), kv_v.value.GetStringLength()),
|
|
std::string_view(kv_lb.value.GetString(), kv_lb.value.GetStringLength()),
|
|
std::string_view(kv_ub.value.GetString(), kv_ub.value.GetStringLength()),
|
|
bounds_from_query);
|
|
}
|
|
if (kv_v.name == "B") {
|
|
auto d_kv_v = unwrap_bytes(kv_v.value, v_from_query);
|
|
auto d_kv_lb = unwrap_bytes(kv_lb.value, lb_from_query);
|
|
auto d_kv_ub = unwrap_bytes(kv_ub.value, ub_from_query);
|
|
if(!d_kv_v || !d_kv_lb || !d_kv_ub) {
|
|
return false;
|
|
}
|
|
return check_BETWEEN(*d_kv_v, *d_kv_lb, *d_kv_ub, bounds_from_query);
|
|
}
|
|
if (v_from_query) {
|
|
throw api_error::validation(
|
|
format("BETWEEN operator requires AttributeValueList elements to be of type String, Number, or Binary; instead got {}",
|
|
kv_lb.name));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Verify one Expect condition on one attribute (whose content is "got")
|
|
// for the verify_expected() below.
|
|
// This function returns true or false depending on whether the condition
|
|
// succeeded - it does not throw ConditionalCheckFailedException.
|
|
// However, it may throw ValidationException on input validation errors.
|
|
static bool verify_expected_one(const rjson::value& condition, const rjson::value* got) {
|
|
const rjson::value* comparison_operator = rjson::find(condition, "ComparisonOperator");
|
|
const rjson::value* attribute_value_list = rjson::find(condition, "AttributeValueList");
|
|
const rjson::value* value = rjson::find(condition, "Value");
|
|
const rjson::value* exists = rjson::find(condition, "Exists");
|
|
// There are three types of conditions that Expected supports:
|
|
// A value, not-exists, and a comparison of some kind. Each allows
|
|
// and requires a different combinations of parameters in the request
|
|
if (value) {
|
|
if (exists && (!exists->IsBool() || exists->GetBool() != true)) {
|
|
throw api_error::validation("Cannot combine Value with Exists!=true");
|
|
}
|
|
if (comparison_operator) {
|
|
throw api_error::validation("Cannot combine Value with ComparisonOperator");
|
|
}
|
|
return check_EQ(got, *value);
|
|
} else if (exists) {
|
|
if (comparison_operator) {
|
|
throw api_error::validation("Cannot combine Exists with ComparisonOperator");
|
|
}
|
|
if (!exists->IsBool() || exists->GetBool() != false) {
|
|
throw api_error::validation("Exists!=false requires Value");
|
|
}
|
|
// Remember Exists=false, so we're checking that the attribute does *not* exist:
|
|
return !got;
|
|
} else {
|
|
if (!comparison_operator) {
|
|
throw api_error::validation("Missing ComparisonOperator, Value or Exists");
|
|
}
|
|
comparison_operator_type op = get_comparison_operator(*comparison_operator);
|
|
switch (op) {
|
|
case comparison_operator_type::EQ:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_EQ(got, (*attribute_value_list)[0]);
|
|
case comparison_operator_type::NE:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_NE(got, (*attribute_value_list)[0]);
|
|
case comparison_operator_type::LT:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_lt{}, false, true);
|
|
case comparison_operator_type::LE:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_le{}, false, true);
|
|
case comparison_operator_type::GT:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_gt{}, false, true);
|
|
case comparison_operator_type::GE:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_compare(got, (*attribute_value_list)[0], cmp_ge{}, false, true);
|
|
case comparison_operator_type::BEGINS_WITH:
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
return check_BEGINS_WITH(got, (*attribute_value_list)[0], false, true);
|
|
case comparison_operator_type::IN:
|
|
verify_operand_count(attribute_value_list, nonempty(), *comparison_operator);
|
|
return check_IN(got, *attribute_value_list);
|
|
case comparison_operator_type::IS_NULL:
|
|
verify_operand_count(attribute_value_list, empty(), *comparison_operator);
|
|
return check_NULL(got);
|
|
case comparison_operator_type::NOT_NULL:
|
|
verify_operand_count(attribute_value_list, empty(), *comparison_operator);
|
|
return check_NOT_NULL(got);
|
|
case comparison_operator_type::BETWEEN:
|
|
verify_operand_count(attribute_value_list, exact_size(2), *comparison_operator);
|
|
return check_BETWEEN(got, (*attribute_value_list)[0], (*attribute_value_list)[1],
|
|
false, true, true);
|
|
case comparison_operator_type::CONTAINS:
|
|
{
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
// Expected's "CONTAINS" has this artificial limitation.
|
|
// ConditionExpression's "contains()" does not...
|
|
const rjson::value& arg = (*attribute_value_list)[0];
|
|
const auto& argtype = (*arg.MemberBegin()).name;
|
|
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
|
throw api_error::validation(
|
|
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
|
"got {} instead", argtype));
|
|
}
|
|
return check_CONTAINS(got, arg, false, true);
|
|
}
|
|
case comparison_operator_type::NOT_CONTAINS:
|
|
{
|
|
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
|
// Expected's "NOT_CONTAINS" has this artificial limitation.
|
|
// ConditionExpression's "contains()" does not...
|
|
const rjson::value& arg = (*attribute_value_list)[0];
|
|
const auto& argtype = (*arg.MemberBegin()).name;
|
|
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
|
throw api_error::validation(
|
|
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
|
"got {} instead", argtype));
|
|
}
|
|
return check_NOT_CONTAINS(got, arg, false, true);
|
|
}
|
|
}
|
|
throw std::logic_error(format("Internal error: corrupted operator enum: {}", int(op)));
|
|
}
|
|
}
|
|
|
|
conditional_operator_type get_conditional_operator(const rjson::value& req) {
|
|
const rjson::value* conditional_operator = rjson::find(req, "ConditionalOperator");
|
|
if (!conditional_operator) {
|
|
return conditional_operator_type::MISSING;
|
|
}
|
|
if (!conditional_operator->IsString()) {
|
|
throw api_error::validation("'ConditionalOperator' parameter, if given, must be a string");
|
|
}
|
|
auto s = rjson::to_string_view(*conditional_operator);
|
|
if (s == "AND") {
|
|
return conditional_operator_type::AND;
|
|
} else if (s == "OR") {
|
|
return conditional_operator_type::OR;
|
|
} else {
|
|
throw api_error::validation(
|
|
fmt::format("'ConditionalOperator' parameter must be AND, OR or missing. Found {}.", s));
|
|
}
|
|
}
|
|
|
|
// Check if the existing values of the item (previous_item) match the
|
|
// conditions given by the Expected and ConditionalOperator parameters
|
|
// (if they exist) in the request (an UpdateItem, PutItem or DeleteItem).
|
|
// This function can throw an ValidationException API error if there
|
|
// are errors in the format of the condition itself.
|
|
bool verify_expected(const rjson::value& req, const rjson::value* previous_item) {
|
|
const rjson::value* expected = rjson::find(req, "Expected");
|
|
auto conditional_operator = get_conditional_operator(req);
|
|
if (conditional_operator != conditional_operator_type::MISSING &&
|
|
(!expected || (expected->IsObject() && expected->GetObject().ObjectEmpty()))) {
|
|
throw api_error::validation("'ConditionalOperator' parameter cannot be specified for missing or empty Expression");
|
|
}
|
|
if (!expected) {
|
|
return true;
|
|
}
|
|
if (!expected->IsObject()) {
|
|
throw api_error::validation("'Expected' parameter, if given, must be an object");
|
|
}
|
|
bool require_all = conditional_operator != conditional_operator_type::OR;
|
|
return verify_condition(*expected, require_all, previous_item);
|
|
}
|
|
|
|
bool verify_condition(const rjson::value& condition, bool require_all, const rjson::value* previous_item) {
|
|
for (auto it = condition.MemberBegin(); it != condition.MemberEnd(); ++it) {
|
|
const rjson::value* got = nullptr;
|
|
if (previous_item) {
|
|
got = rjson::find(*previous_item, rjson::to_string_view(it->name));
|
|
}
|
|
bool success = verify_expected_one(it->value, got);
|
|
if (success && !require_all) {
|
|
// When !require_all, one success is enough!
|
|
return true;
|
|
} else if (!success && require_all) {
|
|
// When require_all, one failure is enough!
|
|
return false;
|
|
}
|
|
}
|
|
// If we got here and require_all, none of the checks failed, so succeed.
|
|
// If we got here and !require_all, all of the checks failed, so fail.
|
|
return require_all;
|
|
}
|
|
|
|
static bool calculate_primitive_condition(const parsed::primitive_condition& cond,
|
|
const rjson::value* previous_item) {
|
|
std::vector<rjson::value> calculated_values;
|
|
calculated_values.reserve(cond._values.size());
|
|
for (const parsed::value& v : cond._values) {
|
|
calculated_values.push_back(calculate_value(v,
|
|
cond._op == parsed::primitive_condition::type::VALUE ?
|
|
calculate_value_caller::ConditionExpressionAlone :
|
|
calculate_value_caller::ConditionExpression,
|
|
previous_item));
|
|
}
|
|
switch (cond._op) {
|
|
case parsed::primitive_condition::type::BETWEEN:
|
|
if (calculated_values.size() != 3) {
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error(format("Wrong number of values {} in BETWEEN primitive_condition", cond._values.size()));
|
|
}
|
|
return check_BETWEEN(&calculated_values[0], calculated_values[1], calculated_values[2],
|
|
cond._values[0].is_constant(), cond._values[1].is_constant(), cond._values[2].is_constant());
|
|
case parsed::primitive_condition::type::IN:
|
|
return check_IN(calculated_values);
|
|
case parsed::primitive_condition::type::VALUE:
|
|
if (calculated_values.size() != 1) {
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error(format("Unexpected values in primitive_condition", cond._values.size()));
|
|
}
|
|
// Unwrap the boolean wrapped as the value (if it is a boolean)
|
|
if (calculated_values[0].IsObject() && calculated_values[0].MemberCount() == 1) {
|
|
auto it = calculated_values[0].MemberBegin();
|
|
if (it->name == "BOOL" && it->value.IsBool()) {
|
|
return it->value.GetBool();
|
|
}
|
|
}
|
|
throw api_error::validation(
|
|
format("ConditionExpression: condition results in a non-boolean value: {}",
|
|
calculated_values[0]));
|
|
default:
|
|
// All the rest of the operators have exactly two parameters (and unless
|
|
// we have a bug in the parser, that's what we have in the parsed object:
|
|
if (calculated_values.size() != 2) {
|
|
throw std::logic_error(format("Wrong number of values {} in primitive_condition object", cond._values.size()));
|
|
}
|
|
}
|
|
switch (cond._op) {
|
|
case parsed::primitive_condition::type::EQ:
|
|
return check_EQ(&calculated_values[0], calculated_values[1]);
|
|
case parsed::primitive_condition::type::NE:
|
|
return check_NE(&calculated_values[0], calculated_values[1]);
|
|
case parsed::primitive_condition::type::GT:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_gt{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
case parsed::primitive_condition::type::GE:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_ge{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
case parsed::primitive_condition::type::LT:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_lt{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
case parsed::primitive_condition::type::LE:
|
|
return check_compare(&calculated_values[0], calculated_values[1], cmp_le{},
|
|
cond._values[0].is_constant(), cond._values[1].is_constant());
|
|
default:
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error(format("Unknown type {} in primitive_condition object", (int)(cond._op)));
|
|
}
|
|
}
|
|
|
|
// Check if the existing values of the item (previous_item) match the
|
|
// conditions given by the given parsed ConditionExpression.
|
|
bool verify_condition_expression(
|
|
const parsed::condition_expression& condition_expression,
|
|
const rjson::value* previous_item) {
|
|
if (condition_expression.empty()) {
|
|
return true;
|
|
}
|
|
bool ret = std::visit(overloaded_functor {
|
|
[&] (const parsed::primitive_condition& cond) -> bool {
|
|
return calculate_primitive_condition(cond, previous_item);
|
|
},
|
|
[&] (const parsed::condition_expression::condition_list& list) -> bool {
|
|
auto verify_condition = [&] (const parsed::condition_expression& e) {
|
|
return verify_condition_expression(e, previous_item);
|
|
};
|
|
switch (list.op) {
|
|
case '&':
|
|
return std::ranges::all_of(list.conditions, verify_condition);
|
|
case '|':
|
|
return std::ranges::any_of(list.conditions, verify_condition);
|
|
default:
|
|
// Shouldn't happen unless we have a bug in the parser
|
|
throw std::logic_error("bad operator in condition_list");
|
|
}
|
|
}
|
|
}, condition_expression._expression);
|
|
return condition_expression._negated ? !ret : ret;
|
|
}
|
|
|
|
}
|