Browse Source

Added c++17 generic specification classes for query-based repositories.

cpp17-fixes
chodak166 4 months ago
parent
commit
0cae61530f
  1. 1
      cpp17/lib/CMakeLists.txt
  2. 3
      cpp17/lib/src/application/interfaces/IItemRepository.h
  3. 141
      cpp17/lib/src/domain/helpers/Specification.cpp
  4. 198
      cpp17/lib/src/domain/helpers/Specification.h
  5. 17
      cpp17/lib/src/domain/polices/ItemExpirationPolicy.h
  6. 11
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp
  7. 2
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.h
  8. 3
      cpp17/tests/CMakeLists.txt
  9. 2
      cpp17/tests/mocks/MockItemRepository.h
  10. 692
      cpp17/tests/unit/Specification.test.cpp

1
cpp17/lib/CMakeLists.txt

@ -13,6 +13,7 @@ find_package(jwt-cpp CONFIG REQUIRED)
configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/autostore/Version.h) configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/autostore/Version.h)
add_library(${TARGET_NAME} STATIC add_library(${TARGET_NAME} STATIC
src/domain/helpers/Specification.cpp
src/application/queries/GetItem.cpp src/application/queries/GetItem.cpp
src/application/queries/ListItems.cpp src/application/queries/ListItems.cpp
src/application/commands/AddItem.cpp src/application/commands/AddItem.cpp

3
cpp17/lib/src/application/interfaces/IItemRepository.h

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "domain/entities/Item.h" #include "domain/entities/Item.h"
#include "domain/polices/ItemExpirationPolicy.h"
#include <optional> #include <optional>
#include <functional> #include <functional>
#include <string> #include <string>
@ -18,6 +19,8 @@ public:
virtual std::vector<domain::Item> findByOwner(domain::User::Id_t ownerId) = 0; virtual std::vector<domain::Item> findByOwner(domain::User::Id_t ownerId) = 0;
virtual std::vector<domain::Item> virtual std::vector<domain::Item>
findWhere(std::function<bool(const domain::Item&)> predicate) = 0; findWhere(std::function<bool(const domain::Item&)> predicate) = 0;
virtual std::vector<domain::Item>
findWhere(const domain::ItemExpirationSpec& spec) = 0;
virtual void remove(domain::Item::Id_t id) = 0; virtual void remove(domain::Item::Id_t id) = 0;
}; };

141
cpp17/lib/src/domain/helpers/Specification.cpp

@ -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

198
cpp17/lib/src/domain/helpers/Specification.h

@ -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

17
cpp17/lib/src/domain/polices/ItemExpirationPolicy.h

@ -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

11
cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp

@ -5,6 +5,7 @@
#include <chrono> #include <chrono>
#include <ctime> #include <ctime>
#include <iterator> #include <iterator>
#include "FileItemRepository.h"
namespace nxl::autostore::infrastructure { namespace nxl::autostore::infrastructure {
@ -91,6 +92,14 @@ std::vector<domain::Item> FileItemRepository::findWhere(
return matchedItems; return matchedItems;
} }
std::vector<domain::Item>
nxl::autostore::infrastructure::FileItemRepository::findWhere(
const domain::ItemExpirationSpec& spec)
{
// Not implemented since no SQL-like query language is used
throw std::runtime_error("Not implemented");
}
void FileItemRepository::remove(domain::Item::Id_t id) void FileItemRepository::remove(domain::Item::Id_t id)
{ {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
@ -120,4 +129,4 @@ void FileItemRepository::persist()
} }
} }
} // namespace nxl::autostore::infrastructure } // namespace nxl::autostore::infrastructure

2
cpp17/lib/src/infrastructure/repositories/FileItemRepository.h

@ -16,6 +16,8 @@ public:
std::vector<domain::Item> findByOwner(domain::User::Id_t userId) override; std::vector<domain::Item> findByOwner(domain::User::Id_t userId) override;
std::vector<domain::Item> std::vector<domain::Item>
findWhere(std::function<bool(const domain::Item&)> predicate) override; findWhere(std::function<bool(const domain::Item&)> predicate) override;
virtual std::vector<domain::Item>
findWhere(const domain::ItemExpirationSpec& spec) override;
void remove(domain::Item::Id_t id) override; void remove(domain::Item::Id_t id) override;
private: private:

3
cpp17/tests/CMakeLists.txt

@ -34,4 +34,5 @@ endfunction()
add_test_target(FileItemRepositoryTest integration/FileItemRepository.test.cpp) add_test_target(FileItemRepositoryTest integration/FileItemRepository.test.cpp)
# add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp) # add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp)
add_test_target(AddItemTest unit/AddItem.test.cpp) add_test_target(AddItemTest unit/AddItem.test.cpp)
add_test_target(TaskSchedulerTest unit/TaskScheduler.test.cpp) add_test_target(SpecificationTest unit/Specification.test.cpp)
add_test_target(TaskSchedulerTest unit/TaskScheduler.test.cpp)

2
cpp17/tests/mocks/MockItemRepository.h

@ -7,6 +7,7 @@ namespace test {
using nxl::autostore::domain::Item; using nxl::autostore::domain::Item;
using nxl::autostore::domain::User; using nxl::autostore::domain::User;
using nxl::autostore::domain::ItemExpirationSpec;
class MockItemRepository : public nxl::autostore::application::IItemRepository class MockItemRepository : public nxl::autostore::application::IItemRepository
{ {
@ -16,6 +17,7 @@ public:
MAKE_MOCK1(findByOwner, std::vector<Item>(User::Id_t), override); MAKE_MOCK1(findByOwner, std::vector<Item>(User::Id_t), override);
MAKE_MOCK1(findWhere, std::vector<Item>(std::function<bool(const Item&)>), MAKE_MOCK1(findWhere, std::vector<Item>(std::function<bool(const Item&)>),
override); override);
MAKE_MOCK1(findWhere, std::vector<Item>(const ItemExpirationSpec&), override);
MAKE_MOCK1(remove, void(Item::Id_t), override); MAKE_MOCK1(remove, void(Item::Id_t), override);
}; };

692
cpp17/tests/unit/Specification.test.cpp

@ -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…
Cancel
Save