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 |
||||
|
||||
#include "domain/entities/Item.h" |
||||
#include "domain/helpers/Specification.h" |
||||
#include <chrono> |
||||
|
||||
namespace nxl::autostore::domain { |
||||
|
||||
using ItemExpirationSpec = std::unique_ptr<nxl::helpers::ISpecificationExpr>; |
||||
|
||||
class ItemExpirationPolicy |
||||
{ |
||||
public: |
||||
bool isExpired(const Item& item, |
||||
const std::chrono::system_clock::time_point& currentTime) const |
||||
using TimePoint = std::chrono::system_clock::time_point; |
||||
constexpr static const char* FIELD_EXP_DATE{"expiration_date"}; |
||||
|
||||
bool isExpired(const Item& item, const TimePoint& currentTime) const |
||||
{ |
||||
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
|
||||
@ -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