Skip to content

Commit

Permalink
feat: add string templating to ReQL
Browse files Browse the repository at this point in the history
Signed-off-by: Gabor Boros <gabor.brs@gmail.com>
  • Loading branch information
gabor-boros committed May 11, 2022
1 parent c131f90 commit 5abe8e0
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/pprint/js_pprint.cc
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ static void pprint_update_reminder() {
case Term::FLOOR:
case Term::CEIL:
case Term::ROUND:
case Term::FORMAT:
break;
}
ql::datum_t::type_t d = ql::datum_t::type_t::R_NULL;
Expand Down
3 changes: 3 additions & 0 deletions src/rdb_protocol/ql2.proto
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,9 @@ message Term {
BIT_NOT = 194;
BIT_SAL = 195;
BIT_SAR = 196;

// String formatting
FORMAT = 197;
}
optional TermType type = 1;

Expand Down
1 change: 1 addition & 0 deletions src/rdb_protocol/term.cc
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ counted_t<const term_t> compile_on_current_stack(
case Term::FLOOR: return make_floor_term(env, t);
case Term::CEIL: return make_ceil_term(env, t);
case Term::ROUND: return make_round_term(env, t);
case Term::FORMAT: return make_format_term(env, t);
default: unreachable();
}
unreachable();
Expand Down
3 changes: 3 additions & 0 deletions src/rdb_protocol/term_walker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ bool term_type_is_valid(Term::TermType type) {
case Term::FLOOR:
case Term::CEIL:
case Term::ROUND:
case Term::FORMAT:
return true;
default:
return false;
Expand Down Expand Up @@ -589,6 +590,7 @@ bool term_is_write_or_meta(Term::TermType type) {
case Term::FLOOR:
case Term::CEIL:
case Term::ROUND:
case Term::FORMAT:
return false;
default: unreachable();
}
Expand Down Expand Up @@ -784,6 +786,7 @@ bool term_forbids_writes(Term::TermType type) {
case Term::FLOOR:
case Term::CEIL:
case Term::ROUND:
case Term::FORMAT:
return false;
default: unreachable();
}
Expand Down
65 changes: 64 additions & 1 deletion src/rdb_protocol/terms/string.cc
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright 2010-2015 RethinkDB, all rights reserved.
// Copyright 2010-2022 RethinkDB, all rights reserved.
#include "rdb_protocol/terms/terms.hpp"

#include <re2/re2.h>

#include <algorithm>

#include "parsing/utf8.hpp"
#include "rdb_protocol/datum_string.hpp"
#include "rdb_protocol/error.hpp"
#include "rdb_protocol/minidriver.hpp"
#include "rdb_protocol/op.hpp"

namespace ql {
Expand Down Expand Up @@ -125,6 +127,63 @@ class match_term_t : public op_term_t {
virtual const char *name() const { return "match"; }
};

class format_term_t : public op_term_t {
public:
format_term_t(compile_env_t *env, const raw_term_t &term)
: op_term_t{env, term, argspec_t{2}}, src_term{term}, compile_env{env} {}
private:
virtual scoped_ptr_t <val_t> eval_impl(scope_env_t *env, args_t *args, eval_flags_t) const {
const std::string templ = args->arg(env, 0)->as_str().to_std();
const datum_t datum = args->arg(env, 1)->as_datum();

const size_t template_length = templ.size();
std::string formatted{};

for (size_t pos = 0; pos < template_length; ++pos) {
rcheck(templ[pos] != '}',
base_exc_t::LOGIC,
"No parameter tags to close");

if (templ[pos] == '{') {
size_t param_end_pos {1};
std::string param_name {};

while (pos + param_end_pos < template_length && templ[pos + param_end_pos] != '}') {
char next_char{templ[pos + param_end_pos]};

rcheck(next_char != '{',
base_exc_t::LOGIC,
"Nested template parameters are not allowed");

param_name.push_back(next_char);
++param_end_pos;
}

rcheck(pos + param_end_pos != template_length,
base_exc_t::LOGIC,
"Parameter tag must be closed");

minidriver_t r{src_term.bt()};
datum_t field{datum.get_field(datum_string_t{param_name})};
counted_t<const term_t> term = compile_term(
compile_env,r.expr(field).coerce_to("STRING").root_term());

formatted += term->eval(env)->as_datum().as_str().to_std();
pos += param_end_pos;
} else {
formatted += templ[pos];
}
}

return new_val(datum_t(formatted));
}

virtual const char *name() const { return "format"; }

raw_term_t src_term;
compile_env_t *compile_env;
};

template <typename It>
It find_utf8_pred(It start, It end, std::function<bool(char32_t)> &&fn) {
It pos(start);
Expand Down Expand Up @@ -304,5 +363,9 @@ counted_t<term_t> make_split_term(
compile_env_t *env, const raw_term_t &term) {
return make_counted<split_term_t>(env, term);
}
counted_t<term_t> make_format_term(
compile_env_t *env, const raw_term_t &term) {
return make_counted<format_term_t>(env, term);
}

} // namespace ql
4 changes: 3 additions & 1 deletion src/rdb_protocol/terms/terms.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,13 @@ counted_t<term_t> make_json_term(
counted_t<term_t> make_to_json_string_term(
compile_env_t *env, const raw_term_t &term);

// match.cc
// string.cc
counted_t<term_t> make_match_term(
compile_env_t *env, const raw_term_t &term);
counted_t<term_t> make_split_term(
compile_env_t *env, const raw_term_t &term);
counted_t<term_t> make_format_term(
compile_env_t *env, const raw_term_t &term);

// case.cc
counted_t<term_t> make_upcase_term(
Expand Down
51 changes: 51 additions & 0 deletions test/rql_test/src/format.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
desc: Tests string formatting function
tests:
# Formatting at the middle of the text
- cd: r.format("With great {noun} comes great responsibility.", {"noun": "power"})
rb: r.format("With great {noun} comes great responsibility.", {"noun" => "power"})
ot: "With great power comes great responsibility."
# Formatting at the beginning and end of the text
- cd: r.format("{with} great power comes great {what}", {"with": "With", "what": "responsibility"})
rb: r.format("{with} great power comes great {what}", {"with" => "With", "what" => "responsibility"})
ot: "With great power comes great responsibility."
# Formatting with empty placeholder
- cd: r.format("With great {} comes great responsibility.", {"": "power"})
rb: r.format("With great {} comes great responsibility.", {"" => "power"})
ot: "With great power comes great responsibility."
# Parameter not found in text
- cd: r.format("With great power comes great responsibility.", {"text": "test" })
rb: r.format("With great power comes great responsibility.", {"text" => "test" })
ot: "With great power comes great responsibility."
# Using integers for formatting
- cd: r.format("{bottles} bottles of beer on the wall.", {"bottles": 100})
rb: r.format("{bottles} bottles of beer on the wall.", {"bottles" => 100})
ot: "100 bottles of beer on the wall."
# Using doubles for formatting
- cd: r.format("{bottles} bottles of beer on the wall.", {"bottles": 99.12345})
rb: r.format("{bottles} bottles of beer on the wall.", {"bottles" => 99.12345})
ot: "99.12345 bottles of beer on the wall."
# Using mixed values for formatting
- cd: r.format("x is not in {list}", {"list": ["one", 2, 3.45678, {"nine": 9}]})
rb: r.format("x is not in {list}", {"list" => ["one", 2, 3.45678, {"nine" => 9}]})
ot: "x is not in ["one",2,3.45678,{"nine":9}]"
# Placeholder not found in replacement parameters
- cd: r.format("foo {bar} baz", {})
rb: r.format("foo {bar} baz", {})
ot: err('ReqlNonExistenceError', 'No attribute `bar` in object'))
# Placeholder indicator curly braces is not closed
- cd: r.format("foo {bar baz", {"bar": "bar" })
rb: r.format("foo {bar baz", {"bar" => "bar" })
ot: err('ReqlQueryLogicError', 'Parameter tag must be closed')
# Placeholder indicator curly braces is not opened
- cd: r.format("foo bar} baz", {"bar": "bar" })
rb: r.format("foo bar} baz", {"bar" => "bar" })
ot: err('ReqlQueryLogicError', 'No parameter tags to close')
- cd: r.format("foo {{nested}} baz", {"nested": "bar" })
rb: r.format("foo {{nested}} baz", {"nested" => "bar" })
ot: err('ReqlQueryLogicError', 'Nested template parameters are not allowed')
- cd: r.format("{foo} {bar} {baz}", ["foo", "bar", "baz"])
ot: err('ReqlQueryLogicError', 'Expected type OBJECT but found ARRAY')
- cd: r.format("{foo} bar baz", "foo")
ot: err('ReqlQueryLogicError', 'Expected type OBJECT but found STRING')
- cd: r.format("{foo} bar baz", 1)
ot: err('ReqlQueryLogicError', 'Expected type OBJECT but found NUMBER')

0 comments on commit 5abe8e0

Please sign in to comment.