Skip to content

Commit

Permalink
fix(json_family): fix JSON.GET commmand for JSON legacy mode (#3261)
Browse files Browse the repository at this point in the history
* fix(json_family): fix JSON.GET commmand for JSON legacy mode

fixes #1523

Signed-off-by: Stepan Bagritsevich <[email protected]>

* refactor(json_family): address comments

Signed-off-by: Stepan Bagritsevich <[email protected]>

---------

Signed-off-by: Stepan Bagritsevich <[email protected]>
  • Loading branch information
BagritsevichStepan authored Jul 5, 2024
1 parent 8240c7f commit d9dd54d
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 55 deletions.
128 changes: 128 additions & 0 deletions src/server/detail/wrapped_json_path.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2022, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//

#pragma once

#include <string_view>
#include <utility>
#include <variant>

#include "core/json/json_object.h"
#include "core/json/path.h"
#include "core/string_or_view.h"

namespace dfly {

using JsonExpression = jsoncons::jsonpath::jsonpath_expression<JsonType>;

template <typename T>
using JsonPathEvaluateCallback = absl::FunctionRef<T(std::string_view, const JsonType&)>;

template <typename T> class JsonCallbackResult {
public:
using JsonV1Result = std::optional<T>;
using JsonV2Result = std::vector<T>;

explicit JsonCallbackResult(bool legacy_mode_is_enabled)
: legacy_mode_is_enabled_(legacy_mode_is_enabled) {
if (!legacy_mode_is_enabled_) {
result_ = JsonV2Result{};
}
}

void AddValue(T&& value) {
if (IsV1()) {
AsV1().emplace(std::forward<T>(value));
} else {
AsV2().emplace_back(std::forward<T>(value));
}
}

bool IsV1() const {
return legacy_mode_is_enabled_;
}

JsonV1Result& AsV1() {
return std::get<JsonV1Result>(result_);
}

JsonV2Result& AsV2() {
return std::get<JsonV2Result>(result_);
}

private:
std::variant<JsonV1Result, JsonV2Result> result_;
bool legacy_mode_is_enabled_;
};

class WrappedJsonPath {
public:
static constexpr std::string_view kV1PathRootElement = ".";
static constexpr std::string_view kV2PathRootElement = "$";

WrappedJsonPath(json::Path json_path, StringOrView path, bool is_legacy_mode_path)
: parsed_path_(std::move(json_path)),
path_(std::move(path)),
is_legacy_mode_path_(is_legacy_mode_path) {
}

WrappedJsonPath(JsonExpression expression, StringOrView path, bool is_legacy_mode_path)
: parsed_path_(std::move(expression)),
path_(std::move(path)),
is_legacy_mode_path_(is_legacy_mode_path) {
}

template <typename T>
JsonCallbackResult<T> Evaluate(const JsonType* json_entry, JsonPathEvaluateCallback<T> cb) const {
return Evaluate(json_entry, cb, IsLegacyModePath());
}

template <typename T>
JsonCallbackResult<T> Evaluate(const JsonType* json_entry, JsonPathEvaluateCallback<T> cb,
bool legacy_mode_is_enabled) const {
JsonCallbackResult<T> eval_result{legacy_mode_is_enabled};

auto eval_callback = [&cb, &eval_result](std::string_view path, const JsonType& val) {
eval_result.AddValue(cb(path, val));
};

if (HoldsJsonPath()) {
const auto& json_path = AsJsonPath();
json::EvaluatePath(
json_path, *json_entry,
[&eval_callback](std::optional<std::string_view> key, const JsonType& val) {
eval_callback(key ? *key : std::string_view{}, val);
});
} else {
const auto& json_expression = AsJsonExpression();
json_expression.evaluate(*json_entry, eval_callback);
}

return eval_result;
}

bool IsLegacyModePath() const {
return is_legacy_mode_path_;
}

private:
bool HoldsJsonPath() const {
return std::holds_alternative<json::Path>(parsed_path_);
}

const json::Path& AsJsonPath() const {
return std::get<json::Path>(parsed_path_);
}

const JsonExpression& AsJsonExpression() const {
return std::get<JsonExpression>(parsed_path_);
}

private:
std::variant<json::Path, JsonExpression> parsed_path_;
StringOrView path_;
bool is_legacy_mode_path_;
};

} // namespace dfly
25 changes: 25 additions & 0 deletions src/server/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,31 @@ using facade::kWrongTypeErr;

#endif // RETURN_ON_BAD_STATUS

#ifndef RETURN_UNEXPECTED

#define RETURN_UNEXPECTED(x) \
do { \
if (!(x)) { \
return (x).get_unexpected(); \
} \
} while (0)

#endif // RETURN_UNEXPECTED

#ifndef GET_OR_SEND_UNEXPECTED

#define GET_OR_SEND_UNEXPECTED(expr) \
({ \
auto expr_res = (expr); \
if (!expr_res) { \
cntx->SendError(expr_res.error()); \
return; \
} \
std::move(expr_res).value(); \
})

#endif // GET_OR_SEND_UNEXPECTED

namespace rdb {

enum errc {
Expand Down
163 changes: 110 additions & 53 deletions src/server/json_family.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "server/acl/acl_commands_def.h"
#include "server/command_registry.h"
#include "server/common.h"
#include "server/detail/wrapped_json_path.h"
#include "server/error.h"
#include "server/journal/journal.h"
#include "server/search/doc_index.h"
Expand Down Expand Up @@ -55,6 +56,56 @@ static const char DefaultJsonPath[] = "$";

namespace {

namespace json_parser {

template <typename T> using ParseResult = io::Result<T, std::string>;

ParseResult<JsonExpression> ParseJsonPathAsExpression(std::string_view path) {
std::error_code ec;
JsonExpression res = MakeJsonPathExpr(path, ec);
if (ec)
return nonstd::make_unexpected(kSyntaxErr);
return res;
}

ParseResult<WrappedJsonPath> ParseJsonPath(StringOrView path, bool is_legacy_mode_path) {
if (absl::GetFlag(FLAGS_jsonpathv2)) {
auto path_result = json::ParsePath(path.view());
RETURN_UNEXPECTED(path_result);
return WrappedJsonPath{std::move(path_result).value(), std::move(path), is_legacy_mode_path};
}

auto expr_result = ParseJsonPathAsExpression(path.view());
RETURN_UNEXPECTED(expr_result);
return WrappedJsonPath{std::move(expr_result).value(), std::move(path), is_legacy_mode_path};
}

ParseResult<WrappedJsonPath> ParseJsonPathV1(std::string_view path) {
if (path == WrappedJsonPath::kV1PathRootElement) {
return ParseJsonPath(StringOrView::FromView(WrappedJsonPath::kV2PathRootElement), true);
}

std::string v2_path = absl::StrCat(
WrappedJsonPath::kV2PathRootElement, path.front() != '.' && path.front() != '[' ? "." : "",
path); // Convert to V2 path; TODO(path.front() != all kinds of symbols)
return ParseJsonPath(StringOrView::FromString(std::move(v2_path)), true);
}

ParseResult<WrappedJsonPath> ParseJsonPathV2(std::string_view path) {
return ParseJsonPath(StringOrView::FromView(path), false);
}

bool IsJsonPathV2(std::string_view path) {
return path.front() == '$';
}

ParseResult<WrappedJsonPath> ParseJsonPath(std::string_view path) {
DCHECK(!path.empty());
return IsJsonPathV2(path) ? ParseJsonPathV2(path) : ParseJsonPathV1(path);
}

} // namespace json_parser

using JsonPathV2 = variant<json::Path, JsonExpression>;
using ExprCallback = absl::FunctionRef<void(string_view, const JsonType&)>;

Expand Down Expand Up @@ -454,66 +505,79 @@ void SendJsonValue(RedisReplyBuilder* rb, const JsonType& j) {
}
}

OpResult<string> OpJsonGet(const OpArgs& op_args, string_view key,
const vector<pair<string_view, optional<JsonPathV2>>>& expressions,
bool should_format, const OptString& indent, const OptString& new_line,
const OptString& space) {
bool LegacyModeIsEnabled(const std::vector<std::pair<std::string_view, WrappedJsonPath>>& paths) {
return std::all_of(paths.begin(), paths.end(),
[](auto& parsed_path) { return parsed_path.second.IsLegacyModePath(); });
}

OpResult<std::string> OpJsonGet(const OpArgs& op_args, string_view key,
const vector<pair<string_view, WrappedJsonPath>>& paths,
const std::optional<std::string>& indent,
const std::optional<std::string>& new_line,
const std::optional<std::string>& space) {
OpResult<JsonType*> result = GetJson(op_args, key);
if (!result) {
return result.status();
}
RETURN_ON_BAD_STATUS(result);

const JsonType& json_entry = *(result.value());
if (expressions.empty()) {
if (paths.empty()) {
// this implicitly means that we're using $ which
// means we just brings all values
return json_entry.to_string();
}

json_options options;
if (should_format) {
options.spaces_around_comma(spaces_option::no_spaces)
.spaces_around_colon(spaces_option::no_spaces)
.object_array_line_splits(line_split_kind::multi_line)
.indent_size(1)
.new_line_chars("");

if (indent) {
options.indent_chars(*indent);
} else {
options.indent_size(0);
}
options.spaces_around_comma(spaces_option::no_spaces)
.spaces_around_colon(spaces_option::no_spaces)
.object_array_line_splits(line_split_kind::multi_line)
.indent_size(0)
.new_line_chars("");

if (new_line) {
options.new_line_chars(*new_line);
}
if (indent) {
options.indent_size(1);
options.indent_chars(*indent);
}

if (space) {
options.after_key_chars(*space);
}
if (new_line) {
options.new_line_chars(*new_line);
}

auto eval_wrapped = [&json_entry](const optional<JsonPathV2>& expr) -> JsonType {
return expr ? visit([&](auto& arg) { return Evaluate(arg, json_entry); }, *expr) : json_entry;
if (space) {
options.after_key_chars(*space);
}

const bool legacy_mode_is_enabled = LegacyModeIsEnabled(paths);

auto cb = [](std::string_view, const JsonType& val) { return val; };

auto eval_wrapped = [&json_entry, &cb, legacy_mode_is_enabled](
const WrappedJsonPath& json_path) -> std::optional<JsonType> {
auto eval_result = json_path.Evaluate<JsonType>(&json_entry, cb, legacy_mode_is_enabled);

DCHECK(legacy_mode_is_enabled == eval_result.IsV1());

if (eval_result.IsV1()) {
return eval_result.AsV1();
}

return JsonType{eval_result.AsV2()};
};

JsonType out{json_object_arg}; // see https://github.com/danielaparker/jsoncons/issues/482
if (expressions.size() == 1) {
out = eval_wrapped(expressions[0].second);
JsonType out{
jsoncons::json_object_arg}; // see https://github.com/danielaparker/jsoncons/issues/482
if (paths.size() == 1) {
auto eval_result = eval_wrapped(paths[0].second);
out = std::move(eval_result).value(); // TODO(Print not existing path to the user)
} else {
for (const auto& [expr_str, expr] : expressions) {
out[expr_str] = eval_wrapped(expr);
for (const auto& [path_str, path] : paths) {
auto eval_result = eval_wrapped(path);
out[path_str] = std::move(eval_result).value(); // TODO(Print not existing path to the user)
}
}

if (should_format) {
json_printable jp(out, options, indenting::indent);
std::stringstream ss;
jp.dump(ss);
return ss.str();
}

return out.as<string>();
jsoncons::json_printable jp(out, options, jsoncons::indenting::indent);
std::stringstream ss;
jp.dump(ss);
return ss.str();
}

OpResult<vector<string>> OpType(const OpArgs& op_args, string_view key, JsonPathV2 expression) {
Expand Down Expand Up @@ -2067,8 +2131,7 @@ void JsonFamily::Get(CmdArgList args, ConnectionContext* cntx) {
OptString new_line;
OptString space;

// '.' corresponds to the legacy, non-array format and is passed as nullopt.
vector<pair<string_view, optional<JsonPathV2>>> expressions;
vector<pair<string_view, WrappedJsonPath>> paths;

while (parser.HasNext()) {
if (parser.Check("SPACE").IgnoreCase().ExpectTail(1)) {
Expand All @@ -2084,23 +2147,17 @@ void JsonFamily::Get(CmdArgList args, ConnectionContext* cntx) {
continue;
}

optional<JsonPathV2> expr;
string_view expr_str = parser.Next();

if (expr_str != ".") {
JsonPathV2 expression = PARSE_PATHV2(expr_str);
expr.emplace(std::move(expression));
}
string_view path_str = parser.Next();
WrappedJsonPath json_path = GET_OR_SEND_UNEXPECTED(json_parser::ParseJsonPath(path_str));

expressions.emplace_back(expr_str, std::move(expr));
paths.emplace_back(path_str, std::move(json_path));
}

if (auto err = parser.Error(); err)
return cntx->SendError(err->MakeReply());

bool should_format = (indent || new_line || space);
auto cb = [&](Transaction* t, EngineShard* shard) {
return OpJsonGet(t->GetOpArgs(shard), key, expressions, should_format, indent, new_line, space);
return OpJsonGet(t->GetOpArgs(shard), key, paths, indent, new_line, space);
};

Transaction* trans = cntx->transaction;
Expand Down
Loading

0 comments on commit d9dd54d

Please sign in to comment.