Skip to content

Commit 0c155c3

Browse files
authored
Merge pull request #31 from wravery/fuzzysubscriptions
Support fuzzy subscription argument matching
2 parents d9b8f5f + 812ba6a commit 0c155c3

File tree

3 files changed

+136
-12
lines changed

3 files changed

+136
-12
lines changed

GraphQLService.cpp

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,10 +1707,23 @@ void Request::unsubscribe(SubscriptionKey key)
17071707

17081708
void Request::deliver(const SubscriptionName& name, const std::shared_ptr<Object>& subscriptionObject) const
17091709
{
1710-
deliver(name, {}, subscriptionObject);
1710+
deliver(name, SubscriptionArguments {}, subscriptionObject);
17111711
}
17121712

1713-
void Request::deliver(const SubscriptionName& name, const std::unordered_map<std::string, response::Value>& arguments, const std::shared_ptr<Object>& subscriptionObject) const
1713+
void Request::deliver(const SubscriptionName& name, const SubscriptionArguments& arguments, const std::shared_ptr<Object>& subscriptionObject) const
1714+
{
1715+
SubscriptionFilterCallback exactMatch = [&arguments](response::MapType::const_reference required) noexcept -> bool
1716+
{
1717+
auto itrArgument = arguments.find(required.first);
1718+
1719+
return (itrArgument != arguments.cend()
1720+
&& itrArgument->second == required.second);
1721+
};
1722+
1723+
deliver(name, exactMatch, subscriptionObject);
1724+
}
1725+
1726+
void Request::deliver(const SubscriptionName& name, const SubscriptionFilterCallback& apply, const std::shared_ptr<Object>& subscriptionObject) const
17141727
{
17151728
const auto& optionalOrDefaultSubscription = subscriptionObject
17161729
? subscriptionObject
@@ -1738,10 +1751,7 @@ void Request::deliver(const SubscriptionName& name, const std::unordered_map<std
17381751

17391752
for (auto itrRequired = required.begin(); itrRequired != required.end(); ++itrRequired)
17401753
{
1741-
auto itrArgument = arguments.find(itrRequired->first);
1742-
1743-
if (itrArgument == arguments.cend()
1744-
|| itrArgument->second != itrRequired->second)
1754+
if (!apply(*itrRequired))
17451755
{
17461756
matchedArguments = false;
17471757
break;
@@ -1773,13 +1783,13 @@ void Request::deliver(const SubscriptionName& name, const std::unordered_map<std
17731783
{
17741784
result = std::async(std::launch::deferred,
17751785
[registration](std::future<response::Value> data)
1776-
{
1777-
response::Value document(response::Type::Map);
1786+
{
1787+
response::Value document(response::Type::Map);
17781788

1779-
document.emplace_back("data", data.get());
1789+
document.emplace_back("data", data.get());
17801790

1781-
return document;
1782-
}, optionalOrDefaultSubscription->resolve(selectionSetParams, registration->selection, registration->data->fragments, registration->data->variables));
1791+
return document;
1792+
}, optionalOrDefaultSubscription->resolve(selectionSetParams, registration->selection, registration->data->fragments, registration->data->variables));
17831793
}
17841794
catch (const schema_exception& ex)
17851795
{

include/graphqlservice/GraphQLService.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,8 @@ struct OperationData : std::enable_shared_from_this<OperationData>
523523
// Subscription callbacks receive the response::Value representing the result of evaluating the
524524
// SelectionSet against the payload.
525525
using SubscriptionCallback = std::function<void(std::future<response::Value>)>;
526+
using SubscriptionArguments = std::unordered_map<std::string, response::Value>;
527+
using SubscriptionFilterCallback = std::function<bool(response::MapType::const_reference) noexcept>;
526528

527529
// Subscriptions are stored in maps using these keys.
528530
using SubscriptionKey = size_t;
@@ -558,7 +560,8 @@ class Request : public std::enable_shared_from_this<Request>
558560
void unsubscribe(SubscriptionKey key);
559561

560562
void deliver(const SubscriptionName& name, const std::shared_ptr<Object>& subscriptionObject) const;
561-
void deliver(const SubscriptionName& name, const std::unordered_map<std::string, response::Value>& arguments, const std::shared_ptr<Object>& subscriptionObject) const;
563+
void deliver(const SubscriptionName& name, const SubscriptionArguments& arguments, const std::shared_ptr<Object>& subscriptionObject) const;
564+
void deliver(const SubscriptionName& name, const SubscriptionFilterCallback& apply, const std::shared_ptr<Object>& subscriptionObject) const;
562565

563566
private:
564567
TypeMap _operations;

tests.cpp

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,117 @@ TEST_F(TodayServiceCase, SubscribeNodeChangeMismatchedId)
922922
}
923923
}
924924

925+
TEST_F(TodayServiceCase, SubscribeNodeChangeFuzzyComparator)
926+
{
927+
auto ast = peg::parseString(R"(subscription TestSubscription {
928+
changedNode: nodeChange(id: "ZmFr") {
929+
changedId: id
930+
...on Task {
931+
title
932+
isComplete
933+
}
934+
}
935+
})");
936+
response::Value variables(response::Type::Map);
937+
auto state = std::make_shared<today::RequestState>(14);
938+
bool filterCalled = false;
939+
auto filterCallback = [&filterCalled](response::MapType::const_reference fuzzy) noexcept -> bool
940+
{
941+
EXPECT_FALSE(filterCalled);
942+
EXPECT_EQ("id", fuzzy.first) << "should only get called once for the id argument";
943+
EXPECT_EQ("ZmFr", fuzzy.second.get<const response::StringType&>());
944+
filterCalled = true;
945+
return true;
946+
};
947+
auto subscriptionObject = std::make_shared<today::NodeChange>(
948+
[this](const std::shared_ptr<service::RequestState>& state, std::vector<uint8_t>&& idArg) -> std::shared_ptr<service::Object>
949+
{
950+
const std::vector<uint8_t> fuzzyId { 'f', 'a', 'k' };
951+
952+
EXPECT_EQ(14, std::static_pointer_cast<today::RequestState>(state)->requestId) << "should pass the RequestState to the subscription resolvers";
953+
EXPECT_EQ(fuzzyId, idArg);
954+
return std::static_pointer_cast<service::Object>(std::make_shared<today::Task>(std::vector<uint8_t>(_fakeTaskId), "Don't forget", true));
955+
});
956+
response::Value result;
957+
auto key = _service->subscribe(service::SubscriptionParams { state, std::move(ast), "TestSubscription", std::move(std::move(variables)) },
958+
[&result](std::future<response::Value> response)
959+
{
960+
result = response.get();
961+
});
962+
_service->deliver("nodeChange", filterCallback, std::static_pointer_cast<service::Object>(subscriptionObject));
963+
_service->unsubscribe(key);
964+
965+
try
966+
{
967+
ASSERT_TRUE(filterCalled) << "should match the id parameter in the subscription";
968+
ASSERT_TRUE(result.type() == response::Type::Map);
969+
auto errorsItr = result.find("errors");
970+
if (errorsItr != result.get<const response::MapType&>().cend())
971+
{
972+
FAIL() << response::toJSON(response::Value(errorsItr->second));
973+
}
974+
const auto data = service::ScalarArgument::require("data", result);
975+
976+
const auto taskNode = service::ScalarArgument::require("changedNode", data);
977+
EXPECT_EQ(_fakeTaskId, service::IdArgument::require("changedId", taskNode)) << "id should match in base64 encoding";
978+
EXPECT_EQ("Don't forget", service::StringArgument::require("title", taskNode)) << "title should match";
979+
EXPECT_TRUE(service::BooleanArgument::require("isComplete", taskNode)) << "isComplete should match";
980+
}
981+
catch (const service::schema_exception& ex)
982+
{
983+
FAIL() << response::toJSON(response::Value(ex.getErrors()));
984+
}
985+
}
986+
987+
TEST_F(TodayServiceCase, SubscribeNodeChangeFuzzyMismatch)
988+
{
989+
auto ast = peg::parseString(R"(subscription TestSubscription {
990+
changedNode: nodeChange(id: "ZmFrZVRhc2tJZA==") {
991+
changedId: id
992+
...on Task {
993+
title
994+
isComplete
995+
}
996+
}
997+
})");
998+
response::Value variables(response::Type::Map);
999+
bool filterCalled = false;
1000+
auto filterCallback = [&filterCalled](response::MapType::const_reference fuzzy) noexcept -> bool
1001+
{
1002+
EXPECT_FALSE(filterCalled);
1003+
EXPECT_EQ("id", fuzzy.first) << "should only get called once for the id argument";
1004+
EXPECT_EQ("ZmFrZVRhc2tJZA==", fuzzy.second.get<const response::StringType&>());
1005+
filterCalled = true;
1006+
return false;
1007+
};
1008+
bool calledResolver = false;
1009+
auto subscriptionObject = std::make_shared<today::NodeChange>(
1010+
[this, &calledResolver](const std::shared_ptr<service::RequestState>& state, std::vector<uint8_t>&& idArg) -> std::shared_ptr<service::Object>
1011+
{
1012+
calledResolver = true;
1013+
return nullptr;
1014+
});
1015+
bool calledGet = false;
1016+
auto key = _service->subscribe(service::SubscriptionParams { nullptr, std::move(ast), "TestSubscription", std::move(std::move(variables)) },
1017+
[&calledGet](std::future<response::Value>)
1018+
{
1019+
calledGet = true;
1020+
});
1021+
_service->deliver("nodeChange", filterCallback, std::static_pointer_cast<service::Object>(subscriptionObject));
1022+
_service->unsubscribe(key);
1023+
1024+
try
1025+
{
1026+
ASSERT_TRUE(filterCalled) << "should not match the id parameter in the subscription";
1027+
ASSERT_FALSE(calledResolver);
1028+
ASSERT_FALSE(calledGet);
1029+
}
1030+
catch (const service::schema_exception& ex)
1031+
{
1032+
FAIL() << response::toJSON(response::Value(ex.getErrors()));
1033+
}
1034+
}
1035+
9251036
TEST_F(TodayServiceCase, SubscribeNodeChangeMatchingVariable)
9261037
{
9271038
auto ast = peg::parseString(R"(subscription TestSubscription($taskId: ID!) {

0 commit comments

Comments
 (0)