diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 739bd77b8..698625b06 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,8 @@ default_stages: [commit] exclude: | (?x)( src/redis/.* | - contrib/charts/dragonfly/ci/.* + contrib/charts/dragonfly/ci/.* | + patches/.* ) repos: - repo: local diff --git a/patches/jsoncons-v0.171.0.patch b/patches/jsoncons-v0.171.0.patch new file mode 100644 index 000000000..ac7c38243 --- /dev/null +++ b/patches/jsoncons-v0.171.0.patch @@ -0,0 +1,110 @@ +diff --git a/include/jsoncons/json_encoder.hpp b/include/jsoncons/json_encoder.hpp +index 6a1daba63..d20673171 100644 +--- a/include/jsoncons/json_encoder.hpp ++++ b/include/jsoncons/json_encoder.hpp +@@ -355,6 +355,7 @@ namespace detail { + colon_str_.push_back(':'); + break; + } ++ colon_str_.append(options.after_key_chars()); + switch (options.spaces_around_comma()) + { + case spaces_option::space_after: +@@ -1021,9 +1022,9 @@ namespace detail { + sink_.append(options_.new_line_chars().data(),options_.new_line_chars().length()); + for (int i = 0; i < indent_amount_; ++i) + { +- sink_.push_back(' '); ++ sink_.append(options_.indent_chars().data(), options_.indent_chars().length()); + } +- column_ = indent_amount_; ++ column_ = indent_amount_ * options_.new_line_chars().length(); + } + + void new_line(std::size_t len) +@@ -1031,7 +1032,7 @@ namespace detail { + sink_.append(options_.new_line_chars().data(),options_.new_line_chars().length()); + for (std::size_t i = 0; i < len; ++i) + { +- sink_.push_back(' '); ++ sink_.append(options_.indent_chars().data(), options_.indent_chars().length()); + } + column_ = len; + } +diff --git a/include/jsoncons/json_options.hpp b/include/jsoncons/json_options.hpp +index 58dcf3ba3..74d5ab217 100644 +--- a/include/jsoncons/json_options.hpp ++++ b/include/jsoncons/json_options.hpp +@@ -425,6 +425,8 @@ private: + uint8_t indent_size_; + std::size_t line_length_limit_; + string_type new_line_chars_; ++ string_type after_key_chars_; ++ string_type indent_chars_; + public: + basic_json_encode_options() + : escape_all_non_ascii_(false), +@@ -445,6 +447,7 @@ public: + line_length_limit_(line_length_limit_default) + { + new_line_chars_.push_back('\n'); ++ indent_chars_.push_back('\t'); + } + + basic_json_encode_options(const basic_json_encode_options&) = default; +@@ -467,7 +470,9 @@ public: + precision_(other.precision_), + indent_size_(other.indent_size_), + line_length_limit_(other.line_length_limit_), +- new_line_chars_(std::move(other.new_line_chars_)) ++ new_line_chars_(std::move(other.new_line_chars_)), ++ after_key_chars_(std::move(other.after_key_chars_)), ++ indent_chars_(std::move(other.indent_chars)) + { + } + +@@ -515,6 +520,16 @@ public: + return new_line_chars_; + } + ++ string_type after_key_chars() const ++ { ++ return after_key_chars_; ++ } ++ ++ string_type indent_chars() const ++ { ++ return indent_chars_; ++ } ++ + std::size_t line_length_limit() const + { + return line_length_limit_; +@@ -599,6 +614,8 @@ public: + using basic_json_encode_options::pad_inside_object_braces; + using basic_json_encode_options::pad_inside_array_brackets; + using basic_json_encode_options::new_line_chars; ++ using basic_json_encode_options::after_key_chars; ++ using basic_json_encode_options::indent_chars; + using basic_json_encode_options::line_length_limit; + using basic_json_encode_options::float_format; + using basic_json_encode_options::precision; +@@ -761,6 +778,18 @@ public: + return *this; + } + ++ basic_json_options& after_key_chars(const string_type& value) ++ { ++ this->after_key_chars_ = value; ++ return *this; ++ } ++ ++ basic_json_options& indent_chars(const string_type& value) ++ { ++ this->indent_chars_ = value; ++ return *this; ++ } ++ + basic_json_options& lossless_number(bool value) + { + this->lossless_number_ = value; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b2f56ba69..f5be2cab3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,6 +73,7 @@ set(REFLEX "${THIRD_PARTY_LIB_DIR}/reflex/bin/reflex") add_third_party( jsoncons URL https://github.com/danielaparker/jsoncons/archive/refs/tags/v0.171.0.tar.gz + PATCH_COMMAND patch -p1 -i "${CMAKE_SOURCE_DIR}/patches/jsoncons-v0.171.0.patch" CMAKE_PASS_FLAGS "-DJSONCONS_BUILD_TESTS=OFF -DJSONCONS_HAS_POLYMORPHIC_ALLOCATOR=ON" LIB "none" ) diff --git a/src/server/json_family.cc b/src/server/json_family.cc index 1085abe89..9bf0f320d 100644 --- a/src/server/json_family.cc +++ b/src/server/json_family.cc @@ -390,7 +390,8 @@ void SendJsonValue(ConnectionContext* cntx, const JsonType& j) { } OpResult OpGet(const OpArgs& op_args, string_view key, - vector> expressions) { + vector> expressions, bool should_format, + const OptString& indent, const OptString& new_line, const OptString& space) { OpResult result = GetJson(op_args, key); if (!result) { return result.status(); @@ -402,8 +403,39 @@ OpResult OpGet(const OpArgs& op_args, string_view key, // 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); + } + + if (new_line) { + options.new_line_chars(*new_line); + } + + if (space) { + options.after_key_chars(*space); + } + } + if (expressions.size() == 1) { json out = expressions[0].second.evaluate(json_entry); + if (should_format) { + json_printable jp(out, options, indenting::indent); + std::stringstream ss; + jp.dump(ss); + return ss.str(); + } + return out.as(); } @@ -413,6 +445,13 @@ OpResult OpGet(const OpArgs& op_args, string_view key, out[expr.first] = eval; } + if (should_format) { + json_printable jp(out, options, indenting::indent); + std::stringstream ss; + jp.dump(ss); + return ss.str(); + } + return out.as(); } @@ -1748,22 +1787,52 @@ void JsonFamily::Get(CmdArgList args, ConnectionContext* cntx) { DCHECK_GE(args.size(), 1U); string_view key = ArgS(args, 0); + OptString indent; + OptString new_line; + OptString space; vector> expressions; - for (size_t i = 1; i < args.size(); ++i) { - string_view path = ArgS(args, i); + string_view param = ArgS(args, i); + if (absl::EqualsIgnoreCase(param, "space")) { + if (++i >= args.size()) { + return (*cntx)->SendError(facade::WrongNumArgsError(cntx->cid->name()), + facade::kSyntaxErrType); + } else { + space = ArgS(args, i); + continue; + } + } else if (absl::EqualsIgnoreCase(param, "newline")) { + if (++i >= args.size()) { + return (*cntx)->SendError(facade::WrongNumArgsError(cntx->cid->name()), + facade::kSyntaxErrType); + } else { + new_line = ArgS(args, i); + continue; + } + } else if (absl::EqualsIgnoreCase(param, "indent")) { + if (++i >= args.size()) { + return (*cntx)->SendError(facade::WrongNumArgsError(cntx->cid->name()), + facade::kSyntaxErrType); + } else { + indent = ArgS(args, i); + continue; + } + } + error_code ec; - JsonExpression expr = ParseJsonPath(path, &ec); + JsonExpression expr = ParseJsonPath(param, &ec); if (ec) { - LOG(WARNING) << "path '" << path << "': Invalid JSONPath syntax: " << ec.message(); + LOG(WARNING) << "path '" << param << "': Invalid JSONPath syntax: " << ec.message(); return (*cntx)->SendError(kSyntaxErr); } - expressions.emplace_back(path, move(expr)); + expressions.emplace_back(param, move(expr)); } + bool should_format = (indent || new_line || space); auto cb = [&](Transaction* t, EngineShard* shard) { - return OpGet(t->GetOpArgs(shard), key, move(expressions)); + return OpGet(t->GetOpArgs(shard), key, move(expressions), should_format, indent, new_line, + space); }; Transaction* trans = cntx->transaction; diff --git a/src/server/json_family_test.cc b/src/server/json_family_test.cc index 8ad416173..9974e496c 100644 --- a/src/server/json_family_test.cc +++ b/src/server/json_family_test.cc @@ -120,6 +120,28 @@ TEST_F(JsonFamilyTest, SetGetFromPhonebook) { resp = Run({"JSON.GET", "json", "$..phoneNumbers[1].*"}); EXPECT_EQ(resp, R"(["646 555-4567","office"])"); + + resp = Run({"JSON.GET", "json", "$.address.*", "INDENT", "indent", "NEWLINE", "newline"}); + EXPECT_EQ( + resp, + R"([newlineindent"New York",newlineindent"NY",newlineindent"21 2nd Street",newlineindent"10021-3100"newline])"); + + resp = Run({"JSON.GET", "json", "$.address", "SPACE", "space"}); + EXPECT_EQ( + resp, + R"([{"city":space"New York","state":space"NY","street":space"21 2nd Street","zipcode":space"10021-3100"}])"); + + resp = Run({"JSON.GET", "json", "$.firstName", "$.age", "$.lastName", "INDENT", "indent", + "NEWLINE", "newline", "SPACE", "space"}); + EXPECT_EQ( + resp, + R"({newlineindent"$.age":space[newlineindentindent27newlineindent],newlineindent"$.firstName":space[newlineindentindent"John"newlineindent],newlineindent"$.lastName":space[newlineindentindent"Smith"newlineindent]newline})"); + + resp = + Run({"JSON.GET", "json", "$..phoneNumbers.*", "INDENT", "t", "NEWLINE", "s", "SPACE", "s"}); + EXPECT_EQ( + resp, + R"([st{stt"number":s"212 555-1234",stt"type":s"home"st},st{stt"number":s"646 555-4567",stt"type":s"office"st}s])"); } TEST_F(JsonFamilyTest, Type) {