Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,45 @@ if(JSONATA_BUILD_TESTS)
endif()
endif()
endif()

# Optional: demonstrates that jsonata-cpp's pluggable regex engine
# (IRegex, see include/jsonata/IRegex.h) can be backed by Google's RE2
# instead of std::regex. Off by default. RE2 must already be available
# via find_package (e.g. installed through vcpkg/conan/brew) -- it is
# not fetched automatically like nlohmann_json, since current RE2
# releases also require Abseil, making a FetchContent fallback a much
# heavier addition than an optional test warrants.
option(JSONATA_BUILD_RE2_TEST "Build the optional RE2 regex engine test (requires re2 to be installed)" OFF)

if(JSONATA_BUILD_RE2_TEST)
find_package(re2 CONFIG REQUIRED)

add_executable(jsonata_re2_test test/RE2Engine.cpp test/RE2EngineTest.cpp)
target_link_libraries(jsonata_re2_test
jsonata
re2::re2
gtest_main
gtest
Threads::Threads
)
# re2::re2 pulls in Abseil, and Abseil's ABI (e.g. absl::string_view's
# layout) depends on which copy of its headers get resolved first.
# Other PUBLIC dependencies (e.g. a system-wide nlohmann_json found
# via find_package) can inject unrelated include directories that
# happen to also contain a differently-configured Abseil, silently
# shadowing the one re2 was actually built against and causing link
# errors. Force re2/Abseil's own include dirs to be searched first.
target_include_directories(jsonata_re2_test BEFORE PRIVATE
$<TARGET_PROPERTY:re2::re2,INTERFACE_INCLUDE_DIRECTORIES>
)

if(TARGET gtest)
include(GoogleTest)
gtest_discover_tests(jsonata_re2_test
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
endif()
endif()
endif()

# Installation rules
Expand Down
19 changes: 12 additions & 7 deletions include/jsonata/Functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <string>
#include <vector>

#include "jsonata/IRegex.h"
#include "Utils.h"

namespace jsonata {
Expand Down Expand Up @@ -66,7 +67,8 @@ class Functions {
static std::optional<std::string> trim(const std::string& str);
static std::any split(const std::string& str, const std::string& separator,
int64_t limit = -1);
static std::any split(const std::string& str, const std::regex& pattern,
static std::any split(const std::string& str,
const std::shared_ptr<IRegex>& pattern,
int64_t limit = -1);
Comment thread
rayokota marked this conversation as resolved.
static std::optional<std::string> join(const Utils::JList& arr,
const std::string& separator = "");
Expand Down Expand Up @@ -173,9 +175,10 @@ class Functions {
static int64_t millis();

// Regex functions
static Utils::JList evaluateMatcher(const std::regex& pattern,
static Utils::JList evaluateMatcher(const std::shared_ptr<IRegex>& pattern,
const std::string& str);
static Utils::JList match(const std::string& str, const std::regex& pattern,
static Utils::JList match(const std::string& str,
const std::shared_ptr<IRegex>& pattern,
int64_t limit = -1);

// Lambda detection
Expand Down Expand Up @@ -225,16 +228,18 @@ class Functions {
static bool isNumericString(const std::string& str);
static std::string safeReplacement(const std::string& replacement);
static std::string safeReplaceAll(const std::string& str,
const std::regex& pattern,
const std::shared_ptr<IRegex>& pattern,
const std::string& replacement);
static std::string safeReplaceFirst(const std::string& str,
const std::regex& pattern,
const std::shared_ptr<IRegex>& pattern,
const std::string& replacement);
static std::string safeReplaceAllFn(const std::string& str,
const std::regex& pattern,
const std::shared_ptr<IRegex>& pattern,
const std::any& func);
static std::string expandReplacement(const std::string& replacement,
const RegexMatch& match);
static nlohmann::ordered_map<std::string, std::any> toJsonataMatch(
const std::smatch& match);
const RegexMatch& match);
static std::string encodeURI(const std::string& uri);
static std::string leftPad(const std::string& str, int64_t size,
const std::string& padStr = " ");
Expand Down
84 changes: 84 additions & 0 deletions include/jsonata/IRegex.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* jsonata-cpp is the JSONata C++ reference port
*
* Copyright Robert Yokota
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once

#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>

namespace jsonata {

// A single match, engine-agnostic (the equivalent of std::smatch, but not
// tied to std::regex so alternative engines such as RE2 can produce it too).
struct RegexMatch {
std::string text;
size_t position = 0;
size_t length = 0;
// groups[0] is capture group 1, matching std::smatch's [1] indexing
// offset by the implicit full-match group 0.
std::vector<std::optional<std::string>> groups;
};

// Flags parsed from a JSONata regex literal's /pattern/flags suffix. Engines
// translate these into their own native flag representation.
struct RegexFlags {
bool caseInsensitive = false;
bool multiline = false;
};

// Engine-agnostic compiled regex. JSONata regex literals (and patterns given
// as plain strings to functions like $match/$replace/$split) are compiled
// into this interface, so the evaluator never depends on std::regex
// directly. The default engine wraps std::regex (see StdRegex.h); a custom
// engine (e.g. RE2) can be injected via Jsonata's constructor.
class IRegex {
public:
virtual ~IRegex() = default;

// True if the pattern matches anywhere in str. Backs $contains.
virtual bool test(const std::string& str) const = 0;

// The first match starting the search no earlier than byte offset `pos`.
// Backs $replace's first/limited-match paths, and lazy one-at-a-time
// iteration when a regex literal is invoked as a function -- callers
// that only want the first few matches of a large input should call
// this repeatedly (advancing `pos` past each match) rather than use
// findAll, which materializes every match up front.
virtual std::optional<RegexMatch> findFirst(const std::string& str,
size_t pos = 0) const = 0;

// All non-overlapping matches, in order. Backs $match and $contains.
virtual std::vector<RegexMatch> findAll(const std::string& str) const = 0;
Comment thread
rayokota marked this conversation as resolved.
Outdated

// Splits str on every match, returning the segments between matches
// (including empty leading/trailing segments). Backs $split.
virtual std::vector<std::string> split(const std::string& str) const = 0;
};

// Compiles a pattern into an IRegex. Used both for JSONata regex literals
// and for patterns supplied as plain strings at runtime.
using RegexEngine = std::function<std::shared_ptr<IRegex>(
const std::string& pattern, RegexFlags flags)>;

// The built-in std::regex-backed engine; this is jsonata-cpp's default and
// preserves its pre-existing regex behavior exactly.
RegexEngine defaultRegexEngine();

} // namespace jsonata
14 changes: 11 additions & 3 deletions include/jsonata/Jsonata.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <string>
#include <vector>

#include "jsonata/IRegex.h"
#include "jsonata/Parser.h"

// Forward declarations
Expand Down Expand Up @@ -124,8 +125,9 @@ class JFunction {
class Jsonata {
public:
// Constructors
Jsonata();
Jsonata(const std::string& jsonataExpression);
explicit Jsonata(RegexEngine regexEngine = defaultRegexEngine());
Jsonata(const std::string& jsonataExpression,
RegexEngine regexEngine = defaultRegexEngine());
Jsonata(const Jsonata& other); // Copy constructor for per-thread instances

// Main evaluation methods (ordered JSON variants)
Expand All @@ -150,8 +152,13 @@ class Jsonata {
// Environment access
std::shared_ptr<Frame> getEnvironment() const;

// Regex engine used to compile JSONata regex literals and dynamic
// string patterns (e.g. for $match/$replace/$split).
RegexEngine getRegexEngine() const { return regexEngine_; }

// Factory methods
static Jsonata jsonata(const std::string& expression);
static Jsonata jsonata(const std::string& expression,
RegexEngine regexEngine = defaultRegexEngine());

// Instance methods (matching Java reference)
std::shared_ptr<Frame> createFrame();
Expand Down Expand Up @@ -193,6 +200,7 @@ class Jsonata {
static void clearPerThreadInstance();

std::shared_ptr<Frame> environment_;
RegexEngine regexEngine_ = defaultRegexEngine();
static thread_local std::shared_ptr<class Parser> currentParser_;
static thread_local std::any tls_input_;
static thread_local std::shared_ptr<Frame> tls_environment_;
Expand Down
3 changes: 2 additions & 1 deletion include/jsonata/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@ class Parser {

// Main parsing method
std::shared_ptr<Symbol> parse(const std::string& source,
bool recover = false);
bool recover = false,
RegexEngine regexEngine = defaultRegexEngine());

// Get parsing errors
const std::vector<std::shared_ptr<std::exception>>& getErrors() const {
Expand Down
41 changes: 41 additions & 0 deletions include/jsonata/StdRegex.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* jsonata-cpp is the JSONata C++ reference port
*
* Copyright Robert Yokota
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once

#include <regex>

#include "jsonata/IRegex.h"

namespace jsonata {

// Default IRegex implementation, backed by std::regex (ECMAScript grammar).
class StdRegex : public IRegex {
public:
StdRegex(const std::string& pattern, RegexFlags flags);

bool test(const std::string& str) const override;
std::optional<RegexMatch> findFirst(const std::string& str, size_t pos) const override;
std::vector<RegexMatch> findAll(const std::string& str) const override;
std::vector<std::string> split(const std::string& str) const override;

private:
std::regex regex_;
static RegexMatch toRegexMatch(const std::smatch& m);
};

} // namespace jsonata
8 changes: 6 additions & 2 deletions include/jsonata/Tokenizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#include <unordered_map>
#include <vector>

#include "jsonata/IRegex.h"

namespace jsonata {

class Tokenizer {
Expand Down Expand Up @@ -63,10 +65,12 @@ class Tokenizer {
size_t position_;
size_t length_;
int64_t depth_;
RegexEngine regexEngine_;

public:
// Constructor
explicit Tokenizer(const std::string& path);
explicit Tokenizer(const std::string& path,
RegexEngine regexEngine = defaultRegexEngine());

// Main tokenization method
std::unique_ptr<Token> next(bool prefix = false);
Expand All @@ -80,7 +84,7 @@ class Tokenizer {
std::unique_ptr<Token> create(const std::string& type,
const std::any& value);
bool isClosingSlash(size_t position) const;
std::regex scanRegex();
std::shared_ptr<IRegex> scanRegex();

// Codepoint access methods (like Java charAt)
int32_t charAt(size_t index) const;
Expand Down
1 change: 1 addition & 0 deletions include/jsonata/utils/Signature.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include <vector>

// Include Utils to resolve JList
#include "../IRegex.h"
#include "../Utils.h"

namespace jsonata {
Expand Down
Loading
Loading