10 changed files with 1066 additions and 4 deletions
@ -0,0 +1,141 @@ |
|||||||
|
#include "Specification.h" |
||||||
|
#include <stdexcept> |
||||||
|
|
||||||
|
namespace nxl::helpers { |
||||||
|
|
||||||
|
// Condition render implementation
|
||||||
|
std::string Condition::render(const Renderer& renderer) const |
||||||
|
{ |
||||||
|
std::string opStr; |
||||||
|
switch (op) { |
||||||
|
case ComparisonOp::EQ: |
||||||
|
opStr = renderer.opEq; |
||||||
|
break; |
||||||
|
case ComparisonOp::NE: |
||||||
|
opStr = renderer.opNe; |
||||||
|
break; |
||||||
|
case ComparisonOp::LT: |
||||||
|
opStr = renderer.opLt; |
||||||
|
break; |
||||||
|
case ComparisonOp::LE: |
||||||
|
opStr = renderer.opLe; |
||||||
|
break; |
||||||
|
case ComparisonOp::GT: |
||||||
|
opStr = renderer.opGt; |
||||||
|
break; |
||||||
|
case ComparisonOp::GE: |
||||||
|
opStr = renderer.opGe; |
||||||
|
break; |
||||||
|
case ComparisonOp::LIKE: |
||||||
|
opStr = renderer.opLike; |
||||||
|
break; |
||||||
|
case ComparisonOp::IS_NULL: |
||||||
|
opStr = renderer.opIsNull; |
||||||
|
break; |
||||||
|
case ComparisonOp::IS_NOT_NULL: |
||||||
|
opStr = renderer.opIsNotNull; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
std::string result = field + " " + opStr; |
||||||
|
if (value) { |
||||||
|
result += " " + renderer.formatValue(*value); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
// ConditionGroup render implementation
|
||||||
|
std::string ConditionGroup::render(const Renderer& renderer) const |
||||||
|
{ |
||||||
|
if (children.empty()) |
||||||
|
return ""; |
||||||
|
|
||||||
|
std::string opStr = (op == LogicalOp::AND) ? renderer.opAnd : renderer.opOr; |
||||||
|
std::string result = renderer.groupStart; |
||||||
|
|
||||||
|
bool first = true; |
||||||
|
for (const auto& child : children) { |
||||||
|
if (!first) |
||||||
|
result += " " + opStr + " "; |
||||||
|
result += child->render(renderer); |
||||||
|
first = false; |
||||||
|
} |
||||||
|
|
||||||
|
result += renderer.groupEnd; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
// SpecificationBuilder implementation
|
||||||
|
SpecificationBuilder::SpecificationBuilder() |
||||||
|
{ |
||||||
|
root = std::make_unique<ConditionGroup>(LogicalOp::AND); |
||||||
|
current = root.get(); |
||||||
|
} |
||||||
|
|
||||||
|
SpecificationBuilder& SpecificationBuilder::field(const std::string& name) |
||||||
|
{ |
||||||
|
lastField = name; |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
SpecificationBuilder& SpecificationBuilder::like(std::string pattern) |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::LIKE, std::move(pattern)); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
SpecificationBuilder& SpecificationBuilder::isNull() |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::IS_NULL); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
SpecificationBuilder& SpecificationBuilder::isNotNull() |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::IS_NOT_NULL); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
SpecificationBuilder& SpecificationBuilder::andGroup() |
||||||
|
{ |
||||||
|
startGroup(LogicalOp::AND); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
SpecificationBuilder& SpecificationBuilder::orGroup() |
||||||
|
{ |
||||||
|
startGroup(LogicalOp::OR); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
SpecificationBuilder& SpecificationBuilder::endGroup() |
||||||
|
{ |
||||||
|
if (stack.empty()) |
||||||
|
return *this; |
||||||
|
current = stack.back(); |
||||||
|
stack.pop_back(); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
std::unique_ptr<ISpecificationExpr> SpecificationBuilder::build() |
||||||
|
{ |
||||||
|
return std::move(root); |
||||||
|
} |
||||||
|
|
||||||
|
void SpecificationBuilder::startGroup(LogicalOp opType) |
||||||
|
{ |
||||||
|
auto newGroup = std::make_unique<ConditionGroup>(opType); |
||||||
|
auto* newGroupPtr = newGroup.get(); |
||||||
|
current->add(std::move(newGroup)); |
||||||
|
|
||||||
|
stack.push_back(current); |
||||||
|
current = newGroupPtr; |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function
|
||||||
|
SpecificationBuilder makeSpecification() |
||||||
|
{ |
||||||
|
return SpecificationBuilder(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::helpers
|
||||||
@ -0,0 +1,198 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
#include <memory> |
||||||
|
#include <variant> |
||||||
|
#include <optional> |
||||||
|
#include <functional> |
||||||
|
#include <any> |
||||||
|
#include <stdexcept> |
||||||
|
|
||||||
|
namespace nxl::helpers { |
||||||
|
|
||||||
|
class ISpecificationExpr; |
||||||
|
class Condition; |
||||||
|
class ConditionGroup; |
||||||
|
|
||||||
|
enum class ComparisonOp { EQ, NE, LT, LE, GT, GE, LIKE, IS_NULL, IS_NOT_NULL }; |
||||||
|
enum class LogicalOp { AND, OR }; |
||||||
|
|
||||||
|
class ISpecificationExpr |
||||||
|
{ |
||||||
|
public: |
||||||
|
virtual ~ISpecificationExpr() = default; |
||||||
|
virtual std::string render(const struct Renderer& renderer) const = 0; |
||||||
|
}; |
||||||
|
|
||||||
|
class ConditionValue |
||||||
|
{ |
||||||
|
public: |
||||||
|
template <typename T> ConditionValue(T value) : value_(std::move(value)) {} |
||||||
|
|
||||||
|
const std::any& get() const { return value_; } |
||||||
|
|
||||||
|
template <typename T> const T& as() const |
||||||
|
{ |
||||||
|
return std::any_cast<const T&>(value_); |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> bool is() const { return value_.type() == typeid(T); } |
||||||
|
|
||||||
|
private: |
||||||
|
std::any value_; |
||||||
|
}; |
||||||
|
|
||||||
|
class Condition : public ISpecificationExpr |
||||||
|
{ |
||||||
|
std::string field; |
||||||
|
ComparisonOp op; |
||||||
|
std::optional<ConditionValue> value; // Optional for operators like IS_NULL
|
||||||
|
|
||||||
|
public: |
||||||
|
Condition(std::string f, ComparisonOp o, std::optional<ConditionValue> v = {}) |
||||||
|
: field(std::move(f)), op(o), value(std::move(v)) |
||||||
|
{} |
||||||
|
|
||||||
|
const std::string& getField() const { return field; } |
||||||
|
ComparisonOp getOp() const { return op; } |
||||||
|
const std::optional<ConditionValue>& getValue() const { return value; } |
||||||
|
|
||||||
|
std::string render(const struct Renderer& renderer) const override; |
||||||
|
}; |
||||||
|
|
||||||
|
// Logical group of conditions
|
||||||
|
class ConditionGroup : public ISpecificationExpr |
||||||
|
{ |
||||||
|
LogicalOp op; |
||||||
|
std::vector<std::unique_ptr<ISpecificationExpr>> children; |
||||||
|
|
||||||
|
public: |
||||||
|
ConditionGroup(LogicalOp o) : op(o) {} |
||||||
|
|
||||||
|
void add(std::unique_ptr<ISpecificationExpr> expr) |
||||||
|
{ |
||||||
|
children.push_back(std::move(expr)); |
||||||
|
} |
||||||
|
|
||||||
|
LogicalOp getOp() const { return op; } |
||||||
|
const std::vector<std::unique_ptr<ISpecificationExpr>>& getChildren() const |
||||||
|
{ |
||||||
|
return children; |
||||||
|
} |
||||||
|
|
||||||
|
std::string render(const struct Renderer& renderer) const override; |
||||||
|
}; |
||||||
|
|
||||||
|
// Renderer interface - defines how to render operators and grouping
|
||||||
|
struct Renderer |
||||||
|
{ |
||||||
|
std::string opEq; |
||||||
|
std::string opNe; |
||||||
|
std::string opLt; |
||||||
|
std::string opLe; |
||||||
|
std::string opGt; |
||||||
|
std::string opGe; |
||||||
|
std::string opLike; |
||||||
|
std::string opIsNull; |
||||||
|
std::string opIsNotNull; |
||||||
|
|
||||||
|
std::string opAnd; |
||||||
|
std::string opOr; |
||||||
|
|
||||||
|
std::string groupStart; |
||||||
|
std::string groupEnd; |
||||||
|
|
||||||
|
// Value formatting function
|
||||||
|
std::function<std::string(const ConditionValue&)> formatValue; |
||||||
|
}; |
||||||
|
|
||||||
|
// Fluent builder for specifications
|
||||||
|
class SpecificationBuilder |
||||||
|
{ |
||||||
|
std::unique_ptr<ConditionGroup> root; |
||||||
|
ConditionGroup* current; |
||||||
|
std::vector<ConditionGroup*> stack; |
||||||
|
std::string lastField; |
||||||
|
|
||||||
|
public: |
||||||
|
SpecificationBuilder(); |
||||||
|
|
||||||
|
// Set the field for the next condition
|
||||||
|
SpecificationBuilder& field(const std::string& name); |
||||||
|
|
||||||
|
template <typename T> SpecificationBuilder& equals(T value) |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::EQ, std::move(value)); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> SpecificationBuilder& notEquals(T value) |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::NE, std::move(value)); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> SpecificationBuilder& lessThan(T value) |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::LT, std::move(value)); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> SpecificationBuilder& lessOrEqual(T value) |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::LE, std::move(value)); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> SpecificationBuilder& greaterThan(T value) |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::GT, std::move(value)); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
|
||||||
|
template <typename T> SpecificationBuilder& greaterOrEqual(T value) |
||||||
|
{ |
||||||
|
addCondition(ComparisonOp::GE, std::move(value)); |
||||||
|
return *this; |
||||||
|
} |
||||||
|
SpecificationBuilder& like(std::string pattern); |
||||||
|
SpecificationBuilder& isNull(); |
||||||
|
SpecificationBuilder& isNotNull(); |
||||||
|
|
||||||
|
SpecificationBuilder& andGroup(); |
||||||
|
SpecificationBuilder& orGroup(); |
||||||
|
SpecificationBuilder& endGroup(); |
||||||
|
|
||||||
|
std::unique_ptr<ISpecificationExpr> build(); |
||||||
|
|
||||||
|
private: |
||||||
|
template <typename T> void addCondition(ComparisonOp op, T value) |
||||||
|
{ |
||||||
|
if (lastField.empty()) { |
||||||
|
throw std::runtime_error("No field specified for condition"); |
||||||
|
} |
||||||
|
|
||||||
|
auto condition = std::make_unique<Condition>( |
||||||
|
lastField, op, ConditionValue(std::move(value))); |
||||||
|
current->add(std::move(condition)); |
||||||
|
lastField.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
void addCondition(ComparisonOp op) |
||||||
|
{ |
||||||
|
if (lastField.empty()) { |
||||||
|
throw std::runtime_error("No field specified for condition"); |
||||||
|
} |
||||||
|
|
||||||
|
auto condition = std::make_unique<Condition>(lastField, op, std::nullopt); |
||||||
|
current->add(std::move(condition)); |
||||||
|
lastField.clear(); |
||||||
|
} |
||||||
|
void startGroup(LogicalOp opType); |
||||||
|
}; |
||||||
|
|
||||||
|
// Helper function to create a specification builder
|
||||||
|
SpecificationBuilder makeSpecification(); |
||||||
|
|
||||||
|
} // namespace nxl::helpers
|
||||||
@ -1,18 +1,31 @@ |
|||||||
#pragma once |
#pragma once |
||||||
|
|
||||||
#include "domain/entities/Item.h" |
#include "domain/entities/Item.h" |
||||||
|
#include "domain/helpers/Specification.h" |
||||||
#include <chrono> |
#include <chrono> |
||||||
|
|
||||||
namespace nxl::autostore::domain { |
namespace nxl::autostore::domain { |
||||||
|
|
||||||
|
using ItemExpirationSpec = std::unique_ptr<nxl::helpers::ISpecificationExpr>; |
||||||
|
|
||||||
class ItemExpirationPolicy |
class ItemExpirationPolicy |
||||||
{ |
{ |
||||||
public: |
public: |
||||||
bool isExpired(const Item& item, |
using TimePoint = std::chrono::system_clock::time_point; |
||||||
const std::chrono::system_clock::time_point& currentTime) const |
constexpr static const char* FIELD_EXP_DATE{"expiration_date"}; |
||||||
|
|
||||||
|
bool isExpired(const Item& item, const TimePoint& currentTime) const |
||||||
{ |
{ |
||||||
return item.expirationDate <= currentTime; |
return item.expirationDate <= currentTime; |
||||||
} |
} |
||||||
|
|
||||||
|
ItemExpirationSpec getExpiredSpecification(const TimePoint& currentTime) const |
||||||
|
{ |
||||||
|
return nxl::helpers::SpecificationBuilder() |
||||||
|
.field(FIELD_EXP_DATE) |
||||||
|
.lessOrEqual(currentTime) |
||||||
|
.build(); |
||||||
|
} |
||||||
}; |
}; |
||||||
|
|
||||||
} // namespace nxl::autostore::domain
|
} // namespace nxl::autostore::domain
|
||||||
@ -0,0 +1,692 @@ |
|||||||
|
#include "domain/helpers/Specification.h" |
||||||
|
#include <catch2/catch_test_macros.hpp> |
||||||
|
#include <catch2/matchers/catch_matchers_string.hpp> |
||||||
|
#include <trompeloeil.hpp> |
||||||
|
#include <memory> |
||||||
|
#include <stdexcept> |
||||||
|
#include <chrono> |
||||||
|
|
||||||
|
using trompeloeil::_; |
||||||
|
using namespace nxl::helpers; |
||||||
|
|
||||||
|
TEST_CASE("Specification Unit Tests", "[unit][Specification]") |
||||||
|
{ |
||||||
|
const Renderer TEST_RENDERER{ |
||||||
|
.opEq = "=", |
||||||
|
.opNe = "!=", |
||||||
|
.opLt = "<", |
||||||
|
.opLe = "<=", |
||||||
|
.opGt = ">", |
||||||
|
.opGe = ">=", |
||||||
|
.opLike = "LIKE", |
||||||
|
.opIsNull = "IS NULL", |
||||||
|
.opIsNotNull = "IS NOT NULL", |
||||||
|
.opAnd = "AND", |
||||||
|
.opOr = "OR", |
||||||
|
.groupStart = "(", |
||||||
|
.groupEnd = ")", |
||||||
|
.formatValue = [](const ConditionValue& v) -> std::string { |
||||||
|
if (v.is<std::string>()) { |
||||||
|
return "'" + v.as<std::string>() + "'"; |
||||||
|
} else if (v.is<const char*>()) { |
||||||
|
return "'" + std::string(v.as<const char*>()) + "'"; |
||||||
|
} else if (v.is<bool>()) { |
||||||
|
return v.as<bool>() ? "TRUE" : "FALSE"; |
||||||
|
} else if (v.is<int>()) { |
||||||
|
return std::to_string(v.as<int>()); |
||||||
|
} else if (v.is<double>()) { |
||||||
|
return std::to_string(v.as<double>()); |
||||||
|
} else if (v.is<std::chrono::system_clock::time_point>()) { |
||||||
|
auto time = v.as<std::chrono::system_clock::time_point>(); |
||||||
|
auto time_t = std::chrono::system_clock::to_time_t(time); |
||||||
|
std::string time_str = std::ctime(&time_t); |
||||||
|
if (!time_str.empty() && time_str.back() == '\n') { |
||||||
|
time_str.pop_back(); |
||||||
|
} |
||||||
|
return "'" + time_str + "'"; |
||||||
|
} |
||||||
|
return "'unknown'"; |
||||||
|
}}; |
||||||
|
|
||||||
|
SECTION("when single equals string condition is added then proper operator " |
||||||
|
"is rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").equals("foo").build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F = 'foo')"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when condition with chrono time_point is used then it renders correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto now = std::chrono::system_clock::now(); |
||||||
|
auto spec = makeSpecification().field("timestamp").greaterThan(now).build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result.find("timestamp >") != std::string::npos); |
||||||
|
REQUIRE(result.find("'") != std::string::npos); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when single not equals string condition is added then proper " |
||||||
|
"operator is rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").notEquals("foo").build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F != 'foo')"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when single less than condition is added then proper operator is rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").lessThan(42).build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F < 42)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when single less than or equal condition is added then proper " |
||||||
|
"operator is rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").lessOrEqual(42).build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F <= 42)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when single greater than condition is added then proper operator is " |
||||||
|
"rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").greaterThan(42).build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F > 42)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when single greater than or equal condition is added then proper " |
||||||
|
"operator is rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").greaterOrEqual(42).build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F >= 42)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when single like condition is added then proper operator is rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").like("%foo%").build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F LIKE '%foo%')"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when single is null condition is added then proper operator is rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").isNull().build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F IS NULL)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when single is not null condition is added then proper operator is " |
||||||
|
"rendered") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("F").isNotNull().build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(F IS NOT NULL)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when condition with double value is used then it renders correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("price").equals(19.99).build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(price = 19.990000)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when condition with boolean value is used then it renders correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("active").equals(true).build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(active = TRUE)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when condition with const char* value is used then it renders correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification().field("name").equals("test").build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(name = 'test')"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when multiple AND conditions are added then they are rendered with " |
||||||
|
"AND operator") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("name") |
||||||
|
.equals("John") |
||||||
|
.field("age") |
||||||
|
.greaterThan(30) |
||||||
|
.field("active") |
||||||
|
.equals(true) |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(name = 'John' AND age > 30 AND active = TRUE)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when AND group is created then conditions are grouped properly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("name") |
||||||
|
.equals("John") |
||||||
|
.andGroup() |
||||||
|
.field("age") |
||||||
|
.greaterThan(30) |
||||||
|
.field("active") |
||||||
|
.equals(true) |
||||||
|
.endGroup() |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(name = 'John' AND (age > 30 AND active = TRUE))"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when OR group is created then conditions are grouped with OR operator") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.orGroup() |
||||||
|
.field("name") |
||||||
|
.equals("John") |
||||||
|
.field("age") |
||||||
|
.greaterThan(30) |
||||||
|
.field("active") |
||||||
|
.equals(true) |
||||||
|
.endGroup() |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "((name = 'John' OR age > 30 OR active = TRUE))"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when nested groups are created then they are rendered properly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("category") |
||||||
|
.equals("electronics") |
||||||
|
.andGroup() |
||||||
|
.field("price") |
||||||
|
.lessThan(1000) |
||||||
|
.orGroup() |
||||||
|
.field("brand") |
||||||
|
.equals("Sony") |
||||||
|
.field("brand") |
||||||
|
.equals("Samsung") |
||||||
|
.endGroup() |
||||||
|
.endGroup() |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE( |
||||||
|
result |
||||||
|
== "(category = 'electronics' AND (price < 1000 AND (brand = 'Sony' " |
||||||
|
"OR brand = 'Samsung')))"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when complex specification with multiple nested groups is created " |
||||||
|
"then it renders correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.orGroup() |
||||||
|
.andGroup() |
||||||
|
.field("type") |
||||||
|
.equals("food") |
||||||
|
.field("expiration_date") |
||||||
|
.lessOrEqual("2023-01-01") |
||||||
|
.endGroup() |
||||||
|
.andGroup() |
||||||
|
.field("type") |
||||||
|
.equals("non-food") |
||||||
|
.field("expiration_date") |
||||||
|
.lessOrEqual("2023-03-01") |
||||||
|
.endGroup() |
||||||
|
.endGroup() |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result |
||||||
|
== "(((type = 'food' AND expiration_date <= '2023-01-01') OR (type " |
||||||
|
"= 'non-food' AND expiration_date <= '2023-03-01')))"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when empty group is created then it renders as empty") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("name") |
||||||
|
.equals("John") |
||||||
|
.andGroup() |
||||||
|
.endGroup() |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(name = 'John' AND )"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when endGroup is called without matching startGroup then it should " |
||||||
|
"handle gracefully") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = |
||||||
|
makeSpecification().field("name").equals("John").endGroup().build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(name = 'John')"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when condition is added without field then exception is thrown") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto builder = makeSpecification(); |
||||||
|
|
||||||
|
// When/Then
|
||||||
|
REQUIRE_THROWS_AS(builder.equals("value"), std::runtime_error); |
||||||
|
REQUIRE_THROWS_WITH(builder.equals("value"), |
||||||
|
"No field specified for condition"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when field is called multiple times then last field is used") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("field1") |
||||||
|
.field("field2") |
||||||
|
.equals("value") |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(field2 = 'value')"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when multiple conditions with same field are added then they are " |
||||||
|
"rendered correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("age") |
||||||
|
.greaterThan(18) |
||||||
|
.field("age") |
||||||
|
.lessThan(65) |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "(age > 18 AND age < 65)"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when specification has only conditions with no explicit grouping " |
||||||
|
"then AND is used") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("field1") |
||||||
|
.equals("value1") |
||||||
|
.field("field2") |
||||||
|
.equals("value2") |
||||||
|
.field("field3") |
||||||
|
.equals("value3") |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE( |
||||||
|
result |
||||||
|
== "(field1 = 'value1' AND field2 = 'value2' AND field3 = 'value3')"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when mixed data types are used then they render correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto now = std::chrono::system_clock::now(); |
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("name") |
||||||
|
.equals("John") |
||||||
|
.field("age") |
||||||
|
.greaterThan(30) |
||||||
|
.field("salary") |
||||||
|
.greaterOrEqual(50000.50) |
||||||
|
.field("active") |
||||||
|
.equals(true) |
||||||
|
.field("created_at") |
||||||
|
.lessThan(now) |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result.find("(name = 'John' AND age > 30 AND salary >= " |
||||||
|
"50000.500000 AND active = TRUE AND created_at < '") |
||||||
|
!= std::string::npos); |
||||||
|
REQUIRE(result.find("'") != std::string::npos); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when ConditionValue is created with string then it stores and " |
||||||
|
"retrieves correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
ConditionValue value(std::string("test")); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(value.is<std::string>()); |
||||||
|
REQUIRE_FALSE(value.is<int>()); |
||||||
|
REQUIRE(value.as<std::string>() == "test"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when ConditionValue is created with int then it stores and " |
||||||
|
"retrieves correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
ConditionValue value(42); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(value.is<int>()); |
||||||
|
REQUIRE_FALSE(value.is<std::string>()); |
||||||
|
REQUIRE(value.as<int>() == 42); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when ConditionValue is created with double then it stores and " |
||||||
|
"retrieves correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
ConditionValue value(3.14); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(value.is<double>()); |
||||||
|
REQUIRE_FALSE(value.is<int>()); |
||||||
|
REQUIRE(value.as<double>() == 3.14); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when ConditionValue is created with bool then it stores and " |
||||||
|
"retrieves correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
ConditionValue value(true); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(value.is<bool>()); |
||||||
|
REQUIRE_FALSE(value.is<int>()); |
||||||
|
REQUIRE(value.as<bool>() == true); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when ConditionValue is created with time_point then it stores and " |
||||||
|
"retrieves correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto now = std::chrono::system_clock::now(); |
||||||
|
ConditionValue value(now); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(value.is<std::chrono::system_clock::time_point>()); |
||||||
|
REQUIRE_FALSE(value.is<int>()); |
||||||
|
REQUIRE(value.as<std::chrono::system_clock::time_point>() == now); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when renderer has custom formatValue function then it is used correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
Renderer customRenderer{ |
||||||
|
.opEq = "=", |
||||||
|
.opNe = "!=", |
||||||
|
.opLt = "<", |
||||||
|
.opLe = "<=", |
||||||
|
.opGt = ">", |
||||||
|
.opGe = ">=", |
||||||
|
.opLike = "LIKE", |
||||||
|
.opIsNull = "IS NULL", |
||||||
|
.opIsNotNull = "IS NOT NULL", |
||||||
|
.opAnd = "AND", |
||||||
|
.opOr = "OR", |
||||||
|
.groupStart = "[", |
||||||
|
.groupEnd = "]", |
||||||
|
.formatValue = [](const ConditionValue& v) -> std::string { |
||||||
|
if (v.is<std::string>()) { |
||||||
|
return "\"" + v.as<std::string>() + "\""; |
||||||
|
} else if (v.is<const char*>()) { |
||||||
|
return "\"" + std::string(v.as<const char*>()) + "\""; |
||||||
|
} else if (v.is<int>()) { |
||||||
|
return "INT:" + std::to_string(v.as<int>()); |
||||||
|
} |
||||||
|
return "CUSTOM"; |
||||||
|
}}; |
||||||
|
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("name") |
||||||
|
.equals(std::string("John")) |
||||||
|
.field("age") |
||||||
|
.greaterThan(30) |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(customRenderer); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "[name = \"John\" AND age > INT:30]"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when renderer has custom grouping symbols then they are used correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
Renderer customRenderer{ |
||||||
|
.opEq = "=", |
||||||
|
.opNe = "!=", |
||||||
|
.opLt = "<", |
||||||
|
.opLe = "<=", |
||||||
|
.opGt = ">", |
||||||
|
.opGe = ">=", |
||||||
|
.opLike = "LIKE", |
||||||
|
.opIsNull = "IS NULL", |
||||||
|
.opIsNotNull = "IS NOT NULL", |
||||||
|
.opAnd = "AND", |
||||||
|
.opOr = "OR", |
||||||
|
.groupStart = "{", |
||||||
|
.groupEnd = "}", |
||||||
|
.formatValue = [](const ConditionValue& v) -> std::string { |
||||||
|
if (v.is<std::string>()) { |
||||||
|
return "'" + v.as<std::string>() + "'"; |
||||||
|
} else if (v.is<const char*>()) { |
||||||
|
return "'" + std::string(v.as<const char*>()) + "'"; |
||||||
|
} else if (v.is<int>()) { |
||||||
|
return std::to_string(v.as<int>()); |
||||||
|
} |
||||||
|
return "'unknown'"; |
||||||
|
}}; |
||||||
|
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.field("name") |
||||||
|
.equals(std::string("John")) |
||||||
|
.andGroup() |
||||||
|
.field("age") |
||||||
|
.greaterThan(30) |
||||||
|
.endGroup() |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(customRenderer); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "{name = 'John' AND {age > 30}}"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when renderer has custom logical operators then they are used correctly") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
Renderer customRenderer{ |
||||||
|
.opEq = "=", |
||||||
|
.opNe = "!=", |
||||||
|
.opLt = "<", |
||||||
|
.opLe = "<=", |
||||||
|
.opGt = ">", |
||||||
|
.opGe = ">=", |
||||||
|
.opLike = "LIKE", |
||||||
|
.opIsNull = "IS NULL", |
||||||
|
.opIsNotNull = "IS NOT NULL", |
||||||
|
.opAnd = "&&", |
||||||
|
.opOr = "||", |
||||||
|
.groupStart = "(", |
||||||
|
.groupEnd = ")", |
||||||
|
.formatValue = [](const ConditionValue& v) -> std::string { |
||||||
|
if (v.is<std::string>()) { |
||||||
|
return "'" + v.as<std::string>() + "'"; |
||||||
|
} else if (v.is<const char*>()) { |
||||||
|
return "'" + std::string(v.as<const char*>()) + "'"; |
||||||
|
} else if (v.is<int>()) { |
||||||
|
return std::to_string(v.as<int>()); |
||||||
|
} else if (v.is<bool>()) { |
||||||
|
return v.as<bool>() ? "TRUE" : "FALSE"; |
||||||
|
} |
||||||
|
return "'unknown'"; |
||||||
|
}}; |
||||||
|
|
||||||
|
auto spec = makeSpecification() |
||||||
|
.orGroup() |
||||||
|
.field("name") |
||||||
|
.equals(std::string("John")) |
||||||
|
.orGroup() |
||||||
|
.field("age") |
||||||
|
.greaterThan(30) |
||||||
|
.field("active") |
||||||
|
.equals(true) |
||||||
|
.endGroup() |
||||||
|
.endGroup() |
||||||
|
.build(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = spec->render(customRenderer); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == "((name = 'John' || (age > 30 || active = TRUE)))"); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION( |
||||||
|
"when ConditionGroup has no children then render returns empty string") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
ConditionGroup group(LogicalOp::AND); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto result = group.render(TEST_RENDERER); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(result == ""); |
||||||
|
} |
||||||
|
|
||||||
|
SECTION("when makeSpecification helper is used then it creates valid builder") |
||||||
|
{ |
||||||
|
// Given
|
||||||
|
auto builder = makeSpecification(); |
||||||
|
|
||||||
|
// When
|
||||||
|
auto spec = builder.field("test").equals("value").build(); |
||||||
|
|
||||||
|
// Then
|
||||||
|
REQUIRE(spec != nullptr); |
||||||
|
auto result = spec->render(TEST_RENDERER); |
||||||
|
REQUIRE(result == "(test = 'value')"); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue