Skip to content

Commit c104dd5

Browse files
Fix RedisJSON#145 - support for get formatting.
1 parent 41887a7 commit c104dd5

File tree

7 files changed

+249
-66
lines changed

7 files changed

+249
-66
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ name = "rejson"
1111
[dependencies]
1212
bson = "0.14"
1313
serde_json = "1.0"
14+
serde = "1.0"
1415
libc = "0.2"
1516
jsonpath_lib = { git="https://github.com/RedisJSON/jsonpath.git", branch="public-parser" }
1617
redis-module = { version="0.9.0", features = ["experimental-api"]}

docs/commands.md

-6
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ JSON.GET <key>
6060
[INDENT indentation-string]
6161
[NEWLINE line-break-string]
6262
[SPACE space-string]
63-
[NOESCAPE]
6463
[path ...]
6564
```
6665

@@ -75,11 +74,6 @@ The following subcommands change the reply's format and are all set to the empty
7574
* `NEWLINE` sets the string that's printed at the end of each line
7675
* `SPACE` sets the string that's put between a key and a value
7776

78-
The `NOESCAPE` option will disable the sending of \uXXXX escapes for non-ascii
79-
characters. This option should be used for efficiency if you deal mainly with
80-
such text. The escaping of JSON strings will be deprecated in the future and this
81-
option will become the implicit default.
82-
8377
Pretty-formatted JSON is producible with `redis-cli` by following this example:
8478

8579
```

src/formatter.rs

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Custom serde_json formatter supporting ReJSON formatting options.
2+
// Based on serde_json::ser::PrettyFormatter
3+
/*
4+
Permission is hereby granted, free of charge, to any
5+
person obtaining a copy of this software and associated
6+
documentation files (the "Software"), to deal in the
7+
Software without restriction, including without
8+
limitation the rights to use, copy, modify, merge,
9+
publish, distribute, sublicense, and/or sell copies of
10+
the Software, and to permit persons to whom the Software
11+
is furnished to do so, subject to the following
12+
conditions:
13+
14+
The above copyright notice and this permission notice
15+
shall be included in all copies or substantial portions
16+
of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
19+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
20+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
21+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
22+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
25+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26+
DEALINGS IN THE SOFTWARE.
27+
*/
28+
29+
use serde_json::ser::Formatter;
30+
use std::io;
31+
32+
pub struct RedisJsonFormatter<'a> {
33+
current_indent: usize,
34+
has_value: bool,
35+
indent: &'a [u8],
36+
space: &'a [u8],
37+
newline: &'a [u8],
38+
}
39+
40+
impl<'a> RedisJsonFormatter<'a> {
41+
pub fn new(indent: &'a [u8], space: &'a [u8], newline: &'a [u8]) -> Self {
42+
RedisJsonFormatter {
43+
current_indent: 0,
44+
has_value: false,
45+
indent,
46+
space,
47+
newline,
48+
}
49+
}
50+
51+
fn indent<W: ?Sized>(wr: &mut W, n: usize, s: &[u8]) -> io::Result<()>
52+
where
53+
W: io::Write,
54+
{
55+
for _ in 0..n {
56+
wr.write_all(s)?;
57+
}
58+
59+
Ok(())
60+
}
61+
}
62+
63+
impl<'a> Formatter for RedisJsonFormatter<'a> {
64+
fn begin_array<W: ?Sized>(&mut self, writer: &mut W) -> io::Result<()>
65+
where
66+
W: io::Write,
67+
{
68+
self.current_indent += 1;
69+
self.has_value = false;
70+
writer.write_all(b"[")
71+
}
72+
73+
fn end_array<W: ?Sized>(&mut self, writer: &mut W) -> io::Result<()>
74+
where
75+
W: io::Write,
76+
{
77+
self.current_indent -= 1;
78+
79+
if self.has_value {
80+
writer.write_all(self.newline)?;
81+
Self::indent(writer, self.current_indent, self.indent)?;
82+
}
83+
84+
writer.write_all(b"]")
85+
}
86+
87+
fn begin_array_value<W: ?Sized>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
88+
where
89+
W: io::Write,
90+
{
91+
if !first {
92+
writer.write_all(b",")?;
93+
}
94+
writer.write_all(self.newline)?;
95+
Self::indent(writer, self.current_indent, self.indent)
96+
}
97+
98+
fn end_array_value<W: ?Sized>(&mut self, _writer: &mut W) -> io::Result<()>
99+
where
100+
W: io::Write,
101+
{
102+
self.has_value = true;
103+
Ok(())
104+
}
105+
106+
fn begin_object<W: ?Sized>(&mut self, writer: &mut W) -> io::Result<()>
107+
where
108+
W: io::Write,
109+
{
110+
self.current_indent += 1;
111+
self.has_value = false;
112+
writer.write_all(b"{")
113+
}
114+
115+
fn end_object<W: ?Sized>(&mut self, writer: &mut W) -> io::Result<()>
116+
where
117+
W: io::Write,
118+
{
119+
self.current_indent -= 1;
120+
121+
if self.has_value {
122+
writer.write_all(self.newline)?;
123+
Self::indent(writer, self.current_indent, self.indent)?;
124+
}
125+
126+
writer.write_all(b"}")
127+
}
128+
129+
fn begin_object_key<W: ?Sized>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
130+
where
131+
W: io::Write,
132+
{
133+
if !first {
134+
writer.write_all(b",")?;
135+
}
136+
writer.write_all(self.newline)?;
137+
Self::indent(writer, self.current_indent, self.indent)
138+
}
139+
140+
fn begin_object_value<W: ?Sized>(&mut self, writer: &mut W) -> io::Result<()>
141+
where
142+
W: io::Write,
143+
{
144+
writer.write_all(b":")?;
145+
writer.write_all(self.space)
146+
}
147+
148+
fn end_object_value<W: ?Sized>(&mut self, _writer: &mut W) -> io::Result<()>
149+
where
150+
W: io::Write,
151+
{
152+
self.has_value = true;
153+
Ok(())
154+
}
155+
}

src/lib.rs

+18-25
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod array_index;
1414
mod backward;
1515
mod commands;
1616
mod error;
17+
mod formatter;
1718
mod nodevisitor;
1819
mod redisjson;
1920
mod schema; // TODO: Remove
@@ -162,7 +163,6 @@ fn json_set(ctx: &Context, args: Vec<String>) -> RedisResult {
162163
/// [INDENT indentation-string]
163164
/// [NEWLINE line-break-string]
164165
/// [SPACE space-string]
165-
/// [NOESCAPE]
166166
/// [path ...]
167167
///
168168
/// TODO add support for multi path
@@ -171,33 +171,25 @@ fn json_get(ctx: &Context, args: Vec<String>) -> RedisResult {
171171
let key = args.next_string()?;
172172

173173
let mut paths: Vec<Path> = vec![];
174-
let mut first_loop = true;
175174
let mut format = Format::JSON;
175+
let mut indent = String::new();
176+
let mut space = String::new();
177+
let mut newline = String::new();
176178
loop {
177179
let arg = match args.next_string() {
178180
Ok(s) => s,
179-
Err(_) => {
180-
// path is optional -> no path found on the first loop we use root "$"
181-
if first_loop {
182-
paths.push(Path::new("$".to_string()));
183-
}
184-
break;
185-
}
181+
Err(_) => break,
186182
};
187-
first_loop = false;
188183

189184
match arg.to_uppercase().as_str() {
190185
"INDENT" => {
191-
args.next();
192-
} // TODO add support
186+
indent = args.next_string()?;
187+
}
193188
"NEWLINE" => {
194-
args.next();
195-
} // TODO add support
189+
newline = args.next_string()?;
190+
}
196191
"SPACE" => {
197-
args.next();
198-
} // TODO add support
199-
"NOESCAPE" => {
200-
continue;
192+
space = args.next_string()?;
201193
} // TODO add support
202194
"FORMAT" => {
203195
format = Format::from_str(args.next_string()?.as_str())?;
@@ -208,15 +200,16 @@ fn json_get(ctx: &Context, args: Vec<String>) -> RedisResult {
208200
};
209201
}
210202

203+
// path is optional -> no path found we use root "$"
204+
if paths.is_empty() {
205+
paths.push(Path::new("$".to_string()));
206+
}
207+
211208
let key = ctx.open_key_writable(&key);
212209
let value = match key.get_value::<RedisJSON>(&REDIS_JSON_TYPE)? {
213-
Some(doc) => if paths.len() == 1 {
214-
doc.to_string(&paths[0].fixed, format)?
215-
} else {
216-
// can't be smaller than 1
217-
doc.to_json(&mut paths)?
218-
}
219-
.into(),
210+
Some(doc) => doc
211+
.to_json(&mut paths, indent, newline, space, format)?
212+
.into(),
220213
None => RedisValue::Null,
221214
};
222215

src/redisjson.rs

+47-24
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
use crate::backward;
88
use crate::commands::index;
99
use crate::error::Error;
10+
use crate::formatter::RedisJsonFormatter;
1011
use crate::nodevisitor::NodeVisitorImpl;
1112
use crate::REDIS_JSON_TYPE_VERSION;
1213

1314
use bson::decode_document;
1415
use index::schema_map;
1516
use jsonpath_lib::SelectorMut;
1617
use redis_module::raw::{self, Status};
17-
use serde_json::Value;
18+
use serde::Serialize;
19+
use serde_json::{Map, Value};
1820
use std::io::Cursor;
1921
use std::mem;
2022
use std::os::raw::{c_int, c_void};
@@ -208,30 +210,51 @@ impl RedisJSON {
208210
Ok(res)
209211
}
210212

211-
// FIXME: Implement this by manipulating serde_json::Value values,
212-
// and then using serde to serialize to JSON instead of doing it ourselves with strings.
213-
pub fn to_json(&self, paths: &mut Vec<Path>) -> Result<String, Error> {
214-
let mut selector = jsonpath_lib::selector(&self.data);
215-
let mut result = paths.drain(..).fold(String::from("{"), |mut acc, path| {
216-
let value = match selector(&path.fixed) {
217-
Ok(s) => match s.first() {
218-
Some(v) => v,
219-
None => &Value::Null,
220-
},
221-
Err(_) => &Value::Null,
222-
};
223-
acc.push('\"');
224-
acc.push_str(&path.path);
225-
acc.push_str("\":");
226-
acc.push_str(value.to_string().as_str());
227-
acc.push(',');
228-
acc
229-
});
230-
if result.ends_with(',') {
231-
result.pop();
213+
pub fn to_json(
214+
&self,
215+
paths: &mut Vec<Path>,
216+
indent: String,
217+
newline: String,
218+
space: String,
219+
format: Format,
220+
) -> Result<String, Error> {
221+
let temp_doc;
222+
let res = if paths.len() > 1 {
223+
let mut selector = jsonpath_lib::selector(&self.data);
224+
// TODO: Creating a temp doc here duplicates memory usage. This can be very memory inefficient.
225+
// A better way would be to create a doc of references to the original doc but no current support
226+
// in serde_json. I'm going for this implementation anyway because serde_json isn't supposed to be
227+
// memory efficient and we're using it anyway. See https://github.com/serde-rs/json/issues/635.
228+
temp_doc = Value::Object(paths.drain(..).fold(Map::new(), |mut acc, path| {
229+
let value = match selector(&path.fixed) {
230+
Ok(s) => match s.first() {
231+
Some(v) => v,
232+
None => &Value::Null,
233+
},
234+
Err(_) => &Value::Null,
235+
};
236+
acc.insert(path.path, (*value).clone());
237+
acc
238+
}));
239+
&temp_doc
240+
} else {
241+
self.get_first(&paths[0].fixed)?
242+
};
243+
244+
match format {
245+
Format::JSON => {
246+
let formatter = RedisJsonFormatter::new(
247+
indent.as_bytes(),
248+
space.as_bytes(),
249+
newline.as_bytes(),
250+
);
251+
252+
let mut out = serde_json::Serializer::with_formatter(Vec::new(), formatter);
253+
res.serialize(&mut out).unwrap();
254+
Ok(String::from_utf8(out.into_inner()).unwrap())
255+
}
256+
Format::BSON => Err("Soon to come...".into()), //results.into() as Bson,
232257
}
233-
result.push('}');
234-
Ok(result)
235258
}
236259

237260
pub fn str_len(&self, path: &str) -> Result<usize, Error> {

0 commit comments

Comments
 (0)