diff --git a/CMakeLists.txt b/CMakeLists.txt index 1032787..9ea9a90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,28 +2,40 @@ cmake_minimum_required(VERSION 3.22) project(jsonata VERSION 0.1.1 - DESCRIPTION "JSONata for C++" + DESCRIPTION "QJSONata for C++" LANGUAGES CXX ) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) include(GNUInstallDirs) include(FetchContent) +set( JSONATA_ENABLED_BACKENDS ) + # Find nlohmann_json or fetch if not found find_package(nlohmann_json CONFIG QUIET) if(NOT nlohmann_json_FOUND) FetchContent_Declare( - nlohmann_json - GIT_REPOSITORY https://github.com/nlohmann/json.git - GIT_TAG v3.12.0 + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 ) FetchContent_MakeAvailable(nlohmann_json) # Mark that we used FetchContent set(JSONATA_USED_FETCHCONTENT_JSON TRUE) + list( APPEND JSONATA_ENABLED_BACKENDS nlohmann::ordered_json nlohmann::json ) + set( JSONATA_NLOHMANN_ENABLED true ) +endif() + +find_package(Qt6 QUIET COMPONENTS Core) +if(Qt6_FOUND) + list( APPEND JSONATA_ENABLED_BACKENDS QJsonValue QVariant ) + set( JSONATA_QT_ENABLED true ) +else() + warning( "Cannot find Qt >=6.10." ) endif() # Collect source files @@ -31,75 +43,91 @@ file(GLOB_RECURSE HEADERS "include/*.h") file(GLOB_RECURSE SOURCES "src/*.cpp") # Library type option -option(JSONATA_BUILD_SHARED "Build jsonata as a shared library" OFF) +option(JSONATA_BUILD_SHARED "Build qjsonata as a shared library" OFF) set(JSONATA_LIB_TYPE STATIC) if(JSONATA_BUILD_SHARED) set(JSONATA_LIB_TYPE SHARED) endif() # Create the library -add_library(jsonata ${JSONATA_LIB_TYPE} ${SOURCES} ${HEADERS}) +add_library(qjsonata ${JSONATA_LIB_TYPE} ${SOURCES} ${HEADERS}) -set_target_properties(jsonata PROPERTIES +set_target_properties(qjsonata PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} POSITION_INDEPENDENT_CODE ON ) # Provide a canonical alias for consumers who add_subdirectory() -add_library(jsonata::jsonata ALIAS jsonata) +add_library(qjsonata::qjsonata ALIAS qjsonata) # Compiler features -target_compile_features(jsonata PUBLIC cxx_std_17) +# target_compile_features(qjsonata PUBLIC cxx_std_17) # Compiler options -target_compile_options(jsonata - PRIVATE - # Common warnings - $<$,$,$>:-Wall -Wno-unused-variable> - # Clang-only suppression - $<$,$>:-Wno-unused-lambda-capture> - # MSVC: ensure source files are treated as UTF-8 to avoid codepage warnings - $<$:/utf-8> +target_compile_options(qjsonata + PRIVATE + # Common warnings + $<$,$,$>:-Wall -Wno-unused-variable> + # Clang-only suppression + $<$,$>:-Wno-unused-lambda-capture> + # MSVC: ensure source files are treated as UTF-8 to avoid codepage warnings + $<$:/utf-8> ) # Set include directories for the library -target_include_directories(jsonata PUBLIC - $ - $ - $ +target_include_directories(qjsonata PUBLIC + $ + $ + $ ) -# nlohmann_json is used from public headers, so propagate it PUBLIC. -# Ensure the exported target only records the imported name in the install -# interface to avoid referencing non-exported build targets. -if(TARGET nlohmann_json::nlohmann_json) - target_link_libraries(jsonata PUBLIC - $ - $ - ) -elseif(TARGET nlohmann_json) - target_link_libraries(jsonata PUBLIC - $ - $ - ) -else() - # Not present in the build; rely on the installed config to find it. - target_link_libraries(jsonata PUBLIC - $ - ) +if( JSONATA_NLOHMANN_ENABLED ) + # nlohmann_json is used from public headers, so propagate it PUBLIC. + # Ensure the exported target only records the imported name in the install + # interface to avoid referencing non-exported build targets. + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(qjsonata PUBLIC + $ + $ + ) + elseif(TARGET nlohmann_json) + target_link_libraries(qjsonata PUBLIC + $ + $ + ) + else() + # Not present in the build; rely on the installed config to find it. + target_link_libraries(qjsonata PUBLIC + $ + ) + endif() +endif() + +if( JSONATA_QT_ENABLED ) + target_link_libraries(qjsonata PUBLIC Qt6::Core) endif() # Optionally build tests -option(JSONATA_BUILD_TESTS "Build JSONata C++ tests" OFF) +option(JSONATA_BUILD_TESTS "Build QJSONata C++ tests" OFF) if(JSONATA_BUILD_TESTS) + # for testing at least one backend needs to be enabled + + # 1. Check the length of the list + list(LENGTH JSONATA_ENABLED_BACKENDS LIST_LEN) + + # 2. If the length is 0, throw a fatal error + if(LIST_LEN EQUAL 0) + message(FATAL_ERROR "At least one backend must be enabled.") + endif() + # Download GoogleTest if not already present # See https://google.github.io/googletest/quickstart-cmake.html FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip - DOWNLOAD_EXTRACT_TIMESTAMP true + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip + DOWNLOAD_EXTRACT_TIMESTAMP true ) FetchContent_MakeAvailable(googletest) @@ -121,100 +149,145 @@ if(JSONATA_BUILD_TESTS) test/TypesTest.cpp ) - # Create main test executable - add_executable( - jsonata_tests - ${TEST_SOURCES} + # Create test generator executable + add_executable(qgenerate test/Generate.cpp + include/jsonata/backend.h + include/jsonata/ordered_map.h) + target_link_libraries(qgenerate qjsonata) + list(GET JSONATA_ENABLED_BACKENDS 0 FIRST_BACKEND) + target_compile_definitions(qgenerate PRIVATE + JSONATA_TEST_BACKEND=${FIRST_BACKEND} ) - target_link_libraries( - jsonata_tests - jsonata - gtest_main - gtest - Threads::Threads - ) + if(WIN32) + set_target_properties(qgenerate PROPERTIES + WIN32_EXECUTABLE FALSE + ) + endif() + + foreach( BE IN LISTS JSONATA_ENABLED_BACKENDS ) + # 1. Strip the extension to create a clean executable name + string(REPLACE "::" "_" CLEAN_BE ${BE}) + set(EXE_NAME qjsonata_tests_${CLEAN_BE}) + + # 2. Create the executable + add_executable( + ${EXE_NAME} + ${TEST_SOURCES} + ) + + target_compile_definitions(${EXE_NAME} PRIVATE + JSONATA_TEST_BACKEND=${BE} + ) + + target_link_libraries( + ${EXE_NAME} + qjsonata + gtest_main + gtest + Threads::Threads + ) + if(WIN32) + set_target_properties(${EXE_NAME} PROPERTIES + WIN32_EXECUTABLE FALSE + ) + endif() + + endforeach() - # Create test generator executable - add_executable(generate test/Generate.cpp) - target_link_libraries(generate jsonata) # Custom command to generate test files add_custom_command( OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/test/gen/generated.stamp - COMMAND $ + COMMAND $ COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_SOURCE_DIR}/test/gen/generated.stamp - DEPENDS generate + DEPENDS qgenerate WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Generating JSONata test files from test suite" + COMMENT "Generating QJSONata test files from test suite" ) # Custom target that depends on the generated files - add_custom_target(generate_tests + add_custom_target(qgenerate_tests DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/test/gen/generated.stamp ) - # Make sure generated tests are created before building main tests - add_dependencies(jsonata_tests generate_tests) + foreach( BE IN LISTS JSONATA_ENABLED_BACKENDS ) + # 1. Strip the extension to create a clean executable name + string(REPLACE "::" "_" CLEAN_BE ${BE}) + set(EXE_NAME qjsonata_tests_${CLEAN_BE}) + # Make sure generated tests are created before building main tests + add_dependencies(${EXE_NAME} qgenerate_tests) + endforeach() # Convenience target to just run the generator add_custom_target(run_generator - COMMAND $ - DEPENDS generate + COMMAND $ + DEPENDS qgenerate WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Running JSONata test generator" + COMMENT "Running QJSONata test generator" ) # Option to build generated tests as separate executables - option(BUILD_GENERATED_TESTS "Build generated JSONata test suites" ON) + option(BUILD_GENERATED_TESTS "Build generated QJSONata test suites" ON) if(BUILD_GENERATED_TESTS) - # Find all generated test files - file(GLOB GENERATED_TEST_FILES "${CMAKE_CURRENT_SOURCE_DIR}/test/gen/*Test.cpp") + foreach( BE IN LISTS JSONATA_ENABLED_BACKENDS ) + # 1. Strip the extension to create a clean executable name + string(REPLACE "::" "_" CLEAN_BE ${BE}) + set(EXE_NAME qjsonata_generated_tests_${CLEAN_BE}) + + # Find all generated test files + file(GLOB GENERATED_TEST_FILES "${CMAKE_CURRENT_SOURCE_DIR}/test/gen/*Test.cpp") - if(GENERATED_TEST_FILES) - # Create executable for generated tests - add_executable(jsonata_generated_tests ${GENERATED_TEST_FILES} test/JsonataTest.cpp) - target_include_directories(jsonata_generated_tests PRIVATE test) - target_link_libraries(jsonata_generated_tests - jsonata - gtest_main - gtest - Threads::Threads - ) - # Windows-only: increase stack size to 8MB for generated tests - if(WIN32) - if(MSVC) - target_link_options(jsonata_generated_tests PRIVATE /STACK:8388608) - else() - target_link_options(jsonata_generated_tests PRIVATE -Wl,--stack,8388608) - endif() - endif() - add_dependencies(jsonata_generated_tests generate_tests) - - # Add to testing (only if Google Test is available) - if(TARGET gtest) - include(GoogleTest) - gtest_discover_tests(jsonata_generated_tests - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + if(GENERATED_TEST_FILES) + # Create executable for generated tests + add_executable(${EXE_NAME} ${GENERATED_TEST_FILES} test/JsonataTest.cpp) + target_compile_definitions(${EXE_NAME} PRIVATE + JSONATA_TEST_BACKEND=${BE} + ) + target_include_directories(${EXE_NAME} PRIVATE test) + target_link_libraries(${EXE_NAME} + qjsonata + gtest_main + gtest + Threads::Threads ) + # Windows-only: increase stack size to 8MB for generated tests + if(WIN32) + if(MSVC) + target_link_options(${EXE_NAME} PRIVATE /STACK:8388608) + else() + target_link_options(${EXE_NAME} PRIVATE -Wl,--stack,8388608) + endif() + endif() + add_dependencies(${EXE_NAME} qgenerate_tests) + + # Add to testing (only if Google Test is available) + if(TARGET gtest) + include(GoogleTest) + gtest_discover_tests(${EXE_NAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() endif() - endif() + endforeach() endif() endif() # Installation rules if(JSONATA_USED_FETCHCONTENT_JSON) - install(TARGETS jsonata nlohmann_json - EXPORT jsonataTargets - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - ) + if( JSONATA_NLOHMANN_ENABLED ) + install(TARGETS qjsonata nlohmann_json + EXPORT qjsonataTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ) + endif() else() - install(TARGETS jsonata - EXPORT jsonataTargets + install(TARGETS qjsonata + EXPORT qjsonataTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} @@ -226,37 +299,42 @@ install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(DIRECTORY third_party/utfcpp/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) # Export targets for the install-tree -install(EXPORT jsonataTargets +install(EXPORT qjsonataTargets FILE jsonataTargets.cmake - NAMESPACE jsonata:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsonata + NAMESPACE qjsonata:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/qjsonata ) # Generate and install package config + version files include(CMakePackageConfigHelpers) write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/jsonataConfigVersion.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/qjsonataConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion ) configure_package_config_file( cmake/jsonataConfig.cmake.in - "${CMAKE_CURRENT_BINARY_DIR}/jsonataConfig.cmake" - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsonata + "${CMAKE_CURRENT_BINARY_DIR}/qjsonataConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/qjsonata ) install(FILES - "${CMAKE_CURRENT_BINARY_DIR}/jsonataConfig.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/jsonataConfigVersion.cmake" - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jsonata + "${CMAKE_CURRENT_BINARY_DIR}/qjsonataConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/qjsonataConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/qjsonata ) # Enable testing with Google Test if(JSONATA_BUILD_TESTS) - enable_testing() - include(GoogleTest) - gtest_discover_tests(jsonata_tests - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) + foreach( BE IN LISTS JSONATA_ENABLED_BACKENDS ) + # 1. Strip the extension to create a clean executable name + string(REPLACE "::" "_" CLEAN_BE ${BE}) + set(EXE_NAME qjsonata_tests_${CLEAN_BE}) + enable_testing() + include(GoogleTest) + gtest_discover_tests(${EXE_NAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endforeach() endif() diff --git a/README.md b/README.md index 82db649..3b30919 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ -# jsonata-cpp +# Qjsonata [![Build Status][github-actions-shield]][github-actions-link] [github-actions-shield]: https://github.com/rayokota/jsonata-cpp/actions/workflows/cmake-multi-platform.yml/badge.svg?branch=master [github-actions-link]: https://github.com/rayokota/jsonata-cpp/actions -C++ implementation of JSONata. +C++ implementation of JSONata with support for abstract backend. This is a C++ port of the [JSONata reference implementation](https://github.com/jsonata-js/jsonata), and also borrows from the [Dashjoin Java port](https://github.com/dashjoin/jsonata-java). -This implementation supports 100% of the language features of JSONata, using [nlohmann_json](https://github.com/nlohmann/json). -The JSONata documentation can be found [here](https://jsonata.org). +QJsonata it is based on the jsonata-cpp port. + +This implementation supports 100% of the language features of JSONata, with the exception that the ordering of keys of objects depend on the backend used. +When using nlohmann - which has support for unordered json objects, ordering is as read or generated. When using a backend like QJson which only supports sorted objects, objects are sorted when readed (parsed) or printed (dumped). This 'breaks' some tests as the expected result objects converted to string sometimes do not match the query result converted to string, although the content is compatible as a json object. +In fact QJson supports any 'backend' that supports a certain API, whether it is a json object or not (e.g. a QVariant is also valid). The API is defined +using a c++ context (isCompatible). + +The JSONata documentation can be found [here](https://jsonata.org). ## Installation @@ -22,19 +28,19 @@ Installation uses CMake as follows: include(FetchContent) FetchContent_Declare( - jsonata - URL https://github.com/rayokota/jsonata-cpp/archive/refs/tags/v0.1.0.zip + qjsonata + URL https://github.com/u19809/QJsonata/archive/refs/tags/v0.1.0.zip URL_HASH SHA256=3ee1798f28a29d36ebbb273853979926716a384e4d491a6bd408e1f6de51760d # Optional ) -FetchContent_MakeAvailable(jsonata) +FetchContent_MakeAvailable(qjsonata) # Use the library -target_link_libraries(your_target jsonata::jsonata) +target_link_libraries(your_target qjsonata::qjsonata) ``` ## Getting Started -A very simple start: +A very simple start (with nlohmann as backend: ``` #include @@ -63,6 +69,35 @@ int main() { return 0; } ``` +the same test with QJson as backend + +``` +#include +#include +#include + +int main() { + // Create the JSON data + auto data = QJsonValue::fromJson(R"({ + "example": [ + {"value": 4}, + {"value": 7}, + {"value": 13} + ] + })"); + + // Create the JSONata expression + jsonata::Jsonata expr("$sum(example.value)"); + + // Evaluate the expression with the data + auto result = expr.evaluate(data); + + // Print the result + std::cout << "Result: " << result << std::endl; + + return 0; +} +``` ## Running Tests diff --git a/include/jsonata/Functions.h b/include/jsonata/Functions.h index f71bcd9..3778e69 100644 --- a/include/jsonata/Functions.h +++ b/include/jsonata/Functions.h @@ -20,7 +20,6 @@ #include #include -#include #include #include #include @@ -28,6 +27,7 @@ #include #include "Utils.h" +#include "ordered_map.h" namespace jsonata { @@ -133,7 +133,7 @@ class Functions { const std::string& char_ = " "); static std::optional formatNumber( double value, const std::string& picture, - const nlohmann::ordered_map& options = {}); + const jsonata::ordered_map& options = {}); static std::any shuffle(const Utils::JList& args); static std::any lookup(const std::any& input, const std::string& key); static void error(const std::string& message); @@ -195,7 +195,7 @@ class Functions { : implementation(impl), signature(sig) {} }; - static nlohmann::ordered_map + static jsonata::ordered_map getFunctionRegistry(); static std::any applyFunction(const std::string& name, const Utils::JList& args); @@ -233,7 +233,7 @@ class Functions { static std::string safeReplaceAllFn(const std::string& str, const std::regex& pattern, const std::any& func); - static nlohmann::ordered_map toJsonataMatch( + static jsonata::ordered_map toJsonataMatch( const std::smatch& match); static std::string encodeURI(const std::string& uri); static std::string leftPad(const std::string& str, int64_t size, @@ -257,7 +257,7 @@ class Functions { }; static FormatSymbols processOptionsArg( - const nlohmann::ordered_map& options); + const jsonata::ordered_map& options); static std::string getFormattingCharacter(const std::string& value, const std::string& propertyName, bool isChar); diff --git a/include/jsonata/JException.h b/include/jsonata/JException.h index 3388573..2334a9a 100644 --- a/include/jsonata/JException.h +++ b/include/jsonata/JException.h @@ -19,11 +19,8 @@ #pragma once #include -#include #include #include -#include - // Include Utils for JList #include "Utils.h" diff --git a/include/jsonata/Jsonata.h b/include/jsonata/Jsonata.h index 68db659..c2fcf67 100644 --- a/include/jsonata/Jsonata.h +++ b/include/jsonata/Jsonata.h @@ -29,485 +29,683 @@ #include #include #include -#include #include #include #include +#include "Jsonata/ordered_map.h" #include "jsonata/Parser.h" +#include "Jsonata/backend.h" // Forward declarations -namespace jsonata { -class JException; -namespace utils { -class Signature; -} -namespace json { -struct JsonValue; -} -} // namespace jsonata - -namespace jsonata { - -// Forward declaration for callback types -class Frame; -using EntryCallback = std::function, const std::any&, std::shared_ptr)>; -using ExitCallback = - std::function, const std::any&, - std::shared_ptr, const std::any&)>; - -/** +namespace jsonata +{ + class JException; + namespace utils { class Signature; } + namespace json { struct JsonValue; } +} // namespace jsonata + +namespace jsonata +{ + // Forward declaration for callback types + class Frame; + using EntryCallback = std:: + function, const std::any &, std::shared_ptr)>; + using ExitCallback = std::function, const std::any &, std::shared_ptr, const std::any &)>; + + /** * Frame class for variable bindings and scope management */ -class Frame { - private: - std::shared_ptr parent_; - nlohmann::ordered_map bindings_; - std::chrono::time_point timestamp_; - int64_t timeout_; - int64_t recursionDepth_; - EntryCallback entryCallback_; - ExitCallback exitCallback_; - std::unique_ptr timebox_; - - public: - bool isParallelCall = false; - - public: - // Constructors - Frame(); - Frame(std::shared_ptr parent); - - // Variable binding and lookup - void bind(const std::string& name, const std::any& value); - std::any lookup(const std::string& name) const; - - // Runtime bounds - void setRuntimeBounds(int64_t timeout, int64_t maxRecursionDepth); - - // Evaluation callbacks - void setEvaluateEntryCallback(EntryCallback callback); - void setEvaluateExitCallback(ExitCallback callback); - - // Parent access - std::shared_ptr getParent() const { return parent_; } - - // Bindings access - const nlohmann::ordered_map& getBindings() const { - return bindings_; - } -}; + class Frame + { + private: + std::shared_ptr parent_; + jsonata::ordered_map bindings_; + std::chrono::time_point timestamp_; + int64_t timeout_; + int64_t recursionDepth_; + EntryCallback entryCallback_; + ExitCallback exitCallback_; + std::unique_ptr timebox_; + + public: + bool isParallelCall = false; + + public: + // Constructors + Frame(); + Frame(std::shared_ptr parent); + + // Variable binding and lookup + void bind(const std::string &name, const std::any &value); + std::any lookup(const std::string &name) const; + + // Runtime bounds + void setRuntimeBounds(int64_t timeout, int64_t maxRecursionDepth); + + // Evaluation callbacks + void setEvaluateEntryCallback(EntryCallback callback); + void setEvaluateExitCallback(ExitCallback callback); + + // Parent access + std::shared_ptr getParent() const { return parent_; } + + // Bindings access + const jsonata::ordered_map &getBindings() const + { + return bindings_; + } + }; -/** + /** * Function types for JSONata functions */ -class JFunction { - public: - std::function)> - implementation; - std::shared_ptr signature; - - JFunction() = default; - JFunction(std::function)> - impl) - : implementation(impl) {} - - virtual ~JFunction() = default; -}; + class JFunction + { + public: + std::function)> + implementation; + std::shared_ptr signature; + + JFunction() = default; + JFunction(std::function)> impl) + : implementation(impl) + {} + + virtual ~JFunction() = default; + }; -/** + /** * Main Jsonata evaluator class */ -class Jsonata { - public: - // Constructors - Jsonata(); - Jsonata(const std::string& jsonataExpression); - Jsonata(const Jsonata& other); // Copy constructor for per-thread instances - - // Main evaluation methods (ordered JSON variants) - nlohmann::ordered_json evaluate(const nlohmann::ordered_json& input); - nlohmann::ordered_json evaluate(const nlohmann::ordered_json& input, - std::shared_ptr bindings); - nlohmann::ordered_json evaluate(std::nullptr_t); - nlohmann::ordered_json evaluate(std::nullptr_t, - std::shared_ptr bindings); - - // Main evaluation methods (unordered nlohmann::json variants) - nlohmann::json evaluate(const nlohmann::json& input); - nlohmann::json evaluate(const nlohmann::json& input, - std::shared_ptr bindings); - nlohmann::json evaluateUnordered(std::nullptr_t); - nlohmann::json evaluateUnordered(std::nullptr_t, - std::shared_ptr bindings); - std::any evaluate(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - - // Environment access - std::shared_ptr getEnvironment() const; - - // Factory methods - static Jsonata jsonata(const std::string& expression); - - // Instance methods (matching Java reference) - std::shared_ptr createFrame(); - std::shared_ptr createFrame( - std::shared_ptr enclosingEnvironment); - - // Parse expression - std::shared_ptr parse(const std::string& expression); - - // Debug helper - std::shared_ptr getExpression() const { - return expression_; - } - - // Lambda function application support - std::any apply(const std::any& lambda, const Utils::JList& args, - const std::any& input, std::shared_ptr environment); - std::any applyInner(const std::any& proc, const Utils::JList& args, - const std::any& input, - std::shared_ptr environment); - std::any applyProcedure(const std::any& proc, const Utils::JList& args); - Utils::JList validateArguments(const std::any& signature, - const Utils::JList& args, - const std::any& context); - - // Partial application support (matching Java implementation) - std::any partialApplyProcedure(std::shared_ptr proc, - const Utils::JList& args); - std::any partialApplyNativeFunction(const std::any& native, - const Utils::JList& args, - const std::string& functionName); - - // Thread-local instances (matching Java implementation) - static thread_local Jsonata* currentInstance_; - // Owns the per-thread clone created by getPerThreadInstance(); ensures - // deletion on thread exit and avoids manual cleanup. - static thread_local std::unique_ptr ownedInstance_; - // Optional: explicitly clear the per-thread instance before thread exit. - static void clearPerThreadInstance(); - - std::shared_ptr environment_; - static thread_local std::shared_ptr currentParser_; - static thread_local std::any tls_input_; - static thread_local std::shared_ptr tls_environment_; - static Jsonata* getCurrentInstance(); - static std::shared_ptr getCurrentParser(); - Jsonata* getPerThreadInstance(); - - // Public accessors for thread-local context (used by Functions) - const std::any& getCurrentInput() const { return tls_input_; } - std::shared_ptr getCurrentEnvironment() const { - return tls_environment_; - } - - // Missing public API methods from Java - void assign(const std::string& name, const std::any& value); - void registerFunction(const std::string& name, - const JFunction& implementation); - void registerFunction( - const std::string& name, - std::function implementation); - - // Type-safe function registration overloads (equivalent to Java's Fn0, - // Fn1, etc.) - template - void registerFunction(const std::string& name, - std::function implementation); - - template - void registerFunction(const std::string& name, - std::function implementation); - - template - void registerFunction(const std::string& name, - std::function implementation); - - template - void registerFunction(const std::string& name, - std::function implementation); - - template - void registerFunction(const std::string& name, - std::function implementation); - - public: - // Function-like detection (Java reference: line 1551) - bool isFunctionLike(const std::any& o) const; - - // Input validation control - bool isValidateInput() const; - void setValidateInput(bool validateInput); - - // Error handling - std::vector getErrors() const; - - // Utility methods - static bool boolize(const std::any& value); - - public: - std::unique_ptr parser_; + class Jsonata + { + public: + // Constructors + Jsonata(); + Jsonata(const std::string &jsonataExpression); + Jsonata(const Jsonata &other); // Copy constructor for per-thread instances + + template + requires jsonata::isReadCompatible + static backend parse( const std::string & s ) { + return wrap(T::parse( s )); + } + + // Main evaluation methods + template + jsonata::backend evaluate(const T &input, std::shared_ptr bindings) + { + currentInstance_ = this; + + // Check for syntax errors (equivalent to Java's check for errors != null) + if (!expression_) { + throw JException("S0500", 0); // Expression compilation failed + } + + // Convert JSON input to std::any domain for the evaluator + std::any result; + try { + // Set thread-local input/environment for this evaluation + std::any anyInput = toAny(input); + result = evaluate(anyInput, bindings); + return fromAny(result); + } catch (const std::exception &err) { + // TODO: populateMessage(err); + throw; + } + } + + template + jsonata::backend evaluate(const jsonata::backend &input, std::shared_ptr bindings) { + return evaluate( *input, bindings ); + } + + template + inline jsonata::backend evaluate(const T &input) + { + return evaluate(input, nullptr); + } + + template + inline jsonata::backend evaluate(const jsonata::backend &input) + { + return evaluate(*input ); + } + + template + jsonata::backend evaluate(std::nullptr_t) + { + return evaluate(T()); + } + template + jsonata::backend evaluate(std::nullptr_t, std::shared_ptr bindings) + { + return evaluate(T(), bindings); + } + + std::any evaluate(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); - static std::shared_ptr staticFrame_; - static std::shared_ptr getStaticFrame(); - static void initializeBuiltinFunctions(std::shared_ptr frame); + // Environment access + std::shared_ptr getEnvironment() const; - // New members for string constructor - std::shared_ptr expression_; + // Factory methods + static Jsonata jsonata(const std::string &expression); - bool validateInput_ = false; - std::vector errors_; - int64_t timestamp_ = 0; + // Instance methods (matching Java reference) + std::shared_ptr createFrame(); + std::shared_ptr createFrame(std::shared_ptr enclosingEnvironment); - // Current evaluation context is stored in thread-local variables + // Parse expression + std::shared_ptr parse(const std::string &expression); - // GroupEntry structure for object grouping (matches Java implementation) - struct GroupEntry { - std::any data; - size_t exprIndex; - }; + // Debug helper + std::shared_ptr getExpression() const { return expression_; } - // Core evaluation method - std::any _evaluate(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - - // Expression evaluation methods - std::any evaluateLiteral(std::shared_ptr expr); - std::any evaluateName(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateVariable(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateBinary(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateUnary(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateFunction(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateFunctionWithContext(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment, - const std::any& applytoContext); - std::any evaluateRegex(std::shared_ptr expr, - const std::any& input, + // Lambda function application support + std::any apply(const std::any &lambda, + const Utils::JList &args, + const std::any &input, std::shared_ptr environment); - std::any evaluateWildcard(std::shared_ptr expr, - const std::any& input); - std::any flatten(const std::any& arg, Utils::JList* flattened = nullptr); - std::any evaluateFilter(std::shared_ptr predicate, - const std::any& input, - std::shared_ptr environment); - std::any evaluatePath(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateCondition(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateDescendant(std::shared_ptr expr, - const std::any& input, + std::any applyInner(const std::any &proc, + const Utils::JList &args, + const std::any &input, std::shared_ptr environment); - void recurseDescendants(const std::any& input, - std::vector& results); - std::any evaluateApply(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateRange(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateBind(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateLambda(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateSort(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateTransform(std::shared_ptr expr, - const std::any& input, + std::any applyProcedure(const std::any &proc, const Utils::JList &args); + Utils::JList validateArguments(const std::any &signature, + const Utils::JList &args, + const std::any &context); + + // Partial application support (matching Java implementation) + std::any partialApplyProcedure(std::shared_ptr proc, + const Utils::JList &args); + std::any partialApplyNativeFunction(const std::any &native, + const Utils::JList &args, + const std::string &functionName); + + // Thread-local instances (matching Java implementation) + static thread_local Jsonata *currentInstance_; + // Owns the per-thread clone created by getPerThreadInstance(); ensures + // deletion on thread exit and avoids manual cleanup. + static thread_local std::unique_ptr ownedInstance_; + // Optional: explicitly clear the per-thread instance before thread exit. + static void clearPerThreadInstance(); + + std::shared_ptr environment_; + static thread_local std::shared_ptr currentParser_; + static thread_local std::any tls_input_; + static thread_local std::shared_ptr tls_environment_; + static Jsonata *getCurrentInstance(); + static std::shared_ptr getCurrentParser(); + Jsonata *getPerThreadInstance(); + + // Public accessors for thread-local context (used by Functions) + const std::any &getCurrentInput() const { return tls_input_; } + std::shared_ptr getCurrentEnvironment() const { return tls_environment_; } + + // Missing public API methods from Java + void assign(const std::string &name, const std::any &value); + void registerFunction(const std::string &name, const JFunction &implementation); + void registerFunction(const std::string &name, + std::function implementation); + + // Type-safe function registration overloads (equivalent to Java's Fn0, + // Fn1, etc.) + template + void registerFunction(const std::string &name, std::function implementation); + + template + void registerFunction(const std::string &name, std::function implementation); + + template + void registerFunction(const std::string &name, std::function implementation); + + template + void registerFunction(const std::string &name, std::function implementation); + + template + void registerFunction(const std::string &name, + std::function implementation); + + private : + + // helper + std::any evaluate( const std::any input, std::shared_ptr bindings ); + + public: + // Function-like detection (Java reference: line 1551) + bool isFunctionLike(const std::any &o) const; + + // Input validation control + bool isValidateInput() const; + void setValidateInput(bool validateInput); + + // Error handling + std::vector getErrors() const; + + // Utility methods + static bool boolize(const std::any &value); + + public: + std::unique_ptr parser_; + + static std::shared_ptr staticFrame_; + static std::shared_ptr getStaticFrame(); + static void initializeBuiltinFunctions(std::shared_ptr frame); + + // New members for string constructor + std::shared_ptr expression_; + + bool validateInput_ = false; + std::vector errors_; + int64_t timestamp_ = 0; + + // Current evaluation context is stored in thread-local variables + + // GroupEntry structure for object grouping (matches Java implementation) + struct GroupEntry + { + std::any data; + size_t exprIndex; + }; + + // Core evaluation method + std::any _evaluate(std::shared_ptr expr, + const std::any &input, std::shared_ptr environment); - std::any evaluateParent(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateBlock(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - // Binary operation evaluators - std::any evaluateNumericExpression(const std::any& lhs, const std::any& rhs, - const std::string& op); - std::any evaluateComparisonExpression(const std::any& lhs, - const std::any& rhs, - const std::string& op); - bool deepEquals(const std::any& lhs, const std::any& rhs); - std::any evaluateEqualityExpression(const std::any& lhs, - const std::any& rhs, - const std::string& op); - std::any evaluateBooleanExpression(const std::any& lhs, - std::function rhs, - const std::string& op); - std::any evaluateStringConcat(const std::any& lhs, const std::any& rhs); - std::any evaluateRangeExpression(const std::any& lhs, const std::any& rhs); - std::any evaluateIncludesExpression(const std::any& lhs, - const std::any& rhs); - - // Missing advanced evaluation methods from Java - std::any evaluateStages( - const std::vector>& stages, - const std::any& input, std::shared_ptr environment); - std::any evaluateStep(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment, - bool lastStep = false); - std::any evaluateTupleStep(std::shared_ptr expr, - const Utils::JList& input, - const std::optional& tupleBindings, - std::shared_ptr environment); - std::any evaluateGroupExpression(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateTransformExpression(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluateApplyExpression(std::shared_ptr expr, - const std::any& input, - std::shared_ptr environment); - std::any evaluatePartialApplication(std::shared_ptr expr, - const std::any& input, + // Expression evaluation methods + std::any evaluateLiteral(std::shared_ptr expr); + std::any evaluateName(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateVariable(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateBinary(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateUnary(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateFunction(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateFunctionWithContext(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment, + const std::any &applytoContext); + std::any evaluateRegex(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateWildcard(std::shared_ptr expr, const std::any &input); + std::any flatten(const std::any &arg, Utils::JList *flattened = nullptr); + std::any evaluateFilter(std::shared_ptr predicate, + const std::any &input, + std::shared_ptr environment); + std::any evaluatePath(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateCondition(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateDescendant(std::shared_ptr expr, + const std::any &input, std::shared_ptr environment); - - std::shared_ptr createFrameFromTuple( - std::shared_ptr environment, const std::any& tupleAny); - std::any reduceTupleStream(const std::any& tupleStream); - - // Conversion helpers between engine types and JSON - // Ordered variants preserve insertion order using nlohmann::ordered_json - static std::any orderedJsonToAny(const nlohmann::ordered_json& j); - static nlohmann::ordered_json anyToOrderedJson(const std::any& value); - // Unordered variants using nlohmann::json (key order may not be preserved) - static std::any jsonToAny(const nlohmann::json& j); - static nlohmann::json anyToJson(const std::any& value); - - // Error code mappings - void initializeErrorCodes(); -}; - -// Global static methods -std::any jsonata_evaluate(const std::string& expression, const std::any& input); - -// Template implementations (must be in header for templates) - -template -void Jsonata::registerFunction(const std::string& name, - std::function implementation) { - auto wrapper = [implementation](const std::vector&) -> std::any { - if constexpr (std::is_void_v) { - implementation(); - return std::any{}; - } else { - return std::any(implementation()); - } - }; - registerFunction(name, wrapper); -} - -template -void Jsonata::registerFunction(const std::string& name, - std::function implementation) { - auto wrapper = [implementation, - name](const std::vector& args) -> std::any { - if (args.empty()) { - throw JException("S0410", -1, - "Function " + name + " expects 1 argument, got 0"); - } - A arg = std::any_cast(args[0]); - if constexpr (std::is_void_v) { - implementation(arg); - return std::any{}; - } else { - return std::any(implementation(arg)); - } - }; - registerFunction(name, wrapper); -} - -template -void Jsonata::registerFunction(const std::string& name, - std::function implementation) { - auto wrapper = [implementation, - name](const std::vector& args) -> std::any { - if (args.size() < 2) { - throw JException("S0410", -1, - "Function " + name + " expects 2 arguments, got " + - std::to_string(args.size())); - } - A arg1 = std::any_cast(args[0]); - B arg2 = std::any_cast(args[1]); - if constexpr (std::is_void_v) { - implementation(arg1, arg2); - return std::any{}; - } else { - return std::any(implementation(arg1, arg2)); - } + void recurseDescendants(const std::any &input, std::vector &results); + std::any evaluateApply(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateRange(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateBind(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateLambda(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateSort(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateTransform(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateParent(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateBlock(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + + // Binary operation evaluators + std::any evaluateNumericExpression(const std::any &lhs, + const std::any &rhs, + const std::string &op); + std::any evaluateComparisonExpression(const std::any &lhs, + const std::any &rhs, + const std::string &op); + bool deepEquals(const std::any &lhs, const std::any &rhs); + std::any evaluateEqualityExpression(const std::any &lhs, + const std::any &rhs, + const std::string &op); + std::any evaluateBooleanExpression(const std::any &lhs, + std::function rhs, + const std::string &op); + std::any evaluateStringConcat(const std::any &lhs, const std::any &rhs); + std::any evaluateRangeExpression(const std::any &lhs, const std::any &rhs); + std::any evaluateIncludesExpression(const std::any &lhs, const std::any &rhs); + + // Missing advanced evaluation methods from Java + std::any evaluateStages(const std::vector> &stages, + const std::any &input, + std::shared_ptr environment); + std::any evaluateStep(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment, + bool lastStep = false); + std::any evaluateTupleStep(std::shared_ptr expr, + const Utils::JList &input, + const std::optional &tupleBindings, + std::shared_ptr environment); + std::any evaluateGroupExpression(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateTransformExpression(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluateApplyExpression(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + std::any evaluatePartialApplication(std::shared_ptr expr, + const std::any &input, + std::shared_ptr environment); + + std::shared_ptr createFrameFromTuple(std::shared_ptr environment, + const std::any &tupleAny); + std::any reduceTupleStream(const std::any &tupleStream); + + // Conversion helpers between engine types and JSON + // Ordered variants preserve insertion order using jsonata::ordered_json + template + requires jsonata::isReadCompatible + static std::any toAny(const T &jv) + { + auto j = jsonata::backend(jv); + if (j.isNull()) + return std::any{}; + if (j.isBool()) + return std::any(j.template get()); + if (j.isInteger()) { + // Preserve sign by using int64_t; for large unsigned, capture as + // uint64_t + int64_t si = 0; + try { + si = j.template get(); + return std::any(static_cast(si)); + } catch (...) { + } + uint64_t ui = j.template get(); + return std::any(static_cast(ui)); + } + if (j.isUnsignedInteger()) { + uint64_t ui = j.template get(); + return std::any(static_cast(ui)); + } + if (j.isFloat()) + return std::any(j.template get()); + if (j.isString()) + return std::any(j.template get()); + if (j.isArray()) { + std::vector out; + out.reserve(j.size()); + jsonata::copy(j, out, [](const T &el) { return toAny(el); }); + return out; + } + if (j.isObject()) { + jsonata::ordered_map m; + jsonata::copy(j, m, [](const T &el) { return toAny(el); }); + return m; + } + return std::any{}; + } + + template + requires jsonata::isWriteCompatible + static jsonata::backend fromAny(const std::any &value) + { + if (!value.has_value()) + return jsonata::backend::create(); + + // Canonicalize numeric types: convert doubles with no fractional part to + // integer types + try { + if (Utils::isNumeric(value)) { + std::any canon = Utils::convertNumber(value); + if (canon.type() != value.type()) { + return fromAny(canon); + } + } + } catch (...) { + // Ignore conversion errors here; fall back to original value + } + + const std::type_info &type = value.type(); + if (type == typeid(bool)) + return jsonata::backend::create(std::any_cast(value)); + if (type == typeid(double)) + return jsonata::backend::create(std::any_cast(value)); + if (type == typeid(int64_t)) + return jsonata::backend::create(std::any_cast(value)); + if (type == typeid(uint64_t)) + return jsonata::backend::create(std::any_cast(value)); + // Cross-platform support for additional integral types (e.g., long/long long) + if (type == typeid(long long)) + return jsonata::backend::create(static_cast(std::any_cast(value))); + if (type == typeid(unsigned long long)) + return jsonata::backend::create(static_cast(std::any_cast(value))); + if (type == typeid(long)) + return jsonata::backend::create(static_cast(std::any_cast(value))); + if (type == typeid(unsigned long)) + return jsonata::backend::create(static_cast(std::any_cast(value))); + if (type == typeid(int)) + return jsonata::backend::create(static_cast(std::any_cast(value))); + if (type == typeid(unsigned int)) + return jsonata::backend::create(static_cast(std::any_cast(value))); + if (type == typeid(std::string)) + return jsonata::backend::create(std::any_cast(value)); + if (type == typeid(Utils::JList)) { + auto arr = jsonata::backend::array(); + const auto &jlist = std::any_cast(value); + jsonata::copy(jlist, arr, [](const std::any &a) { return fromAny(a); }); + return arr; + } + if (type == typeid(std::vector)) { + auto arr = jsonata::backend::array(); + const auto &vec = std::any_cast &>(value); + jsonata::copy(vec, arr, [](const std::any &a) { return fromAny(a); }); + return arr; + } + if (type == typeid(jsonata::ordered_map)) { + auto obj = jsonata::backend::object(); + const auto &map = std:: + any_cast &>( + value); + jsonata::copy(map, obj, [](const std::any &a) { return fromAny(a); }); + return obj; + } + if (type == typeid(std::shared_ptr)) { + return jsonata::backend::create(); + } + // Directly handle json returned from custom functions + if (type == typeid(T)) { + return jsonata::backend(std::any_cast(value)); + } + + return jsonata::backend::create(); + } + + template + requires jsonata::isReadCompatible && jsonata::isReadCompatible + static to convert( const from & oj ) { + // Recursive lambda to convert ordered_json to json + std::function convert = [&](const from & j) -> to { + if (j.isNull()) return jsonata::backend::create(); + if (j.isBool()) return jsonata::backend::create(j.template get()); + if (j.isInteger()) { + int64_t si = 0; + try { + si = j.template get(); + return jsonata::backend::create(static_cast(si)); + } catch (...) { + } + uint64_t ui = j.template get(); + return jsonata::backend::create(static_cast(ui)); + } + if (j.isUnsignedInteger()) + return jsonata::backend::create(j.template get()); + if (j.isFloat()) + return jsonata::backend::create(j.template get()); + if (j.isString()) + return jsonata::backend::create(j.template get()); + if (j.isArray()) { + auto arr = jsonata::backend::array(); + jsonata::copy( j, arr, [&convert]( const from& el ) { + return convert(el ); + }); + return arr; + } + if (j.isObject()) { + auto obj = jsonata::backend::object(); + jsonata::copy( j, obj, [&convert]( const from& el ) { + return convert(el ); + }); + return obj; + } + return jsonata::backend::create(); + }; + + return convert(oj); + } + + // static std::any toAny(const jsonata::ordered_json &j); + // static jsonata::ordered_json fromAny(const std::any &value); + // // Unordered variants using jsonata::json (key order may not be preserved) + // static std::any jsonToAny(const jsonata::json &j); + // static jsonata::json anyToJson(const std::any &value); + + // Error code mappings + void initializeErrorCodes(); }; - registerFunction(name, wrapper); -} - -template -void Jsonata::registerFunction(const std::string& name, - std::function implementation) { - auto wrapper = [implementation, - name](const std::vector& args) -> std::any { - if (args.size() < 3) { - throw JException("S0410", -1, - "Function " + name + " expects 3 arguments, got " + - std::to_string(args.size())); - } - A arg1 = std::any_cast(args[0]); - B arg2 = std::any_cast(args[1]); - C arg3 = std::any_cast(args[2]); - if constexpr (std::is_void_v) { - implementation(arg1, arg2, arg3); - return std::any{}; - } else { - return std::any(implementation(arg1, arg2, arg3)); - } - }; - registerFunction(name, wrapper); -} - -template -void Jsonata::registerFunction(const std::string& name, - std::function implementation) { - auto wrapper = [implementation, - name](const std::vector& args) -> std::any { - if (args.size() < 4) { - throw JException("S0410", -1, - "Function " + name + " expects 4 arguments, got " + - std::to_string(args.size())); - } - A arg1 = std::any_cast(args[0]); - B arg2 = std::any_cast(args[1]); - C arg3 = std::any_cast(args[2]); - D arg4 = std::any_cast(args[3]); - if constexpr (std::is_void_v) { - implementation(arg1, arg2, arg3, arg4); - return std::any{}; - } else { - return std::any(implementation(arg1, arg2, arg3, arg4)); - } - }; - registerFunction(name, wrapper); -} -} // namespace jsonata + // Global static methods + std::any jsonata_evaluate(const std::string &expression, const std::any &input); + + // Template implementations (must be in header for templates) + + template + void Jsonata::registerFunction(const std::string &name, std::function implementation) + { + auto wrapper = [implementation](const std::vector &) -> std::any { + if constexpr (std::is_void_v) { + implementation(); + return std::any{}; + } else { + return std::any(implementation()); + } + }; + registerFunction(name, wrapper); + } + + template + void Jsonata::registerFunction(const std::string &name, std::function implementation) + { + auto wrapper = [implementation, name](const std::vector &args) -> std::any { + if (args.empty()) { + throw JException("S0410", -1, "Function " + name + " expects 1 argument, got 0"); + } + A arg = std::any_cast(args[0]); + if constexpr (std::is_void_v) { + implementation(arg); + return std::any{}; + } else { + return std::any(implementation(arg)); + } + }; + registerFunction(name, wrapper); + } + + template + void Jsonata::registerFunction(const std::string &name, std::function implementation) + { + auto wrapper = [implementation, name](const std::vector &args) -> std::any { + if (args.size() < 2) { + throw JException("S0410", + -1, + "Function " + name + " expects 2 arguments, got " + + std::to_string(args.size())); + } + A arg1 = std::any_cast(args[0]); + B arg2 = std::any_cast(args[1]); + if constexpr (std::is_void_v) { + implementation(arg1, arg2); + return std::any{}; + } else { + return std::any(implementation(arg1, arg2)); + } + }; + registerFunction(name, wrapper); + } + + template + void Jsonata::registerFunction(const std::string &name, std::function implementation) + { + auto wrapper = [implementation, name](const std::vector &args) -> std::any { + if (args.size() < 3) { + throw JException("S0410", + -1, + "Function " + name + " expects 3 arguments, got " + + std::to_string(args.size())); + } + A arg1 = std::any_cast(args[0]); + B arg2 = std::any_cast(args[1]); + C arg3 = std::any_cast(args[2]); + if constexpr (std::is_void_v) { + implementation(arg1, arg2, arg3); + return std::any{}; + } else { + return std::any(implementation(arg1, arg2, arg3)); + } + }; + registerFunction(name, wrapper); + } + + template + void Jsonata::registerFunction(const std::string &name, + std::function implementation) + { + auto wrapper = [implementation, name](const std::vector &args) -> std::any { + if (args.size() < 4) { + throw JException("S0410", + -1, + "Function " + name + " expects 4 arguments, got " + + std::to_string(args.size())); + } + A arg1 = std::any_cast(args[0]); + B arg2 = std::any_cast(args[1]); + C arg3 = std::any_cast(args[2]); + D arg4 = std::any_cast(args[3]); + if constexpr (std::is_void_v) { + implementation(arg1, arg2, arg3, arg4); + return std::any{}; + } else { + return std::any(implementation(arg1, arg2, arg3, arg4)); + } + }; + registerFunction(name, wrapper); + } +} // namespace jsonata diff --git a/include/jsonata/backend.h b/include/jsonata/backend.h new file mode 100644 index 0000000..3daa920 --- /dev/null +++ b/include/jsonata/backend.h @@ -0,0 +1,682 @@ +#pragma once + +#include +#include +#include + +#include "jsonata/common_backend.h" +#include "jsonata/nlohmann_backend.h" +#include "jsonata/qtjson_backend.h" +#include "jsonata/qtvariant_backend.h" + +namespace jsonata +{ + // test for existence of methods + struct Anything + { + // Implicit conversion to absolutely anything + template + operator T() const; + // This allows the mock to stay 'active' when passed by reference + template + operator T &() const; + + // Also handle const references just in case + template + operator const T &() const; + }; + + template + concept isBasicallyReadCompatible = requires(const T &ConstObj) { + typename json_bridge::sortedPartner; + json_bridge::forAll(ConstObj, [](const std::string &, auto &) {}); + json_bridge::array(); + json_bridge::object(); + json_bridge::at(ConstObj, 1); + json_bridge::dump(ConstObj); + json_bridge::parse(std::string()); + { + json_bridge::size(ConstObj) + } -> std::same_as; + { + json_bridge::isEmpty(ConstObj) + } -> std::same_as; + { + json_bridge::EQ(ConstObj, ConstObj) + } -> std::same_as; + { + json_bridge::isObject(ConstObj) + } -> std::same_as; + { + json_bridge::isArray(ConstObj) + } -> std::same_as; + { + json_bridge::contains(ConstObj, std::string()) + } -> std::same_as; + { + json_bridge::isNull(ConstObj) + } -> std::same_as; + { + json_bridge::isBool(ConstObj) + } -> std::same_as; + { + json_bridge::isString(ConstObj) + } -> std::same_as; + { + json_bridge::isNumber(ConstObj) + } -> std::same_as; + { + json_bridge::isInteger(ConstObj) + } -> std::same_as; + { + json_bridge::isUnsignedInteger(ConstObj) + } -> std::same_as; + { + json_bridge::isFloat(ConstObj) + } -> std::same_as; + requires requires(Anything &property) { + { + json_bridge::getPropertyValueOfType(ConstObj, std::string(), property) + } -> std::same_as; + }; + { + json_bridge::template get(ConstObj) + } -> std::same_as; + }; + + template + concept isBasicallyWriteCompatible = isBasicallyReadCompatible && requires(T &Obj) { + json_bridge::mutateForAll(Obj, [](const std::string &, auto &) {}); + json_bridge::appendValue(Obj, std::string(), Anything{}); + json_bridge::set(Obj, std::string(), Anything{}); + json_bridge::pushBack(Obj, Anything{}); + { + json_bridge::create(std::string()) + } -> std::same_as; + { + json_bridge::create(1) + } -> std::same_as; + { + json_bridge::create(nullptr) + } -> std::same_as; + { + json_bridge::create() + } -> std::same_as; + }; + + // test for proper signalture of methods + template + concept isReadCompatible = isBasicallyReadCompatible && requires(const T &ConstObj) { + requires requires(std::string s, T &innerVal) { + json_bridge::forAll(ConstObj, [](const std::string & key, auto & value) { + // This is the CRITICAL part: + // We force a compile-time check on the nested value + static_assert(isBasicallyWriteCompatible>); + }); + }; + isBasicallyWriteCompatible< + std::remove_cvref_t::parse(std::string()))>>; + isBasicallyWriteCompatible< + std::remove_cvref_t::at(ConstObj, std::string()))>>; + isBasicallyWriteCompatible::at(ConstObj, 1))>>; + isBasicallyWriteCompatible::array())>>; + isBasicallyWriteCompatible::object())>>; + + // To check recursive compatibility without crashing the compiler: + requires requires { + typename json_bridge::is_json_bridge_type; // A simple tag + }; + }; + + // test for proper signalture of methods + template + concept isWriteCompatible = + isReadCompatible && !std::is_const_v && isBasicallyWriteCompatible + && requires(T &Obj) { + requires requires(std::string s, T &innerVal) { + json_bridge::mutateForAll(Obj, [](const std::string & key, auto & value) { + // This is the CRITICAL part: + // We force a compile-time check on the nested value + static_assert( + isBasicallyWriteCompatible>); + }); + }; + }; + + template + struct backend; + + template + struct isBackend : std::false_type {}; + + template + struct isBackend> : std::true_type {}; + + template + inline constexpr bool isBackend_v = isBackend::value; + + // test to see of type T is of the form backend + template + struct extractBackendType + { + using type = void; + static constexpr bool isBackend = false; + }; + + // Specialization for your backend + template + struct extractBackendType> + { + using type = std::remove_cvref_t; + static constexpr bool isBackend = true; + }; + + // test to see if T is of form backend with isReadCompatible (i.e X is const) + template + concept isReadCompatibleBackend = [] { + using Extract = extractBackendType>; + if constexpr (Extract::isBackend) { + // Check if the inner type X is compatible + return isReadCompatible; + } + return false; + }(); + + // test to see if T is of form backend with isWriteCompatible + template + concept isWriteCompatibleBackend = [] { + using Extract = extractBackendType>; + if constexpr (Extract::isBackend) { + // Check if the inner type X is compatible + return isWriteCompatible; + } + return false; + }(); + + template + struct backend + { + // Use the base type for bridge lookups + using BaseT = std::remove_cvref_t; + using sortedPartner = backend::sortedPartner>; + + backend() {} + backend(std::nullptr_t) {} + + backend(const backend &) = default; + backend(backend &&) noexcept = default; + + template + // Ensure U is compatible with T to avoid greedy matching + requires std::is_convertible_v *, T *> + explicit backend(U &&v) noexcept + : val(std::forward(v)) + {} + + template + // Ensure U is compatible with T to avoid greedy matching + requires isWriteCompatibleBackend + explicit backend(U &v) noexcept + : val(v.val) + {} + + template + requires(!std::is_convertible_v *, T *>) + backend(U &&value) + { + // Use the bridge to turn the literal into a real JSON object + val.emplace(json_bridge::create(std::forward(value))); + } + + operator T &() { + if( ! val ) { + val.emplace(); + } + return *val; + } + + operator const T &() const { + if( ! val ) { + val.emplace(); + } + return *val; + } + + // to unwrap explicitely + template + auto &&operator*(this Self &&self) + { + return *self.val; + } + + static auto create(auto &&value) + { + return wrap(json_bridge::create(std::forward(value))); + } + + static auto create() { return wrap(json_bridge::create()); } + + backend &operator=(backend &&other) + { + val = std::move(other.val); + return *this; + } + + backend &operator=(const backend &other) + { + val = other.val; + return *this; + } + + bool isEmpty() const + requires isReadCompatible + { + return ! val || json_bridge::isEmpty(*val); + } + + bool isObject() const + requires isReadCompatible + { + return val && json_bridge::isObject(*val); + } + + size_t size() const + requires isReadCompatible + { + return (val) ? json_bridge::size(*val) : 0; + } + + bool isArray() const + requires isReadCompatible + { + return val && json_bridge::isArray(*val); + } + + bool contains(const std::string &k) const + requires isReadCompatible + { + return val && json_bridge::contains(*val, k); + } + + bool isString() const + requires isReadCompatible + { + return val && json_bridge::isString(*val); + } + + bool isNull() const + requires isReadCompatible + { + return !val || json_bridge::isNull(*val); + } + + bool isBool() const + requires isReadCompatible + { + return val && json_bridge::isBool(*val); + } + + bool isNumber() const + requires isReadCompatible + { + return val && json_bridge::isNumber(*val); + } + + bool isInteger() const + requires isReadCompatible + { + return val && json_bridge::isInteger(*val); + } + + bool isFloat() const + requires isReadCompatible + { + return val && json_bridge::isFloat(*val); + } + + bool isUnsignedInteger() const + requires isReadCompatible + { + return val && json_bridge::isUnsignedInteger(*val); + } + + auto at(auto key) + requires isReadCompatible + { + if (!val) { + throw "Backend has no value"; + } + return wrap(json_bridge::at(*val, key)); + } + + auto operator[](const std::string &k) const + requires isReadCompatible + { + if (!val) { + throw "Backend has no value"; + } + return wrap(json_bridge::at(*val, k)); + } + + auto operator[](int k) const + requires isReadCompatible + { + if (!val) { + throw "Backend has no value"; + } + return wrap(json_bridge::at(*val, k)); + } + + template + outType get() const + requires isReadCompatible + { + if (!val) { + throw "Backend has no value"; + } + return json_bridge::template get(*val); + } + + static auto parse(const std::string &s) + requires isReadCompatible + { + return wrap(json_bridge::parse(s)); + } + + // apply action to all items in any type of collection allow mutation of value + void mutateForAll(auto action) + requires isWriteCompatible + { + if (!val) { + return; + } + json_bridge::mutateForAll(*val, [action](const std::string &k, T &val) { + auto b = wrap(val); + auto rv = action(k, b); + if (rv) { + val = *b; + } + return rv; + }); + } + + // apply action to all items in any type of collection + void forAll(auto action) const + requires isReadCompatible + { + if (!val) { + return; + } + json_bridge::forAll(*val, [action](const std::string &k, const T &val) { + action(k, wrap(val)); + }); + } + + void set(const auto &key, auto value) + requires isWriteCompatible + { + if (!val) { + *this = std::move(object()); + } + json_bridge::set(*val, key, value); + } + + std::string dump() const + requires isReadCompatible + { + if (!val) { + return ""; + } + return json_bridge::dump(*val); + } + + template + bool getPropertyValueOfType(const std::string &propertyName, PropT &&propertyValue) const + requires isReadCompatible + { + + using rawPropT = std::decay_t; + + if (!val) { + return false; + } + + // Detect if we are dealing with a TaggedProperty (TaggedProperty is always converted to a backend(BaseT) + if constexpr (isTaggedProperty_v) { + // Extract the backend object of the property and the Tag + using Tag = typename rawPropT::tagType; + // return type requested by the property + using objType = typename std::decay_t; + + // local copy of returned value (if any value is returned) + BaseT Buffer; + + // Repackage buffer to pass tag to the bridge + auto repackaged = TaggedProperty{Buffer}; + + if( json_bridge::getPropertyValueOfType(*val, + propertyName, + repackaged) ) { + // data returned in repackage + if constexpr (isOptional_v) { + propertyValue.value.emplace( wrap( std::forward( Buffer )) ); + } else { + propertyValue.value.val.emplace(Buffer); + } + return true; + } // else no value + // 2. NEW SECTION: Handling backend or std::optional> + } else if constexpr (isBackend_v>) { + using objType = rawPropT; + + BaseT Buffer; + // We fetch the raw BaseT (json) first + if (json_bridge::getPropertyValueOfType(*val, propertyName, Buffer)) { + if constexpr (isOptional_v) { + // Construct the backend wrapper inside the optional + propertyValue.emplace(); + // transfer the baseT + propertyValue->val.emplace( std::move(Buffer) ); + } else { + // Direct assignment (invokes backend constructor/assignment) + propertyValue.val.emplace( std::move(Buffer) ); + } + return true; + } + } else { + using objType = typename std::decay_t; + // get base type + using baseType = unwrapType_t< std::remove_cvref_t >; + + baseType Buffer; + + if( json_bridge::getPropertyValueOfType(*val, propertyName, Buffer) ) { + // data returned in repackage + if constexpr (isOptional_v) { + propertyValue.emplace( std::forward( Buffer ) ); + } else { + propertyValue = std::move(Buffer); + } + return true; + } + } + return false; + } + + void push_back(auto value) + requires isWriteCompatible + { + if (!val) { + return; + } + return json_bridge::pushBack(*val, value); + } + + void appendValue(const std::string &K, auto value) + requires isWriteCompatible + { + if (!val) { + return; + } + json_bridge::appendValue(*val, K, value); + } + + static auto array() + requires isReadCompatible + { + return wrap(json_bridge::array()); + } + + static auto object() + requires isReadCompatible + { + return wrap(json_bridge::object()); + } + + static auto array(std::initializer_list> args); + + static auto object(std::initializer_list>> args); + + // This allows: if (myJson != nullptr) + bool operator!=(std::nullptr_t) const { return !isNull(); } + + bool operator==(std::nullptr_t) const { return isNull(); } + + bool operator!=(const backend &O) const + { + return ! json_bridge::EQ(*val, *(O.val)); + } + + bool operator==(const backend &O) const + { + return json_bridge::EQ(*val, *(O.val)); + } + + private: + mutable std::optional val; + + // Helper to wrap the result of doAt + static auto wrap(auto &&result) + { + // We use remove_cvref_t to get the base type for the template + using RawType = std::remove_cvref_t; + // C++23 way to ensure we preserve the 'const' and 'reference' nature + // of the result exactly as it came out of the bridge + return backend>( + std::forward(result)); + } + }; + + // Deduction guide to make it easy to use + template + backend(T &) -> backend; + + template + backend(T *) -> backend; + + template + concept isPairLike = requires(T t) { + typename T::first_type; + typename T::second_type; + t.first; + t.second; + }; + + template + concept isMappedContainer = requires { typename std::remove_cvref_t::value_type; } + && isPairLike::value_type>; + + template + concept isListContainer = !isMappedContainer; + + // apply action to all items in any type of collection + static void forAll(const auto &from, auto action) + { + using fromCollection = std::decay_t; + using actionType = std::decay_t; + + if constexpr (isReadCompatible) { + jsonata::backend(from).forAll(action); + } else if constexpr (isReadCompatibleBackend) { + from.forAll(action); + } else if constexpr (isMappedContainer) { + for (const auto &[k, v] : from) { + action(k, v); + } + } else if constexpr (isListContainer) { + int idx = 0; + for (const auto &el : from) { + action(std::to_string(idx++), el); + } + } else { + // This triggers ONLY if none of the above branches are taken + static_assert(sizeof(fromCollection) == 0, "Unsupported outCollection type."); + } + } + + static void copy(const auto &from, auto &out) + { + forAll(from, [&out](auto k, auto v) { + using outType = std::decay_t; + using VType = std::decay_t; + using KType = std::decay_t; + + if constexpr (isWriteCompatible) { + backend(out).appendValue(k, v); + } else if constexpr (isWriteCompatibleBackend) { + out.appendValue(k, v); + } else if constexpr (isMappedContainer) { + out[k] = v; + } else if constexpr (isListContainer) { + out.push_back(v); + } else { + // This triggers ONLY if none of the above branches are taken + static_assert( + sizeof(outType) == 0, + "Unsupported outCollection type: Must be initializer, Qt " "JSON, " "nlohmann " "JSON, a " "Map, or must " "support " "push" "_bac" "k."); + } + }); + } + + static void copy(const auto &from, auto &out, auto convert) + { + using outType = std::decay_t; + + forAll(from, [&out, convert](const std::string &k, auto v) { + auto cv = convert(v); + using VType = std::decay_t; + using KType = std::decay_t; + + if constexpr (isWriteCompatible) { + backend(out).appendValue(k, cv); + } else if constexpr (isWriteCompatibleBackend) { + out.appendValue(k, cv); + } else if constexpr (isMappedContainer) { + out[k] = convert(v); + } else if constexpr (isListContainer) { + out.push_back(convert(v)); + } else { + // This triggers ONLY if none of the above branches are taken + static_assert( + sizeof(outType) == 0, + "Unsupported outCollection type: Must be Qt JSON, nlohmann " "JSON, " "a Map, " "or must " "support " "push_back."); + } + }); + } + + template + auto backend::array(std::initializer_list> args) + { + auto a = array(); + copy(args, *a); + return a; + } + + template + auto backend::object(std::initializer_list>> args) + { + auto o = object(); + copy(args, *o); + return o; + } +} // namespace jsonata diff --git a/include/jsonata/common_backend.h b/include/jsonata/common_backend.h new file mode 100644 index 0000000..061409a --- /dev/null +++ b/include/jsonata/common_backend.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + +// for jlIST +template +struct is_vector : std::false_type +{}; + +template +struct is_vector> : std::true_type +{}; + +template +concept isStdVector = is_vector::value; + +// This is the "Contract" class which we will specify based on actual json classes (see qjson_backend e.a.) +namespace jsonata +{ + template + struct json_bridge_impl; + + template + struct json_bridge_impl {}; + + template + using json_bridge = + json_bridge_impl,void>; +} + +// for special conversion in getPropertyValueOfType +template +struct TaggedProperty { + using valueType = T; + using tagType = Tag; + T& value; +}; + +// Convenience helpers +struct AsArray {}; + +template auto Array(T& v) { return TaggedProperty{v}; } +// use as x.getPropertyValueOfType( "name", Array(destination) ); + +template struct isTaggedProperty : std::false_type {}; + +template +struct isTaggedProperty> : std::true_type {}; + +template +inline constexpr bool isTaggedProperty_v = isTaggedProperty::value; + +#include +#include + +// 1. The base case: any type that is NOT a std::optional +template +struct isOptional : std::false_type {}; + +// 2. The specialization: if the type is std::optional, it matches this +template +struct isOptional> : std::true_type {}; + +// 3. The helper variable (the "_v" version) +template +inline constexpr bool isOptional_v = isOptional>::value; + +// +// given a type T of either std::optional or X, extract X +// + +// 1. The "Default" case: If it's not an optional, just return the type itself. +template +struct unwrapType { + using type = T; +}; + +// 2. The "Special" case: If it IS an optional, extract the inner value_type. +template +struct unwrapType> { + using type = T; +}; + +// Helper alias to keep your code clean +template +using unwrapType_t = typename unwrapType::type; + + diff --git a/include/jsonata/nlohmann_backend.h b/include/jsonata/nlohmann_backend.h new file mode 100644 index 0000000..2731d68 --- /dev/null +++ b/include/jsonata/nlohmann_backend.h @@ -0,0 +1,217 @@ +#pragma once + +#include "Utils.h" +#include + +#include "common_backend.h" + +#include + +#include +#include +#include + +template +concept isNlohmann = requires(T j) { + typename T::value_type; + { j.is_object() } -> std::same_as; + { j.is_array() } -> std::same_as; + // This is the "fingerprint" of a nlohmann-like container + T::array(); + }; + +namespace jsonata +{ +// Single bridge specialization +template + requires isNlohmann +struct json_bridge_impl +{ + using BaseT = std::remove_cvref_t; + using is_json_bridge_type = void; // Tag to satisfy the concept + // all nlohmann have json a sorted version + using sortedPartner = nlohmann::json; + + static BaseT create(auto&& value) { + using V = std::decay_t; + + if constexpr (std::is_same_v) { + return BaseT(); + } else if constexpr (std::is_constructible_v) { + return value; + } else { + static_assert(false, "Unsupported type for nlohmann creation"); + } + } + + static BaseT create() { return BaseT(); } + + static size_t size(const BaseT &value) { return value.size(); } + + static bool isEmpty(const BaseT &value) { return value.empty(); } + + static bool isObject(const BaseT &value) { return value.is_object(); } + + static bool isArray(const BaseT &value) { return value.is_array(); } + + static bool isNumber(const BaseT &value) { return value.is_number(); } + + static bool isInteger(const BaseT &value) { return value.is_number_integer(); } + + static bool isFloat(const BaseT &value) { return value.is_number_float(); } + + static bool isUnsignedInteger(const BaseT &value) { return value.is_number_unsigned(); } + + static bool isBool(const BaseT &value) { return value.is_boolean(); } + + static bool isString(const BaseT &value) { return value.is_string(); } + + static bool isNull(const BaseT &value) { return value.is_null(); } + + static bool EQ(const BaseT &v1,const BaseT & v2) { return v1 == v2; } + + static bool contains(const BaseT &value, const std::string &k) { return value.contains(k); } + + static decltype(auto) at(const BaseT &value, const std::string &k) { return value[k]; } + + static decltype(auto) at(const BaseT &value, int k) { return value[k]; } + + template + static outType get(const BaseT &value) + { + return value.template get(); + } + + static BaseT parse(const std::string &s) { return T::parse(s); } + + static BaseT array() { return BaseT::array(); } + + static BaseT object() { return BaseT::object(); } + + static void pushBack(BaseT &val, auto value) { val.push_back(value); } + + template + static void mutateForAll(BaseT &from, action a) + { + if (from.is_array()) { + int idx = 0; + for (const auto &el : from) { + a(std::to_string(idx++), el); + } + } else { + for (const auto &[k, v] : from) { + a(k, v); + } + } + return; + } + + template + static void forAll(const BaseT &from, action a) + { + using fromType = std::decay_t; + if constexpr (std::is_same_v< + fromType, + std::initializer_list>> + || std::is_same_v< + fromType, + std::initializer_list>>) { + for (const auto &[k, v] : from) { + action(k, v); + } + } else if constexpr (std::is_same_v> + || std::is_same_v>) { + int idx = 0; + for (const auto &el : from) { + action(std::to_string(idx++), el); + } + } else if (from.is_array()) { + int idx = 0; + for (const auto &el : from) { + a(std::to_string(idx++), el); + } + } else { + for (const auto &[k, v] : from.items()) { + a(k, v); + } + } + } + + template + static void appendValue(BaseT &to, const keyType &k, const valueType &v) + { + if (to.is_array()) { + to.push_back(v); + } else { + to[k] = v; + } + } + + template + static void set(BaseT &to, const keyType &k, const valueType &v) + { + to[k] = v; + } + + static std::string dump(const BaseT &v) { return v.dump(); } + + static bool getPropertyValueOfType(const BaseT &root, + const std::string &propertyName, + auto &propertyValue) + { + using propertyType = std::decay_t; + auto convertor = [&](auto E, auto &Prop) { + using EType = std::decay_t; // This is the type of E + + if constexpr ( isTaggedProperty_v ) { + // special conversion -> Array + if (E.is_array()) { + propertyValue.value = E; + return true; + } + return false; + } else if constexpr (std::is_base_of_v) { + // property must be string + if (E.is_string()) { + Prop = E.template get(); + return true; + } + } else if constexpr (std::is_same_v) { + if (E.is_boolean()) { + Prop = E.template get(); + return true; + } + } else if constexpr (isNlohmann) { + Prop = E; + return true; + } else { + // This triggers ONLY if none of the above branches are taken + static_assert(sizeof(propertyType) == 0, "cannot get unknown propertytype"); + } + + return false; + }; + + auto overrideIt = root.find(propertyName); + return (overrideIt != root.end()) ? convertor((*overrideIt), propertyValue) : false; + } + + // static bool getPropertyValueOfType(const BaseT &root, + // const std::string &propertyName, + // TaggedProperty propertyValue) + // { + // auto convertor = [&](auto E, TaggedProperty Prop) { + // using EType = std::decay_t; // This is the type of E + // if (E.is_array()) { + // Prop.value = E; + // return true; + // } + // return false; + // }; + + // auto overrideIt = root.find(propertyName); + // return (overrideIt != root.end()) ? convertor((*overrideIt), propertyValue) : false;; + // } +}; +} \ No newline at end of file diff --git a/include/jsonata/ordered_map.h b/include/jsonata/ordered_map.h new file mode 100644 index 0000000..e58b5cb --- /dev/null +++ b/include/jsonata/ordered_map.h @@ -0,0 +1,370 @@ +// From nlohmann/json adapted for jsonata +// Refactored to Modern C++23 + +#pragma once + +#include // equal_to, less +#include // initializer_list +#include // input_iterator_tag, distance, next, prev +#include // allocator +#include // remove_cvref_t, is_same_v, is_convertible_v +#include // pair, move, forward +#include // vector +#include "JException.h" + +namespace jsonata { + + // C++20/23 Concept checking if Compare functor can compare types A and B interchangeably + template + concept ComparableWith = requires(Compare comp, A&& a, B&& b) { + { comp(std::forward(a), std::forward(b)) }; + { comp(std::forward(b), std::forward(a)) }; + }; + + // Simplified C++23 trait evaluating if a generic type can be safely used as a lookup key + template + struct is_usable_as_key_type { + using KeyType = std::remove_cvref_t; + + // Mocking nlohmann's internally expected detection traits + // (If using nlohmann framework directly, adjust detect_is_transparent checks as needed) + static constexpr bool is_transparent = requires { typename Comparator::is_transparent; }; + + static constexpr bool value = + ComparableWith && + !(ExcludeObjectKeyType && std::is_same_v) && + (!RequireTransparentComparator || is_transparent); + }; + + /// ordered_map: a minimal map-like container that preserves insertion order + template , + class Allocator = std::allocator>> + struct ordered_map : std::vector, Allocator> + { + using key_type = Key; + using mapped_type = T; + using Container = std::vector, Allocator>; + using iterator = typename Container::iterator; + using const_iterator = typename Container::const_iterator; + using size_type = typename Container::size_type; + using value_type = typename Container::value_type; + using key_compare = std::equal_to; + + // Default constructors + ordered_map() noexcept(noexcept(Container())) : Container{} {} + explicit ordered_map(const Allocator& alloc) noexcept(noexcept(Container(alloc))) : Container{alloc} {} + + template + ordered_map(It first, It last, const Allocator& alloc = Allocator()) + : Container{first, last, alloc} {} + + ordered_map(std::initializer_list init, const Allocator& alloc = Allocator()) + : Container{init, alloc} {} + + ordered_map(const ordered_map&) = default; + ordered_map(ordered_map&&) noexcept(std::is_nothrow_move_constructible_v) = default; + ~ordered_map() = default; + + ordered_map& operator=(const ordered_map& other) + { + if (this != &other) + { + ordered_map tmp(other); + Container::operator=(std::move(static_cast(tmp))); + } + return *this; + } + + ordered_map& operator=(ordered_map&& other) noexcept(std::is_nothrow_move_assignable_v) + { + Container::operator=(std::move(static_cast(other))); + return *this; + } + + // Standard Emplace + std::pair emplace(const key_type& key, T&& t) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return {it, false}; + } + } + Container::emplace_back(key, std::forward(t)); + return {std::prev(this->end()), true}; + } + + // C++23 Heterogeneous Emplace using a 'requires' clause constraint + template + requires (is_usable_as_key_type::value) + std::pair emplace(KeyType&& key, T&& t) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return {it, false}; + } + } + Container::emplace_back(std::forward(key), std::forward(t)); + return {std::prev(this->end()), true}; + } + + // Element Access Operators + T& operator[](const key_type& key) + { + return emplace(key, T{}).first->second; + } + + template + requires (is_usable_as_key_type::value) + T& operator[](KeyType&& key) + { + return emplace(std::forward(key), T{}).first->second; + } + + const T& operator[](const key_type& key) const + { + return at(key); + } + + template + requires (is_usable_as_key_type::value) + const T& operator[](KeyType&& key) const + { + return at(std::forward(key)); + } + + // At Methods + T& at(const key_type& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it->second; + } + } + throw JException("key not found"); + } + + template + requires (is_usable_as_key_type::value) + T& at(KeyType&& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it->second; + } + } + throw JException("key not found"); + } + + const T& at(const key_type& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it->second; + } + } + throw JException("key not found"); + } + + template + requires (is_usable_as_key_type::value) + const T& at(KeyType&& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it->second; + } + } + throw JException("key not found"); + } + + // Erase Operations + size_type erase(const key_type& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + for (auto next = it; ++next != this->end(); ++it) + { + it->~value_type(); + new (&*it) value_type{std::move(*next)}; + } + Container::pop_back(); + return 1; + } + } + return 0; + } + + template + requires (is_usable_as_key_type::value) + size_type erase(KeyType&& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + for (auto next = it; ++next != this->end(); ++it) + { + it->~value_type(); + new (&*it) value_type{std::move(*next)}; + } + Container::pop_back(); + return 1; + } + } + return 0; + } + + iterator erase(iterator pos) + { + return erase(pos, std::next(pos)); + } + + iterator erase(iterator first, iterator last) + { + if (first == last) + { + return first; + } + + const auto elements_affected = std::distance(first, last); + const auto offset = std::distance(Container::begin(), first); + + for (auto it = first; std::next(it, elements_affected) != Container::end(); ++it) + { + it->~value_type(); + new (&*it) value_type{std::move(*std::next(it, elements_affected))}; + } + + Container::resize(this->size() - static_cast(elements_affected)); + return Container::begin() + offset; + } + + // Count and Find Lookup Operations + size_type count(const key_type& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return 1; + } + } + return 0; + } + + template + requires (is_usable_as_key_type::value) + size_type count(KeyType&& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return 1; + } + } + return 0; + } + + iterator find(const key_type& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it; + } + } + return Container::end(); + } + + template + requires (is_usable_as_key_type::value) + iterator find(KeyType&& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it; + } + } + return Container::end(); + } + + const_iterator find(const key_type& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it; + } + } + return Container::end(); + } + + template + requires (is_usable_as_key_type::value) + const_iterator find(KeyType&& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, key)) + { + return it; + } + } + return Container::end(); + } + + // Insert Operations + std::pair insert(value_type&& value) + { + return emplace(value.first, std::move(value.second)); + } + + std::pair insert(const value_type& value) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (m_compare(it->first, value.first)) + { + return {it, false}; + } + } + Container::push_back(value); + return {--this->end(), true}; + } + + // C++20/23 Concept constrained range input insertion instead of require_input_iter traits + template + requires std::is_convertible_v::iterator_category, std::input_iterator_tag> + void insert(InputIt first, InputIt last) + { + for (auto it = first; it != last; ++it) + { + insert(*it); + } + } + + private: + key_compare m_compare = key_compare(); + }; + +} // namespace jsonata \ No newline at end of file diff --git a/include/jsonata/qtjson_backend.h b/include/jsonata/qtjson_backend.h new file mode 100644 index 0000000..c7f360e --- /dev/null +++ b/include/jsonata/qtjson_backend.h @@ -0,0 +1,417 @@ +#pragma once + +#include "JException.h" +#include "Utils.h" +#include + +#include "common_backend.h" + +// #include +// #include +#include +#include +#include +#include +#include + +template +concept IsQtJson = std::is_base_of_v || std::is_base_of_v + || std::is_base_of_v || std::is_base_of_v; + +namespace jsonata +{ +template<> +struct json_bridge_impl +{ + using is_json_bridge_type = void; // Tag to satisfy the concept + using sortedPartner = QJsonValue; + + static QJsonValue create(auto &&value) + { + using V = std::decay_t; + + if constexpr (std::is_same_v) { + return QJsonValue(QJsonValue::Null); + } else if constexpr (std::is_same_v || std::is_same_v) { + // Handle both std::string and literal "strings" + if constexpr (std::is_same_v) + return QJsonValue(QString(value)); + else + return QJsonValue(QString::fromStdString(value)); + } else if constexpr (std::is_integral_v && !std::is_same_v) { + // Handle all integers (int, long, long long, size_t, etc.) + // qlonglong is the safest sink for integral types in Qt + return QJsonValue(static_cast(value)); + } + // Handle floating point + else if constexpr (std::is_floating_point_v) { + return QJsonValue(static_cast(value)); + } else if constexpr (std::is_constructible_v) { + return value; + } else { + static_assert(false, "Unsupported type for QJsonValue creation"); + } + } + + static QJsonValue create() { return QJsonValue(); } + + static size_t size(const QJsonValue &value) + { + return value.isArray() ? value.toArray().count() : 0; + } + + static bool isEmpty(const QJsonValue &value) { + return value.isNull() || + (value.isObject() && value.toObject().empty()) || + (value.isArray() && value.toArray().empty()); + } + + static bool isObject(const QJsonValue &value) { return value.isObject(); } + + static bool isArray(const QJsonValue &value) { return value.isArray(); } + + static bool isNumber(const QJsonValue &value) { return value.isDouble() ; } + static bool isInteger(const QJsonValue &value) + { + return value.isDouble() && value.toVariant().typeId() == QMetaType::LongLong; + } + + static bool isFloat(const QJsonValue &value) + { + return value.isDouble() && value.toVariant().typeId() == QMetaType::Double; + } + + static bool isUnsignedInteger(const QJsonValue &value) + { + if (!value.isDouble()) { + return false; + } + QVariant V = value.toVariant(); + return V.typeId() == QMetaType::LongLong && V.toLongLong() >= 0; + } + + static bool isBool(const QJsonValue &value) { return value.isBool(); } + + static bool isString(const QJsonValue &value) { return value.isString(); } + + static bool isNull(const QJsonValue &value) { return value.isNull(); } + + static bool EQ(const QJsonValue &v1,const QJsonValue & v2) { return v1 == v2; } + + static bool contains(const QJsonValue &value, const std::string &k) + { + return value.isObject() && value.toObject().contains(QString::fromStdString(k)); + } + + static QJsonValue at(const QJsonValue &value, const std::string &k) + { + if (!value.isObject()) { + throw jsonata::JException("Value is not an object"); + } + return value.toObject()[QString::fromStdString(k)]; + } + + static QJsonValue at(const QJsonValue &value, int k) + { + if (!value.isArray()) { + throw jsonata::JException("Value is not an array"); + } + return value.toArray()[k]; + } + + template + static outType get(const QJsonValue &value) + { + return value.toVariant().template value(); + } + + template<> + std::string get(const QJsonValue &value) + { + return get(value).toStdString(); + } + + static QJsonValue array() { return QJsonArray(); } + + static QJsonValue object() { return QJsonObject(); } + + static void pushBack(QJsonValue &val, auto value) + { + QJsonArray a = val.toArray(); + a.push_back(value); + val = a; + } + + template + static void mutateForAll(QJsonValue &from, action a) + { + using fromType = std::decay_t; + if (from.isArray()) { + int idx = 0; + bool changed = false; + QJsonArray arr = from.toArray(); + for (auto el : arr) { + if (a(std::to_string(idx++), el)) { + arr[idx - 1] = el; + changed = true; + } + } + if (changed) { + from = arr; + } + } else if (from.isObject()) { + bool changed = false; + QJsonObject obj = from.toObject(); + for (auto [key, value] : obj.asKeyValueRange()) { + QJsonValue J(value); + if (a(key.toString().toStdString(), J)) { + value = J; + changed = true; + } + } + if (changed) { + from = obj; + } + } + } + + template + static void forAll(const QJsonValue &from, action a) + { + if (from.isArray()) { + int idx = 0; + for (const auto &el : from.toArray()) { + a(std::to_string(idx++), QJsonValue(el)); + } + } else if (from.isObject()) { + QJsonObject obj = from.toObject(); + for (auto [key, value] : obj.asKeyValueRange()) { + a(key.toString().toStdString(), QJsonValue(value)); + } + } + } + + template + static void appendValue(QJsonValue &to, const keyType &k, const valueType &v) + { + if (to.isArray()) { + auto x = to.toArray(); + if constexpr (std::is_convertible_v) { + x.push_back(QString::fromStdString(v)); + } else if constexpr (!std::is_convertible_v) { + x.push_back(v); + } + to = x; + } else { + auto x = to.toObject(); + if constexpr (std::is_convertible_v) { + x[QString::fromStdString(k)] = QString::fromStdString(v); + } else if constexpr (!std::is_convertible_v) { + x[QString::fromStdString(k)] = v; + } + to = x; + } + } + + template + static void set(QJsonValue &to, const keyType &k, const valueType &v) + { + if constexpr (std::is_same_v) { + auto x = to.toObject(); + if constexpr (std::is_convertible_v) { + x[k] = QString::fromStdString(v); + } else { + x[k] = v; + } + to = x; + } else if constexpr (std::is_convertible_v) { + auto x = to.toObject(); + if constexpr (std::is_convertible_v) { + x[QString::fromStdString(k)] = QString::fromStdString(v); + } else { + x[QString::fromStdString(k)] = v; + } + to = x; + } else if constexpr (std::is_convertible_v) { + auto x = to.toArray(); + if constexpr (std::is_convertible_v) { + x[k] = QString::fromStdString(v); + } else { + x[k] = v; + } + to = x; + } + } + + static std::string dump(const QJsonValue &v) + { + return QString::fromUtf8(QJsonValue(v).toJson(QJsonValue::JsonFormat::Compact)).toStdString(); + } + + static QJsonValue parse(const std::string &s) + { + auto convert = []( const QString & input, QString & ok ) -> QString { + + auto utf = []( const QStringView & V, qsizetype off, QString & errorCode ) { + bool ok; + ushort code = V.sliced( off, 4 ).toUShort(&ok, 16); + if( ok ) { + return QChar(code); + } + errorCode = "S0104"; // invalid \\u sequence + return QChar(); + }; + + auto validUtfSequence = [utf]( const QStringView & v, + qsizetype len, qsizetype off, const QString & match, QString & errorCode ) -> qsizetype { + off += match.length(); + QChar f = utf( v, off, errorCode ); + off += 4; + if( ! errorCode.isEmpty() ) { + return off; + } + if( f.isSurrogate( ) ) { + if( (off + (4+match.length())) >= len || v.sliced( off, match.length() ) != match ) { + errorCode = "S3141"; // invalid utf character sequence + return off; + } + off += match.length(); + QChar s = utf( v, off, errorCode ); + off += 4; + if( ! errorCode.isEmpty() ) { + return off; + } + if( ! f.isHighSurrogate() || ! s.isLowSurrogate() ) { + errorCode = "S3141"; // invalid utf character sequence + return off; + } + } + return off; + }; + + QString errorCode; + QString out; + bool dQuoted = false; + qsizetype i = 0; + QStringView v(input); + + while( i < input.length() ) { + if( input[i] == '\\' ) { + + if( i+1< input.length() && v.sliced( i, 2 ) == "\\\"" ) { + dQuoted = ! dQuoted; + out += '"'; + i += 2; + continue; + } + + if( dQuoted ) { + if( i+6< input.length() && v.sliced( i, 3 ) == "\\\\u" ) { + i = validUtfSequence( v, input.length(), i, "\\\\u", errorCode ); + if( ! errorCode.isEmpty() ) { + break; + } + } + + if( i+1< input.length() && v.sliced( i, 2 ) == "\\\\" ) { + out += '\\'; + i += 2; + continue; + } + } else { + if( i+5< input.length() && v.sliced( i, 2 ) == "\\u" ) { + // start of utf16 char + i = validUtfSequence( v, input.length(), i, "\\u", ok ); + if( ! errorCode.isEmpty() ) { + break; + } + } + } + + // remove \ will insert next char + i ++; + continue; + } + + out += input[i]; + i ++; + } + return out; + }; + + QString errorCode; + QString str = QString::fromStdString( s ); + QString convertedStr = convert( str, errorCode ); + if( ! errorCode.isEmpty() ) { + // It's a lone surrogate! (Like your \uD800 test case) + throw jsonata::JException( errorCode.toStdString(), 0); + } + + QJsonParseError Err; + auto r = QJsonValue::fromJson(str.toUtf8(), &Err); + if (Err.error != QJsonParseError::NoError) { + throw jsonata::JException("D3141", Err.offset, Err.errorString().data()); + } + return r; + } + + // for simple types + static bool getPropertyValueOfType(const QJsonValue &root, + const std::string &propertyName, + auto &propertyValue) + { + using propertyType = std::decay_t; + + QJsonObject obj = root.toObject(); + QJsonValue E = obj.value(QString::fromStdString(propertyName)); + + if( E.isUndefined() ) { + // propertyname does not exist + return false; + } + + if constexpr (std::is_base_of_v) { + // property must be string + if (E.isString()) { + propertyValue = E.toString().toStdString(); + return true; + } + } else if constexpr (std::is_same_v) { + if (E.isBool()) { + propertyValue = E.toBool(); + return true; + } + } else if constexpr (IsQtJson) { + propertyValue = E; + return true; + } else { + // This triggers ONLY if none of the above branches are taken + static_assert(sizeof(propertyType) == 0, "cannot get unknown propertytype"); + } + + return false; + } + + static bool getPropertyValueOfType(const QJsonValue &root, + const std::string &propertyName, + TaggedProperty propertyValue) + { + QJsonObject obj = root.toObject(); + QJsonValue E = obj.value(QString::fromStdString(propertyName)); + + if( E.isUndefined() ) { + // propertyname does not exist + return false; + } + + if (E.isArray()) { + // value MUST be an array + propertyValue.value = E.toArray(); + return true; + } + return false; + } + +}; + +} diff --git a/include/jsonata/qtvariant_backend.h b/include/jsonata/qtvariant_backend.h new file mode 100644 index 0000000..cb1be2f --- /dev/null +++ b/include/jsonata/qtvariant_backend.h @@ -0,0 +1,455 @@ +#pragma once + +#include "JException.h" +#include "Utils.h" +#include + +#include "common_backend.h" + +// #include +// #include +#include +#include +#include +#include +#include + +template +concept IsQVariant = std::is_base_of_v; + +namespace jsonata +{ + template<> + struct json_bridge_impl + { + using is_json_bridge_type = void; // Tag to satisfy the concept + using sortedPartner = QVariant; + + static QVariant create(auto &&value) + { + using V = std::decay_t; + + if constexpr (std::is_same_v) { + return QVariant(); + } else if constexpr (std::is_same_v || std::is_same_v) { + // Handle both std::string and literal "strings" + if constexpr (std::is_same_v) + return QVariant(QString(value)); + else + return QVariant(QString::fromStdString(value)); + } else if constexpr (std::is_integral_v && !std::is_same_v) { + // Handle all integers (int, long, long long, size_t, etc.) + // qlonglong is the safest sink for integral types in Qt + return QVariant(static_cast(value)); + } + // Handle floating point + else if constexpr (std::is_floating_point_v) { + return QVariant(static_cast(value)); + } else if constexpr (std::is_constructible_v) { + return value; + } else { + static_assert(false, "Unsupported type for QVariant creation"); + } + } + + static QVariant create() { return QVariant(); } + + static size_t size(const QVariant &value) + { + return isArray(value) ? value.toList().size() : 0; + } + + static bool isEmpty(const QVariant &value) { + return value.isNull() || + ( isObject( value ) && value.toMap().empty()) || + ( isArray( value ) && value.toList().empty()); + } + + static bool isObject(const QVariant &value) { return isOfType( value, QMetaType::QVariantMap ); } + + static bool isArray(const QVariant &value) { return isOfType( value, QMetaType::QVariantList ); } + + static bool isNumber(const QVariant &value) { + switch (value.typeId()) { + case QMetaType::Int: + case QMetaType::Long: + case QMetaType::LongLong: + case QMetaType::ULong: + case QMetaType::UInt: + case QMetaType::ULongLong: + case QMetaType::Double: + case QMetaType::Float: + return true; + default : + return false; + } + } + static bool isInteger(const QVariant &value) + { + switch (value.typeId()) { + case QMetaType::Int: + case QMetaType::Long: + case QMetaType::LongLong: + case QMetaType::ULong: + case QMetaType::UInt: + case QMetaType::ULongLong: + return true; + default : + return false; + } + } + + static bool isFloat(const QVariant &value) + { + switch (value.typeId()) { + case QMetaType::Double: + case QMetaType::Float: + return true; + default : + return false; + } + } + + static bool isUnsignedInteger(const QVariant &value) + { + switch (value.typeId()) { + case QMetaType::ULong: + case QMetaType::UInt: + case QMetaType::ULongLong: + return true; + default : + return false; + } + } + + static bool isBool(const QVariant &value) { return isOfType( value, QMetaType::Bool ); } + + static bool isString(const QVariant &value) { return isOfType( value, QMetaType::QString ); } + + static bool isNull(const QVariant &value) { return value.isNull(); } + + static bool EQ(const QVariant &v1,const QVariant & v2) { return v1 == v2; } + + static bool contains(const QVariant &value, const std::string &k) + { + return isObject(value) && value.toMap().contains(QString::fromStdString(k)); + } + + static QVariant at(const QVariant &value, const std::string &k) + { + if (!isObject(value)) { + throw jsonata::JException("Value is not an object"); + } + return value.toMap()[QString::fromStdString(k)]; + } + + static QVariant at(const QVariant &value, int k) + { + if (!isArray(value)) { + throw jsonata::JException("Value is not an array"); + } + return value.toList()[k]; + } + + template + static outType get(const QVariant &value) + { + return value.template value(); + } + + template<> + std::string get(const QVariant &value) + { + return get(value).toStdString(); + } + + static QVariant array() { return QVariantList(); } + + static QVariant object() { return QVariantMap(); } + + static void pushBack(QVariant &val, auto value) + { + QVariantList a = val.toList(); + a.push_back(value); + val = a; + } + + template + static void mutateForAll(QVariant &from, action a) + { + using fromType = std::decay_t; + if (isArray(from)) { + int idx = 0; + bool changed = false; + auto arr = from.toList(); + for (auto el : arr) { + if (a(std::to_string(idx++), el)) { + arr[idx - 1] = el; + changed = true; + } + } + if (changed) { + from = arr; + } + } else if (isObject(from)) { + bool changed = false; + auto obj = from.toMap(); + for (auto [key, value] : obj.asKeyValueRange()) { + QVariant J(value); + if (a(key.toStdString(), J)) { + value = J; + changed = true; + } + } + if (changed) { + from = obj; + } + } + } + + template + static void forAll(const QVariant &from, action a) + { + if (isArray(from)) { + int idx = 0; + for (const auto &el : from.toList()) { + a(std::to_string(idx++), QVariant(el)); + } + } else if (isObject(from)) { + auto obj = from.toMap(); + for (auto [key, value] : obj.asKeyValueRange()) { + a(key.toStdString(), QVariant(value)); + } + } + } + + template + static void appendValue(QVariant &to, const keyType &k, const valueType &v) + { + if (isArray(to)) { + auto x = to.toList(); + if constexpr (std::is_convertible_v) { + x.push_back(QString::fromStdString(v)); + } else if constexpr (!std::is_convertible_v) { + x.push_back(v); + } + to = x; + } else { + auto x = to.toMap(); + if constexpr (std::is_convertible_v) { + x[QString::fromStdString(k)] = QString::fromStdString(v); + } else if constexpr (!std::is_convertible_v) { + x[QString::fromStdString(k)] = v; + } + to = x; + } + } + + template + static void set(QVariant &to, const keyType &k, const valueType &v) + { + if constexpr (std::is_same_v) { + auto x = to.toMap(); + if constexpr (std::is_convertible_v) { + x[k] = QString::fromStdString(v); + } else { + x[k] = v; + } + to = x; + } else if constexpr (std::is_convertible_v) { + auto x = to.toMap(); + if constexpr (std::is_convertible_v) { + x[QString::fromStdString(k)] = QString::fromStdString(v); + } else { + x[QString::fromStdString(k)] = v; + } + to = x; + } else if constexpr (std::is_convertible_v) { + auto x = to.toList(); + if constexpr (std::is_convertible_v) { + x[k] = QString::fromStdString(v); + } else { + x[k] = v; + } + to = x; + } + } + + static std::string dump(const QVariant &v) + { + return QString::fromUtf8(QJsonValue::fromVariant(v).toJson(QJsonValue::JsonFormat::Compact)).toStdString(); + } + + static QVariant parse(const std::string &s) + { + auto convert = []( const QString & input, QString & ok ) -> QString { + + auto utf = []( const QStringView & V, qsizetype off, QString & errorCode ) { + bool ok; + ushort code = V.sliced( off, 4 ).toUShort(&ok, 16); + if( ok ) { + return QChar(code); + } + errorCode = "S0104"; // invalid \\u sequence + return QChar(); + }; + + auto validUtfSequence = [utf]( const QStringView & v, + qsizetype len, qsizetype off, const QString & match, QString & errorCode ) -> qsizetype { + off += match.length(); + QChar f = utf( v, off, errorCode ); + off += 4; + if( ! errorCode.isEmpty() ) { + return off; + } + if( f.isSurrogate( ) ) { + if( (off + (4+match.length())) >= len || v.sliced( off, match.length() ) != match ) { + errorCode = "S3141"; // invalid utf character sequence + return off; + } + off += match.length(); + QChar s = utf( v, off, errorCode ); + off += 4; + if( ! errorCode.isEmpty() ) { + return off; + } + if( ! f.isHighSurrogate() || ! s.isLowSurrogate() ) { + errorCode = "S3141"; // invalid utf character sequence + return off; + } + } + return off; + }; + + QString errorCode; + QString out; + bool dQuoted = false; + qsizetype i = 0; + QStringView v(input); + + while( i < input.length() ) { + if( input[i] == '\\' ) { + + if( i+1< input.length() && v.sliced( i, 2 ) == "\\\"" ) { + dQuoted = ! dQuoted; + out += '"'; + i += 2; + continue; + } + + if( dQuoted ) { + if( i+6< input.length() && v.sliced( i, 3 ) == "\\\\u" ) { + i = validUtfSequence( v, input.length(), i, "\\\\u", errorCode ); + if( ! errorCode.isEmpty() ) { + break; + } + } + + if( i+1< input.length() && v.sliced( i, 2 ) == "\\\\" ) { + out += '\\'; + i += 2; + continue; + } + } else { + if( i+5< input.length() && v.sliced( i, 2 ) == "\\u" ) { + // start of utf16 char + i = validUtfSequence( v, input.length(), i, "\\u", ok ); + if( ! errorCode.isEmpty() ) { + break; + } + } + } + + // remove \ will insert next char + i ++; + continue; + } + + out += input[i]; + i ++; + } + return out; + }; + + QString errorCode; + QString str = QString::fromStdString( s ); + QString convertedStr = convert( str, errorCode ); + if( ! errorCode.isEmpty() ) { + // It's a lone surrogate! (Like your \uD800 test case) + throw jsonata::JException( errorCode.toStdString(), 0); + } + + QJsonParseError Err; + auto r = QJsonValue::fromJson(str.toUtf8(), &Err); + if (Err.error != QJsonParseError::NoError) { + throw jsonata::JException("D3141", Err.offset, Err.errorString().data()); + } + return r.toVariant(); + } + + // for simple types + static bool getPropertyValueOfType(const QVariant &root, + const std::string &propertyName, + auto &propertyValue) + { + using propertyType = std::decay_t; + + auto obj = root.toMap(); + QString S = QString::fromStdString(propertyName); + if( ! obj.contains( S )) { + return false; + } + + QVariant E = obj.value( S ); + + if constexpr (std::is_base_of_v) { + // property must be string + if ( isString( E ) ) { + propertyValue = E.toString().toStdString(); + return true; + } + } else if constexpr (std::is_same_v) { + if (isBool( E ) ) { + propertyValue = E.toBool(); + return true; + } + } else if constexpr (IsQVariant) { + propertyValue = E; + return true; + } else { + // This triggers ONLY if none of the above branches are taken + static_assert(sizeof(propertyType) == 0, "cannot get unknown propertytype"); + } + + return false; + } + + static bool getPropertyValueOfType(const QVariant &root, + const std::string &propertyName, + TaggedProperty propertyValue) + { + auto obj = root.toMap(); + QString S = QString::fromStdString(propertyName); + + if( ! obj.contains( S )) { + return false; + } + + QVariant E = obj.value( S ); + + if (isArray(E)) { + // value MUST be an array + propertyValue.value = E.toList(); + return true; + } + return false; + } + + private : + static bool isOfType( const QVariant & v, int type ) { + return v.typeId() == type; + } + + }; + +} diff --git a/src/jsonata/Functions.cpp b/src/jsonata/Functions.cpp index 96e9342..90cfca1 100644 --- a/src/jsonata/Functions.cpp +++ b/src/jsonata/Functions.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -464,12 +465,12 @@ static bool isDeepEqualForDistinct(const std::any& lhs, const std::any& rhs) { // Compare direct map objects if (lhs.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { const auto& leftMap = - std::any_cast>( + std::any_cast>( lhs); const auto& rightMap = - std::any_cast>( + std::any_cast>( rhs); if (leftMap.size() != rightMap.size()) { @@ -846,9 +847,9 @@ bool Functions::isLambda(const std::any& value) { // Deprecated: type-check helpers moved to Utils -nlohmann::ordered_map +jsonata::ordered_map Functions::getFunctionRegistry() { - static nlohmann::ordered_map + static jsonata::ordered_map registryWithSignatures = { // Aggregation functions {"sum", FunctionEntry( @@ -1259,11 +1260,11 @@ Functions::getFunctionRegistry() { (args.size() >= 2 && isString(args[1])) ? std::any_cast(args[1]) : ""; - nlohmann::ordered_map options; + jsonata::ordered_map options; if (args.size() >= 3) { try { options = std::any_cast< - nlohmann::ordered_map>( + jsonata::ordered_map>( args[2]); } catch (const std::bad_any_cast&) { // Ignore invalid options @@ -1794,9 +1795,9 @@ std::optional Functions::toBoolean(const std::any& arg) { result = std::any_cast(arg); // Check for map/object } else if (arg.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { const auto& map = std::any_cast< - const nlohmann::ordered_map&>(arg); + const jsonata::ordered_map&>(arg); result = !map.empty(); } // Functions are falsy @@ -2187,7 +2188,7 @@ std::any Functions::merge(const Utils::JList& args) { } try { - nlohmann::ordered_map result; + jsonata::ordered_map result; // Convert to vector if needed Utils::JList inputArray; @@ -2204,9 +2205,9 @@ std::any Functions::merge(const Utils::JList& args) { try { // Handle direct map if (item.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { const auto& map = - std::any_cast&>(item); for (const auto& entry : map) { result[entry.first] = entry.second; @@ -2312,11 +2313,11 @@ std::any Functions::spread(const Utils::JList& args) { // Handle object - create an array with one object per key-value // pair } else if (arg.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { const auto& map = std::any_cast< - const nlohmann::ordered_map&>(arg); + const jsonata::ordered_map&>(arg); for (const auto& entry : map) { - nlohmann::ordered_map obj; + jsonata::ordered_map obj; obj[entry.first] = entry.second; result.push_back(obj); } @@ -2347,13 +2348,13 @@ std::any Functions::sift(const Utils::JList& args) { } try { - nlohmann::ordered_map result; + jsonata::ordered_map result; // Handle direct map if (objectArg.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { const auto& map = std::any_cast< - const nlohmann::ordered_map&>(objectArg); + const jsonata::ordered_map&>(objectArg); for (const auto& entry : map) { // Prepare arguments for the predicate function auto funcArgs = hofFuncArgs(predicateArg, entry.second, @@ -2422,9 +2423,9 @@ void Functions::stringifyInternal(std::ostringstream& os, const std::any& arg, if (prettify && !vec.empty()) os << indent; os << "]"; } else if (arg.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { auto map = - std::any_cast>( + std::any_cast>( arg); os << "{"; if (prettify && !map.empty()) os << "\n"; @@ -3005,10 +3006,10 @@ Utils::JList Functions::keys(const std::any& arg) { result.push_back(key); } } else if (arg.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { // Java: if (arg instanceof Map) - return keySet const auto& map = std::any_cast< - const nlohmann::ordered_map&>(arg); + const jsonata::ordered_map&>(arg); for (const auto& [key, value] : map) { result.push_back(key); } @@ -3038,9 +3039,9 @@ std::any Functions::each(const std::any& obj, const std::any& func) { // Handle direct map if (obj.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { const auto& map = std::any_cast< - const nlohmann::ordered_map&>(obj); + const jsonata::ordered_map&>(obj); // Java: for (var key : obj.keySet()) for (const auto& entry : map) { const std::string& key = entry.first; @@ -3613,7 +3614,7 @@ std::string Functions::formatScientificNotation(double value, std::optional Functions::formatNumber( double value, const std::string& picture, - const nlohmann::ordered_map& options) { + const jsonata::ordered_map& options) { if (std::isnan(value) || std::isinf(value)) { return std::nullopt; } @@ -3958,6 +3959,14 @@ std::any Functions::sortWithContext(const Utils::JList& args, // Sort with lambda comparator using stable_sort - Java reference uses // stable sort + + // MAJOR CHANGED. in MSVC the sort function checks that IF the comparator returns + // false for a > b then b > a must NOT also return false. this is not the case when + // a == b. + // to keep compatibility with java sorting but not mess with c++ sorting we keep the + // c++ sorting convention. so NOT reverse the result. This yields an sorted + // array but in the reverted order as in java. so we just reverse the array at the end + std::stable_sort( result.begin(), result.end(), [&](const std::any& a, const std::any& b) { @@ -3970,6 +3979,10 @@ std::any Functions::sortWithContext(const Utils::JList& args, auto compareResult = instance->apply( comparator, compareArgs, input, environment); + // PROBLEM with MSVC + // MSVC checks that if the comparator returns true for a>b then + // b < a Must be false. So we must take are of == cases. + // Convert result to boolean for comparison // Java reference: if swap=true, return 1 (a > b), else // return -1 (a < b) In C++ std::sort: return true if a < b @@ -3977,7 +3990,7 @@ std::any Functions::sortWithContext(const Utils::JList& args, // a > b, so return false auto boolResult = toBoolean(compareResult); if (boolResult.has_value()) { - return !boolResult.value(); // Invert the result to + return boolResult.value(); // Invert the result to // match Java logic } @@ -3989,6 +4002,45 @@ std::any Functions::sortWithContext(const Utils::JList& args, return defaultComparator(a, b); } }); + + // now revert the result + std::reverse( result.begin(), result.end() ); + + // std::stable_sort( + // result.begin(), result.end(), + // [&](const std::any& a, const std::any& b) { + // try { + // // Java reference lines 1955-1961: funcApply(comparator, + // // Arrays.asList(o1, o2)) boolean swap = (boolean) + // // funcApply(comparator, Arrays.asList(o1, o2)); if (swap) + // // return 1; else return -1; + // Utils::JList compareArgs = {a, b}; + // auto compareResult = instance->apply( + // comparator, compareArgs, input, environment); + + // // PROBLEM with MSVC + // // MSVC checks that if the comparator returns true for a>b then + // // b < a Must be false. So we must take are of == cases. + + // // Convert result to boolean for comparison + // // Java reference: if swap=true, return 1 (a > b), else + // // return -1 (a < b) In C++ std::sort: return true if a < b + // // So we need to invert: if lambda returns true (swap), then + // // a > b, so return false + // auto boolResult = toBoolean(compareResult); + // if (boolResult.has_value()) { + // return !boolResult.value(); // Invert the result to + // // match Java logic + // } + + // // Fallback to default comparison if lambda returns + // // undefined + // return defaultComparator(a, b); + // } catch (...) { + // // Fallback to default comparison on error + // return defaultComparator(a, b); + // } + // }); } else { // Natural ordering for homogeneous arrays - same logic as basic sort // function @@ -4105,11 +4157,11 @@ std::any Functions::lookup(const std::any& input, const std::string& key) { return result.empty() ? std::any{} : std::any(result); } else if (input.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { // Java: if (input instanceof Map) - get key and handle null // detection const auto& map = std::any_cast< - const nlohmann::ordered_map&>(input); + const jsonata::ordered_map&>(input); auto it = map.find(key); if (it != map.end()) { auto result = it->second; @@ -5007,7 +5059,7 @@ Utils::JList Functions::evaluateMatcher(const std::regex& pattern, for (; iter != end; ++iter) { const std::smatch& smatch = *iter; - nlohmann::ordered_map match; + jsonata::ordered_map match; match["match"] = smatch.str(); match["index"] = static_cast(smatch.position()); @@ -5408,9 +5460,9 @@ std::string Functions::safeReplaceAllFn(const std::string& str, return finalResult; } -nlohmann::ordered_map Functions::toJsonataMatch( +jsonata::ordered_map Functions::toJsonataMatch( const std::smatch& match) { - nlohmann::ordered_map result; + jsonata::ordered_map result; result["match"] = match.str(); @@ -5514,7 +5566,7 @@ std::string Functions::rightPad(const std::string& str, int64_t size, // formatNumber options processing helper functions Functions::FormatSymbols Functions::processOptionsArg( - const nlohmann::ordered_map& options) { + const jsonata::ordered_map& options) { FormatSymbols symbols; // Use default values if (options.empty()) { diff --git a/src/jsonata/JException.cpp b/src/jsonata/JException.cpp index d44192d..d040781 100644 --- a/src/jsonata/JException.cpp +++ b/src/jsonata/JException.cpp @@ -17,8 +17,6 @@ * limitations under the License. */ #include "jsonata/JException.h" - -#include #include #include diff --git a/src/jsonata/Jsonata.cpp b/src/jsonata/Jsonata.cpp index 8dc42c1..ce33b2c 100644 --- a/src/jsonata/Jsonata.cpp +++ b/src/jsonata/Jsonata.cpp @@ -17,6 +17,7 @@ * limitations under the License. */ #include "jsonata/Jsonata.h" +#include "jsonata/ordered_map.h" #include #include @@ -228,6 +229,8 @@ std::any Jsonata::_evaluate(std::shared_ptr expr, result = std::any{}; } + // std::cout << "RRRRRRRRRRR" << Jsonata::fromAny(result).dump() << std::endl; + // Apply predicates if present - matches Java lines 210-213 if (!expr->predicate.empty()) { for (const auto& pred : expr->predicate) { @@ -603,7 +606,9 @@ std::any Jsonata::evaluateFunctionWithContext( // Then add the regular arguments for (const auto& arg : expr->arguments) { + // std::cout << "XXXXXXXXXXXXX" << Jsonata::fromAny(arg).dump() << std::endl; auto argValue = evaluate(arg, input, environment); + // std::cout << "VVVVVVVVVVVVVVV" << Jsonata::fromAny(arg).dump() << std::endl; evaluatedArgs.push_back(argValue); } @@ -747,9 +752,9 @@ std::any Jsonata::evaluateWildcard(std::shared_ptr expr, try { // Handle map/object input if (_input.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { auto map = - std::any_cast>( + std::any_cast>( _input); for (const auto& [key, value] : map) { // Java reference line 713: if((value instanceof List)) @@ -867,7 +872,7 @@ std::any Jsonata::evaluatePath(std::shared_ptr expr, if (tupleBindings.has_value()) { for (const auto& tupleAny : *tupleBindings) { auto tuple = std::any_cast< - nlohmann::ordered_map>(tupleAny); + jsonata::ordered_map>(tupleAny); auto it = tuple.find("@"); if (it != tuple.end()) { result.push_back(it->second); @@ -1151,14 +1156,14 @@ bool Jsonata::deepEquals(const std::any& lhs, const std::any& rhs) { // Handle object comparison - Java uses Map.equals() which compares all // key-value pairs if (lhsConverted.type() == - typeid(nlohmann::ordered_map) && + typeid(jsonata::ordered_map) && rhsConverted.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { const auto& leftMap = - std::any_cast&>( + std::any_cast&>( lhsConverted); const auto& rightMap = - std::any_cast&>( + std::any_cast&>( rhsConverted); if (leftMap.size() != rightMap.size()) { @@ -1356,7 +1361,7 @@ void Jsonata::recurseDescendants(const std::any& input, // in the codebase try { auto map = - std::any_cast>( + std::any_cast>( input); for (const auto& [key, value] : map) { recurseDescendants(value, results); @@ -1548,9 +1553,9 @@ std::any Jsonata::evaluateSort(std::shared_ptr expr, // Extract context from tuple map try { if (a.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { auto tupleMap = std::any_cast< - nlohmann::ordered_map>(a); + jsonata::ordered_map>(a); auto it = tupleMap.find("@"); if (it != tupleMap.end()) { contextA = it->second; @@ -1573,9 +1578,9 @@ std::any Jsonata::evaluateSort(std::shared_ptr expr, if (isTupleSort) { try { if (b.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { auto tupleMap = std::any_cast< - nlohmann::ordered_map>(b); + jsonata::ordered_map>(b); auto it = tupleMap.find("@"); if (it != tupleMap.end()) { contextB = it->second; @@ -1781,16 +1786,16 @@ std::any Jsonata::evaluateTransform(std::shared_ptr expr, if (update.has_value()) { if (update.type() == typeid( - nlohmann::ordered_map) && + jsonata::ordered_map) && match.type() == typeid( - nlohmann::ordered_map)) { + jsonata::ordered_map)) { // Java lines 1457-1459: merge the update auto& matchMap = std::any_cast< - nlohmann::ordered_map&>( + jsonata::ordered_map&>( match); const auto& updateMap = - std::any_cast&>(update); for (const auto& [prop, value] : updateMap) { @@ -1921,9 +1926,9 @@ std::any Jsonata::evaluateFilter(std::shared_ptr predicate, } if (isTupleStream && item.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { auto tupleMap = - std::any_cast>( + std::any_cast>( item); auto it = tupleMap.find("@"); if (it != tupleMap.end()) { @@ -1998,267 +2003,285 @@ std::any Jsonata::evaluateBlock(std::shared_ptr expr, return result.has_value() ? result.value() : false; } -/* static */ std::any Jsonata::orderedJsonToAny( - const nlohmann::ordered_json& j) { - if (j.is_null()) return std::any{}; - if (j.is_boolean()) return std::any(j.get()); - if (j.is_number_integer()) { - // Preserve sign by using int64_t; for large unsigned, capture as - // uint64_t - int64_t si = 0; - try { - si = j.get(); - return std::any(static_cast(si)); - } catch (...) { - } - uint64_t ui = j.get(); - return std::any(static_cast(ui)); - } - if (j.is_number_unsigned()) { - uint64_t ui = j.get(); - return std::any(static_cast(ui)); - } - if (j.is_number_float()) return std::any(j.get()); - if (j.is_string()) return std::any(j.get()); - if (j.is_array()) { - std::vector out; - out.reserve(j.size()); - for (const auto& el : j) out.push_back(orderedJsonToAny(el)); - return out; - } - if (j.is_object()) { - nlohmann::ordered_map m; - for (auto it = j.begin(); it != j.end(); ++it) { - m[it.key()] = orderedJsonToAny(it.value()); - } - return m; - } - return std::any{}; -} - -/* static */ nlohmann::ordered_json Jsonata::anyToOrderedJson( - const std::any& value) { - if (!value.has_value()) return nlohmann::ordered_json(); - - // Canonicalize numeric types: convert doubles with no fractional part to - // integer types - try { - if (Utils::isNumeric(value)) { - std::any canon = Utils::convertNumber(value); - if (canon.type() != value.type()) { - return anyToOrderedJson(canon); - } - } - } catch (...) { - // Ignore conversion errors here; fall back to original value - } - - const std::type_info& type = value.type(); - if (type == typeid(bool)) - return nlohmann::ordered_json(std::any_cast(value)); - if (type == typeid(double)) - return nlohmann::ordered_json(std::any_cast(value)); - if (type == typeid(int64_t)) - return nlohmann::ordered_json(std::any_cast(value)); - if (type == typeid(uint64_t)) - return nlohmann::ordered_json(std::any_cast(value)); - // Cross-platform support for additional integral types (e.g., long/long long) - if (type == typeid(long long)) - return nlohmann::ordered_json(static_cast(std::any_cast(value))); - if (type == typeid(unsigned long long)) - return nlohmann::ordered_json(static_cast(std::any_cast(value))); - if (type == typeid(long)) - return nlohmann::ordered_json(static_cast(std::any_cast(value))); - if (type == typeid(unsigned long)) - return nlohmann::ordered_json(static_cast(std::any_cast(value))); - if (type == typeid(int)) - return nlohmann::ordered_json(static_cast(std::any_cast(value))); - if (type == typeid(unsigned int)) - return nlohmann::ordered_json(static_cast(std::any_cast(value))); - if (type == typeid(std::string)) - return nlohmann::ordered_json(std::any_cast(value)); - if (type == typeid(Utils::JList)) { - nlohmann::ordered_json arr = nlohmann::ordered_json::array(); - const auto& jlist = std::any_cast(value); - for (const auto& item : jlist) arr.push_back(anyToOrderedJson(item)); - return arr; - } - if (type == typeid(std::vector)) { - nlohmann::ordered_json arr = nlohmann::ordered_json::array(); - const auto& vec = std::any_cast&>(value); - for (const auto& item : vec) arr.push_back(anyToOrderedJson(item)); - return arr; - } - if (type == typeid(nlohmann::ordered_map)) { - nlohmann::ordered_json obj = nlohmann::ordered_json::object(); - const auto& map = - std::any_cast&>( - value); - for (const auto& [k, v] : map) obj[k] = anyToOrderedJson(v); - return obj; - } - if (type == typeid(std::shared_ptr)) { - return nlohmann::ordered_json(); - } - // Directly handle nlohmann::ordered_json returned from custom functions - if (type == typeid(nlohmann::ordered_json)) { - return std::any_cast(value); - } - return nlohmann::ordered_json(); -} - -// Unordered JSON helpers (nlohmann::json) -/* static */ std::any Jsonata::jsonToAny(const nlohmann::json& j) { - if (j.is_null()) return std::any{}; - if (j.is_boolean()) return std::any(j.get()); - if (j.is_number_integer()) { - int64_t si = 0; - try { - si = j.get(); - return std::any(static_cast(si)); - } catch (...) { - } - uint64_t ui = j.get(); - return std::any(static_cast(ui)); - } - if (j.is_number_unsigned()) { - uint64_t ui = j.get(); - return std::any(static_cast(ui)); - } - if (j.is_number_float()) return std::any(j.get()); - if (j.is_string()) return std::any(j.get()); - if (j.is_array()) { - std::vector out; - out.reserve(j.size()); - for (const auto& el : j) out.push_back(jsonToAny(el)); - return out; - } - if (j.is_object()) { - nlohmann::ordered_map m; - for (auto it = j.begin(); it != j.end(); ++it) { - m[it.key()] = jsonToAny(it.value()); - } - return m; - } - return std::any{}; -} - -/* static */ nlohmann::json Jsonata::anyToJson(const std::any& value) { - if (!value.has_value()) return nlohmann::json(); - - // Canonicalize numeric types similar to anyToOrderedJson - try { - if (Utils::isNumeric(value)) { - std::any canon = Utils::convertNumber(value); - if (canon.type() != value.type()) { - return anyToJson(canon); - } - } - } catch (...) { - // Ignore conversion errors here; fall back to original value - } - - const std::type_info& type = value.type(); - if (type == typeid(bool)) - return nlohmann::json(std::any_cast(value)); - if (type == typeid(double)) - return nlohmann::json(std::any_cast(value)); - if (type == typeid(int64_t)) - return nlohmann::json(std::any_cast(value)); - if (type == typeid(uint64_t)) - return nlohmann::json(std::any_cast(value)); - // Cross-platform support for additional integral types (e.g., long/long long) - if (type == typeid(long long)) - return nlohmann::json(static_cast(std::any_cast(value))); - if (type == typeid(unsigned long long)) - return nlohmann::json(static_cast(std::any_cast(value))); - if (type == typeid(long)) - return nlohmann::json(static_cast(std::any_cast(value))); - if (type == typeid(unsigned long)) - return nlohmann::json(static_cast(std::any_cast(value))); - if (type == typeid(int)) - return nlohmann::json(static_cast(std::any_cast(value))); - if (type == typeid(unsigned int)) - return nlohmann::json(static_cast(std::any_cast(value))); - if (type == typeid(std::string)) - return nlohmann::json(std::any_cast(value)); - - if (type == typeid(Utils::JList)) { - nlohmann::json arr = nlohmann::json::array(); - const auto& jlist = std::any_cast(value); - for (const auto& item : jlist) arr.push_back(anyToJson(item)); - return arr; - } - if (type == typeid(std::vector)) { - nlohmann::json arr = nlohmann::json::array(); - const auto& vec = std::any_cast&>(value); - for (const auto& item : vec) arr.push_back(anyToJson(item)); - return arr; - } - if (type == typeid(nlohmann::ordered_map)) { - nlohmann::json obj = nlohmann::json::object(); - const auto& map = - std::any_cast&>( - value); - for (const auto& [k, v] : map) obj[k] = anyToJson(v); - return obj; - } - - // Ignore function symbols - if (type == typeid(std::shared_ptr)) { - return nlohmann::json(); - } - - // Directly handle nlohmann::json returned from custom functions - if (type == typeid(nlohmann::json)) { - return std::any_cast(value); - } - - // Handle nlohmann::ordered_json without dump/parse by converting - if (type == typeid(nlohmann::ordered_json)) { - const auto& oj = std::any_cast(value); - - // Recursive lambda to convert ordered_json to json - std::function convert = - [&](const nlohmann::ordered_json& j) -> nlohmann::json { - if (j.is_null()) return nlohmann::json(); - if (j.is_boolean()) return nlohmann::json(j.get()); - if (j.is_number_integer()) { - int64_t si = 0; - try { - si = j.get(); - return nlohmann::json(static_cast(si)); - } catch (...) { - } - uint64_t ui = j.get(); - return nlohmann::json(static_cast(ui)); - } - if (j.is_number_unsigned()) - return nlohmann::json(j.get()); - if (j.is_number_float()) - return nlohmann::json(j.get()); - if (j.is_string()) - return nlohmann::json(j.get()); - if (j.is_array()) { - nlohmann::json arr = nlohmann::json::array(); - for (const auto& el : j) arr.push_back(convert(el)); - return arr; - } - if (j.is_object()) { - nlohmann::json obj = nlohmann::json::object(); - for (auto it = j.begin(); it != j.end(); ++it) { - obj[it.key()] = convert(it.value()); - } - return obj; - } - return nlohmann::json(); - }; - - return convert(oj); - } - - return nlohmann::json(); -} +// std::any Jsonata::toAny( +// const jsonata::ordered_json& j) { +// if (j.is_null()) return std::any{}; +// if (j.isBool()) return std::any(j.get()); +// if (j.isInteger()) { +// // Preserve sign by using int64_t; for large unsigned, capture as +// // uint64_t +// int64_t si = 0; +// try { +// si = j.get(); +// return std::any(static_cast(si)); +// } catch (...) { +// } +// uint64_t ui = j.get(); +// return std::any(static_cast(ui)); +// } +// if (j.isUnsignedInteger()) { +// uint64_t ui = j.get(); +// return std::any(static_cast(ui)); +// } +// if (j.isFloat()) return std::any(j.get()); +// if (j.isString()) return std::any(j.get()); +// if (j.isArray()) { +// std::vector out; +// out.reserve(j.size()); +// jsonata::copy( j, out, [](const jsonata::ordered_json&el){ +// return toAny(el); +// }); +// return out; +// } +// if (j.isObject()) { +// jsonata::ordered_map m; +// jsonata::copy( j, m, [](const jsonata::ordered_json&el){ +// return toAny(el); +// }); +// return m; +// } +// return std::any{}; +// } + +// jsonata::ordered_json Jsonata::fromAny( +// const std::any& value) { +// if (!value.has_value()) return jsonata::ordered_json(); + +// // Canonicalize numeric types: convert doubles with no fractional part to +// // integer types +// try { +// if (Utils::isNumeric(value)) { +// std::any canon = Utils::convertNumber(value); +// if (canon.type() != value.type()) { +// return fromAny(canon); +// } +// } +// } catch (...) { +// // Ignore conversion errors here; fall back to original value +// } + +// const std::type_info& type = value.type(); +// if (type == typeid(bool)) +// return jsonata::ordered_json(std::any_cast(value)); +// if (type == typeid(double)) +// return jsonata::ordered_json(std::any_cast(value)); +// if (type == typeid(int64_t)) +// return jsonata::ordered_json(std::any_cast(value)); +// if (type == typeid(uint64_t)) +// return jsonata::ordered_json(std::any_cast(value)); +// // Cross-platform support for additional integral types (e.g., long/long long) +// if (type == typeid(long long)) +// return jsonata::ordered_json(static_cast(std::any_cast(value))); +// if (type == typeid(unsigned long long)) +// return jsonata::ordered_json(static_cast(std::any_cast(value))); +// if (type == typeid(long)) +// return jsonata::ordered_json(static_cast(std::any_cast(value))); +// if (type == typeid(unsigned long)) +// return jsonata::ordered_json(static_cast(std::any_cast(value))); +// if (type == typeid(int)) +// return jsonata::ordered_json(static_cast(std::any_cast(value))); +// if (type == typeid(unsigned int)) +// return jsonata::ordered_json(static_cast(std::any_cast(value))); +// if (type == typeid(std::string)) +// return jsonata::ordered_json(std::any_cast(value)); +// if (type == typeid(Utils::JList)) { +// jsonata::ordered_json arr = jsonata::ordered_json::array(); +// const auto& jlist = std::any_cast(value); +// jsonata::copy( jlist, arr, []( const std::any & a ) { +// return fromAny( a ); +// }); +// return arr; +// } +// if (type == typeid(std::vector)) { +// jsonata::ordered_json arr = jsonata::ordered_json::array(); +// const auto& vec = std::any_cast&>(value); +// jsonata::copy( vec, arr, []( const std::any & a ) { +// return fromAny( a ); +// }); +// return arr; +// } +// if (type == typeid(jsonata::ordered_map)) { +// jsonata::ordered_json obj = jsonata::ordered_json::object(); +// const auto& map = +// std::any_cast&>( +// value); +// jsonata::copy( map, obj, []( const std::any & a ) { +// return fromAny(a); +// }); +// return obj; +// } +// if (type == typeid(std::shared_ptr)) { +// return jsonata::ordered_json(); +// } +// // Directly handle jsonata::ordered_json returned from custom functions +// if (type == typeid(jsonata::ordered_json)) { +// return std::any_cast(value); +// } +// return jsonata::ordered_json(); +// } + +// // Unordered JSON helpers (jsonata::json) +// std::any Jsonata::jsonToAny(const jsonata::json& j) { +// if (j.is_null()) return std::any{}; +// if (j.isBool()) return std::any(j.get()); +// if (j.isNumber_integer()) { +// int64_t si = 0; +// try { +// si = j.get(); +// return std::any(static_cast(si)); +// } catch (...) { +// } +// uint64_t ui = j.get(); +// return std::any(static_cast(ui)); +// } +// if (j.isUnsignedInteger()) { +// uint64_t ui = j.get(); +// return std::any(static_cast(ui)); +// } +// if (j.isFloat()) return std::any(j.get()); +// if (j.isString()) return std::any(j.get()); +// if (j.isArray()) { +// std::vector out; +// out.reserve(j.size()); +// jsonata::copy( j, out, []( const jsonata::json & el ) { +// return jsonToAny(el); +// }); +// return out; +// } +// if (j.isObject()) { +// jsonata::ordered_map m; +// jsonata::copy( j, m, []( const jsonata::json & el ) { +// return jsonToAny(el); +// }); +// return m; +// } +// return std::any{}; +// } + +// jsonata::json Jsonata::anyToJson(const std::any& value) { +// if (!value.has_value()) return jsonata::json(); + +// // Canonicalize numeric types similar to fromAny +// try { +// if (Utils::isNumeric(value)) { +// std::any canon = Utils::convertNumber(value); +// if (canon.type() != value.type()) { +// return anyToJson(canon); +// } +// } +// } catch (...) { +// // Ignore conversion errors here; fall back to original value +// } + +// const std::type_info& type = value.type(); +// if (type == typeid(bool)) +// return jsonata::json(std::any_cast(value)); +// if (type == typeid(double)) +// return jsonata::json(std::any_cast(value)); +// if (type == typeid(int64_t)) +// return jsonata::json(std::any_cast(value)); +// if (type == typeid(uint64_t)) +// return jsonata::json(std::any_cast(value)); +// // Cross-platform support for additional integral types (e.g., long/long long) +// if (type == typeid(long long)) +// return jsonata::json(static_cast(std::any_cast(value))); +// if (type == typeid(unsigned long long)) +// return jsonata::json(static_cast(std::any_cast(value))); +// if (type == typeid(long)) +// return jsonata::json(static_cast(std::any_cast(value))); +// if (type == typeid(unsigned long)) +// return jsonata::json(static_cast(std::any_cast(value))); +// if (type == typeid(int)) +// return jsonata::json(static_cast(std::any_cast(value))); +// if (type == typeid(unsigned int)) +// return jsonata::json(static_cast(std::any_cast(value))); +// if (type == typeid(std::string)) +// return jsonata::json(std::any_cast(value)); + +// if (type == typeid(Utils::JList)) { +// jsonata::json arr = jsonata::json::array(); +// const auto& jlist = std::any_cast(value); +// jsonata::copy( jlist, arr, []( const std::any & a ) { +// return anyToJson(a); +// }); +// return arr; +// } +// if (type == typeid(std::vector)) { +// jsonata::json arr = jsonata::json::array(); +// const auto& vec = std::any_cast&>(value); +// jsonata::copy( vec, arr, []( const std::any & a ) { +// return anyToJson(a); +// }); +// return arr; +// } +// if (type == typeid(jsonata::ordered_map)) { +// jsonata::json obj = jsonata::json::object(); +// const auto& map = +// std::any_cast&>( +// value); +// jsonata::copy( map, obj, []( const std::any & a ) { +// return anyToJson(a); +// }); +// return obj; +// } + +// // Ignore function symbols +// if (type == typeid(std::shared_ptr)) { +// return jsonata::json(); +// } + +// // Directly handle jsonata::json returned from custom functions +// if (type == typeid(jsonata::json)) { +// return std::any_cast(value); +// } + +// // Handle jsonata::ordered_json without dump/parse by converting +// if (type == typeid(jsonata::ordered_json)) { +// const auto& oj = std::any_cast(value); + +// // Recursive lambda to convert ordered_json to json +// std::function convert = +// [&](const jsonata::ordered_json& j) -> jsonata::json { +// if (j.is_null()) return jsonata::json(); +// if (j.isBool()) return jsonata::json(j.get()); +// if (j.isNumber_integer()) { +// int64_t si = 0; +// try { +// si = j.get(); +// return jsonata::json(static_cast(si)); +// } catch (...) { +// } +// uint64_t ui = j.get(); +// return jsonata::json(static_cast(ui)); +// } +// if (j.isUnsignedInteger()) +// return jsonata::json(j.get()); +// if (j.isFloat()) +// return jsonata::json(j.get()); +// if (j.isString()) +// return jsonata::json(j.get()); +// if (j.isArray()) { +// jsonata::json arr = jsonata::json::array(); +// jsonata::copy( j, arr, [&convert]( const jsonata::ordered_json& el ) { +// return convert(el ); +// }); +// return arr; +// } +// if (j.isObject()) { +// jsonata::json obj = jsonata::json::object(); +// jsonata::copy( j, obj, [&convert]( const jsonata::ordered_json& el ) { +// return convert(el ); +// }); +// return obj; +// } +// return jsonata::json(); +// }; + +// return convert(oj); +// } + +// return jsonata::json(); +// } // Missing public API methods from Java @@ -2300,7 +2323,7 @@ std::shared_ptr Jsonata::createFrameFromTuple( // Java reference lines 324-329: createFrameFromTuple implementation auto frame = createFrame(environment); auto tuple = - std::any_cast>(tupleAny); + std::any_cast>(tupleAny); for (const auto& [key, value] : tuple) { frame->bind(key, value); } @@ -2319,15 +2342,15 @@ std::any Jsonata::reduceTupleStream(const std::any& tupleStream) { return std::any{}; } - nlohmann::ordered_map result; + jsonata::ordered_map result; // Java line 1144: result.putAll(tupleStream.get(0)); result = - std::any_cast>(tuples[0]); + std::any_cast>(tuples[0]); // Java lines 1147-1158: merge remaining tuples for (size_t i = 1; i < tuples.size(); i++) { const auto& el = - std::any_cast>( + std::any_cast>( tuples[i]); for (const auto& [prop, value] : el) { // Java line 1154: result.put(prop, @@ -2432,22 +2455,8 @@ Jsonata::Jsonata(const Jsonata& other) { timestamp_ = other.timestamp_; } -nlohmann::ordered_json Jsonata::evaluate(const nlohmann::ordered_json& input) { - return evaluate(input, nullptr); -} - -nlohmann::ordered_json Jsonata::evaluate(const nlohmann::ordered_json& input, - std::shared_ptr bindings) { - currentInstance_ = this; - - // Check for syntax errors (equivalent to Java's check for errors != null) - if (!expression_) { - throw JException("S0500", 0); // Expression compilation failed - } - - // Convert JSON input to std::any domain for the evaluator - std::any anyInput = orderedJsonToAny(input); - +std::any Jsonata::evaluate(const std::any anyInput, + std::shared_ptr bindings) { // Always evaluate in a fresh child frame of the shared environment, // then (optionally) copy provided bindings into it. This avoids // concurrent mutations of the shared environment. @@ -2493,85 +2502,15 @@ nlohmann::ordered_json Jsonata::evaluate(const nlohmann::ordered_json& input, // Clear TLS after evaluation to avoid dangling references tls_input_.reset(); tls_environment_.reset(); - result = Utils::convertNulls(result); - // Convert result back to nlohmann::ordered_json - return anyToOrderedJson(result); + // std::cout << "RRR1" << Jsonata::fromAny(result).dump() << std::endl; + // std::cout << "RRR2" << Jsonata::fromAny(Utils::convertNulls(result)).dump() << std::endl; + return Utils::convertNulls(result); } catch (const std::exception& err) { // TODO: populateMessage(err); throw; } } -nlohmann::ordered_json Jsonata::evaluate(std::nullptr_t) { - return evaluate(nlohmann::ordered_json()); -} - -nlohmann::ordered_json Jsonata::evaluate(std::nullptr_t, - std::shared_ptr bindings) { - return evaluate(nlohmann::ordered_json(), bindings); -} - -// Unordered nlohmann::json variants -nlohmann::json Jsonata::evaluate(const nlohmann::json& input) { - return evaluate(input, nullptr); -} - -nlohmann::json Jsonata::evaluate(const nlohmann::json& input, - std::shared_ptr bindings) { - currentInstance_ = this; - - if (!expression_) { - throw JException("S0500", 0); - } - - // Convert JSON input to std::any domain for the evaluator - std::any anyInput = jsonToAny(input); - - std::shared_ptr exec_env = createFrame(environment_); - if (bindings != nullptr) { - for (const auto& [key, value] : bindings->getBindings()) { - exec_env->bind(key, value); - } - } - - std::any processedInput = anyInput; - if (anyInput.has_value() && Utils::isArray(anyInput) && - !Utils::isSequence(anyInput)) { - auto sequence = Utils::createSequence(anyInput); - sequence.outerWrapper = true; - processedInput = sequence; - } - - exec_env->bind("$", processedInput); - - if (validateInput_) { - Functions::validateInput(processedInput); - } - - std::any result; - try { - tls_input_ = processedInput; - tls_environment_ = exec_env; - result = evaluate(expression_, processedInput, exec_env); - tls_input_.reset(); - tls_environment_.reset(); - result = Utils::convertNulls(result); - // Convert result to nlohmann::json - return anyToJson(result); - } catch (const std::exception& err) { - throw; - } -} - -nlohmann::json Jsonata::evaluateUnordered(std::nullptr_t) { - return evaluate(nlohmann::json()); -} - -nlohmann::json Jsonata::evaluateUnordered(std::nullptr_t, - std::shared_ptr bindings) { - return evaluate(nlohmann::json(), bindings); -} - std::any Jsonata::apply(const std::any& proc, const Utils::JList& args, const std::any& input, std::shared_ptr environment) { @@ -2657,7 +2596,7 @@ static std::any regexClosure(std::shared_ptr state) { if (state->it == state->end) return std::any{}; auto match = *state->it; ++(state->it); - nlohmann::ordered_map result; + jsonata::ordered_map result; result["match"] = std::string(match.str()); result["start"] = static_cast(match.position()); result["end"] = static_cast(match.position() + match.length()); @@ -2841,9 +2780,9 @@ std::any Jsonata::applyProcedure(const std::any& _proc, std::shared_ptr environment; if (symbol->value.has_value() && symbol->value.type() == - typeid(nlohmann::ordered_map)) { + typeid(jsonata::ordered_map)) { auto closureMap = std::any_cast< - nlohmann::ordered_map>( + jsonata::ordered_map>( symbol->value); if (closureMap.find("environment") != closureMap.end()) { try { @@ -2907,16 +2846,16 @@ std::any Jsonata::applyProcedure(const std::any& _proc, return std::any_cast(a) == std::any_cast(b); if (a.type() == - typeid(nlohmann::ordered_map) && b.type() == - typeid(nlohmann::ordered_map)) { const auto& ma = - std::any_cast&>(a); const auto& mb = - std::any_cast&>(b); if (ma.size() != mb.size()) return false; for (const auto& [k, va] : ma) { @@ -2958,19 +2897,19 @@ std::any Jsonata::applyProcedure(const std::any& _proc, std::function&)> + const jsonata::ordered_map&)> applyUpdateToFirstMatch = [&](std::any& node, const std::any& target, - const nlohmann::ordered_map< + const jsonata::ordered_map< std::string, std::any>& updateMap) -> bool { if (node.type() == - typeid(nlohmann::ordered_map) && target.type() == - typeid(nlohmann::ordered_map)) { if (deepEqualsAny(node, target)) { - auto& m = std::any_cast&>(node); for (const auto& [k, v] : updateMap) { m[k] = v; @@ -2978,7 +2917,7 @@ std::any Jsonata::applyProcedure(const std::any& _proc, return true; } auto& m = std::any_cast< - nlohmann::ordered_map&>( + jsonata::ordered_map&>( node); for (auto it = m.begin(); it != m.end(); ++it) { if (applyUpdateToFirstMatch(it->second, target, @@ -3017,13 +2956,13 @@ std::any Jsonata::applyProcedure(const std::any& _proc, const std::vector& deletions) -> bool { if (node.type() == - typeid(nlohmann::ordered_map) && target.type() == - typeid(nlohmann::ordered_map)) { if (deepEqualsAny(node, target)) { - auto& m = std::any_cast&>(node); for (const auto& key : deletions) { m.erase(key); @@ -3031,7 +2970,7 @@ std::any Jsonata::applyProcedure(const std::any& _proc, return true; } auto& m = std::any_cast< - nlohmann::ordered_map&>( + jsonata::ordered_map&>( node); for (auto it = m.begin(); it != m.end(); ++it) { if (applyDeleteToFirstMatch(it->second, target, @@ -3070,13 +3009,13 @@ std::any Jsonata::applyProcedure(const std::any& _proc, instance->evaluate(update, match, environment); if (updateValue.has_value()) { if (updateValue.type() != - typeid(nlohmann::ordered_map)) { throw JException("T2011", update->position, updateValue); } const auto& updateMap = - std::any_cast&>(updateValue); // Apply update to the first occurrence of match // within result @@ -3088,10 +3027,10 @@ std::any Jsonata::applyProcedure(const std::any& _proc, // delete uses a structure that deep-equals the // updated node in 'result'. if (match.type() == - typeid(nlohmann::ordered_map)) { auto& matchMap = - std::any_cast&>(match); for (const auto& [k, v] : updateMap) { matchMap[k] = v; @@ -3220,7 +3159,7 @@ std::any Jsonata::evaluateStages( std::string stageValue = std::any_cast(stage->value); auto tuple = std::any_cast< - nlohmann::ordered_map>( + jsonata::ordered_map>( tupleList[ee]); tuple[stageValue] = static_cast(ee); tupleList[ee] = std::any(tuple); @@ -3233,9 +3172,9 @@ std::any Jsonata::evaluateStages( for (size_t ee = 0; ee < resultList.size(); ee++) { if (resultList[ee].type() == typeid( - nlohmann::ordered_map)) { + jsonata::ordered_map)) { auto tuple = std::any_cast< - nlohmann::ordered_map>( + jsonata::ordered_map>( resultList[ee]); if (stage->value.has_value()) { std::string stageValue = @@ -3373,7 +3312,7 @@ std::any Jsonata::evaluateTupleStep( if (sorted.has_value() && Utils::isArray(sorted)) { auto sortedVec = Utils::arrayify(sorted); for (size_t i = 0; i < sortedVec.size(); ++i) { - nlohmann::ordered_map tuple; + jsonata::ordered_map tuple; tuple["@"] = sortedVec[i]; if (expr->index.has_value()) { try { @@ -3412,7 +3351,7 @@ std::any Jsonata::evaluateTupleStep( // Java line 435: create initial tuple bindings (only when tupleBindings // is null, not just empty) for (const auto& item : input) { - nlohmann::ordered_map tuple; + jsonata::ordered_map tuple; tuple["@"] = item; bindings.push_back(std::any(tuple)); } @@ -3421,7 +3360,7 @@ std::any Jsonata::evaluateTupleStep( // Java reference lines 438-472: process each tuple binding for (const auto& bindingAny : bindings) { auto binding = - std::any_cast>( + std::any_cast>( bindingAny); // Create frame from tuple - Java line 439 auto stepEnv = createFrameFromTuple(environment, bindingAny); @@ -3445,14 +3384,14 @@ std::any Jsonata::evaluateTupleStep( // Java lines 449-469: create output tuples for (size_t i = 0; i < resVec.size(); ++i) { - nlohmann::ordered_map tuple( + jsonata::ordered_map tuple( binding); // Copy existing bindings if (resVec.tupleStream) { // cast resVec[i] to a map and overwrite existing keys // (match Java Map.putAll semantics) auto resTuple = std::any_cast< - nlohmann::ordered_map>( + jsonata::ordered_map>( resVec[i]); for (const auto& kv : resTuple) { tuple[kv.first] = kv.second; @@ -3514,14 +3453,14 @@ std::any Jsonata::evaluateGroupExpression(std::shared_ptr expr, if (!expr) return std::any{}; // Port exact Java implementation: Jsonata.java lines 1051-1134 - nlohmann::ordered_map result; + jsonata::ordered_map result; // C++ equivalent of Java's LinkedHashMap struct GroupEntry { std::any data; int64_t exprIndex; }; - nlohmann::ordered_map groups; + jsonata::ordered_map groups; // Java line 1054: var reduce = (_input instanceof JList) && // ((JList)_input).tupleStream ? true : false; For now, simplify this - the @@ -3559,7 +3498,7 @@ std::any Jsonata::evaluateGroupExpression(std::shared_ptr expr, ? createFrameFromTuple( environment, std::any_cast< - nlohmann::ordered_map>(item)) + jsonata::ordered_map>(item)) : environment; for (size_t pairIndex = 0; pairIndex < expr->lhsObject.size(); @@ -3571,7 +3510,7 @@ std::any Jsonata::evaluateGroupExpression(std::shared_ptr expr, std::any keyContext = item; if (reduce) { auto itemMap = - std::any_cast>( + std::any_cast>( item); auto atIt = itemMap.find("@"); keyContext = @@ -3638,7 +3577,7 @@ std::any Jsonata::evaluateGroupExpression(std::shared_ptr expr, if (reduce) { auto tuple = reduceTupleStream(entry.data); auto tupleMap = - std::any_cast>( + std::any_cast>( tuple); auto atIt = tupleMap.find("@"); context = (atIt != tupleMap.end()) ? atIt->second : std::any{}; @@ -3684,7 +3623,7 @@ std::any Jsonata::evaluateTransformExpression( transformer->arguments.push_back(dummyArg); // Store the closure environment - transformer->value = nlohmann::ordered_map{ + transformer->value = jsonata::ordered_map{ {"input", input}, {"environment", environment}}; return transformer; diff --git a/src/jsonata/Parser.cpp b/src/jsonata/Parser.cpp index c85c977..9f85633 100644 --- a/src/jsonata/Parser.cpp +++ b/src/jsonata/Parser.cpp @@ -1559,7 +1559,7 @@ void Parser::pushAncestry(std::shared_ptr result, if (!value || !result) return; if (!value->seekingParentList.empty() || value->type == "parent") { - auto slots = value->seekingParentList; + auto slotList = value->seekingParentList; if (value->type == "parent") { // Add parent slot if available if (value->slot.has_value()) { @@ -1567,7 +1567,7 @@ void Parser::pushAncestry(std::shared_ptr result, auto parentSlot = std::any_cast>(value->slot); if (parentSlot) { - slots.push_back(parentSlot); + slotList.push_back(parentSlot); } } catch (const std::bad_any_cast&) { // Handle conversion error @@ -1576,10 +1576,10 @@ void Parser::pushAncestry(std::shared_ptr result, } if (result->seekingParentList.empty()) { - result->seekingParentList = slots; + result->seekingParentList = slotList; } else { result->seekingParentList.insert(result->seekingParentList.end(), - slots.begin(), slots.end()); + slotList.begin(), slotList.end()); } } } @@ -1591,7 +1591,7 @@ void Parser::resolveAncestry(std::shared_ptr path) { auto lastStep = path->steps[index]; if (!lastStep) return; - auto slots = lastStep->seekingParentList; + auto slotList = lastStep->seekingParentList; if (lastStep->type == "parent") { // Add parent slot if available @@ -1600,7 +1600,7 @@ void Parser::resolveAncestry(std::shared_ptr path) { auto parentSlot = std::any_cast>(lastStep->slot); if (parentSlot) { - slots.push_back(parentSlot); + slotList.push_back(parentSlot); } } catch (const std::bad_any_cast&) { // Handle conversion error @@ -1608,7 +1608,7 @@ void Parser::resolveAncestry(std::shared_ptr path) { } } - for (auto slot : slots) { + for (auto slot : slotList) { if (!slot) continue; index = path->steps.size() - 2; while (slot && slot->level > 0) { diff --git a/src/jsonata/Tokenizer.cpp b/src/jsonata/Tokenizer.cpp index 066e2ad..4c71ec8 100644 --- a/src/jsonata/Tokenizer.cpp +++ b/src/jsonata/Tokenizer.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "jsonata/JException.h" #include "jsonata/Utils.h" @@ -375,8 +376,8 @@ std::unique_ptr Tokenizer::next(bool prefix) { // Use byte offsets to get iterators into the original string without copying auto byteStart = path_.cbegin() + static_cast(byte_offsets_[position_]); auto byteEnd = path_.cend(); - std::cmatch match; - if (std::regex_search(&*byteStart, &*byteEnd, match, numregex) && + std::smatch match; + if (std::regex_search(byteStart, byteEnd, match, numregex) && match.position() == 0) { std::string numStr = match.str(0); double num = std::stod(numStr); diff --git a/src/jsonata/Utils.cpp b/src/jsonata/Utils.cpp index af969c3..baeb321 100644 --- a/src/jsonata/Utils.cpp +++ b/src/jsonata/Utils.cpp @@ -17,6 +17,7 @@ * limitations under the License. */ #include "jsonata/Utils.h" +#include "jsonata/ordered_map.h" #include #include @@ -172,7 +173,7 @@ bool Utils::isArray(const std::any& value) { bool Utils::isObject(const std::any& value) { if (!value.has_value()) return false; - return value.type() == typeid(nlohmann::ordered_map); + return value.type() == typeid(jsonata::ordered_map); } std::optional Utils::type(const std::any& value) { @@ -401,7 +402,7 @@ std::any Utils::convertNulls(const std::any& res) { void Utils::convertNullsMap(std::any& res) { try { auto& map = - std::any_cast&>(res); + std::any_cast&>(res); for (auto it = map.begin(); it != map.end(); ++it) { std::any& valRef = it->second; std::any converted = convertValue(valRef); diff --git a/src/jsonata/utils/DateTimeUtils.cpp b/src/jsonata/utils/DateTimeUtils.cpp index ac0f60f..3ad4b58 100644 --- a/src/jsonata/utils/DateTimeUtils.cpp +++ b/src/jsonata/utils/DateTimeUtils.cpp @@ -205,6 +205,10 @@ std::string DateTimeUtils::numberToWords(int64_t value, bool ordinal) { std::string DateTimeUtils::lookup(int64_t num, bool prev, bool ord) { std::string words = ""; + if( num < 0 ) { + // negative numbers trigger 'out of range' exception in windows + num = 0; + } if (num <= 19) { words = (prev ? " and " : "") + (ord ? ordinals[num] : few[num]); } else if (num < 100) { @@ -672,7 +676,7 @@ std::string DateTimeUtils::formatDateTime(int64_t millis, int64_t offsetHours = 0; int64_t offsetMinutes = 0; - if (!timezone.empty()) { + if (!timezone.empty() && timezone != "UTC") { try { int64_t offset = std::stoi(timezone); offsetHours = offset / 100; diff --git a/src/jsonata/utils/Signature.cpp b/src/jsonata/utils/Signature.cpp index 711c0d8..d0ced3c 100644 --- a/src/jsonata/utils/Signature.cpp +++ b/src/jsonata/utils/Signature.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -131,7 +130,7 @@ bool Signature::isArrayType(const std::any &value) { std::string Signature::checkObjectType(const std::any &value) { // Try map (object) try { - std::any_cast>(value); + auto r = std::any_cast>(value); return "o"; } catch (const std::bad_any_cast &) { // Try shared_ptr - check if it's a regex object @@ -148,12 +147,12 @@ bool Signature::isFunctionType(const std::any &value) { // Check for JFunction (native functions like $sum, $map, etc.) - Java // reference equivalent try { - std::any_cast(value); + auto r = std::any_cast(value); return true; } catch (const std::bad_any_cast &) { // Check for std::function (generic function objects) try { - std::any_cast>(value); + auto r = std::any_cast>(value); return true; } catch (const std::bad_any_cast &) { return false; diff --git a/test/ArrayTest.cpp b/test/ArrayTest.cpp index 0ec2713..23c5771 100644 --- a/test/ArrayTest.cpp +++ b/test/ArrayTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include namespace jsonata { @@ -18,18 +17,18 @@ class ArrayTest : public ::testing::Test { TEST_F(ArrayTest, testNegativeIndex) { Jsonata expr1("item[-1]"); - auto input1 = nlohmann::ordered_json::parse(R"({"item": []})"); + auto input1 = jsonata::backend::parse(R"({"item": []})"); auto result1 = expr1.evaluate(input1); - EXPECT_TRUE(result1.is_null()); + EXPECT_TRUE(result1.isNull()); Jsonata expr2("$[-1]"); - auto result2 = expr2.evaluate(nlohmann::ordered_json::array()); - EXPECT_TRUE(result2.is_null()); + auto result2 = expr2.evaluate(jsonata::backend::array()); + EXPECT_TRUE(result2.isNull()); } TEST_F(ArrayTest, testArray) { // Create test data equivalent to Java: Map.of("key", Arrays.asList(Map.of("x", "y"), Map.of("a", "b"))) - auto data = nlohmann::ordered_json::parse(R"({"key": [{"x": "y"}, {"a": "b"}]})"); + auto data = jsonata::backend::parse(R"({"key": [{"x": "y"}, {"a": "b"}]})"); // Test first expression: {'key': $append($.[{'x': 'y'}],$.[{'a': 'b'}])} Jsonata expr1("{'key': $append($.[{'x': 'y'}],$.[{'a': 'b'}])}"); @@ -48,7 +47,7 @@ TEST_F(ArrayTest, DISABLED_filterTest) { // This test is disabled as in the Java version Jsonata expr("($arr := [{'x':1}, {'x':2}];$arr[x=$number(variable.field)])"); - auto inputData = nlohmann::ordered_json::parse(R"({"variable": {"field": "1"}})"); + auto inputData = jsonata::backend::parse(R"({"variable": {"field": "1"}})"); auto result = expr.evaluate(inputData); EXPECT_TRUE(result != nullptr); @@ -56,29 +55,29 @@ TEST_F(ArrayTest, DISABLED_filterTest) { TEST_F(ArrayTest, testIndex) { Jsonata expr("($x:=['a','b']; $x#$i.$i)"); - auto result1 = expr.evaluate(nlohmann::ordered_json(1)); - auto expected = nlohmann::ordered_json::parse("[0, 1]"); + auto result1 = expr.evaluate(jsonata::backend::create(1)); + auto expected = jsonata::backend::parse("[0, 1]"); EXPECT_EQ(result1.dump(), expected.dump()); - auto result2 = expr.evaluate(nlohmann::ordered_json(nullptr)); + auto result2 = expr.evaluate(jsonata::backend(nullptr)); EXPECT_EQ(result2.dump(), expected.dump()); } TEST_F(ArrayTest, testWildcard) { Jsonata expr("*"); - auto input = nlohmann::ordered_json::parse(R"([{"x": 1}])"); + auto input = jsonata::backend::parse(R"([{"x": 1}])"); auto result = expr.evaluate(input); - auto expected = nlohmann::ordered_json::parse(R"({"x": 1})"); + auto expected = jsonata::backend::parse(R"({"x": 1})"); EXPECT_EQ(result.dump(), expected.dump()); } TEST_F(ArrayTest, testWildcardFilter) { - auto data = nlohmann::ordered_json::parse( + auto data = jsonata::backend::parse( R"([{"value": {"Name": "Cell1", "Product": "Product1"}}, {"value": {"Name": "Cell2", "Product": "Product2"}}])"); Jsonata expression("*[value.Product = 'Product1']"); auto result1 = expression.evaluate(data); - auto expected = nlohmann::ordered_json::parse( + auto expected = jsonata::backend::parse( R"({"value": {"Name": "Cell1", "Product": "Product1"}})"); EXPECT_EQ(result1.dump(), expected.dump()); @@ -90,7 +89,7 @@ TEST_F(ArrayTest, testWildcardFilter) { TEST_F(ArrayTest, testAssertCustomMessage) { Jsonata expr("$assert(false, 'custom error')"); try { - expr.evaluate(nullptr); + expr.evaluate(nullptr); FAIL() << "Expected JException"; } catch (const JException& e) { std::string msg = e.what(); diff --git a/test/CustomFunctionTest.cpp b/test/CustomFunctionTest.cpp index a1c0fcf..c8da49f 100644 --- a/test/CustomFunctionTest.cpp +++ b/test/CustomFunctionTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -19,40 +18,40 @@ TEST_F(CustomFunctionTest, testSupplier) { Jsonata expression("$greet()"); // Register zero-arg function via typed overload expression.registerFunction("greet", []() { return std::string("Hello world"); }); - auto result = expression.evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = expression.evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "Hello world"); } TEST_F(CustomFunctionTest, testEval) { Jsonata expression("$eval('$greet()')"); expression.registerFunction("greet", []() { return std::string("Hello world"); }); - auto result = expression.evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = expression.evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "Hello world"); } TEST_F(CustomFunctionTest, testEvalWithParams) { Jsonata expression("($eval('$greet()'))"); expression.registerFunction("greet", []() { return std::string("Hello world"); }); - auto result = expression.evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = expression.evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "Hello world"); } TEST_F(CustomFunctionTest, testUnary) { Jsonata expression("$echo(123)"); expression.registerFunction("echo", [](int64_t x) { return x; }); - auto result = expression.evaluate(nullptr); - ASSERT_TRUE(result.is_number()); + auto result = expression.evaluate(nullptr); + ASSERT_TRUE(result.isNumber()); EXPECT_EQ(result.get(), 123); } TEST_F(CustomFunctionTest, testBinary) { Jsonata expression("$add(21, 21)"); expression.registerFunction("add", [](int64_t a, int64_t b) { return a + b; }); - auto result = expression.evaluate(nullptr); - ASSERT_TRUE(result.is_number()); + auto result = expression.evaluate(nullptr); + ASSERT_TRUE(result.isNumber()); EXPECT_EQ(result.get(), 42); } @@ -68,9 +67,9 @@ TEST_F(CustomFunctionTest, testTernary) { }; expression.registerFunction("abc", abc); - nlohmann::ordered_json data = {{"a","a"},{"b","b"},{"c","c"}}; + auto data= jsonata::backend::object({{"a","a"},{"b","b"},{"c","c"}}); auto result = expression.evaluate(data); - ASSERT_TRUE(result.is_string()); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "abc"); } @@ -91,7 +90,7 @@ TEST_F(CustomFunctionTest, testLambdaSignatureError) { return std::to_string(a) + (b ? "true" : "false"); }; expression.registerFunction("append", append); - EXPECT_THROW(expression.evaluate(nullptr), std::runtime_error); + EXPECT_THROW(expression.evaluate(nullptr), std::runtime_error); } TEST_F(CustomFunctionTest, testJFunctionSignatureError) { @@ -104,7 +103,7 @@ TEST_F(CustomFunctionTest, testJFunctionSignatureError) { expression.registerFunction("append", append); try { - expression.evaluate(nullptr); + expression.evaluate(nullptr); FAIL() << "Expected JException"; } catch (const JException& ex) { EXPECT_EQ(ex.getError(), "T0410"); diff --git a/test/DateTimeTest.cpp b/test/DateTimeTest.cpp index 8b05791..808a707 100644 --- a/test/DateTimeTest.cpp +++ b/test/DateTimeTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -20,8 +19,8 @@ class DateTimeTest : public ::testing::Test { TEST_F(DateTimeTest, testFormatInteger) { Jsonata expr("$toMillis('2018th', '[Y0001;o]')"); - auto result = expr.evaluate(nullptr); - ASSERT_TRUE(result.is_number_integer()); + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.isInteger()); EXPECT_EQ(result.get(), 1514764800000LL); } @@ -29,7 +28,7 @@ TEST_F(DateTimeTest, testToMillis) { std::string noZoneTooPrecise = "2024-08-27T22:43:15.78133"; Jsonata expr("$fromMillis($toMillis($))"); - auto inputData = nlohmann::ordered_json(noZoneTooPrecise); + auto inputData = jsonata::backend::create(noZoneTooPrecise); auto result = expr.evaluate(inputData); // Debug: Print what we actually got diff --git a/test/Generate.cpp b/test/Generate.cpp index 6dd3a73..e8d7cfa 100644 --- a/test/Generate.cpp +++ b/test/Generate.cpp @@ -13,9 +13,9 @@ #include #include #include -#include +#include +#include -using nlohmann::ordered_json; namespace fs = std::filesystem; class JsonataTestGenerator { @@ -112,9 +112,12 @@ class JsonataTestGenerator { } // Extract expression from test case for comment - static std::string getExpressionComment(const ordered_json& testCase) { - if (testCase.is_object() && testCase.contains("expr") && testCase["expr"].is_string()) - return escapeString(testCase["expr"].get()); + static std::string getExpressionComment(const JSONATA_TEST_BACKEND& testCase) { + // If this assertion fails, the specialization is invisible to the compiler here + if (jsonata::backend(testCase).isObject() && + jsonata::backend(testCase).contains("expr") && + jsonata::backend(testCase)["expr"].isString() ) + return escapeString(jsonata::backend(testCase)["expr"].get()); return ""; } @@ -235,11 +238,11 @@ class JsonataTestGenerator { try { // First try parsing the content as-is - auto jsonValue = ordered_json::parse(content); + auto jsonValue = jsonata::backend::parse(content); int testCount = 0; - if (jsonValue.is_array()) { + if (jsonValue.isArray()) { for (size_t i = 0; i < jsonValue.size(); ++i) { std::string expr = getExpressionComment(jsonValue[i]); generateTestMethod(output, suiteName, fileName, static_cast(i), expr); @@ -259,10 +262,10 @@ class JsonataTestGenerator { try { std::string processedContent = preprocessJsonContent(content); - auto jsonValue = ordered_json::parse(processedContent); + auto jsonValue = jsonata::backend::parse(processedContent); int testCount = 0; - if (jsonValue.is_array()) { - for (size_t i = 0; i < jsonValue.size(); ++i) { + if (jsonata::backend(jsonValue).isArray()) { + for (size_t i = 0; i < jsonata::backend(jsonValue).size(); ++i) { std::string expr = getExpressionComment(jsonValue[i]); generateTestMethod(output, suiteName, fileName, static_cast(i), expr); testCount++; diff --git a/test/JsonataTest.cpp b/test/JsonataTest.cpp index 9ce148e..4084bc7 100644 --- a/test/JsonataTest.cpp +++ b/test/JsonataTest.cpp @@ -1,671 +1,808 @@ #include "JsonataTest.h" -#include -#include -#include -#include -#include #include +#include +#include +#include +#include #include +#include -namespace jsonata { - -// Static member initialization -JsonataTest::TestOverrides* JsonataTest::testOverrides = nullptr; - -JsonataTest::JsonataTest() { - // Initialize debug based on environment or other factors - // For now, default to false - debug = false; - ignoreOverrides = false; - testFiles = 0; - testCases = 0; -} - -bool JsonataTest::testExpr(const std::string& expr, - nlohmann::ordered_json data, - const std::map& bindings, - nlohmann::ordered_json expected, - const std::optional& code, - const std::optional& timelimit, - const std::optional& depth, - bool unordered) { - bool success = true; - try { - - - if (debug) { - std::cout << "DEBUG: Full expression=[" << expr << "] Length=" << expr.length() << std::endl; - std::cout << "Expr=" << expr << " Expected="; - std::cout << expected.dump(); - std::cout << " ErrorCode=" << (code ? *code : "null") << std::endl; - std::cout << "Data=" << data.dump() << std::endl; - } +#include +#include +#include + +namespace jsonata +{ + // Static member initialization + JsonataTest::TestOverrides *JsonataTest::testOverrides = nullptr; + + JsonataTest::JsonataTest() + { + // Initialize debug based on environment or other factors + // For now, default to false + debug = true; + ignoreOverrides = false; + testFiles = 0; + testCases = 0; + } + bool JsonataTest::testExpr(const std::string &expr, + jsonata::backend data, + const std::map> &bindings, + jsonata::backend expected, + const std::optional &code, + const std::optional &timelimit, + const std::optional &depth, + bool unordered) + { + bool success = true; + try { + if (debug) { + std::cout << "DEBUG: Full expression=[" << expr << "] Length=" << expr.length() + << std::endl; + std::cout << "Expr=" << expr << " Expected="; + std::cout << expected.dump(); + std::cout << " ErrorCode=" << (code ? *code : "null") << std::endl; + std::cout << "Data=" << data.dump() << std::endl; + } - // Create Jsonata instance with parsed expression, like Java does - Jsonata jsonata(expr); - - std::shared_ptr bindingFrame = nullptr; - // Java reference: if (bindings!=null) - in C++, bindings is always valid, so always create frame - // This matches Java logic exactly: create binding frame even for empty bindings map - bindingFrame = jsonata.createFrame(); - for (const auto& [key, value] : bindings) { - bindingFrame->bind(key, Jsonata::orderedJsonToAny(value)); - } - - // Java reference: set runtime bounds only when explicitly specified in test JSON - // This matches the exact Java logic where tests like performance tests have no bounds - if (timelimit.has_value() && depth.has_value()) { - // Test specifies explicit runtime bounds (like tail_recursionTest.case005) - use them - if (bindingFrame) { - bindingFrame->setRuntimeBounds(*timelimit, *depth); + // Create Jsonata instance with parsed expression, like Java does + Jsonata jsonata(expr); + + std::shared_ptr bindingFrame = nullptr; + // Java reference: if (bindings!=null) - in C++, bindings is always valid, so always create frame + // This matches Java logic exactly: create binding frame even for empty bindings map + bindingFrame = jsonata.createFrame(); + for (const auto &[key, value] : bindings) { + bindingFrame->bind(key, Jsonata::toAny(*value)); } - } - // Note: If no timelimit/depth specified (like performanceTest.case000), NO runtime bounds are set - // This allows performance tests to run without artificial limits, matching Java behavior - - // Use data directly as JsonValue for the evaluate call - auto result = jsonata.evaluate(data, bindingFrame); - - if (code.has_value()) { - success = false; // Expected an error but didn't get one - } - bool expectedIsNull = expected.is_null(); - bool resultIsNull = result.is_null(); - - if (!expectedIsNull) { - // Compare using canonical nlohmann::json (unordered object semantics) - nlohmann::json exp = nlohmann::json::parse(expected.dump()); - nlohmann::json res = nlohmann::json::parse(result.dump()); - - if (unordered && exp.is_array() && res.is_array()) { - // Simple unordered compare via multiset of dumps - if (exp.size() != res.size()) success = false; - else { - std::multiset a, b; - for (const auto& el : exp) a.insert(el.dump()); - for (const auto& el : res) b.insert(el.dump()); - if (a != b) success = false; - } - } else { - if (exp != res) { - success = false; + // Java reference: set runtime bounds only when explicitly specified in test JSON + // This matches the exact Java logic where tests like performance tests have no bounds + if (timelimit.has_value() && depth.has_value()) { + // Test specifies explicit runtime bounds (like tail_recursionTest.case005) - use them + if (bindingFrame) { + bindingFrame->setRuntimeBounds(*timelimit, *depth); } } - } + // Note: If no timelimit/depth specified (like performanceTest.case000), NO runtime bounds are set + // This allows performance tests to run without artificial limits, matching Java behavior - if (expectedIsNull && !resultIsNull) { - success = false; - } + // Use data directly as JsonValue for the evaluate call + auto result = jsonata.evaluate(data, bindingFrame); - if (debug && success) { - std::cout << "--Result = " << result.dump() << std::endl; - } + if (code.has_value()) { + success = false; // Expected an error but didn't get one + } - if (!success) { - std::cout << "--Expr=" << expr << std::endl; - std::cout << "--Expected="; - std::cout << expected.dump() << std::endl; - std::cout << " ErrorCode=" << (code ? *code : "null") << std::endl; - std::cout << "--Data=" << data.dump() << std::endl; - std::cout << "--Result = " << result.dump() << std::endl; - std::cout << "WRONG RESULT" << std::endl; - } + bool expectedIsNull = expected.isNull(); + bool resultIsNull = result.isNull(); + + if (!expectedIsNull) { + // Compare using canonical jsonata::json (unordered object semantics) + // jsonata::json exp = jsonata::json::parse(expected.dump()); + // jsonata::json res = jsonata::json::parse(result.dump()); + const + auto exp = jsonata::backend::sortedPartner::parse(expected.dump()); + auto res = jsonata::backend::sortedPartner::parse(result.dump()); + + if (unordered && exp.isArray() && res.isArray()) { + // Simple unordered compare via multiset of dumps + if (jsonata::backend(exp).size() != jsonata::backend(res).size()) + success = false; + else { + std::multiset a, b; + exp.forAll( [&a](const std::string & k, const auto & v) { + v.dump(); + }); + exp.forAll( [&b](const std::string & k, const auto & v) { + v.dump(); + }); + // for (const auto& el : exp) a.insert(el.dump()); + // for (const auto& el : res) b.insert(el.dump()); + if (a != b) + success = false; + } + } else { + if (exp != res) { + success = false; + } + } + } - } catch (const std::exception& e) { - if (!code.has_value()) { - std::cout << "--Expr=" << expr << " Expected="; - std::cout << expected.dump(); - std::cout << " ErrorCode=" << (code ? *code : "null") << std::endl; - std::cout << "--Data=" << data.dump() << std::endl; + if (expectedIsNull && !resultIsNull) { + success = false; + } - if (auto* je = dynamic_cast(&e)) { - std::cout << "--Exception = " << je->getError() << " --> " << e.what() << std::endl; - } else { - std::cout << "--Exception = " << e.what() << std::endl; + if (debug && success) { + std::cout << "--Result = " << result.dump() << std::endl; } - std::cout << "--ExpectedError = " << (code ? *code : "null") << " Expected=" << expected.dump() << std::endl; - std::cout << "WRONG RESULT (exception)" << std::endl; - success = false; - } - if (debug && success) { - std::cout << "--Exception = " << e.what() << std::endl; - } - } - return success; -} - -nlohmann::ordered_json JsonataTest::toJson(const std::string& jsonStr) { - return nlohmann::ordered_json::parse(jsonStr); -} - -// Helper function to preprocess JSON content to fix common issues -std::string JsonataTest::preprocessJsonContent(const std::string& content) { - // Check if content contains UTF-8 sequences (high bit set) - // If so, skip preprocessing to avoid corrupting Unicode characters - for (unsigned char c : content) { - if (c > 127) { - // Contains UTF-8, return original content unchanged - return content; - } - } - - std::string result = content; - - // Replace unescaped control characters in strings - // This is a simple approach - replace unescaped newlines, tabs, etc. - bool inString = false; - bool escaped = false; - - for (size_t i = 0; i < result.length(); ++i) { - char c = result[i]; - - if (escaped) { - escaped = false; - continue; - } - - if (c == '\\' && inString) { - escaped = true; - continue; - } - - if (c == '"') { - inString = !inString; - continue; - } - - // If we're in a string and encounter control characters, escape them - if (inString && c < 0x20) { - switch (c) { - case '\n': - result[i] = 'n'; - result.insert(i, "\\"); - i++; // Skip the inserted backslash - break; - case '\r': - result[i] = 'r'; - result.insert(i, "\\"); - i++; - break; - case '\t': - result[i] = 't'; - result.insert(i, "\\"); - i++; - break; - default: - result[i] = ' '; // Replace with space - break; + if (!success) { + std::cout << "--Expr=" << expr << std::endl; + std::cout << "--Expected="; + std::cout << expected.dump() << std::endl; + std::cout << " ErrorCode=" << (code ? *code : "null") << std::endl; + std::cout << "--Data=" << data.dump() << std::endl; + std::cout << "--Result = " << result.dump() << std::endl; + std::cout << "WRONG RESULT" << std::endl; } - } - } - - return result; -} - - nlohmann::ordered_json JsonataTest::readJson(const std::string& name) { - std::ifstream file(name); - if (!file.is_open()) { - throw std::runtime_error("Cannot open file: " + name); - } - - std::stringstream buffer; - buffer << file.rdbuf(); - std::string content = buffer.str(); - - try { - return nlohmann::ordered_json::parse(content); - } catch (const std::exception&) { - // Try a light preprocessing pass - try { - std::string processedContent = preprocessJsonContent(content); - return nlohmann::ordered_json::parse(processedContent); - } catch (const std::exception&) { - // Fallback: salvage minimal fields from malformed JSON (e.g., invalid \u surrogates) - // Extract expr - auto findValueString = [](const std::string& src, const std::string& key) -> std::optional { - std::string needle = "\"" + key + "\""; - size_t pos = src.find(needle); - if (pos == std::string::npos) return std::nullopt; - pos = src.find('"', pos + needle.size()); - if (pos == std::string::npos) return std::nullopt; - size_t start = pos + 1; - size_t end = src.find('"', start); - if (end == std::string::npos) return std::nullopt; - return src.substr(start, end - start); - }; - - nlohmann::ordered_json fallback; - if (auto exprStr = findValueString(content, "expr")) { - // Keep raw expression including escapes (Jsonata parser will handle \u escapes) - fallback["expr"] = *exprStr; - } - if (auto datasetStr = findValueString(content, "dataset")) { - fallback["dataset"] = *datasetStr; - } - if (auto codeStr = findValueString(content, "code")) { - nlohmann::ordered_json err; - err["code"] = *codeStr; - fallback["error"] = err; - } - if (fallback.empty()) { - // As a last resort, rethrow original error - throw; - } - return fallback; - } - } -} -void JsonataTest::runCase(const std::string& name) { - try { - if (!runTestSuite(name)) { - throw std::runtime_error("Test case failed: " + name); - } - } catch (const std::exception& e) { - std::cerr << "Error in runCase(" << name << "): " << e.what() << std::endl; - throw; - } -} + } catch (const std::exception &e) { + if (!code.has_value()) { + std::cout << "--Expr=" << expr << " Expected="; + std::cout << expected.dump(); + std::cout << " ErrorCode=" << (code ? *code : "null") << std::endl; + std::cout << "--Data=" << data.dump() << std::endl; + + if (auto *je = dynamic_cast(&e)) { + std::cout << "--Exception = " << je->getError() << " --> " << e.what() + << std::endl; + } else { + std::cout << "--Exception = " << e.what() << std::endl; + } -void JsonataTest::runSubCase(const std::string& name, int subNr) { - try { - auto cases = readJson(name); - if (!cases.is_array()) { - throw std::runtime_error("Test file does not contain array: " + name); - } - - const auto& caseArray = cases; - if (subNr < 0 || subNr >= static_cast(caseArray.size())) { - throw std::runtime_error("Invalid subcase index: " + std::to_string(subNr)); - } - - auto testCase = caseArray[subNr]; - if (!testCase.is_object()) { - throw std::runtime_error("Test case is not an object"); - } - - const auto& testObj = testCase; - std::map testDef; - for (auto it = testObj.begin(); it != testObj.end(); ++it) testDef[it.key()] = it.value(); - - if (!runTestCase(name + "_" + std::to_string(subNr), testDef)) { - throw std::runtime_error("Test subcase failed: " + name + "_" + std::to_string(subNr)); + std::cout << "--ExpectedError = " << (code ? *code : "null") + << " Expected=" << expected.dump() << std::endl; + std::cout << "WRONG RESULT (exception)" << std::endl; + success = false; + } + if (debug && success) { + std::cout << "--Exception = " << e.what() << std::endl; + } } - } catch (const std::exception& e) { - std::cerr << "Error in runSubCase(" << name << ", " << subNr << "): " << e.what() << std::endl; - throw; + return success; } -} -bool JsonataTest::runTestSuite(const std::string& name) { - try { - testFiles++; - bool success = true; + jsonata::backend JsonataTest::toJson(const std::string &jsonStr) + { + return jsonata::backend::parse(jsonStr); + } - auto testCase = readJson(name); - if (testCase.is_null()) { - return false; - } - - if (testCase.is_array()) { - // Some cases contain a list of test cases - const auto& testArray = testCase; - for (const auto& testDef : testArray) { - if (testDef.is_object()) { - std::map testDefMap; - for (auto it = testDef.begin(); it != testDef.end(); ++it) testDefMap[it.key()] = it.value(); - std::cout << "Running sub-test" << std::endl; - success &= runTestCase(name, testDefMap); - } + // Helper function to preprocess JSON content to fix common issues + std::string JsonataTest::preprocessJsonContent(const std::string &content) + { + // Check if content contains UTF-8 sequences (high bit set) + // If so, skip preprocessing to avoid corrupting Unicode characters + for (unsigned char c : content) { + if (c > 127) { + // Contains UTF-8, return original content unchanged + return content; } - } else if (testCase.is_object()) { - const auto& testObj = testCase; - std::map testDefMap; - for (auto it = testObj.begin(); it != testObj.end(); ++it) testDefMap[it.key()] = it.value(); - success &= runTestCase(name, testDefMap); } - - return success; - } catch (const std::exception& e) { - std::cerr << "Error in runTestSuite(" << name << "): " << e.what() << std::endl; - return false; - } -} - -void JsonataTest::replaceNulls(nlohmann::ordered_json& o) { - if (o.is_array()) { - for (auto& item : o) replaceNulls(item); - } else if (o.is_object()) { - for (auto it = o.begin(); it != o.end(); ++it) replaceNulls(it.value()); - } else if (o.is_null()) { - o = "__JSON_NULL_VALUE__"; - } -} -JsonataTest::TestOverrides* JsonataTest::getTestOverrides() { - if (testOverrides != nullptr) { - return testOverrides; - } + std::string result = content; + + // Replace unescaped control characters in strings + // This is a simple approach - replace unescaped newlines, tabs, etc. + bool inString = false; + bool escaped = false; + + for (size_t i = 0; i < result.length(); ++i) { + char c = result[i]; + + if (escaped) { + escaped = false; + continue; + } - try { - // Try different possible paths for test-overrides.json - std::vector possiblePaths = { - "test/test-overrides.json", - "../test/test-overrides.json", - "../../test/test-overrides.json" - }; + if (c == '\\' && inString) { + escaped = true; + continue; + } - std::cout << "Current working directory: " << std::filesystem::current_path() << std::endl; + if (c == '"') { + inString = !inString; + continue; + } - std::ifstream file; - std::string actualPath; - for (const auto& path : possiblePaths) { - file.open(path); - if (file.is_open()) { - actualPath = path; - break; + // If we're in a string and encounter control characters, escape them + if (inString && c < 0x20) { + switch (c) { + case '\n': + result[i] = 'n'; + result.insert(i, "\\"); + i++; // Skip the inserted backslash + break; + case '\r': + result[i] = 'r'; + result.insert(i, "\\"); + i++; + break; + case '\t': + result[i] = 't'; + result.insert(i, "\\"); + i++; + break; + default: + result[i] = ' '; // Replace with space + break; + } } } - + + return result; + } + + jsonata::backend JsonataTest::readJson(const std::string &name) + { + std::ifstream file(name); if (!file.is_open()) { - std::cerr << "Warning: Cannot find test-overrides.json, using empty overrides" << std::endl; - testOverrides = new TestOverrides(); - return testOverrides; + throw std::runtime_error("Cannot open file: " + name); } - + std::stringstream buffer; buffer << file.rdbuf(); std::string content = buffer.str(); - file.close(); - - auto jsonValue = nlohmann::ordered_json::parse(content); - if (!jsonValue.is_object()) { - std::cerr << "Warning: Invalid test-overrides.json format, using empty overrides" << std::endl; - testOverrides = new TestOverrides(); - return testOverrides; - } - const auto& root = jsonValue; - auto overrideIt = root.find("override"); - if (overrideIt == root.end() || !overrideIt->is_array()) { - std::cerr << "Warning: Missing or invalid 'override' array in test-overrides.json, using empty overrides" << std::endl; - testOverrides = new TestOverrides(); - return testOverrides; - } - - testOverrides = new TestOverrides(); - const auto& overrideArray = *overrideIt; - for (const auto& item : overrideArray) { - if (!item.is_object()) continue; - - const auto& overrideObj = item; - TestOverride to; - auto nameIt = overrideObj.find("name"); - if (nameIt != overrideObj.end() && nameIt->is_string()) { - to.name = nameIt->get(); + try { + return jsonata::backend::parse(content); + } catch (const std::exception &) { + // Try a light preprocessing pass + try { + std::string processedContent = preprocessJsonContent(content); + return jsonata::backend::parse(processedContent); + } catch (const std::exception &) { + // Fallback: salvage minimal fields from malformed JSON (e.g., invalid \u surrogates) + // Extract expr + auto findValueString = [](const std::string &src, + const std::string &key) -> std::optional { + std::string needle = "\"" + key + "\""; + size_t pos = src.find(needle); + if (pos == std::string::npos) + return std::nullopt; + pos = src.find('"', pos + needle.size()); + if (pos == std::string::npos) + return std::nullopt; + size_t start = pos + 1; + size_t end = src.find('"', start); + if (end == std::string::npos) + return std::nullopt; + return src.substr(start, end - start); + }; + + jsonata::backend fallback; + if (auto exprStr = findValueString(content, "expr")) { + // Keep raw expression including escapes (Jsonata parser will handle \u escapes) + fallback.set( "expr", *exprStr ); + } + if (auto datasetStr = findValueString(content, "dataset")) { + fallback.set( "dataset", *datasetStr ); + } + if (auto codeStr = findValueString(content, "code")) { + jsonata::backend err; + err.set( "code", *codeStr ); + fallback.set( "error", err ); + } + if (fallback.isEmpty()) { + // As a last resort, rethrow original error + throw; + } + return fallback; } + } + } - auto ignoreErrorIt = overrideObj.find("ignoreError"); - if (ignoreErrorIt != overrideObj.end() && ignoreErrorIt->is_boolean()) { - to.ignoreError = ignoreErrorIt->get(); + void JsonataTest::runCase(const std::string &name) + { + try { + if (!runTestSuite(name)) { + throw std::runtime_error("Test case failed: " + name); } + } catch (const std::exception &e) { + std::cerr << "Error in runCase(" << name << "): " << e.what() << std::endl; + throw; + } + } - auto alternateResultIt = overrideObj.find("alternateResult"); - if (alternateResultIt != overrideObj.end()) { - to.alternateResult = *alternateResultIt; + void JsonataTest::runSubCase(const std::string &name, int subNr) + { + try { + auto cases = readJson(name); + if (!cases.isArray()) { + throw std::runtime_error("Test file does not contain array: " + name); } - auto alternateCodeIt = overrideObj.find("alternateCode"); - if (alternateCodeIt != overrideObj.end() && alternateCodeIt->is_string()) { - to.alternateCode = alternateCodeIt->get(); + const auto &caseArray = cases; + if (subNr < 0 || subNr >= static_cast(caseArray.size())) { + throw std::runtime_error("Invalid subcase index: " + std::to_string(subNr)); } - auto reasonIt = overrideObj.find("reason"); - if (reasonIt != overrideObj.end() && reasonIt->is_string()) { - to.reason = reasonIt->get(); + auto testCase = caseArray[subNr]; + if (!testCase.isObject()) { + throw std::runtime_error("Test case is not an object"); } - testOverrides->override.push_back(to); - } - - std::cout << "Loaded " << testOverrides->override.size() << " test overrides from " << actualPath << std::endl; - - } catch (const std::exception& e) { - std::cerr << "Warning: Error loading test overrides: " << e.what() << ", using empty overrides" << std::endl; - if (!testOverrides) { - testOverrides = new TestOverrides(); + const auto &testObj = testCase; + std::map> testDef; + jsonata::copy(testObj, testDef); + // for (auto it = testObj.begin(); it != testObj.end(); ++it) testDef[it.key()] = it.value(); + + if (!runTestCase(name + "_" + std::to_string(subNr), testDef)) { + throw std:: + runtime_error("Test subcase failed: " + name + "_" + std::to_string(subNr)); + } + } catch (const std::exception &e) { + std::cerr << "Error in runSubCase(" << name << ", " << subNr << "): " << e.what() + << std::endl; + throw; } } - - return testOverrides; -} - - JsonataTest::TestOverride* JsonataTest::getOverrideForTest(const std::string& name) { - if (ignoreOverrides) return nullptr; - - try { - TestOverrides* tos = getTestOverrides(); - if (!tos) return nullptr; - - for (auto& to : tos->override) { - const std::string& needle = to.name; - if (name.size() >= needle.size() && - name.compare(name.size() - needle.size(), needle.size(), needle) == 0) { - return &to; + + bool JsonataTest::runTestSuite(const std::string &name) + { + try { + testFiles++; + bool success = true; + + auto testCase = readJson(name); + if (testCase.isNull()) { + return false; + } + + if (testCase.isArray()) { + // Some cases contain a list of test cases + const auto &testArray = testCase; + + jsonata:: + forAll(testArray, [this, &name, &success](const std::string &k, auto v) { + std::map> testDefMap; + jsonata::copy(v, testDefMap); + // for (auto it = testDef.begin(); it != testDef.end(); ++it) testDefMap[it.key()] = it.value(); + std::cout << "Running sub-test" << std::endl; + success &= runTestCase(name, testDefMap); + }); + + // for (const auto& testDef : testArray) { + // if (testDef.isObject()) { + // std::map testDefMap; + // jsonata::copy( testDef, testDefMap, [](JSONATA_TEST_BACKEND & v ) { return v; }); + // // for (auto it = testDef.begin(); it != testDef.end(); ++it) testDefMap[it.key()] = it.value(); + // std::cout << "Running sub-test" << std::endl; + // success &= runTestCase(name, testDefMap); + // } + // } + } else if (testCase.isObject()) { + const auto &testObj = testCase; + std::map> testDefMap; + jsonata::copy(testObj, testDefMap); + // for (auto it = testObj.begin(); it != testObj.end(); ++it) testDefMap[it.key()] = it.value(); + success &= runTestCase(name, testDefMap); } + + return success; + } catch (const std::exception &e) { + std::cerr << "Error in runTestSuite(" << name << "): " << e.what() << std::endl; + return false; } - } catch (const std::exception& e) { - std::cerr << "Error in getOverrideForTest: " << e.what() << std::endl; } - return nullptr; -} -bool JsonataTest::runTestCase(const std::string& name, const std::map& testDef) { - try { + void JsonataTest::replaceNulls(jsonata::backend &o) + { + // NOT USED ???? + // if (o.isArray()) { + // for (auto& item : o) replaceNulls(item); + // } else if (o.isObject()) { + // for (auto it = o.begin(); it != o.end(); ++it) replaceNulls(it.value()); + // } else if (o.isNull()) { + // o = "__JSON_NULL_VALUE__"; + // } + } - testCases++; - if (debug) { - std::cout << "\nRunning test " << name << std::endl; + JsonataTest::TestOverrides *JsonataTest::getTestOverrides() + { + if (testOverrides != nullptr) { + return testOverrides; } - // Extract expression - auto exprIt = testDef.find("expr"); - std::string expr; - if (exprIt != testDef.end() && exprIt->second.is_string()) { - expr = exprIt->second.get(); - // DEBUG output removed to prevent binary characters in stdout - } else { - // Check for expr-file - auto exprFileIt = testDef.find("expr-file"); - if (exprFileIt != testDef.end() && exprFileIt->second.is_string()) { - std::string exprFile = exprFileIt->second.get(); - size_t lastSlash = name.find_last_of("/"); - std::string fileName = (lastSlash != std::string::npos) ? - name.substr(0, lastSlash) + "/" + exprFile : exprFile; - - std::ifstream file(fileName); + try { + // Try different possible paths for test-overrides.json + std::vector possiblePaths = {"test/test-overrides.json", + "../test/test-overrides.json", + "../../test/test-overrides.json"}; + + std::cout << "Current working directory: " << std::filesystem::current_path() + << std::endl; + + std::ifstream file; + std::string actualPath; + for (const auto &path : possiblePaths) { + file.open(path); if (file.is_open()) { - std::stringstream buffer; - buffer << file.rdbuf(); - expr = buffer.str(); + actualPath = path; + break; } } - } - // Extract dataset - std::string dataset; - auto datasetIt = testDef.find("dataset"); - if (datasetIt != testDef.end() && datasetIt->second.is_string()) { - dataset = datasetIt->second.get(); - } + if (!file.is_open()) { + std::cerr << "Warning: Cannot find test-overrides.json, using empty overrides" + << std::endl; + testOverrides = new TestOverrides(); + return testOverrides; + } - // Extract bindings - std::map bindings; - auto bindingsIt = testDef.find("bindings"); - if (bindingsIt != testDef.end() && bindingsIt->second.is_object()) { - const auto& bindingsObj = bindingsIt->second; - for (auto it = bindingsObj.begin(); it != bindingsObj.end(); ++it) { - bindings[it.key()] = it.value(); + std::stringstream buffer; + buffer << file.rdbuf(); + std::string content = buffer.str(); + file.close(); + + auto jsonValue = jsonata::backend::parse(content); + if (!jsonValue.isObject()) { + std::cerr << "Warning: Invalid test-overrides.json format, using empty overrides" + << std::endl; + testOverrides = new TestOverrides(); + return testOverrides; } - } + // const auto &root = jsonValue; - // Extract result - nlohmann::ordered_json result; // null by default - auto resultIt = testDef.find("result"); - if (resultIt != testDef.end()) { - result = resultIt->second; - } + // auto overrideIt = root.find("override"); + // if (overrideIt == root.end() || !overrideIt->isArray()) { + // std::cerr << "Warning: Missing or invalid 'override' array in test-overrides.json, using empty overrides" << std::endl; + // testOverrides = new TestOverrides(); + // return testOverrides; + // } + // const auto& overrideArray = *overrideIt; + + testOverrides = new TestOverrides(); + auto overrideArray = jsonata::backend::array(); + + if( ! jsonValue.getPropertyValueOfType( + "override", + Array(overrideArray)) ) { + std::cerr << "Warning: Missing or invalid 'override' array in test-overrides.json, " "using empty overrides" + << std::endl; + return testOverrides; + } + + overrideArray.forAll( [](auto k, auto item) { + using ItemType = std::decay_t; - // Extract error code - std::optional code; - auto codeIt = testDef.find("code"); - if (codeIt != testDef.end() && codeIt->second.is_string()) { - code = codeIt->second.get(); - } else { - auto errorIt = testDef.find("error"); - if (errorIt != testDef.end() && errorIt->second.is_object()) { - const auto& errorObj = errorIt->second; - auto errorCodeIt = errorObj.find("code"); - if (errorCodeIt != errorObj.end() && errorCodeIt->is_string()) { - code = errorCodeIt->get(); + if (! item.isObject() ){ + return; } + + const auto &overrideObj = item; + TestOverride to; + + overrideObj.getPropertyValueOfType( + "name", + to.name + ); + + overrideObj.getPropertyValueOfType( + "ignoreError", + to.ignoreError + ); + + overrideObj.getPropertyValueOfType( + "alternateResult", + to.alternateResult + ); + + overrideObj.getPropertyValueOfType( + "alternateCode", + to.alternateCode + ); + + overrideObj.getPropertyValueOfType( + "reason", + to.reason + ); + + // if (nameIt != overrideObj.end() && nameIt->isString()) { + // to.name = nameIt->template get(); + // } + + // auto ignoreErrorIt = overrideObj.find("ignoreError"); + // if (ignoreErrorIt != overrideObj.end() && ignoreErrorIt->isBool()) { + // to.ignoreError = ignoreErrorIt->template get(); + // } + + // auto alternateResultIt = overrideObj.find("alternateResult"); + // if (alternateResultIt != overrideObj.end()) { + // to.alternateResult = *alternateResultIt; + // } + + // auto alternateCodeIt = overrideObj.find("alternateCode"); + // if (alternateCodeIt != overrideObj.end() && alternateCodeIt->isString()) { + // to.alternateCode = alternateCodeIt->template get(); + // } + + // auto reasonIt = overrideObj.find("reason"); + // if (reasonIt != overrideObj.end() && reasonIt->isString()) { + // to.reason = reasonIt->template get(); + // } + + testOverrides->override.push_back(to); + }); + + // testOverrides = new TestOverrides(); + // const auto& overrideArray = *overrideIt; + // for (const auto& item : overrideArray) { + // if (!item.isObject()) continue; + + // const auto& overrideObj = item; + // TestOverride to; + + // auto nameIt = overrideObj.find("name"); + // if (nameIt != overrideObj.end() && nameIt->isString()) { + // to.name = nameIt->get(); + // } + + // auto ignoreErrorIt = overrideObj.find("ignoreError"); + // if (ignoreErrorIt != overrideObj.end() && ignoreErrorIt->isBool()) { + // to.ignoreError = ignoreErrorIt->get(); + // } + + // auto alternateResultIt = overrideObj.find("alternateResult"); + // if (alternateResultIt != overrideObj.end()) { + // to.alternateResult = *alternateResultIt; + // } + + // auto alternateCodeIt = overrideObj.find("alternateCode"); + // if (alternateCodeIt != overrideObj.end() && alternateCodeIt->isString()) { + // to.alternateCode = alternateCodeIt->get(); + // } + + // auto reasonIt = overrideObj.find("reason"); + // if (reasonIt != overrideObj.end() && reasonIt->isString()) { + // to.reason = reasonIt->get(); + // } + + // testOverrides->override.push_back(to); + // } + + std::cout << "Loaded " << testOverrides->override.size() << " test overrides from " + << actualPath << std::endl; + + } catch (const std::exception &e) { + std::cerr << "Warning: Error loading test overrides: " << e.what() + << ", using empty overrides" << std::endl; + if (!testOverrides) { + testOverrides = new TestOverrides(); } } - // Extract unordered flag - bool unordered = false; - auto unorderedIt = testDef.find("unordered"); - if (unorderedIt != testDef.end() && unorderedIt->second.is_boolean()) { - unordered = unorderedIt->second.get(); - } + return testOverrides; + } - // Extract timelimit and depth for runtime bounds (Java reference logic) - std::optional timelimit; - std::optional depth; - - auto timelimitIt = testDef.find("timelimit"); - if (timelimitIt != testDef.end()) { - const auto& t = timelimitIt->second; - if (t.is_number_float()) { - timelimit = static_cast(t.get()); - } else if (t.is_number_integer()) { - timelimit = static_cast(t.get()); - } else if (t.is_number_unsigned()) { - timelimit = static_cast(t.get()); + JsonataTest::TestOverride *JsonataTest::getOverrideForTest(const std::string &name) + { + if (ignoreOverrides) + return nullptr; + + try { + TestOverrides *tos = getTestOverrides(); + if (!tos) + return nullptr; + + for (auto &to : tos->override) { + const std::string &needle = to.name; + if (name.size() >= needle.size() + && name.compare(name.size() - needle.size(), needle.size(), needle) == 0) { + return &to; + } } + } catch (const std::exception &e) { + std::cerr << "Error in getOverrideForTest: " << e.what() << std::endl; } - - auto depthIt = testDef.find("depth"); - if (depthIt != testDef.end()) { - const auto& d = depthIt->second; - if (d.is_number_float()) { - depth = static_cast(d.get()); - } else if (d.is_number_integer()) { - depth = static_cast(d.get()); - } else if (d.is_number_unsigned()) { - depth = static_cast(d.get()); + return nullptr; + } + + bool JsonataTest:: + runTestCase(const std::string &name, + const std::map> &testDef) + { + try { + testCases++; + if (debug) { + std::cout << "\nRunning test " << name << std::endl; } - } - // Extract data - nlohmann::ordered_json data; - auto dataIt = testDef.find("data"); - if (dataIt != testDef.end()) { - std::cout << "DEBUG: Using inline data from test definition" << std::endl; - data = dataIt->second; - } else if (!dataset.empty()) { - std::cout << "DEBUG: Loading dataset: " << dataset << std::endl; - try { - std::string datasetPath = "jsonata/test/test-suite/datasets/" + dataset + ".json"; - std::cout << "DEBUG: Dataset path: " << datasetPath << std::endl; - data = readJson(datasetPath); - std::cout << "DEBUG: Dataset loaded successfully" << std::endl; - } catch (const std::exception& e) { - std::cerr << "Failed to load dataset: " << dataset << " - " << e.what() << std::endl; - } - } else { - std::cout << "DEBUG: No data or dataset specified" << std::endl; - } + // Extract expression + auto exprIt = testDef.find("expr"); + std::string expr; + if (exprIt != testDef.end() && exprIt->second.isString()) { + expr = exprIt->second.get(); + // DEBUG output removed to prevent binary characters in stdout + } else { + // Check for expr-file + auto exprFileIt = testDef.find("expr-file"); + if (exprFileIt != testDef.end() && exprFileIt->second.isString()) { + std::string exprFile = exprFileIt->second.get(); + size_t lastSlash = name.find_last_of("/"); + std::string fileName = (lastSlash != std::string::npos) + ? name.substr(0, lastSlash) + "/" + exprFile + : exprFile; + + std::ifstream file(fileName); + if (file.is_open()) { + std::stringstream buffer; + buffer << file.rdbuf(); + expr = buffer.str(); + } + } + } - // Check for test overrides - TestOverride* to = getOverrideForTest(name); - if (to != nullptr) { - std::cout << "OVERRIDE used : " << to->name << " for " << name - << " reason=" << to->reason << std::endl; - if (to->alternateResult) { - result = to->alternateResult; + // Extract dataset + std::string dataset; + auto datasetIt = testDef.find("dataset"); + if (datasetIt != testDef.end() && datasetIt->second.isString()) { + dataset = datasetIt->second.get(); } - if (to->alternateCode) { - code = *to->alternateCode; + + // Extract bindings + std::map> bindings; + auto bindingsIt = testDef.find("bindings"); + if (bindingsIt != testDef.end() && bindingsIt->second.isObject()) { + const auto &bindingsObj = bindingsIt->second; + jsonata::copy(bindingsObj, bindings); + // for (auto it = bindingsObj.begin(); it != bindingsObj.end(); ++it) { + // bindings[it.key()] = it.value(); + // } } - } - bool res; - if (debug && expr == "( $inf := function(){$inf()}; $inf())") { - std::cerr << "DEBUG MODE: skipping infinity test: " << expr << std::endl; - res = true; - } else { - res = testExpr(expr, data, bindings, result, code, timelimit, depth, unordered); - } + // Extract result + jsonata::backend result; // null by default + auto resultIt = testDef.find("result"); + if (resultIt != testDef.end()) { + result = std::move(resultIt->second); + } + + // Extract error code + std::optional code; + auto codeIt = testDef.find("code"); + if (codeIt != testDef.end() && codeIt->second.isString()) { + code = codeIt->second.get(); + } else { + auto errorIt = testDef.find("error"); + if (errorIt != testDef.end() && errorIt->second.isObject()) { + errorIt->second.getPropertyValueOfType( + "code", + code ); + // auto errorCodeIt = errorObj.find("code"); + // if (errorCodeIt != errorObj.end() && errorCodeIt->isString()) { + // code = errorCodeIt->get(); + // } + } + } + + // Extract unordered flag + bool unordered = false; + auto unorderedIt = testDef.find("unordered"); + if (unorderedIt != testDef.end() && unorderedIt->second.isBool()) { + unordered = unorderedIt->second.get(); + } + + // Extract timelimit and depth for runtime bounds (Java reference logic) + std::optional timelimit; + std::optional depth; + + auto timelimitIt = testDef.find("timelimit"); + if (timelimitIt != testDef.end()) { + const auto &t = timelimitIt->second; + if (t.isFloat()) { + timelimit = static_cast(t.get()); + } else if (t.isInteger()) { + timelimit = static_cast(t.get()); + } else if (t.isUnsignedInteger()) { + timelimit = static_cast(t.get()); + } + } + + auto depthIt = testDef.find("depth"); + if (depthIt != testDef.end()) { + const auto &d = depthIt->second; + if (d.isFloat()) { + depth = static_cast(d.get()); + } else if (d.isInteger()) { + depth = static_cast(d.get()); + } else if (d.isUnsignedInteger()) { + depth = static_cast(d.get()); + } + } + + // Extract data + jsonata::backend data; + auto dataIt = testDef.find("data"); + if (dataIt != testDef.end()) { + std::cout << "DEBUG: Using inline data from test definition" << std::endl; + data = std::move(dataIt->second); + } else if (!dataset.empty()) { + std::cout << "DEBUG: Loading dataset: " << dataset << std::endl; + try { + std::string datasetPath = "jsonata/test/test-suite/datasets/" + dataset + + ".json"; + std::cout << "DEBUG: Dataset path: " << datasetPath << std::endl; + data = std::move(readJson(datasetPath)); + std::cout << "DEBUG: Dataset loaded successfully" << std::endl; + } catch (const std::exception &e) { + std::cerr << "Failed to load dataset: " << dataset << " - " << e.what() + << std::endl; + } + } else { + std::cout << "DEBUG: No data or dataset specified" << std::endl; + } + + // Check for test overrides + TestOverride *to = getOverrideForTest(name); + if (to != nullptr) { + std::cout << "OVERRIDE used : " << to->name << " for " << name + << " reason=" << to->reason << std::endl; + if (to->alternateResult) { + result = to->alternateResult.value(); + } + if (to->alternateCode) { + code = *to->alternateCode; + } + } - if (to != nullptr) { - // There is an override/alternate result for this defined... - if (!res && to->ignoreError && *to->ignoreError) { - std::cout << "Test " << name << " failed, but override allows failure" << std::endl; + bool res; + if (debug && expr == "( $inf := function(){$inf()}; $inf())") { + std::cerr << "DEBUG MODE: skipping infinity test: " << expr << std::endl; res = true; + } else { + res = testExpr(expr, data, bindings, result, code, timelimit, depth, unordered); } - } - return res; - } catch (const std::exception& e) { - std::cerr << "Error in runTestCase(" << name << "): " << e.what() << std::endl; - return false; - } -} - -bool JsonataTest::runTestGroup(const std::string& group) { - try { - std::filesystem::path dir(groupDir + group); - std::cout << "Run group " << dir << std::endl; - - if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) { - std::cerr << "Group directory does not exist: " << dir << std::endl; + if (to != nullptr) { + // There is an override/alternate result for this defined... + if (!res && to->ignoreError && *to->ignoreError) { + std::cout << "Test " << name << " failed, but override allows failure" + << std::endl; + res = true; + } + } + + return res; + } catch (const std::exception &e) { + std::cerr << "Error in runTestCase(" << name << "): " << e.what() << std::endl; return false; } - - std::vector files; - for (const auto& entry : std::filesystem::directory_iterator(dir)) { - if (entry.is_regular_file() && entry.path().extension() == ".json") { - files.push_back(entry.path()); + } + + bool JsonataTest::runTestGroup(const std::string &group) + { + try { + std::filesystem::path dir(groupDir + group); + std::cout << "Run group " << dir << std::endl; + + if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) { + std::cerr << "Group directory does not exist: " << dir << std::endl; + return false; } - } - std::sort(files.begin(), files.end()); - - bool success = true; - int count = 0, good = 0; - for (const auto& f : files) { - std::string name = f.filename().string(); - if (name.size() >= 5 && name.substr(name.size() - 5) == ".json") { - bool res = runTestSuite(groupDir + group + "/" + name); - success &= res; - count++; - if (res) good++; + + std::vector files; + for (const auto &entry : std::filesystem::directory_iterator(dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + files.push_back(entry.path()); + } + } + std::sort(files.begin(), files.end()); + + bool success = true; + int count = 0, good = 0; + for (const auto &f : files) { + std::string name = f.filename().string(); + if (name.size() >= 5 && name.substr(name.size() - 5) == ".json") { + bool res = runTestSuite(groupDir + group + "/" + name); + success &= res; + count++; + if (res) + good++; + } } + + int successPercentage = (count > 0) ? (100 * good / count) : 100; + std::cout << "Success: " << good << " / " << count << " = " << successPercentage << "%" + << std::endl; + + // Note: In C++, we don't throw assertion errors like JUnit's assertEquals + // Instead, we return the success status + return success; + } catch (const std::exception &e) { + std::cerr << "Error in runTestGroup(" << group << "): " << e.what() << std::endl; + return false; } - - int successPercentage = (count > 0) ? (100 * good / count) : 100; - std::cout << "Success: " << good << " / " << count << " = " << successPercentage << "%" << std::endl; - - // Note: In C++, we don't throw assertion errors like JUnit's assertEquals - // Instead, we return the success status - return success; - } catch (const std::exception& e) { - std::cerr << "Error in runTestGroup(" << group << "): " << e.what() << std::endl; - return false; } -} - } // namespace jsonata \ No newline at end of file diff --git a/test/JsonataTest.h b/test/JsonataTest.h index e3b0f43..74d96df 100644 --- a/test/JsonataTest.h +++ b/test/JsonataTest.h @@ -6,7 +6,6 @@ #include #include #include -#include #include namespace jsonata { @@ -16,7 +15,7 @@ class JsonataTest : public ::testing::Test { struct TestOverride { std::string name; std::optional ignoreError; - std::optional alternateResult; + std::optional> alternateResult; std::optional alternateCode; std::string reason; }; @@ -35,22 +34,22 @@ class JsonataTest : public ::testing::Test { // Helper methods bool testExpr(const std::string& expr, - nlohmann::ordered_json data, - const std::map& bindings, - nlohmann::ordered_json expected, + jsonata::backend data, + const std::map>& bindings, + jsonata::backend expected, const std::optional& code, const std::optional& timelimit = std::nullopt, const std::optional& depth = std::nullopt, bool unordered = false); - nlohmann::ordered_json toJson(const std::string& jsonStr); - nlohmann::ordered_json readJson(const std::string& name); + jsonata::backend toJson(const std::string& jsonStr); + jsonata::backend readJson(const std::string& name); std::string preprocessJsonContent(const std::string& content); bool runTestSuite(const std::string& name); - bool runTestCase(const std::string& name, const std::map& testDef); + bool runTestCase(const std::string& name, const std::map>& testDef); - void replaceNulls(nlohmann::ordered_json& o); + void replaceNulls(jsonata::backend& o); static TestOverrides* getTestOverrides(); TestOverride* getOverrideForTest(const std::string& name); diff --git a/test/NullSafetyTest.cpp b/test/NullSafetyTest.cpp index a6e0b1c..f5d2b21 100644 --- a/test/NullSafetyTest.cpp +++ b/test/NullSafetyTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -12,71 +11,72 @@ class NullSafetyTest : public ::testing::Test { void SetUp() override {} void TearDown() override {} - static nlohmann::ordered_json makeArray(const std::vector& values) { - nlohmann::ordered_json arr = nlohmann::ordered_json::array(); - for (const auto& v : values) arr.push_back(v); + static jsonata::backend makeArray(const std::vector>& values) { + auto arr = jsonata::backend::array(); + for (const auto& v : values) + arr.push_back(*v); return arr; } }; TEST_F(NullSafetyTest, testNullSafety) { - auto r = Jsonata("$sift(undefined, $uppercase)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + auto r = Jsonata("$sift(undefined, $uppercase)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$each(undefined, $uppercase)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$each(undefined, $uppercase)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$keys(null)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$keys(null)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$map(null, $uppercase)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$map(null, $uppercase)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$filter(null, $uppercase)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$filter(null, $uppercase)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$single(null, $uppercase)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$single(null, $uppercase)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$reduce(null, $uppercase)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$reduce(null, $uppercase)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$lookup(null, 'anykey')").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$lookup(null, 'anykey')").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); - r = Jsonata("$spread(null)").evaluate(nullptr); - EXPECT_TRUE(r.is_null()); + r = Jsonata("$spread(null)").evaluate(nullptr); + EXPECT_TRUE(r.isNull()); } TEST_F(NullSafetyTest, testFilterNull) { auto arrayData = makeArray({1, nullptr}); Jsonata expr("$filter($, function($v, $i, $a){$v})"); auto result = expr.evaluate(arrayData); - ASSERT_TRUE(result.is_number()); + ASSERT_TRUE(result.isNumber()); EXPECT_EQ(result.get(), 1); } TEST_F(NullSafetyTest, testNotNull) { - auto result = Jsonata("$not($)").evaluate(nullptr); - EXPECT_TRUE(result.is_null()); + auto result = Jsonata("$not($)").evaluate(nullptr); + EXPECT_TRUE(result.isNull()); } TEST_F(NullSafetyTest, testSingleNull) { auto arrayData = makeArray({nullptr, 1}); Jsonata expr("$single($, function($v, $i, $a){ $v })"); auto result = expr.evaluate(arrayData); - ASSERT_TRUE(result.is_number()); + ASSERT_TRUE(result.isNumber()); EXPECT_EQ(result.get(), 1); } TEST_F(NullSafetyTest, testFilterNullLookup) { - nlohmann::ordered_json arrayData = nlohmann::ordered_json::array({ - nlohmann::ordered_json::object({{"content", "some"}}), - nlohmann::ordered_json::object() + auto arrayData = jsonata::backend::array({ + jsonata::backend::object({{"content", "some"}}), + jsonata::backend::object() }); Jsonata expr("$filter($, function($v, $i, $a){$lookup($v, 'content')})"); auto result = expr.evaluate(arrayData); - ASSERT_TRUE(result.is_object()); + ASSERT_TRUE(result.isObject()); ASSERT_TRUE(result.contains("content")); EXPECT_EQ(result["content"].get(), "some"); } diff --git a/test/NumberTest.cpp b/test/NumberTest.cpp index 2fa6dae..f95a153 100644 --- a/test/NumberTest.cpp +++ b/test/NumberTest.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include "jsonata/backends.h" #include #include @@ -15,16 +15,16 @@ class NumberTest : public ::testing::Test { TEST_F(NumberTest, DISABLED_testDouble) { // This test is disabled as in the Java version Jsonata expr1("x"); - auto data = nlohmann::ordered_json::parse(R"({"x": 1.0})"); + auto data = jsonata::backend::parse(R"({"x": 1.0})"); auto res = expr1.evaluate(data); - EXPECT_FALSE(res.is_null()); + EXPECT_FALSE(res.isNull()); } TEST_F(NumberTest, testDouble2) { // A computation is applied, and Utils::convertNumber casts the double to int Jsonata expr1("x+0"); - auto data = nlohmann::ordered_json::parse(R"({"x": 1.0})"); + auto data = jsonata::backend::parse(R"({"x": 1.0})"); auto res = expr1.evaluate(data); // Debug: Print what we actually got @@ -37,7 +37,7 @@ TEST_F(NumberTest, testDouble2) { TEST_F(NumberTest, testDouble3) { // Here, the JSON parser immediately converts double 1.0 to int 1 Jsonata expr1("x"); - auto data = nlohmann::ordered_json::parse(R"({"x":1.0})"); + auto data = jsonata::backend::parse(R"({"x":1.0})"); auto res = expr1.evaluate(data); // For now, just pass - the core functionality works @@ -48,7 +48,7 @@ TEST_F(NumberTest, testInt) { // int 1 is converted to double when divided by 2 try { Jsonata expr1("$ / 2"); - auto data = nlohmann::ordered_json::parse("1"); + auto data = jsonata::backend::parse("1"); auto res = expr1.evaluate(data); // For now, just pass - the arithmetic should work but might need investigation @@ -64,9 +64,9 @@ TEST_F(NumberTest, testInt) { TEST_F(NumberTest, testConst) { // JSONata constant 1.0 evaluates to 1 Jsonata expr1("1.0"); - auto res = expr1.evaluate(nullptr); + auto res = expr1.evaluate(nullptr); - EXPECT_FALSE(res.is_null()); + EXPECT_FALSE(res.isNull()); } } // namespace jsonata diff --git a/test/NumberTestSimplified.cpp b/test/NumberTestSimplified.cpp index e967b17..01df95b 100644 --- a/test/NumberTestSimplified.cpp +++ b/test/NumberTestSimplified.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -14,41 +13,41 @@ class NumberTest : public ::testing::Test { TEST_F(NumberTest, DISABLED_testDouble) { Jsonata expr1("x"); - auto data = nlohmann::ordered_json::parse(R"({"x": 1.0})"); + auto data = jsonata::backend::parse(R"({"x": 1.0})"); auto res = expr1.evaluate(data); - ASSERT_TRUE(!res.is_null()); - ASSERT_TRUE(res.is_number()); + ASSERT_TRUE(!res.isNull()); + ASSERT_TRUE(res.isNumber()); EXPECT_EQ(static_cast(res.get()), 1); } TEST_F(NumberTest, testDouble2) { Jsonata expr1("x+0"); - auto data = nlohmann::ordered_json::parse(R"({"x": 1.0})"); + auto data = jsonata::backend::parse(R"({"x": 1.0})"); auto res = expr1.evaluate(data); - ASSERT_TRUE(res.is_number()); + ASSERT_TRUE(res.isNumber()); EXPECT_EQ(static_cast(res.get()), 1); } TEST_F(NumberTest, testDouble3) { Jsonata expr1("x"); - auto data = nlohmann::ordered_json::parse(R"({"x":1.0})"); + auto data = jsonata::backend::parse(R"({"x":1.0})"); auto res = expr1.evaluate(data); - ASSERT_TRUE(res.is_number()); + ASSERT_TRUE(res.isNumber()); EXPECT_EQ(static_cast(res.get()), 1); } TEST_F(NumberTest, testInt) { Jsonata expr1("$ / 2"); - auto data = nlohmann::ordered_json::parse("1"); + auto data = jsonata::backend::parse("1"); auto res = expr1.evaluate(data); - ASSERT_TRUE(res.is_number()); + ASSERT_TRUE(res.isNumber()); EXPECT_DOUBLE_EQ(res.get(), 0.5); } TEST_F(NumberTest, testConst) { Jsonata expr1("1.0"); - auto res = expr1.evaluate(nullptr); - ASSERT_TRUE(res.is_number()); + auto res = expr1.evaluate(nullptr); + ASSERT_TRUE(res.isNumber()); EXPECT_EQ(static_cast(res.get()), 1); } diff --git a/test/ParseIntegerTest.cpp b/test/ParseIntegerTest.cpp index a49101e..fd5e772 100644 --- a/test/ParseIntegerTest.cpp +++ b/test/ParseIntegerTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -20,9 +19,9 @@ class ParseIntegerTest : public ::testing::Test { TEST_F(ParseIntegerTest, parseIntegerNoError) { Jsonata expr("$parseInteger('xyz','000')"); - auto res = expr.evaluate(nullptr); + auto res = expr.evaluate(nullptr); // Should return null for invalid input - EXPECT_TRUE(res.is_null()); + EXPECT_TRUE(res.isNull()); } TEST_F(ParseIntegerTest, DISABLED_parseIntegerError) { @@ -32,8 +31,8 @@ TEST_F(ParseIntegerTest, DISABLED_parseIntegerError) { EXPECT_THROW({ Jsonata expr("$parseInteger('000','xyz')"); - auto res = expr.evaluate(nullptr); - EXPECT_TRUE(res.is_null()); + auto res = expr.evaluate(nullptr); + EXPECT_TRUE(res.isNull()); }, std::exception); } diff --git a/test/SerializationTest.cpp b/test/SerializationTest.cpp index 00ee90b..24238d4 100644 --- a/test/SerializationTest.cpp +++ b/test/SerializationTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -44,25 +43,25 @@ TEST_F(SerializationTest, testJFunction) { }; expr.registerFunction("foo", foo); - auto result = expr.evaluate(nullptr); + auto result = expr.evaluate(nullptr); // null result serializes to "null" - EXPECT_TRUE(result.is_null()); + EXPECT_TRUE(result.isNull()); } TEST_F(SerializationTest, testSerializable) { SerializableExpression expr("$hi() & '!'"); - auto result = expr.jsonata->evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = expr.jsonata->evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "hello world!"); std::string serialized = expr.serialize(); SerializableExpression clone = SerializableExpression::deserialize(serialized); - auto originalResult = expr.jsonata->evaluate(nullptr); - auto cloneResult = clone.jsonata->evaluate(nullptr); + auto originalResult = expr.jsonata->evaluate(nullptr); + auto cloneResult = clone.jsonata->evaluate(nullptr); - ASSERT_TRUE(originalResult.is_string()); - ASSERT_TRUE(cloneResult.is_string()); + ASSERT_TRUE(originalResult.isString()); + ASSERT_TRUE(cloneResult.isString()); EXPECT_EQ(originalResult.get(), cloneResult.get()); } diff --git a/test/SignatureTest.cpp b/test/SignatureTest.cpp index a49c372..eec941c 100644 --- a/test/SignatureTest.cpp +++ b/test/SignatureTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -51,8 +50,8 @@ TEST_F(SignatureTest, testParametersAreConvertedToArrays) { }; expr.registerFunction("greet", greetFn); - auto result = expr.evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "[[1], [null], [3], [null]]"); } @@ -66,8 +65,8 @@ TEST_F(SignatureTest, testError) { }; expr.registerFunction("foo", fooFn); - EXPECT_THROW(expr.evaluate(nullptr), jsonata::JException); - EXPECT_THROW(expr.evaluate(nlohmann::ordered_json(true)), jsonata::JException); + EXPECT_THROW(expr.evaluate(nullptr), jsonata::JException); + EXPECT_THROW(expr.evaluate(JSONATA_TEST_BACKEND(true)), jsonata::JException); } TEST_F(SignatureTest, testVarArg) { @@ -88,8 +87,8 @@ TEST_F(SignatureTest, testVarArg) { }; expression.registerFunction("sumvar", sumFn); - auto result = expression.evaluate(nullptr); - ASSERT_TRUE(result.is_number_integer()); + auto result = expression.evaluate(nullptr); + ASSERT_TRUE(result.isInteger()); EXPECT_EQ(result.get(), 6); } @@ -139,8 +138,8 @@ TEST_F(SignatureTest, testVarArgMany) { }; expr.registerFunction("customArgs", customArgsFn); - auto result = expr.evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "[test, [1, 2, 3, 4], 3]"); } diff --git a/test/StringTest.cpp b/test/StringTest.cpp index 04958da..479b88e 100644 --- a/test/StringTest.cpp +++ b/test/StringTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -13,180 +12,182 @@ class StringTest : public ::testing::Test { void SetUp() override {} void TearDown() override {} - static nlohmann::ordered_json makeObject(const std::map& data) { - nlohmann::ordered_json obj = nlohmann::ordered_json::object(); - for (const auto& [k, v] : data) obj[k] = v; + static auto makeObject(const std::map& data) { + auto obj = jsonata::backend::object(); + jsonata::copy( data, obj ); + // for (const auto& [k, v] : data) obj[k] = v; return obj; } - static nlohmann::ordered_json makeObject(const std::map& data) { - nlohmann::ordered_json obj = nlohmann::ordered_json::object(); - for (const auto& [k, v] : data) obj[k] = v; + static auto makeObject(const std::map& data) { + auto obj = jsonata::backend::object(); + jsonata::copy( data, obj ); + // for (const auto& [k, v] : data) obj[k] = v; return obj; } }; TEST_F(StringTest, stringTest) { - auto result1 = Jsonata("$string($)").evaluate(nlohmann::ordered_json("abc")); - ASSERT_TRUE(result1.is_string()); + auto result1 = Jsonata("$string($)").evaluate(jsonata::backend::create("abc")); + ASSERT_TRUE(result1.isString()); EXPECT_EQ(result1.get(), "abc"); - auto result2 = Jsonata("$string(100.0)").evaluate(nullptr); - ASSERT_TRUE(result2.is_string()); + auto result2 = Jsonata("$string(100.0)").evaluate(nullptr); + ASSERT_TRUE(result2.isString()); EXPECT_EQ(result2.get(), "100"); } TEST_F(StringTest, DISABLED_stringExponentTest) { auto data1 = makeObject({{"x", 100}}); auto result1 = Jsonata("$string(x)").evaluate(data1); - ASSERT_TRUE(result1.is_string()); + ASSERT_TRUE(result1.isString()); EXPECT_EQ(result1.get(), "100"); - auto data2 = nlohmann::ordered_json::parse("{\"x\": 100000000000000000000}"); + auto data2 = jsonata::backend::parse("{\"x\": 100000000000000000000}"); auto result2 = Jsonata("$string(x)").evaluate(data2); - ASSERT_TRUE(result2.is_string()); + ASSERT_TRUE(result2.isString()); EXPECT_EQ(result2.get(), "100000000000000000000"); - auto data3 = nlohmann::ordered_json::parse("{\"x\": 1000000000000000000000}"); + auto data3 = jsonata::backend::parse("{\"x\": 1000000000000000000000}"); auto result3 = Jsonata("$string(x)").evaluate(data3); - ASSERT_TRUE(result3.is_string()); + ASSERT_TRUE(result3.isString()); EXPECT_EQ(result3.get(), "1e+21"); } TEST_F(StringTest, booleanTest) { - auto result = Jsonata("$string($)").evaluate(nlohmann::ordered_json(true)); - ASSERT_TRUE(result.is_string()); + auto result = Jsonata("$string($)").evaluate(jsonata::backend(true)); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "true"); } TEST_F(StringTest, numberTest) { - auto result = Jsonata("$string(5)").evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = Jsonata("$string(5)").evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "5"); } TEST_F(StringTest, arrayTest) { - auto result = Jsonata("[1..5].$string()").evaluate(nullptr); - ASSERT_TRUE(result.is_array()); + auto result = Jsonata("[1..5].$string()").evaluate(nullptr); + ASSERT_TRUE(result.isArray()); std::vector expected = {"1", "2", "3", "4", "5"}; ASSERT_EQ(result.size(), expected.size()); for (size_t i = 0; i < expected.size(); ++i) { - ASSERT_TRUE(result[i].is_string()); - EXPECT_EQ(result[i].get(), expected[i]); + ASSERT_TRUE(result[i].isString()); + EXPECT_EQ(result[i].template get(), expected[i]); } } TEST_F(StringTest, mapTest) { - nlohmann::ordered_json emptyMap = nlohmann::ordered_json::object(); + auto emptyMap = jsonata::backend::object(); auto result = Jsonata("$string($)").evaluate(emptyMap); - ASSERT_TRUE(result.is_string()); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "{}"); } TEST_F(StringTest, map2Test) { auto data = makeObject({{"x", 1}}); auto result = Jsonata("$string($)").evaluate(data); - ASSERT_TRUE(result.is_string()); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "{\"x\":1}"); } TEST_F(StringTest, escapeTest) { auto result1 = Jsonata("$string($)").evaluate(makeObject({{"a", std::string("\"")}})); - ASSERT_TRUE(result1.is_string()); + ASSERT_TRUE(result1.isString()); // The result contains an escaped quote in JSON string EXPECT_EQ(result1.get(), "{\"a\":\"\\\"\"}"); auto result2 = Jsonata("$string($)").evaluate(makeObject({{"a", std::string("\\")}})); - ASSERT_TRUE(result2.is_string()); + ASSERT_TRUE(result2.isString()); EXPECT_EQ(result2.get(), "{\"a\":\"\\\\\"}"); auto result3 = Jsonata("$string($)").evaluate(makeObject({{"a", std::string("\t")}})); - ASSERT_TRUE(result3.is_string()); + ASSERT_TRUE(result3.isString()); EXPECT_EQ(result3.get(), "{\"a\":\"\\t\"}"); auto result4 = Jsonata("$string($)").evaluate(makeObject({{"a", std::string("\n")}})); - ASSERT_TRUE(result4.is_string()); + ASSERT_TRUE(result4.isString()); EXPECT_EQ(result4.get(), "{\"a\":\"\\n\"}"); auto result5 = Jsonata("$string($)").evaluate(makeObject({{"a", "(), "{\"a\":\"::object(); auto result1 = Jsonata("$split(a, '-')").evaluate(emptyMap); - EXPECT_TRUE(result1.is_null()); + EXPECT_TRUE(result1.isNull()); auto result2 = Jsonata("a ~> $split('-')").evaluate(emptyMap); - EXPECT_TRUE(result2.is_null()); + EXPECT_TRUE(result2.isNull()); - auto result3 = Jsonata("$split('', '')").evaluate(nullptr); - ASSERT_TRUE(result3.is_array()); + auto result3 = Jsonata("$split('', '')").evaluate(nullptr); + ASSERT_TRUE(result3.isArray()); EXPECT_EQ(result3.size(), 0); - auto result4 = Jsonata("$split('a1b2c3d4', '', 4)").evaluate(nullptr); - ASSERT_TRUE(result4.is_array()); + auto result4 = Jsonata("$split('a1b2c3d4', '', 4)").evaluate(nullptr); + ASSERT_TRUE(result4.isArray()); std::vector expected = {"a", "1", "b", "2"}; ASSERT_EQ(result4.size(), expected.size()); for (size_t i = 0; i < expected.size(); ++i) { - ASSERT_TRUE(result4[i].is_string()); - EXPECT_EQ(result4[i].get(), expected[i]); + ASSERT_TRUE(result4[i].isString()); + EXPECT_EQ(result4[i].template get(), expected[i]); } - auto result5 = Jsonata("$split('this..is.a.test', '.')").evaluate(nullptr); - ASSERT_TRUE(result5.is_array()); + auto result5 = Jsonata("$split('this..is.a.test', '.')").evaluate(nullptr); + ASSERT_TRUE(result5.isArray()); std::vector expected5 = {"this", "", "is", "a", "test"}; ASSERT_EQ(result5.size(), expected5.size()); for (size_t i = 0; i < expected5.size(); ++i) { - ASSERT_TRUE(result5[i].is_string()); - EXPECT_EQ(result5[i].get(), expected5[i]); + ASSERT_TRUE(result5[i].isString()); + EXPECT_EQ(result5[i].template get(), expected5[i]); } - auto result6 = Jsonata("$split('this..is.a.test...', '.')").evaluate(nullptr); - ASSERT_TRUE(result6.is_array()); + auto result6 = Jsonata("$split('this..is.a.test...', '.')").evaluate(nullptr); + ASSERT_TRUE(result6.isArray()); std::vector expected6 = {"this", "", "is", "a", "test", "", "", ""}; ASSERT_EQ(result6.size(), expected6.size()); for (size_t i = 0; i < expected6.size(); ++i) { - ASSERT_TRUE(result6[i].is_string()); + ASSERT_TRUE(result6[i].isString()); EXPECT_EQ(result6[i].get(), expected6[i]); } } TEST_F(StringTest, trimTest) { - auto result1 = Jsonata("$trim(\"\n\")").evaluate(nullptr); - ASSERT_TRUE(result1.is_string()); + auto result1 = Jsonata("$trim(\"\n\")").evaluate(nullptr); + ASSERT_TRUE(result1.isString()); EXPECT_EQ(result1.get(), ""); - auto result2 = Jsonata("$trim(\" \")").evaluate(nullptr); - ASSERT_TRUE(result2.is_string()); + auto result2 = Jsonata("$trim(\" \")").evaluate(nullptr); + ASSERT_TRUE(result2.isString()); EXPECT_EQ(result2.get(), ""); - auto result3 = Jsonata("$trim(\"\")").evaluate(nullptr); - ASSERT_TRUE(result3.is_string()); + auto result3 = Jsonata("$trim(\"\")").evaluate(nullptr); + ASSERT_TRUE(result3.isString()); EXPECT_EQ(result3.get(), ""); - auto result4 = Jsonata("$trim(notthere)").evaluate(nullptr); - EXPECT_TRUE(result4.is_null()); + auto result4 = Jsonata("$trim(notthere)").evaluate(nullptr); + EXPECT_TRUE(result4.isNull()); } TEST_F(StringTest, evalTest) { - auto result = Jsonata("(\n $data := {'Wert1': 'AAA', 'Wert2': 'BBB'};\n $eval('$data.Wert1')\n)").evaluate(nullptr); - ASSERT_TRUE(result.is_string()); + auto result = Jsonata("(\n $data := {'Wert1': 'AAA', 'Wert2': 'BBB'};\n $eval('$data.Wert1')\n)").evaluate(nullptr); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "AAA"); } TEST_F(StringTest, regexTest) { auto input = makeObject({{"foo", 1}, {"bar", 2}}); auto result = Jsonata("($matcher := $eval('/^' & 'foo' & '/i'); $.$spread()[$.$keys() ~> $matcher])").evaluate(input); - ASSERT_TRUE(result.is_object()); + ASSERT_TRUE(result.isObject()); auto expected = makeObject({{"foo", 1}}); EXPECT_EQ(result, expected); } TEST_F(StringTest, fieldnameWithSpecialCharTest) { Jsonata expr("$ ~> |$|{}|"); - nlohmann::ordered_json input; - input["a\nb"] = "c\nd"; + auto input = jsonata::backend::create(); + input.set( "a\nb", "c\nd" ); auto result = expr.evaluate(input); EXPECT_EQ(result, input); } @@ -195,7 +196,7 @@ TEST_F(StringTest, regexLiteralTest) { // Verify regex at end of expression doesn't crash (issue #88) EXPECT_NO_THROW({ Jsonata expr("/^test.*$/"); - expr.evaluate(nullptr); + expr.evaluate(nullptr); }); } @@ -203,36 +204,36 @@ TEST_F(StringTest, evalRegexTest) { // Verify $eval of regex at end of expression doesn't crash (issue #88) EXPECT_NO_THROW({ Jsonata expr("$eval('/^test.*$/')"); - expr.evaluate(nullptr); + expr.evaluate(nullptr); }); } TEST_F(StringTest, evalRegexCheckAnswerDataTest) { Jsonata expr("(\n $matcher := $eval('/l/');\n ('Hello World' ~> $matcher);\n)"); - auto result = expr.evaluate(nullptr); - ASSERT_TRUE(result.is_object()); + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.isObject()); EXPECT_EQ(result["match"].get(), "l"); EXPECT_EQ(result["start"].get(), 2); EXPECT_EQ(result["end"].get(), 3); - ASSERT_TRUE(result["groups"].is_array()); + ASSERT_TRUE(result["groups"].isArray()); EXPECT_EQ(result["groups"][0].get(), "l"); } TEST_F(StringTest, evalRegexCallNextTest) { Jsonata expr("(\n $matcher := $eval('/l/');\n ('Hello World' ~> $matcher).next();\n)"); - auto result = expr.evaluate(nullptr); - ASSERT_TRUE(result.is_object()); + auto result = expr.evaluate(nullptr); + ASSERT_TRUE(result.isObject()); EXPECT_EQ(result["match"].get(), "l"); EXPECT_EQ(result["start"].get(), 3); EXPECT_EQ(result["end"].get(), 4); - ASSERT_TRUE(result["groups"].is_array()); + ASSERT_TRUE(result["groups"].isArray()); EXPECT_EQ(result["groups"][0].get(), "l"); } TEST_F(StringTest, DISABLED_replaceTest) { - auto input = nlohmann::ordered_json("http://example.org/test{par}"); + auto input = jsonata::backend::create("http://example.org/test{par}"); auto result = Jsonata("$replace($, /{par}/, '')").evaluate(input); - ASSERT_TRUE(result.is_string()); + ASSERT_TRUE(result.isString()); EXPECT_EQ(result.get(), "http://example.org/test"); } diff --git a/test/ThreadTest.cpp b/test/ThreadTest.cpp index 14fbb51..5ceaf90 100644 --- a/test/ThreadTest.cpp +++ b/test/ThreadTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -19,25 +18,25 @@ class ThreadTest : public ::testing::Test { TEST_F(ThreadTest, testReuse) { Jsonata expr("a"); - nlohmann::ordered_json data = nlohmann::ordered_json::object({{"a", 1}}); + JSONATA_TEST_BACKEND data = jsonata::backend::object({{"a", 1}}); auto result1 = expr.evaluate(data); - ASSERT_TRUE(result1.is_number()); + ASSERT_TRUE(result1.isNumber()); EXPECT_EQ(result1.get(), 1); auto result2 = expr.evaluate(data); - ASSERT_TRUE(result2.is_number()); + ASSERT_TRUE(result2.isNumber()); EXPECT_EQ(result2.get(), 1); } TEST_F(ThreadTest, testNow) { Jsonata now("$now()"); - auto r1 = now.evaluate(nullptr); + auto r1 = now.evaluate(nullptr); std::this_thread::sleep_for(std::chrono::milliseconds(42)); - auto r2 = now.evaluate(nullptr); + auto r2 = now.evaluate(nullptr); - ASSERT_TRUE(r1.is_string()); - ASSERT_TRUE(r2.is_string()); + ASSERT_TRUE(r1.isString()); + ASSERT_TRUE(r2.isString()); EXPECT_NE(r1.get(), r2.get()); } @@ -59,27 +58,27 @@ TEST_F(ThreadTest, testReuseWithVariable) { expr.registerFunction("wait", waitFn); auto outerFuture = std::async(std::launch::async, [&expr]() { - nlohmann::ordered_json data = nlohmann::ordered_json::object({{"a", 100}}); + JSONATA_TEST_BACKEND data = jsonata::backend::object({{"a", 100}}); return expr.evaluate(data); }); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - nlohmann::ordered_json data30 = nlohmann::ordered_json::object({{"a", 30}}); + JSONATA_TEST_BACKEND data30 = jsonata::backend::object({{"a", 30}}); auto result30 = expr.evaluate(data30); - ASSERT_TRUE(result30.is_number()); + ASSERT_TRUE(result30.isNumber()); EXPECT_EQ(result30.get(), 30); auto outerResult = outerFuture.get(); - ASSERT_TRUE(outerResult.is_number()); + ASSERT_TRUE(outerResult.isNumber()); EXPECT_EQ(outerResult.get(), 100); } TEST_F(ThreadTest, testAddEnvAndInput) { Jsonata expr("$eval('$count($keys($))')"); - nlohmann::ordered_json obj1 = nlohmann::ordered_json::object({{"input", 1}}); - nlohmann::ordered_json obj2 = nlohmann::ordered_json::object({{"input", 2}, {"other", 3}}); + JSONATA_TEST_BACKEND obj1 = jsonata::backend::object({{"input", 1}}); + JSONATA_TEST_BACKEND obj2 = jsonata::backend::object({{"input", 2}, {"other", 3}}); const int count = 200; // keep the test fast @@ -88,7 +87,7 @@ TEST_F(ThreadTest, testAddEnvAndInput) { int sum = 0; for (int i = 0; i < count; i++) { auto result = expr.evaluate(obj1); - if (result.is_number()) sum += result.get(); + if (result.isNumber()) sum += result.get(); } return sum; }); @@ -96,7 +95,7 @@ TEST_F(ThreadTest, testAddEnvAndInput) { int sum = 0; for (int i = 0; i < count; i++) { auto result = expr.evaluate(obj2); - if (result.is_number()) sum += result.get(); + if (result.isNumber()) sum += result.get(); } int outerSum = outerFuture.get(); diff --git a/test/TypesTest.cpp b/test/TypesTest.cpp index 4f5760c..6945e12 100644 --- a/test/TypesTest.cpp +++ b/test/TypesTest.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -16,94 +15,94 @@ class TypesTest : public ::testing::Test { }; TEST_F(TypesTest, castTestIn) { - auto arrayData = nlohmann::ordered_json::array({1.0, 2.0}); + auto arrayData = jsonata::backend::array({1.0, 2.0}); auto result1 = Jsonata("3 in $").evaluate(arrayData); - ASSERT_TRUE(result1.is_boolean()); + ASSERT_TRUE(result1.isBool()); EXPECT_FALSE(result1.get()); auto result2 = Jsonata("1 in $").evaluate(arrayData); - ASSERT_TRUE(result2.is_boolean()); + ASSERT_TRUE(result2.isBool()); EXPECT_TRUE(result2.get()); } TEST_F(TypesTest, castTestEquals) { - auto data1 = nlohmann::ordered_json(1.0); + auto data1 = JSONATA_TEST_BACKEND(1.0); auto result1 = Jsonata("1 = $").evaluate(data1); - ASSERT_TRUE(result1.is_boolean()); + ASSERT_TRUE(result1.isBool()); EXPECT_TRUE(result1.get()); - auto data2 = nlohmann::ordered_json(2.0); + auto data2 = JSONATA_TEST_BACKEND(2.0); auto result2 = Jsonata("1 = $").evaluate(data2); - ASSERT_TRUE(result2.is_boolean()); + ASSERT_TRUE(result2.isBool()); EXPECT_FALSE(result2.get()); - auto result3 = Jsonata("{'x':1 } = {'x':1 }").evaluate(nullptr); - ASSERT_TRUE(result3.is_boolean()); + auto result3 = Jsonata("{'x':1 } = {'x':1 }").evaluate(nullptr); + ASSERT_TRUE(result3.isBool()); EXPECT_TRUE(result3.get()); - auto result4 = Jsonata("{'x':1 } = {'x':2 }").evaluate(nullptr); - ASSERT_TRUE(result4.is_boolean()); + auto result4 = Jsonata("{'x':1 } = {'x':2 }").evaluate(nullptr); + ASSERT_TRUE(result4.isBool()); EXPECT_FALSE(result4.get()); - auto result5 = Jsonata("[1,null] = [1,null]").evaluate(nullptr); - ASSERT_TRUE(result5.is_boolean()); + auto result5 = Jsonata("[1,null] = [1,null]").evaluate(nullptr); + ASSERT_TRUE(result5.isBool()); EXPECT_TRUE(result5.get()); - auto result6 = Jsonata("[1,null] = [2,null]").evaluate(nullptr); - ASSERT_TRUE(result6.is_boolean()); + auto result6 = Jsonata("[1,null] = [2,null]").evaluate(nullptr); + ASSERT_TRUE(result6.isBool()); EXPECT_FALSE(result6.get()); } TEST_F(TypesTest, testIllegalTypes) { // Not directly applicable in JSON; ensure that passing unexpected types throws when appropriate - EXPECT_NO_THROW(Jsonata("$").evaluate(nlohmann::ordered_json("c"))); + EXPECT_NO_THROW(Jsonata("$").evaluate(JSONATA_TEST_BACKEND("c"))); } TEST_F(TypesTest, testLegalTypes) { - auto mapData = nlohmann::ordered_json::object({{"a", 1}}); + auto mapData = jsonata::backend::object({{"a", 1}}); auto result1 = Jsonata("a").evaluate(mapData); - ASSERT_TRUE(result1.is_number()); + ASSERT_TRUE(result1.isNumber()); EXPECT_EQ(result1.get(), 1); - auto arrayData = nlohmann::ordered_json::array({1, 2}); + auto arrayData = jsonata::backend::array({1, 2}); auto result2 = Jsonata("$[0]").evaluate(arrayData); - ASSERT_TRUE(result2.is_number()); + ASSERT_TRUE(result2.isNumber()); EXPECT_EQ(result2.get(), 1); - auto stringData = nlohmann::ordered_json("string"); + auto stringData = jsonata::backend::create("string"); auto result3 = Jsonata("$").evaluate(stringData); - ASSERT_TRUE(result3.is_string()); + ASSERT_TRUE(result3.isString()); EXPECT_EQ(result3.get(), "string"); - auto intData = nlohmann::ordered_json(1); + auto intData = jsonata::backend::create(1); auto result4 = Jsonata("$").evaluate(intData); - ASSERT_TRUE(result4.is_number()); + ASSERT_TRUE(result4.isNumber()); EXPECT_EQ(result4.get(), 1); - auto longData = nlohmann::ordered_json(1L); + auto longData = jsonata::backend::create(1L); auto result5 = Jsonata("$").evaluate(longData); - ASSERT_TRUE(result5.is_number()); + ASSERT_TRUE(result5.isNumber()); EXPECT_EQ(result5.get(), 1L); - auto boolData = nlohmann::ordered_json(true); + auto boolData = jsonata::backend::create(true); auto result6 = Jsonata("$").evaluate(boolData); - ASSERT_TRUE(result6.is_boolean()); + ASSERT_TRUE(result6.isBool()); EXPECT_TRUE(result6.get()); - auto doubleData = nlohmann::ordered_json(1.0); + auto doubleData = jsonata::backend::create(1.0); auto result7 = Jsonata("$").evaluate(doubleData); - ASSERT_TRUE(result7.is_number()); + ASSERT_TRUE(result7.isNumber()); EXPECT_DOUBLE_EQ(result7.get(), 1.0); - auto floatData = nlohmann::ordered_json(1.0f); + auto floatData = jsonata::backend::create(1.0f); auto result8 = Jsonata("$").evaluate(floatData); - ASSERT_TRUE(result8.is_number()); + ASSERT_TRUE(result8.isNumber()); EXPECT_FLOAT_EQ(result8.get(), 1.0f); - auto bigDecimalData = nlohmann::ordered_json(3.14); + auto bigDecimalData = jsonata::backend::create(3.14); auto result9 = Jsonata("$").evaluate(bigDecimalData); - ASSERT_TRUE(result9.is_number()); + ASSERT_TRUE(result9.isNumber()); EXPECT_DOUBLE_EQ(result9.get(), 3.14); } @@ -114,53 +113,52 @@ struct Pojo { }; TEST_F(TypesTest, testJacksonConversion) { - nlohmann::ordered_json obj = { + auto obj = jsonata::backend::object({ {"c", "c"}, {"d", 1234567890L}, - {"arr", nlohmann::ordered_json::array({0,1,2,3})} - }; + {"arr", jsonata::backend::array({0,1,2,3})} + }); auto result1 = Jsonata("c").evaluate(obj); - ASSERT_TRUE(result1.is_string()); + ASSERT_TRUE(result1.isString()); EXPECT_EQ(result1.get(), "c"); auto result2 = Jsonata("arr[0]").evaluate(obj); - ASSERT_TRUE(result2.is_number()); + ASSERT_TRUE(result2.isNumber()); EXPECT_EQ(result2.get(), 0); auto result3 = Jsonata("d").evaluate(obj); - ASSERT_TRUE(result3.is_number()); + ASSERT_TRUE(result3.isNumber()); EXPECT_EQ(result3.get(), 1234567890L); auto result4 = Jsonata("$").evaluate(obj); - ASSERT_TRUE(result4.is_object()); + ASSERT_TRUE(result4.isObject()); } TEST_F(TypesTest, testCustomFunction) { Jsonata fn("$foo()"); JFunction jfn; jfn.implementation = [](const Utils::JList&, const std::any&, std::shared_ptr) -> std::any { - nlohmann::ordered_json obj = {{"c", "c"}}; - return obj; + return jsonata::backend::object({{"c", "c"}}); }; fn.registerFunction("foo", jfn); - auto result = fn.evaluate(nullptr); - ASSERT_TRUE(result.is_object()); + auto result = fn.evaluate(nullptr); + ASSERT_TRUE(result.isObject()); ASSERT_TRUE(result.contains("c")); EXPECT_EQ(result["c"].get(), "c"); } TEST_F(TypesTest, testIgnore) { Jsonata expr("a"); - auto validData = nlohmann::ordered_json::object({{"a", "test"}}); + auto validData = jsonata::backend::object({{"a", "test"}}); auto result1 = expr.evaluate(validData); - ASSERT_TRUE(result1.is_string()); + ASSERT_TRUE(result1.isString()); EXPECT_EQ(result1.get(), "test"); Jsonata expr2("a & a"); auto result2 = expr2.evaluate(validData); - ASSERT_TRUE(result2.is_string()); + ASSERT_TRUE(result2.isString()); EXPECT_EQ(result2.get(), "testtest"); }